@nowcrew/daemon 0.6.88-beta.2 → 0.6.88-beta.20
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/README.md +38 -10
- package/dist/agent-memory/policy.js +3 -1
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +5 -3
- package/dist/list-models.js +2 -0
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -37
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +154 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +14 -11
- package/dist/local-memory/read-cli.js +59 -8
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/machine-info.js +54 -16
- package/dist/main.js +4 -69
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +3 -4
- package/dist/runner.js +14 -15
- package/dist/runtime-probe.js +34 -5
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +224 -0
- package/dist/runtimes/deepseek-harness.js +64 -11
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/kimi-acp-runner.js +63 -10
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/serve.js +58 -88
- package/dist/session.js +4 -0
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/package.json +2 -2
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/dist/provider-env.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
13
|
-
import { DEEPSEEK_HARNESS_CONFIG_ENV } from "./runtimes/deepseek-harness.js";
|
|
14
13
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
14
|
const RESERVED_ENV = new Set([
|
|
16
15
|
"PATH",
|
|
@@ -96,9 +95,9 @@ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverri
|
|
|
96
95
|
}
|
|
97
96
|
return env;
|
|
98
97
|
}
|
|
99
|
-
const deepSeekHarnessMachineOwnedKey = (key) => (key.startsWith("DEEPSEEK_")
|
|
100
|
-
|| key
|
|
101
|
-
|| key.startsWith("DSH_"));
|
|
98
|
+
const deepSeekHarnessMachineOwnedKey = (key) => (key.toUpperCase().startsWith("DEEPSEEK_")
|
|
99
|
+
|| key.toUpperCase().startsWith("NOWCREW_DEEPSEEK_HARNESS_")
|
|
100
|
+
|| key.toUpperCase().startsWith("DSH_"));
|
|
102
101
|
/** Agent env remains available to tools, but cannot replace Harness process policy or credentials. */
|
|
103
102
|
export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
|
|
104
103
|
const env = { ...base };
|
package/dist/runner.js
CHANGED
|
@@ -11,6 +11,7 @@ import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decisi
|
|
|
11
11
|
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
12
12
|
import { launchSupervisedRuntime } from "./supervised-runtime.js";
|
|
13
13
|
import { awaitWithCancellation, } from "./runtime-cancellation.js";
|
|
14
|
+
import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
14
15
|
export { awaitExit, exitActivity } from "./local-executor.js";
|
|
15
16
|
export { sanitizeEnvVars } from "./runtime-env.js";
|
|
16
17
|
const ICON = {
|
|
@@ -23,8 +24,13 @@ function runtimeName(value) {
|
|
|
23
24
|
throw new Error(`unsupported runtime: ${value}`);
|
|
24
25
|
}
|
|
25
26
|
export async function runAgent(config, input, onActivity = defaultPrint, onConsole = () => { }, dependencies = {}) {
|
|
27
|
+
const threadId = input.scheduled ? undefined : input.threadId;
|
|
28
|
+
const taskAnchor = threadId ?? input.wakeMessageId;
|
|
29
|
+
// Old servers have no typed maintenance purpose. A legacy marker can only
|
|
30
|
+
// reduce native-session reuse; it never grants maintenance permissions.
|
|
31
|
+
const suppressNativeSession = parseMemoryPruneTraceId(input.wake ?? "") !== null;
|
|
26
32
|
const credential = await awaitWithCancellation((dependencies.mintAgentToken ?? mintAgentToken)(config.serverUrl, config.machineToken, input.handle, input.displayName, {
|
|
27
|
-
...(
|
|
33
|
+
...(threadId ? { wakeThreadRoot: threadId } : {}),
|
|
28
34
|
...(input.wakeContextUpToSeq === undefined ? {} : { wakeContextUpToSeq: input.wakeContextUpToSeq }),
|
|
29
35
|
...(input.runId ? { agentRunId: input.runId } : {}),
|
|
30
36
|
}), dependencies.cancellation);
|
|
@@ -45,11 +51,13 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
45
51
|
handle: input.handle,
|
|
46
52
|
channelId: input.channelId,
|
|
47
53
|
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
48
|
-
...(
|
|
54
|
+
...(threadId === undefined ? {} : { threadId,
|
|
55
|
+
...(!suppressNativeSession ? { resumeKey: threadId } : {}) }),
|
|
56
|
+
...(taskAnchor === undefined ? {} : { wakeMessageId: taskAnchor }),
|
|
49
57
|
systemPrompt: ({ workspace, resuming }) => buildSystemPrompt({
|
|
50
58
|
handle: input.handle,
|
|
51
59
|
channelId: input.channelId,
|
|
52
|
-
...(
|
|
60
|
+
...(threadId ? { wakeMessageId: threadId } : {}),
|
|
53
61
|
agentId: credential.agentId,
|
|
54
62
|
homeDir: workspace.dir,
|
|
55
63
|
productName: config.productName,
|
|
@@ -64,17 +72,8 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
64
72
|
collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
|
|
65
73
|
localMemoryV2Enabled,
|
|
66
74
|
}),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? "\n(注意:本会话上下文已接近预算,稍后将轮换重启。本轮结束前,把当前状态——阶段/关键结论/下一步/卡点/关键文件路径——完整更新到 $CREW_TASK_LOG,下轮将以它为基础继续。)"
|
|
70
|
-
: "";
|
|
71
|
-
if (resuming) {
|
|
72
|
-
return `(继续之前的会话:频道历史与你的进度已在上下文里,不必从头重读;要新消息用 \`crew message read --channel ${input.channelId}\` 增量拉即可。)${distillHint}\n\n${baseWake}`;
|
|
73
|
-
}
|
|
74
|
-
return rotatedForBudget
|
|
75
|
-
? `(上下文已轮换:之前的会话过大已重启。你的 work-log 已注入系统提示词末尾,以它为基础继续;频道/线程历史用 crew 命令按需拉取,不必全量重读。)\n\n${baseWake}`
|
|
76
|
-
: baseWake;
|
|
77
|
-
},
|
|
75
|
+
// Native adapters, not a launch-intent prompt, choose history versus delta.
|
|
76
|
+
wakePrompt: () => baseWake,
|
|
78
77
|
runtime: {
|
|
79
78
|
name: runtime,
|
|
80
79
|
...(providerConfig.model ? { model: providerConfig.model } : {}),
|
|
@@ -107,7 +106,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
107
106
|
},
|
|
108
107
|
},
|
|
109
108
|
session: {
|
|
110
|
-
enabled: config.resume,
|
|
109
|
+
enabled: config.resume && !suppressNativeSession,
|
|
111
110
|
warmMs: config.resumeWarmMs,
|
|
112
111
|
budgetTokens: config.sessionBudgetTokens,
|
|
113
112
|
softTokens: config.sessionSoftTokens,
|
package/dist/runtime-probe.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { dslog } from "./slog.js";
|
|
1
3
|
import { executableRuntimes } from "./runtime-capabilities.js";
|
|
2
4
|
export function conservativeExecutionRuntimes(installed) {
|
|
3
5
|
return executableRuntimes(installed)
|
|
@@ -7,16 +9,43 @@ export function createRuntimeProbeCoordinator() {
|
|
|
7
9
|
const abort = new AbortController();
|
|
8
10
|
const flights = new Map();
|
|
9
11
|
return {
|
|
10
|
-
detect(installed, detector) {
|
|
12
|
+
detect(installed, detector, connectionId) {
|
|
11
13
|
const key = [...installed].sort().join("\0");
|
|
12
14
|
const existing = flights.get(key);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const probeId = existing?.probeId ?? randomUUID();
|
|
16
|
+
const fields = { probe_id: probeId, connection_id: connectionId, installed_runtimes: JSON.stringify(installed) };
|
|
17
|
+
dslog("runtime_probe.request", "请求 runtime 执行能力探测", fields);
|
|
18
|
+
if (existing) {
|
|
19
|
+
dslog("runtime_probe.cache_hit", "复用 runtime 探测结果", { ...fields, cache_state: existing.state });
|
|
20
|
+
return existing.promise;
|
|
21
|
+
}
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
const complete = (outcome, executionRuntimes) => {
|
|
24
|
+
dslog("runtime_probe.complete", "runtime 探测聚合结束", {
|
|
25
|
+
...fields, outcome, duration_ms: Date.now() - startedAt, aborted: abort.signal.aborted,
|
|
26
|
+
...(executionRuntimes === undefined ? {} : { execution_runtimes: JSON.stringify(executionRuntimes) }),
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
let detection;
|
|
30
|
+
try {
|
|
31
|
+
detection = detector(installed, abort.signal, probeId);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
complete("threw");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const pending = detection.then((result) => {
|
|
38
|
+
const flight = flights.get(key);
|
|
39
|
+
if (flight)
|
|
40
|
+
flights.set(key, { ...flight, state: "resolved" });
|
|
41
|
+
complete("resolved", result);
|
|
42
|
+
return result;
|
|
43
|
+
}, (error) => {
|
|
16
44
|
flights.delete(key);
|
|
45
|
+
complete("threw");
|
|
17
46
|
throw error;
|
|
18
47
|
});
|
|
19
|
-
flights.set(key, pending);
|
|
48
|
+
flights.set(key, { promise: pending, probeId, state: "pending" });
|
|
20
49
|
return pending;
|
|
21
50
|
},
|
|
22
51
|
stop() {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Readable as NodeReadable } from "node:stream";
|
|
2
|
+
import { ReadableStream, WritableStream } from "node:stream/web";
|
|
3
|
+
/** Probe-only counters. Never retain/log chunks, JSON-RPC params, stderr or env values. */
|
|
4
|
+
export class AcpProbeTransport {
|
|
5
|
+
elapsed;
|
|
6
|
+
initializeRequestedMs;
|
|
7
|
+
initializeResponseMs;
|
|
8
|
+
firstStdinWriteMs;
|
|
9
|
+
firstStdoutMs;
|
|
10
|
+
stdinBytesWritten = 0;
|
|
11
|
+
stdoutBytes = 0;
|
|
12
|
+
stdoutEnded = false;
|
|
13
|
+
constructor(elapsed) {
|
|
14
|
+
this.elapsed = elapsed;
|
|
15
|
+
}
|
|
16
|
+
initializeRequested() { this.initializeRequestedMs = this.elapsed(); }
|
|
17
|
+
initializeResponse() { this.initializeResponseMs = this.elapsed(); }
|
|
18
|
+
fields() {
|
|
19
|
+
return {
|
|
20
|
+
initialize_requested_ms: this.initializeRequestedMs,
|
|
21
|
+
initialize_response_ms: this.initializeResponseMs,
|
|
22
|
+
first_stdin_write_ms: this.firstStdinWriteMs,
|
|
23
|
+
first_stdout_ms: this.firstStdoutMs,
|
|
24
|
+
stdin_bytes_written: this.stdinBytesWritten,
|
|
25
|
+
stdout_bytes: this.stdoutBytes,
|
|
26
|
+
stdout_ended: this.stdoutEnded,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A write callback confirms acceptance by the pipe, NOT receipt by the agent. */
|
|
30
|
+
writable(stdin) {
|
|
31
|
+
return new WritableStream({
|
|
32
|
+
start: (controller) => { stdin.once("error", (error) => controller.error(error)); },
|
|
33
|
+
write: (chunk) => new Promise((resolve, reject) => {
|
|
34
|
+
stdin.write(chunk, (error) => {
|
|
35
|
+
if (error) {
|
|
36
|
+
reject(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.firstStdinWriteMs ??= this.elapsed();
|
|
40
|
+
this.stdinBytesWritten += chunk.byteLength;
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
}),
|
|
44
|
+
close: () => new Promise((resolve, reject) => {
|
|
45
|
+
stdin.end((error) => error ? reject(error) : resolve());
|
|
46
|
+
}),
|
|
47
|
+
abort: () => { stdin.destroy(); },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Observe before NDJSON decoding, without adding a competing 'data' listener. */
|
|
51
|
+
readable(stdout) {
|
|
52
|
+
const reader = NodeReadable.toWeb(stdout).getReader();
|
|
53
|
+
return new ReadableStream({
|
|
54
|
+
pull: async (controller) => {
|
|
55
|
+
const item = await reader.read();
|
|
56
|
+
if (item.done) {
|
|
57
|
+
this.stdoutEnded = true;
|
|
58
|
+
controller.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const chunk = item.value;
|
|
62
|
+
this.firstStdoutMs ??= this.elapsed();
|
|
63
|
+
this.stdoutBytes += chunk.byteLength;
|
|
64
|
+
controller.enqueue(chunk);
|
|
65
|
+
},
|
|
66
|
+
cancel: (reason) => reader.cancel(reason),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
/** Called by Claude itself before its first user turn. Never reads a transcript. */
|
|
5
|
+
export async function writeClaudeSessionProof(path, nonce, raw) {
|
|
6
|
+
if (!isAbsolute(path) || !/^[a-f0-9-]{36}$/u.test(nonce))
|
|
7
|
+
throw new Error("invalid_claude_proof_destination");
|
|
8
|
+
const input = JSON.parse(raw);
|
|
9
|
+
if (input.hook_event_name !== "SessionStart" || !["startup", "resume"].includes(String(input.source))
|
|
10
|
+
|| typeof input.session_id !== "string" || !input.session_id)
|
|
11
|
+
throw new Error("invalid_claude_session_start");
|
|
12
|
+
await writeFile(path, JSON.stringify({ nonce, source: input.source, sessionId: input.session_id }), { flag: "wx", mode: 0o600 });
|
|
13
|
+
}
|
|
14
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
for await (const chunk of process.stdin) {
|
|
18
|
+
bytes += Buffer.byteLength(chunk);
|
|
19
|
+
if (bytes > 64 * 1024)
|
|
20
|
+
throw new Error("claude_session_start_too_large");
|
|
21
|
+
chunks.push(Buffer.from(chunk));
|
|
22
|
+
}
|
|
23
|
+
await writeClaudeSessionProof(process.argv[2] ?? "", process.argv[3] ?? "", Buffer.concat(chunks).toString("utf8"));
|
|
24
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import spawn from "cross-spawn";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
|
|
11
|
+
const InputSchema = z.object({ bin: z.string().min(1), args: z.array(z.string()),
|
|
12
|
+
wakePrompt: z.string().min(1), sessionId: z.string().min(1).optional(), resume: z.boolean() }).strict();
|
|
13
|
+
class PreflightError extends Error {
|
|
14
|
+
}
|
|
15
|
+
function hookCommand(path, nonce) {
|
|
16
|
+
const source = import.meta.url.endsWith(".ts");
|
|
17
|
+
const hook = fileURLToPath(new URL(`./claude-session-start-hook.${source ? "ts" : "js"}`, import.meta.url));
|
|
18
|
+
const args = [process.execPath, ...(source
|
|
19
|
+
? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href] : []), hook, path, nonce];
|
|
20
|
+
const quote = process.platform === "win32"
|
|
21
|
+
? (value) => `"${value.replaceAll("%", "%%").replaceAll('"', '\\"')}"`
|
|
22
|
+
: (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
23
|
+
return args.map(quote).join(" ");
|
|
24
|
+
}
|
|
25
|
+
function sessionArgs(args, sessionId, resume) {
|
|
26
|
+
const result = [];
|
|
27
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
+
if (args[index] === "--resume" || args[index] === "--session-id") {
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
result.push(args[index]);
|
|
33
|
+
}
|
|
34
|
+
return [...result, resume ? "--resume" : "--session-id", sessionId];
|
|
35
|
+
}
|
|
36
|
+
function deferred() {
|
|
37
|
+
let resolve;
|
|
38
|
+
let reject;
|
|
39
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
40
|
+
// A process can fail before its caller starts awaiting the next phase.
|
|
41
|
+
void promise.catch(() => undefined);
|
|
42
|
+
return { promise, resolve, reject };
|
|
43
|
+
}
|
|
44
|
+
async function bounded(promise, milliseconds, label) {
|
|
45
|
+
let timer;
|
|
46
|
+
try {
|
|
47
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
48
|
+
timer = setTimeout(() => reject(new Error(label)), milliseconds);
|
|
49
|
+
})]);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function attempt(input, sessionId, resume, options) {
|
|
56
|
+
const proofDir = await mkdtemp(join(tmpdir(), "nowwork-claude-session-"));
|
|
57
|
+
const proofPath = join(proofDir, "proof.json");
|
|
58
|
+
const nonce = randomUUID();
|
|
59
|
+
const settings = { hooks: { SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command",
|
|
60
|
+
command: hookCommand(proofPath, nonce), timeout: 15 }] }] } };
|
|
61
|
+
const env = options.env ?? process.env;
|
|
62
|
+
const output = options.output ?? (event => { process.stdout.write(`${JSON.stringify(event)}\n`); });
|
|
63
|
+
const child = spawn(input.bin, [...sessionArgs(input.args, sessionId, resume),
|
|
64
|
+
"--input-format", "stream-json", "--replay-user-messages", "--settings", JSON.stringify(settings)],
|
|
65
|
+
// The existing execution supervisor owns the process group / Windows Job.
|
|
66
|
+
// Never create a detached native group: its forced cancellation may kill this
|
|
67
|
+
// wrapper before our local TERM grace expires, so all descendants must remain
|
|
68
|
+
// reachable by the supervisor's final tree-wide kill.
|
|
69
|
+
{ cwd: process.cwd(), env, stdio: ["pipe", "pipe", "pipe"], detached: false });
|
|
70
|
+
const initialized = deferred();
|
|
71
|
+
const accepted = deferred();
|
|
72
|
+
const closed = deferred();
|
|
73
|
+
const initId = randomUUID();
|
|
74
|
+
const userId = randomUUID();
|
|
75
|
+
let submitted = false;
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
let sawResult = false;
|
|
78
|
+
let exitObserved = false;
|
|
79
|
+
let acknowledged = false;
|
|
80
|
+
let delivery;
|
|
81
|
+
const pendingAgents = new Set();
|
|
82
|
+
let processing = Promise.resolve();
|
|
83
|
+
let failure;
|
|
84
|
+
const fail = (error) => { failure ??= error; initialized.reject(error); accepted.reject(error); };
|
|
85
|
+
const send = (event) => new Promise((resolve, reject) => {
|
|
86
|
+
if (!child.stdin || child.stdin.destroyed) {
|
|
87
|
+
reject(new Error("claude_input_closed"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
child.stdin.write(`${JSON.stringify(event)}\n`, error => error ? reject(error) : resolve());
|
|
91
|
+
});
|
|
92
|
+
const nativeAlive = () => child.pid !== undefined && child.exitCode === null && child.signalCode === null;
|
|
93
|
+
const waitStopped = async () => {
|
|
94
|
+
const deadline = Date.now() + 1_000;
|
|
95
|
+
while (nativeAlive() && Date.now() < deadline)
|
|
96
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
97
|
+
return !nativeAlive();
|
|
98
|
+
};
|
|
99
|
+
let stopPromise;
|
|
100
|
+
const stop = () => stopPromise ??= (async () => {
|
|
101
|
+
if (!nativeAlive())
|
|
102
|
+
return;
|
|
103
|
+
// This is only local native-process cleanup (including preflight recovery),
|
|
104
|
+
// not proof that its descendants exited. Do not signal the inherited group:
|
|
105
|
+
// that would also kill this wrapper and prevent safe cold recovery. The
|
|
106
|
+
// supervisor retains ownership and verifies whole-tree exit on completion.
|
|
107
|
+
child.kill("SIGTERM");
|
|
108
|
+
if (await waitStopped())
|
|
109
|
+
return;
|
|
110
|
+
child.kill("SIGKILL");
|
|
111
|
+
if (!await waitStopped())
|
|
112
|
+
throw new Error("claude_stop_failed");
|
|
113
|
+
})();
|
|
114
|
+
const onSignal = () => { cancelled = true; fail(new Error("claude_thread_cancelled")); void stop().catch(fail); };
|
|
115
|
+
process.once("SIGTERM", onSignal);
|
|
116
|
+
process.once("SIGINT", onSignal);
|
|
117
|
+
child.stdin?.on("error", error => fail(error));
|
|
118
|
+
child.once("error", error => { fail(error); closed.resolve(1); exitObserved = true; });
|
|
119
|
+
child.once("close", code => {
|
|
120
|
+
exitObserved = true;
|
|
121
|
+
closed.resolve(code ?? 1);
|
|
122
|
+
if (!submitted)
|
|
123
|
+
fail(new Error("claude_exited_before_session_ready"));
|
|
124
|
+
});
|
|
125
|
+
// Diagnostics may contain provider secrets or prompts. The parent receives a
|
|
126
|
+
// stable phase error, never raw third-party stderr or preflight result text.
|
|
127
|
+
child.stderr?.resume();
|
|
128
|
+
const lines = child.stdout ? createInterface({ input: child.stdout }) : null;
|
|
129
|
+
lines?.on("line", line => {
|
|
130
|
+
processing = processing.then(async () => {
|
|
131
|
+
if (failure)
|
|
132
|
+
return;
|
|
133
|
+
let event;
|
|
134
|
+
try {
|
|
135
|
+
event = JSON.parse(line);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error("claude_invalid_stream_json");
|
|
139
|
+
}
|
|
140
|
+
if (event.type === "control_response") {
|
|
141
|
+
const response = event.response;
|
|
142
|
+
if (response?.request_id === initId) {
|
|
143
|
+
if (response.subtype !== "success")
|
|
144
|
+
throw new Error("claude_initialize_rejected");
|
|
145
|
+
initialized.resolve();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (event.type === "control_request") {
|
|
150
|
+
const request = event.request;
|
|
151
|
+
await send({ type: "control_response", response: request?.subtype === "can_use_tool"
|
|
152
|
+
? { subtype: "success", request_id: event.request_id,
|
|
153
|
+
response: { behavior: "deny", message: "Interactive approval is unavailable in this execution." } }
|
|
154
|
+
: { subtype: "error", request_id: event.request_id, error: "Unsupported Claude control request" } });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!submitted) {
|
|
158
|
+
if (event.type === "result")
|
|
159
|
+
throw new Error("claude_session_preflight_failed");
|
|
160
|
+
return; // Startup hooks and old replay events are not business output.
|
|
161
|
+
}
|
|
162
|
+
if (event.type === "system" && event.subtype === "init" && event.session_id !== sessionId) {
|
|
163
|
+
throw new Error("claude_actual_session_mismatch");
|
|
164
|
+
}
|
|
165
|
+
if (!acknowledged && event.type === "user" && event.uuid === userId && event.session_id === sessionId) {
|
|
166
|
+
await delivery.commit();
|
|
167
|
+
acknowledged = true;
|
|
168
|
+
accepted.resolve();
|
|
169
|
+
}
|
|
170
|
+
if (event.type === "system" && typeof event.task_id === "string") {
|
|
171
|
+
if (event.subtype === "task_started" && ["local_agent", "local_workflow"].includes(String(event.task_type))) {
|
|
172
|
+
pendingAgents.add(event.task_id);
|
|
173
|
+
sawResult = false;
|
|
174
|
+
}
|
|
175
|
+
else if (event.subtype === "task_notification")
|
|
176
|
+
pendingAgents.delete(event.task_id);
|
|
177
|
+
else if (event.subtype === "task_updated" && event.patch && typeof event.patch === "object"
|
|
178
|
+
&& ["completed", "failed", "killed", "stopped"].includes(String(event.patch.status)))
|
|
179
|
+
pendingAgents.delete(event.task_id);
|
|
180
|
+
}
|
|
181
|
+
// Like the official SDK, leave control input open while delegated agent
|
|
182
|
+
// work is outstanding; background shells must not hold it open forever.
|
|
183
|
+
if (event.type === "result")
|
|
184
|
+
sawResult = pendingAgents.size === 0;
|
|
185
|
+
output(event);
|
|
186
|
+
}).catch(error => { fail(error instanceof Error ? error : new Error("claude_event_failed")); void stop().catch(fail); });
|
|
187
|
+
});
|
|
188
|
+
try {
|
|
189
|
+
try {
|
|
190
|
+
await send({ type: "control_request", request_id: initId, request: { subtype: "initialize" } });
|
|
191
|
+
await bounded(initialized.promise, options.initializeTimeoutMs ?? 30_000, "claude_initialize_timeout");
|
|
192
|
+
const proof = JSON.parse(await readFile(proofPath, "utf8"));
|
|
193
|
+
if (proof.nonce !== nonce || proof.sessionId !== sessionId || proof.source !== (resume ? "resume" : "startup")) {
|
|
194
|
+
throw new Error("claude_session_proof_mismatch");
|
|
195
|
+
}
|
|
196
|
+
if (cancelled || failure)
|
|
197
|
+
throw failure ?? new Error("claude_thread_cancelled");
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (cancelled)
|
|
201
|
+
throw error;
|
|
202
|
+
throw new PreflightError("claude_native_session_unconfirmed");
|
|
203
|
+
}
|
|
204
|
+
delivery = await (options.prepareDelivery ?? prepareThreadDelivery)({ env, resumed: resume, sessionId, wakePrompt: input.wakePrompt });
|
|
205
|
+
if (cancelled || failure)
|
|
206
|
+
throw failure ?? new Error("claude_thread_cancelled");
|
|
207
|
+
if (resume)
|
|
208
|
+
output({ type: "nowwork.native_resume", runtime: "claude", session_id: sessionId });
|
|
209
|
+
submitted = true;
|
|
210
|
+
await send({ type: "user", session_id: "", uuid: userId, parent_tool_use_id: null,
|
|
211
|
+
message: { role: "user", content: delivery.prompt } });
|
|
212
|
+
await bounded(accepted.promise, options.ackTimeoutMs ?? 30_000, "claude_user_message_not_acknowledged");
|
|
213
|
+
// Keep stdin open for control requests until the model result has arrived.
|
|
214
|
+
while (!sawResult && !exitObserved && !failure)
|
|
215
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
216
|
+
if (failure)
|
|
217
|
+
throw failure;
|
|
218
|
+
child.stdin?.end();
|
|
219
|
+
const code = await closed.promise;
|
|
220
|
+
await processing;
|
|
221
|
+
if (failure)
|
|
222
|
+
throw failure;
|
|
223
|
+
if (!sawResult)
|
|
224
|
+
throw new Error("claude_result_missing");
|
|
225
|
+
return code;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
process.off("SIGTERM", onSignal);
|
|
229
|
+
process.off("SIGINT", onSignal);
|
|
230
|
+
try {
|
|
231
|
+
await stop();
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
lines?.close();
|
|
235
|
+
await rm(proofDir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
export async function runClaudeThread(input, options = {}) {
|
|
240
|
+
const sessionId = input.sessionId ?? randomUUID();
|
|
241
|
+
try {
|
|
242
|
+
return await attempt(input, sessionId, input.resume, options);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
// The failed attempt never received a business user turn or entered its
|
|
246
|
+
// model/tool loop. Native startup hooks may run again on cold recovery.
|
|
247
|
+
if (!input.resume || !(error instanceof PreflightError || error instanceof ThreadDeliveryColdStartRequired))
|
|
248
|
+
throw error;
|
|
249
|
+
return attempt(input, randomUUID(), false, options);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
253
|
+
let raw = "";
|
|
254
|
+
for await (const chunk of process.stdin) {
|
|
255
|
+
raw += String(chunk);
|
|
256
|
+
if (Buffer.byteLength(raw) > 8 * 1024 * 1024)
|
|
257
|
+
throw new Error("claude_thread_input_too_large");
|
|
258
|
+
}
|
|
259
|
+
runClaudeThread(InputSchema.parse(JSON.parse(raw))).then(code => { process.exitCode = code; }).catch(error => {
|
|
260
|
+
const message = error instanceof Error ? error.message : "";
|
|
261
|
+
const reason = /^claude_[a-z_]+$/u.test(message) || message.startsWith("Thread input delivery failed:")
|
|
262
|
+
? message : "claude_thread_contract_failed";
|
|
263
|
+
process.stderr.write(`Claude thread runner failed: ${reason}\n`);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
});
|
|
266
|
+
}
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Claude Code runtime 适配:print + stream-json 模式,headless 驱动。
|
|
3
3
|
*/
|
|
4
4
|
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { posix, win32 } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
8
|
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
7
9
|
import spawn from "cross-spawn";
|
|
8
10
|
// Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
|
|
@@ -276,11 +278,28 @@ export function buildClaudeArgs(input) {
|
|
|
276
278
|
args.push(input.wakePrompt);
|
|
277
279
|
return args;
|
|
278
280
|
}
|
|
281
|
+
export function buildClaudeLaunch(input) {
|
|
282
|
+
const args = buildClaudeArgs(input);
|
|
283
|
+
if (!input.env.CREW_THREAD_ROOT?.trim())
|
|
284
|
+
return { command: input.bin, args, cwd: input.cwd, env: input.env };
|
|
285
|
+
const source = import.meta.url.endsWith(".ts");
|
|
286
|
+
return { command: process.execPath,
|
|
287
|
+
args: [...(source ? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href] : []),
|
|
288
|
+
fileURLToPath(new URL(`./claude-thread-runner.${source ? "ts" : "js"}`, import.meta.url))],
|
|
289
|
+
cwd: input.cwd, env: input.env,
|
|
290
|
+
stdinText: JSON.stringify({ bin: input.bin, args: args.slice(0, -1), wakePrompt: input.wakePrompt,
|
|
291
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}), resume: input.resume === true }) };
|
|
292
|
+
}
|
|
279
293
|
export function spawnClaude(input) {
|
|
294
|
+
const launch = buildClaudeLaunch(input);
|
|
280
295
|
// stdio 固定 ignore/pipe/pipe,stdout/stderr 必为 Readable;cross-spawn 类型不带该细化,断言之
|
|
281
|
-
|
|
282
|
-
cwd:
|
|
283
|
-
|
|
284
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
296
|
+
const child = spawn(launch.command, [...launch.args], {
|
|
297
|
+
cwd: launch.cwd, env: launch.env,
|
|
298
|
+
stdio: [launch.stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
285
299
|
});
|
|
300
|
+
if (launch.stdinText !== undefined) {
|
|
301
|
+
child.stdin?.on("error", () => undefined);
|
|
302
|
+
child.stdin?.end(launch.stdinText);
|
|
303
|
+
}
|
|
304
|
+
return child;
|
|
286
305
|
}
|
|
@@ -9,6 +9,7 @@ import { signalSupervisorTree } from "../execution-supervisor.js";
|
|
|
9
9
|
import { assertCodexCollaborationVersion, assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
|
|
10
10
|
import { decideTurnRetry, itemMakesTurnUnsafe } from "./codex-turn-retry.js";
|
|
11
11
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
12
|
+
import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
|
|
12
13
|
const RunnerInputSchema = z.object({
|
|
13
14
|
systemPrompt: z.string().min(1),
|
|
14
15
|
wakePrompt: z.string().min(1),
|
|
@@ -440,7 +441,7 @@ async function stopChildTree(child) {
|
|
|
440
441
|
throw new Error(`Codex app-server process tree ${child.pid ?? "unknown"} did not exit`);
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
|
-
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs) {
|
|
444
|
+
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs, resumeEventSessionId) {
|
|
444
445
|
const attemptStartedAt = Date.now();
|
|
445
446
|
const child = spawn(bin, codexAppServerArgs({
|
|
446
447
|
...(input.projectRootMarkers === undefined ? {} : { projectRootMarkers: input.projectRootMarkers }),
|
|
@@ -589,6 +590,10 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
589
590
|
}
|
|
590
591
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
591
592
|
threadId = thread.thread.id;
|
|
593
|
+
if (input.resume) {
|
|
594
|
+
if (!input.sessionId || threadId !== input.sessionId)
|
|
595
|
+
throw new Error("native_resume_session_mismatch");
|
|
596
|
+
}
|
|
592
597
|
if (announcedThreadId !== threadId) {
|
|
593
598
|
announcedThreadId = threadId;
|
|
594
599
|
await jsonLine({ type: "thread.started", thread_id: threadId });
|
|
@@ -603,6 +608,15 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
603
608
|
throw codexProjectWorkspaceUnsupported();
|
|
604
609
|
}
|
|
605
610
|
}
|
|
611
|
+
activeStage = "thread_context";
|
|
612
|
+
stageStartedAt = Date.now();
|
|
613
|
+
const delivery = await prepareThreadDelivery({ env: process.env, resumed: input.resume,
|
|
614
|
+
sessionId: threadId, wakePrompt: input.wakePrompt });
|
|
615
|
+
// Publish cross-execution resume only after context preparation succeeds; a
|
|
616
|
+
// missing prior cursor can still cause a fresh session before any business turn.
|
|
617
|
+
if (input.resume && threadId === resumeEventSessionId)
|
|
618
|
+
await jsonLine({ type: "nowwork.native_resume", session_id: threadId });
|
|
619
|
+
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
606
620
|
firstProgress = startFirstProgressWatchdog(() => {
|
|
607
621
|
completionReject(new Error("Codex produced no semantic progress within the startup window"));
|
|
608
622
|
void cancel();
|
|
@@ -612,7 +626,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
612
626
|
const started = await rpc.request("turn/start", {
|
|
613
627
|
threadId,
|
|
614
628
|
input: [
|
|
615
|
-
{ type: "text", text:
|
|
629
|
+
{ type: "text", text: delivery.prompt, text_elements: [] },
|
|
616
630
|
...(input.imagePaths ?? []).map((path) => ({ type: "localImage", path })),
|
|
617
631
|
],
|
|
618
632
|
...(input.reasoning === undefined || input.reasoning === "default"
|
|
@@ -622,8 +636,11 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
622
636
|
sandboxPolicy: { type: "workspaceWrite", networkAccess: true },
|
|
623
637
|
} : {}),
|
|
624
638
|
});
|
|
625
|
-
|
|
639
|
+
if (!started || typeof started.turn?.id !== "string" || !started.turn.id)
|
|
640
|
+
throw new Error("invalid_turn_start_response");
|
|
626
641
|
turnId = started.turn.id;
|
|
642
|
+
await delivery.commit();
|
|
643
|
+
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
627
644
|
activeStage = "turn_complete";
|
|
628
645
|
stageStartedAt = Date.now();
|
|
629
646
|
const completed = await completion;
|
|
@@ -656,9 +673,15 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
656
673
|
initializeTimedOut: false,
|
|
657
674
|
...(retryReason === undefined ? {} : { retryReason }),
|
|
658
675
|
unsafeToRetry: turnHasUnsafeSideEffect || collabStartedMissing,
|
|
676
|
+
deliveredSessionId: threadId,
|
|
659
677
|
};
|
|
660
678
|
}
|
|
661
679
|
catch (error) {
|
|
680
|
+
if (error instanceof ThreadDeliveryColdStartRequired && activeStage === "thread_context"
|
|
681
|
+
&& turnId === null && !turnHasUnsafeSideEffect) {
|
|
682
|
+
logStage(activeStage, "error", attempt, stageStartedAt, "recovery=restart_cold_before_turn");
|
|
683
|
+
return { code: 1, initializeTimedOut: false, unsafeToRetry: false, restartCold: true };
|
|
684
|
+
}
|
|
662
685
|
const initializeTimedOut = error instanceof CodexRpcTimeoutError && error.method === "initialize";
|
|
663
686
|
if (initializeTimedOut) {
|
|
664
687
|
const tail = stderr.redactedTail();
|
|
@@ -692,8 +715,19 @@ export async function runCodexAppServer(bin, options = {}) {
|
|
|
692
715
|
const turnRetryDelayMs = options.turnRetryDelayMs ?? TRANSIENT_TURN_RETRY_DELAY_MS;
|
|
693
716
|
let initializeTimeouts = 0;
|
|
694
717
|
let turnRetries = 0;
|
|
718
|
+
let coldRestarted = false;
|
|
719
|
+
let attemptInput = input;
|
|
720
|
+
const resumeEventSessionId = input.resume ? input.sessionId : undefined;
|
|
695
721
|
for (let attempt = 1;; attempt += 1) {
|
|
696
|
-
const result = await runCodexAppServerAttempt(bin,
|
|
722
|
+
const result = await runCodexAppServerAttempt(bin, attemptInput, attempt, initializeTimeoutMs, resumeEventSessionId);
|
|
723
|
+
if (result.restartCold) {
|
|
724
|
+
if (coldRestarted)
|
|
725
|
+
return result.code;
|
|
726
|
+
coldRestarted = true;
|
|
727
|
+
const { sessionId: _oldSessionId, ...freshInput } = input;
|
|
728
|
+
attemptInput = { ...freshInput, resume: false };
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
697
731
|
if (result.initializeTimedOut) {
|
|
698
732
|
initializeTimeouts += 1;
|
|
699
733
|
if (initializeTimeouts >= MAX_INITIALIZE_ATTEMPTS)
|
|
@@ -703,6 +737,11 @@ export async function runCodexAppServer(bin, options = {}) {
|
|
|
703
737
|
}
|
|
704
738
|
const decision = decideTurnRetry(result, turnRetries, MAX_TRANSIENT_TURN_RETRIES);
|
|
705
739
|
if (decision.kind === "retry") {
|
|
740
|
+
// The accepted turn is already in the native transcript. Keep its session
|
|
741
|
+
// and committed thread cursor so a safe retry cannot inject full history twice.
|
|
742
|
+
if (process.env.CREW_THREAD_ROOT && result.deliveredSessionId) {
|
|
743
|
+
attemptInput = { ...input, resume: true, sessionId: result.deliveredSessionId };
|
|
744
|
+
}
|
|
706
745
|
const delayMs = turnRetryDelayMs * TRANSIENT_TURN_RETRY_BACKOFF_FACTOR ** turnRetries;
|
|
707
746
|
turnRetries += 1;
|
|
708
747
|
process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=${decision.reason}\n`);
|