@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1

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 (177) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +38 -1
  20. package/dist/context/compaction.js +138 -0
  21. package/dist/context/manager.d.ts +34 -2
  22. package/dist/context/manager.js +254 -58
  23. package/dist/context/token-counter.js +13 -0
  24. package/dist/credentials/access.d.ts +56 -0
  25. package/dist/credentials/access.js +183 -0
  26. package/dist/credentials/index.d.ts +1 -0
  27. package/dist/credentials/index.js +1 -0
  28. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  29. package/dist/credentials/inject-credential-tool.js +30 -11
  30. package/dist/credentials/types.d.ts +2 -2
  31. package/dist/credentials/use-credential-tool.d.ts +9 -1
  32. package/dist/credentials/use-credential-tool.js +58 -45
  33. package/dist/engine/engine.d.ts +38 -12
  34. package/dist/engine/engine.js +477 -163
  35. package/dist/engine/image-policy.d.ts +6 -0
  36. package/dist/engine/image-policy.js +17 -6
  37. package/dist/engine/input-attachments.d.ts +13 -0
  38. package/dist/engine/input-attachments.js +255 -0
  39. package/dist/engine/model-facade.d.ts +5 -2
  40. package/dist/engine/model-facade.js +6 -16
  41. package/dist/engine/parse-task.d.ts +10 -0
  42. package/dist/engine/parse-task.js +5 -0
  43. package/dist/engine/query.js +2 -0
  44. package/dist/engine/session-usage.d.ts +12 -0
  45. package/dist/engine/session-usage.js +56 -0
  46. package/dist/engine/steer-queue.d.ts +2 -1
  47. package/dist/engine/steer-queue.js +2 -2
  48. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  49. package/dist/engine/streaming-tool-queue.js +11 -7
  50. package/dist/engine/turn-loop.d.ts +34 -2
  51. package/dist/engine/turn-loop.js +269 -52
  52. package/dist/engine/types.d.ts +8 -0
  53. package/dist/git/worktree/crud.d.ts +69 -0
  54. package/dist/git/worktree/crud.js +206 -0
  55. package/dist/git/worktree/diff.d.ts +14 -0
  56. package/dist/git/worktree/diff.js +82 -0
  57. package/dist/git/worktree/git-exec.d.ts +7 -0
  58. package/dist/git/worktree/git-exec.js +51 -0
  59. package/dist/git/worktree/index.d.ts +5 -0
  60. package/dist/git/worktree/index.js +5 -0
  61. package/dist/git/worktree/query.d.ts +42 -0
  62. package/dist/git/worktree/query.js +121 -0
  63. package/dist/git/worktree/slug.d.ts +11 -0
  64. package/dist/git/worktree/slug.js +58 -0
  65. package/dist/git/worktree.d.ts +1 -84
  66. package/dist/git/worktree.js +5 -230
  67. package/dist/index.d.ts +31 -28
  68. package/dist/index.js +27 -25
  69. package/dist/logging/logger.js +6 -6
  70. package/dist/logging/sanitize-messages.d.ts +10 -2
  71. package/dist/logging/sanitize-messages.js +21 -6
  72. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  73. package/dist/plugins/installer/checkUpdate.js +4 -2
  74. package/dist/plugins/installer/install.js +2 -0
  75. package/dist/plugins/installer/installFromSource.js +9 -1
  76. package/dist/plugins/installer/parseSource.d.ts +4 -1
  77. package/dist/plugins/installer/parseSource.js +28 -10
  78. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  79. package/dist/plugins/installer/sourcePath.js +50 -0
  80. package/dist/plugins/installer/update.d.ts +4 -1
  81. package/dist/plugins/installer/update.js +5 -3
  82. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  83. package/dist/plugins/parseMarketplaceInput.js +4 -3
  84. package/dist/plugins/pluginInstaller.js +24 -22
  85. package/dist/preset/index.d.ts +1 -0
  86. package/dist/preset/index.js +16 -9
  87. package/dist/prompt/sections/base.md +1 -1
  88. package/dist/protocol/chat-session-manager.d.ts +2 -0
  89. package/dist/protocol/chat-session-manager.js +38 -2
  90. package/dist/protocol/chat-session.d.ts +5 -0
  91. package/dist/protocol/chat-session.js +2 -0
  92. package/dist/protocol/client.d.ts +13 -5
  93. package/dist/protocol/client.js +26 -3
  94. package/dist/protocol/server.d.ts +43 -12
  95. package/dist/protocol/server.js +326 -104
  96. package/dist/protocol/types.d.ts +51 -0
  97. package/dist/protocol/types.js +6 -0
  98. package/dist/run/FileRunStore.js +10 -1
  99. package/dist/run/Heartbeat.js +12 -0
  100. package/dist/run/RunApprovalBackend.d.ts +3 -0
  101. package/dist/run/RunApprovalBackend.js +41 -6
  102. package/dist/run/RunLock.js +2 -0
  103. package/dist/run/RunManager.d.ts +2 -0
  104. package/dist/run/RunManager.js +64 -24
  105. package/dist/run/ids.d.ts +2 -0
  106. package/dist/run/ids.js +23 -0
  107. package/dist/runtime/background-shell.d.ts +1 -0
  108. package/dist/runtime/background-shell.js +23 -13
  109. package/dist/runtime/safe-spawn.js +74 -11
  110. package/dist/runtime/spawn-common.js +10 -0
  111. package/dist/services/auto-dream.d.ts +15 -4
  112. package/dist/services/auto-dream.js +20 -20
  113. package/dist/services/dream-consolidation.d.ts +2 -3
  114. package/dist/services/dream-consolidation.js +65 -15
  115. package/dist/services/extract-memories.d.ts +14 -5
  116. package/dist/services/extract-memories.js +20 -3
  117. package/dist/services/global-dream-promotion.d.ts +23 -0
  118. package/dist/services/global-dream-promotion.js +112 -0
  119. package/dist/services/memory-orchestrator.js +347 -34
  120. package/dist/session/memory.d.ts +61 -18
  121. package/dist/session/memory.js +342 -79
  122. package/dist/session/session-manager.d.ts +35 -1
  123. package/dist/session/session-manager.js +197 -4
  124. package/dist/session/transcript.d.ts +5 -1
  125. package/dist/session/transcript.js +38 -5
  126. package/dist/settings/manager.d.ts +1 -0
  127. package/dist/settings/manager.js +87 -37
  128. package/dist/settings/schema-export.d.ts +2 -3
  129. package/dist/settings/schema-export.js +2 -3
  130. package/dist/settings/schema.d.ts +21 -0
  131. package/dist/settings/schema.js +12 -0
  132. package/dist/skills/scanner.d.ts +3 -2
  133. package/dist/skills/scanner.js +12 -9
  134. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  135. package/dist/tool-system/builtin/background-jobs.js +12 -1
  136. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  137. package/dist/tool-system/builtin/background-work.js +51 -5
  138. package/dist/tool-system/builtin/config.d.ts +2 -1
  139. package/dist/tool-system/builtin/config.js +16 -11
  140. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  141. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  142. package/dist/tool-system/builtin/edit.js +5 -2
  143. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  144. package/dist/tool-system/builtin/generate-video.js +13 -4
  145. package/dist/tool-system/builtin/index.d.ts +8 -3
  146. package/dist/tool-system/builtin/index.js +37 -21
  147. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  148. package/dist/tool-system/builtin/lsp.js +6 -3
  149. package/dist/tool-system/builtin/memory.js +40 -8
  150. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  151. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  152. package/dist/tool-system/builtin/powershell.js +11 -7
  153. package/dist/tool-system/builtin/read.js +118 -6
  154. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  155. package/dist/tool-system/builtin/view-image.js +109 -19
  156. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  157. package/dist/tool-system/builtin/worktree.js +297 -74
  158. package/dist/tool-system/builtin/write.js +5 -3
  159. package/dist/tool-system/context.d.ts +23 -1
  160. package/dist/tool-system/executor.d.ts +1 -5
  161. package/dist/tool-system/executor.js +94 -115
  162. package/dist/tool-system/mcp-manager.d.ts +18 -5
  163. package/dist/tool-system/mcp-manager.js +157 -74
  164. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  165. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  166. package/dist/tool-system/path-policy.d.ts +2 -0
  167. package/dist/tool-system/path-policy.js +41 -12
  168. package/dist/tool-system/permission.d.ts +28 -7
  169. package/dist/tool-system/permission.js +130 -49
  170. package/dist/tool-system/registry.js +11 -4
  171. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  172. package/dist/tool-system/tool-result-redaction.js +48 -0
  173. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  174. package/dist/tool-system/workspace-bridge.js +1 -0
  175. package/dist/types.d.ts +86 -5
  176. package/dist/utils/toolDisplay.js +1 -1
  177. package/package.json +4 -3
@@ -47,10 +47,43 @@ export declare const ErrorCodes: {
47
47
  */
48
48
  readonly Cancelled: -32005;
49
49
  };
50
+ export type InputAttachmentKind = "image" | "file" | "directory";
51
+ export type InputAttachmentOrigin = "paste" | "os-drop" | "file-panel" | "picker" | "mention" | "generated" | "tool";
52
+ export interface InputAttachmentMeta {
53
+ id: string;
54
+ sessionId: string;
55
+ kind: InputAttachmentKind;
56
+ origin: InputAttachmentOrigin;
57
+ path: string;
58
+ absPath: string;
59
+ relPath?: string;
60
+ mime?: string;
61
+ size: number;
62
+ sha256: string;
63
+ originalName?: string;
64
+ createdAt: number;
65
+ sourcePath?: string;
66
+ width?: number;
67
+ height?: number;
68
+ vision?: {
69
+ include: boolean;
70
+ mediaPath?: string;
71
+ detail?: "low" | "standard" | "high";
72
+ };
73
+ directory?: {
74
+ treePath?: string;
75
+ truncated?: boolean;
76
+ entryCount?: number;
77
+ };
78
+ }
50
79
  /** Start an agent run with a user task. */
51
80
  export interface RunParams {
52
81
  sessionId: string;
53
82
  task: string;
83
+ /** Structured input attachments. Legacy `<codeshell-image>` task blocks remain supported. */
84
+ attachments?: InputAttachmentMeta[];
85
+ /** Stable id for the user's submit intent; duplicate ids are idempotent. */
86
+ clientMessageId?: string;
54
87
  /**
55
88
  * Working directory for this run. When omitted, the Engine uses its
56
89
  * configured cwd.
@@ -121,6 +154,10 @@ export interface CancelParams {
121
154
  export interface CloseSessionParams {
122
155
  sessionId: string;
123
156
  }
157
+ /** Reset a live session's workspace binding back to main. */
158
+ export interface ReleaseWorkspaceParams {
159
+ sessionId: string;
160
+ }
124
161
  /** Inject context into a session transcript. */
125
162
  export interface InjectParams {
126
163
  sessionId: string;
@@ -133,6 +170,8 @@ export interface SteerParams {
133
170
  /** Stable host-side id for this queued draft. Rides through to the
134
171
  * steer_injected event and is the handle Unsteer uses to revoke it. */
135
172
  id?: string;
173
+ /** Stable submit-intent id, distinct from the queued steer id. */
174
+ clientMessageId?: string;
136
175
  }
137
176
  /** Revoke a still-pending steer entry by id (before the loop consumes it). */
138
177
  export interface UnsteerParams {
@@ -256,6 +295,12 @@ export interface ApprovalRequestNotification {
256
295
  requestId: string;
257
296
  request: ApprovalRequest;
258
297
  }
298
+ /** Server tells clients a pending approval/ask has been resolved elsewhere. */
299
+ export interface ApprovalResolvedNotification {
300
+ /** Originating engine session when known. */
301
+ sessionId?: string;
302
+ requestId: string;
303
+ }
259
304
  /** Server status changed. */
260
305
  export interface StatusNotification {
261
306
  status: "ready" | "running" | "error" | "shutdown";
@@ -275,6 +320,8 @@ export declare const Methods: {
275
320
  readonly Unsteer: "agent/unsteer";
276
321
  /** Close (destroy) a session. */
277
322
  readonly CloseSession: "agent/closeSession";
323
+ /** Reset a session's workspace binding to main without closing it. */
324
+ readonly ReleaseWorkspace: "agent/releaseWorkspace";
278
325
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
279
326
  readonly GoalExtend: "agent/goalExtend";
280
327
  /** Clear a session's persisted active goal (CC /goal clear). */
@@ -289,6 +336,10 @@ export declare const Methods: {
289
336
  readonly BackgroundWork: "agent/backgroundWork";
290
337
  readonly StreamEvent: "agent/streamEvent";
291
338
  readonly ApprovalRequest: "agent/approvalRequest";
339
+ /** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
340
+ * AskUserQuestion that timed out) so every client can dismiss its stale card
341
+ * without a user decision. Same envelope the desktop main broadcasts. */
342
+ readonly ApprovalResolved: "agent/approvalResolved";
292
343
  readonly Status: "agent/status";
293
344
  };
294
345
  export declare function createRequest(method: string, params?: Record<string, unknown>): RpcRequest;
@@ -41,6 +41,8 @@ export const Methods = {
41
41
  Unsteer: "agent/unsteer",
42
42
  /** Close (destroy) a session. */
43
43
  CloseSession: "agent/closeSession",
44
+ /** Reset a session's workspace binding to main without closing it. */
45
+ ReleaseWorkspace: "agent/releaseWorkspace",
44
46
  /** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
45
47
  GoalExtend: "agent/goalExtend",
46
48
  /** Clear a session's persisted active goal (CC /goal clear). */
@@ -56,6 +58,10 @@ export const Methods = {
56
58
  // Server → Client (notifications, no id)
57
59
  StreamEvent: "agent/streamEvent",
58
60
  ApprovalRequest: "agent/approvalRequest",
61
+ /** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
62
+ * AskUserQuestion that timed out) so every client can dismiss its stale card
63
+ * without a user decision. Same envelope the desktop main broadcasts. */
64
+ ApprovalResolved: "agent/approvalResolved",
59
65
  Status: "agent/status",
60
66
  };
61
67
  // ─── Helpers ────────────────────────────────────────────────────────
@@ -12,6 +12,8 @@
12
12
  import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, renameSync, rmSync, } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { homedir } from "node:os";
15
+ import { randomUUID } from "node:crypto";
16
+ import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
15
17
  export class FileRunStore {
16
18
  runsDir;
17
19
  constructor(storageDir) {
@@ -20,6 +22,7 @@ export class FileRunStore {
20
22
  }
21
23
  // ─── Helpers ───────────────────────────────────────────────────
22
24
  runDir(runId) {
25
+ assertSafeRunId(runId);
23
26
  return join(this.runsDir, runId);
24
27
  }
25
28
  ensureRunDir(runId) {
@@ -31,7 +34,7 @@ export class FileRunStore {
31
34
  return dir;
32
35
  }
33
36
  writeJson(filePath, data) {
34
- const tmp = filePath + ".tmp";
37
+ const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
35
38
  try {
36
39
  writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
37
40
  // Atomic rename — prevents partial writes on crash
@@ -153,6 +156,8 @@ export class FileRunStore {
153
156
  }
154
157
  // ─── Checkpoints ───────────────────────────────────────────────
155
158
  async saveCheckpoint(cp) {
159
+ assertSafeRunId(cp.runId);
160
+ assertSafeRunFileId(cp.checkpointId, "checkpoint id");
156
161
  const dir = this.runDir(cp.runId);
157
162
  this.writeJson(join(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
158
163
  }
@@ -175,10 +180,14 @@ export class FileRunStore {
175
180
  }
176
181
  // ─── Approvals ─────────────────────────────────────────────────
177
182
  async saveApproval(approval) {
183
+ assertSafeRunId(approval.runId);
184
+ assertSafeRunFileId(approval.approvalId, "approval id");
178
185
  const dir = this.runDir(approval.runId);
179
186
  this.writeJson(join(dir, "approvals", `${approval.approvalId}.json`), approval);
180
187
  }
181
188
  async getApproval(runId, approvalId) {
189
+ assertSafeRunId(runId);
190
+ assertSafeRunFileId(approvalId, "approval id");
182
191
  return this.readJson(join(this.runDir(runId), "approvals", `${approvalId}.json`));
183
192
  }
184
193
  async getPendingApproval(runId) {
@@ -11,6 +11,7 @@
11
11
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { homedir } from "node:os";
14
+ import { assertSafeRunId } from "./ids.js";
14
15
  export class Heartbeat {
15
16
  runsDir;
16
17
  intervalMs;
@@ -23,6 +24,12 @@ export class Heartbeat {
23
24
  * Start heartbeat for a run. Writes immediately, then repeats on interval.
24
25
  */
25
26
  start(runId) {
27
+ assertSafeRunId(runId);
28
+ const existing = this.timers.get(runId);
29
+ if (existing) {
30
+ clearInterval(existing);
31
+ this.timers.delete(runId);
32
+ }
26
33
  // Write first heartbeat immediately
27
34
  this.write(runId);
28
35
  // Schedule periodic heartbeats
@@ -37,6 +44,7 @@ export class Heartbeat {
37
44
  * Stop heartbeat for a run and remove the heartbeat file.
38
45
  */
39
46
  stop(runId) {
47
+ assertSafeRunId(runId);
40
48
  const timer = this.timers.get(runId);
41
49
  if (timer) {
42
50
  clearInterval(timer);
@@ -56,6 +64,7 @@ export class Heartbeat {
56
64
  * Read the last heartbeat for a run. Returns null if no heartbeat exists.
57
65
  */
58
66
  read(runId) {
67
+ assertSafeRunId(runId);
59
68
  const filePath = this.filePath(runId);
60
69
  if (!existsSync(filePath))
61
70
  return null;
@@ -71,6 +80,7 @@ export class Heartbeat {
71
80
  * Returns true if stale or missing, false if recent.
72
81
  */
73
82
  isStale(runId, thresholdMs) {
83
+ assertSafeRunId(runId);
74
84
  const threshold = thresholdMs ?? this.intervalMs * 3;
75
85
  const data = this.read(runId);
76
86
  if (!data)
@@ -81,6 +91,7 @@ export class Heartbeat {
81
91
  * Check if the process that wrote the heartbeat is still alive.
82
92
  */
83
93
  isProcessAlive(runId) {
94
+ assertSafeRunId(runId);
84
95
  const data = this.read(runId);
85
96
  if (!data)
86
97
  return false;
@@ -118,6 +129,7 @@ export class Heartbeat {
118
129
  }
119
130
  }
120
131
  filePath(runId) {
132
+ assertSafeRunId(runId);
121
133
  return join(this.runsDir, runId, "heartbeat");
122
134
  }
123
135
  }
@@ -16,6 +16,7 @@ import type { AskUserFn } from "../tool-system/builtin/ask-user.js";
16
16
  export interface PendingApproval {
17
17
  resolve: (result: ApprovalResult) => void;
18
18
  request: ApprovalRequest;
19
+ timer: ReturnType<typeof setTimeout>;
19
20
  }
20
21
  export interface PendingInput {
21
22
  resolve: (answer: string) => void;
@@ -32,6 +33,7 @@ export interface RunLifecycleHooks {
32
33
  export declare class RunApprovalBackend implements ApprovalBackend {
33
34
  private pendingApproval;
34
35
  private hooks;
36
+ private approvalRequestSeq;
35
37
  setHooks(hooks: RunLifecycleHooks): void;
36
38
  /** Max time (ms) to wait for approval before auto-rejecting. Default: 24h */
37
39
  private timeoutMs;
@@ -42,6 +44,7 @@ export declare class RunApprovalBackend implements ApprovalBackend {
42
44
  */
43
45
  resolveApproval(result: ApprovalResult): boolean;
44
46
  hasPendingApproval(): boolean;
47
+ private supersedePendingApproval;
45
48
  }
46
49
  /**
47
50
  * Creates an AskUserFn that suspends the run when user input is needed.
@@ -14,6 +14,7 @@
14
14
  export class RunApprovalBackend {
15
15
  pendingApproval = null;
16
16
  hooks = null;
17
+ approvalRequestSeq = 0;
17
18
  setHooks(hooks) {
18
19
  this.hooks = hooks;
19
20
  }
@@ -23,7 +24,8 @@ export class RunApprovalBackend {
23
24
  this.timeoutMs = ms;
24
25
  }
25
26
  async requestApproval(request) {
26
- if (!this.hooks) {
27
+ const hooks = this.hooks;
28
+ if (!hooks) {
27
29
  // No hooks wired — fail CLOSED. This backend is the engine's approval
28
30
  // path for an interactive run; if a host forgot to setHooks() there is
29
31
  // no UI to ask, so auto-approving every tool (the old behavior) silently
@@ -34,11 +36,8 @@ export class RunApprovalBackend {
34
36
  reason: "run approval backend has no lifecycle hooks wired (denied fail-closed)",
35
37
  };
36
38
  }
37
- // Notify RunManager that approval is needed
38
- const { approvalId } = await this.hooks.onApprovalNeeded(request);
39
- // Suspend execution until resolved (with timeout safety net)
39
+ const requestSeq = ++this.approvalRequestSeq;
40
40
  return new Promise((resolve) => {
41
- this.pendingApproval = { resolve, request };
42
41
  const timer = setTimeout(() => {
43
42
  if (this.pendingApproval?.resolve === resolve) {
44
43
  this.pendingApproval = null;
@@ -48,6 +47,30 @@ export class RunApprovalBackend {
48
47
  if (typeof timer === "object" && "unref" in timer) {
49
48
  timer.unref();
50
49
  }
50
+ this.supersedePendingApproval();
51
+ this.pendingApproval = { resolve, request, timer };
52
+ void Promise.resolve()
53
+ .then(() => hooks.onApprovalNeeded(request))
54
+ .then(() => {
55
+ if (requestSeq !== this.approvalRequestSeq &&
56
+ this.pendingApproval?.resolve === resolve) {
57
+ this.pendingApproval = null;
58
+ clearTimeout(timer);
59
+ resolve({
60
+ approved: false,
61
+ reason: "superseded: a newer approval request replaced this one before it was answered",
62
+ });
63
+ }
64
+ }, (err) => {
65
+ if (this.pendingApproval?.resolve !== resolve)
66
+ return;
67
+ this.pendingApproval = null;
68
+ clearTimeout(timer);
69
+ resolve({
70
+ approved: false,
71
+ reason: `approval request failed: ${err instanceof Error ? err.message : String(err)}`,
72
+ });
73
+ });
51
74
  });
52
75
  }
53
76
  /**
@@ -56,14 +79,26 @@ export class RunApprovalBackend {
56
79
  resolveApproval(result) {
57
80
  if (!this.pendingApproval)
58
81
  return false;
59
- const { resolve } = this.pendingApproval;
82
+ const { resolve, timer } = this.pendingApproval;
60
83
  this.pendingApproval = null;
84
+ clearTimeout(timer);
61
85
  resolve(result);
62
86
  return true;
63
87
  }
64
88
  hasPendingApproval() {
65
89
  return this.pendingApproval !== null;
66
90
  }
91
+ supersedePendingApproval() {
92
+ if (!this.pendingApproval)
93
+ return;
94
+ const { resolve, timer } = this.pendingApproval;
95
+ this.pendingApproval = null;
96
+ clearTimeout(timer);
97
+ resolve({
98
+ approved: false,
99
+ reason: "superseded: a newer approval request replaced this one before it was answered",
100
+ });
101
+ }
67
102
  }
68
103
  // ─── RunAskUserAdapter ───────────────────────────────────────────
69
104
  /**
@@ -10,6 +10,7 @@ import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { lock, check, unlock } from "../utils/lockfile.js";
12
12
  import { logger } from "../logging/logger.js";
13
+ import { assertSafeRunId } from "./ids.js";
13
14
  export class RunLock {
14
15
  runsDir;
15
16
  staleMs;
@@ -135,6 +136,7 @@ export class RunLock {
135
136
  }
136
137
  }
137
138
  lockTarget(runId) {
139
+ assertSafeRunId(runId);
138
140
  return join(this.runsDir, runId, "run.json");
139
141
  }
140
142
  }
@@ -66,6 +66,7 @@ export declare class RunManager {
66
66
  resume(runId: string, input?: ResumeRunInput): Promise<void>;
67
67
  private resumeInner;
68
68
  cancel(runId: string, reason?: string): Promise<void>;
69
+ private cancelInner;
69
70
  get(runId: string): Promise<RunSnapshot | null>;
70
71
  list(query?: ListRunsQuery): Promise<RunSnapshot[]>;
71
72
  getEvents(runId: string): Promise<RunEvent[]>;
@@ -90,5 +91,6 @@ export declare class RunManager {
90
91
  private transition;
91
92
  private emitRunEvent;
92
93
  private notifySubscribers;
94
+ private getPendingApprovalForResume;
93
95
  private getOrThrow;
94
96
  }
@@ -19,7 +19,13 @@ import { ArtifactTracker } from "./ArtifactTracker.js";
19
19
  import { RunLock } from "./RunLock.js";
20
20
  import { Heartbeat } from "./Heartbeat.js";
21
21
  import { NoopEvaluator } from "./Evaluator.js";
22
+ import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
22
23
  import { VALID_TRANSITIONS } from "./types.js";
24
+ function hasOwnString(value, key) {
25
+ return (value !== undefined &&
26
+ Object.prototype.hasOwnProperty.call(value, key) &&
27
+ typeof value[key] === "string");
28
+ }
23
29
  export class RunManager {
24
30
  store;
25
31
  queue;
@@ -69,6 +75,9 @@ export class RunManager {
69
75
  async submit(input) {
70
76
  const now = Date.now();
71
77
  const runId = nanoid(16);
78
+ if (input.parentRunId !== undefined && input.parentRunId !== null) {
79
+ assertSafeRunId(input.parentRunId);
80
+ }
72
81
  const snapshot = {
73
82
  runId,
74
83
  objective: input.objective,
@@ -110,6 +119,7 @@ export class RunManager {
110
119
  }
111
120
  // ─── Resume ────────────────────────────────────────────────────
112
121
  async resume(runId, input) {
122
+ assertSafeRunId(runId);
113
123
  // Serialize resume/cancel decisions per run. Without this, two concurrent
114
124
  // resumes (or resume racing cancel) both pass the status check below —
115
125
  // status stays `waiting_*` until a later `await transition()` — and both
@@ -139,18 +149,15 @@ export class RunManager {
139
149
  if (handle) {
140
150
  if (run.status === "waiting_approval" && input?.approvalDecision) {
141
151
  const { approvalId, approved, reason } = input.approvalDecision;
142
- // Persist the approval decision
143
- const approval = await this.store.getApproval(runId, approvalId);
144
- if (approval && approval.status === "pending") {
145
- approval.status = approved ? "approved" : "rejected";
146
- approval.resolvedAt = Date.now();
147
- await this.store.saveApproval(approval);
148
- await this.emitRunEvent(runId, "approval_resolved", {
149
- approvalId,
150
- approved,
151
- reason,
152
- });
153
- }
152
+ const approval = await this.getPendingApprovalForResume(run, approvalId);
153
+ approval.status = approved ? "approved" : "rejected";
154
+ approval.resolvedAt = Date.now();
155
+ await this.store.saveApproval(approval);
156
+ await this.emitRunEvent(runId, "approval_resolved", {
157
+ approvalId,
158
+ approved,
159
+ reason,
160
+ });
154
161
  // Transition back to running and resolve the suspended Engine
155
162
  await this.transition(run, "queued");
156
163
  await this.transition(run, "running");
@@ -158,7 +165,7 @@ export class RunManager {
158
165
  handle.resolveApproval(approved, reason);
159
166
  return;
160
167
  }
161
- if (run.status === "waiting_input" && input?.userInput) {
168
+ if (run.status === "waiting_input" && hasOwnString(input, "userInput")) {
162
169
  await this.transition(run, "queued");
163
170
  await this.transition(run, "running");
164
171
  await this.emitRunEvent(runId, "run_resumed", { userInput: input.userInput });
@@ -177,17 +184,15 @@ export class RunManager {
177
184
  // Re-queue the run for a fresh execution attempt
178
185
  if (input?.approvalDecision) {
179
186
  const { approvalId, approved, reason } = input.approvalDecision;
180
- const approval = await this.store.getApproval(runId, approvalId);
181
- if (approval && approval.status === "pending") {
182
- approval.status = approved ? "approved" : "rejected";
183
- approval.resolvedAt = Date.now();
184
- await this.store.saveApproval(approval);
185
- await this.emitRunEvent(runId, "approval_resolved", {
186
- approvalId,
187
- approved,
188
- reason,
189
- });
190
- }
187
+ const approval = await this.getPendingApprovalForResume(run, approvalId);
188
+ approval.status = approved ? "approved" : "rejected";
189
+ approval.resolvedAt = Date.now();
190
+ await this.store.saveApproval(approval);
191
+ await this.emitRunEvent(runId, "approval_resolved", {
192
+ approvalId,
193
+ approved,
194
+ reason,
195
+ });
191
196
  }
192
197
  await this.transition(run, "queued");
193
198
  await this.emitRunEvent(runId, "run_resumed", {
@@ -197,6 +202,19 @@ export class RunManager {
197
202
  }
198
203
  // ─── Cancel ────────────────────────────────────────────────────
199
204
  async cancel(runId, reason) {
205
+ assertSafeRunId(runId);
206
+ if (this.resolvingRuns.has(runId)) {
207
+ throw new Error(`Cannot cancel run ${runId}: another resume/cancel is already in progress`);
208
+ }
209
+ this.resolvingRuns.add(runId);
210
+ try {
211
+ await this.cancelInner(runId, reason);
212
+ }
213
+ finally {
214
+ this.resolvingRuns.delete(runId);
215
+ }
216
+ }
217
+ async cancelInner(runId, reason) {
200
218
  const run = await this.getOrThrow(runId);
201
219
  if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
202
220
  throw new Error(`Cannot cancel run ${runId}: already in terminal state ${run.status}`);
@@ -225,16 +243,19 @@ export class RunManager {
225
243
  }
226
244
  // ─── Query ─────────────────────────────────────────────────────
227
245
  async get(runId) {
246
+ assertSafeRunId(runId);
228
247
  return this.store.get(runId);
229
248
  }
230
249
  async list(query) {
231
250
  return this.store.list(query);
232
251
  }
233
252
  async getEvents(runId) {
253
+ assertSafeRunId(runId);
234
254
  return this.store.listEvents(runId);
235
255
  }
236
256
  // ─── Attach (live stream) ──────────────────────────────────────
237
257
  attach(runId, cb) {
258
+ assertSafeRunId(runId);
238
259
  if (!this.subscribers.has(runId)) {
239
260
  this.subscribers.set(runId, new Set());
240
261
  }
@@ -326,6 +347,7 @@ export class RunManager {
326
347
  }
327
348
  // ─── Internal: Execute a run ───────────────────────────────────
328
349
  async executeRun(runId) {
350
+ assertSafeRunId(runId);
329
351
  const run = await this.getOrThrow(runId);
330
352
  // Acquire run lock — prevents concurrent execution by multiple workers
331
353
  const lockResult = await this.lock.acquire(runId);
@@ -657,7 +679,25 @@ export class RunManager {
657
679
  }
658
680
  }
659
681
  // ─── Helpers ───────────────────────────────────────────────────
682
+ async getPendingApprovalForResume(run, approvalId) {
683
+ assertSafeRunFileId(approvalId, "approval id");
684
+ if (run.latestApprovalId !== approvalId) {
685
+ throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} is not the latest pending approval`);
686
+ }
687
+ const approval = await this.store.getApproval(run.runId, approvalId);
688
+ if (!approval) {
689
+ throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} was not found`);
690
+ }
691
+ if (approval.runId !== run.runId || approval.approvalId !== approvalId) {
692
+ throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} does not match this run`);
693
+ }
694
+ if (approval.status !== "pending") {
695
+ throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} is ${approval.status}, not pending`);
696
+ }
697
+ return approval;
698
+ }
660
699
  async getOrThrow(runId) {
700
+ assertSafeRunId(runId);
661
701
  const run = await this.store.get(runId);
662
702
  if (!run)
663
703
  throw new Error(`Run not found: ${runId}`);
@@ -0,0 +1,2 @@
1
+ export declare function assertSafeRunId(runId: unknown): asserts runId is string;
2
+ export declare function assertSafeRunFileId(value: unknown, label: string): asserts value is string;
@@ -0,0 +1,23 @@
1
+ function assertSafeRunSegment(value, label) {
2
+ if (typeof value !== "string" || value.length === 0) {
3
+ throw new Error(`invalid ${label}: must be a non-empty string`);
4
+ }
5
+ if (value.includes("/") || value.includes("\\")) {
6
+ throw new Error(`invalid ${label}: contains path separator: ${value}`);
7
+ }
8
+ if (value === "." || value === ".." || value.includes("..")) {
9
+ throw new Error(`invalid ${label}: contains parent-dir token: ${value}`);
10
+ }
11
+ if (!/^[A-Za-z0-9_.-]+$/.test(value)) {
12
+ throw new Error(`invalid ${label}: unexpected characters: ${value}`);
13
+ }
14
+ if (value.length > 128) {
15
+ throw new Error(`invalid ${label}: too long (max 128 chars)`);
16
+ }
17
+ }
18
+ export function assertSafeRunId(runId) {
19
+ assertSafeRunSegment(runId, "run id");
20
+ }
21
+ export function assertSafeRunFileId(value, label) {
22
+ assertSafeRunSegment(value, label);
23
+ }
@@ -103,6 +103,7 @@ export declare class BackgroundShellManager {
103
103
  /** Raw retained output (no cleaning) — test/UI helper. */
104
104
  readOutputRaw(shellId: string): string | undefined;
105
105
  listForSession(sessionId: string): BgShell[];
106
+ list(): BgShell[];
106
107
  /**
107
108
  * Read a shell's output. `mode=incremental` (default) returns text since the
108
109
  * agent's last read; `mode=all` returns the full retained buffer. Output is