@gajae-code/coding-agent 0.18.6 → 0.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/config/settings.d.ts +4 -0
  4. package/dist/types/discovery/helpers.d.ts +2 -2
  5. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  6. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  7. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  8. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  9. package/dist/types/sdk/host/host.d.ts +6 -0
  10. package/dist/types/sdk/router/session-router.d.ts +7 -0
  11. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  12. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  13. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  14. package/dist/types/session/session-retirement.d.ts +47 -0
  15. package/dist/types/session/session-storage.d.ts +0 -46
  16. package/dist/types/session/session-transcript-header.d.ts +2 -0
  17. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  18. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  19. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  20. package/package.json +7 -7
  21. package/scripts/resident-memory-bench.ts +174 -20
  22. package/src/capability/index.ts +1 -0
  23. package/src/capability/types.ts +4 -0
  24. package/src/cli/config-cli.ts +8 -18
  25. package/src/cli-main.ts +7 -8
  26. package/src/commands/sdk.ts +112 -21
  27. package/src/config/settings.ts +44 -4
  28. package/src/discovery/helpers.ts +9 -3
  29. package/src/discovery/ssh.ts +8 -3
  30. package/src/gjc-runtime/gc-runtime.ts +2 -1
  31. package/src/hindsight/config.ts +9 -3
  32. package/src/internal-urls/docs-index.generated.ts +2 -2
  33. package/src/modes/acp/acp-agent.ts +75 -3
  34. package/src/runtime-mcp/client.ts +13 -4
  35. package/src/runtime-mcp/elicitation.ts +142 -0
  36. package/src/sdk/broker/broker.ts +8 -1
  37. package/src/sdk/broker/lifecycle.ts +109 -7
  38. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  39. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  40. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  41. package/src/sdk/bus/index.ts +3 -3
  42. package/src/sdk/bus/telegram-daemon.ts +4 -2
  43. package/src/sdk/host/host.ts +6 -0
  44. package/src/sdk/host/session-runtime.test.ts +43 -2
  45. package/src/sdk/host/session-runtime.ts +2 -2
  46. package/src/sdk/mcp/server.ts +15 -3
  47. package/src/sdk/router/session-router.ts +19 -2
  48. package/src/sdk/session.ts +14 -0
  49. package/src/session/agent-session.ts +57 -6
  50. package/src/session/internal/managed-session-storage.ts +107 -1
  51. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  52. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  53. package/src/session/session-retirement.ts +155 -0
  54. package/src/session/session-storage.ts +1 -160
  55. package/src/session/session-transcript-header.ts +17 -0
  56. package/src/session/task-artifact-owner-codec.ts +578 -0
  57. package/src/session/task-artifact-owner-retirement.ts +475 -0
  58. package/src/session/task-artifact-owner.ts +289 -0
@@ -2,24 +2,25 @@
2
2
  * Pinned, child-isolated resident-cache benchmark.
3
3
  *
4
4
  * Each `--runs` repetition executes in a fresh Bun child. The default fixture is
5
- * 5,000 deterministic, unique 48 KiB messages; use the small overrides only for
6
- * local smoke checks, not performance comparisons.
5
+ * 500 deterministic, unique 48 KiB messages; it stays inside the product's
6
+ * eager-resume ceiling so a default run completes, and larger scales are explicit
7
+ * `--entries`/`--bytes-per-entry` overrides that fail closed above that ceiling.
7
8
  *
8
9
  * Normal measurements (five child runs per command):
9
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode rss --runs 5
10
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode put-latency --runs 5
11
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --runs 5
10
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode rss --runs 5
11
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode put-latency --runs 5
12
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --runs 5
12
13
  *
13
14
  * HEAD forced-rebuild baseline (copy this script to the pinned HEAD worktree):
14
15
  * git worktree add /tmp/gjc-bench-head 3649db42e
15
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --baseline forced-rebuild --runs 5
16
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --baseline forced-rebuild --runs 5
16
17
  *
17
18
  * Small smoke fixture and deliberate invalid-run demonstration:
18
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode rss --entries 8 --bytes-per-entry 4096 --runs 1
19
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode put-latency --puts 64 --runs 1
20
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --entries 8 --bytes-per-entry 4096 --cache-cap-bytes 1024 --runs 1
21
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --entries 8 --bytes-per-entry 4096 --cache-cap-bytes 1024 --skip-gc --runs 1
22
- * TMPDIR="$HOME/tmp-gjc-tests/" GJC_CODING_AGENT_DIR="$(mktemp -d)" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode rss --force-memory-only --runs 1
19
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode rss --entries 8 --bytes-per-entry 4096 --runs 1
20
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode put-latency --puts 64 --runs 1
21
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --entries 8 --bytes-per-entry 4096 --cache-cap-bytes 1024 --runs 1
22
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode read-churn --entries 8 --bytes-per-entry 4096 --cache-cap-bytes 1024 --skip-gc --runs 1
23
+ * TMPDIR="$HOME/tmp-gjc-tests/" NO_COLOR=1 bun packages/coding-agent/scripts/resident-memory-bench.ts --mode rss --force-memory-only --runs 1
23
24
  *
24
25
  * `--skip-gc` deliberately bypasses the required turn boundary and forced GC;
25
26
  * the --skip-gc command must exit non-zero with the invalid-run diagnostic. Do not
@@ -28,9 +29,17 @@
28
29
  * exercise the existing fallback and is not a supported product configuration.
29
30
  * RSS mode reports append-phase and fresh-process reopen measurements with the full
30
31
  * `process.memoryUsage()` breakdown, plus repeated forced-GC idle-turn reclaim samples.
31
- * AC-1 passes only when append steady-state RSS delta stays within 100 MiB; a
32
- * post-reclaim result within that limit is separately labeled documented evidence. AC-2
33
- * measures direct `putSync` calls over pre-generated unique Buffers into a
32
+ * AC-1 passes only when all three ceilings hold: the append steady-state RSS delta stays
33
+ * within 100 MiB; the read-path residual (post-read forced-GC delta) stays within
34
+ * max(READ_PATH_RESIDUAL_FLOOR_BYTES, READ_PATH_RESIDUAL_TRANSCRIPT_RATIO_LIMIT x the
35
+ * fixture's persisted transcript); and the reclaim contract holds
36
+ * (RECLAIM_ALLOCATOR_FLOOR_BYTES, post-reclaim delta inside the append limit). A reclaim
37
+ * proof only excuses a steady-state miss: the read-path ceiling and the reclaim contract
38
+ * are hard gates, so a failed read path exits non-zero instead of being relabeled
39
+ * documented evidence. The harness always runs against its own throwaway agent dir, so
40
+ * nothing has to be pre-set, an ambient `GJC_CODING_AGENT_DIR` is ignored, and the
41
+ * operator's `~/.gjc` is never read; `GJC_RESIDENT_MEMORY_BENCH_AGENT_DIR` pins it.
42
+ * AC-2 measures direct `putSync` calls over pre-generated unique Buffers into a
34
43
  * canonical MemoryBlobStore and an adopted verified EphemeralBlobStore.
35
44
  * SessionManager append figures are separate `e2eAppend` diagnostics. A direct
36
45
  * store ratio above 1.5x is documented evidence, not a relaxation of the
@@ -43,6 +52,7 @@ import { createRequire, syncBuiltinESMExports } from "node:module";
43
52
  import * as os from "node:os";
44
53
  import * as path from "node:path";
45
54
  import { EphemeralBlobStore, MemoryBlobStore, openVerifiedResidentCacheInstanceDir } from "../src/session/blob-store";
55
+ import { MANAGED_ARTIFACT_MAX_FILE_BYTES } from "../src/session/internal/managed-session-storage";
46
56
  import type {
47
57
  SessionManagerObservabilityStats,
48
58
  SessionManager as SessionManagerType,
@@ -54,14 +64,50 @@ const { SessionManager } = sessionManagerModule;
54
64
  const SCHEMA_VERSION = 4;
55
65
  const SYNTHETIC_SEED = 0x5eedc0de;
56
66
  const DEFAULT_RUNS = 5;
57
- const RSS_ENTRY_COUNT = 5_000;
67
+ /**
68
+ * Default fixture. It has to stay inside the eager-resume ceiling, and at the default
69
+ * scale the append steady state has to stay inside AC-1: the previous 5_000 x 48 KiB
70
+ * default persisted about 234 MiB, so every default run died with the product's
71
+ * oversized-transcript recovery message before it could report anything. Larger scales
72
+ * are explicit opt-in through `--entries`/`--bytes-per-entry`, bounded by
73
+ * `assertFixtureWithinEagerResumeCeiling`.
74
+ */
75
+ const RSS_ENTRY_COUNT = 500;
58
76
  const RSS_BYTES_PER_ENTRY = 48 * 1024;
77
+ /**
78
+ * The eager-resume ceiling the product enforces, imported rather than copied so a
79
+ * product-side change is reflected here instead of silently invalidating the fixture.
80
+ */
81
+ const EAGER_RESUME_TRANSCRIPT_CEILING_BYTES = MANAGED_ARTIFACT_MAX_FILE_BYTES;
82
+ /** Measured per-entry framing overhead of the persisted JSONL transcript. */
83
+ const SYNTHETIC_ENTRY_OVERHEAD_BYTES = 160;
59
84
  const READ_CHURN_CYCLES = 100;
60
85
  const PUT_WARMUP_ITERATIONS = 1_000;
61
86
  const PUT_MEASURE_ITERATIONS = 10_000;
62
87
  const PUT_BYTES = 4 * 1024;
63
88
  const BASELINE_MARKER_BYTES = 128;
64
89
  const AC1_APPEND_PHASE_RSS_LIMIT_BYTES = 100 * 1024 * 1024;
90
+ /**
91
+ * Read-path residual ceiling: after the read plus the forced-GC idle turns the append
92
+ * phase must not still hold more than `max(READ_PATH_RESIDUAL_FLOOR_BYTES, ratio x the
93
+ * fixture's persisted transcript)` above the pre-append baseline. Expressing the ceiling
94
+ * against the fixture keeps an `--entries` override from walking out from under it, and
95
+ * the absolute floor covers the part of the residual that does not scale with the
96
+ * fixture: on a tiny smoke fixture the forced-GC delta is a few MiB of ordinary runtime
97
+ * overhead, which a pure ratio would flag as a failure. AC-1 gates only the append steady
98
+ * state, so without this second ceiling a retention regression on the read path can keep
99
+ * a large block of RSS and still report pass. The current tree measures about 5x on the
100
+ * default fixture (about 121 MiB retained over a 24 MiB transcript).
101
+ */
102
+ const READ_PATH_RESIDUAL_TRANSCRIPT_RATIO_LIMIT = 8;
103
+ const READ_PATH_RESIDUAL_FLOOR_BYTES = 32 * 1024 * 1024;
104
+ /**
105
+ * Bun's allocator retains freed pages, so the forced-GC reclaim returns nothing on this
106
+ * runtime. The floor is an explicit contract instead of an unexplained zero: the reclaim
107
+ * must never increase RSS, and the post-reclaim delta must stay inside the append limit,
108
+ * or the run fails closed.
109
+ */
110
+ const RECLAIM_ALLOCATOR_FLOOR_BYTES = 0;
65
111
  const MEMORY_RECLAIM_GC_ROUNDS = 4;
66
112
  const SYNTHETIC_TEXT_PATTERN =
67
113
  "gjc-resident-cache-fixture-abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789";
@@ -353,11 +399,34 @@ function parseArgs(argv: string[]): CliArgs {
353
399
  }
354
400
 
355
401
  function fixtureFor(args: CliArgs): FixtureDimensions {
356
- return {
402
+ const fixture: FixtureDimensions = {
357
403
  seed: SYNTHETIC_SEED,
358
404
  entries: args.entries ?? RSS_ENTRY_COUNT,
359
405
  bytesPerEntry: args.bytesPerEntry ?? RSS_BYTES_PER_ENTRY,
360
406
  };
407
+ assertFixtureWithinEagerResumeCeiling(fixture);
408
+ return fixture;
409
+ }
410
+
411
+ /**
412
+ * Fail closed before the run instead of dying inside the product: a fixture whose
413
+ * persisted transcript exceeds the eager-resume ceiling cannot be re-opened by the
414
+ * fresh-open phase, and the failure otherwise surfaces as the product's oversized-
415
+ * transcript recovery message with no hint that the fixture size is the cause.
416
+ */
417
+ /** Estimated persisted JSONL size of a fixture; the ceiling and read-path gates share it. */
418
+ function estimatedTranscriptBytes(fixture: FixtureDimensions): number {
419
+ return fixture.entries * (fixture.bytesPerEntry + SYNTHETIC_ENTRY_OVERHEAD_BYTES);
420
+ }
421
+
422
+ function assertFixtureWithinEagerResumeCeiling(fixture: FixtureDimensions): void {
423
+ const estimatedBytes = estimatedTranscriptBytes(fixture);
424
+ if (estimatedBytes <= EAGER_RESUME_TRANSCRIPT_CEILING_BYTES) return;
425
+ const estimatedMiB = Math.round(estimatedBytes / (1024 * 1024));
426
+ const ceilingMiB = Math.round(EAGER_RESUME_TRANSCRIPT_CEILING_BYTES / (1024 * 1024));
427
+ throw new Error(
428
+ `--entries/--bytes-per-entry would persist about ${estimatedMiB} MiB of transcript, above the ${ceilingMiB} MiB eager-resume ceiling; lower the fixture so the fresh-open phase can re-open it.`,
429
+ );
361
430
  }
362
431
 
363
432
  function fixtureForMode(args: CliArgs): FixtureDimensions {
@@ -539,6 +608,34 @@ async function withPersistentFixture<T>(
539
608
  }
540
609
  }
541
610
 
611
+ /**
612
+ * The harness runs against its own throwaway agent dir so it never reads or writes the
613
+ * operator's `~/.gjc` — an ambient `GJC_CODING_AGENT_DIR` (an agent session exports one)
614
+ * must not redirect a benchmark into live state, and the resident-cache poison that
615
+ * `--force-memory-only` relies on is only meaningful in a fresh directory anyway.
616
+ * `GJC_RESIDENT_MEMORY_BENCH_AGENT_DIR` is the explicit opt-in for pinning the directory.
617
+ */
618
+ let isolatedAgentDir: string | undefined;
619
+
620
+ async function ensureIsolatedAgentDir(): Promise<void> {
621
+ const pinned = process.env.GJC_RESIDENT_MEMORY_BENCH_AGENT_DIR;
622
+ if (pinned) {
623
+ process.env.GJC_CODING_AGENT_DIR = pinned;
624
+ process.env.PI_CODING_AGENT_DIR ??= pinned;
625
+ return;
626
+ }
627
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-resident-memory-agent-"));
628
+ process.env.GJC_CODING_AGENT_DIR = dir;
629
+ process.env.PI_CODING_AGENT_DIR = dir;
630
+ isolatedAgentDir = dir;
631
+ }
632
+
633
+ async function cleanupIsolatedAgentDir(): Promise<void> {
634
+ const dir = isolatedAgentDir;
635
+ isolatedAgentDir = undefined;
636
+ if (dir) await fs.rm(dir, { recursive: true, force: true });
637
+ }
638
+
542
639
  async function forceResidentCacheMemoryFallback(): Promise<void> {
543
640
  const agentDir = process.env.GJC_CODING_AGENT_DIR;
544
641
  if (!agentDir) throw new Error("--force-memory-only requires GJC_CODING_AGENT_DIR to be set.");
@@ -590,7 +687,10 @@ async function settleAndForceGcTwice(): Promise<void> {
590
687
 
591
688
  /**
592
689
  * Let the collector and allocator reclaim after an idle turn without injecting a
593
- * larger temporary allocation that itself becomes an RSS high-water artifact.
690
+ * larger temporary allocation that itself becomes an RSS high-water artifact. The
691
+ * allocator retains the pages it already mapped, so this is expected to release
692
+ * `RECLAIM_ALLOCATOR_FLOOR_BYTES`; the AC-1 report gates that contract instead of
693
+ * printing a bare zero.
594
694
  */
595
695
  async function reclaimAfterMemoryPressure(): Promise<MemoryPressureReclaim> {
596
696
  for (let round = 0; round < MEMORY_RECLAIM_GC_ROUNDS; round++) {
@@ -635,6 +735,9 @@ async function runFreshOpenRss(args: CliArgs): Promise<FreshOpenRssWorkerResult>
635
735
  try {
636
736
  await settleFreshOpenRss();
637
737
  const baseline = memorySample();
738
+ // `fixtureFor` already proved the persisted transcript stays under the eager-resume
739
+ // ceiling, so this is the ordinary resume path the product takes for a session a
740
+ // user would actually reopen.
638
741
  manager = await SessionManager.open(sessionFile);
639
742
  const postOpen = memorySample();
640
743
  const diskBytes = await residentCacheDiskBytes();
@@ -1113,11 +1216,24 @@ function summarizeAc1(runs: readonly RssWorkerResult[]): {
1113
1216
  postReclaimRssDelta: number;
1114
1217
  classification: "documented-evidence";
1115
1218
  };
1219
+ readPathResidual: {
1220
+ postReadGcRssDelta: number;
1221
+ limitBytes: number;
1222
+ floorBytes: number;
1223
+ ratioLimit: number;
1224
+ passes: boolean;
1225
+ };
1226
+ reclaimFloor: {
1227
+ allocatorFloorBytes: number;
1228
+ postReclaimWithinLimit: boolean;
1229
+ passes: boolean;
1230
+ };
1116
1231
  verdict: "pass" | "documented-evidence-with-reclaim-proof" | "fail";
1117
1232
  } {
1118
1233
  const appendSteadyDelta = summarize(runs.map(run => run.appendPhase.steadyStateDelta.rssBytes));
1119
1234
  const postReclaimDelta = summarize(runs.map(run => run.appendPhase.postReclaimDelta.rssBytes));
1120
1235
  const reclaimedBytes = summarize(runs.map(run => run.appendPhase.reclaimedBytes));
1236
+ const readPathResidualDelta = summarize(runs.map(run => run.appendPhase.postReadGcDelta.rssBytes));
1121
1237
  const steadyStateExternal = summarize(runs.map(run => run.appendPhase.steadyState.externalBytes));
1122
1238
  const postReclaimExternal = summarize(runs.map(run => run.appendPhase.postReclaim.externalBytes));
1123
1239
  const postReclaimExternalDelta = summarize(runs.map(run => run.appendPhase.postReclaimDelta.externalBytes));
@@ -1127,7 +1243,19 @@ function summarizeAc1(runs: readonly RssWorkerResult[]): {
1127
1243
  const freshOpenRssDelta = summarize(runs.map(run => run.freshOpenPhase.steadyStateDelta.rssBytes));
1128
1244
  const freshOpenPostReclaimRssDelta = summarize(runs.map(run => run.freshOpenPhase.postReclaimDelta.rssBytes));
1129
1245
  const passesSteadyStateGate = appendSteadyDelta.median <= AC1_APPEND_PHASE_RSS_LIMIT_BYTES;
1130
- const hasReclaimProof = postReclaimDelta.median <= AC1_APPEND_PHASE_RSS_LIMIT_BYTES && reclaimedBytes.median > 0;
1246
+ const readPathResidualLimitBytes = Math.max(
1247
+ READ_PATH_RESIDUAL_FLOOR_BYTES,
1248
+ estimatedTranscriptBytes(runs[0]?.fixture ?? { seed: SYNTHETIC_SEED, entries: 0, bytesPerEntry: 0 }) *
1249
+ READ_PATH_RESIDUAL_TRANSCRIPT_RATIO_LIMIT,
1250
+ );
1251
+ const passesReadPathGate = readPathResidualDelta.median <= readPathResidualLimitBytes;
1252
+ const postReclaimWithinLimit = postReclaimDelta.median <= AC1_APPEND_PHASE_RSS_LIMIT_BYTES;
1253
+ const passesReclaimFloorGate = reclaimedBytes.median >= RECLAIM_ALLOCATOR_FLOOR_BYTES && postReclaimWithinLimit;
1254
+ const hasReclaimProof = postReclaimWithinLimit && reclaimedBytes.median > 0;
1255
+ // The reclaim proof excuses the steady-state ceiling only. The read-path ceiling and
1256
+ // the reclaim contract are hard gates, so a proof must not relabel a failed read path
1257
+ // as documented evidence and let the process exit zero.
1258
+ const reclaimProofExcusesOnlySteadyState = hasReclaimProof && passesReadPathGate && passesReclaimFloorGate;
1131
1259
  return {
1132
1260
  appendSteadyDelta: appendSteadyDelta.median,
1133
1261
  postReclaimDelta: postReclaimDelta.median,
@@ -1147,7 +1275,24 @@ function summarizeAc1(runs: readonly RssWorkerResult[]): {
1147
1275
  postReclaimRssDelta: freshOpenPostReclaimRssDelta.median,
1148
1276
  classification: "documented-evidence",
1149
1277
  },
1150
- verdict: passesSteadyStateGate ? "pass" : hasReclaimProof ? "documented-evidence-with-reclaim-proof" : "fail",
1278
+ readPathResidual: {
1279
+ postReadGcRssDelta: readPathResidualDelta.median,
1280
+ limitBytes: readPathResidualLimitBytes,
1281
+ floorBytes: READ_PATH_RESIDUAL_FLOOR_BYTES,
1282
+ ratioLimit: READ_PATH_RESIDUAL_TRANSCRIPT_RATIO_LIMIT,
1283
+ passes: passesReadPathGate,
1284
+ },
1285
+ reclaimFloor: {
1286
+ allocatorFloorBytes: RECLAIM_ALLOCATOR_FLOOR_BYTES,
1287
+ postReclaimWithinLimit,
1288
+ passes: passesReclaimFloorGate,
1289
+ },
1290
+ verdict:
1291
+ passesSteadyStateGate && passesReadPathGate && passesReclaimFloorGate
1292
+ ? "pass"
1293
+ : reclaimProofExcusesOnlySteadyState
1294
+ ? "documented-evidence-with-reclaim-proof"
1295
+ : "fail",
1151
1296
  };
1152
1297
  }
1153
1298
 
@@ -1323,6 +1468,10 @@ async function runParent(args: CliArgs): Promise<void> {
1323
1468
  runResults: runs,
1324
1469
  };
1325
1470
  process.stdout.write(`${JSON.stringify(output)}\n`);
1471
+ // A failed RSS acceptance verdict has to fail the process, otherwise the declared
1472
+ // ceilings are only advisory and nothing downstream can gate on them.
1473
+ if (args.mode === "rss" && (acceptanceReport as { ac1: { verdict: string } }).ac1.verdict === "fail")
1474
+ process.exitCode = 1;
1326
1475
  }
1327
1476
 
1328
1477
  async function main(): Promise<void> {
@@ -1332,7 +1481,12 @@ async function main(): Promise<void> {
1332
1481
  process.stdout.write(`${JSON.stringify(result)}\n`);
1333
1482
  return;
1334
1483
  }
1335
- await runParent(args);
1484
+ await ensureIsolatedAgentDir();
1485
+ try {
1486
+ await runParent(args);
1487
+ } finally {
1488
+ await cleanupIsolatedAgentDir();
1489
+ }
1336
1490
  }
1337
1491
 
1338
1492
  await main().catch(error => {
@@ -429,6 +429,7 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
429
429
  repoRoot,
430
430
  isolatedHome: false,
431
431
  settings: ordinaryOptions.settings,
432
+ bypassCache: ordinaryOptions.bypassCache,
432
433
  };
433
434
  const providers = filterProviders(capability, ordinaryOptions);
434
435
 
@@ -40,6 +40,8 @@ export interface LoadContext {
40
40
  repoRoot: string | null;
41
41
  /** Whether discovery must remain isolated to the supplied home boundary. */
42
42
  isolatedHome?: boolean;
43
+ /** Bypass lexical filesystem caches for this discovery operation. */
44
+ bypassCache?: boolean;
43
45
  /** Owning session settings for provider policy decisions. */
44
46
  settings?: Settings;
45
47
  }
@@ -112,6 +114,8 @@ export interface LoadOptions {
112
114
  includeDisabledProviders?: boolean;
113
115
  /** Explicit disabled extension IDs to apply instead of settings. */
114
116
  disabledExtensions?: string[];
117
+ /** Bypass lexical filesystem caches for this discovery operation. */
118
+ bypassCache?: boolean;
115
119
  /** Session settings whose provider policy applies to this load. */
116
120
  settings?: Settings;
117
121
  /** Internal: prevent explicit-home loads from consulting process-global policy. */
@@ -63,29 +63,19 @@ type CliSettingDef = {
63
63
  const ALL_SETTING_PATHS = Object.keys(SETTINGS_SCHEMA) as SettingPath[];
64
64
  const REDACTED_SECRET_VALUE = "<redacted>";
65
65
  const SECRET_SETTING_WORDS = new Set(["token", "secret", "password", "passwd", "pwd", "credential", "credentials"]);
66
- const SECRET_SETTING_COMPOUND_PREFIXES = [
67
- "api",
68
- "auth",
69
- "access",
70
- "refresh",
71
- "bearer",
72
- "session",
73
- "client",
74
- "broker",
75
- "bot",
76
- "basic",
77
- ];
78
- const SECRET_SETTING_COMPOUND_SUFFIXES = ["token", "secret", "password", "credential"];
66
+ /**
67
+ * camelCase segments (`appToken`, `basicPassword`) do not split on `-`/`_`, so any segment whose
68
+ * normalized form ends in a secret word is secret. Plural budget names (`maxTokens`) and words that
69
+ * merely contain one (`showTokenUsage`) stay visible.
70
+ */
71
+ const SECRET_SETTING_SUFFIX = /(?:token|secret|password|passwd|pwd|credentials?)$/;
79
72
 
80
73
  function isSecretSettingSegment(segment: string): boolean {
81
74
  const normalized = segment.toLowerCase();
82
- if (SECRET_SETTING_WORDS.has(normalized)) return true;
75
+ if (SECRET_SETTING_SUFFIX.test(normalized)) return true;
83
76
  if (/api[-_]?key/i.test(segment)) return true;
84
77
  const words = normalized.split(/[-_]/).filter(Boolean);
85
- if (words.some(word => SECRET_SETTING_WORDS.has(word))) return true;
86
- return SECRET_SETTING_COMPOUND_PREFIXES.some(prefix =>
87
- SECRET_SETTING_COMPOUND_SUFFIXES.some(suffix => normalized === `${prefix}${suffix}`),
88
- );
78
+ return words.some(word => SECRET_SETTING_WORDS.has(word));
89
79
  }
90
80
 
91
81
  function isSecretSettingPath(path: string): boolean {
package/src/cli-main.ts CHANGED
@@ -8,19 +8,12 @@ import "@gajae-code/utils/postmortem";
8
8
  import { Args, type CliConfig, Command, type CommandEntry, run } from "@gajae-code/utils/cli";
9
9
  import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
10
10
  import { time } from "@gajae-code/utils/logger";
11
- import { runFixtureReport } from "./cli/fixture-report";
12
11
  import { ROOT_LAUNCH_FLAGS } from "./cli/root-flags";
13
- import QuickLane from "./commands/quick-lane";
14
- import { runBashShellGuardian } from "./exec/bash-shell-guardian";
15
- import { runBashShellSupervisor } from "./exec/bash-shell-supervisor";
16
- import { runBashShellWorker } from "./exec/bash-shell-worker";
17
12
  import {
18
13
  BASH_SHELL_RUNTIME_ARG,
19
14
  BASH_SHELL_SUPERVISOR_ARG,
20
15
  BASH_SHELL_WORKER_ARG,
21
16
  } from "./exec/bash-shell-worker-protocol";
22
- import { smokeTestIsolatedShell } from "./exec/isolated-shell";
23
- import { smokeTestTabWorker } from "./tools/browser/tab-worker-smoke";
24
17
 
25
18
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
26
19
  process.stderr.write(
@@ -81,7 +74,7 @@ export const commands: CommandEntry[] = [
81
74
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
82
75
  { name: "completion", load: () => import("./commands/completion").then(m => m.default) },
83
76
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
84
- { name: "quick-lane", load: async () => QuickLane },
77
+ { name: "quick-lane", load: () => import("./commands/quick-lane").then(m => m.default) },
85
78
  ];
86
79
 
87
80
  async function showHelp(config: CliConfig): Promise<void> {
@@ -346,6 +339,8 @@ async function runSmokeTest(): Promise<void> {
346
339
  await smokeTestSyncWorker();
347
340
  const { runNativeSmokeTest } = await import("./cli/native-smoke");
348
341
  await runNativeSmokeTest();
342
+ const { smokeTestTabWorker } = await import("./tools/browser/tab-worker-smoke");
343
+ const { smokeTestIsolatedShell } = await import("./exec/isolated-shell");
349
344
  await smokeTestTabWorker();
350
345
  await smokeTestIsolatedShell();
351
346
  process.stdout.write("smoke-test: ok\n");
@@ -401,14 +396,17 @@ export function routeRootArgv(argv: readonly string[]): string[] {
401
396
  /** Run the CLI with the given argv (no `process.argv` prefix). */
402
397
  export async function runCliAfterAdmission(argv: string[]): Promise<void> {
403
398
  if (argv.length === 1 && argv[0] === BASH_SHELL_WORKER_ARG) {
399
+ const { runBashShellGuardian } = await import("./exec/bash-shell-guardian");
404
400
  await runBashShellGuardian();
405
401
  return;
406
402
  }
407
403
  if (argv.length === 1 && argv[0] === BASH_SHELL_SUPERVISOR_ARG) {
404
+ const { runBashShellSupervisor } = await import("./exec/bash-shell-supervisor");
408
405
  await runBashShellSupervisor();
409
406
  return;
410
407
  }
411
408
  if (argv.length === 1 && argv[0] === BASH_SHELL_RUNTIME_ARG) {
409
+ const { runBashShellWorker } = await import("./exec/bash-shell-worker");
412
410
  await runBashShellWorker();
413
411
  return;
414
412
  }
@@ -478,6 +476,7 @@ export async function runCliAfterAdmission(argv: string[]): Promise<void> {
478
476
  process.exitCode = 1;
479
477
  return;
480
478
  }
479
+ const { runFixtureReport } = await import("./cli/fixture-report");
481
480
  process.exitCode = await runFixtureReport(id);
482
481
  return;
483
482
  }
@@ -1,10 +1,15 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import * as nodeFs from "node:fs";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
- import { exactRemoveDirectoryTree, type NativeDirectoryTreeSnapshot, snapshotDirectoryTree } from "@gajae-code/natives";
7
+ import {
8
+ exactRemoveDirectoryTree,
9
+ exactUnlinkDirect,
10
+ type NativeDirectoryTreeSnapshot,
11
+ snapshotDirectoryTree,
12
+ } from "@gajae-code/natives";
8
13
  import { logger, postmortem } from "@gajae-code/utils";
9
14
  import { CliParseError, Command } from "@gajae-code/utils/cli";
10
15
  import type { Args as ParsedArgs } from "../cli/args";
@@ -24,6 +29,7 @@ import {
24
29
  writeBrokerStartupExitRecordBounded,
25
30
  } from "../sdk/broker/broker-exit";
26
31
  import { readBrokerDiscovery } from "../sdk/broker/discovery";
32
+ import { type EndpointFileRead, readEndpointFile } from "../sdk/broker/endpoint-authority";
27
33
  import {
28
34
  emitBrokerStartupTestSignal,
29
35
  reconcileBrokerGenerationForStartup,
@@ -653,6 +659,7 @@ export async function runSessionHost(
653
659
  let startupComplete = false;
654
660
  let readinessPublicationCleanupComplete = true;
655
661
  let revokePendingReadinessMarker: (() => Promise<boolean>) | undefined;
662
+ let revokePublishedReadinessMarker: (() => Promise<boolean>) | undefined;
656
663
  let readinessRevocation: Promise<boolean> | undefined;
657
664
  let startupInterruption: SdkStartupFailure | undefined;
658
665
  const interrupted = Promise.withResolvers<SdkStartupFailure>();
@@ -1267,20 +1274,76 @@ export async function runSessionHost(
1267
1274
  return failureRollback;
1268
1275
  };
1269
1276
  const sessionEndpointPath = path.join(request.stateRoot, "sdk", `${request.sessionId}.json`);
1270
- const exitAfterSessionDisposal = async (reason?: "detached_idle"): Promise<void> => {
1271
- await disposeSession(reason);
1277
+ const removeOwnedSessionEndpoint = async (): Promise<boolean> => {
1278
+ try {
1279
+ await fs.stat(sessionEndpointPath);
1280
+ } catch (error) {
1281
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
1282
+ return false;
1283
+ }
1284
+ const endpoint: EndpointFileRead | undefined = await readEndpointFile(sessionEndpointPath);
1285
+ if (!endpoint) return false;
1286
+ let parsed: { pid?: unknown; sessionId?: unknown };
1287
+ try {
1288
+ parsed = JSON.parse(endpoint.source) as { pid?: unknown; sessionId?: unknown };
1289
+ } catch {
1290
+ return false;
1291
+ }
1292
+ if (parsed.pid !== process.pid || parsed.sessionId !== request.sessionId) {
1293
+ return false;
1294
+ }
1295
+ const removal = (() => {
1296
+ try {
1297
+ return exactUnlinkDirect(sessionEndpointPath, {
1298
+ dev: endpoint.dev,
1299
+ ino: endpoint.ino,
1300
+ nlink: endpoint.nlink,
1301
+ size: endpoint.size,
1302
+ mtimeNs: endpoint.mtimeNs,
1303
+ quarantineName: `.gjc-delete-endpoint-${request.sessionId}-${randomUUID()}`,
1304
+ sha256: createHash("sha256").update(endpoint.source).digest("hex"),
1305
+ allowHardLink: true,
1306
+ });
1307
+ } catch (error) {
1308
+ logger.warn("SDK host endpoint cleanup failed", { error: String(error) });
1309
+ return undefined;
1310
+ }
1311
+ })();
1312
+ if (!removal) return false;
1313
+ return (
1314
+ removal.ok ||
1315
+ removal.code === "not_found" ||
1316
+ (removal.code === "cleanup_pending" &&
1317
+ removal.payloadDurable === true &&
1318
+ !removal.retainedSuccessorPath &&
1319
+ !removal.retainedUnknownPath)
1320
+ );
1321
+ };
1322
+ let unregisterHostPostmortem: (() => void) | undefined;
1323
+ let sessionDisposalExit: Promise<void> | undefined;
1324
+ const exitAfterSessionDisposal = async (reason?: "detached_idle", exitProcess = true): Promise<void> => {
1272
1325
  let failure: SdkStartupFailure | undefined;
1326
+ let ownsEndpoint = false;
1327
+ const endpointCleanup = (async (): Promise<boolean> => {
1328
+ let complete = await removeOwnedSessionEndpoint();
1329
+ for (let attempt = 0; !complete && attempt < 20; attempt += 1) {
1330
+ await Bun.sleep(25);
1331
+ complete = await removeOwnedSessionEndpoint();
1332
+ }
1333
+ return complete;
1334
+ })();
1335
+ let [endpointCleanupComplete, readinessRevoked] = await Promise.all([
1336
+ endpointCleanup,
1337
+ revokePublishedReadinessMarker?.() ?? Promise.resolve(true),
1338
+ ]);
1339
+ await disposeSession(reason);
1340
+ endpointCleanupComplete = await removeOwnedSessionEndpoint();
1273
1341
  try {
1274
1342
  const endpoint = JSON.parse(await fs.readFile(sessionEndpointPath, "utf8")) as {
1275
1343
  pid?: unknown;
1276
1344
  sessionId?: unknown;
1277
1345
  };
1278
- if (endpoint.pid === process.pid && endpoint.sessionId === request.sessionId)
1279
- failure = {
1280
- phase: "startup",
1281
- reason: "failed",
1282
- message: `SDK host endpoint remained after graceful shutdown: ${request.sessionId}`,
1283
- };
1346
+ ownsEndpoint = endpoint.pid === process.pid && endpoint.sessionId === request.sessionId;
1284
1347
  } catch (error) {
1285
1348
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
1286
1349
  failure = {
@@ -1288,27 +1351,57 @@ export async function runSessionHost(
1288
1351
  reason: "failed",
1289
1352
  message: `SDK host endpoint cleanup could not be verified: ${request.sessionId}`,
1290
1353
  };
1291
- }
1354
+ } else endpointCleanupComplete = true;
1292
1355
  }
1356
+ if (!ownsEndpoint && !failure && !readinessRevoked && revokePublishedReadinessMarker) {
1357
+ failure = {
1358
+ phase: "startup",
1359
+ reason: "failed",
1360
+ message: `SDK host readiness marker cleanup could not be verified: ${request.sessionId}`,
1361
+ };
1362
+ }
1363
+ if (ownsEndpoint && !endpointCleanupComplete && !failure)
1364
+ failure = {
1365
+ phase: "startup",
1366
+ reason: "failed",
1367
+ message: `SDK host endpoint remained after graceful shutdown: ${request.sessionId}`,
1368
+ };
1293
1369
  if (failure) {
1294
1370
  process.exitCode = 1;
1295
1371
  process.stderr.write(`${failure.message}\n`);
1296
1372
  await writeFailure(failure, rollback.result).catch(() => {});
1297
1373
  }
1298
- process.exit(process.exitCode ?? 0);
1374
+ unregisterHostPostmortem?.();
1375
+ unregisterHostPostmortem = undefined;
1376
+ if (exitProcess) process.exit(process.exitCode ?? 0);
1299
1377
  };
1300
1378
  let stopping = false;
1379
+ const requestSessionStop = (reason?: "detached_idle", exitProcess = true): Promise<void> => {
1380
+ if (stopping) return sessionDisposalExit ?? Promise.resolve();
1381
+ stopping = true;
1382
+ sessionDisposalExit = exitAfterSessionDisposal(reason, exitProcess);
1383
+ return sessionDisposalExit;
1384
+ };
1301
1385
  const stop = (reason?: "detached_idle") => {
1302
1386
  if (startupComplete) {
1303
- if (stopping) return;
1304
- stopping = true;
1305
- void exitAfterSessionDisposal(reason);
1387
+ void requestSessionStop(reason);
1306
1388
  return;
1307
1389
  }
1308
1390
  const failure = capability.normalizeFailure("startup", "failed", "SDK lifecycle host terminated.");
1309
1391
  interruptStartup(failure);
1310
1392
  };
1311
- const onReadySignal = (): void => stop();
1393
+ const registerHostPostmortem = (): void => {
1394
+ if (unregisterHostPostmortem) return;
1395
+ unregisterHostPostmortem = postmortem.register("sdk-session-host:exit", async reason => {
1396
+ if (reason !== postmortem.Reason.SIGTERM && reason !== postmortem.Reason.SIGINT) return;
1397
+ // The postmortem signal authority defaults to 143/130; a session host
1398
+ // signal is graceful and must preserve its cleanup result instead.
1399
+ await requestSessionStop(undefined, false);
1400
+ process.exitCode = 0;
1401
+ process.exit(0);
1402
+ });
1403
+ };
1404
+ registerHostPostmortem();
1312
1405
 
1313
1406
  try {
1314
1407
  const startupThinkingLevel = request.modelId ? parseModelString(request.modelId)?.thinkingLevel : undefined;
@@ -1356,6 +1449,7 @@ export async function runSessionHost(
1356
1449
  effectMarker,
1357
1450
  () => startupInterruption === undefined && now() < request.semanticReadyDeadlineAt,
1358
1451
  revoke => {
1452
+ revokePublishedReadinessMarker = revoke;
1359
1453
  revokePendingReadinessMarker = revoke;
1360
1454
  if (startupInterruption !== undefined) startReadinessRevocation(revoke);
1361
1455
  else if (now() >= request.semanticReadyDeadlineAt) {
@@ -1390,8 +1484,6 @@ export async function runSessionHost(
1390
1484
  revokePendingReadinessMarker = undefined;
1391
1485
  readinessRevocation = undefined;
1392
1486
  removeStartupSignalHandlers();
1393
- process.once("SIGTERM", onReadySignal);
1394
- process.once("SIGINT", onReadySignal);
1395
1487
  } catch (error) {
1396
1488
  if (error instanceof LifecycleReadinessCleanupError) constructionCleanupComplete = false;
1397
1489
  const failure =
@@ -1404,8 +1496,6 @@ export async function runSessionHost(
1404
1496
  await failAfterRollback(durableFailure);
1405
1497
  } finally {
1406
1498
  removeStartupSignalHandlers();
1407
- process.removeListener("SIGTERM", onReadySignal);
1408
- process.removeListener("SIGINT", onReadySignal);
1409
1499
  }
1410
1500
  throw error;
1411
1501
  }
@@ -1655,7 +1745,8 @@ export default class Sdk extends Command {
1655
1745
  child.unref();
1656
1746
  return;
1657
1747
  }
1658
- if (internal.action === "broker-internal") usePostmortemSignalExitAuthority();
1748
+ if (internal.action === "broker-internal" || internal.action === "session-host-internal")
1749
+ usePostmortemSignalExitAuthority();
1659
1750
  if (internal.action === "session-host-internal") {
1660
1751
  await runSessionHost();
1661
1752
  return;