@cjhyy/code-shell-core 0.5.0-rc.0 → 0.5.0-rc.2

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 (283) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +29 -0
  2. package/dist/agent/agent-definition-registry.js +74 -0
  3. package/dist/agent/agent-definition.d.ts +37 -0
  4. package/dist/agent/agent-definition.js +59 -0
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +118 -18
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +256 -16
  68. package/dist/engine/engine.js +856 -200
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +32 -7
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +10 -0
  171. package/dist/prompt/composer.js +29 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +45 -1
  220. package/dist/settings/manager.js +121 -17
  221. package/dist/settings/manager.test.d.ts +1 -0
  222. package/dist/settings/manager.test.js +73 -0
  223. package/dist/settings/personalization.d.ts +19 -0
  224. package/dist/settings/personalization.js +7 -0
  225. package/dist/settings/schema.d.ts +1163 -126
  226. package/dist/settings/schema.js +126 -10
  227. package/dist/skills/scanner.d.ts +13 -6
  228. package/dist/skills/scanner.js +23 -4
  229. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  230. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  231. package/dist/tool-system/builtin/agent-registry.d.ts +15 -0
  232. package/dist/tool-system/builtin/agent-registry.js +14 -4
  233. package/dist/tool-system/builtin/agent.d.ts +55 -0
  234. package/dist/tool-system/builtin/agent.js +165 -17
  235. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  236. package/dist/tool-system/builtin/arena.js +6 -7
  237. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  238. package/dist/tool-system/builtin/complete-goal.js +45 -0
  239. package/dist/tool-system/builtin/cron.js +53 -9
  240. package/dist/tool-system/builtin/edit.d.ts +2 -1
  241. package/dist/tool-system/builtin/edit.js +6 -1
  242. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  243. package/dist/tool-system/builtin/generate-image.js +146 -0
  244. package/dist/tool-system/builtin/glob.js +7 -0
  245. package/dist/tool-system/builtin/grep.js +6 -0
  246. package/dist/tool-system/builtin/index.d.ts +18 -1
  247. package/dist/tool-system/builtin/index.js +62 -1
  248. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  249. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  250. package/dist/tool-system/builtin/read.d.ts +2 -1
  251. package/dist/tool-system/builtin/read.js +9 -1
  252. package/dist/tool-system/builtin/skill.js +23 -5
  253. package/dist/tool-system/builtin/sleep.js +9 -3
  254. package/dist/tool-system/builtin/task.js +4 -1
  255. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  256. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  257. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  258. package/dist/tool-system/builtin/view-image.js +96 -0
  259. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  260. package/dist/tool-system/builtin/web-search.js +36 -3
  261. package/dist/tool-system/builtin/write.d.ts +2 -1
  262. package/dist/tool-system/builtin/write.js +7 -1
  263. package/dist/tool-system/context.d.ts +43 -0
  264. package/dist/tool-system/executor.d.ts +0 -1
  265. package/dist/tool-system/executor.js +34 -40
  266. package/dist/tool-system/investigation-guard.d.ts +4 -1
  267. package/dist/tool-system/investigation-guard.js +19 -3
  268. package/dist/tool-system/mcp-manager.d.ts +36 -12
  269. package/dist/tool-system/mcp-manager.js +173 -10
  270. package/dist/tool-system/path-policy.d.ts +94 -0
  271. package/dist/tool-system/path-policy.js +279 -0
  272. package/dist/tool-system/permission.js +43 -50
  273. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  274. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  275. package/dist/tool-system/registry.js +12 -1
  276. package/dist/tool-system/validation.d.ts +10 -0
  277. package/dist/tool-system/validation.js +16 -3
  278. package/dist/types.d.ts +90 -15
  279. package/dist/utils/format.js +4 -2
  280. package/dist/utils/lockfile.js +9 -2
  281. package/dist/utils/theme.d.ts +6 -0
  282. package/dist/utils/theme.js +4 -2
  283. package/package.json +2 -1
@@ -40,6 +40,12 @@ export declare const ErrorCodes: {
40
40
  readonly Overloaded: -32001;
41
41
  readonly SessionNotFound: -32002;
42
42
  readonly SessionClosed: -32004;
43
+ /**
44
+ * Run was cancelled by the user (ESC / Stop). Clients should treat
45
+ * this as a clean terminal state, not a real error: no banner, no
46
+ * red toast — just clear busy and stop streaming.
47
+ */
48
+ readonly Cancelled: -32005;
43
49
  };
44
50
  /** Start an agent run with a user task. */
45
51
  export interface RunParams {
@@ -60,6 +66,15 @@ export interface RunParams {
60
66
  * this run; when omitted, the engine keeps its configured default.
61
67
  */
62
68
  planMode?: boolean;
69
+ /**
70
+ * Goal mode for this run. When set, the engine runs loop-until-done:
71
+ * on each natural completion a GoalStopHook judges whether this goal is
72
+ * met and re-prompts if not (bounded). Orthogonal to permissionMode.
73
+ *
74
+ * Accepts a bare objective string or a full GoalConfig (objective +
75
+ * optional token/time budgets). Normalized at the engine run boundary.
76
+ */
77
+ goal?: string | import("../engine/goal.js").GoalConfig;
63
78
  }
64
79
  export interface RunResult {
65
80
  text: string;
@@ -104,6 +119,14 @@ export interface ConfigureParams {
104
119
  * running engine picks them up without a process restart.
105
120
  */
106
121
  reloadModels?: boolean;
122
+ /**
123
+ * Re-read disk settings and hot-push the disk-default config fields (preset /
124
+ * customSystemPrompt / appendSystemPrompt / personalization / mcpServers) +
125
+ * settings hooks onto ALREADY-RUNNING sessions ("config hot-reload layer 2").
126
+ * No sessionId → applies to every live session; with sessionId → that one.
127
+ * Applied at the next turn boundary; in-flight turns are not interrupted.
128
+ */
129
+ reloadSettings?: boolean;
107
130
  }
108
131
  /** Query server state. */
109
132
  export interface QueryParams {
@@ -18,6 +18,12 @@ export const ErrorCodes = {
18
18
  Overloaded: -32001,
19
19
  SessionNotFound: -32002,
20
20
  SessionClosed: -32004,
21
+ /**
22
+ * Run was cancelled by the user (ESC / Stop). Clients should treat
23
+ * this as a clean terminal state, not a real error: no banner, no
24
+ * red toast — just clear busy and stop streaming.
25
+ */
26
+ Cancelled: -32005,
21
27
  };
22
28
  // ─── Method Names ───────────────────────────────────────────────────
23
29
  export const Methods = {
@@ -4,6 +4,7 @@
4
4
  * Architecture:
5
5
  * Local terminal ←→ SSH ←→ Remote code-shell (NDJSON stdio mode)
6
6
  */
7
+ import { type ChildProcess } from "node:child_process";
7
8
  export interface BridgeConfig {
8
9
  host: string;
9
10
  user?: string;
@@ -11,11 +12,20 @@ export interface BridgeConfig {
11
12
  identityFile?: string;
12
13
  remoteCommand?: string;
13
14
  }
15
+ /** A spawn function (injectable for tests). Defaults to child_process.spawn. */
16
+ export type SpawnFn = (command: string, args: string[]) => ChildProcess;
17
+ /**
18
+ * Build the ssh argv. Each value is a discrete element — spawn runs ssh
19
+ * without a shell, so identityFile/port/host are never shell-interpreted
20
+ * (there is no local command-injection surface here).
21
+ */
22
+ export declare function buildSSHArgs(config: BridgeConfig): string[];
14
23
  export declare class RemoteBridge {
15
24
  private readonly config;
16
25
  private ssh;
17
26
  private connected;
18
- constructor(config: BridgeConfig);
27
+ private readonly spawnFn;
28
+ constructor(config: BridgeConfig, spawnFn?: SpawnFn);
19
29
  /**
20
30
  * Connect to the remote host via SSH and start code-shell in NDJSON mode.
21
31
  */
@@ -33,5 +43,4 @@ export declare class RemoteBridge {
33
43
  */
34
44
  disconnect(): void;
35
45
  get isConnected(): boolean;
36
- private buildSSHArgs;
37
46
  }
@@ -6,42 +6,71 @@
6
6
  */
7
7
  import { spawn } from "node:child_process";
8
8
  import { createInterface } from "node:readline";
9
+ /**
10
+ * Build the ssh argv. Each value is a discrete element — spawn runs ssh
11
+ * without a shell, so identityFile/port/host are never shell-interpreted
12
+ * (there is no local command-injection surface here).
13
+ */
14
+ export function buildSSHArgs(config) {
15
+ const args = ["-T"]; // No pseudo-terminal
16
+ if (config.port)
17
+ args.push("-p", String(config.port));
18
+ if (config.identityFile)
19
+ args.push("-i", config.identityFile);
20
+ args.push(config.user ? `${config.user}@${config.host}` : config.host);
21
+ return args;
22
+ }
9
23
  export class RemoteBridge {
10
24
  config;
11
25
  ssh;
12
26
  connected = false;
13
- constructor(config) {
27
+ spawnFn;
28
+ constructor(config, spawnFn) {
14
29
  this.config = config;
30
+ this.spawnFn = spawnFn ?? ((cmd, args) => spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] }));
15
31
  }
16
32
  /**
17
33
  * Connect to the remote host via SSH and start code-shell in NDJSON mode.
18
34
  */
19
35
  async connect() {
20
- const sshArgs = this.buildSSHArgs();
36
+ const sshArgs = buildSSHArgs(this.config);
21
37
  const remoteCmd = this.config.remoteCommand ?? "code-shell run --output stream-json";
22
- this.ssh = spawn("ssh", [...sshArgs, remoteCmd], {
23
- stdio: ["pipe", "pipe", "pipe"],
24
- });
38
+ this.ssh = this.spawnFn("ssh", [...sshArgs, remoteCmd]);
25
39
  return new Promise((resolve, reject) => {
40
+ // Settle exactly once: the first stdout resolves; an error/early-exit
41
+ // before that rejects. Events after settlement are ignored (the old
42
+ // code re-rejected an already-settled promise and used a dead
43
+ // `if (!this.connected)` guard that was always true).
44
+ let settled = false;
26
45
  const timer = setTimeout(() => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
27
49
  reject(new Error("SSH connection timed out"));
28
50
  this.disconnect();
29
51
  }, 30000);
30
52
  this.ssh.stdout?.once("data", () => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
31
56
  clearTimeout(timer);
32
57
  this.connected = true;
33
58
  resolve();
34
59
  });
35
60
  this.ssh.on("error", (err) => {
61
+ if (settled)
62
+ return;
63
+ settled = true;
36
64
  clearTimeout(timer);
37
65
  reject(err);
38
66
  });
39
67
  this.ssh.on("exit", (code) => {
40
68
  this.connected = false;
41
- if (!this.connected) {
42
- clearTimeout(timer);
43
- reject(new Error(`SSH exited with code ${code}`));
44
- }
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ clearTimeout(timer);
73
+ reject(new Error(`SSH exited with code ${code}`));
45
74
  });
46
75
  });
47
76
  }
@@ -52,7 +81,13 @@ export class RemoteBridge {
52
81
  if (!this.connected || !this.ssh?.stdin) {
53
82
  throw new Error("Not connected");
54
83
  }
55
- this.ssh.stdin.write(JSON.stringify(message) + "\n");
84
+ this.ssh.stdin.write(JSON.stringify(message) + "\n", (err) => {
85
+ if (err) {
86
+ // A broken pipe means the remote is gone — surface it as a
87
+ // disconnect rather than swallowing the failed write.
88
+ this.connected = false;
89
+ }
90
+ });
56
91
  }
57
92
  /**
58
93
  * Async iterator for messages from the remote code-shell.
@@ -61,17 +96,24 @@ export class RemoteBridge {
61
96
  if (!this.ssh?.stdout)
62
97
  return;
63
98
  const rl = createInterface({ input: this.ssh.stdout, crlfDelay: Infinity });
64
- for await (const line of rl) {
65
- const trimmed = line.trim();
66
- if (!trimmed)
67
- continue;
68
- try {
69
- yield JSON.parse(trimmed);
70
- }
71
- catch {
72
- // Skip malformed
99
+ try {
100
+ for await (const line of rl) {
101
+ const trimmed = line.trim();
102
+ if (!trimmed)
103
+ continue;
104
+ try {
105
+ yield JSON.parse(trimmed);
106
+ }
107
+ catch {
108
+ // Skip malformed
109
+ }
73
110
  }
74
111
  }
112
+ finally {
113
+ // Close the interface if the consumer breaks/returns early — otherwise
114
+ // it (and its stdout listener) leaks.
115
+ rl.close();
116
+ }
75
117
  }
76
118
  /**
77
119
  * Disconnect from the remote host.
@@ -86,18 +128,4 @@ export class RemoteBridge {
86
128
  get isConnected() {
87
129
  return this.connected;
88
130
  }
89
- buildSSHArgs() {
90
- const args = ["-T"]; // No pseudo-terminal
91
- if (this.config.port)
92
- args.push("-p", String(this.config.port));
93
- if (this.config.identityFile)
94
- args.push("-i", this.config.identityFile);
95
- if (this.config.user) {
96
- args.push(`${this.config.user}@${this.config.host}`);
97
- }
98
- else {
99
- args.push(this.config.host);
100
- }
101
- return args;
102
- }
103
131
  }
@@ -12,6 +12,7 @@
12
12
  * - Judgment: file writes are artifacts, reads are not
13
13
  */
14
14
  import { nanoid } from "nanoid";
15
+ import { parseRedirectTarget } from "./redirect-target.js";
15
16
  export class ArtifactTracker {
16
17
  config;
17
18
  pendingCalls = new Map();
@@ -111,10 +112,10 @@ export class ArtifactTracker {
111
112
  await this.config.store.appendArtifactRef(ref);
112
113
  return;
113
114
  }
114
- // Redirect output: command > file or command >> file
115
- const redirectMatch = trimmed.match(/>\s*(\S+)\s*$/);
116
- if (redirectMatch) {
117
- await this.recordFileArtifact(redirectMatch[1], "output", "file");
115
+ // Redirect output: command > file or command >> file (quoted-path aware)
116
+ const redirectTarget = parseRedirectTarget(trimmed);
117
+ if (redirectTarget) {
118
+ await this.recordFileArtifact(redirectTarget, "output", "file");
118
119
  return;
119
120
  }
120
121
  // cp / mv — destination is an artifact
@@ -9,9 +9,25 @@
9
9
  * - Return execution result (does NOT manage run state transitions)
10
10
  */
11
11
  import { type EngineConfig, type EngineHookConfig } from "../engine/engine.js";
12
- import type { LLMConfig, RegisteredTool } from "../types.js";
12
+ import type { ClientDefaults, LLMConfig, RegisteredTool } from "../types.js";
13
13
  import type { RunSnapshot, RunExecutionContext, RunExecutionResult } from "./types.js";
14
14
  import { type RunLifecycleHooks } from "./RunApprovalBackend.js";
15
+ import type { ApprovalBackend } from "../tool-system/permission.js";
16
+ /** Unattended runs (those with an injected approval backend) run headless so
17
+ * the in-process AgentServer does not wire an interactive askUser. Exported
18
+ * for unit testing the decision rule. */
19
+ export declare function buildHeadlessFlag(override: ApprovalBackend | undefined): boolean;
20
+ /** Run-metadata `source` value tagging a run as unattended automation
21
+ * (set by the automation host; read here to decide prompt/headless behavior). */
22
+ export declare const AUTOMATION_RUN_SOURCE = "automation";
23
+ /** Appended to the system prompt for unattended automation runs so the model
24
+ * knows it IS the automation and must not ask the user or offer to schedule
25
+ * automation. English by repo convention; the model answers in the user's
26
+ * language regardless. */
27
+ export declare const AUTOMATION_PROMPT_NOTE: string;
28
+ /** Compose the run's appendSystemPrompt: prepend the automation note when the
29
+ * run is tagged source "automation", preserving any host-provided append. */
30
+ export declare function buildAppendSystemPrompt(hostAppend: string | undefined, metadata: Record<string, unknown> | undefined): string | undefined;
15
31
  /**
16
32
  * Handles returned from execute() to allow RunManager to resolve
17
33
  * pending approvals and user input while Engine is suspended.
@@ -48,6 +64,8 @@ export interface CustomToolEntry {
48
64
  }
49
65
  export interface EngineRunnerConfig {
50
66
  llm: LLMConfig;
67
+ /** Cross-model runtime knobs (temperature/timeout/imageDetail). */
68
+ clientDefaults?: ClientDefaults;
51
69
  maxTurns?: number;
52
70
  maxContextTokens?: number;
53
71
  sessionStorageDir?: string;
@@ -61,6 +79,14 @@ export interface EngineRunnerConfig {
61
79
  customTools?: CustomToolEntry[];
62
80
  /** Hooks registered into each Engine instance before run execution. */
63
81
  hooks?: EngineHookConfig[];
82
+ /**
83
+ * Override the approval backend. When set, the run uses THIS backend instead
84
+ * of the interactive run-aware RunApprovalBackend — used by unattended hosts
85
+ * (e.g. automation/cron) that must auto-decide without a UI, e.g. a
86
+ * HeadlessApprovalBackend("approve-read-only"). When unset, the interactive
87
+ * run-aware backend is used (default REPL/desktop behavior).
88
+ */
89
+ approvalBackend?: ApprovalBackend;
64
90
  }
65
91
  export declare class EngineRunner implements RunExecutor {
66
92
  private readonly config;
@@ -11,6 +11,33 @@
11
11
  import { Engine } from "../engine/engine.js";
12
12
  import { RunApprovalBackend, createRunAskUserFn, } from "./RunApprovalBackend.js";
13
13
  import { createInProcessClient } from "../protocol/helpers.js";
14
+ /** Unattended runs (those with an injected approval backend) run headless so
15
+ * the in-process AgentServer does not wire an interactive askUser. Exported
16
+ * for unit testing the decision rule. */
17
+ export function buildHeadlessFlag(override) {
18
+ return override !== undefined;
19
+ }
20
+ /** Run-metadata `source` value tagging a run as unattended automation
21
+ * (set by the automation host; read here to decide prompt/headless behavior). */
22
+ export const AUTOMATION_RUN_SOURCE = "automation";
23
+ /** Appended to the system prompt for unattended automation runs so the model
24
+ * knows it IS the automation and must not ask the user or offer to schedule
25
+ * automation. English by repo convention; the model answers in the user's
26
+ * language regardless. */
27
+ export const AUTOMATION_PROMPT_NOTE = "This is an unattended, scheduled automation run. No human is watching, and " +
28
+ "AskUserQuestion will not reach anyone. You ARE the automation — do not ask " +
29
+ "the user questions and do not offer to set up or schedule automation. " +
30
+ "Produce the requested output directly; when uncertain, state your assumption " +
31
+ "and proceed." +
32
+ " When finished, call UpdateAutomationMemory exactly once with a concise " +
33
+ "summary of this run's key findings/state for the next run.";
34
+ /** Compose the run's appendSystemPrompt: prepend the automation note when the
35
+ * run is tagged source "automation", preserving any host-provided append. */
36
+ export function buildAppendSystemPrompt(hostAppend, metadata) {
37
+ if (metadata?.source !== AUTOMATION_RUN_SOURCE)
38
+ return hostAppend;
39
+ return hostAppend ? `${AUTOMATION_PROMPT_NOTE}\n\n${hostAppend}` : AUTOMATION_PROMPT_NOTE;
40
+ }
14
41
  // ─── EngineRunner (built-in RunExecutor) ────────────────────────
15
42
  export class EngineRunner {
16
43
  config;
@@ -18,23 +45,30 @@ export class EngineRunner {
18
45
  this.config = config;
19
46
  }
20
47
  async execute(run, context, lifecycleHooks, onHandleReady) {
21
- // Create run-aware approval backend
22
- const approvalBackend = new RunApprovalBackend();
48
+ // Run-aware approval backend (interactive: suspends the run and waits for
49
+ // RunManager to resolve via the handle). Always constructed so the handle
50
+ // stays well-formed, but only wired into the engine when no override is set.
51
+ const runApprovalBackend = new RunApprovalBackend();
52
+ // Unattended override: a host (automation/cron) can inject a backend that
53
+ // auto-decides (e.g. approve-read-only). When set, the engine uses it and
54
+ // the interactive resolve path is inert (no UI to resolve approvals).
55
+ const override = this.config.approvalBackend;
56
+ const engineApprovalBackend = override ?? runApprovalBackend;
23
57
  // Create run-aware askUser adapter
24
58
  let resolveInputFn = () => false;
25
59
  let hasPendingInputFn = () => false;
26
60
  let askUserFn;
27
- if (lifecycleHooks) {
28
- approvalBackend.setHooks(lifecycleHooks);
61
+ if (lifecycleHooks && !override) {
62
+ runApprovalBackend.setHooks(lifecycleHooks);
29
63
  const askAdapter = createRunAskUserFn(lifecycleHooks);
30
64
  askUserFn = askAdapter.askUserFn;
31
65
  resolveInputFn = askAdapter.resolveInput;
32
66
  hasPendingInputFn = askAdapter.hasPendingInput;
33
67
  }
34
68
  const handle = {
35
- resolveApproval: (approved, reason) => approvalBackend.resolveApproval(approved ? { approved: true } : { approved: false, reason }),
69
+ resolveApproval: (approved, reason) => runApprovalBackend.resolveApproval(approved ? { approved: true } : { approved: false, reason }),
36
70
  resolveInput: resolveInputFn,
37
- hasPendingApproval: () => approvalBackend.hasPendingApproval(),
71
+ hasPendingApproval: () => runApprovalBackend.hasPendingApproval(),
38
72
  hasPendingInput: hasPendingInputFn,
39
73
  };
40
74
  // Expose handle immediately so RunManager can resolve approvals/input
@@ -42,19 +76,23 @@ export class EngineRunner {
42
76
  onHandleReady?.(handle);
43
77
  const engineConfig = {
44
78
  llm: this.config.llm,
79
+ clientDefaults: this.config.clientDefaults,
45
80
  cwd: run.cwd,
46
81
  maxTurns: this.config.maxTurns ?? 30,
47
82
  maxContextTokens: this.config.maxContextTokens ?? 200_000,
48
83
  permissionMode: this.config.permissionMode ?? "acceptEdits",
84
+ headless: buildHeadlessFlag(override),
49
85
  preset: run.preset,
50
86
  enabledBuiltinTools: this.config.enabledBuiltinTools,
51
87
  disabledBuiltinTools: this.config.disabledBuiltinTools,
52
88
  customSystemPrompt: this.config.customSystemPrompt,
53
- appendSystemPrompt: this.config.appendSystemPrompt,
89
+ // NOTE: callers must not pass appendSystemPrompt in engineConfigOverrides
90
+ // below, or it will clobber this composed (automation note + host) value.
91
+ appendSystemPrompt: buildAppendSystemPrompt(this.config.appendSystemPrompt, run.metadata),
54
92
  sessionStorageDir: this.config.sessionStorageDir,
55
93
  mcpServers: this.config.mcpServers,
56
94
  hooks: this.config.hooks,
57
- approvalBackend,
95
+ approvalBackend: engineApprovalBackend,
58
96
  askUser: askUserFn,
59
97
  ...context.engineConfigOverrides,
60
98
  };
@@ -32,9 +32,16 @@ export class FileRunStore {
32
32
  }
33
33
  writeJson(filePath, data) {
34
34
  const tmp = filePath + ".tmp";
35
- writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
36
- // Atomic rename prevents partial writes on crash
37
- renameSync(tmp, filePath);
35
+ try {
36
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
37
+ // Atomic rename — prevents partial writes on crash
38
+ renameSync(tmp, filePath);
39
+ }
40
+ catch (err) {
41
+ // Don't leave a dangling .tmp behind on a failed write/rename.
42
+ rmSync(tmp, { force: true });
43
+ throw err;
44
+ }
38
45
  }
39
46
  readJson(filePath) {
40
47
  if (!existsSync(filePath))
@@ -44,13 +51,30 @@ export class FileRunStore {
44
51
  /** Serializes concurrent JSONL appends per file path. */
45
52
  appendLocks = new Map();
46
53
  async appendJsonl(filePath, data) {
47
- // Serialize writes to the same file to prevent interleaved output
54
+ // Serialize writes to the same file to prevent interleaved output. The
55
+ // chain we STORE as the lock must never reject — otherwise a single failed
56
+ // write poisons the lock and every later write chains off the rejected
57
+ // promise, skips its callback, and fails forever. So sequence off the
58
+ // previous lock's settlement (.catch(()=>{})), do the write, and surface
59
+ // this write's own error to *this* caller via a separate promise.
48
60
  const prev = this.appendLocks.get(filePath) ?? Promise.resolve();
49
- const current = prev.then(() => {
50
- appendFileSync(filePath, JSON.stringify(data) + "\n", "utf-8");
61
+ let settle;
62
+ const lock = new Promise((resolve) => {
63
+ settle = resolve;
51
64
  });
52
- this.appendLocks.set(filePath, current);
53
- await current;
65
+ this.appendLocks.set(filePath, lock);
66
+ try {
67
+ await prev.catch(() => { });
68
+ appendFileSync(filePath, JSON.stringify(data) + "\n", "utf-8");
69
+ }
70
+ finally {
71
+ // Release the lock for the next writer regardless of success/failure,
72
+ // and drop the map entry if no newer writer has queued behind us.
73
+ settle();
74
+ if (this.appendLocks.get(filePath) === lock) {
75
+ this.appendLocks.delete(filePath);
76
+ }
77
+ }
54
78
  }
55
79
  readJsonl(filePath) {
56
80
  if (!existsSync(filePath))
@@ -10,16 +10,42 @@ export interface RunLockConfig {
10
10
  runsDir?: string;
11
11
  /** Stale lock timeout in ms. Default: 60_000 (1 minute) */
12
12
  staleMs?: number;
13
+ /** How long to wait for runs/<id>/run.json to appear before failing. */
14
+ targetWaitMs?: number;
15
+ /** Poll interval while waiting for the lock target file. */
16
+ targetPollMs?: number;
13
17
  }
18
+ export type RunLockAcquireResult = {
19
+ acquired: true;
20
+ } | {
21
+ acquired: false;
22
+ reason: "missing_target" | "locked";
23
+ message?: string;
24
+ code?: string;
25
+ };
14
26
  export declare class RunLock {
15
27
  private readonly runsDir;
16
28
  private readonly staleMs;
29
+ private readonly targetWaitMs;
30
+ private readonly targetPollMs;
17
31
  private readonly releaseFns;
18
32
  constructor(config?: RunLockConfig);
19
33
  /**
20
- * Acquire a lock for a run. Returns true if acquired, false if already held.
34
+ * Acquire a lock for a run. Missing target and held-lock failures are
35
+ * intentionally distinct so callers don't silently abandon a run as "locked"
36
+ * when the real issue is a bad/mismatched run store path.
37
+ *
38
+ * Race note: if the queue reaches acquire() before the file is visible, a
39
+ * short wait bridges that gap. Only a proper-lockfile conflict is "locked".
21
40
  */
22
- acquire(runId: string): Promise<boolean>;
41
+ acquire(runId: string): Promise<RunLockAcquireResult>;
42
+ /**
43
+ * Wait (up to ~targetWaitMs) for the lock target file to exist, polling on a
44
+ * short interval. Returns true once present, false if it never appears
45
+ * within the window. Bridges the submit→executeRun create race without
46
+ * blocking meaningfully when the file is already there (first check hits).
47
+ */
48
+ private waitForTarget;
23
49
  /**
24
50
  * Release the lock for a run.
25
51
  */
@@ -13,18 +13,33 @@ import { logger } from "../logging/logger.js";
13
13
  export class RunLock {
14
14
  runsDir;
15
15
  staleMs;
16
+ targetWaitMs;
17
+ targetPollMs;
16
18
  releaseFns = new Map();
17
19
  constructor(config) {
18
20
  this.runsDir = config?.runsDir ?? join(homedir(), ".code-shell", "runs");
19
21
  this.staleMs = config?.staleMs ?? 60_000;
22
+ this.targetWaitMs = config?.targetWaitMs ?? 2_000;
23
+ this.targetPollMs = config?.targetPollMs ?? 10;
20
24
  }
21
25
  /**
22
- * Acquire a lock for a run. Returns true if acquired, false if already held.
26
+ * Acquire a lock for a run. Missing target and held-lock failures are
27
+ * intentionally distinct so callers don't silently abandon a run as "locked"
28
+ * when the real issue is a bad/mismatched run store path.
29
+ *
30
+ * Race note: if the queue reaches acquire() before the file is visible, a
31
+ * short wait bridges that gap. Only a proper-lockfile conflict is "locked".
23
32
  */
24
33
  async acquire(runId) {
25
34
  const lockTarget = this.lockTarget(runId);
26
- if (!existsSync(lockTarget))
27
- return false;
35
+ if (!(await this.waitForTarget(lockTarget))) {
36
+ logger.warn("run.lock.target_missing", { runId, lockTarget });
37
+ return {
38
+ acquired: false,
39
+ reason: "missing_target",
40
+ message: `Lock target did not appear: ${lockTarget}`,
41
+ };
42
+ }
28
43
  try {
29
44
  const release = await lock(lockTarget, {
30
45
  stale: this.staleMs,
@@ -32,12 +47,39 @@ export class RunLock {
32
47
  });
33
48
  this.releaseFns.set(runId, release);
34
49
  logger.info("run.lock.acquired", { runId });
35
- return true;
50
+ return { acquired: true };
36
51
  }
37
- catch {
38
- // Lock already held by another process
39
- return false;
52
+ catch (err) {
53
+ const e = err;
54
+ logger.warn("run.lock.conflict", {
55
+ runId,
56
+ lockTarget,
57
+ code: e?.code,
58
+ message: e?.message,
59
+ alreadyHeldByThisInstance: this.releaseFns.has(runId),
60
+ });
61
+ return {
62
+ acquired: false,
63
+ reason: "locked",
64
+ code: e?.code,
65
+ message: e?.message,
66
+ };
67
+ }
68
+ }
69
+ /**
70
+ * Wait (up to ~targetWaitMs) for the lock target file to exist, polling on a
71
+ * short interval. Returns true once present, false if it never appears
72
+ * within the window. Bridges the submit→executeRun create race without
73
+ * blocking meaningfully when the file is already there (first check hits).
74
+ */
75
+ async waitForTarget(lockTarget) {
76
+ const deadline = Date.now() + this.targetWaitMs;
77
+ while (!existsSync(lockTarget)) {
78
+ if (Date.now() >= deadline)
79
+ return false;
80
+ await new Promise((r) => setTimeout(r, this.targetPollMs));
40
81
  }
82
+ return true;
41
83
  }
42
84
  /**
43
85
  * Release the lock for a run.
@@ -293,9 +293,25 @@ export class RunManager {
293
293
  async executeRun(runId) {
294
294
  const run = await this.getOrThrow(runId);
295
295
  // Acquire run lock — prevents concurrent execution by multiple workers
296
- const acquired = await this.lock.acquire(runId);
297
- if (!acquired) {
298
- logger.warn("run.lock.failed", { runId, reason: "already locked by another worker" });
296
+ const lockResult = await this.lock.acquire(runId);
297
+ if (!lockResult.acquired) {
298
+ if (lockResult.reason === "missing_target") {
299
+ const errorMsg = lockResult.message ?? "Run lock target was missing";
300
+ run.error = errorMsg;
301
+ await this.transition(run, "blocked");
302
+ await this.emitRunEvent(runId, "run_blocked", {
303
+ error: errorMsg,
304
+ reason: "missing_lock_target",
305
+ });
306
+ logger.error("run.lock.target_missing", { runId, error: errorMsg });
307
+ return;
308
+ }
309
+ logger.warn("run.lock.failed", {
310
+ runId,
311
+ reason: "already locked by another worker",
312
+ code: lockResult.code,
313
+ message: lockResult.message,
314
+ });
299
315
  return;
300
316
  }
301
317
  // Start heartbeat so crash recovery can detect liveness
@@ -331,6 +347,38 @@ export class RunManager {
331
347
  // Feed events to checkpoint writer and artifact tracker
332
348
  await checkpointWriter.onStreamEvent(event);
333
349
  await artifactTracker.onStreamEvent(event);
350
+ // Link the run's sessionId the moment the engine resolves it.
351
+ // session_started fires at run START (engine.ts:1081); without this,
352
+ // sessionId is only written at completion, so in-flight runs have
353
+ // sessionId=null on disk and never reach the sidebar import.
354
+ //
355
+ // The `!agentId` guard is defensive: the built-in engine already
356
+ // suppresses sub-agent session_started upstream (engine.ts:830-835),
357
+ // but a custom RunExecutor could forward sub-agent events — and only
358
+ // the MAIN run's session should identify the run, never a sub-agent's.
359
+ if (event.type === "session_started" &&
360
+ !event.agentId) {
361
+ try {
362
+ const run = await this.getOrThrow(runId);
363
+ if (run.sessionId !== event.sessionId) {
364
+ run.sessionId = event.sessionId;
365
+ checkpointWriter.setSessionId(event.sessionId);
366
+ await this.store.update(run);
367
+ await this.emitRunEvent(runId, "session_linked", {
368
+ sessionId: event.sessionId,
369
+ });
370
+ }
371
+ }
372
+ catch (linkErr) {
373
+ // Don't let a snapshot-write failure abort the run as a swallowed
374
+ // unhandled rejection — log and continue; the completion-time link
375
+ // is the backstop.
376
+ logger.warn("run.session_link_error", {
377
+ runId,
378
+ error: linkErr instanceof Error ? linkErr.message : String(linkErr),
379
+ });
380
+ }
381
+ }
334
382
  // Forward to run subscribers
335
383
  this.notifySubscribers(runId, { type: "engine_stream", event });
336
384
  },