@cjhyy/code-shell-core 0.5.0-rc.1 → 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.
- package/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
package/dist/protocol/types.d.ts
CHANGED
|
@@ -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 {
|
package/dist/protocol/types.js
CHANGED
|
@@ -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 = {
|
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/remote/bridge.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
36
|
+
const sshArgs = buildSSHArgs(this.config);
|
|
21
37
|
const remoteCmd = this.config.remoteCommand ?? "code-shell run --output stream-json";
|
|
22
|
-
this.ssh =
|
|
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 (
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
116
|
-
if (
|
|
117
|
-
await this.recordFileArtifact(
|
|
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;
|
package/dist/run/EngineRunner.js
CHANGED
|
@@ -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
|
-
//
|
|
22
|
-
|
|
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
|
-
|
|
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) =>
|
|
69
|
+
resolveApproval: (approved, reason) => runApprovalBackend.resolveApproval(approved ? { approved: true } : { approved: false, reason }),
|
|
36
70
|
resolveInput: resolveInputFn,
|
|
37
|
-
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
|
-
|
|
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
|
};
|
package/dist/run/FileRunStore.js
CHANGED
|
@@ -32,9 +32,16 @@ export class FileRunStore {
|
|
|
32
32
|
}
|
|
33
33
|
writeJson(filePath, data) {
|
|
34
34
|
const tmp = filePath + ".tmp";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
50
|
-
|
|
61
|
+
let settle;
|
|
62
|
+
const lock = new Promise((resolve) => {
|
|
63
|
+
settle = resolve;
|
|
51
64
|
});
|
|
52
|
-
this.appendLocks.set(filePath,
|
|
53
|
-
|
|
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))
|
package/dist/run/RunLock.d.ts
CHANGED
|
@@ -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.
|
|
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<
|
|
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
|
*/
|
package/dist/run/RunLock.js
CHANGED
|
@@ -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.
|
|
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 (!
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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.
|
package/dist/run/RunManager.js
CHANGED
|
@@ -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
|
|
297
|
-
if (!acquired) {
|
|
298
|
-
|
|
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
|
},
|