@cjhyy/code-shell-capability-coding 0.9.43 → 0.9.46

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 (49) hide show
  1. package/dist/capability-runtime.js +4 -1
  2. package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
  3. package/dist/cc-orchestrator/codex-session-history.js +11 -4
  4. package/dist/cc-orchestrator/external-agent-changes.js +7 -65
  5. package/dist/cc-orchestrator/external-agent-driver.js +61 -3
  6. package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
  7. package/dist/cc-orchestrator/session-discovery.js +63 -6
  8. package/dist/cc-orchestrator/session-history.js +5 -0
  9. package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
  10. package/dist/external-runtimes/claude-code/event-translator.js +58 -9
  11. package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
  12. package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
  13. package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
  14. package/dist/external-runtimes/claude-code/permissions.js +120 -0
  15. package/dist/external-runtimes/claude-code/runtime.d.ts +40 -4
  16. package/dist/external-runtimes/claude-code/runtime.js +130 -39
  17. package/dist/external-runtimes/codex/app-server-client.d.ts +12 -0
  18. package/dist/external-runtimes/codex/app-server-client.js +16 -0
  19. package/dist/external-runtimes/codex/runtime.d.ts +11 -0
  20. package/dist/external-runtimes/codex/runtime.js +143 -16
  21. package/dist/external-runtimes/index.d.ts +2 -0
  22. package/dist/external-runtimes/index.js +1 -0
  23. package/dist/external-runtimes/session-factory.d.ts +14 -0
  24. package/dist/external-runtimes/session-factory.js +40 -1
  25. package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
  26. package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
  27. package/dist/git/parse-log.d.ts +6 -2
  28. package/dist/git/parse-log.js +7 -3
  29. package/dist/git/utils.d.ts +4 -0
  30. package/dist/git/utils.js +29 -10
  31. package/dist/git/worktree/crud.js +28 -0
  32. package/dist/git/worktree/diff.js +57 -29
  33. package/dist/index.capability.js +14 -1
  34. package/dist/index.git.d.ts +1 -0
  35. package/dist/index.git.js +1 -0
  36. package/dist/quota/index.d.ts +2 -1
  37. package/dist/quota/index.js +27 -7
  38. package/dist/quota/types.d.ts +5 -2
  39. package/dist/review/review-prompt.d.ts +2 -0
  40. package/dist/review/review-prompt.js +70 -29
  41. package/dist/review/review-target.d.ts +29 -0
  42. package/dist/review/review-target.js +353 -0
  43. package/dist/tools/apply-patch/applier.js +64 -39
  44. package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
  45. package/dist/tools/apply-patch/backup-targets.js +24 -0
  46. package/dist/tools/apply-patch/parser.js +4 -2
  47. package/dist/tools/check-quota.js +3 -1
  48. package/dist/tools/lsp.js +36 -5
  49. package/package.json +2 -2
@@ -0,0 +1,120 @@
1
+ /** Values passed to `claude --permission-mode`. */
2
+ export const CLAUDE_PERMISSION_MODES = [
3
+ "default",
4
+ "acceptEdits",
5
+ "plan",
6
+ "dontAsk",
7
+ "bypassPermissions",
8
+ ];
9
+ /**
10
+ * Map a CodeShell permission selection onto Claude's.
11
+ *
12
+ * Never escalates: Plan wins over everything; full access maps to
13
+ * `bypassPermissions` only when CodeShell itself is in `bypassPermissions`; and
14
+ * anything without an exact Claude equivalent — notably CodeShell's legacy
15
+ * `auto`, which in Claude would mean a model-classifier auto-approval — becomes
16
+ * `default`.
17
+ */
18
+ export function claudePermissionModeFor(selection) {
19
+ if (selection.planMode || selection.permissionMode === "plan")
20
+ return "plan";
21
+ switch (selection.permissionMode) {
22
+ case "acceptEdits":
23
+ case "dontAsk":
24
+ case "bypassPermissions":
25
+ return selection.permissionMode;
26
+ default:
27
+ return "default";
28
+ }
29
+ }
30
+ /**
31
+ * Whether native asks should be routed to the host in this mode. Plan stays
32
+ * read-only (an approved ExitPlanMode would otherwise leave it); `dontAsk` means
33
+ * "do not ask"; `bypassPermissions` never asks.
34
+ */
35
+ export function claudeModeUsesPermissionPrompt(mode) {
36
+ return mode === "default" || mode === "acceptEdits";
37
+ }
38
+ /** Plain CodeShell-side name; the bridge qualifies it as `mcp__<server>__…`. */
39
+ export const CLAUDE_PERMISSION_PROMPT_TOOL = "CodeShellPermissionPrompt";
40
+ function asRecord(value) {
41
+ return value && typeof value === "object" && !Array.isArray(value)
42
+ ? value
43
+ : undefined;
44
+ }
45
+ function deny(message) {
46
+ // The prompt tool's verdict is its TEXT result; an MCP error result would be
47
+ // treated by the CLI as a failed prompt rather than as a decision.
48
+ return { result: JSON.stringify({ behavior: "deny", message }) };
49
+ }
50
+ /**
51
+ * Add the permission-prompt tool to a session's bridge surface.
52
+ *
53
+ * Answers, in order:
54
+ * - the prompt tool itself → deny. It is visible to the model like any MCP
55
+ * tool; a direct call must not raise a host approval dialog.
56
+ * - this server's own tools → allow without asking. `ToolExecutor` authorizes
57
+ * them; asking here too would approve one operation twice (the same rule the
58
+ * Codex adapter applies to its MCP elicitations).
59
+ * - `ExitPlanMode` → deny. The mode is CodeShell's selection, not the model's.
60
+ * - everything else → `decide`, failing closed on any error or abort.
61
+ */
62
+ export function withClaudePermissionPrompt(host, decide, serverName) {
63
+ const ownPrefix = `mcp__${serverName}__`;
64
+ const qualifiedSelf = `${ownPrefix}${CLAUDE_PERMISSION_PROMPT_TOOL}`;
65
+ return {
66
+ businessSessionId: host.businessSessionId,
67
+ listTools: () => [
68
+ ...host.listTools(),
69
+ {
70
+ name: CLAUDE_PERMISSION_PROMPT_TOOL,
71
+ description: "Internal: Claude Code's permission prompt for its native tools, answered by " +
72
+ "the CodeShell user. Not for direct use.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ tool_name: { type: "string" },
77
+ input: { type: "object" },
78
+ tool_use_id: { type: "string" },
79
+ },
80
+ required: ["tool_name", "input"],
81
+ },
82
+ },
83
+ ],
84
+ execute: async (call, signal) => {
85
+ if (call.name !== CLAUDE_PERMISSION_PROMPT_TOOL)
86
+ return host.execute(call, signal);
87
+ const params = asRecord(call.input);
88
+ const toolName = typeof params?.tool_name === "string" ? params.tool_name : "";
89
+ const input = asRecord(params?.input);
90
+ if (!toolName || !input)
91
+ return deny("Malformed permission request.");
92
+ if (toolName === qualifiedSelf || toolName === CLAUDE_PERMISSION_PROMPT_TOOL) {
93
+ return deny("The permission prompt tool cannot be called directly.");
94
+ }
95
+ if (toolName.startsWith(ownPrefix)) {
96
+ return { result: JSON.stringify({ behavior: "allow", updatedInput: input }) };
97
+ }
98
+ if (toolName === "ExitPlanMode") {
99
+ return deny("The permission mode is set by the CodeShell user and cannot be changed here.");
100
+ }
101
+ try {
102
+ if (signal?.aborted)
103
+ return deny("The permission request was cancelled.");
104
+ const decision = await decide({
105
+ toolName,
106
+ input,
107
+ ...(typeof params?.tool_use_id === "string" ? { toolUseId: params.tool_use_id } : {}),
108
+ }, signal);
109
+ if (signal?.aborted)
110
+ return deny("The permission request was cancelled.");
111
+ return decision.approved
112
+ ? { result: JSON.stringify({ behavior: "allow", updatedInput: input }) }
113
+ : deny(decision.reason || "The CodeShell user denied this tool call.");
114
+ }
115
+ catch {
116
+ return deny("The CodeShell approval request failed.");
117
+ }
118
+ },
119
+ };
120
+ }
@@ -1,4 +1,5 @@
1
1
  import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
2
+ import { type ClaudePermissionMode, type ClaudePermissionPromptDecider } from "./permissions.js";
2
3
  import type { McpBridgeHandle } from "../shared/mcp-bridge.js";
3
4
  import { type ExternalRuntimeTurnInput } from "../turn-input.js";
4
5
  export interface ClaudeRuntimeOptions {
@@ -15,11 +16,40 @@ export interface ClaudeRuntimeOptions {
15
16
  model?: string;
16
17
  resumeRuntimeSessionId?: string;
17
18
  initialContext?: string;
19
+ /**
20
+ * Stable host guidance, appended to Claude's system prompt on EVERY turn —
21
+ * new and resumed alike, because each turn is a new process. Contrast
22
+ * `initialContext`, a one-time transcript replay for a fresh conversation.
23
+ */
24
+ developerInstructions?: string;
25
+ /**
26
+ * `--permission-mode` for Claude's NATIVE tools. Derive it with
27
+ * `claudePermissionModeFor`; see `permissions.ts` for why it is always sent.
28
+ */
29
+ permissionMode?: ClaudePermissionMode;
30
+ /**
31
+ * Plain name of the bridge tool that answers Claude's permission prompts
32
+ * (`withClaudePermissionPrompt`). Qualified here as `mcp__<server>__<name>`.
33
+ */
34
+ permissionPromptTool?: string;
18
35
  serverName?: string;
36
+ /**
37
+ * How long `interrupt()`/`close()` wait after SIGTERM before SIGKILL.
38
+ * Defaults to {@link DEFAULT_INTERRUPT_GRACE_MS}.
39
+ */
40
+ interruptGraceMs?: number;
19
41
  log?: (event: string, data: Record<string, unknown>) => void;
20
42
  }
43
+ /** SIGTERM grace before escalating to SIGKILL. */
44
+ export declare const DEFAULT_INTERRUPT_GRACE_MS = 3000;
21
45
  export interface ClaudeRuntimeHooks {
22
46
  onEvent?: (event: StreamEvent) => void;
47
+ /**
48
+ * Answer Claude's permission prompts for its NATIVE tools (Bash, Edit, …).
49
+ * Wired by the session factory through the bridge's permission-prompt tool;
50
+ * without it every native ask is denied. CodeShell Host Tools never reach it.
51
+ */
52
+ onClaudePermissionPrompt?: ClaudePermissionPromptDecider;
23
53
  }
24
54
  export interface ClaudeTurnHandle {
25
55
  readonly done: Promise<void>;
@@ -29,7 +59,7 @@ export declare class ClaudeCodeRuntime {
29
59
  private readonly hooks;
30
60
  readonly kind: "claude-code";
31
61
  private readonly log;
32
- private child?;
62
+ private active?;
33
63
  private claudeSessionId?;
34
64
  private closed;
35
65
  private firstTurn;
@@ -49,12 +79,18 @@ export declare class ClaudeCodeRuntime {
49
79
  private onLine;
50
80
  private emit;
51
81
  /**
52
- * Interrupt the active turn.
82
+ * Interrupt the active turn and wait until its process is gone.
83
+ *
84
+ * `claude -p` has no interrupt RPC, so this is a signal: SIGTERM, then SIGKILL
85
+ * after the grace period. Returning while the process still runs would let a
86
+ * CLI that ignores SIGTERM keep working — and keep the turn pending — after
87
+ * the host believes it stopped. The turn settles with `aborted_streaming`.
53
88
  *
54
- * `claude -p` has no interrupt RPC, so this is a signal. As with Codex, it
55
- * cannot retract side effects already dispatched — it only stops further steps.
89
+ * As with Codex, it cannot retract side effects already dispatched — it only
90
+ * stops further steps.
56
91
  */
57
92
  interrupt(): Promise<void>;
93
+ private terminate;
58
94
  close(): Promise<void>;
59
95
  /** Start a fresh conversation on the next `send()`, discarding resume state. */
60
96
  resetConversation(): void;
@@ -17,15 +17,32 @@
17
17
  import { spawn } from "node:child_process";
18
18
  import { createInterface } from "node:readline";
19
19
  import { ClaudeEventTranslator } from "./event-translator.js";
20
- import { claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME } from "./mcp-config.js";
20
+ import { claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME, writeClaudeSystemPromptFile, } from "./mcp-config.js";
21
21
  import { buildRuntimeSpawnEnv } from "../shared/spawn-env.js";
22
+ import { CLAUDE_PERMISSION_MODES, } from "./permissions.js";
22
23
  import { textWithAttachmentReferences } from "../turn-input.js";
24
+ /** SIGTERM grace before escalating to SIGKILL. */
25
+ export const DEFAULT_INTERRUPT_GRACE_MS = 3_000;
26
+ /**
27
+ * Bound on waiting for `exit` after SIGKILL. SIGKILL cannot be ignored, so this
28
+ * only guards against a lost `exit` event; the turn is settled regardless.
29
+ */
30
+ const KILL_SETTLE_MS = 2_000;
31
+ function settlesWithin(promise, ms) {
32
+ return new Promise((resolve) => {
33
+ const timer = setTimeout(() => resolve(false), ms);
34
+ void promise.then(() => {
35
+ clearTimeout(timer);
36
+ resolve(true);
37
+ });
38
+ });
39
+ }
23
40
  export class ClaudeCodeRuntime {
24
41
  options;
25
42
  hooks;
26
43
  kind = "claude-code";
27
44
  log;
28
- child;
45
+ active;
29
46
  claudeSessionId;
30
47
  closed = false;
31
48
  firstTurn = true;
@@ -33,6 +50,16 @@ export class ClaudeCodeRuntime {
33
50
  constructor(options, hooks = {}) {
34
51
  this.options = options;
35
52
  this.hooks = hooks;
53
+ // Typed, but also reachable from untyped callers: an unknown mode must not
54
+ // reach the CLI, where e.g. `auto` would mean classifier auto-approval.
55
+ if (options.permissionMode !== undefined &&
56
+ !CLAUDE_PERMISSION_MODES.includes(options.permissionMode)) {
57
+ throw new Error(`Unsupported Claude permission mode: ${String(options.permissionMode)}`);
58
+ }
59
+ if (options.permissionPromptTool !== undefined &&
60
+ !/^[A-Za-z][A-Za-z0-9_]*$/.test(options.permissionPromptTool)) {
61
+ throw new Error("permissionPromptTool must be a plain bridge tool name");
62
+ }
36
63
  this.log = options.log ?? (() => { });
37
64
  this.claudeSessionId = options.resumeRuntimeSessionId;
38
65
  }
@@ -51,7 +78,7 @@ export class ClaudeCodeRuntime {
51
78
  async send(input, onEvent) {
52
79
  if (this.closed)
53
80
  throw new Error("ClaudeCodeRuntime is closed");
54
- if (this.child)
81
+ if (this.active)
55
82
  throw new Error("a turn is already running");
56
83
  let terminalSeen = false;
57
84
  const turnNumber = ++this.turnCount;
@@ -65,6 +92,15 @@ export class ClaudeCodeRuntime {
65
92
  exposedToolNames: this.options.exposedToolNames,
66
93
  ...(this.options.serverName ? { serverName: this.options.serverName } : {}),
67
94
  });
95
+ // Per turn: each `claude -p` is a new process, and `--resume` does not carry a
96
+ // previous invocation's appended system prompt forward.
97
+ const systemPrompt = this.options.developerInstructions?.trim()
98
+ ? writeClaudeSystemPromptFile(this.options.developerInstructions)
99
+ : undefined;
100
+ const cleanupTurnFiles = () => {
101
+ wiring.cleanup();
102
+ systemPrompt?.cleanup();
103
+ };
68
104
  const args = [
69
105
  "-p",
70
106
  "--output-format",
@@ -76,21 +112,36 @@ export class ClaudeCodeRuntime {
76
112
  // Continuity across turns: a fresh process per turn means the session id is
77
113
  // the only thread of memory.
78
114
  ...(this.claudeSessionId ? ["--resume", this.claudeSessionId] : []),
115
+ ...(systemPrompt ? ["--append-system-prompt-file", systemPrompt.path] : []),
116
+ ...(this.options.permissionMode ? ["--permission-mode", this.options.permissionMode] : []),
117
+ ...(this.options.permissionPromptTool
118
+ ? [
119
+ "--permission-prompt-tool",
120
+ `mcp__${this.options.serverName ?? CLAUDE_MCP_SERVER_NAME}__${this.options.permissionPromptTool}`,
121
+ ]
122
+ : []),
79
123
  ...(this.options.extraArgs ?? []),
80
124
  ...wiring.args,
81
125
  ];
82
- const child = spawn(this.options.command ?? "claude", args, {
83
- cwd: this.options.cwd,
84
- env: buildRuntimeSpawnEnv({
85
- bridgeToken: {
86
- name: this.options.bridge.tokenEnvVar,
87
- value: this.options.bridge.token,
88
- },
89
- }),
90
- stdio: ["pipe", "pipe", "pipe"],
91
- shell: false,
92
- });
93
- this.child = child;
126
+ let child;
127
+ try {
128
+ child = spawn(this.options.command ?? "claude", args, {
129
+ cwd: this.options.cwd,
130
+ env: buildRuntimeSpawnEnv({
131
+ bridgeToken: {
132
+ name: this.options.bridge.tokenEnvVar,
133
+ value: this.options.bridge.token,
134
+ },
135
+ }),
136
+ stdio: ["pipe", "pipe", "pipe"],
137
+ shell: false,
138
+ });
139
+ }
140
+ catch (error) {
141
+ // A synchronous spawn failure never reaches the exit/error handlers below.
142
+ cleanupTurnFiles();
143
+ throw error;
144
+ }
94
145
  child.stdout.setEncoding("utf8");
95
146
  const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
96
147
  lines.on("line", (line) => {
@@ -112,31 +163,46 @@ export class ClaudeCodeRuntime {
112
163
  }
113
164
  this.firstTurn = false;
114
165
  child.stdin.end(text);
115
- const done = new Promise((resolve) => {
116
- let finished = false;
117
- const finish = (code, error) => {
166
+ let resolveSettled;
167
+ const settled = new Promise((resolve) => (resolveSettled = resolve));
168
+ let finished = false;
169
+ const active = {
170
+ child,
171
+ settled,
172
+ interrupted: false,
173
+ finish: (code, error) => {
118
174
  if (finished)
119
175
  return;
120
176
  finished = true;
121
177
  lines.close();
122
- // Clean up the config file that carried the bearer token.
123
- wiring.cleanup();
124
- this.child = undefined;
178
+ // Clean up the config file that carried the bearer token, and the
179
+ // system-prompt file.
180
+ cleanupTurnFiles();
181
+ if (this.active === active)
182
+ this.active = undefined;
125
183
  if (!terminalSeen) {
126
- const detail = error?.message ??
127
- `Claude Code exited without a terminal result (code ${code ?? "unknown"})`;
128
- this.emit({ type: "error", error: detail }, onEvent);
129
- this.emit({ type: "turn_complete", reason: "model_error" }, onEvent);
184
+ if (active.interrupted) {
185
+ // A stop the user asked for is not a model failure. Same terminal
186
+ // reason Codex reports for an interrupted turn.
187
+ this.emit({ type: "turn_complete", reason: "aborted_streaming" }, onEvent);
188
+ }
189
+ else {
190
+ const detail = error?.message ??
191
+ `Claude Code exited without a terminal result (code ${code ?? "unknown"})`;
192
+ this.emit({ type: "error", error: detail }, onEvent);
193
+ this.emit({ type: "turn_complete", reason: "model_error" }, onEvent);
194
+ }
130
195
  }
131
- resolve();
132
- };
133
- child.once("exit", (code) => finish(code));
134
- child.once("error", (error) => {
135
- this.log("claude.spawn_failed", { error: error.message.slice(0, 200) });
136
- finish(undefined, error);
137
- });
196
+ resolveSettled();
197
+ },
198
+ };
199
+ this.active = active;
200
+ child.once("exit", (code) => active.finish(code));
201
+ child.once("error", (error) => {
202
+ this.log("claude.spawn_failed", { error: error.message.slice(0, 200) });
203
+ active.finish(undefined, error);
138
204
  });
139
- return { done };
205
+ return { done: settled };
140
206
  }
141
207
  onLine(line, translator, emit) {
142
208
  if (!line.trim())
@@ -171,16 +237,41 @@ export class ClaudeCodeRuntime {
171
237
  }
172
238
  }
173
239
  /**
174
- * Interrupt the active turn.
240
+ * Interrupt the active turn and wait until its process is gone.
175
241
  *
176
- * `claude -p` has no interrupt RPC, so this is a signal. As with Codex, it
177
- * cannot retract side effects already dispatched — it only stops further steps.
242
+ * `claude -p` has no interrupt RPC, so this is a signal: SIGTERM, then SIGKILL
243
+ * after the grace period. Returning while the process still runs would let a
244
+ * CLI that ignores SIGTERM keep working — and keep the turn pending — after
245
+ * the host believes it stopped. The turn settles with `aborted_streaming`.
246
+ *
247
+ * As with Codex, it cannot retract side effects already dispatched — it only
248
+ * stops further steps.
178
249
  */
179
250
  async interrupt() {
180
- const child = this.child;
181
- if (!child)
251
+ const active = this.active;
252
+ if (!active)
253
+ return;
254
+ active.interrupted = true;
255
+ active.stopping ??= this.terminate(active);
256
+ await active.stopping;
257
+ }
258
+ async terminate(active) {
259
+ const { child } = active;
260
+ const running = () => child.exitCode === null && child.signalCode === null;
261
+ if (running())
262
+ child.kill("SIGTERM");
263
+ const grace = this.options.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
264
+ if (await settlesWithin(active.settled, grace))
265
+ return;
266
+ this.log("claude.sigkill", { graceMs: grace });
267
+ if (running())
268
+ child.kill("SIGKILL");
269
+ if (await settlesWithin(active.settled, KILL_SETTLE_MS))
182
270
  return;
183
- child.kill("SIGTERM");
271
+ // No `exit` event even after SIGKILL (or the spawn never produced a process).
272
+ // Settle the turn anyway so neither the caller nor the session hangs.
273
+ this.log("claude.exit_not_observed", {});
274
+ active.finish(null);
184
275
  }
185
276
  async close() {
186
277
  this.closed = true;
@@ -21,6 +21,14 @@ export type NotificationHandler = (method: string, params: unknown) => void;
21
21
  * waiting either way.
22
22
  */
23
23
  export type ServerRequestHandler = (method: string, params: unknown) => Promise<unknown> | unknown;
24
+ /**
25
+ * Told once when the transport is gone for good — the process exited or failed
26
+ * to spawn, the stream broke, or {@link CodexAppServerClient.close} ran. Pending
27
+ * requests are rejected by then; this is for state that is NOT a request, such
28
+ * as a turn whose RPC was already answered and now waits on notifications that
29
+ * can never arrive.
30
+ */
31
+ export type CloseHandler = (reason: string) => void;
24
32
  export declare class CodexAppServerClient {
25
33
  private readonly options;
26
34
  private child?;
@@ -29,6 +37,7 @@ export declare class CodexAppServerClient {
29
37
  private readonly pending;
30
38
  private notificationHandler?;
31
39
  private requestHandler?;
40
+ private closeHandler?;
32
41
  private closed;
33
42
  private closeReason?;
34
43
  /** Buffer lines that arrive before a handler exists, then drain in order. */
@@ -42,6 +51,7 @@ export declare class CodexAppServerClient {
42
51
  */
43
52
  onNotification(handler: NotificationHandler): void;
44
53
  onServerRequest(handler: ServerRequestHandler): void;
54
+ onClose(handler: CloseHandler): void;
45
55
  /** Spawn the app-server. Handlers must already be registered. */
46
56
  start(): void;
47
57
  private acceptLine;
@@ -60,6 +70,8 @@ export declare class CodexAppServerClient {
60
70
  notify(method: string, params?: unknown): void;
61
71
  private write;
62
72
  private failAll;
73
+ /** Why the transport closed, once it has. */
74
+ get closedReason(): string | undefined;
63
75
  get isClosed(): boolean;
64
76
  /** Stop via stdin EOF, then SIGTERM and SIGKILL if the server does not exit. */
65
77
  close(): Promise<void>;
@@ -36,6 +36,7 @@ export class CodexAppServerClient {
36
36
  pending = new Map();
37
37
  notificationHandler;
38
38
  requestHandler;
39
+ closeHandler;
39
40
  closed = false;
40
41
  closeReason;
41
42
  /** Buffer lines that arrive before a handler exists, then drain in order. */
@@ -56,6 +57,9 @@ export class CodexAppServerClient {
56
57
  onServerRequest(handler) {
57
58
  this.requestHandler = handler;
58
59
  }
60
+ onClose(handler) {
61
+ this.closeHandler = handler;
62
+ }
59
63
  /** Spawn the app-server. Handlers must already be registered. */
60
64
  start() {
61
65
  if (this.child)
@@ -234,6 +238,18 @@ export class CodexAppServerClient {
234
238
  entry.reject(new Error(reason));
235
239
  }
236
240
  this.pending.clear();
241
+ try {
242
+ this.closeHandler?.(reason);
243
+ }
244
+ catch (error) {
245
+ this.log("appserver.close_handler_failed", {
246
+ error: error instanceof Error ? error.name : "unknown",
247
+ });
248
+ }
249
+ }
250
+ /** Why the transport closed, once it has. */
251
+ get closedReason() {
252
+ return this.closeReason;
237
253
  }
238
254
  get isClosed() {
239
255
  return this.closed;
@@ -111,7 +111,18 @@ export declare class CodexRuntime {
111
111
  * that no further step starts (§13.3).
112
112
  */
113
113
  interrupt(): Promise<void>;
114
+ private requestInterrupt;
114
115
  close(): Promise<void>;
116
+ /** Learn the active turn's id; deliver a Stop that was waiting for it, once. */
117
+ private bindTurnId;
118
+ /**
119
+ * The app-server is gone (exited, failed, broken stream). A turn Codex already
120
+ * accepted would otherwise wait forever for a `turn/completed` that can never
121
+ * arrive, so report it terminal now as a model error. A turn whose
122
+ * `turn/start` is still unanswered is NOT reported here: that RPC rejects, and
123
+ * the rejected `send()` is its caller's terminal signal.
124
+ */
125
+ private onTransportClosed;
115
126
  private onNotification;
116
127
  private emitEvent;
117
128
  /**