@agent-compose/sdk 0.8.2 → 0.8.4

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 (43) hide show
  1. package/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
  2. package/dist/agent/agent-context.d.ts +1 -1
  3. package/dist/agent/agent-loop.d.ts +9 -1
  4. package/dist/agent/desktop-open.d.ts +184 -0
  5. package/dist/agent/perf-sampler.d.ts +99 -0
  6. package/dist/agent/services-manifest.d.ts +88 -0
  7. package/dist/agent/services-restore.d.ts +58 -0
  8. package/dist/client.d.ts +164 -8
  9. package/dist/display.d.ts +17 -0
  10. package/dist/index.d.ts +14 -5
  11. package/dist/index.js +1393 -53
  12. package/dist/runtimes/_cli-agent.d.ts +359 -2
  13. package/dist/runtimes/claude-code.d.ts +12 -0
  14. package/dist/runtimes/codex.d.ts +8 -0
  15. package/dist/runtimes/openai-desktop.js +1329 -53
  16. package/dist/runtimes/session-env.test.d.ts +14 -0
  17. package/dist/sandbox.d.ts +1 -1
  18. package/dist/types/api-conversations.d.ts +309 -1
  19. package/dist/types/api-factory.d.ts +115 -10
  20. package/dist/types/api-runs.d.ts +21 -0
  21. package/dist/types/protocol.d.ts +44 -1
  22. package/dist/types/runtime.d.ts +120 -0
  23. package/package.json +1 -1
  24. package/src/agent/agent-context.ts +100 -11
  25. package/src/agent/agent-loop.ts +15 -3
  26. package/src/agent/desktop-open.ts +418 -0
  27. package/src/agent/perf-sampler.ts +202 -0
  28. package/src/agent/services-manifest.ts +356 -0
  29. package/src/agent/services-restore.ts +195 -0
  30. package/src/client.ts +328 -12
  31. package/src/display.ts +44 -1
  32. package/src/index.ts +65 -2
  33. package/src/runtimes/_cli-agent.ts +911 -35
  34. package/src/runtimes/claude-code.ts +198 -14
  35. package/src/runtimes/codex.ts +58 -1
  36. package/src/sandbox/providers/e2b.ts +29 -1
  37. package/src/sandbox/providers/local.ts +16 -4
  38. package/src/sandbox.ts +1 -1
  39. package/src/types/api-conversations.ts +307 -3
  40. package/src/types/api-factory.ts +118 -10
  41. package/src/types/api-runs.ts +23 -0
  42. package/src/types/protocol.ts +44 -1
  43. package/src/types/runtime.ts +122 -0
@@ -23,13 +23,243 @@
23
23
  * (see each spec's `authEnv`); `commands.run` inherits the sandbox env, so
24
24
  * values set via `agentc secrets set` are visible to the CLI.
25
25
  */
26
- import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider, ToolCallGateResult } from "../index.js";
26
+ import type { AgentMessage, ModelExecutionContract, RunnerLivenessVerdict, RuntimeOptions, SandboxProvider, ToolCallGateResult, TurnExitNotify } from "../index.js";
27
+ import type { GuestPerfSample } from "../agent/perf-sampler.js";
27
28
  import type { ProcessorContext, ToolCall } from "../processors/processor.js";
28
29
  /** Backoff between tail re-attach attempts after a mid-turn stream fault
29
30
  * (durable detached transport). Short: the runner is alive and producing,
30
31
  * and each retry costs one exec; the executor's evidence machinery — not
31
32
  * this loop — bounds a truly dead sandbox. Exported for tests. */
32
33
  export declare const TAIL_REATTACH_DELAY_MS = 2000;
34
+ /** Cadence of the in-guest heartbeat writer (seconds — it is a shell loop). */
35
+ export declare const RUNNER_HEARTBEAT_INTERVAL_SECONDS = 10;
36
+ /** How recently a file under the CLI's own task state dir (~/.claude/tasks)
37
+ * must have been touched to count as a LIVE background task in the busy
38
+ * sentinel (minutes — it is `find -mmin`). A running task's state/output
39
+ * files are written continuously, so two minutes is generous; a finished
40
+ * task's files go quiet and age out on their own. */
41
+ export declare const RUNNER_BUSY_TASK_FRESH_MINUTES = 2;
42
+ /** Cap on the task-id list the busy sentinel publishes — the ids are for
43
+ * server-side log attribution, not an inventory. */
44
+ export declare const RUNNER_BUSY_TASK_ID_LIMIT = 16;
45
+ /** The sh fragment the heartbeat subshell runs each beat to maintain the
46
+ * busy sentinel — the guest half of the background-work marker contract:
47
+ * - `<promptPath>.busy` — the COUNT of recently-active files under the
48
+ * CLI's task state dir (`$ac_busy` stays set in-shell for the loop's
49
+ * own exit decision);
50
+ * - `<promptPath>.tasks` — the task IDS those files belong to (first
51
+ * path segment under tasks/, deduped, capped).
52
+ * The marker's freshness stamp is the files' own mtime (each beat rewrites
53
+ * them; the server probe gates on `-mmin -1`). Cheap (one bounded find
54
+ * over a small dir), and read by the server's park path so a VM hosting
55
+ * live background work is never frozen mid-flight (2026-08-15 forensics:
56
+ * a 3-min idle park suspended a VM with three research agents mid-flight).
57
+ * A missing tasks dir counts 0. Exported for tests. */
58
+ export declare function runnerBusySentinelFragment(busyPath: string, tasksPath: string): string;
59
+ /** A heartbeat older than this (by the GUEST's own clock — the probe reads
60
+ * `date +%s%3N` in the same exec, so host clock skew is irrelevant) is
61
+ * stale: three missed writes. */
62
+ export declare const RUNNER_HEARTBEAT_STALE_MS: number;
63
+ /** Watchdog cadence while a tail stream is attached. Each tick is one fresh
64
+ * short exec; a healthy stream makes every tick a no-op. */
65
+ export declare const TAIL_WATCHDOG_INTERVAL_MS = 15000;
66
+ /** Explicit ceiling on one durable-trio probe exec. A probe that can't
67
+ * answer is "probe-failed" — never a verdict. */
68
+ export declare const TURN_PROBE_TIMEOUT_MS = 10000;
69
+ /** After this many stream deaths in ONE turn, stop re-attaching tails and
70
+ * finish the turn on durable-file polling alone (the GHA permanent-
71
+ * fallback pattern): a wire that killed three streams will kill the
72
+ * fourth, and the durable plane already delivers everything. */
73
+ export declare const TAIL_ABANDON_AFTER_STREAM_DEATHS = 3;
74
+ /** Poll cadence once streaming is abandoned — a full durable read per poll
75
+ * (envd HTTP, ~65ms RTT), consumed from the byte offset. Latency-tuned:
76
+ * this is the degraded path, correctness never depends on it being fast. */
77
+ export declare const DURABLE_POLL_INTERVAL_MS = 1000;
78
+ /** The claude CLI's stderr complaint when `--resume <id>` names a thread
79
+ * that does not exist on this machine (the 2026-08-18 split-brain
80
+ * forensic). When a nonzero exit's stderr carries it, the transport
81
+ * surfaces the stderr as the turn's LAST error event even though a bare
82
+ * result error already streamed — the consumer's classifier needs the
83
+ * specific complaint, not the generic `error_during_execution` token. */
84
+ export declare const RESUME_TARGET_MISSING_STDERR: RegExp;
85
+ /** One durable-trio reading, parsed from the probe exec's single line. */
86
+ export interface TurnProbeReading {
87
+ /** Bytes currently in the durable stdout file (-1: unreadable/absent). */
88
+ size: number;
89
+ /** Is the detached runner's process group leader alive (kill -0)? */
90
+ alive: boolean;
91
+ /** Last heartbeat value (epoch ms, guest clock; 0 = no heartbeat yet). */
92
+ hbMs: number;
93
+ /** The guest's own clock at probe time (epoch ms). */
94
+ nowMs: number;
95
+ /** Is the exit sentinel already in the durable file (turn FINISHED)? */
96
+ sentinelSeen: boolean;
97
+ /** Latest guest perf sample (the trailing `.perf` token field), when the
98
+ * guest wrote one AND it parsed clean. Absent = older guest image, no
99
+ * sample yet, or a garbled token — evidence-absent, never an error.
100
+ * Telemetry-only: no liveness verdict may ever read it. */
101
+ perf?: GuestPerfSample;
102
+ }
103
+ /** The probe exec: one line, six fields, always exit 0 — faults surface as
104
+ * parse failures, not exec failures. Reads the guest's own clock in the
105
+ * same exec so staleness math never involves the host clock. The sixth
106
+ * field is the perf token (perf-sampler.ts), `-` when absent; the char
107
+ * whitelist (`tr -cd`) collapses any garbled write to at most one token —
108
+ * it can never add whitespace and desync the field positions. */
109
+ export declare function turnLivenessProbeCommand(paths: {
110
+ outPath: string;
111
+ hbPath: string;
112
+ perfPath: string;
113
+ pid: number;
114
+ sentinel: string;
115
+ }): string;
116
+ /** Parse the probe's line. Null = unusable output (a wedged or faulted exec)
117
+ * — the caller treats that as "probe-failed", never as a verdict. The
118
+ * sixth field (perf token) is OPTIONAL both ways: a five-field line from
119
+ * an old guest parses fine, and a token that fails its own clamp simply
120
+ * leaves `perf` absent — perf can never fail a liveness reading. */
121
+ export declare function parseTurnProbeOutput(raw: string): TurnProbeReading | null;
122
+ /** The three-valued verdict from one durable reading. A FINISHED turn
123
+ * (sentinel durable in the file) is ALIVE — the transport's watchdog
124
+ * harvests it within one interval; reporting it dead is exactly the
125
+ * incident's ten-minute blindness. */
126
+ export declare function livenessVerdictFromReading(reading: TurnProbeReading | null): RunnerLivenessVerdict;
127
+ /** Feeder poll cadence (seconds — it is a shell `sleep`; GNU sleep accepts
128
+ * fractions and the durable transport only runs on the GNU guest image). */
129
+ export declare const INJECT_FEEDER_POLL_SECONDS = "0.3";
130
+ /** How much of the durable stdout tail the feeder greps for the terminal
131
+ * result line each poll — bounded so a long transcript never turns the
132
+ * poll into a full-file scan. Comfortably above the JSONL guard's line cap,
133
+ * so a capped result line still fits whole. */
134
+ export declare const INJECT_FEEDER_RESULT_TAIL_BYTES = 262144;
135
+ /** One in-guest delivery-ack exec: append the line, then poll the delivered
136
+ * counter until it covers this append (delivered), the runner dies, or the
137
+ * attempts run out (both: not delivered — the message stays owed). */
138
+ export declare const INJECT_ACK_POLL_ATTEMPTS = 30;
139
+ export declare const INJECT_ACK_POLL_SECONDS = "0.5";
140
+ export declare const INJECT_ACK_EXEC_TIMEOUT_MS = 25000;
141
+ /** The in-guest feeder fragment, prepended to the detached wrapper script.
142
+ * Runs inside the setsid session (group-kill reaps it) and keys its loop to
143
+ * the wrapper's own pid (`$$`), like the heartbeat subshell. Exported for
144
+ * tests. */
145
+ export declare function streamInputFeederFragment(paths: {
146
+ promptPath: string;
147
+ fifoPath: string;
148
+ inboxPath: string;
149
+ deliveredPath: string;
150
+ outPath: string;
151
+ }): string;
152
+ /** The instructional envelope around a user message delivered INTO a
153
+ * running turn. Claude Code's own interactive harness wraps queued
154
+ * mid-turn input with an explicit "address this as you continue"
155
+ * instruction; a bare `--input-format stream-json` line carries no such
156
+ * framing, so an injected message reads like any other transcript text
157
+ * and the model tends to continue its work narration without
158
+ * acknowledging it. Every mid-turn inject producer (the runner's
159
+ * `injectUserMessage` and the workflow lane's `deliverInjectedMessage`
160
+ * activity) MUST wrap through here — the wrapper rides only the wire to
161
+ * the model; the stored conversation row keeps the user's raw text.
162
+ * The text is pinned by tests: change it deliberately or not at all. */
163
+ export declare function wrapMidTurnUserMessage(text: string): string;
164
+ /** The one delivery-ack exec `injectUserMessage` runs: append the message
165
+ * line to the durable inbox, then wait for the feeder's delivered counter
166
+ * to cover it. Exit 0 = delivered into the CLI's stdin pre-result; 4 = the
167
+ * runner died first; 5 = not delivered within the ack window (feeder
168
+ * stopped on the result, or the guest is crawling) — both non-zero exits
169
+ * mean "leave the message owed". Exported for tests. */
170
+ export declare function injectAppendAndAckCommand(args: {
171
+ line: string;
172
+ target: number;
173
+ pid: number;
174
+ inboxPath: string;
175
+ deliveredPath: string;
176
+ }): string;
177
+ /** SIGTERM grace before escalation: attempts × sleep = 5s. */
178
+ export declare const REAP_TERM_WAIT_ATTEMPTS = 20;
179
+ /** Post-SIGKILL confirm window: attempts × sleep = 2s (a KILLed process only
180
+ * lingers as an unreaped zombie, which `kill -0` still sees — brief). */
181
+ export declare const REAP_KILL_WAIT_ATTEMPTS = 8;
182
+ export declare const REAP_POLL_SECONDS = "0.25";
183
+ /** The confirm exec's "still alive after SIGKILL" exit code (surfaced as a
184
+ * warn — the launch preflight is the backstop for whatever survived). */
185
+ export declare const REAP_STILL_ALIVE_EXIT = 7;
186
+ /** Ceiling on the whole kill-and-confirm exec (TERM 5s + KILL 2s + slack). */
187
+ export declare const REAP_EXEC_TIMEOUT_MS = 15000;
188
+ /** The one kill-and-confirm exec: TERM the group (setsid made `pid` the
189
+ * leader) and the leader itself, poll for exit, escalate to KILL, poll
190
+ * again. Exit 0 = confirmed dead; REAP_STILL_ALIVE_EXIT = it survived
191
+ * SIGKILL's confirm window. Exported for tests. */
192
+ export declare function reapKillAndConfirmCommand(pid: number): string;
193
+ /** Guest idle TTL for a resident with no new inbox line: hot window (180s)
194
+ * + margin, so the park gate's graceful `.end` normally wins and the
195
+ * watchdog only answers a dead server / lost record. */
196
+ export declare const RESIDENT_IDLE_TTL_S = 240;
197
+ /** Watchdog poll cadence (seconds). */
198
+ export declare const RESIDENT_WATCHDOG_POLL_S = 15;
199
+ /** Anchored prefix of the CLI's terminal result event line. Anchoring keeps
200
+ * the guest-side counts honest against tool output that merely CONTAINS
201
+ * the substring; a serialization-order change fails SAFE (parity check
202
+ * refuses adoption → cold launch — a latency cost, never correctness). */
203
+ export declare const RESIDENT_RESULT_LINE_PREFIX = "{\"type\":\"result\"";
204
+ /** One bounded exec printing the count of result lines at or past
205
+ * `fromByte` (0-based) in the durable .out. Always exits 0; stdout is the
206
+ * count (a missing file reads as 0). Hint-grade by design. */
207
+ export declare function residentResultCountCommand(outPath: string, fromByte?: number): string;
208
+ /** The resident feeder: `streamInputFeederFragment` with the result-break
209
+ * REPLACED by the end-file break — the ONLY guest-contract change the
210
+ * resident shape needs (verified live by the spec's probe: two messages,
211
+ * one process, one session file, graceful exit on `.end`). */
212
+ export declare function residentFeederFragment(paths: {
213
+ promptPath: string;
214
+ fifoPath: string;
215
+ inboxPath: string;
216
+ deliveredPath: string;
217
+ endPath: string;
218
+ }): string;
219
+ /** Sets `ac_idle` (1 = between turns: every delivered turn has its result
220
+ * and the inbox is fully fed). Embedded by the watchdog and the resident
221
+ * heartbeat gate — one idle predicate, stated once. */
222
+ export declare function residentIdleCheckFragment(paths: {
223
+ outPath: string;
224
+ inboxPath: string;
225
+ deliveredPath: string;
226
+ servedPath: string;
227
+ }): string;
228
+ /** Guest idle watchdog: reap the whole process group after
229
+ * RESIDENT_IDLE_TTL_S of CONTINUOUS idleness (any busy reading resets the
230
+ * clock — a long-running TURN is never reaped; the parity check reads
231
+ * busy mid-turn). The park gate's graceful `.end` normally wins; this is
232
+ * the orphan backstop for a dead server or a lost durable record.
233
+ *
234
+ * BACKGROUND TASKS ARE NOT IDLE (2026-08-19 forensics: a user's
235
+ * between-turns background search died to a harness teardown while its
236
+ * journal was still advancing). The turn-parity predicate alone reads a
237
+ * resident BETWEEN turns as idle even while harness background tasks it
238
+ * tracks are hard at work — and this watchdog's group-kill would take
239
+ * those children with the tree. So idle additionally requires the CLI's
240
+ * own task journal (`$HOME/.claude/tasks`) to be QUIET: any file touched
241
+ * within RUNNER_BUSY_TASK_FRESH_MINUTES resets the clock — the exact
242
+ * journal-freshness license the server's park gate honors
243
+ * (guestBusyProbeCommand). A stalled task goes stale within the window
244
+ * and the TTL resumes on its own; liveness alone never counts (the
245
+ * 2026-08-17 wake-loop damper is a SERVER rule about waking — this is a
246
+ * guest rule about not killing, and journal advance is required, not mere
247
+ * process existence). */
248
+ export declare function residentIdleWatchdogFragment(paths: {
249
+ outPath: string;
250
+ inboxPath: string;
251
+ deliveredPath: string;
252
+ servedPath: string;
253
+ }): string;
254
+ /** Exit-event doorbell with the turnId read from a guest FILE at push time
255
+ * (a resident serves many turns; each adoption rewrites `<prompt>.turnid`
256
+ * so a mid-turn death rings the turn actually being served). Same
257
+ * validation posture as `exitEventPushCommand`; an empty turnid file
258
+ * pushes nothing. */
259
+ export declare function deferredExitEventPushCommand(notify: {
260
+ url: string;
261
+ tokenEnv: string;
262
+ }, turnIdFile: string): string;
33
263
  /** Provider deadline on the detached LAUNCH exec. The launch shell only
34
264
  * truncates the durable files, forks the detached runner (stdio fully
35
265
  * redirected — see the launch command), writes the pidfile, and echoes the
@@ -50,8 +280,50 @@ export declare const LAUNCH_PID_RECOVERY_DELAY_MS = 1000;
50
280
  * call in the turn path carries an explicit timeout so no SDK default can
51
281
  * decide a turn's fate. Exported for tests. */
52
282
  export declare const INSTALL_PROBE_TIMEOUT_MS = 30000;
283
+ /** Deadline on the on-demand CLI install itself (`spec.install`). Shared by
284
+ * this transport's ensureInstalled and the server's CloudTurnWorkflow launch
285
+ * path (turn-workflow/tailer.ts), which provisions the same way before a
286
+ * detached launch — the session images bake only `claude`, so codex/
287
+ * opencode/cursor/droid MUST be installable at launch on a fresh machine. */
288
+ export declare const CLI_INSTALL_TIMEOUT_MS = 300000;
53
289
  /** Single-quote a value for safe interpolation into a `sh -c` command line. */
54
290
  export declare function shellQuote(value: string): string;
291
+ /** $HOME-relative path of the platform-managed session env file. The server
292
+ * writes it at sandbox acquire (session secrets → `export KEY='…'` lines);
293
+ * the detached turn launch sources it fresh EVERY turn, so a secret set or
294
+ * rotated between turns lands on the next turn with no VM recycle. Shared
295
+ * constant so the writer (server) and the reader (this runtime) can never
296
+ * drift. */
297
+ export declare const SESSION_ENV_FILE_RELPATH = ".agent-compose/session-env.sh";
298
+ /** The `. $HOME/<file>;` fragment sourced ahead of the CLI command, or ""
299
+ * when no env file is configured. Errors are swallowed — a missing or
300
+ * unreadable file must never fail a turn. */
301
+ export declare function sessionEnvSourceFragment(relPath: string | undefined): string;
302
+ /** The per-turn model-credential source fragment (`RuntimeOptions.
303
+ * credEnvFile` — an ABSOLUTE guest path), or "" when none is configured.
304
+ * Sourced AFTER the session env file so the turn actor's credential wins.
305
+ * Same swallow-errors posture: a missing file must never fail a turn. */
306
+ export declare function credEnvSourceFragment(absPath: string | undefined): string;
307
+ /** Ceiling on ONE curl attempt (seconds — it is `curl -m`). Short: the file
308
+ * is the truth and a slow push must not keep the wrapper alive for long. */
309
+ export declare const EXIT_PUSH_ATTEMPT_TIMEOUT_SECONDS = 5;
310
+ /** Pause before the single retry. */
311
+ export declare const EXIT_PUSH_RETRY_DELAY_SECONDS = 1;
312
+ /**
313
+ * The sh fragment appended to the detached wrapper AFTER the sentinel
314
+ * `printf` — never before it, and never in a way that can fail the wrapper:
315
+ * - reads the credential from the guest env at push time (`$<tokenEnv>`;
316
+ * the token never appears in the script text, argv-visible command
317
+ * lines the server logs, or any log line);
318
+ * - silently skips when the credential or curl is absent;
319
+ * - two attempts max (`-m 5`, one retry after 1s), then gives up silently
320
+ * — all output discarded, exit status swallowed (`|| true`).
321
+ * `$ac_rc` is the wrapper-local capture of the guarded pipeline's exit code
322
+ * (the same value the sentinel line carries). Returns "" — push disabled —
323
+ * for any `tokenEnv`/`turnId` that is not a plain identifier/uuid shape, so
324
+ * hostile config can never become shell injection.
325
+ */
326
+ export declare function exitEventPushCommand(notify: TurnExitNotify): string;
55
327
  /**
56
328
  * A UNIQUE per-turn prompt path. Uniqueness is load-bearing, not cosmetic:
57
329
  * E2B's envd cannot overwrite an existing non-root file in /tmp — it opens
@@ -129,13 +401,18 @@ export interface CliAgentSpec {
129
401
  /** Build the one-shot shell command for a turn. `promptPath` is a file in the
130
402
  * sandbox holding `promptPayload(prompt)`; `sessionId` continues a thread.
131
403
  * `effort` is present only when the caller configured a reasoning effort
132
- * AND the spec has a real knob for it (see `CliReasoningEffort`). */
404
+ * AND the spec has a real knob for it (see `CliReasoningEffort`).
405
+ * `streamInput` is true when the durable transport is feeding stdin as a
406
+ * live message stream (see `CliAgentSpec.streamInput`): `promptPath` is
407
+ * then the FIFO the feeder writes, and the CLI must be invoked in its
408
+ * stream-input mode (claude: `--input-format stream-json`). */
133
409
  buildCommand(args: {
134
410
  promptPath: string;
135
411
  sessionId?: string;
136
412
  model?: string;
137
413
  cwd?: string;
138
414
  effort?: CliReasoningEffort;
415
+ streamInput?: boolean;
139
416
  }): string;
140
417
  /** Map one parsed JSONL stdout event to `AgentMessage`s. The base emits
141
418
  * `init`/`done`/`error` lifecycle itself, so a spec maps only content +
@@ -150,6 +427,16 @@ export interface CliAgentSpec {
150
427
  * resume it (codex `thread.started.thread_id`, amp `session_id`).
151
428
  * LEGACY FALLBACK PATH (see `mapEvent`). */
152
429
  extractSessionId(parsed: Record<string, unknown>): string | undefined;
430
+ /** The runtime's thread persistence admits only ONE live writer per thread
431
+ * (codex: an OS advisory flock per thread id, held for the holding
432
+ * process's whole lifetime — codex-rs thread-store writer_lock.rs). A
433
+ * platform that wants a SUCCESSOR turn to resume the same thread must
434
+ * therefore KILL the predecessor's process tree first — a merely-detached
435
+ * live predecessor blocks every `thread/resume` with "already has an
436
+ * active writer" (the 2026-08-22 superseded-codex-turn incident). Absent
437
+ * ⇒ concurrent resume is safe (claude-code: append-only session JSONL)
438
+ * and a superseded runner may be detached alive as an asset. */
439
+ exclusiveSessionWriter?: boolean;
153
440
  /** ACP-mode invocation (the Zed `agent_servers` shape). When present, the
154
441
  * runner spawns the CLI in ACP agent mode and delegates the whole wire
155
442
  * protocol to `AcpClientPeer`; the legacy `promptPayload`/`buildCommand`/
@@ -160,6 +447,27 @@ export interface CliAgentSpec {
160
447
  args: string[];
161
448
  env?: Record<string, string>;
162
449
  };
450
+ /** Mid-turn STREAM-INPUT support (the delivery half of
451
+ * `injectUserMessage`). When present AND the durable detached transport
452
+ * is in play, the CLI's stdin is fed from a FIFO by an in-guest feeder:
453
+ * the prompt line first, then any lines appended to the turn's durable
454
+ * inbox file — so a steering-capable CLI (claude: queued user input is
455
+ * folded into the RUNNING turn at the next tool boundary; verified live
456
+ * against claude 2.1.233) receives user messages while the turn runs.
457
+ * The feeder stops at the CLI's terminal result line (a message that
458
+ * races the result is NOT forwarded — it stays owed) and closes the
459
+ * FIFO, which is what ends the CLI process (stream-input CLIs exit on
460
+ * stdin EOF, not after a result). Absent → the prompt file is the whole
461
+ * stdin, exactly as before. */
462
+ streamInput?: {
463
+ /** Serialise the opening prompt into ONE stream-input stdin line
464
+ * (claude: a stream-json user message). No trailing newline — the
465
+ * transport owns line framing. */
466
+ promptLine(prompt: string): string;
467
+ /** Serialise one mid-turn user message into ONE stdin line. No
468
+ * trailing newline. */
469
+ messageLine(text: string): string;
470
+ };
163
471
  }
164
472
  export declare class CliAgentRunner implements ModelExecutionContract {
165
473
  private readonly sandbox;
@@ -208,6 +516,55 @@ export declare class CliAgentRunner implements ModelExecutionContract {
208
516
  * exercising the real `sendMessageAcp` even while the production default is
209
517
  * dormant. Not a public API. */
210
518
  private acpTransportReady;
519
+ /** The CURRENT turn's durable-trio reader (set once the detached runner is
520
+ * launched, replaced by the next launch). Null = no durable transport is
521
+ * active for this runner (boot phase, ACP path, single-exec providers) —
522
+ * `probeTurnLiveness` then answers null and the executor keeps its own
523
+ * fallback probe. */
524
+ private turnProbe;
525
+ /** The CURRENT turn's mid-turn injection state (durable stream-input
526
+ * transport only). Null = no live stream-input turn — `injectUserMessage`
527
+ * answers "unsupported" and the caller leaves the message owed. `chain`
528
+ * serializes appends so each delivery targets a deterministic line count. */
529
+ private turnInject;
530
+ /** Deliver one user message INTO the live turn — see the contract doc
531
+ * (types/runtime.ts). Appends a stream-input line to the turn's durable
532
+ * inbox and waits for the in-guest feeder's delivered-counter ack; only
533
+ * an acked forward (pre-result, into the CLI's stdin) reports
534
+ * "delivered". Guest exit 5 (ack window exhausted with the runner still
535
+ * alive — a CLI that is not draining stdin mid-step) reports "pending":
536
+ * the appended line may still be read when the current step finishes,
537
+ * but it was NOT seen yet. Never throws. */
538
+ injectUserMessage(text: string): Promise<"delivered" | "pending" | "closed" | "unsupported">;
539
+ /** Durable three-valued liveness for the current turn — see the contract
540
+ * doc (types/runtime.ts). Never throws; never blocks past the probe's
541
+ * explicit exec timeout. */
542
+ probeTurnLiveness(): Promise<RunnerLivenessVerdict | null>;
543
+ /** Wakes the CURRENT tail-watchdog interval early. Armed only while the
544
+ * watchdog's wake timer is pending; null between arms / between turns. */
545
+ private wakeTailWatchdog;
546
+ /** A doorbell that rang while no watchdog wake was armed — consumed at the
547
+ * next arm (immediate wake), dropped at the start of a new turn. */
548
+ private nudgePending;
549
+ /** DOORBELL, NEVER A VERDICT (contract doc: types/runtime.ts). Wake the
550
+ * tail watchdog NOW so its normal verification pass — durable probe, then
551
+ * harvest-on-sentinel / honest no-sentinel death — runs immediately
552
+ * instead of at the next TAIL_WATCHDOG_INTERVAL_MS tick. Carries no
553
+ * information: a nudge for a live mid-turn runner probes alive and is a
554
+ * no-op; a spurious/duplicate nudge costs at most one probe exec. No-op
555
+ * when no durable watchdog is armed (boot phase, ACP path, single-exec
556
+ * transports, abandoned-streaming polling) — those phases already carry
557
+ * their own bounds. */
558
+ nudgeTurnProbe(): void;
559
+ /** RESUME HANDOFF, NEVER A KILL (contract doc: types/runtime.ts). Once
560
+ * set, abort/early-exit unwinds the transport WITHOUT reaping the
561
+ * detached guest tree and WITHOUT the opportunistic durable-file cleanup
562
+ * — the successor turn resumes the same guest session, and the park
563
+ * path's busy signal keeps reading the surviving heartbeat/busy files.
564
+ * One-way for this runner instance (a runner is per-turn on the cloud
565
+ * path). */
566
+ private guestDetached;
567
+ detachGuest(): void;
211
568
  constructor(sandbox: SandboxProvider, options: RuntimeOptions, spec: CliAgentSpec, configModel?: string | undefined, configEffort?: CliReasoningEffort | undefined);
212
569
  /** TEST-ONLY: flip the live-ACP readiness gate on for this runner instance so
213
570
  * the ACP lifecycle / fallback tests can exercise `sendMessageAcp` while the
@@ -33,6 +33,7 @@
33
33
  * sessions this is the gateway-minted session virtual key pointed at the
34
34
  * token-metering gateway's Anthropic passthrough (ADR-0039).
35
35
  */
36
+ import type { AgentMessageTaskNotification } from "../index.js";
36
37
  import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
37
38
  /** Pinned Claude Code ACP adapter (Zed's npm shim — `claude` has no native
38
39
  * ACP mode). Pinned EXACT, not a range: the adapter's README warns of
@@ -40,6 +41,17 @@ import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
40
41
  * bridge daemon (cli/src/bridge/acp.ts) imports this same pin so both
41
42
  * executors launch the identical adapter. Verified against 0.16.2. */
42
43
  export declare const CLAUDE_CODE_ACP_ADAPTER = "@zed-industries/claude-code-acp@0.16.2";
44
+ /** Parse every `<task-notification>` block out of one user-role text blob.
45
+ * Pure and tolerant over untrusted harness text: a block without a task id
46
+ * is skipped, absent fields stay absent, everything is clamped. Exported
47
+ * for tests. */
48
+ export declare function parseTaskNotifications(text: string, timestamp: string): AgentMessageTaskNotification[];
49
+ /** One `system`/`task_notification` stream-json event mapped onto the same
50
+ * structured message the XML parse produces, or null when the event names
51
+ * no task id. Pure and tolerant over untrusted harness JSON: absent fields
52
+ * stay absent, everything is clamped; `output_file` (internal plumbing) is
53
+ * deliberately not forwarded. Exported for tests. */
54
+ export declare function parseSystemTaskNotification(p: Record<string, unknown>, timestamp: string): AgentMessageTaskNotification | null;
43
55
  /** Claude Code's real reasoning knob is its own `--effort <level>` flag
44
56
  * (low|medium|high|xhigh|max — verified against `claude -p --help`). The
45
57
  * CliReasoningEffort union IS the CLI's vocabulary, so the level rides the
@@ -16,6 +16,14 @@ import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
16
16
  /** True when an error-item message is a known codex advisory (log-level
17
17
  * noise), not a real error. Exported for tests. */
18
18
  export declare function isCodexAdvisoryNoise(text: string): boolean;
19
+ /** Bounded wait for a dying previous writer: attempts × sleep = 5s, matching
20
+ * the teardown's SIGTERM grace (sdk _cli-agent.ts REAP_TERM_WAIT_ATTEMPTS). */
21
+ export declare const CODEX_LOCK_WAIT_ATTEMPTS = 20;
22
+ export declare const CODEX_LOCK_WAIT_SECONDS = "0.25";
23
+ /** The sh fragment prepended to a resume launch. `waitAttempts` is a test
24
+ * seam (the live-holder test must not sleep 5s); production callers take
25
+ * the default. Exported for tests. */
26
+ export declare function codexWriterLockPreflight(threadId: string, waitAttempts?: number): string;
19
27
  /** Exported for the fixture-based parity tests (ADR-0020): the legacy JSONL
20
28
  * `mapEvent` is the golden the ACP normaliser is asserted equal to. Not part
21
29
  * of the public runtime surface — `createCodexRuntime` stays the entry point. */