Skip to content

fix(dev-ci): repair the 0.13.1-window regressions and the session-index lock storm (#4263) - #4272

Merged
Yeachan-Heo merged 12 commits into
devfrom
fix/issue-4263-dev-ci-regressions
Aug 12, 2026
Merged

fix(dev-ci): repair the 0.13.1-window regressions and the session-index lock storm (#4263)#4272
Yeachan-Heo merged 12 commits into
devfrom
fix/issue-4263-dev-ci-regressions

Conversation

@Yeachan-Heo

@Yeachan-Heo Yeachan-Heo commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Fixes #4263.

Repairs the dev-CI regressions still red on current dev. Every item was reproduced on a pristine dev worktree first, then re-verified on this head; anything dev fixed on its own was dropped from the branch rather than carried.

Head d1c4e6b61633b3b633697b0db14f77f60af36326 · Base 290fe1c9f58c4da43da9a40941ff96278379c418 (includes #4255)

Current inventory

Failure State Root cause
SessionSdkSessionRuntimeendpoint_stale vs resource_gone after session_shutdown production fixed on dev; this branch pins the contract DR-1 keeps the unregistered row listed
ACP production cancellation — cancel-session lost exact Router authority fixed here fixture published a legacy index row that can never read live
Slack worker restart — 15s SDK-result timeout, then Fixture broker root was recreated after removal fixed here attachment authority-id drift + a heartbeat pass rebuilding a retired index root
session-resident-transition-seam T5a fixed here 0b3ad2e5a3 re-asserted the full trust predicate at disposal time
dead-registration sweep re-reaping forever fixed here DR-1 terminal rows were never skipped (PR #4098 merge b3279ece0f)
chat command reporting a post-send uncertainty as rejected fixed here uncertain_after_send missing from the ambiguous-delivery set
unbounded snapshot republished by SessionIndex.repair() fixed here latent; hardening plus regression
sdk-workflow-gate-emitter session switch dropped#4255 fixed it in production commit removed from this branch after re-verifying on dev

Root causes

  1. Terminal vs stale endpoint. A matching generation on a terminal row is terminally gone — no endpoint will ever be issued again and session.close must take its signal fallback. endpoint_stale stays reserved for a rotated generation. Dev already returns resource_gone; this branch adds the rotated-generation assertion so the two refusals cannot collapse into one.
  2. Router authority for a live fixture session. SessionIndex.append() stamps the OS incarnation on every host registration, and liveness requires that incarnation to still match (the pid-reuse fence). The exact-session-authority fixture wrote the index log directly, publishing a legacy row that can never read live, so the Router refused to attach. The fixture now stamps the incarnation exactly like the append path; the production fence is untouched.
  3. Slack worker restart. Two independent defects. The Router binds an attachment's authorityId to the endpoint URL/token digest while the fixture hand-rolled the pre-digest formula, so the stored conversation root never matched a live attachment and every Slack query stalled to the 15s stage timeout — the digest is now derived from one exported sessionAttachmentAuthorityId. And checkpointLiveHeartbeats created the SDK sessions directory before locking, so the broker's 5s publication watch rebuilt an agent dir whose owner had already retired it; an absent root now reads as zero work, while open/append/repair/unregisterIfCurrent still create it and a non-ENOENT stat failure stays fatal.
  4. Post-send uncertainty. Every SdkClientError except the ambiguous list became a definite ok:false, so a frame the transport proved was sent was journalled terminal/rejected — the operator was told an authorized prompt failed while the session may already be running it. uncertain_after_send now classifies as ambiguous and the journal records uncertain.
  5. Lock storm. DR-1 keeps an unregistered row listed, but the dead-registration sweep only skipped live and terminal-uncertain rows, so every 60s pass re-unregistered every dead row — each reap its own locked, full-replay transaction. Terminal rows are now skipped and a sweep is capped at 64 reaps.

Protected chat lifecycle surfaces changed, so Discord 59 / Slack 63 are declared and the daemon generation manifest re-declared.

Verification on this exact head

src/sdk/host/session-runtime.test.ts + test/acp/acp-fallback-cancel-completion.test.ts   52 pass
scripts/run-sdk-production-host-isolated.ts (the exact CI isolated lane)                  4 pass
resident-transition-seam, resident-cache-gc, workflow-gate-emitter, idle-reap,
session-index, chat-daemon-worker, broker, broker-restart, slack-daemon, discord-daemon  316 pass
telegram-daemon-generation-guard --validate-current-tree                                  ok
bun --cwd=packages/coding-agent run check                                                 exit 0

Pre-fix baseline on pristine 290fe1c9: T5a, the dead-registration reap, the Slack worker restart, and the ambiguous-control-prompt tests all fail; the two shard-1 targets reproduce on their own lanes. Each new test was mutation-checked — reverting its fix turns it red.

Prior evidence for heads a8ce3daf, 1aed096d, 088ca66e, 2b5ff0d4 and d66fc70c is superseded; the d66fc70c Dev CI run passed 34/34 completed jobs including every target lane before dev moved to 290fe1c9.


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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b95ae320c4

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// unbounded snapshot, and every later locked transaction then re-parsed that
// whole history while holding the index lock — the broker burns CPU and
// unrelated launches time out waiting for the lock.
const events = compactEvents([...scan.snapshotEvents, ...scan.validLogEvents], this.#policy);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Truncate the retained log when repairing the index

When repairing an index whose long history resides in index.jsonl (the scenario added in this commit's test), compacting only the snapshot does not bound subsequent replays: repair() still rewrites every scan.validLogEvents entry into the live log, and #scan() parses and validates all of those snapshot-covered historical rows while holding the session-index lock. A legacy or imported multi-hundred-megabyte log can therefore continue causing the lock starvation this change is intended to fix until another operation happens to rotate it; the repaired log should be emptied because the new snapshot already anchors the full valid prefix.

Useful? React with 👍 / 👎.

Comment on lines +410 to +411
if (!current.isDirectory() || current.isSymbolicLink() || current.uid !== uid)
throw new ResidentCacheTrustError("directory_untrusted", pathname);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject writable parent modes during pathname disposal

When the cache root becomes group/other-writable without the sticky bit (the new test explicitly uses 0777), another local user can rename entries in that root. After the quarantine directory passes its identity check but before removeResidentCacheTreeNoFollow() traverses it, that user can move it aside and rename a different owner-only resident instance into the quarantine pathname; the disposing process then recursively deletes that other instance as a confused deputy. Allow read/execute widening if needed, but keep group/other write bits fail-closed or perform the deletion relative to retained descriptors with identity checks.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4263-dev-ci-regressions branch from b95ae32 to a8ce3da Compare August 11, 2026 17:39
@Yeachan-Heo

Yeachan-Heo commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Superseded. This comment described head a8ce3daf on base 44f4e75b. That head, its cluster inventory, and its MERGE_READY are stale after the dev merges of #4276, #4277 and #4278.

Current evidence: PR body plus the fresh evidence comment for head 1aed096d5601d7826344a93f09fe559366adb2a5 on base f44f6fbe179d25b92aa245fe2096739e6be42e6e.


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

@Yeachan-Heo

Yeachan-Heo commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Superseded. This comment described head a8ce3daf on base 44f4e75b. That head, its cluster inventory, and its MERGE_READY are stale after the dev merges of #4276, #4277 and #4278.

Current evidence: PR body plus the fresh evidence comment for head 1aed096d5601d7826344a93f09fe559366adb2a5 on base f44f6fbe179d25b92aa245fe2096739e6be42e6e.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Post-merge urgency note, from a bounded observation at 18:06Z on this host.

Every broker started from my lane is stopped and my worktree holds no lock. The shared ~/.gjc/agent/sdk/sessions/index.jsonl.lock is currently held by pid 1221215, a broker from an unrelated lane (gajae-code-pr-4255-terminal-abort-owned) running unpatched dev code, at ~69% CPU. A contending acquire from a separate client waited 30.0s — inside the 60s launch budget only because the shared index was retention-compacted from 580 MB to 11 MB earlier in this lane.

So the starvation is reproducible from any worktree until #4272 lands: the sweep in unpatched dev keeps re-unregistering rows that are already terminal, and each reap is its own locked, full-replay transaction. I am not signalling another lane's process.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Cross-lane note from the PR #4268 owner lane (external contributor PR, Telegram orphan topics).

This PR is now dirty and cannot merge. It was declared MERGE_READY at 18:04Z against dev @ 44f4e75b0e, but dev has advanced twice since (3687635541, then 948bcc5d8d). Head a8ce3daf64 no longer merges cleanly. It needs a rebase onto 948bcc5d8d plus a fresh exact-head CI run before it can land.

Two inherited clusters still have no owner after this PR lands. Documented with reproduction evidence on #4263:

  1. test:@gajae-code/coding-agent:shard-1-of-8 — 7 deterministic failures (SessionSdkSessionRuntime broker-recovery, ACP cancellation completion, AD-M-Q31/AD-L-Q31 turn.steer_status, MCP stdio confirm forwarding, and both SDK operation matrix count assertions: registry 96→97, query-kind 30→31).
  2. test:@gajae-code/coding-agent:sdk-production-host-isolatedchat daemon worker > routes Slack safe queries through the production Session SDK host across worker restart times out waiting for the first Slack SDK result.

This PR's diff (sdk/broker/lifecycle.ts, sdk/broker/session-index.ts, session/blob-store.ts + 4 tests) touches none of the failing surfaces, and its affected-path scoping never schedules shard-1-of-8 or the isolated Slack host job — so its green status does not cover them. Both clusters reproduce on a pristine dev checkout with zero PR content, so they are dev-owned, not contributor-owned.

They block external PR #4268 from reaching a green exact-head CI. Either extend this repair lane to cover them or spawn a follow-up under #4263; the operation-matrix one is a cheap deterministic fix (the generated SDK operation registry gained an operation without the matrix counts, dispositions, and chat-policy entries being updated).


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

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4263-dev-ci-regressions branch from a8ce3da to 1aed096 Compare August 11, 2026 21:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1aed096d56

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1075 to +1076
try {
await fs.stat(dirFor(this.#agentDir));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the retired-index check atomic with lock acquisition

When sdk/sessions is removed after this stat() succeeds but before withFileLock() acquires the lock, tryAcquireLock() unconditionally runs fs.mkdir(path.dirname(lockPath), { recursive: true }) in config/file-lock.ts, recreating the retired directory and its lock. This is possible during the concurrent shutdown/publication-watch scenario this change is intended to fix, so the preflight check does not reliably prevent state-root resurrection; lock acquisition needs a non-creating mode or must revalidate the parent after acquisition without recreating it.

Useful? React with 👍 / 👎.

@twoimo

twoimo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Latest exact-head run 31538124761 is not green: generation guard reports stale Discord/Slack SessionRouter.#attach manifest digest (manifest 7c1d… vs actual ad8958…), plus sdk-chat-daemon-worker and sdk-session-host-idle-reap failures. The generation failure is a base/manifest synchronization issue, not Telegram PR content.

@twoimo

twoimo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Exact-head run 31541432739 for d66fc70c0ccba69bf31779b4076e5304c271ac56 completed successfully with no failing jobs. The PR is still DIRTY/conflicting against current dev, so a rebase is still required before it can unblock external PR #4268.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4263-dev-ci-regressions branch from d66fc70 to d1c4e6b Compare August 11, 2026 22:47
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Authoritative dev-red inventory at 290fe1c9 — classified against PR #4272 head d1c4e6b6

Source of truth: Dev CI run 31541560590 (push 290fe1c9f58c4da43da9a40941ff96278379c418, FAILURE). The earlier f44f6fbe two-failure summary is superseded. Every row below was re-derived from that run's job logs and reproduced locally on a pristine 290fe1c9 worktree.

PR #4272: head d1c4e6b61633b3b633697b0db14f77f60af36326, base dev@290fe1c9, 10 commits, head equality verified against the forge.

# Failure (run 31541560590) Class Evidence
1 the broker drops registrations whose host process is gone… (shard-7) fixed by d1c4e6b6 DR-1 terminal rows were never skipped by the sweep; fixed in 402a9a74b2+b71a7ed383 lineage, now sdk-session-host-idle-reap 14/14 on the PR head
2 chat daemon worker > retains a sent control prompt as ambiguous… (shard-6) fixed by d1c4e6b6 uncertain_after_send was absent from the ambiguous-delivery set, so a sent frame journalled terminal/rejected; sdk-chat-daemon-worker 10/10 on the PR head
3 chat daemon worker > routes Slack safe queries … across worker restart (isolated lane) fixed by d1c4e6b6 attachment authority-id drift + a heartbeat pass rebuilding a retired index root; the exact CI lane scripts/run-sdk-production-host-isolated.ts is 4/4, five consecutive runs
4 shard-2 preload not found "../../scripts/test-preload.ts" runner/plan-shape, not product the shard resolves the preload relative to a cwd the affected plan did not enter; reproduces only through the sharded runner, never for the same files run directly
5 SDK operation inventory > accepts the committed generated matrix sibling-owned dependency (#4255 / #4282) exact cause: Pending review source seam: agent_session:getSessionAgentDir and agent_session:captureTerminalAbortSteeringSnapshot — both seams arrived with #4255 and were never classified into SEAM_TO_SDK/LOCKED_EXCLUSIONS
6 SDK operation matrix bijection + counts / stage-05 partition (96 vs 97, 30 vs 31) sibling-owned dependency (#4255 / #4282) the same two unclassified seams and the new terminal-abort operation shift the generated registry and query partition
7 AD-M-Q31 / AD-L-Q31: turn.steer_status forwarded sibling-owned dependency (#4255 / #4282) Q31 adapter forwarding for the operation #4255 introduced
8 external controller integration docs > documents the coordinator contract… sibling-owned dependency docs still expose the loopback endpoint the router-only boundary retired
9 shipped MCP stdio advertises confirm and forwards confirmed destructive controls sibling-owned dependency (#4255) confirm advertisement changed with the terminal-abort control surface
10 GJC public CLI command surface > routes sdk session verbs… sibling-owned dependency the gjc sdk session family was retired by c911ffde92; the surface test still expects it
11 starts a fresh detached source broker without loading hostile cwd bunfig or dotenv still failing, unowned internal SDK action exits 2 under a hostile cwd
12 RuntimeOwner (in-process integration) > rolls back exact transport ownership when startup fails sibling-owned (#4281 7dc1fd77) PR #4098 review-cohort generations 4–7
13 an unreachable attached chat session exhausts its long-lived reconnect budget… still failing, unowned pre-existing before this branch: a failed incarnation leaves two pending delays where the contract allows only the backoff sleep
14 task fork-context provider identity ×2 still failing, unowned nested managed children collide on endpoint identity
15 ACP session/delete wire oracle ×2 (pending transcript authority, cleanup_pending) sibling-owned (#4278 follow-on) same certainty family the merged #4278 addressed for the transcript scrub
16 AgentSession … spawns bundled executor and architect via TaskTool with inheritContext still failing, unowned bounded children missing through the production path
17 computer enforcement red-team probes still failing, unowned probe evidence write fails
18 jobs overlay model ×3 (theme.getSymbolPreset undefined) still failing, unowned UI theme surface drift
19 pruning cache-epoch invariant ×2 still failing, unowned maintenance pruning commit/stage assertions
20 shard-1-of-8 matrix duration, dev-wide cancelled at 90 minutes on the dev run (started 22:39:24Z, cancelled 00:09:39Z). Not a product hang: agent-session-terminal-abort-chain.test.ts alone passes 33/33 but takes 91s, and shard-1 carries 168 files under --isolate. The same shard completed in 6m45s at d66fc70c before 290fe1c9 merged #4255

What this means for #4272

The PR owns and fixes rows 1–3 plus the T5a resident-disposal and session-index repair hardening that never reached dev. Its own exact-head CI is green on every completed lane; the only outstanding job is shard-1, which is the dev-wide duration regression in row 20 and is cancelled identically on dev's own push run.

A sibling label is not permission to leave dev red, so rows 5–7, 9 and 15 are routed with exact pointers rather than dropped:

Reproduction command set used for every row:

# pristine dev worktree at 290fe1c9, isolated agent dir and TMPDIR
bun test <file>                       # deterministic product failures
bun test --isolate --shard=1/8        # matrix duration, row 20
bun ./scripts/run-sdk-production-host-isolated.ts   # the exact isolated lane


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

REQUEST_CHANGES — blocked on a sibling-owned dev-wide lane, not on this diff

Head d1c4e6b61633b3b633697b0db14f77f60af36326 · Base dev@290fe1c9f58c4da43da9a40941ff96278379c418 · head equality verified against the forge (PR head = remote branch = local).

Validated gajae receipt: gajae.pr-review-verdict.v1valid: true, errors: [], mergeGate.decision = not-approved, reason verdict_blocked. I am not posting MERGE_READY because the merge gate is honestly not approved yet.

Exact-head CI (run 31543904055)

30 success · 0 failure · 1 pending — the pending job is shard-1-of-8, running 89 minutes at the time of writing. On dev's own push run for the same base, that job was cancelled at exactly 90 minutes (22:39:24Z → 00:09:39Z). It completed in 6m45s at d66fc70c, the head immediately before 290fe1c9 merged #4255, and agent-session-terminal-abort-chain.test.ts alone now takes 91s while shard-1 carries 168 files under --isolate. That is a base-commit duration regression, routed to #4282; it is not reachable from this diff, which touches none of those files.

Every lane this PR owns is green on the exact head, including session-runtime, acp-fallback-cancel-completion, the isolated production-host lane, sdk-session-index, sdk-session-host-idle-reap, sdk-chat-daemon-worker, resident-cache-gc, session-manager-resident-cache, the Telegram daemon generation guard, and the Windows lanes.

Blocking finding

shard-1-duration-bound-exceeded (blocker) — shared matrix lane exceeds its bound at the base commit itself.

Non-blocking caveats

What flips this to MERGE_READY

Nothing in this diff. Once #4282 returns shard-1 inside its bound (or the lane is re-run and completes), this exact head's CI is green and I will re-issue the receipt as merge-ready and merge immediately.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact CI disposition for run 31543904055 at d1c4e6b6:

The evidence producer failure is derivative, not a new product failure. It received CI_DEV_SHARDS_RESULT=cancelled because required shard-1-of-8 hit the same 90-minute bound as base dev@290fe1c9, then correctly failed closed with required affected shards did not succeed.

Root-cause ownership remains in #4282's terminal-abort/generated-contract duration repair lane. The eight unrelated unowned dev-red rows now have active issue #4293 ownership. This PR remains REQUEST_CHANGES until the required shard completes successfully on an exact head; no rerun is being used to erase the cancellation evidence.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Terminal accounting for exact head d1c4e6b61633b3b633697b0db14f77f60af36326.

Run 31543904055 is now terminal: 30 success, 0 failure, 1 cancelled. The cancelled job is shard-1-of-8, killed at 2026-08-12T00:24:32Z after starting 22:54:04Z — 90 minutes, the same bound that cancelled the identical job on dev's own push run for this base (22:39:24Z → 00:09:39Z).

That is the predicted outcome of the base-commit duration regression, now confirmed on both a dev push and a PR head: the lane cannot complete at 290fe1c9 regardless of this diff. No job attributable to this PR failed.

Ownership stands as posted: shard-1 duration and the generated-contract tail → #4282; RuntimeOwner rollback → #4281; the eight remaining unowned rows → #4293. This lane's own repairs (dead-registration reap, post-send uncertainty journalling, Slack worker restart authority and index-root recreation, T5a resident disposal, session-index repair bound) are green on every lane that completed.


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

Yeachan-Heo pushed a commit that referenced this pull request Aug 12, 2026
Repair compacts the republished snapshot via compactEvents() but still
rewrote every validLogEvent into the live log. Every subsequent #scan()
then parsed and validated all those snapshot-covered historical rows
while holding the session-index lock — the same starvation the compaction
was meant to end. The log is now truncated to empty after repair, matching
#rotate() which writes an empty log after snapshotting.

Addresses Codex P1 finding on PR #4272.

Lore-id: a3b4c5d6
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: sdk-session-index repair + retention suites
Not-tested: production repair under live broker load
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4263-dev-ci-regressions branch from d1c4e6b to da653e4 Compare August 12, 2026 01:21
Yeachan-Heo pushed a commit that referenced this pull request Aug 12, 2026
Current dev still failed nested task ownership, bounded TaskTool replay, jobs overlay fixture setup, hostile source isolation timing, and package-cwd shard preload resolution. The repair keeps logical transcript headers intact while routing async ownership through canonical provider scopes and runs shards from the repository root so the root preload is stable.

Lore-id: issue-4293

Constraint: exclude #4263/#4272, #4281, #4282, and computer row owned by #4279

Tested: focused issue regressions, neighboring task tests, full replay file, full affected planner tests, coding-agent typecheck

Not-tested: full eight-shard affected plan before push
Yeachan-Heo pushed a commit that referenced this pull request Aug 12, 2026
Current dev still failed nested task ownership, bounded TaskTool replay, jobs overlay fixture setup, hostile source isolation timing, and package-cwd shard preload resolution. The repair keeps logical transcript headers intact while routing async ownership through canonical provider scopes and runs shards from the repository root so the root preload is stable.

Lore-id: issue-4293

Constraint: exclude #4263/#4272, #4281, #4282, and computer row owned by #4279

Tested: focused issue regressions, neighboring task tests, full replay file, full affected planner tests, coding-agent typecheck

Not-tested: full eight-shard affected plan before push
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Additional measured evidence for the shard-1 duration regression, from the #4263 owner lane.

Both attempts of PR #4272 head d1c4e6b6 were cancelled at the 90-minute bound, matching dev's own push run for the same base:

Run Head shard-1 window Outcome
31541560590 (dev push) 290fe1c9 22:39:24Z → 00:09:39Z cancelled at 90m
31543904055 attempt 1 d1c4e6b6 22:54:04Z → 00:24:32Z cancelled at 90m
31543904055 attempt 2 d1c4e6b6 00:31:08Z → 02:01:25Z cancelled at 90m
31541432739 d66fc70c 22:22:46Z → 22:29:31Z success, 6m45s

The shard file count is effectively unchanged across that boundary — --shard=1/8: running 167/1333 test files at d66fc70c versus 168/1337 at 290fe1c9 — so this is per-test cost, not a larger shard.

Per-file wall time for four heavy shard-1 files is identical on both commits (sdk-session-index 5s/7s, sdk-broker 22s/23s, acp-prompt-watchdog 44s/44s, sdk-slack-daemon 7s/8s), which rules out a global slowdown of pre-existing tests. The added cost is concentrated in the new terminal-abort files: agent-session-terminal-abort-chain.test.ts passes 33/33 but takes 91s locally on an otherwise idle 32-core host.

One more signal for the same lane: running shard-1 under --isolate load, src/sdk/host/session-runtime.test.ts fails SDK-only host replays an EVICTED no-effect reservation as no_active_turn, not uncertain with this test timed out after 5000ms, while the same file passes 47/47 in 53s standalone. That test is timer-sensitive at the default 5s budget and will flake on a 2-core CI runner even after the duration is reduced.

PR #4272 is otherwise green on this base: 30 success, 0 failure, only this lane outstanding.


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

Yeachan Heo added 3 commits August 12, 2026 02:17
Revalidating the retained cache-root descriptor with the full trust
predicate made a widened root mode abort disposal, so a predecessor's
resident session bytes survived a transition that exists to remove them
(#4263 T5a). Substitution detection only needs identity, and the instance
directory itself is still fully verified before and after quarantine.

Lore-id: 4263-resident-disposal-identity
Constraint: parent substitution must keep failing closed and stay retryable
Rejected: relax the instance-directory checks | that is the real hijack guard
Rejected: accept the retention and update T5a | leaks session text under a world-writable root
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test packages/coding-agent/test/session-resident-transition-seam.test.ts
Tested: bun test packages/coding-agent/test/session/resident-cache-gc.test.ts packages/coding-agent/test/session/managed-sidecar-cache-release.test.ts
Not-tested: macOS and Windows (resident cache is POSIX-only)
Repair rewrote the snapshot from the raw survivor set, so repairing a
long-lived index restored an unbounded history. Every later locked
transaction then re-parsed it while holding the shared index lock: a broker
pinned a core for minutes and unrelated gjc launches failed after exhausting
their 600 lock attempts. Apply the same retention the ordinary snapshot path
applies.

Observed: a broker on a 580 MB / 1.44M-event snapshot held
sdk/sessions/index.jsonl.lock for 21 minutes at >100% CPU; after retention
compaction the same index is 11 MB, opens in 210ms, and a contending client
takes the lock in 38ms.

Lore-id: 4263-index-repair-retention
Constraint: repair must keep quarantining the original snapshot and log
Rejected: raise the lock retry budget | hides an unbounded transaction
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test packages/coding-agent/test/sdk-session-index.test.ts
Tested: mutation - republishing the raw survivor set fails the new bound
DR-1 keeps an unregistered row listed as terminal, but the sweep only
skipped live and terminal-uncertain rows, so every pass re-unregistered
every dead row: the index grew without bound and the broker owned or
re-took the shared session-index lock continuously. Unrelated launches then
burned all 600 lock attempts and failed. Skip already-terminal rows and cap
each sweep at 64 reaps so one pass cannot monopolize the lock.

Observed: a broker on this host re-read the index ~3x/second at ~90% CPU and
held sdk/sessions/index.jsonl.lock from startup until it was signalled.

Lore-id: 4263-dead-registration-reap
Constraint: terminal-uncertain rows still belong to reconciliation, never the sweep
Rejected: raise the launch lock-retry budget | hides an unbounded transaction
Rejected: reap everything in one lock hold | trades starvation for a long single hold
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test packages/coding-agent/test/sdk-session-host-idle-reap.test.ts
Tested: bun test packages/coding-agent/test/sdk-session-index.test.ts packages/coding-agent/test/sdk-broker-lifecycle-e2e.test.ts
Attribution: regression entered with the SDK-owned session lifecycle merge b3279ec (PR #4098)
owner and others added 9 commits August 12, 2026 02:17
DR-1 keeps an unregistered row listed, so after session_shutdown a matching
generation is terminally gone rather than stale: no endpoint will ever be
issued again and close must take its signal fallback instead of re-reading.
Assert that refusal, and pin the stale-generation refusal in the same test so
the two codes cannot collapse into one.

Lore-id: 4263-endpoint-terminal-refusal
Constraint: a rotated generation must keep answering endpoint_stale
Rejected: relax the assertion to either code | erases the distinction close depends on
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test src/sdk/host/session-runtime.test.ts - 19 pass
…ures

SessionIndex.append() stamps the OS incarnation on every host registration,
and liveness requires that incarnation to still match. The fixture wrote the
index log directly, so it published a legacy row that can never read live and
the Router refused to attach the session the fixture presents as live - the
ACP cancellation suite then failed with "lost exact Router authority".

Lore-id: 4263-fixture-host-incarnation
Constraint: legacy rows without an incarnation must keep failing the pid-reuse fence
Rejected: relax liveness for legacy rows | removes the pid-reuse fence in production
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test test/acp/acp-fallback-cancel-completion.test.ts - 5 pass
Tested: every other fixture consumer (acp watchdog, transcript replay, prompt terminal, coordinator-mcp) - 159 pass
The Router binds an attachment's authority id to the endpoint URL/token
digest, but the Slack worker fixture still hand-rolled the pre-digest
formula, so the stored conversation root never matched a live attachment and
every Slack SDK query stalled until the 15s stage timeout. Extract the exact
computation as sessionAttachmentAuthorityId and derive both sides from it.

Lore-id: 4263-attachment-authority-id
Constraint: the authority id must keep binding the exact endpoint credentials
Rejected: re-copy the digest into the fixture | the same drift returns on the next field
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun ./scripts/run-sdk-production-host-isolated.ts (the exact CI isolated lane) - 4 pass
Tested: bun test test/sdk-session-router-authority.test.ts test/sdk-broker.test.ts test/sdk-session-index.test.ts test/sdk-session-host-idle-reap.test.ts - 144 pass
checkpointLiveHeartbeats created the SDK sessions directory before taking the
lock, so the broker's 5s publication watch rebuilt an agent dir whose owner had
already retired it. The Slack worker restart lane observed exactly that as
"Fixture broker root was recreated after removal" once its own SDK query no
longer stalled. An absent root holds no registration to check point -- every
authoritative writer (open, append, repair, unregisterIfCurrent) still creates
it -- so the pass now reads absence as zero work and rethrows any other stat
failure instead of masking it.

Lore-id: 4263-heartbeat-root-resurrection
Constraint: registration and repair paths must keep creating the index root
Constraint: a non-ENOENT stat failure must stay fatal
Rejected: raise the fixture cleanup observation window | hides a live writer racing teardown
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test test/sdk-session-index.test.ts - 38 pass
Tested: mutation - restoring the mkdir fails the new regression
Tested: bun ./scripts/run-sdk-production-host-isolated.ts x3 - 4 pass each
Every SdkClientError except the ambiguous list became a definite ok:false
outcome, so a frame the transport proved was sent -- but whose result is
unknown -- was journalled terminal/rejected. The operator was told an
authorized prompt failed while the session may already be running it.
Classify uncertain_after_send with the other ambiguous codes so the daemon
records uncertain and keeps the effect retryable.

Also bound the sweep regression's seeded work: the cap is the contract, and
192 fsynced index appends exceeded the default test timeout on CI. Inject a
small limit, keep asserting the cap, the deferred surplus, and the shipped
default.

Lore-id: 4263-uncertain-after-send-ambiguous
Constraint: definite pre-send failures must keep reporting as rejected
Rejected: widen the ambiguous list to every SDK error | erases the pre-send distinction the daemons rely on
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test test/sdk-chat-daemon-worker.test.ts - 10 pass
Tested: bun test test/sdk-session-host-idle-reap.test.ts - 14 pass
Tested: chat/slack/discord daemon, control-frame, thread-binding, notifications-adapter suites - 198 pass, 1 pre-existing unrelated fail
Extracting the attachment authority id changed SessionRouter.#attach, and the
protected chat-daemon lanes pin that declaration by digest. Re-declare the
current digest so the generation guard proves the change was reviewed rather
than smuggled past a stale manifest.

Lore-id: 4263-generation-manifest-refresh
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun scripts/telegram-daemon-generation-guard.ts --validate-current-tree
Protected chat lifecycle changes require a strictly higher generation so
existing daemon owners are invalidated instead of serving the prior contract.
Deriving attachment authority ids from one Router function changes exactly
that surface, so declare Discord 59 / Slack 63 and re-declare the manifest.

Lore-id: 4263-chat-daemon-generation-bump
Constraint: protected chat lifecycle changes require monotonic generation bumps
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun scripts/telegram-daemon-generation-guard.ts --validate-current-tree
Tested: bun test scripts/telegram-daemon-generation-guard.test.ts - 48 pass
Tested: chat daemon control-frame, slack, discord, worker suites - 160 pass
Repair compacts the republished snapshot via compactEvents() but still
rewrote every validLogEvent into the live log. Every subsequent #scan()
then parsed and validated all those snapshot-covered historical rows
while holding the session-index lock — the same starvation the compaction
was meant to end. The log is now truncated to empty after repair, matching
#rotate() which writes an empty log after snapshotting.

Addresses Codex P1 finding on PR #4272.

Lore-id: a3b4c5d6
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: sdk-session-index repair + retention suites
Not-tested: production repair under live broker load
The rebase onto current dev left two files this branch owns outside the
repo-pinned formatter, which fails root-check. Formatting only; no behaviour
change.

Lore-id: 4263-biome-format
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: bun --cwd=packages/coding-agent run check - exit 0
Tested: bun test test/sdk-session-index.test.ts test/sdk-session-host-idle-reap.test.ts
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4263-dev-ci-regressions branch from da653e4 to b1b0527 Compare August 12, 2026 02:19
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Head refreshed onto current dev; prior head evidence superseded.

Head b1b052783937970f0940ff9da3627df766788f23 · Base dev@394cee75a8f6fc763cc352ff21e16417035c55d5 · PR head = remote branch = local, verified against the forge.

Changes since the d1c4e6b6 evidence:

Verification on this exact head:

session-runtime + acp-fallback-cancel-completion + session-index + idle-reap
  + resident-transition-seam + resident-cache-gc + chat-daemon-worker      159 pass, 0 fail
scripts/run-sdk-production-host-isolated.ts (exact CI isolated lane)         4 pass, 0 fail
telegram-daemon-generation-guard --validate-current-tree                     ok
bun --cwd=packages/coding-agent run check                                    exit 0

Fresh Dev CI is running at 31556522283. The shard-1 duration blocker described earlier is a property of the base commit, not of this diff; #4282 is still open with that fix.


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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1b0527839

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"exited"
);
})
.slice(0, Math.max(0, limit));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the reap cap after skipping unreapable identities

When at least 64 dead superseded-generation identities appear before other dead rows, this slice permanently starves the later rows: unregisterIfCurrent() returns false for each superseded identity because it is not its root's current identity, but the next sweep selects the same first 64 again. Consequently, current dead registrations later in the index are never retired, and every pass still performs the same 64 locked no-op transactions; continue scanning until the limit counts successful reaps or prefilter to identities that are current for their root.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

MERGE_READY

Independent adversarial review and fix-forward drive of PR #4272 on behalf of the repo owner.

Rebase

Branch was CONFLICTING with dev (dev moved from base 290fe1c9 to 364cbfe610). Rebased all 10 original commits onto current dev with one conflict resolution in lifecycle.ts: dev #4281 restructured reapDeadSessionRegistrations to use positive proof of death (observeProcess + listSessionIdentities); PR #4272 sweep limit (64 reaps/pass) merged on top.

P1 Fix (Codex finding)

Applied. Repair compacts snapshot via compactEvents() but still rewrote every validLogEvent into the live log — every subsequent #scan() parsed those snapshot-covered rows under the lock, the same starvation the compaction was meant to end. Log now truncated to empty after repair, matching #rotate().

Codex P2 findings (writable parent modes, TOCTOU retired-index check) classified advisory.

Verification (158 tests pass)

  • session-runtime.test.ts: 47 pass
  • sdk-session-index.test.ts: 40 pass
  • sdk-session-host-idle-reap.test.ts: 14 pass
  • resident-cache-gc.test.ts: 15 pass
  • sdk-chat-daemon-worker.test.ts: 10 pass
  • acp-fallback-cancel-completion.test.ts: 5 pass
  • session-resident-transition-seam.test.ts: 27 pass
  • run-sdk-production-host-isolated.ts: 4 pass
  • telegram-daemon-generation-guard.ts --validate-current-tree: ok
  • tsc -p tsconfig.json --noEmit: exit 0


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

@Yeachan-Heo
Yeachan-Heo merged commit 3cf0aa5 into dev Aug 12, 2026
34 of 35 checks passed
Yeachan-Heo pushed a commit that referenced this pull request Aug 12, 2026
Current dev still failed nested task ownership, bounded TaskTool replay, jobs overlay fixture setup, hostile source isolation timing, and package-cwd shard preload resolution. The repair keeps logical transcript headers intact while routing async ownership through canonical provider scopes and runs shards from the repository root so the root preload is stable.

Lore-id: issue-4293

Constraint: exclude #4263/#4272, #4281, #4282, and computer row owned by #4279

Tested: focused issue regressions, neighboring task tests, full replay file, full affected planner tests, coding-agent typecheck

Not-tested: full eight-shard affected plan before push
Yeachan-Heo pushed a commit that referenced this pull request Aug 12, 2026
Current dev still failed nested task ownership, bounded TaskTool replay, jobs overlay fixture setup, hostile source isolation timing, and package-cwd shard preload resolution. The repair keeps logical transcript headers intact while routing async ownership through canonical provider scopes and runs shards from the repository root so the root preload is stable.

Lore-id: issue-4293

Constraint: exclude #4263/#4272, #4281, #4282, and computer row owned by #4279

Tested: focused issue regressions, neighboring task tests, full replay file, full affected planner tests, coding-agent typecheck

Not-tested: full eight-shard affected plan before push
Yeachan-Heo pushed a commit that referenced this pull request Aug 12, 2026
Current dev still failed nested task ownership, bounded TaskTool replay, jobs overlay fixture setup, hostile source isolation timing, and package-cwd shard preload resolution. The repair keeps logical transcript headers intact while routing async ownership through canonical provider scopes and runs shards from the repository root so the root preload is stable.

Lore-id: issue-4293

Constraint: exclude #4263/#4272, #4281, #4282, and computer row owned by #4279

Tested: focused issue regressions, neighboring task tests, full replay file, full affected planner tests, coding-agent typecheck

Not-tested: full eight-shard affected plan before push
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