@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.30

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 (129) hide show
  1. package/README.md +89 -14
  2. package/dist/agent-ability/controller.js +11 -1
  3. package/dist/agent-ability/git-errors.js +22 -0
  4. package/dist/agent-ability/materializer.js +21 -10
  5. package/dist/agent-ability/resolver.js +12 -4
  6. package/dist/agent-environment-contract.js +55 -0
  7. package/dist/agent-environment-control.js +24 -0
  8. package/dist/agent-environment-execution.js +98 -0
  9. package/dist/agent-environments.js +275 -0
  10. package/dist/agent-memory/policy.js +3 -1
  11. package/dist/automatic-daemon-installer.js +51 -2
  12. package/dist/automatic-install-target.js +8 -0
  13. package/dist/computer-main.js +18 -0
  14. package/dist/computer-profile.js +17 -4
  15. package/dist/computer-setup-client.js +318 -0
  16. package/dist/computer-setup-orchestration.js +203 -0
  17. package/dist/computer-setup.js +244 -0
  18. package/dist/config.js +30 -12
  19. package/dist/console-formatter.js +3 -3
  20. package/dist/console.js +15 -2
  21. package/dist/daemon-update-eligibility.js +1 -1
  22. package/dist/execution-journal.js +9 -6
  23. package/dist/execution-protocol.js +2 -1
  24. package/dist/execution-runner.js +16 -10
  25. package/dist/i18n.js +19 -0
  26. package/dist/list-models.js +13 -11
  27. package/dist/local-execution-lease.js +22 -0
  28. package/dist/local-executor.js +88 -92
  29. package/dist/local-memory/candidate.js +43 -0
  30. package/dist/local-memory/index-budget.js +8 -0
  31. package/dist/local-memory/inventory-cli.js +61 -0
  32. package/dist/local-memory/inventory-preview.js +159 -0
  33. package/dist/local-memory/inventory-source.js +159 -0
  34. package/dist/local-memory/pending-match.js +29 -0
  35. package/dist/local-memory/pending-query.js +100 -0
  36. package/dist/local-memory/pending-schema.js +29 -0
  37. package/dist/local-memory/pending-source.js +126 -0
  38. package/dist/local-memory/pending.js +34 -84
  39. package/dist/local-memory/prompt.js +20 -12
  40. package/dist/local-memory/read-cli.js +97 -12
  41. package/dist/local-memory/rollout.js +82 -0
  42. package/dist/local-memory/runtime.js +24 -17
  43. package/dist/local-memory/work-log-execution.js +31 -0
  44. package/dist/local-memory/work-log.js +58 -0
  45. package/dist/local-memory/write.js +27 -58
  46. package/dist/local-runtime-environment.js +50 -0
  47. package/dist/machine-environment-cache.js +147 -0
  48. package/dist/machine-environment-contract.js +10 -0
  49. package/dist/machine-environment-control.js +27 -0
  50. package/dist/machine-environment-policy.js +75 -0
  51. package/dist/machine-environment-process.js +110 -0
  52. package/dist/machine-info.js +70 -25
  53. package/dist/machine-runtime-environment.js +189 -0
  54. package/dist/machine-shell-environment-capture.js +135 -0
  55. package/dist/machine-windows-environment-capture.js +215 -0
  56. package/dist/machine-windows-environment-script.js +158 -0
  57. package/dist/main.js +4 -69
  58. package/dist/marketplace-controller.js +75 -0
  59. package/dist/marketplace-protocol.js +38 -0
  60. package/dist/marketplace-runner.js +234 -0
  61. package/dist/marketplace-serve.js +17 -0
  62. package/dist/message-file-preview.js +246 -0
  63. package/dist/native-thread-context.js +59 -0
  64. package/dist/pre-admission-rejection.js +64 -0
  65. package/dist/project-skills/execution-adapter.js +3 -2
  66. package/dist/project-skills/runtime-launch.js +14 -0
  67. package/dist/prompt.js +15 -15
  68. package/dist/provider-env.js +26 -15
  69. package/dist/runner.js +31 -19
  70. package/dist/runtime-environment-merge.js +16 -0
  71. package/dist/runtime-health-handler.js +10 -0
  72. package/dist/runtime-health.js +3 -1
  73. package/dist/runtime-path.js +22 -12
  74. package/dist/runtime-probe.js +35 -6
  75. package/dist/runtimes/acp-probe-transport.js +69 -0
  76. package/dist/runtimes/claude-models.js +20 -9
  77. package/dist/runtimes/claude-session-start-hook.js +24 -0
  78. package/dist/runtimes/claude-thread-runner.js +266 -0
  79. package/dist/runtimes/claude.js +23 -4
  80. package/dist/runtimes/codex-app-server-runner.js +43 -4
  81. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  82. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  83. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  84. package/dist/runtimes/deepseek-harness.js +76 -15
  85. package/dist/runtimes/harness-probe-retry.js +42 -0
  86. package/dist/runtimes/hermes-models.js +2 -1
  87. package/dist/runtimes/kimi-acp-runner.js +84 -15
  88. package/dist/runtimes/opencode-runner.js +7 -1
  89. package/dist/runtimes/opencode.js +2 -2
  90. package/dist/serve.js +95 -184
  91. package/dist/server-url.js +25 -0
  92. package/dist/session.js +4 -0
  93. package/dist/slog.js +2 -1
  94. package/dist/storage-identity.js +0 -3
  95. package/dist/storage-runtime.js +2 -2
  96. package/dist/storage-serve-adapter.js +6 -72
  97. package/dist/supervised-runtime.js +28 -18
  98. package/dist/thread-input-delivery.js +217 -0
  99. package/dist/token.js +0 -1
  100. package/dist/workspace-control-handler.js +27 -0
  101. package/dist/workspace-fs.js +66 -15
  102. package/dist/workspace.js +1 -1
  103. package/package.json +5 -4
  104. package/dist/codex-home-launch.js +0 -7
  105. package/dist/codex-home-mode-controller.js +0 -68
  106. package/dist/daemon-migration-controller.js +0 -211
  107. package/dist/daemon-migration-wiring.js +0 -22
  108. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  109. package/dist/runtimes/codex-home-migration.js +0 -112
  110. package/dist/runtimes/codex-home.js +0 -236
  111. package/dist/storage-control.js +0 -212
  112. package/dist/storage-copy.js +0 -628
  113. package/dist/storage-credential-guard.js +0 -21
  114. package/dist/storage-doctor.js +0 -228
  115. package/dist/storage-manifest.js +0 -1027
  116. package/dist/storage-migration-controller.js +0 -440
  117. package/dist/storage-migration-manifest.js +0 -105
  118. package/dist/storage-migration-policy.js +0 -100
  119. package/dist/storage-runtime-controller.js +0 -381
  120. package/dist/storage-runtime-readiness.js +0 -240
  121. package/dist/storage-safe-fs.js +0 -302
  122. package/dist/storage-status-delivery.js +0 -224
  123. package/dist/storage-status-replay.js +0 -140
  124. package/dist/storage-target-confirmation.js +0 -129
  125. package/dist/storage-worker-command.js +0 -37
  126. package/dist/storage-worker-executor.js +0 -449
  127. package/dist/storage-worker-handoff.js +0 -151
  128. package/dist/storage-worker-operation.js +0 -294
  129. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,226 @@
1
+ /** 新版 Harness 的短命 ACP 桥:产品状态与重试仍属于 server。 */
2
+ import { once } from "node:events";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { parseArgs } from "node:util";
5
+ import { pathToFileURL } from "node:url";
6
+ import { isAbsolute } from "node:path";
7
+ import spawn from "cross-spawn";
8
+ import { z } from "zod";
9
+ import { client, methods, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
10
+ import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
11
+ import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
12
+ import { mapAcpUpdate } from "./kimi-acp-runner.js";
13
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
14
+ const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
15
+ export const HarnessInputSchema = z
16
+ .object({
17
+ systemPrompt: z.string(),
18
+ wakePrompt: z.string().min(1),
19
+ stateRoot: absolutePath,
20
+ skillRoots: z.array(absolutePath).default([]),
21
+ effectivePermission: z.enum([
22
+ "sandboxed",
23
+ "workspace_write",
24
+ "full_access",
25
+ ]),
26
+ })
27
+ .strict();
28
+ const emit = async (event) => {
29
+ if (!process.stdout.write(`${JSON.stringify(event)}\n`))
30
+ await once(process.stdout, "drain");
31
+ };
32
+ async function settleWithin(promise, ms) {
33
+ let timer;
34
+ try {
35
+ return await Promise.race([
36
+ promise,
37
+ new Promise((resolve) => {
38
+ timer = setTimeout(() => resolve(undefined), ms);
39
+ }),
40
+ ]);
41
+ }
42
+ finally {
43
+ if (timer)
44
+ clearTimeout(timer);
45
+ }
46
+ }
47
+ export async function runHarness(input, bin, patchPath) {
48
+ const delivery = await prepareThreadDelivery({
49
+ env: process.env,
50
+ resumed: false,
51
+ sessionId: "cold-harness",
52
+ wakePrompt: input.wakePrompt,
53
+ });
54
+ const profile = await prepareHarnessProfile({
55
+ ...input,
56
+ cwd: process.cwd(),
57
+ env: process.env,
58
+ ...(patchPath ? { patchPath } : {}),
59
+ });
60
+ const launch = deepSeekHarnessLaunch(bin, profile.args);
61
+ const child = spawn(launch.bin, launch.args, {
62
+ cwd: profile.home,
63
+ env: profile.env,
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ });
66
+ // Boot away from the repository: the CLI must not load a project's .env into
67
+ // trusted process credentials. ACP session/new carries the real workspace.
68
+ let spawnError;
69
+ const closed = new Promise((resolve) => {
70
+ child.once("error", (error) => {
71
+ spawnError = error;
72
+ resolve();
73
+ });
74
+ child.once("close", () => resolve());
75
+ });
76
+ if (!child.stdin || !child.stdout || !child.stderr)
77
+ throw new Error("DeepSeek Harness stdio unavailable");
78
+ // Provider diagnostics may include secrets or prompts; do not stream raw stderr.
79
+ child.stderr.resume();
80
+ let context;
81
+ let sessionId;
82
+ let cancelled = false;
83
+ let stage = "initialize";
84
+ let exitCode = 1;
85
+ let stopping;
86
+ const stop = () => (stopping ??= (async () => {
87
+ child.stdin?.end();
88
+ if (await settleWithin(closed.then(() => true), 2000))
89
+ return;
90
+ child.kill("SIGTERM");
91
+ if (await settleWithin(closed.then(() => true), 1000))
92
+ return;
93
+ child.kill("SIGKILL");
94
+ if (!(await settleWithin(closed.then(() => true), 2000)))
95
+ throw new Error("DeepSeek Harness process stop unconfirmed");
96
+ })());
97
+ const cancel = () => {
98
+ cancelled = true;
99
+ void (async () => {
100
+ if (context && sessionId)
101
+ await settleWithin(context
102
+ .notify(methods.agent.session.cancel, { sessionId })
103
+ .catch(() => undefined), 1000);
104
+ await stop();
105
+ })().catch(() => {
106
+ /* finally reports stop failure; supervisor owns process tree */
107
+ });
108
+ };
109
+ process.once("SIGTERM", cancel);
110
+ process.once("SIGINT", cancel);
111
+ const app = client({ name: "nowwork-harness" })
112
+ .onRequest(methods.client.session.requestPermission, () => ({
113
+ outcome: { outcome: "cancelled" },
114
+ }))
115
+ .onNotification(methods.client.session.update, async ({ params }) => {
116
+ if (params.sessionId !== sessionId)
117
+ return;
118
+ for (const event of mapAcpUpdate("deepseek-harness", params.update))
119
+ await emit(event);
120
+ });
121
+ try {
122
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
123
+ const result = await app.connectWith(stream, async (ctx) => {
124
+ context = ctx;
125
+ const initialized = await ctx.request(methods.agent.initialize, {
126
+ protocolVersion: PROTOCOL_VERSION,
127
+ clientCapabilities: {},
128
+ clientInfo: { name: "nowwork", version: "1" },
129
+ });
130
+ if (initialized.protocolVersion !== PROTOCOL_VERSION)
131
+ throw new Error("Incompatible ACP protocol");
132
+ if (cancelled)
133
+ throw new Error("cancelled");
134
+ stage = "session_new";
135
+ const session = await ctx.request(methods.agent.session.new, {
136
+ cwd: process.cwd(),
137
+ mcpServers: [],
138
+ });
139
+ sessionId = session.sessionId;
140
+ if (!sessionId || cancelled)
141
+ throw new Error("Harness session unavailable");
142
+ await emit({ type: "thread.started", thread_id: sessionId });
143
+ stage = "prompt";
144
+ const response = await ctx.request(methods.agent.session.prompt, {
145
+ sessionId,
146
+ prompt: [{ type: "text", text: delivery.prompt }],
147
+ });
148
+ if (initialized.agentCapabilities?.sessionCapabilities?.close != null) {
149
+ stage = "session_close";
150
+ const close = await settleWithin(ctx
151
+ .request(methods.agent.session.close, { sessionId })
152
+ .then(() => true), 5000);
153
+ if (!close)
154
+ throw new Error("Harness session close timed out");
155
+ }
156
+ return response;
157
+ });
158
+ const usage = result.usage;
159
+ if (!cancelled)
160
+ await emit({
161
+ type: "turn.completed",
162
+ stop_reason: result.stopReason,
163
+ ...(usage
164
+ ? {
165
+ usage: {
166
+ input_tokens: usage.inputTokens,
167
+ output_tokens: usage.outputTokens,
168
+ cache_read_input_tokens: usage.cachedReadTokens ?? 0,
169
+ cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
170
+ },
171
+ }
172
+ : {}),
173
+ });
174
+ exitCode =
175
+ result.stopReason === "end_turn"
176
+ ? 0
177
+ : result.stopReason === "cancelled"
178
+ ? 130
179
+ : 1;
180
+ }
181
+ catch {
182
+ if (!cancelled)
183
+ process.stderr.write(spawnError
184
+ ? "DeepSeek Harness launch failed; verify the installed dsh executable.\n"
185
+ : `DeepSeek Harness ACP execution failed at ${stage}; verify DEEPSEEK_API_KEY, model access, the pinned version and profile patch.\n`);
186
+ }
187
+ finally {
188
+ try {
189
+ await stop();
190
+ }
191
+ finally {
192
+ process.off("SIGTERM", cancel);
193
+ process.off("SIGINT", cancel);
194
+ }
195
+ }
196
+ if (cancelled)
197
+ return 130;
198
+ return child.exitCode !== 0 ? 1 : exitCode;
199
+ }
200
+ async function main() {
201
+ const { values } = parseArgs({
202
+ options: { bin: { type: "string" }, patch: { type: "string" } },
203
+ });
204
+ if (!values.bin)
205
+ throw new Error("DeepSeek Harness --bin is required");
206
+ process.stdin.setEncoding("utf8");
207
+ let text = "";
208
+ for await (const chunk of process.stdin) {
209
+ text += String(chunk);
210
+ if (Buffer.byteLength(text) > 16 * 1024 * 1024)
211
+ throw new Error("Harness input exceeds 16 MiB");
212
+ }
213
+ const input = HarnessInputSchema.parse(JSON.parse(text));
214
+ return runHarness(input, values.bin, values.patch);
215
+ }
216
+ if (process.argv[1] &&
217
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
218
+ main()
219
+ .then((code) => {
220
+ process.exitCode = code;
221
+ })
222
+ .catch(() => {
223
+ process.stderr.write("DeepSeek Harness configuration or cleanup failed; check execution paths and machine patch.\n");
224
+ process.exitCode = 1;
225
+ });
226
+ }
@@ -1,20 +1,81 @@
1
1
  import spawn from "cross-spawn";
2
- import { isAbsolute } from "node:path";
3
- import { probeKimiAcp } from "./kimi-acp-runner.js";
2
+ import { isAbsolute, win32, join } from "node:path";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
6
+ import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
7
+ import { probeKimiAcp, AcpProbeCleanupError } from "./kimi-acp-runner.js";
8
+ import { runtimeEnvironmentValue } from "../runtime-environment-merge.js";
9
+ import { augmentedPath } from "../runtime-path.js";
4
10
  export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
5
- export function deepSeekHarnessConfigPath(env = process.env) {
6
- const path = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
7
- return path && isAbsolute(path) ? path : null;
11
+ export const DEEPSEEK_HARNESS_PATCH_ENV = "NOWCREW_DEEPSEEK_HARNESS_PATCH";
12
+ export function deepSeekHarnessLauncher(env = process.env, platform = process.platform) {
13
+ const configPath = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_CONFIG_ENV, platform)?.trim();
14
+ const patchPath = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_PATCH_ENV, platform)?.trim();
15
+ const absolute = platform === "win32" ? win32.isAbsolute : isAbsolute;
16
+ if (configPath && !absolute(configPath))
17
+ throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
18
+ if (patchPath && !absolute(patchPath))
19
+ throw new Error(`${DEEPSEEK_HARNESS_PATCH_ENV} must be an absolute path`);
20
+ if (configPath && patchPath)
21
+ throw new Error("DeepSeek Harness legacy config and profile patch are mutually exclusive");
22
+ return configPath
23
+ ? { kind: "legacy", bin: "dsh-acp-demo", configPath }
24
+ : { kind: "profile", bin: "dsh", ...(patchPath ? { patchPath } : {}) };
25
+ }
26
+ export function deepSeekHarnessConfigPath(env = process.env, platform = process.platform) {
27
+ const path = runtimeEnvironmentValue(env, DEEPSEEK_HARNESS_CONFIG_ENV, platform)?.trim();
28
+ return path && (platform === "win32" ? win32.isAbsolute(path) : isAbsolute(path)) ? path : null;
8
29
  }
9
30
  /** The published DeepSeek Harness ACP executable speaks JSON-RPC directly on stdio. */
10
- export function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
11
- const configPath = options.configPath ?? deepSeekHarnessConfigPath();
12
- if (!configPath || !isAbsolute(configPath))
13
- return Promise.resolve(false);
14
- return probeKimiAcp({
15
- ...options,
16
- provider: "deepseek-harness",
17
- configPath,
18
- effectivePermission: "sandboxed",
19
- }, spawnProcess);
31
+ export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
32
+ if (options.signal?.aborted)
33
+ return false;
34
+ const sourceEnv = options.env ?? { ...process.env, PATH: augmentedPath() };
35
+ let root;
36
+ try {
37
+ const launcher = options.configPath === undefined
38
+ ? deepSeekHarnessLauncher(sourceEnv)
39
+ : deepSeekHarnessLauncher({
40
+ ...sourceEnv,
41
+ [DEEPSEEK_HARNESS_CONFIG_ENV]: options.configPath,
42
+ });
43
+ if (launcher.kind === "legacy")
44
+ return await probeKimiAcp({
45
+ ...options,
46
+ env: sourceEnv,
47
+ provider: "deepseek-harness",
48
+ configPath: launcher.configPath,
49
+ effectivePermission: "sandboxed",
50
+ }, spawnProcess);
51
+ root = await mkdtemp(join(tmpdir(), "nowwork-dsh-probe-"));
52
+ const profile = await prepareHarnessProfile({
53
+ stateRoot: root,
54
+ cwd: root,
55
+ systemPrompt: "",
56
+ skillRoots: [],
57
+ effectivePermission: "sandboxed",
58
+ env: sourceEnv,
59
+ ...(launcher.patchPath ? { patchPath: launcher.patchPath } : {}),
60
+ });
61
+ const launch = deepSeekHarnessLaunch(options.bin, profile.args);
62
+ return await probeKimiAcp({
63
+ ...options,
64
+ bin: launch.bin,
65
+ provider: "deepseek-harness",
66
+ effectivePermission: "sandboxed",
67
+ launchArgs: launch.args,
68
+ launchEnv: profile.env,
69
+ launchCwd: root,
70
+ }, spawnProcess);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof AcpProbeCleanupError)
74
+ throw error;
75
+ return false;
76
+ }
77
+ finally {
78
+ if (root)
79
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
80
+ }
20
81
  }
@@ -0,0 +1,42 @@
1
+ import { dslog } from "../slog.js";
2
+ const RETRY_DELAYS_MS = [1_000, 2_000];
3
+ /** 只延迟当前启动探测,不设后台定时任务;stop 立即清除等待。 */
4
+ function waitForRetry(ms, signal) {
5
+ if (signal?.aborted)
6
+ return Promise.resolve(false);
7
+ return new Promise((resolve) => {
8
+ const finish = (ready) => {
9
+ clearTimeout(timer);
10
+ signal?.removeEventListener("abort", onAbort);
11
+ resolve(ready);
12
+ };
13
+ const onAbort = () => finish(false);
14
+ const timer = setTimeout(() => finish(true), ms);
15
+ signal?.addEventListener("abort", onAbort, { once: true });
16
+ });
17
+ }
18
+ /** 成功才公布能力;每次 probe 返回前已完成进程清理,异常保持原有传播。 */
19
+ export async function retryHarnessProbe(probe, signal, probeId) {
20
+ for (let attempt = 1; attempt <= RETRY_DELAYS_MS.length + 1; attempt++) {
21
+ if (signal?.aborted)
22
+ return false;
23
+ const ready = await probe();
24
+ dslog("runtime_probe.harness_attempt", "Harness 启动探测尝试结束", {
25
+ probe_id: probeId, runtime: "deepseek-harness", attempt, ready,
26
+ aborted: signal?.aborted ?? false,
27
+ });
28
+ if (signal?.aborted)
29
+ return false;
30
+ if (ready)
31
+ return true;
32
+ const delayMs = RETRY_DELAYS_MS[attempt - 1];
33
+ if (delayMs === undefined)
34
+ return false;
35
+ dslog("runtime_probe.harness_retry", "延迟重试 Harness 启动探测", {
36
+ probe_id: probeId, runtime: "deepseek-harness", next_attempt: attempt + 1, delay_ms: delayMs,
37
+ });
38
+ if (!await waitForRetry(delayMs, signal))
39
+ return false;
40
+ }
41
+ return false;
42
+ }
@@ -71,9 +71,10 @@ async function stopChild(child) {
71
71
  }
72
72
  export async function listHermesModels(options = {}) {
73
73
  const spawnProcess = options.spawnProcess ?? spawn;
74
+ const env = options.env ?? { ...process.env, PATH: augmentedPath() };
74
75
  const child = spawnProcess("hermes", ["acp"], {
75
76
  cwd: process.cwd(),
76
- env: { ...process.env, PATH: augmentedPath() },
77
+ env,
77
78
  stdio: ["pipe", "pipe", "pipe"],
78
79
  });
79
80
  if (child.stdin === null || child.stdout === null || child.stderr === null)
@@ -1,3 +1,6 @@
1
+ import { AcpProbeTransport } from "./acp-probe-transport.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dslog } from "../slog.js";
1
4
  import { once } from "node:events";
2
5
  import { isAbsolute } from "node:path";
3
6
  import { parseArgs } from "node:util";
@@ -6,10 +9,16 @@ import { pathToFileURL } from "node:url";
6
9
  import spawn from "cross-spawn";
7
10
  import { PROTOCOL_VERSION, RequestError, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
8
11
  import { augmentedPath } from "../runtime-path.js";
12
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
9
13
  import { startFirstProgressWatchdog } from "./progress-watchdog.js";
10
14
  import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
11
15
  const ERROR_MESSAGE_CAP = 2_000;
12
- const PROBE_TIMEOUT_MS = 5_000;
16
+ // macOS launchd cold-starts ACP runtimes materially slower than an interactive
17
+ // shell. DeepSeek Harness was measured at ~4.4s before returning initialize
18
+ // under launchd, while the daemon also creates an isolated profile. Keep this
19
+ // bounded below the control-plane request budget, with the existing 1s/2s
20
+ // Harness retries providing the outer bound.
21
+ export const ACP_PROBE_TIMEOUT_MS = 10_000;
13
22
  function jsonLine(event) {
14
23
  if (process.stdout.write(`${JSON.stringify(event)}\n`))
15
24
  return Promise.resolve();
@@ -127,10 +136,10 @@ function deepSeekPermissionMode(permission) {
127
136
  return "workspace-write";
128
137
  return "danger-full-access";
129
138
  }
130
- function providerEnv(provider, permission) {
139
+ function providerEnv(provider, permission, env = process.env) {
131
140
  return provider === "deepseek-harness"
132
- ? { ...process.env, DSH_PERMISSION_MODE: deepSeekPermissionMode(permission) }
133
- : process.env;
141
+ ? { ...env, DSH_PERMISSION_MODE: deepSeekPermissionMode(permission) }
142
+ : env;
134
143
  }
135
144
  function providerDisplayName(provider) {
136
145
  if (provider === "deepseek-harness")
@@ -193,18 +202,48 @@ async function stopChild(child) {
193
202
  await closed;
194
203
  }
195
204
  }
205
+ export class AcpProbeCleanupError extends Error {
206
+ constructor() { super("ACP probe process cleanup unconfirmed"); this.name = "AcpProbeCleanupError"; }
207
+ }
196
208
  /** Probe the ACP transport without starting a session or forcing an optional interactive login flow. */
197
209
  export async function probeKimiAcp(options, spawnProcess = spawn) {
198
210
  const provider = options.provider ?? "kimi";
199
211
  const permission = options.effectivePermission ?? "sandboxed";
200
- const child = spawnProcess(options.bin, providerArgs(provider, options.configPath), {
201
- cwd: process.cwd(),
202
- // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
203
- env: { ...providerEnv(provider, permission), PATH: augmentedPath() },
212
+ const probeId = options.probeId ?? randomUUID();
213
+ const started = performance.now();
214
+ const elapsed = () => Math.round(performance.now() - started);
215
+ const transport = new AcpProbeTransport(elapsed);
216
+ let spawnConfirmedMs;
217
+ let reason = "initializing", protocolVersion;
218
+ let stderrSeen = false;
219
+ const fields = () => ({ probe_id: probeId, runtime: provider, duration_ms: elapsed(),
220
+ ...transport.fields(), spawn_confirmed_ms: spawnConfirmedMs, child_pid: child.pid,
221
+ reason, protocol_version: protocolVersion, stderr_seen: stderrSeen, aborted: options.signal?.aborted ?? false });
222
+ dslog("execution.acp_probe_started", "ACP 探测开始", { probe_id: probeId, runtime: provider, node_version: process.version });
223
+ const sourceEnv = options.env ?? { ...process.env, PATH: augmentedPath() };
224
+ const launchEnv = options.launchEnv ?? providerEnv(provider, permission, sourceEnv);
225
+ const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
226
+ cwd: options.launchCwd ?? process.cwd(),
227
+ // 显式 launchEnv 已准备好 PATH/HOME,不能二次补 PATH 或混入父进程环境。
228
+ env: launchEnv,
204
229
  stdio: ["pipe", "pipe", "pipe"],
205
230
  });
231
+ // spawn errors are emitted by ChildProcess, not its stdio streams. A missing
232
+ // launcher must fail admission, not become an uncaught daemon exception.
233
+ const spawnFailed = new Promise((resolve) => {
234
+ child.once("error", () => {
235
+ if (reason === "initializing")
236
+ reason = "spawn_error";
237
+ resolve(false);
238
+ });
239
+ });
240
+ child.once("spawn", () => {
241
+ spawnConfirmedMs = elapsed();
242
+ dslog("execution.acp_probe_spawned", "ACP 探测子进程已启动", fields());
243
+ });
206
244
  if (child.stdin === null || child.stdout === null || child.stderr === null)
207
245
  return false;
246
+ child.stderr.on("data", () => { stderrSeen = true; });
208
247
  child.stderr.resume();
209
248
  const app = client({ name: `nowcrew-daemon-${provider}-probe` })
210
249
  .onRequest(methods.client.session.requestPermission, () => ({
@@ -214,25 +253,37 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
214
253
  let timeout;
215
254
  let onAbort;
216
255
  try {
217
- const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
256
+ const stream = ndJsonStream(transport.writable(child.stdin), transport.readable(child.stdout));
218
257
  const connected = app.connectWith(stream, async (context) => {
219
- await context.request(methods.agent.initialize, {
258
+ transport.initializeRequested();
259
+ const initialized = await context.request(methods.agent.initialize, {
220
260
  protocolVersion: PROTOCOL_VERSION,
221
261
  clientCapabilities: {},
222
262
  clientInfo: { name: "nowcrew-daemon", version: "1" },
223
263
  });
224
- return true;
264
+ transport.initializeResponse();
265
+ protocolVersion = initialized.protocolVersion;
266
+ const ready = protocolVersion === PROTOCOL_VERSION;
267
+ if (reason === "initializing")
268
+ reason = ready ? "ready" : "protocol_mismatch";
269
+ dslog("execution.acp_probe_handshake", "ACP 握手结果", { ...fields(), ready });
270
+ return ready;
225
271
  });
226
272
  const result = await Promise.race([
227
273
  connected,
274
+ spawnFailed,
228
275
  new Promise((resolve) => {
229
276
  timeout = setTimeout(() => {
277
+ if (reason === "initializing")
278
+ reason = "timeout";
230
279
  void stopChild(child);
231
280
  resolve(false);
232
- }, PROBE_TIMEOUT_MS);
281
+ }, ACP_PROBE_TIMEOUT_MS);
233
282
  }),
234
283
  new Promise((resolve) => {
235
284
  onAbort = () => {
285
+ if (reason === "initializing")
286
+ reason = "aborted";
236
287
  void stopChild(child);
237
288
  resolve(false);
238
289
  };
@@ -245,6 +296,8 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
245
296
  return result;
246
297
  }
247
298
  catch {
299
+ if (reason === "initializing")
300
+ reason = options.signal?.aborted ? "aborted" : "transport_error";
248
301
  return false;
249
302
  }
250
303
  finally {
@@ -252,17 +305,33 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
252
305
  clearTimeout(timeout);
253
306
  if (onAbort !== undefined)
254
307
  options.signal?.removeEventListener("abort", onAbort);
255
- await stopChild(child);
308
+ dslog("execution.acp_probe_cleanup_started", "ACP 探测清理开始", fields());
309
+ let cleanupFinished = false;
310
+ try {
311
+ await stopChild(child);
312
+ cleanupFinished = true;
313
+ }
314
+ catch {
315
+ throw new AcpProbeCleanupError();
316
+ }
317
+ finally {
318
+ dslog("execution.acp_probe_finished", "ACP 探测结束", { ...fields(),
319
+ cleanup_finished: cleanupFinished, exit_code: child.exitCode, exit_signal: child.signalCode,
320
+ level: reason === "ready" && cleanupFinished ? "INFO" : "WARN" });
321
+ }
256
322
  }
257
323
  }
258
- export async function runKimiAcp(options) {
324
+ export async function runKimiAcp(requestedOptions) {
325
+ // Until this adapter verifies native restoration, thread-bound runs are cold.
326
+ const options = process.env.CREW_THREAD_ROOT ? { ...requestedOptions, resume: false } : requestedOptions;
259
327
  const provider = options.provider ?? "kimi";
260
328
  const displayName = providerDisplayName(provider);
261
329
  const permission = options.effectivePermission ?? "full_access";
262
330
  if (provider === "deepseek-harness" && options.resume) {
263
331
  throw new Error("DeepSeek Harness ACP does not support session resume");
264
332
  }
265
- const prompt = await readPrompt();
333
+ const prompt = (await prepareThreadDelivery({ env: process.env, resumed: false,
334
+ sessionId: "unverified-cold-acp", wakePrompt: await readPrompt() })).prompt;
266
335
  const child = spawn(options.bin, providerArgs(provider, options.configPath), {
267
336
  cwd: process.cwd(),
268
337
  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;
@@ -11,7 +11,7 @@ const REQUIRED_RUN_FLAGS = [
11
11
  "--variant",
12
12
  "--session",
13
13
  ];
14
- export async function probeOpenCodeRun(signal) {
14
+ export async function probeOpenCodeRun(signal, env = { ...process.env, PATH: augmentedPath() }) {
15
15
  try {
16
16
  const { stdout, stderr } = await execFileP("opencode", ["run", "--help"], {
17
17
  shell: isWin(),
@@ -19,7 +19,7 @@ export async function probeOpenCodeRun(signal) {
19
19
  killSignal: "SIGKILL",
20
20
  maxBuffer: 1024 * 1024,
21
21
  signal,
22
- env: { ...process.env, PATH: augmentedPath() },
22
+ env,
23
23
  });
24
24
  const help = `${stdout}\n${stderr}`;
25
25
  return REQUIRED_RUN_FLAGS.every((flag) => help.includes(flag));