@nowcrew/daemon 0.6.88-beta.7 → 0.6.88-beta.9

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 (38) hide show
  1. package/README.md +14 -10
  2. package/dist/agent-memory/policy.js +3 -1
  3. package/dist/execution-protocol.js +1 -1
  4. package/dist/execution-runner.js +3 -2
  5. package/dist/local-executor.js +30 -9
  6. package/dist/native-thread-context.js +59 -0
  7. package/dist/prompt.js +15 -15
  8. package/dist/runner.js +14 -15
  9. package/dist/runtimes/claude-session-start-hook.js +24 -0
  10. package/dist/runtimes/claude-thread-runner.js +266 -0
  11. package/dist/runtimes/claude.js +23 -4
  12. package/dist/runtimes/codex-app-server-runner.js +43 -4
  13. package/dist/runtimes/kimi-acp-runner.js +6 -2
  14. package/dist/runtimes/opencode-runner.js +7 -1
  15. package/dist/serve.js +7 -8
  16. package/dist/session.js +4 -0
  17. package/dist/supervised-runtime.js +11 -16
  18. package/dist/thread-input-delivery.js +217 -0
  19. package/package.json +2 -2
  20. package/dist/project-skills/capability.js +0 -109
  21. package/dist/project-skills/controller-convergence.js +0 -57
  22. package/dist/project-skills/projection-set-switch.js +0 -419
  23. package/dist/project-skills/projection-state-domain.js +0 -170
  24. package/dist/project-skills/projection-state-store.js +0 -842
  25. package/dist/project-skills/projection-state-transaction.js +0 -322
  26. package/dist/project-skills/projection-state.js +0 -3
  27. package/dist/project-skills/runtime-root-bootstrap.js +0 -47
  28. package/dist/project-skills/runtime-root-domain.js +0 -268
  29. package/dist/project-skills/runtime-root-gc.js +0 -293
  30. package/dist/project-skills/runtime-root-lease-artifact.js +0 -46
  31. package/dist/project-skills/runtime-root-leases.js +0 -487
  32. package/dist/project-skills/runtime-root-source-identity.js +0 -60
  33. package/dist/project-skills/runtime-root-startup.js +0 -49
  34. package/dist/project-skills/runtime-root-state-artifact-domain.js +0 -143
  35. package/dist/project-skills/runtime-root-state-index.js +0 -356
  36. package/dist/project-skills/runtime-root-store.js +0 -722
  37. package/dist/project-skills/serve-capability.js +0 -28
  38. package/dist/project-skills/serve-startup.js +0 -37
@@ -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
+ }
@@ -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
- return spawn(input.bin, buildClaudeArgs(input), {
282
- cwd: input.cwd,
283
- env: input.env,
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: input.wakePrompt, text_elements: [] },
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
- logStage(activeStage, "ok", attempt, stageStartedAt);
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, input, attempt, initializeTimeoutMs);
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`);
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
6
6
  import spawn from "cross-spawn";
7
7
  import { PROTOCOL_VERSION, RequestError, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
8
8
  import { augmentedPath } from "../runtime-path.js";
9
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
9
10
  import { startFirstProgressWatchdog } from "./progress-watchdog.js";
10
11
  import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
11
12
  const ERROR_MESSAGE_CAP = 2_000;
@@ -255,14 +256,17 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
255
256
  await stopChild(child);
256
257
  }
257
258
  }
258
- export async function runKimiAcp(options) {
259
+ export async function runKimiAcp(requestedOptions) {
260
+ // Until this adapter verifies native restoration, thread-bound runs are cold.
261
+ const options = process.env.CREW_THREAD_ROOT ? { ...requestedOptions, resume: false } : requestedOptions;
259
262
  const provider = options.provider ?? "kimi";
260
263
  const displayName = providerDisplayName(provider);
261
264
  const permission = options.effectivePermission ?? "full_access";
262
265
  if (provider === "deepseek-harness" && options.resume) {
263
266
  throw new Error("DeepSeek Harness ACP does not support session resume");
264
267
  }
265
- const prompt = await readPrompt();
268
+ const prompt = (await prepareThreadDelivery({ env: process.env, resumed: false,
269
+ sessionId: "unverified-cold-acp", wakePrompt: await readPrompt() })).prompt;
266
270
  const child = spawn(options.bin, providerArgs(provider, options.configPath), {
267
271
  cwd: process.cwd(),
268
272
  env: providerEnv(provider, permission),
@@ -4,6 +4,7 @@ import { parseArgs } from "node:util";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import spawn from "cross-spawn";
6
6
  import { augmentedPath } from "../runtime-path.js";
7
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
7
8
  import { buildOpenCodeArgs, createOpenCodeEventDecoder } from "./opencode.js";
8
9
  const ERROR_CAP = 2_000;
9
10
  async function readPrompt() {
@@ -86,7 +87,12 @@ async function runAttempt(options, prompt) {
86
87
  return { code: 0, staleSession: false };
87
88
  }
88
89
  export async function runOpenCode(options) {
89
- const prompt = await readPrompt();
90
+ const prompt = (await prepareThreadDelivery({ env: process.env, resumed: false,
91
+ sessionId: "unverified-cold-opencode", wakePrompt: await readPrompt() })).prompt;
92
+ if (process.env.CREW_THREAD_ROOT) {
93
+ const { sessionId: _unverified, ...coldOptions } = options;
94
+ return (await runAttempt(coldOptions, prompt)).code;
95
+ }
90
96
  const first = await runAttempt(options, prompt);
91
97
  if (!first.staleSession)
92
98
  return first.code;
package/dist/serve.js CHANGED
@@ -1090,15 +1090,14 @@ export function serve(config, opts = {}) {
1090
1090
  const ackHint = `\n**协作礼仪:决定接手后,先用 \`${sendCmd}\` 在该任务线程说一声你来跟进**——用日常语言带上你对问题的理解或打算先查什么,不要写"我接 task #N"这类内部编号;写不出实质内容就不单发,并进第一条实质进展,但不要长时间闷头干活把线程空着。`;
1091
1091
  // 图片/文件附件:crew message read 会在消息下列出附件及其 id;图片需下载后用 Read 工具查看,才能真正"看到"内容。
1092
1092
  const attHint = `\n若消息带图片/文件附件(read 会列出 id),用 \`crew attachment get <id>\` 下载到本地,图片再用 Read 工具打开查看后再处理。`;
1093
- // 线程隔离:有 threadId 时用 `crew thread read` 只读本线程(避免被其他线程消息干扰);
1094
- // 顶层消息(无 threadId)用 `crew message read` 读整个频道。
1095
- const readCmd = threadId
1096
- ? `crew thread read`
1097
- : `crew message read --channel ${msg.channelId}`;
1093
+ // 启动上下文由执行层交付;legacy 提示词也不能要求模型补读或退回频道历史。
1094
+ const contextHint = threadId
1095
+ ? "直接依据程序交付的本线程上下文与新消息处理,无需启动补读历史。"
1096
+ : "没有线程锚点,只处理明确来信或请求补充当前任务线程,不读取频道历史猜测。";
1098
1097
  const wakeText = scheduled
1099
1098
  ? buildScheduledPrompt(msg.channelId, msg.wake?.content ?? "", scheduled.outputPolicy, scheduled.externalNotificationPolicy)
1100
1099
  : (msg.wake?.content
1101
- ? `你被唤醒(${msg.reason}): ${msg.wake.content}\n用 ${readCmd} 读${threadId ? "本线程" : "频道"}后按需处理。${reasonHint}${ackHint}${threadHint}${attHint}`
1100
+ ? `你被唤醒(${msg.reason}): ${msg.wake.content}\n${contextHint}${reasonHint}${ackHint}${threadHint}${attHint}`
1102
1101
  : undefined);
1103
1102
  const wakeOrigin = scheduled ? undefined : msg.wake?.origin;
1104
1103
  const guarded = await runWithOriginDecisionGuard(wakeOrigin, async (attempt) => {
@@ -1124,8 +1123,8 @@ export function serve(config, opts = {}) {
1124
1123
  },
1125
1124
  } : {}),
1126
1125
  ...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
1127
- // 唤醒锚点是具体消息(非纯频道唤醒)时,把它透传下去,供 `crew task create` 锚定到该消息。
1128
- ...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
1126
+ // legacy wake.threadId 已由 Server 归一到线程根;与普通触发消息 id 分开透传。
1127
+ ...(!scheduled && threadId ? { threadId, wakeMessageId: threadId } : {}),
1129
1128
  ...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
1130
1129
  ...(attemptWake ? { wake: attemptWake } : {}),
1131
1130
  }, reportActivity, reportConsole, {
package/dist/session.js CHANGED
@@ -24,6 +24,10 @@ export async function readSession(runDir) {
24
24
  if (typeof o.sessionId !== "string" || !o.sessionId)
25
25
  return null;
26
26
  return {
27
+ ...(typeof o.runtime === "string" ? { runtime: o.runtime } : {}),
28
+ ...(typeof o.threadRoot === "string" ? { threadRoot: o.threadRoot } : {}),
29
+ ...(Number.isSafeInteger(o.threadContextSeq) && o.threadContextSeq >= 0 ? { threadContextSeq: o.threadContextSeq } : {}),
30
+ ...(typeof o.threadHistoryComplete === "boolean" ? { threadHistoryComplete: o.threadHistoryComplete } : {}),
27
31
  sessionId: o.sessionId,
28
32
  lastRunAt: typeof o.lastRunAt === "number" ? o.lastRunAt : 0,
29
33
  turns: typeof o.turns === "number" ? o.turns : 0,
@@ -1,6 +1,6 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { startDormantSupervisor, } from "./execution-supervisor.js";
3
- import { buildClaudeArgs } from "./runtimes/claude.js";
3
+ import { buildClaudeLaunch } from "./runtimes/claude.js";
4
4
  import { DEEPSEEK_HARNESS_CONFIG_ENV, deepSeekHarnessConfigPath, } from "./runtimes/deepseek-harness.js";
5
5
  import { RuntimeCancelledError } from "./runtime-cancellation.js";
6
6
  export function supervisorLaunch(request) {
@@ -13,23 +13,18 @@ export function supervisorLaunch(request) {
13
13
  };
14
14
  if (request.runtime === "claude") {
15
15
  const additionalDirectories = request.claudeAdditionalDirectories;
16
- return {
17
- command: request.bin,
18
- args: buildClaudeArgs({
19
- ...common,
20
- bin: request.bin,
21
- cwd: request.cwd,
22
- env: request.env,
23
- systemPromptPath: request.systemPromptPath,
24
- ...(additionalDirectories === undefined ? {} : { additionalDirectories }),
25
- ...(request.sessionId === undefined ? {} : {
26
- sessionId: request.sessionId,
27
- resume: request.resume,
28
- }),
29
- }),
16
+ return buildClaudeLaunch({
17
+ ...common,
18
+ bin: request.bin,
30
19
  cwd: request.cwd,
31
20
  env: request.env,
32
- };
21
+ systemPromptPath: request.systemPromptPath,
22
+ ...(additionalDirectories === undefined ? {} : { additionalDirectories }),
23
+ ...(request.sessionId === undefined ? {} : {
24
+ sessionId: request.sessionId,
25
+ resume: request.resume,
26
+ }),
27
+ });
33
28
  }
34
29
  if (request.runtime === "codex") {
35
30
  return {