Skip to content

fix(coding-agent): assert message-pipeline callback payloads instead of arity - #3641

Merged
Yeachan-Heo merged 1 commit into
Yeachan-Heo:devfrom
twoimo:fix/message-pipeline-emit-arity-assertions
Jul 31, 2026
Merged

fix(coding-agent): assert message-pipeline callback payloads instead of arity#3641
Yeachan-Heo merged 1 commit into
Yeachan-Heo:devfrom
twoimo:fix/message-pipeline-emit-arity-assertions

Conversation

@twoimo

@twoimo twoimo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

packages/coding-agent/test/agent-session-message-pipeline.test.ts has three assertions that pin the argument count of session callbacks with toHaveBeenCalledWith. The per-attempt scope facility (#3592 / #3608) threads an additional callback-scope argument through onPayload, onSseEvent, and extension emit, so those three assertions now fail on current dev even though every observable behaviour is unchanged.

This replaces the arity pins with assertions on the payload each hook actually receives.

Reproduction on current dev

At 8937871b1 (dev tip), with freshly built natives:

bun test packages/coding-agent/test/agent-session-message-pipeline.test.ts
 21 pass
 3 fail

The three failures, each an added trailing argument:

Test Line Diff
composes session payload hooks into direct side-request options 106 Received + 1 — extra undefined
records raw SSE diagnostics into the session buffer before request hooks 126 Received + 1 — extra undefined
forwards stop reasons and reasoning summaries to extension handlers 228 received (event, [Function: belongsToCurrentTurn], undefined)

The product side is intentional — prepareSimpleStreamOptions composes (payload, model, callbackScope) at agent-session.ts:7318, and onSseEvent/onResponse use the same 3-argument shape.

Change

  • composes session payload hooks — assert the session hook receives the original payload and the request hook receives the session-transformed payload. This is strictly stronger than the old assertion: it now pins the session→request threading order, which the arity check never covered.
  • records raw SSE diagnostics — assert the forwarded SSE event object.
  • forwards stop reasons and reasoning summaries — locate the reasoning_summary_end call and assert both its event payload and that its continueWhile argument is a function. The previous expect.any(Function) guarantee is preserved, not dropped.

Spy parameter types are widened so no cast is required. Assertions are arity-tolerant, so a future scope-argument change will not re-break them.

Verification

Verified in a clean worktree at dev tip 8937871b1:

Check Result
agent-session-message-pipeline.test.ts 24 pass / 0 fail (was 21 pass / 3 fail)
expect() calls 84 → 91
bun run check:types (packages/coding-agent) exit 0
biome check . exit 0
Files changed 1 (test only)

Fail-on-revert proof

Reverting only this change at the same base restores 21 pass / 3 fail; restoring it returns 24 pass / 0 fail.

Product-coupling proofs

To confirm the new assertions are coupled to product behaviour rather than accommodating the fixture, two independent product breaks were introduced with the test fix in place:

  1. Pass the raw payload instead of sessionResolvedPayload to the request hook (agent-session.ts:7320) → caught, 23 pass / 1 fail.
  2. Replace the session SSE hook assignment with a no-op (the branch this test exercises) → caught, 23 pass / 1 fail.

Both restore to 24 pass / 0 fail. So these assertions detect regressions the previous arity pins could not.

Overlap disclosure

#3638 ("AttemptScope post-merge compatibility repairs") repairs the same family of arity artifacts in agent-session.ts, packages/agent/src/agent.ts, and test/goals/goal-mode-integration.test.ts. At its head 8a3ea37ab I verified it does not touch agent-session-message-pipeline.test.ts, and that file still shows 21 pass / 3 fail there — which is why this is offered separately.

I should also disclose that my #3637 edits the same line of test/goals/goal-mode-integration.test.ts that #3638 edits, using a different approach. #3638 is the broader change and carries the product-side repairs; please prefer it and close #3637 if that is simplest. I am happy to close this PR too if you would rather fold the message-pipeline assertions into #3638.

Also noted for completeness: agent-session-auto-compaction-continue.test.ts still shows 16 pass / 2 fail at #3638's head (resetAttemptBudgetSpy observed 0 calls, expected 1, at lines 476 and 599). That one looks product-side in agent-session.ts, which #3638 owns, so I have not touched it.

No product files, no changelog surface, and no generated artifacts are modified. Thank you for reviewing.

…of arity

The per-attempt scope facility threads an extra callback-scope argument
through onPayload, onSseEvent, and extension emit. Three assertions in
agent-session-message-pipeline.test.ts pin the previous argument count
with toHaveBeenCalledWith, so they now fail on the trailing argument
even though every observable behaviour is unchanged.

Assert the payload that each hook actually receives instead of the
call arity:

- composes session payload hooks: assert the session hook sees the
  original payload and the request hook sees the session-transformed
  payload, which also pins the session-to-request threading order.
- records raw SSE diagnostics: assert the forwarded SSE event object.
- forwards stop reasons and reasoning summaries: locate the
  reasoning_summary_end call and assert both its event payload and
  that its continueWhile argument is a function.

Spy parameter types are widened so no cast is needed. Coverage goes up
rather than down: 21 pass / 3 fail becomes 24 pass / 0 fail, and
expect() calls rise from 84 to 91.
@Yeachan-Heo

Copy link
Copy Markdown
Owner
receipt: gajae-ci/merge-verdict-3641
pr: #3641
head: 5a029c3be1f665116f5c26dc3e4f66ce27d8d3d2
base: 8937871b1da572fece073414fd8eef4f631a449e
ci: 13/13 green, mergeable clean
hostile-verdict: MERGE_READY (P0=0, P1=0)
reviewed:
  - test-only: replaces toHaveBeenCalledWith with explicit mock call
    extraction (toContainEqual for agent_end; find+objectContaining for
    reasoning_summary_end; typeof function for 2nd arg)
  - product-coupled: 3-arg emit shape from AttemptScope deliveryScope
  - payload/order/function contracts preserved
disposition: MERGE_READY; merge to dev

PR #3641 hostile exact-head review: MERGE_READY (P0=0, P1=0). Test-only fix, product-coupled. CI 13/13 green.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo merged commit 738d4bb into Yeachan-Heo:dev Jul 31, 2026
19 checks passed
twoimo added a commit to twoimo/gajae-code that referenced this pull request Aug 7, 2026
* chore: bump version to 0.12.5

* test(session): restore payload-deletion wire contracts after the #3489 revert (#3571)

5daf85a1c aligned acp-session-delete-wire and the resident-cache drop
lifecycle to #3489's fail-closed quarantine semantics while the #3489
revert (#3549) was in flight; after the merge both contracts coexist and
the new "fails closed when artifact payload deletion remains
cleanup_pending" oracle is the one red job on dev (run 30523531166).

With the revert in place, artifact-payload deletion completes again, so
restore the pre-alignment tests: the wire oracle proves full transcript
and artifact-payload absence after delete, and the resident-cache drop
exercises dropSession against a session that still holds payload.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(coding-agent): continue first-event timeout turns (#3573)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(skills): preserve exact-name discovery matches (#3574)

* fix(agent): preserve exact abort terminal ownership (#3565)

* fix(acp): harden prompt terminal resource lifecycle

* test(agent): cover post-seal live admission, seal-boundary rejection, and quarantine fail-closed

The rebase of the prompt terminal resource lifecycle work onto dev reinstates
dev's #3522 fix inside the new `register()` helper: `allowSealed` is gone, so a
post-seal registration on a non-quarantined run is admitted live into
`state.resources` and delays settlement instead of being tombstoned. That is
behavior the original #3539 review never saw, so pin the three properties it
rests on.

- Post-seal live admission already had coverage; leave it as the first proof.
- The seal boundary rejects genuinely new root work in two steps: the first
  `reserveProducer()` reports `sealed` and itself quarantines the run, so every
  later reservation reports `quarantined`. Only the first step was asserted.
- Quarantine stays terminal: resolving late work that only reached the bounded
  tombstone retires nothing and never re-opens the run as settled.

* fix(agent): seal the caller-owned run when maintenance aborts

The loop already treats an aborted maintenance as terminal, but it runs with
`resourceSealOwner: "caller"` for Agent-driven runs, so `publishAgentEnd()`
deliberately skips sealing and leaves it to `Agent`. `Agent`'s stream loop then
short-circuited on any `stopReason: "maintenance"` and never reached
`#finalizeRun`, on the assumption a continuation would follow. An aborted
maintenance produces none, so the ledger run stayed open and
`waitForSettlement` reported `run_not_sealed` with nothing actually pending —
the same permanently-unfenced-cancel shape this change set set out to remove,
just relocated to the caller-owned path.

Only the sealing decision changes. The event keeps its `maintenance` stopReason
and `aborted` outcome, so AgentSession can still publish the aborted
maintenance settlement.

Found by the boundary architect review; the new test fails without the fix.

* fix(agent): preserve exact abort terminal ownership

---------

Co-authored-by: probe <re2rar@gmail.com>
Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(ultragoal): bind hydrated gate evidence exactly (#3543)

* fix(ultragoal): bind hydrated gates exactly

* fix(ultragoal): canonicalize hydrated diff binding

* fix(ultragoal): reject unknown hydrated evidence

* fix(ultragoal): close hydration provenance gaps

* fix(ultragoal): bind replacement aggregate receipts

* fix(ultragoal): preserve exact whitespace paths

* fix(ultragoal): fail closed on byte-lossy paths

* fix(ultragoal): preserve BOM path identity

* fix(ultragoal): fail closed on incomplete diffs

* fix(ultragoal): guard missing tools index diff

* fix(ultragoal): preserve incomplete capture authority

* fix(ultragoal): make capture uncertainty explicit

* fix(ultragoal): distrust incomplete PR inventories

* fix(ultragoal): retain review code provenance

* fix(ultragoal): reject unresolved review branches

* fix(ultragoal): bind remaining review authority

* fix(ultragoal): require file-backed coverage proof

* fix(ultragoal): validate adversarial proof files

* fix(ultragoal): bind adversarial-only coverage

* fix(ultragoal): bind proof to covered contracts

* test(ultragoal): bind dogfood proof contracts

* fix(ultragoal): validate adversarial artifact proof unconditionally

The adversarial-only artifact validation was guarded by
'successfulSurfaceProofLinks === 0 && !artifactIds', which skipped
adversarial artifact proof when surface evidence or artifact-only refs
existed. A receipt-only adversarial proof could pass without a
file-backed artifact.

Remove the guard so adversarial artifact proof files are always
validated against existing non-empty files, matching the
artifact-only and surface-proof validation paths. Update both
expectRejected and expectAccepted helpers to create the adversarial
artifact file, and add a test that rejects receipt-only adversarial
proof when surface proof is valid.

* fix(ultragoal): file-back adversarial fixture proof for #3543

Unconditional adversarial path validation rejected intermediate-story
checkpoints that only supplied inlineEvidence, reding skill-state hooks
and computer red-team CI on this repair head.

Lore-id: 3543fx01
Constraint: keep #3541 hydration fail-closed; fixtures must not weaken production gates
Rejected: weaken adversarial file check for tests | reopens fail-open hydration
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: skill-state-hooks 48/0; computer-red-team-fixtures 16/0; ultragoal-runtime 172/0 focused adversarial filters
Not-tested: full Dev CI matrix on pushed head

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(coding-agent): tolerate minimal bash settings adapters (#3480)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(ci): give the docs-only plan the native producer its gate needs

#3559 made a docs-only dev PR select exactly one shard,
`test:packages/coding-agent/test/docs-index-lazy.test.ts`. That key
starts with `test:`, so `taskNeedsNative` reports it as a native
consumer and the matrix entry carries `native: true`. But the plan had
no native producer, so `has_native` was `false`, `affected-native` was
skipped, and the artifact `dev-affected-native-${run_id}` was never
uploaded — while the shard's `if: ${{ matrix.native }}` download step
still fired against it.

The escalation that would have caught this, `ensureNativeBuild`, sits
after the docs-only early return in `planTargetedTasks`, so that path
never reached it; `planTasks` has no such tail at all. Both are fixed at
their own seam.

#3559's own CI stayed green because it edits the test file and
`scripts/`, so it never produced a docs-only plan and never exercised
the path.

Measured on this branch, `CI_DEV_CHANGED_PATHS=docs/guide.md` now
reports `native=true` where dev reports `native=false`.

The merged assertion `toEqual([EMBEDDED_DOCS_GATE_KEY])` was pinning the
defect; it now requires the producer. A separate contract test states the
invariant as the matrix sees it: a docs-only plan must never carry a
native consumer without a producer. The `--native-build is a no-op`
fixture moves off `docs/readme.md`, which now legitimately has a
producer, onto `CHANGELOG.md`, which has none.

* fix(coding-agent): preserve terminal timeout providers (#3578)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(tui): bind sticky-viewport oracle authority to one reviewed commit and widen provenance to Rust (#3583)

* test: bind viewport evidence to renderer state

* style: format viewport observation types

* docs(tui): record renderer viewport observation and alternate-scroll ownership

* fix: make ascii-no-color viewport evidence host-independent

The ascii-no-color render mode relied on chalk.level = 0, but theme.ts emits
SGR directly through fgAnsi/bgAnsi without consulting chalk. Its colour form
follows detectColorMode(): truecolor when COLORTERM=truecolor, indexed 38;5;N
when TERM is dumb/empty/linux. CI has neither COLORTERM nor a rich TERM, so it
captured indexed colour where a developer host captured truecolor.

Strip ANSI for ascii-no-color artifacts so they are genuinely escape-free on
every host, and derive both the persisted payload and the recorded cursor frame
digest from that one canonical string. Restore the strict escape-free assertion
for that mode; the colour branch keeps the widened check because the previous
regex missed background and bright colour.

* fix: derive viewport capacity oracle from committed paint

The verifier compared transcript_capacity against pin_boundary.row, but tui.ts
assigns both from one renderer local, so that assertion could never fail. It
also dropped the frame-derived capacity derivation and the ordered suffix
marker oracle, leaving 19 of 20 keys bound to renderer self-report.

Derive capacity, status-row index and notice cardinality from the committed
paint, then require the renderer report to AGREE with it, which is strictly
stronger than either source alone. Restore the ordered suffix marker oracle
over the production root order. Add a pin_boundary.row mutation case that is
only detectable against the paint, so the assertion cannot silently become
tautological again.

* fix: address viewport evidence review findings

- capture: replace Awaited<ReturnType<typeof ...>> with a declared
  CaptureProvenance type (AGENTS.md prohibits ReturnType<>)
- tui: reject non-finite setViewportSelection coordinates, matching the
  Number.isFinite guard already in scrollViewportBy. NaN previously
  survived the clamp, latched #mouseSelectionDragged, and forced a
  repaint every frame while reporting a NaN selection
- virtual-terminal: delete unused getCursorPosition(). Measured against
  all 20 fixture keys it agrees with the reported cursor in only 5;
  the other 15 sit at (rows-1, columns) where the final frame write
  left the emulator cursor, so it cannot serve as a cursor oracle
- CHANGELOG: record the reported-cursor-column clamp, and scope the
  wheel-scrollback claim to the capture-off case

* style: collapse capture signature to one line

* fix: canonicalize every persisted ascii-no-color viewport frame

Required metadata.json artifacts carried raw ANSI through captureFrame(),
so the visible-empty IRC frame and all nine resize probes embedded whichever
colour form the capture host negotiated (indexed 38;5;n under TERM=dumb,
truecolor 38;2;r;g;b otherwise). The earlier canonicalAnsi strip covered only
the top-level terminal-ansi.txt and the cursor digest, leaving the required
metadata host-dependent while both verifiers reported success.

- captureFrame() now canonicalizes at the single point every persisted frame
  flows through, deriving text and sha256 from that canonical value.
- The verifier rejects any CSI/ESC byte in every required ascii-no-color frame
  field, not just the top-level payload.
- semantic_anchor.id embedded a per-run entry id, which made the bundle
  irreproducible even within one host; it is now a deterministic digest of the
  anchor's own geometry.
- Adds two-environment reproducibility coverage and a rehashed metadata
  corruption case that reaches the new guard.
- Removes the orphan hardware-cursor doc line left by the deleted accessor.

* docs(tui): move unreleased changelog entries out of the released 0.12.1 section

The PR's Added/Changed/Fixed bullets were nested under the already-released
[0.12.1] heading, leaving [Unreleased] empty. AGENTS.md requires new entries
under [Unreleased] only. Every heading from [0.12.2] down is restored
byte-identical to upstream; the change is 12 insertions, 0 deletions.

* fix(test): bind semantic anchor identity to a domain-separated digest and drop repo-derived status text

The persisted `semantic_anchor.id` was the only field anchoring the painted
row, but it hashed geometry alone and truncated to 8 hex. That made it both
heavily aliased (distinct entries painting different content at equal offsets
collapsed onto one id) and brute-forceable, and the verifier only checked that
it was a nonempty string.

The id is now the full domain-separated SHA-256 of the entry key, namespace,
painted row text, complete geometry including frameRow, and the committed frame
digest. Every preimage input is persisted, and the verifier recomputes the
expected id from those inputs plus the committed paint before any downstream
metadata check, rejecting arbitrary, transplanted, aliased, malformed, and
truncated ids as well as content mutation under unchanged geometry.

Required evidence frames also embedded the capture host's repository state:
`#getGitStatus()` returns its cache and fires the fetch asynchronously, so one
paint rendered `detached` and a later one `detached +8` in the same worktree.
The fixture now pins the status line to model/session_name segments, so no
repo-derived text reaches required metadata at all.

* fix(test): scope viewport provenance digest to its declared surface

The provenance digest hashed `git diff --binary HEAD --` over the entire
worktree, and the verifier recomputes it live. That coupled bundle validity to
every tracked file in the repo, so an unrelated edit anywhere retroactively
marked already-captured bundles stale — and any write landing inside the
capture-to-verify window flipped the digest mid-run, masking whichever guard was
actually under test. Narrow the digest to a declared scope and persist
`git_diff_scope` so the covered surface is read off the bundle rather than
inferred, with the verifier comparing it against its own constant.

Proven behaviorally: an out-of-scope edit is accepted, an in-scope edit is still
rejected.

* docs(tui): keep unreleased changelog entries out of released sections

* fix(test): derive semantic anchor row and geometry from an immutable source expectation

The persisted semantic_anchor.id was a full domain-separated digest, but the
verifier recomputed it from producer-supplied fields. A producer that mints the
bundle therefore mints a cryptographically valid id for any chosen row and
geometry: transplanting geometry from another entry and relocating the anchor one
painted row down, then recomputing every digest, manifest, review-input binding,
and scoped provenance, was accepted. Digest consistency proves internal
consistency, not semantic authenticity.

The verifier now carries a frozen per-entry expectation for frame row and
complete grapheme/cell geometry. That table is authoritative because the
verifier's own bytes are inside source_sha256, so a bundle producer cannot
rewrite it without invalidating provenance. Measured across both color
environments, the geometry is identical for all 17 anchors, so the table is a
host-independent invariant rather than a captured artifact.

The id preimage bound the host-negotiated ANSI frame digest, which made 16 of 17
ids differ between indexed-color and truecolor hosts even though the stripped
semantic paint was byte-identical. Semantic identity now binds the stripped-text
digest; the ANSI digest stays persisted as an artifact binding and is still
checked against the committed frame.

* fix(test): anchor oracle expectation integrity to the committed blob

The semantic-anchor expectation table is only immutable if its digest comes
from outside the bundle's reach. captureProvenance() hashes the WORKTREE
file, so mutating the table and restamping provenance was accepted: the
stamp described the mutation instead of rejecting it.

Compare every oracle source against its committed blob at the manifest's
own git_head. A bundle author cannot restamp that without pushing a commit,
which changes git_head. Fails closed when the blob is unreadable, and runs
before semantic-anchor validation so a mutated table cannot ride through.

* test(tui): reject a restamped oracle-expectation mutation

* fix(test): bind oracle integrity to any reachable committed blob and widen provenance scope to Rust crates

* fix(test): bind oracle integrity to any reachable commit and widen provenance scope to Rust

* test(tui): derive the synthetic-merge base from dev instead of local topology

* fix(test): make oracle integrity authority a single exact commit

The previous gate fell back to reachableBlobSha256s() over --all, so oracle
bytes committed on ANY local or remote-tracking ref satisfied it. Reachability
is not review authority: an unrelated attacker ref could authorize bytes that
were never the reviewed head, and in CI the fetch topology silently became a
security input.

Authority is now exactly one commit: GJC_STICKY_VIEWPORT_ORACLE_COMMIT when
set, otherwise provenance.git_head. It is validated as a full commit object,
both oracle sources must resolve at that same commit, the chosen commit is
persisted as provenance.oracle_commit and cross-checked at verify, and there is
no ref fallback.

* fix(test): supply git identity to the oracle ref-topology regression

---------

Co-authored-by: twoimo <twoimo@twoimoui-MacBookPro.local>
Co-authored-by: twoimo <twoimo@users.noreply.github.com>

* fix(coding-agent): keep agent_end publication non-blocking (#3582)

The exact cancellation-ownership merge made deferred agent_end publication await background persistence and the user extension queue. That tied public session readiness to extension latency and regressed the same message-pipeline shard on two consecutive dev heads. Start persistence and extension delivery in the established background order, but retain the exact producer lease until the queued extension event settles.

Constraint: Public agent_end must settle before user extension handlers while session shutdown still drains queued delivery and exact cancellation ownership remains live through that delivery.
Rejected: Restore the old immediate lease close | loses the cancellation-domain lifetime introduced by the ownership fix.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not await user extension delivery from public terminal publication; retain its producer lease through queue settlement instead.
Tested: 181 focused coding-agent/agent tests passed with 852 assertions; coding-agent Biome and TypeScript checks; git diff --check.
Not-tested: Full hosted Dev CI and install-method matrix.

* fix(tui): make sticky provenance cwd-independent (#3588)

Resolve sticky-viewport provenance and oracle source reads from one canonical repository root, and replace the post-merge-invalid authority topology assumption with an explicit deterministic older commit.

* test(tui): pin whole-frame sticky viewport content (#3591)

Bind ANSI-stripped text for all 20 reviewed sticky-viewport frames to the exact oracle commit, including null-anchor capacity-zero cases and coordinated non-anchor forgery regressions.

* fix(utils): validate positive integer env values (#3599)

* fix(tui): bound select no-match rows (#3602)

* fix(coding-agent): validate settings array items (#3603)

* fix(ultragoal): isolate red-team test fixtures from CI changed-path leak (#3605)

Five ultragoal test suites (critic-gate, dogfood, review,
durable-completion-release, runtime) created their temp checkpoints inside
the enclosing git work tree. computeCheckpointChangeSet merges the CI
planner's CI_DEV_CHANGED_PATHS into the computed change set, so on any
branch that touches a computer control surface path (tools/index.ts,
settings-schema.ts, computer.ts, pi-natives/computer/) the generic gate
fixtures falsely triggered the mandatory computer red-team suite and
failed with COMPUTER_REDTEAM_CASE_MISSING: ... must include
kill-switch-bypass.

The production kill-switch-bypass gate is correct and unchanged. The
fixtures simply did not isolate their own contract from the host branch's
diff. ultragoal-runtime.test.ts already solved this for its validation-
batch tests via batchTempDir() (outside git + pinned env); the other
suites never applied the same hermetic pattern.

Each affected suite now captures CI_DEV_CHANGED_PATHS once at module
load, clears it in beforeEach, and restores the original in afterAll.
The explicit CI-leak tests that set their own value (computer-red-team-
fixtures, review branch-merge) continue to work because beforeEach clears
the baseline before each test and their own try/finally restores within
scope.

Added a positive regression: a complete mandatory computer red-team gate
(all seven adversarialCases incl. kill-switch-bypass) PASSES on a
genuine computer change -- proving the gate is not weakened.

Lore-id: 3533redteam
Constraint: must not weaken, bypass, or suppress the kill-switch-bypass gate
Tested: all 1211 gjc-runtime tests pass with CI_DEV_CHANGED_PATHS pinned to a computer surface path
Not-tested: full dev-ci shard matrix (run on push)
Confidence: high
Scope-risk: narrow
Reversibility: trivial

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(gjc-runtime): make psmux authority host-independent and fence continuation dispatch (#3607)

* fix(gjc-runtime): make psmux authority and continuation dispatch host-independent

Restacks the Windows psmux authority work onto current dev, dropping the
storage lineage dev already absorbed via squash, and repairs every
darwin-only failure in the slice.

Five separate platform sources decided psmux behavior independently; four
ignored the authority test seam entirely, so Windows-authority paths were
unreachable on POSIX and eleven tmux-sessions tests failed there:
- psmuxAuthorityEnvironments gated persisted-authority discovery on raw
  process.platform, aborting before any later guard could run.
- resolveGjcTmuxProviderContext and createGjcTmuxSession each recomputed
  the platform instead of consulting the seam.
- teamProviderAuthority read process.platform directly; GjcTeamConfig now
  carries the platform so startGjcTeam's value reaches it.

Continuation dispatch had two real defects: the ack-loop deadline used
raw Date.now() while every other timestamp in the file routes through the
seamed clock, and the reservation site threw where the revalidation site
records a skipped outcome, so one worker's short lease aborted an entire
monitor pass.

tmux-sessions: 35/35 on darwin (was 24/35). team-runtime: 115/117, with
zero psmux-only failures - both remaining names also fail at dev, and
this slice fixes three that dev fails.

* test: align session-command untagged diagnostic with the psmux authority message

* fix(lifecycle): keep POSIX on dev's tmux lifecycle path

psmux rewrote the darwin lifecycle spawn/cleanup path (completeNonLinuxLifecycleSpawn,
cleanupDirectLifecycleAttempt), which regressed POSIX: dev is 60 pass / 0 fail on
notifications-lifecycle-control-runtime, psmux was 51 / 9. psmux is a Windows-only
provider and POSIX uses tmux, so the darwin path must stay dev's.

Reverting the file to dev keeps every psmux suite green (tmux-sessions 35/0,
psmux-detect 39/0, launch-tmux 137/0, tmux-gc 9/0) and preserves the win32 lifecycle
content, which dev already carries from the earlier merged psmux-authority slices.

* fix(test): restore POSIX lifecycle and session metadata round-trip fidelity

* fix(gjc-runtime): unblock psmux authority publish after dev reverted the managed sync lock

* fix(gjc-runtime): tolerate non-claim task version bumps during continuation revalidation

* Revert "fix(gjc-runtime): tolerate non-claim task version bumps during continuation revalidation"

This reverts commit ebc3ca196801cd6b50b726c51d22ed3ba704e9ce.

* Reapply "fix(gjc-runtime): tolerate non-claim task version bumps during continuation revalidation"

This reverts commit 56f68822097d04d335126c7542f19e9dd720748e.

* fix(gjc-runtime): enforce psmux authority migration exclusion without the managed sync lock

The Windows psmux authority publish previously acquired `acquireManagedLockSync`
from managed-session storage. Dev removed that symbol when it reverted #3489, so
the import broke on merge and the whole slice failed at module load.

Reviewers asked for the sync lock to return only alongside a real consumer and a
shared async/sync release contract, which belongs to the resident storage branch
rather than here. So this slice now enforces the property it actually needs on its
own: a retained lock descriptor for the exact authority name blocks publication
unless its owner process is definitely gone. Expiry alone is not permission,
because a paused writer with an expired lease is still live and stealing the name
would let two writers publish one generation-scoped authority.

`publishManagedFileNoReplaceSync` already supplies create-without-clobber
exclusion for the payload itself, so no lease helper is required.

* fix(gjc-runtime): fence continuation dispatch and restore the split marker

Repairs the two blockers the reviewer identified on the previous head.

1. A public authority-changing operation dispatched no continuation argv.
   The continuation reservation is written under the task lock, that lock is
   released, and only then does dispatch revalidate eligibility. A concurrent
   claim-releasing operation could land in that window, so revalidation observed
   no current claim and the incident was journalled skipped with nothing
   dispatched. Revalidation plus send-keys now runs inside the same team
   mutation fence every other public authority-changing operation takes, and the
   fence is released before the ACK wait because the ACK is published by a
   separate receiver process that must acquire the same cross-process fence.
   Worker GC prune now takes that fence too, since it deletes claim records.

2. Replacement settled before pane-launch ACK. The fake tmux rewrite dropped the
   three-field tmux-last-split marker its shell predecessor published on every
   split. The memory-guard replacement test waits on that marker before
   publishing the generation-bound startup ACK, so the ACK never arrived,
   relaunch hit its startup deadline, and the guard reported retrying. The
   marker is restored. The same test also aborted its marker waiter before its
   own advisory-host early return, which masked the advisory outcome on
   non-Linux hosts; the ordering is fixed.

* fix(gjc-runtime): keep pane id narrowing in the fenced dispatch closure

The fence-hold restructure moved the send-keys argv construction into a
closure, which dropped the outer non-null narrowing of `worker.pane_id`.
Assert it the same way the adjacent `executeTeamTmuxMutation` calls already
do, so the frozen argv stays `readonly string[]`.

* fix(gjc-runtime): bind a proven pane id for fenced continuation dispatch

---------

Co-authored-by: twoimo <twoimo@twoimoui-MacBookPro.local>
Co-authored-by: gjc <gjc@local>

* fix(utils): release discarded retry response bodies (#3577)

Retry-hint inspection tees response bodies, so a response abandoned for another attempt can retain buffered data. Begin cancellation only after every caller-return path is excluded, and do not await transport-controlled cleanup that could stall retry or abort progress.

Constraint: Returned responses must remain unconsumed, including final attempts and over-cap retry hints.
Rejected: Await body cancellation before retry | a custom or stalled transport can leave the cancellation promise pending indefinitely.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep discarded-body cleanup non-blocking and after all response-return decisions.
Tested: 8 focused fetch-retry tests including pending/rejecting cancellation and abort; packages/utils Biome and typecheck; git diff --check
Not-tested: Live network socket reuse and transport-specific cancellation behavior

* fix(agent): reject malformed proxy tool-call endings (#3572)

Proxy event conversion already rejects type-mismatched delta and end events except for toolcall_end. Failing closed at the same conversion boundary prevents a malformed proxy sequence from being silently accepted as a successful terminal message while preserving valid tool-call finalization.

Constraint: Preserve the existing proxy event and valid tool-call output contract.
Rejected: Ignore the malformed event and rely on a later terminal | accepts incomplete provider state without diagnostics.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep proxy content-finalization events fail-closed when their content index has the wrong type.
Tested: bun test packages/agent/test; bun --cwd=packages/agent run check; git diff --check
Not-tested: Live remote proxy transport

* fix(ultragoal): relocate test temp dirs outside git work tree to prevent shard-red captureIncomplete (#3611)

The prior fix (#3605) cleared CI_DEV_CHANGED_PATHS in beforeEach but
left temp dirs inside the enclosing git work tree. Under parallel
shard load (bun test --shard=N/8), git commands inside
computeCheckpointChangeSet timeout at their 5s budget, causing
captureIncomplete=true which unconditionally triggers the mandatory
computer red-team suite -- even when no computer surface path is
present in the change set.

Root cause: captureIncomplete=true is a conservative production safety
default, but it fires falsely in test environments under parallel
load because the git diff/ls-files commands race the timeout.

Fix: relocate all five affected suites' tempDir() to os.tmpdir()
(outside the git work tree). When the cwd is not inside a git work
tree AND ciChangedPaths is non-empty, computeCheckpointChangeSet
returns { paths: ciChangedPaths } without captureIncomplete. Pin
CI_DEV_CHANGED_PATHS to a non-computer test path so the returned
paths don't trigger the suite.

This mirrors the existing batchTempDir() pattern from
ultragoal-runtime.test.ts which has been stable since its
introduction. Also adds .tmp-* to packages/coding-agent/.gitignore
to prevent any remaining in-repo test artifacts from polluting
untracked-file inventory.

The production kill-switch-bypass gate (MANDATORY_COMPUTER_CASE_IDS,
requiresComputerRedTeamSuite, validateMandatoryComputerAdversarialCases)
is byte-identical to dev. No enforcement is weakened.

Lore-id: 3533shard5
Constraint: must not weaken genuine computer red-team enforcement
Tested: shard 5/8 with exact CI plan paths -> 0 COMPUTER_REDTEAM failures
Not-tested: full 8-shard matrix (run on push)
Confidence: high
Scope-risk: narrow
Reversibility: trivial

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(ultragoal): cap review_blocker recursion and add exact-objective dedup (#3613) (#3615)

recordUltragoalReviewBlockers created unbounded review_blocker goal chains
and duplicated goals for identical objectives, unlike its sibling
recordReviewFindingGoals which already deduped via findOpenReviewBlockerGoal.

Independently reproduced on dev 29ddfe08: identical objectives produced
duplicate goals (2 calls -> 2 goals); distinct findings grew without bound
(8 calls -> 8 goals, no cap); no user-reachable safe stop existed.

Fix (3 parts, all in ultragoal-runtime.ts):
1. Exact-objective dedup reusing findOpenReviewBlockerGoal, scoped to the
   same blockedGoalId. On dedup-hit: idempotent return of the matched
   existing goal id, no writePlan, no appendLedger.
2. MAX_REVIEW_BLOCKER_DESCENTS=3 cap counting unresolved descents (status
   not in {complete,superseded}) off the blocked goal. 4th attempt throws
   typed UltragoalReviewBlockerRecursionCapError BEFORE any mutation
   (read-check-then-write on the persisted plan snapshot). CLI surfaces it
   as exit 1 with the typed marker. Dedup applied before budget; same-root
   budget durable across replay/restart/concurrency.
3. CLI record-review-blockers returns the matched existing id on
   dedup-hit, new id on creation (was blindly goals.at(-1)).

9 direct regressions added: identical-objective dedup, CLI receipt
truthfulness, cap+typed-error+no-partial-mutation, CLI cap surfacing,
resolved-ancestor exclusion, restart/replay durability, missing-goal
handling, ordinary single-round preservation, cross-goal non-dedup.

All 198 tests pass (181 existing + 17 nudge-guard + 9 new). TS check clean.

Signed maintainer admission: the unbounded review_blocker growth defect in
recordUltragoalReviewBlockers is independently confirmed on dev 29ddfe08 via
repository-owned reproduction. Evidence is structural unboundedness + identical-
commit repro only; no private token totals or third-party session counts are
validated as repository fact. The disputed attribution figure is retracted.

Lore-id: 3613-ultragoal-recursion-cap
Constraint: MAX_REVIEW_BLOCKER_DESCENTS must default to 3 (descents 1..3 ok, 4th triggers terminal handoff)
Constraint: dedup before budget check; same-root budget durable across replay/restart/concurrency
Constraint: fail closed without corrupting goals.json/ledger
Constraint: preserve ordinary single-round and sibling recordReviewFindingGoals behavior
Rejected: dedup only no cap | distinct findings still unbounded, no safe stop
Rejected: cap only no dedup | duplicates still corrupt identity/ledger
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: 198 tests (181 existing + 17 nudge-guard + 9 new regressions)
Not-tested: cross-process file-locking (out of scope, single-process TOCTOU closed)

Closes #3613

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(acp): keep retained artifact deletion pending (#3569)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(local): encode listing href components (#3623)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* feat(models): add LunaMaxxing Codex profile

Adds the official bundled OpenAI Codex LunaMaxxing profile with exact role mappings and catalog coverage.

* fix(docs): refresh model profile index (#3630)

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* chore: bump version to 0.12.5

* fix(dev): reconcile backlog docs and monitor behavior

Align historical issue dispositions and monitor documentation with the current product surface. Coalesce persistent monitor notifications and preserve local URL authority decoding safeguards.

* fix(ultragoal): bind reviews to source snapshots

Review lanes could self-declare matching hashes even after the worktree advanced. Runtime-owned cohorts now derive canonical source identity before dispatch, persist lane provenance, classify late delivery as stale, and require current delivery receipts at the completion gate.

Lore-id: issue-3469

Constraint: preserve one issue per branch and PR

Rejected: equal reviewer-declared hashes | stale lanes can agree on the wrong source

Confidence: high

Scope-risk: medium

Reversibility: revertable

Tested: package check; 236 focused tests; definition and rebrand gates

Not-tested: full workspace check blocked by pre-existing stale generated docs index on current dev

* fix(ultragoal): register review-source command

The source snapshot coordinator was implemented and documented but omitted from the canonical workflow manifest, so static skill-doc verification rejected the public command. Register the verb and typed arguments, expose help, and add black-box manifest coverage.

Lore-id: issue-3469-ci-repair

Constraint: do not absorb the dev docs-index regeneration owned by PR #3630

Confidence: high

Scope-risk: low

Reversibility: revertable

Tested: coding-agent check; 238 focused tests; workflow manifest and skill-doc gates

* fix(ultragoal): harden review provenance

Hostile review found bypasses around legacy hash-only gates, partial dispatch binding, nested persisted records, post-delivery source changes, and concurrent cohort writes. Require runtime cohort authority, bind every dispatch field, validate nested provenance, revalidate source at join, reject unsupported isolated review execution, sanitize rerun output, and retry CAS conflicts.

Lore-id: issue-3469-review-fixes

Confidence: high

Scope-risk: medium

Reversibility: revertable

Tested: coding-agent check; source/receipt/command/default focused suites; manifest and skill-doc gates

* fix(ultragoal): make delivery authority generation-safe

Fresh hostile review found stale classification could be fooled by reverting owner bytes and that delivery identity remained ambiguous. Bind deliveries to opaque dispatch/cohort IDs, classify against the active cohort, dedupe replay, reject ambiguous persisted authority, reuse unchanged freezes, and bind every declared task field.

Lore-id: issue-3469-final-review-fixes

Confidence: high

Scope-risk: medium

Reversibility: revertable

Tested: coding-agent check; focused source/receipt/command/default suites

* fix(ultragoal): scope source authority to review evidence

The first fail-closed gate applied runtime cohort authority to every completion, breaking ordinary checkpoints and replay flows. Preserve existing non-source-aware cohort semantics while requiring runtime authority whenever persisted cohorts, cohort IDs, or delivery IDs assert source-aware review evidence.

Lore-id: issue-3469-runtime-repair

Confidence: high

Scope-risk: medium

Reversibility: revertable

Tested: complete ultragoal runtime suite; source/command/task receipt suites; coding-agent check; manifest and skill-doc gates

* fix(docs): refresh generated docs index for backlog doc edits (#3632)

`bc11b36ca` edited `docs/sdk-rpc-parity-audit.md` and `docs/tools/monitor.md`
without regenerating the embedded docs index, so `check:public-sync` and
`docs-index-lazy.test.ts` fail on dev and on every PR based on it.

Regenerated with the repo's own generator; no hand edits.

Co-authored-by: GJC <gjc@example.test>

* chore(docs): refresh embedded docs index

The current PR head changes the public workflow surface and public-sync deterministically requires the canonical embedded docs index generated from the exact base plus PR tree.

Lore-id: issue-3469-public-sync

Generated: bun run generate-docs-index

Tested: bun run check:public-sync

* fix(ultragoal): reject duplicate review authority

Persisted review state must have one dispatch per lane and one delivery per dispatch. Reject duplicate lane and conflicting duplicate delivery authority before a gate can select an arbitrary current record.

Lore-id: issue-3469-final-authority

Confidence: high

Scope-risk: low

Reversibility: revertable

Tested: coding-agent check; 239 focused tests; public sync; manifest and skill-doc gates

* test(session): authorize resident lifecycle cleanup fixture

The post-merge resident lifecycle fixture expected terminal deletion without installing descriptor-backed native cleanup authority. Keep production fail-pending semantics covered while giving the success path verified synthetic authority.

Lore-id: resident-lifecycle-ci-repair

Constraint: preserve descriptor-bound cleanup_pending behavior without native authority

Tested: bun test packages/coding-agent/test/session-resident-lifecycle.test.ts

Tested: bun test packages/coding-agent/test/session-manager/session-directory.test.ts packages/coding-agent/test/ultragoal-redteam-resident-cache.test.ts

Tested: bun --cwd=packages/coding-agent run check

* fix(ultragoal): stabilize repository review identity

Plan repository bindings retain plan-time HEAD and branch display metadata. Hash only stable worktree/common-dir authority so a normal implementation commit between goal creation and review does not invalidate unchanged current delivery.

Lore-id: issue-3469-critic-repair

Confidence: high

Scope-risk: low

Reversibility: revertable

Tested: coding-agent check; 240 focused tests; public sync

* feat(agent): immutable per-attempt scope facility (#3592) (#3608)

* feat(agent): immutable per-attempt scope facility (#3592)

Add AttemptScope — an immutable per-attempt identity (attemptId + generation
+ lineage) allocated by packages/agent before every observable lifecycle
emission across all five attempt-entry sites (loop IIFE, continue IIFE,
tool-loop turn_start, managed direct agent_start, direct/ephemeral
streamSimple). The scope is propagated as an explicit parameter through
pre-stream callbacks, lifecycle events, ManagedAttemptOutcome, and the
extension delivery seam — never via a mutable ambient slot or
AsyncLocalStorage.

AttemptScopeAuthority owns per-lineage currentness (main vs side) so a
side attempt never invalidates the main scope and forceAbort advances only
the main lineage. AttemptRecordStore tracks execution state per scope
(unknown → clean → executed) with fail-closed admission, generation-guarded
stale-write rejection, and bounded LRU retention.

ExtensionRunner marks execution at actual handler delivery (not
registration), preserving backward compatibility for sessions whose streams
don't participate in the facility. The after_provider_response boundary
distinguishes configured external handlers from the always-installed
raw-SSE-debug interceptor.

This is the facility-only foundation for #3553's request-scoped
first-event-timeout provenance. No retry/replay-safety gate is changed.

Lore-id: attemptscope-3592
Constraint: must not couple to retry/replay-safety gating — #3553 adds admission
Rejected: mutable current-scope field | late settlement after forceAbort corrupts successor
Rejected: AsyncLocalStorage | implicit ambient flow-control, hard to audit
Rejected: closure-capture-at-construction | provider hooks created before any attempt exists
Confidence: high
Scope-risk: wide
Reversibility: additive
Directive: scope is always an immutable value passed as a parameter, never rediscovered
Tested: packages/ai + packages/agent + packages/coding-agent check:types pass; 72 retry/fallback tests pass
Not-tested: AttemptScope-specific unit/integration tests (follow-up commit)
Supersedes: conservative-registration-veto approach from #3553 stage-03

* test(agent): add AttemptScope facility regression tests (#3592)

Add 12 tests covering the five required AttemptScope-specific regressions:

1. Force-abort late-settlement isolation: a stale G1 markExecuted is
   rejected after G2 supersedes the main lineage (generation guard).
2. Concurrent side-scope isolation: a side attempt does not invalidate
   the main scope and vice versa; retiring a side scope cleans up its
   authority.
3. No-extension no-mark: a session with no facility has no records and
   isClean is always false (fail-closed).
4. Managed-discard scope roundtrip: a discarded managed attempt's scope
   is attributable via isClean (clean if no handler ran, executed if one
   did).
5. Provider-drop fail-closed: a provider that drops attemptScope leaves
   no record → isClean fails closed; LRU eviction bounds record count.

Also covers the AttemptRecordStore state machine: register→unknown,
establishClean→clean, markExecuted→executed (clean-only), retire removes
record and rejects late resurrection.

Lore-id: attemptscope-regressions
Tested: 12 new regression tests + 72 existing retry/fallback tests pass
Confidence: high
Scope-risk: narrow
Reversibility: additive

* fix(agent): AttemptScope hostile-review P1 corrections (#3592)

Address 3 P1 findings from hostile exact-head review:

1. Runner guard is fail-closed at the decision point (isClean), not at
   delivery. A scope-less delivery still runs the handler (backward-
   compatible) but records no mark; isClean returns false for an unmarked
   scope, so #3553 admission refuses. This avoids breaking extension
   handlers for sessions whose streams don't yet stamp AttemptScope.

2. Removed unused #isAttemptScopedEvent predicate (guard is now a no-mark,
   not a throw, so the predicate is dead code).

3. Rewrote provider-drop regression test to document the correct fail-closed
   chain: never-registered scope → isClean false (refused); registered +
   no handler → isClean true (correct); registered + handler ran with
   scope → markExecuted → isClean false (refused).

Lore-id: attemptscope-p1-fix
Tested: 85 tests pass (13 AttemptScope + 46 retry + 26 fallback), 0 fail
Confidence: high
Scope-risk: narrow
Reversibility: additive

* fix(agent): AttemptScope hostile-review P1 identity/lifetime fixes (#3592)

Fix 3 P1 findings from hostile exact-head review:

1. createAttemptMinter now produces strictly-increasing per-lineage
   generations (was always generation 0 — placeholder bug).

2. All AttemptScope values are now Object.freeze'd at creation
   (createAttemptMinter.mint, AttemptScopeAuthority.mintFor), preventing
   runtime mutation of identity after registration/carrier.

3. LRU eviction only removes RETIRED side authorities, never a live one.
   The 1025th side registration no longer silently invalidates an
   in-flight side attempt; only disposed entries are evicted.

Lore-id: attemptscope-p1-identity
Tested: 13 AttemptScope regressions + 46 retry tests pass, 0 fail
Confidence: high
Scope-risk: narrow
Reversibility: additive

* fix(ai): thread AttemptScope through provider hooks and notifyProviderResponse (#3592)

Fix the P1 hostile-review finding: provider hooks (onPayload, onResponse,
onSseEvent) now receive the per-attempt scope as a trailing parameter.

Changes:
- Moved attemptScope from SimpleStreamOptions to StreamOptions so all
  provider option types (AnthropicOptions, OpenAIResponsesOptions, etc.)
  inherit it.
- notifyProviderResponse forwards options.attemptScope to onResponse.
- agent-loop captureOnResponse forwards the scope to the user callback.
- stream.ts mapOptionsForApi forwards attemptScope to per-API options.
- Provider call sites (anthropic, openai-responses, openai-completions,
  google-gemini-cli) pass options.attemptScope to onPayload/onSseEvent.
- LineageCurrentness.isCurrent now validates attemptId in addition to
  lineage + generation, preventing cross-authority same-generation scope
  acceptance.

Lore-id: attemptscope-provider-forwarding
Tested: 13 AttemptScope + 46 retry tests pass; all 3 packages check:types pass
Confidence: high
Scope-risk: wide
Reversibility: additive

* fix(ai): forward AttemptScope through all provider hook and SSE seams (#3592)

Complete provider-layer scope forwarding across ALL active stream providers:
anthropic, openai-responses, openai-completions, azure-openai-responses,
google-shared, google-gemini-cli, amazon-bedrock, openai-codex-responses,
ollama, cursor, openai-anthropic-shim, gitlab-duo, mock.

- createClient/debug-fetch SSE wrappers now forward attemptScope
- notifyProviderResponse forwards options.attemptScope to onResponse
- captureOnResponse in agent-loop forwards scope to user callback
- attemptScope on StreamOptions (inherited by all provider option types)
- mapOptionsForApi forwards attemptScope to per-API options
- Delegated/wrapper providers (shim, gitlab-duo) forward attemptScope
- mock.ts forwards attemptScope to onResponse

AttemptScope identity: frozen values, monotonic per-lineage generation,
isCurrent validates attemptId+generation+lineage, retire-only LRU eviction.

Facility-only: no retry/replay-safety gate change; isClean exposed but uncalled.

Lore-id: attemptscope-all-seams-final
Tested: 13 AttemptScope + 46 retry tests pass; all 3 packages check:types + biome pass
Confidence: high
Scope-risk: wide
Reversibility: additive

* fix(agent): retire-aware LRU, telemetry scope, createClient scope forwarding (#3592)

Fix 3 hostile-review P1 findings:

1. OpenAI createClient call sites now pass options?.attemptScope so the
   SSE debug-fetch wrapper receives the correct scope (openai-responses,
   openai-completions).

2. telemetry.ts captureOnResponse now accepts and forwards the scope
   parameter to userOnResponse, closing the last scope-stripping wrapper.

3. Retire-only LRU: authority disposer marks lineage as retired without
   deleting from maps; evictRetiredIfNeeded only removes retired entries.
   isCurrent returns false for retired lineages. Store eviction skips
   records whose scope is still current (never evicts a live attempt).

Lore-id: attemptscope-p1-retire-lru
Tested: 13 AttemptScope + 46 retry + 26 fallback = 85 tests pass, 0 fail
Confidence: high
Scope-risk: narrow
Reversibility: additive

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* test(session): authorize managed cleanup fixture

Use an explicit immediate-cleanup test seam where the platform cannot provide descriptor-bound final deletion, keeping the lifecycle assertion focused on session detachment.

* fix(session): tolerate legacy extension runner doubles

Attach the attempt record store only when an injected extension runner implements the new setter, preserving compatibility with lightweight test and integration doubles.

* fix(coding-agent): restore import order in managed cleanup fixture (#3635)

The managed cleanup fixture added in 4512ddfaa places the
@gajae-code/natives namespace import after @gajae-code/utils, which
biome's assist/source/organizeImports rule rejects. That makes the
check:@gajae-code/coding-agent CI task (biome check . && bun run
check:types) exit 1 on current dev and on every PR rebased onto it.

Reorder the two imports so the module specifiers sort correctly. No
runtime or test behaviour changes.

Co-authored-by: GJC <gjc@example.test>

* fix(session): guard timeout force-abort without active run

A timed-out cancel-and-submit cleanup can have no registered attempt handle. Skip forced abort in that case while preserving the timeout outcome and recovery notice.

* fix(coding-agent): preserve Bash output and bound SDK ownership

* fix(coding-agent): assert goal_updated payload instead of emit arity (#3637)

The immutable per-attempt scope facility (#3608) routes every session
extension event through emit(event, undefined, deliveryScope). The goal
mode integration test still asserted the single-argument call shape via
toHaveBeenCalledWith, so it fails on current dev with "Number of calls: 2"
even though the product behaves correctly: goal_updated is session-level
and legitimately carries no attempt scope.

Assert the delivered payload rather than the call arity. The test now
locates the terminal goal_updated event the throwing hook received and
proves it carries goal.status === "complete" and state.mode === "exiting",
which is what the test's name actually claims. This is coupled to product
behaviour rather than to the emit signature: dropping state from the
goal_updated emit in agent-session.ts makes the new assertion fail.

No product change. expect() calls in the file go from 80 to 82.

Co-authored-by: GJC <gjc@example.test>

* fix(coding-agent): assert message-pipeline callback payloads instead of arity (#3641)

The per-attempt scope facility threads an extra callback-scope argument
through onPayload, onSseEvent, and extension emit. Three assertions in
agent-session-message-pipeline.test.ts pin the previous argument count
with toHaveBeenCalledWith, so they now fail on the trailing argument
even though every observable behaviour is unchanged.

Assert the payload that each hook actually receives instead of the
call arity:

- composes session payload hooks: assert the session hook sees the
  original payload and the request hook sees the session-transformed
  payload, which also pins the session-to-request threading order.
- records raw SSE diagnostics: assert the forwarded SSE event object.
- forwards stop reasons and reasoning summaries: locate the
  reasoning_summary_end call and assert both its event payload and
  that its continueWhile argument is a function.

Spy parameter types are widened so no cast is needed. Coverage goes up
rather than down: 21 pass / 3 fail becomes 24 pass / 0 fail, and
expect() calls rise from 84 to 91.

Co-authored-by: GJC <gjc@example.test>

* fix(coding-agent): AttemptScope post-merge compatibility repairs (#3592) (#3638)

Fix post-merge regressions from the AttemptScope facility merge (#3608):

1. forceAbort no longer requires logicalRunId — falls back gracefully
   when AttemptScope handle is not registered.
2. setAttemptRecordStore injection uses typeof guard for mock ExtensionRunners.
3. forceAbort overload simplified to single optional signature.
4. abort-timeout: tries managed logicalRunId then active, catches fallback.

External #3637 owns the goal-mode assertion fix; this branch does not
duplicate it.

Lore-id: attemptscope-postmerge-repair-v2
Tested: goal/cancel/retry/fallback/attemptscope/handoff/compaction suites pass
Confidence: high
Scope-risk: narrow
Reversibility: additive

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(coding-agent): tolerate run acceptance without a minted handle (#3645)

#3592's attempt-scope facility routes queued-followup continuations
through an onRunAccepted callback whose first statement dereferences the
handle. Integration doubles that accept a run without minting one make
that callback throw before it reaches the accounting reset, so the
queued-successor branch that calls resetAttemptBudget never runs.

Return early when no handle is supplied, keeping the previously recorded
run id. Real callers in #runLoop always mint a handle, so live behaviour
is unchanged.

This repairs the two remaining red tests in
agent-session-auto-compaction-continue.test.ts on current dev.

Co-authored-by: GJC <gjc@example.test>

* fix(coding-agent): use resolvable read spill fixtures

* fix(telegram): fence delete settlement to its dispatched epoch and keep rollback route-atomic

settleDelete() previously matched only topic id plus delete_pending, so a
held generation's definite remote result could settle a newer re-fenced
delete for the same session and release its topic-id quarantine. It now
requires the caller's dispatched authority epoch to equal both the
record's epoch and the registry's current epoch, and all four daemon call
sites capture that epoch before dispatching the remote delete.

restoreDeleteFence() reinstated a pending collision without rebuilding the
derived routing tables, and its narrow byTopic.delete never ran on the
absent-record branch that settlement creates. It now rebuilds inbound
routes after writing the epoch, so a restored fence re-quarantines the
topic id instead of leaving inbound routing open to the collision partner.

Settlement also rebuilds routes on success so a settled topic id becomes
adoptable and a surviving colliding record becomes routable immediately.

* fix(telegram): make delete settlement transactional and overflow-safe

Settlement is now a two-phase commit. `settleDelete` removes the record,
records the pending settlement, and retains the topic-id quarantine;
`commitSettledDelete` publishes routes only after the clear is durable, and
`rollbackSettledDelete` undoes it under a CAS on the exact post-settlement
state. Epoch advancement saturates at the safe-integer bound and a saturated
fence refuses settlement, so two generations can no longer collapse onto the
same double. The first revoked-lease compensation sets its completion flag only
after the durable commit, so a failed clear persist leaves the fence supervised
instead of stranding cleared memory against a `delete_pending` disk state.

* fix(telegram): retain refused delete settlements

* fix(telegram): fail closed at saturated authority epochs

* test(telegram): prove compensation replay after clear failure

* ci: retry unrelated Windows process-tree flake

* fix(async): harden terminal wait generations

Terminal waits and queued resumes must preserve immutable attempt identity across eviction, reuse, and resume. Generation-scoped delivery acknowledgement and duplicate disposition propagation prevent stale terminal evidence from crossing attempts.

Issue: #3472

Confidence: high

Scope-risk: wide

Reversibility: straightforward

Tested: affected-package typecheck and focused async/task/subagent suites

Not-tested: full repository gates

* fix(coding-agent): repaint async settings views (#3643)

* fix(models): correct lunamaxxing reasoning tiers (#3653)

Executor now uses xhigh and critic uses max, matching the intended
medium default / xhigh executor / max elsewhere mapping.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* style: format issue 3472 changes

Apply the repository Biome formatter to the four issue #3472 files required by CI. No behavioral changes.

Issue: #3472

Tested: bun run check (formatter gate passes; unrelated SDK closure fixture remains failing)

Not-tested: full repository gates remain blocked by unrelated sdk adapter fixture

* fix(telegram): reap abandoned publication staging temps

The notification self-heal reaper never claimed `.tmp` staging files, so every
failed publication left one unreachable file in the agent notifications
directory permanently.

`writeJsonAtomic` stages a sibling `<name>.<pid>.<epoch-ms>.<suffix>.tmp` and
renames it over the destination. If the staging write or the rename fails, or
the process dies between the two, that temp is never published and never read
again. No prefix in NOTIFICATION_LEAK_ARTIFACT_PREFIXES matched it, so the
reaper walked past it. This accumulates across the roots registry, daemon
state, callback aliases, seen-update ids, and the topic registry snapshot, and
is most visible where a rename-blocking condition persists (Windows EPERM from
an antivirus or indexer handle, EACCES, EIO, ENOSPC).

Reaping is shape-matched and still bounded by the existing five-minute mtime
grace window, so a temp an in-flight publication is still staging is never
removed. Fixing it in the reaper rather than the writer also reclaims temps
orphaned by a crash, which no writer-side unwind can reach, and leaves the
protected `writeJsonAtomic` lifecycle declaration byte-identical so no
DAEMON_GENERATION bump is required.

* fix(telegram): fence staging-temp reaping to dead publishers and exact identity

The staging-temp reaper decided purely on filename shape plus a
path-following mtime, so a live-but-blocked publisher whose temp aged past
the grace window could have its in-flight file deleted, and the
readdir/stat -> unlink gap allowed an ABA replacement or a symlink to be
destroyed by pathname.

Reaping now requires a proven-dead publisher and an identity-bound delete:

- The publisher pid is parsed from the writer name shape the writer already
  produces, and only a probe result of `dead` proceeds. `alive` and any
  indeterminate or throwing probe retain the file and count as skipped.
- Identity is captured no-follow via the existing `readEndpointFile` seam,
  multi-link files are rejected, and removal goes through `exactUnlink`,
  which verifies dev+ino+size+mtimeNs+sha256 before unlinking. The grace
  decision uses the captured `mtimeNs`, so the same inode is bound from
  decision through deletion.
- Missing seams retain rather than fall back to an unfenced unlink, and the
  staging quarantine name is added to the leak-artifact prefixes so a
  retained quarantine self-heals instead of becoming a new leak.

No protected declaration changes, so no DAEMON_GENERATION bump is required.

* fix(telegram): reap retained staging quarantine exactly

* test(telegram): prove retained quarantine self-heal

* fix(telegram): report retained cleanup truthfully

* test(telegram): assert source-native retained outcomes

* ci: retry timed-out Telegram generation guard

* chore: bump version to 0.12.6

* fix(coding-agent): validate todo completion targets (#3640)

* fix(utils): surface zero-result glob cancellation

* fix(tui): keep padded text within render width

* fix(shell): restore cross-platform process cleanup

The 0.12.6 native release exposed a Windows test cfg omission and macOS process cleanup regressions. Restore the Windows test lock, tolerate disappearing PIDs during macOS process-tree snapshots, and retain the guarded macOS identity check before signaling.

* chore: bump version to 0.12.7

* revert: drop the unpublished 0.12.6 and 0.12.7 release commits

CI failed for tag v0.12.6 and tag v0.12.7 was cancelled before its publish job ran, so neither version reached npm or a GitHub Release. Both tags are deleted; main is restored to the dev release base (80825a683) so 0.12.6 can be cut again from fixed sources.

* chore: bump version to 0.12.6

* fix(ci): canonicalize BigInt daemon guard declarations

* test(pi-shell): gate the POSIX group-leader cancellation test to unix

cancelling_after_group_leader_exit_fails_closed_without_anchor drives 'sh -c', background jobs, $! and POSIX process-group reaping. It never compiled on Windows before the PROCESS_TEST_LOCK cfg widened, and once it did it was flaky there (nextest TRY 1 fail / TRY 2 pass). Its unix-semantics neighbours are already cfg(unix); the platform-neutral admission-limit tests keep using the lock on Windows.

* fix(ci): stabilize daemon guard attestations

* fix(ci): version BigInt daemon guard repair

* fix(ci): align daemon guard manifest version

* fix(ci): accept versioned daemon guard repair

* docs: add community plugins and extensions

* docs: add community plugins and extensions

* docs: add SDK Extensions README section

Make the SDK extension surface explicit and describe gjc-remote without introducing a version-specific maintenance promise.

* docs: align SDK Extensions heading on main

* fix(pi-shell): stop retiring live pipeline process groups when the leader exits first

A second pipeline member joins the first member's process group. When the
fast-exiting leader died before the member's spawn callback validated the
group anchor, group_anchor_is_current/prune_group_anchors treated the group
as retired (OWNERSHIP_RETIRED_GROUP), killed the member, and failed the run
with 'Shell process ownership incomplete' (reason=13). A recorded, still-
running process whose live kernel pgid matches the anchor is exact proof the
group is still ours; leader liveness alone no longer decides ownership.

Adds a 40-iteration fast-pipeline regression test.

* fix(coding-agent): restore artifact references for lossless minimized bash saves

a98863712 downgraded every successful minimized-output artifact save to
'artifact save could not be verified in the current session', so no bash
result could ever carry a raw-output artifact reference. The downgrade is
only honest when the source capture itself was lossy; a lossless capture
with a verified save must surface the artifact footer again.

Moves the decision into minimizedSaveNotice where the final loss summary
is known: lossy source -> failure notice (unchanged, still pinned by the
source-loss tests); lossless complete save -> the raw-output artifact
footer; lossless capped save -> honest retained/omitted reference.

* Revert "Merge pull request #3631 from Yeachan-Heo/fix/issue-3469-review-source-snapshot"

This reverts commit 9ab1c27efb1a05d5b00d6c260106333bdb824967, reversing
changes made to e510bff006cecdab7f87fdb023cf8cd380b93501.

* revert: drop the 0.12.6 bash-output/SDK-ownership hardening (a98863712)

a98863712 shipped in 0.12.6 and broke three surfaces at once on non-Linux
hosts:

- pi-shell fail-closed process-group ownership rejected healthy fast
  pipelines ('Shell process ownership incomplete'), because a pipeline
  leader exiting before a later member's spawn callback retired the group;
- every successful minimized bash artifact save was downgraded to
  'artifact save could not be verified in the current session', so no
  truncated bash result could carry a raw-output reference;
- artifact/agent URL recovery required a Linux-only retained fs authority
  ('artifact_authority_unavailable'), and a single-use payload-bound proof
  token invalidated artifacts whenever the visible text was rewritten
  after binding.

Reverts the commit and restores crates/pi-shell, bash-executor, and their
tests to the 0.12.5 implementations. The later telegram generation-guard
bigint fix is preserved.

* fix(ci): align daemon guard declarations with the a98863712 revert

The guard's native authority list still named symbols that only existed in
the reverted 0.12.6 hardening (pi-shell process_group_members and the whole
shell.rs/brush-core-vendored observer surface), so manifest generation
failed closed. Restores the pre-a98863712 declaration set and regenerates
the manifest; the later BigInt/attestation guard fixes are preserved.

* chore: bump version to 0.12.7

* fix(pi-shell): fail closed when the process tree cannot be observed

0.12.7 reverted to the v0.12.5 discovery path, which conflates 'we could
not look' with 'there is nothing there': snapshot_all_pids returned an
empty Vec when proc_listallpids failed, build_process_tree returned an
empty map when the Toolhelp snapshot failed, and the Linux walk swallowed
unreadable /proc task lists. terminate_new_descendants then saw an empty
target set and returned early, reporting a clean cancel/timeout while
descendants stayed alive.

Discovery now distinguishes observation failure from an empty result:
descendants_observed / children_observed return None only when the tree
genuinely could not be read, while a pid that exits mid-walk stays an
expected race. add_new_descendants propagates that as a bool, and
terminate_new_descendants only takes the early-return shortcut when the
empty target set was actually observed.

This does not reintroduce the a98863712 fast-pipeline regression: no
process-group anchor is retired here, and the spawn path is untouched.
Verified 201/201 pi-shell tests, 25/25 fast pipelines and four other
pipeline shapes against a rebuilt darwin-arm64 binary, and a timed-out
command leaving zero leaked descendants.

* fix(pi-shell): close remaining fail-open gaps in process-tree observation

Follow-up to e862e49d8, addressing three HIGH findings from boundary review
generation 2.

1. Unproven baseline. current_descendant_pids() still unwrapped…
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