chore: port clean memory-guard successor - #8
Conversation
…ispose flake (Yeachan-Heo#3144) * test(sdk): make teardown ordering witness production-observable The Phase 2 rewrite of "session teardown drains admitted direct gate resolution" was accepted with a narrowed claim because a mutation probe survived: replacing `await rt.waitForGateResolutionQuiescence()` with `void ...` still passed. The test fully mocked `resolveGate`, so the delayed operation never touched the real terminal controller, and it released the resolver after a single setImmediate while `stopSession()` was independently awaiting the native `pushFrameAndWait(session_closed)` barrier — resolution therefore completed before teardown reached detachment even without the quiescence await. Now the test calls through the original `registerGateTerminalController` and the original `resolveGate` (wrapping the latter only with a deferred pre-terminalization gate), makes `pushFrameAndWait(session_closed)` an explicit test-controlled pre-drain barrier, asserts the controller is still attached at the quiescence point, then observes real accepted terminalization and gate continuation before detachment. Mutation-proved: the void-await mutation now FAILS at the pre-detachment assertion (controllerAttached false). Passes 10/10 unmutated. * test(runtime): poll for the TERM marker in the concurrent-dispose redteam Caught live by the stabilization soak: main-nontag rehearsal run 30149261910 failed on shard 11 with (fail) process-lifecycle adversarial owned-process invariants > double and concurrent dispose share one settled result and issue one terminating signal expect(received).toHaveLength(expected) Expected: 1 Received: 0 The child's TERM trap appends its marker asynchronously (`trap 'echo term >> $tmp; exit 0' TERM`), so under shard load `awaitExit` can return before that write lands and the single-sample read observes an empty file. The file already has a `waitForAsync` helper for exactly this shape; the marker assertion just wasn't using it. Polls for the single terminating signal before asserting, preserving the original invariant (exactly one `term` line — not "at least one"). Verified: 15/15 reruns, 4x parallel contention clean, whole file 9/9, typecheck and biome clean. This test was NOT in the 62-suspect audit shortlist: it did not fail or retry during the mined two-week window, so it is a genuinely new observation the soak surfaced. --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
`sh` runs a TERM trap only after the current foreground command returns. With the child looping on `sleep 1` and `gracefulMs: 500`, dispose could escalate to SIGKILL before the handler wrote its `term` marker, so the one-terminating-signal assertion saw an empty file (observed in CI run 30149261910, coding-agent shard 11). Shorten the loop interval to 0.05s and use the module's own `DEFAULT_GRACEFUL_MS` (2000ms) so the trap has a deterministic window. Under 18-worker CPU contention the old shape wins the race 8/20; the new shape wins 20/20. All assertions are unchanged. Also record the missing `## [Unreleased]` changelog entries for Yeachan-Heo#3109, Yeachan-Heo#3127, and Yeachan-Heo#3131, found while auditing release scope after v0.11.9.
…turns (Yeachan-Heo#3147) Lifecycle `session_shutdown` started `controller.stopCurrentSession(ctx)` but discarded the promise, awaiting only `stopSession(id)`. Once startup has settled the host is broker-visible and can accept `session.close` while the startup handler's post-start `reconcileCurrentSession` is still running. That reconciliation can mint a replacement notification-root token; `ensureTelegramDaemon` then unregisters it asynchronously. Shutdown could therefore return — and disposal exit — before that unregister's file lock and atomic registry write settled, leaving a stale `sessions[id]` row that the retained older token is correctly fenced from removing (`unregisterNotificationRoot` rejects token mismatches by design). Now shutdown snapshots `sessionStartPromises.has(id)` first and awaits the settled controller stop after `stopSession` whenever startup was NOT pending, so completed-start reconciliation and its replacement-token cleanup are joined. The intentional nonblocking path is preserved exactly where it matters: a genuinely pending startup entry (the `/notify on` case) still leaves the controller stop fire-and-forget. Surfaced by the flaky-CI stabilization soak. The regression test "Telegram root release failure is retained and retried through lifecycle shutdown" failed deterministically on darwin-arm64 at dev head while Linux CI stayed green (run 30147146988, 34/34 shards) — a completion- ordering divergence, not a `/var` canonicalization or native-addon issue: both registry and notification-root paths are lexical `path.join` with no realpath or case folding. Verified on darwin-arm64: the previously-failing test now passes 10/10, the whole sdk-host-wiring file is 72/72 (first fully green run of this file on Darwin), and telegram daemon + btw-e2e are 463/463. The assertion was kept intact rather than replaced with polling, which would have hidden the lifecycle-return bug. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…eachan-Heo#3149) Caught by the stabilization soak: main-nontag rehearsal run 30151115867 failed on shard 8 with (fail) session_start swallows startup plus owner-release failure without surfacing an extension error expect(surfaced).toEqual([]) received 1 surfaced error The test used `mockRejectedValueOnce` on the SHARED prototypes `NotificationServer.prototype.start` and `SessionSdkHost.prototype.stop`. That is a one-shot global: a peer test scheduled concurrently in the same shard can consume the single rejection first. When that happens this test's own `start()` resolves, so `startSession` never reaches the `stopSession` catch that sets `suppressExtensionError` (src/sdk/bus/index.ts:4407-4420), and the startup error surfaces through `session_start` instead of being swallowed. Three separate tests in this file arm one-shot rejections on the same two prototypes, so the interference is cross-test within a shard rather than a missing restore — every one of them restores correctly. Replaces both one-shots with per-test guarded implementations that reject only on this test's first call and then delegate to the real method, preserving the existing assertions (including `expect(hostStop).toHaveBeenCalledTimes(2)`, which is what makes the "first call rejects" semantics load-bearing). Verified on darwin-arm64: 10/10 targeted reruns, 6x concurrent whole-file runs with zero failures, whole file 72/72, typecheck and biome clean. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…#3139) (Yeachan-Heo#3146) The release CI default was narrowed to `contents: read` in Yeachan-Heo#3136 with `publish` retaining the sole job-level `contents: write` override, but no automated policy test protected that invariant. `dev-ci.yml` also had no explicit permission block and inherited the repository default. Add `scripts/check-workflow-permissions.ts`: a deterministic offline default-deny evaluator plus CLI that parses committed workflow YAML and requires every workflow to declare a least-privilege `permissions` block. Write scopes are denied everywhere except one explicit allowlist triple (ci.yml / publish / contents), and the three known workflows must declare exactly `contents: read`. Violations name workflow, job, permission path, actual, and expected. Codify `contents: read` on `dev-ci.yml` after auditing its jobs: they only check out, install, test, and exchange artifacts through the Actions artifact API, with no `git push`, `gh` CLI, `secrets.*`, or release/PR mutation, so no write-scoped token is required. Back it with real-YAML mutation proofs that read committed workflows from disk, mutate the parsed document, and re-evaluate -- covering workflow and job `write`, `write-all`, extra scopes, deleted permission blocks, and malformed job shapes. Route the regression through affected-path CI in both the broad and targeted planners so workflow and CI-harness changes select it alongside the existing release-policy suite, and pin the invariant in `release-policy.test.ts` as well. Refs Yeachan-Heo#3139 Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…/ resolver (Yeachan-Heo#3145) Closes non-blocking follow-up from the Yeachan-Heo#3080 post-merge review: cleanup_pending marker asymmetry between the async gate and the sync resolver. Reviewed exact head 90aa337 with durable MERGE_READY and green exact-head CI. Merged after post-merge Dev CI 30157160426 on 352a2c1 turned terminal green.
…o what shipped (Yeachan-Heo#3140) Documentation-only correction of readiness claims from Yeachan-Heo#3080. Exact-head MERGE_READY at 6cf2764 with green CI. Held during red/nonterminal post-merge gates; merged after Dev CI 30158413804 on 2317939 terminalized success.
…eachan-Heo#3132) `retryDelayMs()` honoured the server's `Retry-After` verbatim: if (Number.isFinite(seconds)) return Math.max(baseline, Math.max(0, seconds * 1000)); There was no upper bound, so a hostile or misconfigured endpoint could stall the usage fetch for as long as it liked. Verified: `Retry-After: 86400` made the provider sleep 86_400_000ms (24 hours), and the HTTP-date form did the same (86_399_386ms). The sleep is abort-aware, but with no signal — or a long-lived one — the usage refresh simply hangs, and auth-storage ranks credentials off this call. Every sibling path in the repo already bounds this: - utils/fetch-retry.ts DEFAULT_MAX_DELAY_MS = 60_000 (+ fail-fast above cap) - providers/openai-bounded-rate-limits.ts OPENAI_RETRY_DELAY_CAP_MS = 60_000 - session/agent-session.ts maxAcceptableDelayMs = 30_000 Only the Claude usage path was missing a ceiling. Fix: add MAX_RETRY_DELAY_MS = 60_000 and clamp through clampRetryDelay(), which also drops non-finite/negative hints. 60s matches the existing convention and preserves both pre-existing assertions exactly (`Retry-After: 1` → 1000, `Retry-After: 60` → 60_000). Regression coverage (test/claude-usage-retry.test.ts): absurd numeric Retry-After, absurd HTTP-date Retry-After, and a negative Retry-After. The first two fail against the unfixed code (received 86_400_000 / 86_399_386, expected 60_000). Gates: claude-usage-retry (11 pass), usage suite (28 pass across 5 files), tsc --noEmit clean. Co-authored-by: dmae97 <dmae97@users.noreply.github.com>
Owner-approved MERGE_READY at exact head 4caf43c with green CI (31 checks). Merged after post-Yeachan-Heo#3132 Dev CI 30159910672 on 77c9a60 terminalized success. Yeachan-Heo#3134 became dirty and was skipped under one-at-a-time sequencing.
|
Superseded diagnostic: the initial memory run applied the current-dev patch and failed only because the final repair script expected the pre-fix latch shape. |
|
Superseded diagnostic: the second memory run confirmed the latch fix was already present. The verifier now uses an idempotent final-repair path. |
ce4fdb0 to
f14e244
Compare
|
Superseded diagnostic: this run used the pre-idempotence memory repair wrapper and failed before tests. Replaced by the minimal owner-requested regression inserter. |
|
Superseded diagnostic: this run retried the already-resolved scheduler transformation. The current verifier preserves the reviewed implementation and adds only the two requested direct Windows accounting regressions. |
|
SUCCESSOR_VERIFICATION_FAILED branch=fix/memory-guard-domain-scheduler |
|
SUCCESSOR_VERIFICATION_FAILED branch=fix/memory-guard-domain-scheduler |
|
SUCCESSOR_VERIFICATION_FAILED branch=fix/memory-guard-domain-scheduler |
|
SUCCESSOR_VERIFICATION_FAILED branch=fix/memory-guard-domain-scheduler |
|
SUCCESSOR_VERIFICATION_FAILED branch=fix/memory-guard-domain-scheduler |
|
SUCCESSOR_VERIFICATION_FAILED branch=fix/memory-guard-domain-scheduler |
Temporary internal PR used only to apply the Yeachan-Heo#3133 net diff to current dev before the remaining Windows-domain and scheduler repairs. The one-shot trigger file is removed by the port commit.