Skip to content

CRW-165 · 부모를 목표 없는 릴레이 이벤트 실행으로 전환 - #90

Merged
thisisjun786 merged 38 commits into
devfrom
codex/crw-165-goal-free-parent
Sep 21, 2026
Merged

thisisjun786 merged 38 commits into
devfrom
codex/crw-165-goal-free-parent

Conversation

@thisisjun786

@thisisjun786 thisisjun786 commented Sep 21, 2026

Copy link
Copy Markdown
Owner

A CRW project parent no longer holds a native host goal. It ends its turn when nothing but waiting remains, and a delivered relay event starts the next one. The issue child keeps its own goal and CXC Loop unchanged, the initiative supervisor was already goal-free, and crw-loop becomes the opt-in for a parent goal somebody explicitly asked for.

Implements the 2026-09-21 decision in CRW-165, superseding the 2026-09-20 role decision that made the parent goal the default. Both superseded steps are kept in the lifecycle document so a reader meeting a parent that still holds a goal recognises it as carried over rather than as a second valid answer.

What changes

One recorded field became two. goal-free-run previously meant both "no goal" and "no automatic resume is claimed or proven", which makes the new default self-contradictory if the name is simply promoted. run_mode now answers only whether a goal exists; observation_path answers what brings the parent back, taking event-driven-idle, active-observation or blocked. A 3x3 pairing matrix declares the legal combinations: run_mode: blocked pairs only with observation_path: blocked, and loop with event-driven-idle is legal as a compatibility mode reaching idle only after its bounded Stop budget releases, at a turn per block.

A parent may wait idle only on five recorded facts: a registered assignment routing to it, a delivery service whose ticks are actually progressing, its own reachability including goal status, delivery coverage for every disposition it waits on rather than completion alone, and a wake already observed on this operating scope. The fifth is written against a probe recipient because the obvious form is circular — a parent cannot observe its own first wake without first yielding idle on the strength of the observation it has not made.

New OPS-8.5 owns the wake path. Dedupe by relationship, generation, revision and event id. Order by generation and the declared revision lineage, never by arrival time or by comparing hashes; where no single current head exists the parent applies nothing and routes the ambiguity. The per-relationship marker advances only behind a durable apply. A send accepted, a turn that ran, an acknowledgement and an applied change stay four facts. The clause states that it binds the parent's behaviour and is not enforced by the installed relay.

The two busy paths

Reading delivery.py changed this contract. Where the recipient is observed active before the delivery is claimed, the attempt is deferred without being consumed and the row is re-observed indefinitely — a parent that simply stays active keeps its deliveries. Where the recipient observed idle, the delivery was claimed, and the transport then returned busy, an attempt is consumed. Only that second path approaches the cap, and exhausting it holds the delivery terminally: held rows are excluded from eligibility and no supported command clears the hold. OPS-8.5 names its detector, owner and recovery — a fresh execution generation — and states plainly that nothing detects it automatically where the parent is never woken at all.

Goal status is an input to deliverability

The installed relay treats paused, usageLimited and budgetLimited as blocking. An absent, active, blocked or completed goal is deliverable once the task reads idle. Verified against the installed runtime, byte-identical to this checkout for that module.

This reverses the transition that looked obvious. Pausing an existing parent goal to reach the new default makes the parent permanently undeliverable: events withheld, nothing waking it, no fault reported. The supported routes are honest completion at the scope boundary, a carried-over active goal until that boundary, or blocked where the host's own threshold is genuinely met. The pause guard is untouched — it protects a task a person deliberately stopped.

The vocabulary readers kept paraphrasing

run_mode and observation_path are closed sets, and a paraphrase is not a restorable record: the next session either matches the literal or silently re-adjudicates. Four readers of the earlier revisions wrote relay_only, goal_free, run_only and relay_only for run_mode, and relay for observation_path every time. Asking for the literals in prose, offering them as a list to copy, and requiring a strict consumer were each measured, and none of the three changed a reader's behaviour.

crw-run/scripts/start_policy.py makes the enumeration runnable rather than only declared. It parses the pairing table in start-policy.md instead of carrying its own copy, so a renamed literal cannot leave the checker and the contract disagreeing. vocabulary prints the legal pairings as lines to copy; check exits non-zero on a value outside the set, on one field recorded with two different values, or on an illegal pairing; selftest carries sixteen cases including all four recorded paraphrases. scripts/ci/contracts.py runs the selftest, so the pin is enforced by the gate.

Then the failure stopped reproducing, and not because of that script. Four readers of the current text wrote in-set values every time, two of them on deliberately damaged copies — one with every previously wrong value deleted from the page, one with the checker's own paragraph removed — and none of the four ran the check. So the contract no longer claims prose cannot carry these fields, and it does not claim the checker is why records now read correctly; it records that the failure stopped reproducing and that what changed is unidentified.

Files

Thirteen skill documents: crw-run/SKILL.md, crw-loop/SKILL.md, crw-check/SKILL.md, parent-goal.md, start-policy.md, operations.md, scenarios.md, integrations.md, task-packet.md, relay.md, midpoint-check.md, dispatch-verification.md, initiative-supervision.md, reevaluation.md.

Beyond the default itself, the later commits close surfaces the first pass missed: the restoration block now carries what a restarted goal-free parent restores, relay.md names the terminal hold beside the readers that surface it, and crw-check can tell an intentionally idle parent from a stalled one.

One new file, plugins/crw/skills/crw-run/scripts/start_policy.py, and one entry added to scripts/ci/contracts.py so the gate runs its selftest. No package, wiring or installer change: plugins/crw/wiring, scripts/install.py and scripts/runtime_install.py belong to CRW-178 in flight and are untouched, and CXC source and its installed cache are not patched.

A scope-isolation clause (OPS-3.6 and scenario S37) was added late and reverted in b2882b1, verified byte-identical to 0907c3c. It was outside this issue's scope, drew five review findings, and is better landed in its own pass; the measurement motivating it is preserved outside the repository.

Validation

check_operations_contract.py: 79 clause ids, 49 normative, 36 scenarios, 59 distinct citations, every normative clause cited by a fixture. validate.py: 10 skills, local link paths and Python syntax. plugin.py derives permitted roots from the manifest with no violation. git diff --check clean. start_policy.py selftest: 16 cases, the four recorded paraphrases and the malformed-record cases all rejected.

Two independent review rounds ran against the plan before any contract text was edited. Devin then reviewed every head, including a real defect where ordering by revision hash could have made a parent apply one of two forked candidates and advance its marker past the other, an overlap where one case row would have created children that another forbade, and a checker bug where loop_ normalised into the declared literal loop. All nineteen review threads are resolved, each by a fix rather than a waiver except one kept deliberately with the reason recorded at the line.

Not covered by this PR

The live evidence CRW-165 c2–c8 asks for is not in this diff and is not implied by it: the real event wake, the resilience matrix across paused, blocked and absent goal states, and the fixed idle-window token measurement. Of c4's eight cases, seven have offline tests that ran green in the packages jobs; the terminal-hold case has no end-to-end coverage and cannot be reached by waiting, so it must be induced.

One follow-up is returned as a proposal rather than built: a recipient-scoped pending reader and a supported release for a terminal hold, identified as the gap between this contract and fully automatic recovery.

A project parent no longer opens a native goal. It ends its turn when nothing
but waiting remains, and a delivered relay event starts the next one. The issue
child keeps its own goal and CXC Loop unchanged, and crw-loop becomes the
opt-in for a parent goal somebody actually asked for.

The recorded start policy splits one field into two, because one value was
carrying two answers: run_mode says whether a goal exists, observation_path says
what brings the parent back, and a pairing matrix declares which combinations
are legal. A parent may wait idle only on five recorded readiness facts, and the
fifth requires a wake already observed on this operating scope - by this parent
earlier or by a probe recipient - because the obvious version of that fact is
circular.

New OPS-8.5 owns the wake path: dedupe by relationship, generation, revision and
event id; apply in generation order with a superseded event recorded rather than
dropped; a per-relationship marker that advances only behind a durable apply; and
the separation of a send accepted from a turn that ran from an acknowledgement
from an applied change. It also records two busy paths that behave oppositely.
A recipient observed active before the claim is deferred without consuming an
attempt and is retried indefinitely. A recipient that observed idle, was claimed,
and then returned busy consumes one, and exhausting that budget holds the
delivery terminally, which is the concrete form of a queue nobody is woken for.

Goal status is documented as an input to deliverability, which changes the
transition for a parent that already holds a goal. Pausing looks like the
supported move and is the one that strands it: the relay treats a paused
recipient as undeliverable, so the events stop arriving and nothing reports a
fault. The supported routes are honest completion at the scope boundary, or a
carried-over active goal, or blocked where the host's threshold is genuinely
met. The pause guard itself is untouched.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-21T08:42:36.518443Z 2fac5af PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

devin-ai-integration[bot]

This comment was marked as resolved.

…dicting the new default

Review found four things. A revision hash carries no order, so "apply by
generation then revision" could make a parent pick one of two forked candidates
and advance its marker past the other. Ordering now comes from the declared
lineage and the current-head determination, and where that yields no single head
the parent applies nothing, advances nothing, and routes the ambiguity.

The no-goal precedence sentence and S26 still said no-goal leaves the run blocked
without separate authorization, which contradicted the default they sit beside;
the same request would run or stop depending on which clause was read first.
No-goal now bars only an explicitly requested Loop.

The midpoint table inferred that a Loop parent without a goal receives nothing.
An absent goal is deliverable, so a mode mismatch is reported as one and the
delivery path is read separately. A paused goal is the state that actually
receives nothing.

OPS-8.5 now says plainly that it binds the parent's behaviour and is not enforced
by the installed relay.
devin-ai-integration[bot]

This comment was marked as resolved.

…ne status

The previous fix narrowed undeliverability to a paused goal, but the relay blocks
three statuses and OPS-8.2 already classifies them. A checkpoint following the
narrower text would keep investigating the delivery path for a parent whose block
is already known. It now reads the classification from its owner rather than
restating it, so the two cannot drift apart again.
…ooks like waiting

Four surfaces the first pass did not reach. The restoration block was written
for a task bound to an issue, so a block travelling to a parent carried nothing
about how that parent waits; a goal-free parent that ended its turn cannot tell
on restart whether that was a decision or an interruption, and without the mode,
the path, the readiness and the outstanding event state it either resumes work
that already has an owner or waits for an event already handled.

relay.md described delivery phases but never the exhausted budget: a held
delivery is excluded from eligibility, no supported command clears it, and its
recovery is a fresh generation. It looks like waiting from every reader that
does not check holdReason.

A completion check had no way to tell an intentionally idle goal-free parent
from a stalled one, so it now reads the parent's idleness and its delivery path
as two things.

And two claims in integrations.md still called the parent goal the role default,
contradicting the same file's own goal-free statement a few hundred lines away.
An evidence run that points at its own state directory isolates its records and
nothing else: it still resolves the shared scope authority, so the daemon it
starts is the real service and every rule about not ending another parent's
service applies to it. Nothing said so, and the reasonable reading was the wrong
one.

Isolating the scope is a separate explicit act - override the scope directory and
pass the flag - and it still reaches the host through the same App Server socket,
so sharing a socket and sharing a scope are different facts. An isolated
authority is a scope of its own rather than an exception to having one, so
exactly one service runs in it and a second start is refused on that store's lock.

Measured here on 2026-09-21 rather than reasoned about: the isolated run
registered in its own scope root, ran its bounded ticks and released while the
real service kept its lock, and a concurrent second start was refused. Ticks
against an empty store are tick progress, not delivery progress, and the clause
says so.
devin-ai-integration[bot]

This comment was marked as resolved.

Review found the qualification keyed to the wrong thing. State selection and
scope selection are independent, so a run reading a scratch store without the
scope override is still the shared scope's service and cleaning it up early is
exactly the act the shared lifecycle forbids. Outright ownership now follows an
isolated authority, and reaching a configured bound is kept separate from an
owner-initiated early stop.

S37's first case could not happen as written. While the real service holds the
scope, a different-store start is refused as owned by another store: no ticks
run and there is nothing to clean up, so a run claiming it exercised delivery
there claimed something that did not occur. The scenario now splits on who owns
the scope rather than on which store was selected.
devin-ai-integration[bot]

This comment was marked as resolved.

OPS-3.6, the OPS-4.1 qualification and S37 were added at the end of this branch
and are not part of what CRW-165 asked for. Two review rounds produced five
findings against them and three are still open, all in the same area: what an
isolated authority actually owns, what a stopped registration does to an
evidence start, and which assignments a scratch daemon serves.

They are worth landing, but not here. This PR's result is the goal-free parent
transition, which was complete and clean at 0907c3c with every check green and
every thread resolved, and holding it open behind an unrelated clause serves
nobody. The measurement that motivated the clause is preserved outside the
repository and loses nothing by waiting.
OPS-8.5 asserted behaviour that had only been read out of the source. Four of
its claims have now been exercised against real recipient tasks on an isolated
store, so the clause carries a measured subset in the form OPS-8.3 already uses,
with the limits stated: it measures the relay and its store, not the parent-side
drain and marker rules, which stay unenforced. The cap hold was induced in a
synthetic store rather than reached naturally, and only paused was exercised
among the protected states.

Recovering a held delivery also turned out to have no reason of its own. The
available reasons describe an initial assignment and a needs-changes revision,
and a hold recovery is neither, so the record afterwards reads as a correction
round nobody performed. The clause now says so and asks for the real reason to
be kept beside it.
The Stop-continuation section named completing the goal or recording it blocked
as the honest exits without anyone having watched either happen. A trivial test
actor holding an active goal with nothing to do reached blocked within its first
turn, with no further external prompt, then held flat; two goal-free actors in
the same harness held flat with even their updatedAt unmoved.

The paragraph says only that. It does not attribute the transition to the Stop
behaviour, because the hook's firing was never read and only the outcome was
observed. The token figure covers a whole first turn rather than an isolated
idling cost, and one trivial actor leaves frequency, necessity, real
project-parent behaviour and any saving unmeasured.
devin-ai-integration[bot]

This comment was marked as resolved.

The measured paragraph qualified "the token figure" without ever stating one, so
the caveat pointed at evidence the reader could not find. The figure is 2127
tokens, and it is the actor's whole first turn including creating the goal and
answering its prompt.
This task's own completion receipt sat at withheld_pre_send with
role_policy_unconfigured while every readiness fact it had written passed. Fact 2
asks whether the service's ticks progress, and they did. Fact 3 asks whether the
recipient is reachable, and it was. Fact 5 pinned the scope's identity without
pinning the configuration the service decides under. A parent following that gate
would have yielded idle into silence.

Fact 2 now also reads the store's current failure records and treats an
applicable service-side pre-send refusal as a readiness failure, with the
explicit caveat that their absence proves nothing on its own, since a store with
nothing queued has no failures to show. Fact 5 adds role-policy identity, because
a wake observed under a different policy is evidence about a different
configuration.

OPS-8.5 names the observed refusal beside the cap hold, scoped to what was seen
rather than to every cause of withheld_pre_send, and records that its recovery
differs: it is retry-safe, so making the configuration readable and restarting
resumes the existing delivery with nothing lost.
Recording a role for a task quietly changes what delivery to that task requires:
an unbound recipient is delivered to without a role check, a bound one is checked
against the policy the delivering process can read, and a process that can read
none refuses before sending. So a binding written without that policy declared
where the service reads it stops delivery to that recipient while both conditions
hold. OPS-7.1 now says to write them together and treats a binding as incomplete
until the delivering process can resolve the policy it just made necessary.

OPS-8.5 gains the observation behind it. One store's two role bindings were
written at 07:43Z, every acknowledged delivery it holds settled at or before
06:35Z, and the one delivery afterwards to a bound recipient was refused. The
timeline corroborates rather than proves, since those earlier deliveries predate
the bindings and were never candidates for the check; the claim rests on the
branch itself.
…rased them

Two parent actors read this contract from an installed tree and behaved exactly
as it says: no goal, ended the turn, waited idle, with the right reason. Both
then reported field values that do not exist - goal_free and relay_only against
the declared goal-free-run and event-driven-idle - and the second had the
defining section in front of it and had been asked for exact values.

These two fields are not labels. They are the keys the pairing matrix is checked
against and the keys a later session restores from, so a value that reads
correctly but is spelled differently is a record the next reader cannot match,
and the restore it was written for silently becomes a fresh adjudication.

The declaration now says they are closed vocabularies recorded verbatim, names
the observed paraphrases as the failure it prevents, and the restoration item in
the task packet links to it so a consumer can check the spelling where it is
used.
I wrote that sentence and then tested it. A third parent read the corrected file
and wrote run_only/relay, after the first two wrote relay_only/relay and
goal_free/relay. All three observation_path answers were the same wrong value.
The third had this very paragraph in front of it.

So the requirement stays, because a producer should still write the literal, but
the claim that it prevents the failure is withdrawn. The text now says what was
measured: three readers, three paraphrases, one of them after reading the fix.
It also tells a consumer to match tolerantly rather than assume the record is
canonical, which is the half that actually protects restoration today.
devin-ai-integration[bot]

This comment was marked as resolved.

Review caught that tolerant matching is an undefined normalisation layer: with
no alias table, two readers can restore two different policies from the same
invalid record, and relay_only is exactly that case - it names an observation
path while sitting in run_mode, so the obvious reading disagrees with the field
it was written in.

A value outside the declared set is now an unreadable field, re-adjudicated the
same way a missing one is. That is deterministic and leaves a record saying so,
where guessing is neither. The writing-side problem is unchanged and still
unsolved; this only stops it becoming a silent divergence on the reading side.
devin-ai-integration[bot]

This comment was marked as resolved.

codex and others added 3 commits September 21, 2026 21:37
I shipped that rule and then tested it. A reader given a record containing
run_mode: relay_only said the values were settled and would be restored and used
as they stood - the exact behaviour the rule forbids.

That is three interventions measured on one problem: literals asked for in prose,
literals offered as a list to copy, and a consumer required to reject anything
outside the set. None changed a reader's behaviour. The rule stays, because it is
what a correct implementation does and because tolerant matching is worse, but
the file no longer implies that stating it makes it happen. These two fields need
validation in code where they are written and read, which a document cannot
supply.
The two closed-vocabulary fields had no machine reader, so a paraphrase could only
be caught by eye. start_policy.py parses the pairing table in start-policy.md
rather than keeping its own copy of the literals, and answers from it: vocabulary
prints the legal pairings as lines to copy, check exits non-zero on an out-of-set
value or an illegal pairing, selftest carries the four recorded paraphrases as
must-reject cases. scripts/ci/contracts.py runs the selftest, so the script and the
table cannot drift apart unnoticed.

Four fresh readers of the current text then wrote in-set values every time, two of
them on copies with the wrong-value examples or this checker's own paragraph
deleted, and none of them ran the check. So the contract no longer claims prose
cannot carry these fields, and it does not claim the script is why records read
correctly; it records that the failure stopped reproducing and that what changed is
unidentified.

Evidence: /state/crw/crw-165/artifacts/c2-readback-nonreproduction-20260921.md
The consumer rule said an unreadable value is re-adjudicated under the trigger list,
but the list enumerated only changes in the world, so a reader following the
restoration section found nothing changed and restored the unreadable value as it
stood. Two consumers of one record could resume the same parent under two different
policies, which is what the record exists to prevent. The missing case is now stated
where a consumer looks for it, and it is checked even when every valid_while
identity still holds.

Reported by Devin on PR 90.
devin-ai-integration[bot]

This comment was marked as resolved.

Case 7c covered every unsupported explicit Loop, including the narrower 7a where
the user declined goal-free Run, and the table declares no first-match order, so
two readers could answer one input with zero children or with D. 7c now names the
not-declined case it was always meant to cover.

crw-run promised no automatic future wake-up without qualifying it, which
contradicted the event-driven-idle path the same file now prescribes. The sentence
is qualified by the recorded observation_path.

The checker kept the first occurrence of a repeated field, so a stale pair above a
current one passed as the first legal pair while another consumer read the later
value. It now keeps every occurrence and calls two different values unreadable;
the same value stated twice is still readable and says so. It also stripped
decoration and trailing punctuation in one pass, so a markdown value such as
`loop`, kept a backtick and failed; undecorating now repeats until stable.

The selftest's second copy of the vocabulary stays, with the reason written down:
the table decides what check accepts, and that line is what makes a rename
deliberate instead of silent.

All four reported by Devin on PR 90.
devin-ai-integration[bot]

This comment was marked as resolved.

str.strip over a set of marker characters removes them independently from both
ends, so loop_ became loop and observation_path_ became the field name. That is
the checker performing exactly the approximation the contract forbids a consumer
to make, and it turned a malformed record into a legal pair.

Only a wrapper that opens and closes with the same character comes off now, so
backticked, quoted and bold values still read while an unmatched or mixed marker
stays in the text and fails membership. Bullets are removed by a marker followed
by whitespace rather than by stripping asterisks, which keeps **run_mode** intact.

Reported by Devin on PR 90. Selftest covers the unmatched marker, the mismatched
pair, the decorated field name and the bold value that must still pass.
A wake observed while the parent was loaded does not establish the route a long
wait actually uses. An idle recipient is resumed with its own settings, while one
the host reports notLoaded is resumed only when its pair derives from the role's
declared pair, so the two take different transmission paths and only one of them
was ever proven. A parent that waits long enough is evicted, which makes the
unloaded route the one event-driven idle depends on.

Both routes are now observed. A parent idle since 19:05 read notLoaded after three
hours, and a queued event delivered by the daemon created the turn whose id the
attempt record names, with recipientStatusBefore notLoaded. Eviction is not a
deliberate process restart and the parent-side drain is still not exercised; that
scope is recorded with the evidence.

Evidence: /state/crw/crw-165/artifacts/c4-notloaded-wake-20260921.md
devin-ai-integration[bot]

This comment was marked as resolved.

…state

Adding load state to fact 5's match list was the wrong rule. Readiness is
adjudicated while the parent is still loaded, so matching the state observed at
that moment accepts a loaded wake as proof — and the wait itself is what
invalidates it, because a wait long enough to be worth making ends in eviction and
the next event takes the notLoaded route instead.

Fact 5 now asks for the load state the wait ends up in. Where eviction is possible,
which is every ordinary wait, event-driven-idle needs a notLoaded wake on the same
scope; a loaded wake alone leaves the fact unmeasured and the path stays
active-observation. Both wakes are now observed, so the stricter rule is
satisfiable rather than theoretical.

Reported by Devin on PR 90.
devin-ai-integration[bot]

This comment was marked as resolved.

The probe exists so a parent does not have to gamble its assignment on a wake it
has not seen, but an unqualified probe proves the probe's route rather than the
parent's. The relay derives an unloaded resume's pair from the role's declared
pair and refuses a record-based or exception-authorized one, so an ordinary
parent-role probe waking from notLoaded says nothing about a parent running under
a named exception; that parent is refused exactly where the probe succeeded.

A probe now stands in only where it matches the role and the pair provenance.
Where nothing can match, because an exception-authorized pair has no ordinary
equivalent to probe with, the parent has no non-circular evidence for its own
unloaded route: fact 5 is recorded unmeasured with the provenance that made it so
and the path stays active-observation.

Reported by Devin on PR 90.
attempt() resolved the relationship with registry.get() and never looked at its
status, so for an assignment somebody had paused, cancelled or archived it read
the host three times, wrote a recipient_lifecycle row calling that recipient
deliverable, and then returned a bare None once _claim refused. Nothing was ever
sent - the scheduler excludes those rows and the claim repeats the predicate
atomically - but nothing recorded why, and None is indistinguishable from a
delivery whose time has not come.

The status is now checked before any host read, and the refusal is journalled
with the status it refused on. It is deliberately not a hold_reason, because
resume() lifts all three, and deliberately not a failed_operations row, because
somebody stopping their own work is not a service fault and a parent reads this
scope's failures to decide whether it may wait idle. The UPDATE re-checks the
observed status and superseded_by in its own transaction, so a resume landing in
the gap cannot be overwritten by a stale reading.

Superseded relationships are excluded on purpose. They are deactivated too, but
permanently, and closing that terminal means settling how every operator-facing
reader renders it.

A tick that withholds this way now counts a deferral rather than a skip, because
quiet folds deferred and not skipped, and a tick reporting itself quiet after
persisting a withhold is the evidence a parent reads before agreeing to wait.
OPS-8.2 named a paused, cancelled or archived TASK as one guarded class, which
promoted an assignment status into a recipient state the host does not report,
and the measured note claimed cancelled and archived "rest on the same code
path" as paused. They do not: archived is its own earlier lifecycle branch, and
a cancelled recipient task has no branch at all.

The clauses now separate the two levels and say how each refusal is recorded.
On the service's own path a deactivated assignment is enforced by exclusion and
nothing is written, so a queued delivery under a cancelled assignment is not a
stalled one; the assignment-level report is what says abandoned or paused. A
direct attempt, and the narrow race where an assignment is deactivated after
selection, refuse before any host read and journal the reason.
devin-ai-integration[bot]

This comment was marked as resolved.

The recorded git trees for codex-session-relay still described the tree before
the delivery change, so verify-definition passed locally only because the fix
was sitting uncommitted beside it and failed on the pushed head. Recorded from
HEAD instead.

Two sentences also claimed more than the code does. A withheld delivery IS
recorded as a failure when the recipient's own state caused it - _withhold
writes a lifecycle_read record - and only an assignment-status withhold skips
that, which is the distinction a parent reads failure records for. And the
journal kind is delivery_withheld_inactive carrying the relationship and its
status; relationship_not_active is in the returned record, not the journal.
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

The relationship check sat after the next_eligible_at guard, so a delivery that
was eligible by state but waiting out a retry - a busy-deferred one, say - went
back to returning a bare None once its assignment was stopped. An operator
naming that event during the wait learned nothing until the timer expired, which
is the same silence this change set out to remove.

The check now runs before the timer. Recording the status never brings an
existing retry time forward: whatever set it had its own reason, and a
deactivation is not a reason to retry sooner.
The trees are git object ids, so they only settle once the change they describe
is committed. Recorded from the commit above rather than from a working tree
that still had it uncommitted.
devin-ai-integration[bot]

This comment was marked as resolved.

…e-parent

# Conflicts:
#	scripts/crw_runtime/components.json
Comparing against a next_eligible_at read before the transaction let two
overlapping refusals race: the second one could overwrite an extension the first
had already committed, and a resumed delivery would then retry before the newest
backoff expired. The statement now takes MAX of the recheck floor and the row's
own current value, so whichever deadline is later survives whatever order the
callers arrive in.
devin-ai-integration[bot]

This comment was marked as resolved.

The re-adjudication clause requires the superseded value to be kept beside what
replaced it, and the checker collected every occurrence of a field and rejected
any field holding two different values. So a record that obeyed the contract was
reported as unreadable, which is precisely the drift this checker exists to stop.

The second entry is now spelled run_mode (superseded), declared in the contract
and recognised by the reader rather than skipped by accident. The ambiguity guard
is untouched: an unqualified duplicate is still two current values and still
fails, and a known field under any other qualifier now fails too, so an invented
word cannot keep a second current value out of the reader's sight.
devin-ai-integration[bot]

This comment was marked as resolved.

Lowercasing and trimming the parenthesised note accepted run_mode (SUPERSEDED)
and spacing variants, so this checker would pass a record that a consumer
enforcing the declared spelling rejects. Two readers, two policies, from one
record - the thing this script exists to prevent. The comparison is now against
the whole canonical string, and every other qualifier goes down the existing
unreadable branch.
@thisisjun786
thisisjun786 merged commit a427b42 into dev Sep 21, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants