@gajae-code/coding-agent 0.12.19 → 0.12.21

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 (100) hide show
  1. package/CHANGELOG.md +27 -9
  2. package/dist/types/commands/gc.d.ts +4 -0
  3. package/dist/types/commands/sdk.d.ts +66 -0
  4. package/dist/types/config/settings-schema.d.ts +34 -0
  5. package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
  6. package/dist/types/modes/acp/acp-agent.d.ts +33 -1
  7. package/dist/types/modes/components/bash-execution.d.ts +7 -0
  8. package/dist/types/modes/components/eval-execution.d.ts +7 -0
  9. package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
  10. package/dist/types/sdk/acp/adapter.d.ts +9 -17
  11. package/dist/types/sdk/acp/index.d.ts +1 -1
  12. package/dist/types/sdk/broker/broker.d.ts +93 -1
  13. package/dist/types/sdk/broker/ensure.d.ts +17 -0
  14. package/dist/types/sdk/broker/index.d.ts +1 -0
  15. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
  16. package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
  17. package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
  18. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  19. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
  20. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +20 -9
  22. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
  23. package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
  24. package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
  25. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  26. package/dist/types/sdk/lifecycle-session.d.ts +13 -2
  27. package/dist/types/sdk/prompt-failure.d.ts +13 -0
  28. package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
  29. package/dist/types/sdk/session-reconnect.d.ts +20 -0
  30. package/dist/types/sdk/startup-capability.d.ts +1 -1
  31. package/dist/types/session/agent-session.d.ts +14 -4
  32. package/dist/types/session/blob-store.d.ts +32 -0
  33. package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
  34. package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
  35. package/dist/types/session/session-manager.d.ts +302 -27
  36. package/dist/types/session/session-storage.d.ts +170 -0
  37. package/dist/types/task/executor.d.ts +2 -1
  38. package/dist/types/tools/bash.d.ts +1 -0
  39. package/dist/types/tools/todo-contract.d.ts +10 -1
  40. package/dist/types/tui/output-block.d.ts +6 -0
  41. package/package.json +8 -8
  42. package/src/cli/args.ts +75 -34
  43. package/src/commands/acp.ts +7 -1
  44. package/src/commands/gc.ts +31 -2
  45. package/src/commands/sdk.ts +141 -7
  46. package/src/config/settings-schema.ts +43 -0
  47. package/src/coordinator-mcp/server.ts +6 -1
  48. package/src/export/html/index.ts +83 -39
  49. package/src/gjc-runtime/gc-runtime.ts +1283 -4
  50. package/src/internal-urls/docs-index.generated.ts +1 -1
  51. package/src/internal-urls/local-protocol.ts +14 -0
  52. package/src/main.ts +40 -9
  53. package/src/modes/acp/acp-agent.ts +564 -37
  54. package/src/modes/acp/terminal-auth.ts +2 -2
  55. package/src/modes/components/bash-execution.ts +14 -0
  56. package/src/modes/components/eval-execution.ts +14 -0
  57. package/src/modes/controllers/command-controller.ts +63 -8
  58. package/src/modes/utils/ui-helpers.ts +81 -11
  59. package/src/sdk/acp/adapter.ts +46 -45
  60. package/src/sdk/acp/index.ts +1 -1
  61. package/src/sdk/broker/broker.ts +392 -11
  62. package/src/sdk/broker/ensure.ts +131 -61
  63. package/src/sdk/broker/index.ts +1 -0
  64. package/src/sdk/broker/lifecycle-ledger.ts +59 -5
  65. package/src/sdk/broker/lifecycle.ts +278 -63
  66. package/src/sdk/broker/startup-budget.ts +103 -0
  67. package/src/sdk/bus/chat-daemon-control.ts +9 -3
  68. package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
  69. package/src/sdk/bus/conversation-store.ts +1 -1
  70. package/src/sdk/bus/discord-daemon.ts +2 -1
  71. package/src/sdk/bus/discord-live-provider.ts +34 -6
  72. package/src/sdk/bus/index.ts +118 -20
  73. package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
  74. package/src/sdk/bus/prompt-reconciliation.ts +4 -9
  75. package/src/sdk/bus/slack-daemon.ts +91 -18
  76. package/src/sdk/cli/session-cli.ts +8 -1
  77. package/src/sdk/host/query/handlers.ts +1 -1
  78. package/src/sdk/host/query/revision-store.ts +48 -12
  79. package/src/sdk/host/session-runtime.test.ts +50 -0
  80. package/src/sdk/host/session-runtime.ts +12 -2
  81. package/src/sdk/lifecycle-session.ts +26 -3
  82. package/src/sdk/mcp/server.ts +6 -1
  83. package/src/sdk/prompt-failure.ts +42 -0
  84. package/src/sdk/prompt-watchdog.ts +162 -0
  85. package/src/sdk/session-reconnect.ts +43 -0
  86. package/src/sdk/startup-capability.ts +13 -4
  87. package/src/session/agent-session.ts +541 -226
  88. package/src/session/blob-store.ts +89 -0
  89. package/src/session/internal/managed-session-scope.ts +36 -26
  90. package/src/session/internal/managed-session-storage.ts +303 -32
  91. package/src/session/internal/session-memory-sidecar.ts +2160 -0
  92. package/src/session/session-manager.ts +7057 -399
  93. package/src/session/session-storage.ts +1134 -11
  94. package/src/task/executor.ts +17 -4
  95. package/src/tools/bash.ts +109 -7
  96. package/src/tools/descriptors.test.ts +51 -0
  97. package/src/tools/find.ts +55 -26
  98. package/src/tools/todo-contract.ts +82 -13
  99. package/src/tools/todo-write.ts +2 -5
  100. package/src/tui/output-block.ts +11 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.21] - 2026-08-09
6
+
7
+ ### Fixed
8
+ - Strict session resume now sanitizes stale replay metadata in memory without mutating the selected transcript, then publishes the terminal resume breadcrumb only after the first durable user-visible write. Declined auto-continuations restore their deferred predecessor terminal instead of dropping client settlement evidence.
9
+ - Todo completion reminders now honor `todo.reminders.max` across an interactive turn rather than stopping after the first reminder.
10
+ - Hidden `nextTurn` messages that trigger a follow-up model call no longer deadlock the predecessor prompt during post-prompt settlement.
11
+
12
+ ## [0.12.20] - 2026-08-09
13
+
14
+ ### Fixed
15
+ - Parallel task children now use their durable parent/task ownership tuple as a stable provider-facing continuity identity, preventing nested managed children from contending for one websocket owner while preserving logical session headers, artifact hierarchy, detached-resume identity, and explicit provider-session overrides.
16
+ - Session contexts no longer retain materialized resident text after the backing cache disappears; subsequent reads rematerialize the existing public-safe missing-blob placeholder. Headless `--export` coverage now expects the established error for a nonexistent source file.
17
+ - SDK prompt reconciliation now preserves bounded safe provider failure codes after `agent_failed`, while retaining the fixed redacted wire/persisted message and recording a bounded local diagnostic.
18
+
19
+ - Shadow-mode cold-session builds now compare the actual sidecar-derived provider context against the authoritative eager context and expose check/mismatch counters in `/session`; corrupt provider metadata produces a counted mismatch without weakening transcript authority. Cold branch activation now resolves compacted 10k-entry branches through one bounded ordinal-index scan and one bounded transcript range read instead of scanning a dictionary partition for every ancestor, and cold dictionary lookup skips unrelated JSON decoding while retaining full partition-digest verification. Bounded first-open, exact-reopen, and lazy cold-entry paths apply the same stale OpenAI Responses replay-metadata sanitation as eager resume.
20
+ - ACP startup now rejects unrecognized command flags and incompatible terminal-auth modes through the normal CLI usage-error path, while preserving separate dash-leading system-prompt text.
21
+
22
+ - `gjc --mode acp` no longer aborts on flags it advertises in `--help`. `parseArgs` never recognized `--extension`/`-e`, `--hook`, `--no-extensions`, `--no-skills`, or `--skills`, so every one of them landed in `unknownFlags`; once unknown flags became a hard ACP failure, launching ACP with `--no-extensions` crashed at startup with `Unsupported under SDK-backed ACP: extension flags`. All five are parsed again, so `--no-extensions` starts normally while `--extension`, `--hook`, `--no-skills`, and `--skills` are rejected under ACP by their own names — the rejection list in `resolveAcpStartupOptions` had been dead code because nothing ever populated the fields it tested. A genuinely unknown flag is now reported as `unknown flags: --typo` instead of being mislabeled an extension flag.
23
+
24
+ ### Added
25
+
26
+ - Bounded-memory cold-session disk offloading is rollout-ready behind the `sessionMemory` settings namespace. `sessionMemory.mode` (default `"shadow"`) selects `off`/`shadow`/`enabled`: shadow measures without changing observable session behavior, and canary/default-on remain release-channel decisions rather than user-facing enum values. Fixed implementation budgets preserve the ≤64 MiB steady-state guarantee; transcript v5 stays authoritative and derived `.spill.*` sidecars remain disposable. Context materialization uses the typed `SessionContextTooLargeError` preflight, while `sessionMemory.contextOverflowRecovery` independently controls the async compact-once recovery path.
27
+ - Enabled explicit-path resumes validate descriptor-bound commit metadata, base/tail hashes, terminal proof, and hot-record digests before loading only the authenticated post-compaction tail. Fresh-process gates cover 120k compaction cycles, direct/captured forks, and one-million-record first build plus exact lazy reopen. Managed retained-authority startup remains on the eager compatibility path pending retained-authority range support.
28
+ - Enabled canary sessions fail closed to shadow/eager behavior after repeated sidecar build failures or failed adoption, expose the live auto-disable reason through `/session`, and clear the session-local latch through the existing `off` rollback mode. Live `enabled` → `shadow`/`off` rollback keeps existing cold entries lazily readable for the current process; the next `off`-mode start ignores derived state and restores eager compatibility.
29
+
5
30
  ## [0.12.19] - 2026-08-08
6
31
 
7
32
  ### Fixed
@@ -11,9 +36,10 @@
11
36
  ## [0.12.18] - 2026-08-08
12
37
 
13
38
  ### Fixed
39
+ - Repeated provider idle-stream stalls now stop after `retry.maxRetries` instead of inheriting the unbounded transient-error retry path. A one-off stall still retries normally, while a persistently silent stream surfaces its error rather than resubmitting the same billable context indefinitely.
14
40
 
15
41
  - SDK snapshot spill writes now retry once with a fresh temporary file when Bun reports `EBADF` during write or `fsync` under heavily contended descriptor teardown, and no longer fail when `close()` reports `EBADF` after a successful write and sync. The atomic writer removes the abandoned attempt before retrying, preserves primary I/O failures, accepts only the proven already-closed close case, and keeps every non-`EBADF` error or repeated descriptor failure fatal.
16
- - ACP now imports the synthetic model-profile provider namespace from a dependency-free SDK constant module, keeping the machine entrypoint out of model-registry, session-host, MCP-manager, and extension-runner closures enforced by `check:runtime`.
42
+ - Discord Gateway ingestion now decodes text and binary WebSocket payloads, resolves a thread's missing parent-channel metadata through Discord's REST API, and stops reconnecting after terminal authentication, sharding, version, or intent close codes.
17
43
 
18
44
  ## [0.12.17] - 2026-08-08
19
45
 
@@ -94,14 +120,6 @@
94
120
  - GJC no longer advertises `mcpCapabilities.sse`. The legacy MCP HTTP+SSE transport (spec 2024-11-05, deprecated) is not implemented: an `sse` config is routed to the Streamable HTTP transport, which never performs the required `endpoint`-event handshake, so a genuine legacy-SSE server offered by an ACP client could not connect. Locally configured `sse` entries still resolve through `createTransport` and keep working.
95
121
  - Reverse ACP responses (`fs/read_text_file`, terminal output) are now measured as the full serialized frame rather than only the inner `result`. A near-limit result passed the 256 KiB check and then tripped the WebSocket total-frame ceiling, closing the whole session instead of raising the typed `payload_too_large`.
96
122
  - `todo_write` now accepts `complete` and `completed` as aliases for the `done` operation. The status this operation sets is spelled `completed`, so models repeatedly emitted `op: "complete"` and hit a hard mid-turn tool failure; the operation vocabulary is otherwise unchanged and a completion still requires a task or phase target.
97
- ||||||| parent of d5f44548e (fix(todo): accept the operation and field synonyms models actually emit)
98
- - ACP `session/prompt` no longer hangs forever when the agent continues mid-prompt. Any continuation (todo reminder, TTSR resume, auto-continue) re-enters the agent loop and emits a second `agent_start`, which shifted the already-empty pending queue and overwrote the live prompt correlation with `undefined`. The prompt's `agent_end` then carried no correlation, `terminalizePrompt` never ran, and every ACP client (Paseo, Zed, JetBrains Air) waited until the 30-minute `sdk.promptDeadlineMs` before failing with `prompt_deadline_exceeded` — despite the answer having already streamed. `agent_start` now claims a pending correlation only when the session has no active one, and the todo-completion continuation holds the predecessor `agent_end` like every other continuation call site.
99
- - ACP clients can now render the user's own message and its image attachments. A live prompt turn emitted no `user_message_chunk` at all — that update only existed on the `session/load` replay path — so an attached image reached the model but never appeared in the client transcript. `session/prompt` now echoes the prompt's text and image content blocks as `user_message_chunk` before dispatching the turn.
100
- - An oversize ACP prompt now fails with a typed, actionable error instead of killing the session. The SDK WebSocket server caps a request frame at 256 KiB (`REQUEST_FRAME_BYTES`) and answers an oversize frame with `CloseCode::Size`, which surfaced to clients as an opaque `-32603 connection_closed` mid-turn; a 244 KiB PNG reproduced it while a 59 KiB PNG succeeded. The prompt is now measured inside its real `control_request` envelope and rejected up front with `-32602` naming the actual and permitted size.
101
- - ACP `session/cancel` and `session/close` now settle the pending `session/prompt` with `stopReason: "cancelled"` instead of surfacing a transport error. A cancel that landed during prompt preflight rejected with `busy`, and `session/close` rejected with `connection_closed`, both of which clients display as spurious errors for the user's own action. The spec requires agents to "catch these errors and return the semantically meaningful `cancelled` stop reason"; involuntary teardown still rejects.
102
- - GJC no longer advertises `mcpCapabilities.sse`. The legacy MCP HTTP+SSE transport (spec 2024-11-05, deprecated) is not implemented: an `sse` config is routed to the Streamable HTTP transport, which never performs the required `endpoint`-event handshake, so a genuine legacy-SSE server offered by an ACP client could not connect. Locally configured `sse` entries still resolve through `createTransport` and keep working.
103
- - Reverse ACP responses (`fs/read_text_file`, terminal output) are now measured as the full serialized frame rather than only the inner `result`. A near-limit result passed the 256 KiB check and then tripped the WebSocket total-frame ceiling, closing the whole session instead of raising the typed `payload_too_large`.
104
- - `todo_write` now accepts `complete` and `completed` as aliases for the `done` operation. The status this operation sets is spelled `completed`, so models repeatedly emitted `op: "complete"` and hit a hard mid-turn tool failure; the operation vocabulary is otherwise unchanged and a completion still requires a task or phase target.
105
123
 
106
124
  - `todo_write` now rejects malformed raw arguments with bounded, authority-controlled correction codes instead of a generic rejection: unknown root keys, unknown operation-entry keys, done/drop entries without a task or phase target, and unknown init list-entry keys each surface a fixed message naming the accepted shape without echoing the offending input, while recoverable payloads keep the passthrough/coercion path and the existing ask-tool codes are untouched (#3916).
107
125
  - The Alibaba Token Plan onboarding preset and `alibaba-token-plan-qwen-deepseek` profile now reference the provider-supported `qwen3.8-max` model id instead of `qwen-3.8-max`, preventing the built-in profile from selecting an HTTP 400 unsupported model (#3909).
@@ -20,6 +20,10 @@ export default class Gc extends Command {
20
20
  description: string;
21
21
  default: boolean;
22
22
  };
23
+ disk: import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
24
+ description: string;
25
+ default: boolean;
26
+ };
23
27
  "repair-session-index": import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
24
28
  description: string;
25
29
  default: boolean;
@@ -26,11 +26,77 @@ export declare function watchSessionHostBrokerLiveness(deps: {
26
26
  graceMs?: number;
27
27
  pollMs?: number;
28
28
  }): Promise<void>;
29
+ /**
30
+ * How long a session host that has served at least one client tolerates having
31
+ * no client attached before treating itself as abandoned.
32
+ *
33
+ * This cannot fire during healthy work. "Attached" is the host's own live
34
+ * socket-subscription count, so a client that is merely idle — an editor
35
+ * sitting on an open ACP session, a long agent turn with nobody typing — still
36
+ * holds a socket and resets the window on every poll. Only a client that is
37
+ * actually gone opens it, and 30 minutes is far longer than any client
38
+ * reconnect budget (ACP's is seconds), so a crashed-and-restarted client
39
+ * reattaches long before the window closes.
40
+ */
41
+ export declare const SESSION_HOST_DETACHED_IDLE_GRACE_MS: number;
42
+ /**
43
+ * How long a freshly spawned session host waits for its very first client
44
+ * before treating itself as abandoned.
45
+ *
46
+ * A host is ready before its client has finished dialing, so a host that has
47
+ * never seen an attachment must not be judged by the detached window above. One
48
+ * hour is orders of magnitude beyond the slowest observed cold start (worktree
49
+ * preparation, MCP server launch, model-profile application) and beyond any
50
+ * lifecycle readiness deadline the broker will wait on, so it can only elapse
51
+ * for a host nobody ever came for.
52
+ */
53
+ export declare const SESSION_HOST_FIRST_ATTACH_GRACE_MS: number;
54
+ /**
55
+ * Resolves once this host is provably abandoned: either it has been detached
56
+ * from every client for the full idle grace, or nobody has come for it at all
57
+ * for the full first-attach grace.
58
+ *
59
+ * `readAttachedClients` reports the host's own live client/socket subscription
60
+ * count; `undefined` means the SDK endpoint publishes no such evidence yet and
61
+ * is deliberately *not* treated as detachment, so a host can never be reaped on
62
+ * missing evidence — only on observed absence of clients.
63
+ *
64
+ * `readWorkInFlight` reports whether the host is running agent work right now.
65
+ * Work is positive proof that a client did come for this host: a prompt can
66
+ * only arrive over the endpoint a client dialed. So for a host that has never
67
+ * been *observed* attached — a client that connected, prompted and dropped
68
+ * between two 30s polls, or a client count that is momentarily unreadable — the
69
+ * first-attach window is measured from the last moment work was in flight
70
+ * rather than from process start. That still bounds a host whose SDK runtime
71
+ * never came up: with no transport it can never receive a prompt, so it never
72
+ * reports work and its window keeps running from start. And any real turn ends,
73
+ * after which the window resumes; live work defers the bound, never removes it.
74
+ */
75
+ export declare function watchSessionHostClientAttachment(deps: {
76
+ readAttachedClients: () => number | undefined;
77
+ readWorkInFlight?: () => boolean;
78
+ now?: () => number;
79
+ sleep?: (ms: number) => Promise<void>;
80
+ idleGraceMs?: number;
81
+ firstAttachGraceMs?: number;
82
+ pollMs?: number;
83
+ }): Promise<void>;
29
84
  /** Opens lifecycle-authorized history without letting replacement content reach readiness. */
30
85
  export declare function openLifecycleSessionManager(request: SessionLifecycleLaunchRequest, cwd: string, agentDir: string): Promise<{
31
86
  parsed: ParsedArgs;
32
87
  sessionManager: SessionManager | undefined;
33
88
  }>;
89
+ /**
90
+ * Starts the memory backend a lifecycle session deferred past its readiness
91
+ * window.
92
+ *
93
+ * Deliberately not awaited: the local backend summarises every queued rollout
94
+ * through the model, so its duration scales with the backlog and would eat the
95
+ * broker's readiness budget. The session is already published as ready here, so
96
+ * a failure is a degraded-memory condition, not a startup failure — it is
97
+ * logged and swallowed instead of surfacing as an unhandled rejection.
98
+ */
99
+ export declare function startMemoryBackendAfterReadiness(start: () => Promise<void>): void;
34
100
  /** Runs the same persisted AgentSession bootstrap used by the production CLI. */
35
101
  export declare function runSessionHost(timing?: {
36
102
  now?: () => number;
@@ -2500,6 +2500,15 @@ export declare const SETTINGS_SCHEMA: {
2500
2500
  }];
2501
2501
  };
2502
2502
  };
2503
+ readonly "sessionMemory.mode": {
2504
+ readonly type: "enum";
2505
+ readonly values: readonly ["off", "shadow", "enabled"];
2506
+ readonly default: "shadow";
2507
+ };
2508
+ readonly "sessionMemory.contextOverflowRecovery": {
2509
+ readonly type: "boolean";
2510
+ readonly default: true;
2511
+ };
2503
2512
  readonly "edit.mode": {
2504
2513
  readonly type: "enum";
2505
2514
  readonly values: import("../edit").EditMode[];
@@ -3273,6 +3282,26 @@ export declare const SETTINGS_SCHEMA: {
3273
3282
  readonly description: "Parent-process RSS (MB) above which idle tabs are opportunistically evicted LRU.";
3274
3283
  };
3275
3284
  };
3285
+ readonly "gc.sessions.maxAgeDays": {
3286
+ readonly type: "number";
3287
+ readonly default: 60;
3288
+ readonly validate: (value: number) => boolean;
3289
+ };
3290
+ readonly "gc.sessions.maxTotalBytes": {
3291
+ readonly type: "number";
3292
+ readonly default: 0;
3293
+ readonly validate: (value: number) => boolean;
3294
+ };
3295
+ readonly "gc.natives.keepVersions": {
3296
+ readonly type: "number";
3297
+ readonly default: 2;
3298
+ readonly validate: (value: number) => boolean;
3299
+ };
3300
+ readonly "gc.backups.maxAgeDays": {
3301
+ readonly type: "number";
3302
+ readonly default: 30;
3303
+ readonly validate: (value: number) => boolean;
3304
+ };
3276
3305
  readonly "resourceGc.sweepIntervalMs": {
3277
3306
  readonly type: "number";
3278
3307
  readonly default: 30000;
@@ -4465,6 +4494,10 @@ export interface ShellMinimizerSettings {
4465
4494
  except: string[];
4466
4495
  maxCaptureBytes: number;
4467
4496
  }
4497
+ export interface SessionMemorySettings {
4498
+ mode: "off" | "shadow" | "enabled";
4499
+ contextOverflowRecovery: boolean;
4500
+ }
4468
4501
  export interface MemoryGuardSettings {
4469
4502
  enabled: boolean;
4470
4503
  checkIntervalMs: number;
@@ -4538,6 +4571,7 @@ export interface GroupTypeMap {
4538
4571
  thinkingBudgets: ThinkingBudgetsSettings;
4539
4572
  stt: SttSettings;
4540
4573
  memoryGuard: MemoryGuardSettings;
4574
+ sessionMemory: SessionMemorySettings;
4541
4575
  modelRoles: Record<string, ModelSelectorValue>;
4542
4576
  modelTags: ModelTagsSettings;
4543
4577
  cycleOrder: string[];
@@ -122,6 +122,11 @@ export interface GcReport {
122
122
  session_index?: GcSessionIndexHealth;
123
123
  /** Managed-scope capacity, reported only when it is near or past the budget. */
124
124
  session_scope?: GcSessionScopeUsage;
125
+ /**
126
+ * Disk-retention findings. Present only when `--disk` was passed; the
127
+ * PID-liveness axis above is unchanged by its absence or presence.
128
+ */
129
+ disk?: GcDiskReport;
125
130
  }
126
131
  export interface GcRunResult {
127
132
  stdout: string;
@@ -149,9 +154,100 @@ export declare function collectGcReport(adapters: GcStoreAdapter[], ctx: GcConte
149
154
  * - prune mode with a failed intended removal => 1
150
155
  * - warnings alone never fail the run
151
156
  * - otherwise => 0
157
+ *
158
+ * The disk axis reuses the same policy against its own errors/failures, and is
159
+ * inert when `--disk` was not passed (`report.disk` is then undefined). A
160
+ * fail-closed KEEP is never a failure — only a hard scan error or a reclaim
161
+ * that was attempted and threw.
152
162
  */
153
163
  export declare function computeExitCode(report: GcReport): number;
154
- export declare function runGjcGcCommand(argv: string[], cwd?: string, env?: NodeJS.ProcessEnv, adapters?: GcStoreAdapter[]): Promise<GcRunResult>;
164
+ export declare function runGjcGcCommand(argv: string[], cwd?: string, env?: NodeJS.ProcessEnv, adapters?: GcStoreAdapter[], diskPolicy?: Partial<GcDiskPolicy>): Promise<GcRunResult>;
155
165
  export declare function gcHelpText(): string;
156
166
  /** Lazily assemble the real store adapters (kept lazy to avoid import cycles). */
157
167
  export declare function defaultGcAdapters(): Promise<GcStoreAdapter[]>;
168
+ /** On-disk surfaces the retention axis can reclaim. */
169
+ export type GcDiskSurface = "sessions" | "blobs" | "natives" | "backups";
170
+ export declare const GC_DISK_SURFACES: readonly GcDiskSurface[];
171
+ export type GcDiskAction = "keep" | "would_reclaim" | "reclaimed" | "reclaim_failed";
172
+ export interface GcDiskRecord {
173
+ surface: GcDiskSurface;
174
+ /** Session id, blob hash, natives version, or backup entry name. */
175
+ id: string;
176
+ path: string;
177
+ bytes: number;
178
+ age_days: number;
179
+ action: GcDiskAction;
180
+ reason: string;
181
+ error?: string;
182
+ /** Set when `bytes` is a floor because a walk was capped or partially unreadable. */
183
+ partial?: true;
184
+ /** Set when this entry was a reclaim candidate that its surface withheld on incomplete evidence. */
185
+ withheld?: true;
186
+ }
187
+ /**
188
+ * Why a surface refused to reclaim anything, and how much it withheld to stay
189
+ * fail-closed. Present only when the surface declined.
190
+ */
191
+ export interface GcDiskDeclined {
192
+ reason: string;
193
+ withheld: number;
194
+ withheld_bytes: number;
195
+ }
196
+ export interface GcDiskSurfaceReport {
197
+ surface: GcDiskSurface;
198
+ root: string;
199
+ scanned: number;
200
+ scanned_bytes: number;
201
+ reclaimable: number;
202
+ reclaimable_bytes: number;
203
+ reclaimed: number;
204
+ reclaimed_bytes: number;
205
+ kept: number;
206
+ kept_bytes: number;
207
+ failed: number;
208
+ /** Set when the surface declined to reclaim anything because its evidence was incomplete. */
209
+ declined?: GcDiskDeclined;
210
+ records: GcDiskRecord[];
211
+ }
212
+ export interface GcDiskError {
213
+ surface: GcDiskSurface;
214
+ scope: string;
215
+ message: string;
216
+ }
217
+ /** Retention policy, mirroring the `gc.*` settings one-for-one. */
218
+ export interface GcDiskPolicy {
219
+ sessions_max_age_days: number;
220
+ /** 0 disables the size axis; only the age axis retires transcripts then. */
221
+ sessions_max_total_bytes: number;
222
+ natives_keep_versions: number;
223
+ backups_max_age_days: number;
224
+ }
225
+ export interface GcDiskReport {
226
+ dry_run: boolean;
227
+ policy: GcDiskPolicy;
228
+ surfaces: Record<GcDiskSurface, GcDiskSurfaceReport>;
229
+ totals: {
230
+ scanned_bytes: number;
231
+ reclaimable_bytes: number;
232
+ reclaimed_bytes: number;
233
+ kept_bytes: number;
234
+ failed: number;
235
+ };
236
+ errors: GcDiskError[];
237
+ }
238
+ /** Schema-backed defaults, so the CLI and the settings surface cannot drift. */
239
+ export declare const GC_DISK_POLICY_DEFAULTS: GcDiskPolicy;
240
+ export declare function resolveGcDiskPolicy(overrides?: Partial<GcDiskPolicy>): GcDiskPolicy;
241
+ /**
242
+ * Run the disk-retention axis. Nothing is mutated unless `prune` is true; the
243
+ * dry-run report projects exactly the same decisions a prune would make.
244
+ */
245
+ export declare function collectGcDiskReport(input: {
246
+ agentDir: string;
247
+ env: NodeJS.ProcessEnv;
248
+ policy: GcDiskPolicy;
249
+ prune: boolean;
250
+ now?: number;
251
+ runningVersion?: string;
252
+ }): Promise<GcDiskReport>;
253
+ export declare function buildGcDiskReportText(disk: GcDiskReport): string;
@@ -1,5 +1,6 @@
1
1
  import { type Agent, type AgentSideConnection, type AuthenticateRequest, type AuthenticateResponse, type AvailableCommand, type ClientCapabilities, type CloseSessionRequest, type CloseSessionResponse, type DeleteSessionRequest, type DeleteSessionResponse, type ForkSessionRequest, type ForkSessionResponse, type InitializeRequest, type InitializeResponse, type ListSessionsRequest, type ListSessionsResponse, type LoadSessionRequest, type LoadSessionResponse, type NewSessionRequest, type NewSessionResponse, type PromptRequest, type PromptResponse, type ResumeSessionRequest, type ResumeSessionResponse, type SetSessionConfigOptionRequest, type SetSessionConfigOptionResponse, type SetSessionModeRequest, type SetSessionModeResponse } from "@agentclientprotocol/sdk";
2
2
  import { type AcpProviderRegistration, type AcpReverseConnection, AcpSdkAdapter } from "../../sdk/acp";
3
+ import { type PromptWatchdogClock } from "../../sdk/prompt-watchdog";
3
4
  import type { AcpStartupOptions } from "./startup-options";
4
5
  type JsonObject = Record<string, unknown>;
5
6
  /** Applies ACP's offset cursor after narrowing the broker listing to the requested cwd. */
@@ -30,7 +31,37 @@ export interface TranscriptReplayContent {
30
31
  reason: "historical_transcript_images_unavailable";
31
32
  };
32
33
  }
33
- export declare function transcriptReplayContent(entry: unknown): TranscriptReplayContent;
34
+ /** Machine-readable reason replay could not restore a transcript entry. */
35
+ export type TranscriptReplaySkipReason = "transcript_body_unavailable" | "transcript_tool_call_unavailable";
36
+ /**
37
+ * Replay decides per entry. An entry whose production body is missing is not
38
+ * replayable, and the caller reports that boundary instead of failing the whole
39
+ * load; fabricating an empty body would replay a message that never existed.
40
+ */
41
+ export type TranscriptReplayEntry = {
42
+ replayable: true;
43
+ content: TranscriptReplayContent;
44
+ } | {
45
+ replayable: false;
46
+ reason: TranscriptReplaySkipReason;
47
+ };
48
+ export declare function transcriptReplayContent(entry: unknown): TranscriptReplayEntry;
49
+ /**
50
+ * `transcript.list` answers an entry larger than one page with a body-less row
51
+ * `{ id, error: { code: "item_too_large" }, continuations }`. Each continuation is a
52
+ * `Q23` (`resource.body`) descriptor for one indexed string field of that entry, so
53
+ * the row is a pointer to the largest message in the session rather than a broken
54
+ * entry. Replay follows it instead of dropping the message.
55
+ */
56
+ export interface TranscriptContinuation {
57
+ query: string;
58
+ resourceKind: string;
59
+ resourceId: string;
60
+ revision: string;
61
+ itemId: string;
62
+ field: string;
63
+ }
64
+ export declare function transcriptContinuations(entry: unknown): TranscriptContinuation[];
34
65
  /**
35
66
  * Collect every page of `providers.list/active` (Q29, GJC >= 0.12.8) and
36
67
  * return the providers with usable stored credentials or credentialless
@@ -250,6 +281,7 @@ export declare class AcpAgent implements Agent {
250
281
  agentDir?: string;
251
282
  startupOptions?: AcpStartupOptions;
252
283
  cancelSettlementGraceMs?: number;
284
+ promptWatchdogClock?: PromptWatchdogClock;
253
285
  } | unknown);
254
286
  initialize(params: InitializeRequest): Promise<InitializeResponse>;
255
287
  authenticate(params: AuthenticateRequest): Promise<AuthenticateResponse>;
@@ -26,4 +26,11 @@ export declare class BashExecutionComponent extends Container {
26
26
  * Get the command that was executed.
27
27
  */
28
28
  getCommand(): string;
29
+ /**
30
+ * Record that this execution's message reached session state, so a transcript
31
+ * rebuild renders it from the session instead of keeping this live block parked.
32
+ */
33
+ markResultPersisted(): void;
34
+ /** Whether the session already holds this execution's message. */
35
+ hasPersistedResult(): boolean;
29
36
  }
@@ -20,4 +20,11 @@ export declare class EvalExecutionComponent extends Container {
20
20
  }): void;
21
21
  getOutput(): string;
22
22
  getCode(): string;
23
+ /**
24
+ * Record that this execution's message reached session state, so a transcript
25
+ * rebuild renders it from the session instead of keeping this live block parked.
26
+ */
27
+ markResultPersisted(): void;
28
+ /** Whether the session already holds this execution's message. */
29
+ hasPersistedResult(): boolean;
23
30
  }
@@ -37,6 +37,15 @@ export declare class CollapsedChatHistoryComponent implements Component {
37
37
  invalidate(): void;
38
38
  }
39
39
  export declare function addChatChild(ctx: InteractiveModeContext, component: Component): void;
40
+ /**
41
+ * Parked `!`/`$` execution components are listed in `pendingBashComponents` /
42
+ * `pendingPythonComponents`, but `pendingMessagesContainer` is the only
43
+ * authority on parentage: `/clear`, `/context-clear`, extension redraws and the
44
+ * selector all call `pendingMessagesContainer.clear()`, which disposes and
45
+ * evicts parked components without touching those arrays. Drop every entry the
46
+ * container no longer holds so nothing downstream can move a dead component.
47
+ */
48
+ export declare function syncPendingExecutionComponents(ctx: InteractiveModeContext): void;
40
49
  export declare function trimChatChildren(ctx: InteractiveModeContext): void;
41
50
  export declare class UiHelpers {
42
51
  #private;
@@ -84,7 +93,7 @@ export declare class UiHelpers {
84
93
  flushCompactionQueue(options?: {
85
94
  willRetry?: boolean;
86
95
  }): Promise<void>;
87
- /** Move pending bash components from pending area to chat */
96
+ /** Move pending bash/python components from the pending area to chat */
88
97
  flushPendingBashComponents(): void;
89
98
  findLastAssistantMessage(): AssistantMessage | undefined;
90
99
  extractAssistantText(message: AssistantMessage): string;
@@ -1,4 +1,6 @@
1
- import { SdkClient, SdkClientError, type SdkClientOptions, type SdkFrame } from "../client";
1
+ import { SdkClient, SdkClientError, type SdkFrame } from "../client";
2
+ import { ACP_SESSION_RECONNECT } from "../session-reconnect";
3
+ import type { SessionLifecycleMcpServer } from "./mcp";
2
4
  type JsonObject = Record<string, unknown>;
3
5
  /** The small agent-side ACP surface used for reverse requests. */
4
6
  export interface AcpReverseConnection {
@@ -26,23 +28,14 @@ export declare class AcpSdkAdapterError extends Error {
26
28
  readonly code: string;
27
29
  constructor(code: string, message?: string);
28
30
  }
29
- export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
30
- export type AcpFrameHandler = (frame: SdkFrame) => void;
31
- type AcpSessionReconnectOptions = Required<Pick<SdkClientOptions, "reconnectAttempts" | "reconnectBackoffMs" | "reconnectMaxBackoffMs">>;
32
31
  /**
33
- * Reconnect budget for long-lived ACP sessions.
34
- *
35
- * Invariant: the client's total reconnect budget must outlive the host heartbeat
36
- * TTL ({@link HEARTBEAT_TTL_MS}). The SDK host drops a session whose client has
37
- * not ponged within that TTL, so a client that gives up reconnecting sooner turns
38
- * every stall the host reaps into a permanently lost session ("SDK WebSocket
39
- * reconnect attempts exhausted"). The transport defaults (3 attempts at 25ms base
40
- * = 175ms total) are correct only for one-shot request clients.
41
- *
42
- * Backoff ramps 250 -> 500 -> 1000 and then holds at the 2s cap, so individual
43
- * sleeps stay short and recovery is prompt once the host answers again.
32
+ * The error an ACP session launch must throw once a lifecycle request that carried MCP
33
+ * servers has failed.
44
34
  */
45
- export declare const ACP_SESSION_RECONNECT: AcpSessionReconnectOptions;
35
+ export declare function acpMcpLaunchFailure(error: unknown, mcpServers: SessionLifecycleMcpServer[]): unknown;
36
+ export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
37
+ export type AcpFrameHandler = (frame: SdkFrame) => void;
38
+ export { ACP_SESSION_RECONNECT };
46
39
  /**
47
40
  * Pure ACP-to-SDK adapter. It deliberately owns neither an AgentSession nor an
48
41
  * ACP bridge: all session work is performed through authenticated v3 frames.
@@ -81,4 +74,3 @@ export declare class AcpSdkAdapter {
81
74
  handle(method: string, params?: JsonObject): Promise<unknown>;
82
75
  registerProvider(provider: AcpProviderRegistration): Promise<void>;
83
76
  }
84
- export {};
@@ -1,2 +1,2 @@
1
1
  export type { AcpProviderRegistration, AcpReverseConnection, AcpSdkAdapterOptions } from "./adapter";
2
- export { ACP_SESSION_RECONNECT, AcpSdkAdapter, AcpSdkAdapterError } from "./adapter";
2
+ export { ACP_SESSION_RECONNECT, AcpSdkAdapter, AcpSdkAdapterError, acpMcpLaunchFailure } from "./adapter";
@@ -19,7 +19,7 @@ type ResolvedBrokerSettings = {
19
19
  heartbeatTtlMs: number;
20
20
  resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
21
21
  };
22
- export type BrokerErrorCode = "idempotency_conflict" | "terminal_uncertain" | "broker_restarting" | "unavailable" | "endpoint_stale" | "resource_gone" | "invalid_input" | "spawn_failed" | "readiness_timeout" | "close_refused" | "not_found" | "live_session" | "cleanup_pending" | (string & {});
22
+ export type BrokerErrorCode = "idempotency_conflict" | "terminal_uncertain" | "broker_restarting" | "unavailable" | "endpoint_stale" | "resource_gone" | "invalid_input" | "spawn_failed" | "startup_admission_timeout" | "startup_admission_refused" | "readiness_timeout" | "close_refused" | "not_found" | "live_session" | "cleanup_pending" | (string & {});
23
23
  export type BrokerCleanupIdentity = {
24
24
  dev: string;
25
25
  ino: string;
@@ -120,6 +120,77 @@ export type BrokerResponse = {
120
120
  durableEffects?: LifecycleDurableEffectsReceipt;
121
121
  startupFailure?: LifecycleStartupFailureReceipt;
122
122
  };
123
+ /** Tombstone prefix used by {@link Broker.reclaimStaleLock} when a dead owner's lock is renamed aside. */
124
+ export declare const BROKER_LOCK_TOMBSTONE_PREFIX = ".broker.lock.stale-";
125
+ /**
126
+ * Recovery directories left beside the lock by manual and older automated broker
127
+ * restarts. Nothing writes them today, but installs that ever recovered by hand
128
+ * still carry them, so the reaper owns them alongside its own tombstones.
129
+ */
130
+ export declare const BROKER_LOCK_BACKUP_PREFIXES: readonly ["broker-restart-backup-", "broker-stale-backup-"];
131
+ /**
132
+ * Age bound before a reclaimed lock artifact may be removed. Generous enough
133
+ * that a broker still settling after a reclaim can never have its own successor
134
+ * state deleted underneath it.
135
+ */
136
+ export declare const BROKER_LOCK_ARTIFACT_GRACE_MS: number;
137
+ /** Why a candidate lock artifact survived a reap pass. */
138
+ export type BrokerLockArtifactRetentionReason = "within-grace" | "owner-alive" | "owner-record-unreadable" | "owner-record-missing" | "not-a-directory" | "removal-failed";
139
+ export interface BrokerLockArtifactRetention {
140
+ path: string;
141
+ reason: BrokerLockArtifactRetentionReason;
142
+ }
143
+ export interface BrokerLockArtifactReapResult {
144
+ removed: string[];
145
+ retained: BrokerLockArtifactRetention[];
146
+ }
147
+ /**
148
+ * Remove reclaimed broker lock tombstones and legacy restart backups older than
149
+ * the grace window.
150
+ *
151
+ * `#reclaimStaleLock` renames a dead owner's lock to a tombstone named by a hash
152
+ * of the lock's dev+ino, so a machine accrues one directory per dead owner and
153
+ * nothing ever removed them (54 on the install in #3963). Reaping is
154
+ * best-effort and fail-closed: anything live, unreadable, permission-denied, or
155
+ * otherwise ambiguous is kept and the reason is logged.
156
+ */
157
+ export declare function reapStaleBrokerLockArtifacts(input: {
158
+ agentDir: string;
159
+ now?: number;
160
+ graceMs?: number;
161
+ pidAlive?: (pid: number) => boolean;
162
+ }): Promise<BrokerLockArtifactReapResult>;
163
+ export interface StartupAdmissionTiming {
164
+ now(): number;
165
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
166
+ }
167
+ export type StartupAdmissionResult<T> = {
168
+ status: "completed";
169
+ admittedAt: number;
170
+ value: T;
171
+ } | {
172
+ status: "admission_timeout";
173
+ reason: "admission_timeout";
174
+ } | {
175
+ status: "admission_refused";
176
+ reason: "admission_refused";
177
+ };
178
+ export declare function sdkHostStartupConcurrency(availableParallelism?: number): number;
179
+ export declare class StartupAdmissionQueue {
180
+ #private;
181
+ readonly limit: number;
182
+ constructor(limit: number);
183
+ run<T>(queueWaitMs: number, timing: StartupAdmissionTiming, task: (admittedAt: number) => Promise<T>): Promise<StartupAdmissionResult<T>>;
184
+ /**
185
+ * Refuse every queued startup and every later one. A broker that can no longer
186
+ * prove it owns the published root must not spawn children through slots that
187
+ * free up while it is fenced. The epoch also invalidates a waiter that was
188
+ * granted but has not crossed the task execution boundary yet.
189
+ */
190
+ close(): void;
191
+ /** Accept later startups after fresh publication ownership has been proven. */
192
+ reopen(): void;
193
+ }
123
194
  export declare class Broker {
124
195
  #private;
125
196
  readonly settings: ResolvedBrokerSettings;
@@ -127,11 +198,30 @@ export declare class Broker {
127
198
  readonly ledger: LifecycleLedger;
128
199
  discovery: BrokerDiscovery | null;
129
200
  constructor(settings: BrokerSettings);
201
+ runStartup<T>(queueWaitMs: number, timing: StartupAdmissionTiming, task: (admittedAt: number) => Promise<T>): Promise<StartupAdmissionResult<T>>;
130
202
  start(): Promise<BrokerDiscovery>;
131
203
  get ownsDiscovery(): boolean;
132
204
  get completion(): Promise<void>;
133
205
  status(): RedactedBrokerDiscovery | null;
134
206
  heartbeat(): Promise<void>;
207
+ /**
208
+ * Revalidate retained publication ownership and begin one synchronous effect in
209
+ * the same stack. The callback is the authority boundary: callers must perform
210
+ * the authorized effect inside it, so no awaited work can separate proof from
211
+ * the effect it authorizes.
212
+ */
213
+ runSynchronousEffectWithFreshPublicationAuthority<T>(effect: () => T, ..._synchronousOnly: T extends PromiseLike<unknown> ? [never] : []): {
214
+ authorized: true;
215
+ value: T;
216
+ } | {
217
+ authorized: false;
218
+ };
219
+ /**
220
+ * A stop may take the owning path only while it can prove it still owns the root.
221
+ * Claiming ownership it cannot prove keeps the admission queue open, so a startup
222
+ * queued behind this broker is granted a slot that frees after completion and
223
+ * spawns a child the broker has no authority over.
224
+ */
135
225
  stop(): Promise<void>;
136
226
  handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse>;
137
227
  }
@@ -139,6 +229,8 @@ export declare class Broker {
139
229
  export declare function setTerminalPersistenceHookForTest(broker: Broker, hook: (() => void) | undefined): void;
140
230
  /** Test-only hook for shortening the bounded ambiguity deadline. */
141
231
  export declare function setAmbiguityGraceForTest(broker: Broker, graceMs: number | undefined): void;
232
+ /** Test-only hook for shortening the startup lock-artifact reap bound. */
233
+ export declare function setLockArtifactGraceForTest(broker: Broker, graceMs: number | undefined): void;
142
234
  /** Test-only hook for forcing the observation the publication watchdog sees. */
143
235
  export declare function setPublicationObservationForTest(broker: Broker, observation: BrokerPublicationObservation | undefined): void;
144
236
  export {};
@@ -1,4 +1,5 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
+ import type { FileHandle } from "node:fs/promises";
2
3
  import { type BrokerDiscovery } from "./discovery";
3
4
  import { type SdkInternalSpawnCommand } from "./runtime";
4
5
  export interface EnsureBrokerSettings {
@@ -11,6 +12,22 @@ export interface EnsureBrokerSettings {
11
12
  */
12
13
  env?: NodeJS.ProcessEnv;
13
14
  }
15
+ /**
16
+ * Tail of the detached broker's stderr folded into a discovery failure.
17
+ *
18
+ * The broker used to spawn with `stdio: "ignore"`, so a broker that exited
19
+ * cleanly told the caller nothing beyond `code=0` (#3963). Its stderr goes to a
20
+ * file instead of a pipe because the child is detached and outlives this
21
+ * process: a pipe would break under it the moment the parent exits.
22
+ */
23
+ export declare const BROKER_SPAWN_LOG_TAIL_BYTES = 4096;
24
+ export interface BrokerSpawnLog {
25
+ path: string;
26
+ handle: FileHandle;
27
+ }
28
+ /** Opens an isolated, bounded-lifetime diagnostic sink for one broker spawn. */
29
+ export declare function openBrokerSpawnLog(agentDir: string): Promise<BrokerSpawnLog | undefined>;
30
+ export declare function readBrokerSpawnLogTail(spawnLogPath: string): Promise<string>;
14
31
  export interface FixtureBrokerLease {
15
32
  /** Backward-compatible fixture cleanup alias for exact child termination. */
16
33
  close(): Promise<void>;
@@ -5,4 +5,5 @@ export * from "./identity";
5
5
  export * from "./lifecycle";
6
6
  export * from "./lifecycle-ledger";
7
7
  export * from "./session-index";
8
+ export * from "./startup-budget";
8
9
  export * from "./transport";