@nowcrew/daemon 0.6.88-beta.2 → 0.6.88-beta.21

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 (81) hide show
  1. package/README.md +43 -13
  2. package/dist/agent-memory/policy.js +3 -1
  3. package/dist/computer-profile.js +9 -4
  4. package/dist/config.js +0 -9
  5. package/dist/daemon-update-eligibility.js +1 -1
  6. package/dist/execution-journal.js +9 -6
  7. package/dist/execution-protocol.js +2 -1
  8. package/dist/execution-runner.js +5 -4
  9. package/dist/list-models.js +2 -0
  10. package/dist/local-execution-lease.js +22 -0
  11. package/dist/local-executor.js +58 -45
  12. package/dist/local-memory/candidate.js +43 -0
  13. package/dist/local-memory/index-budget.js +8 -0
  14. package/dist/local-memory/inventory-cli.js +61 -0
  15. package/dist/local-memory/inventory-preview.js +154 -0
  16. package/dist/local-memory/inventory-source.js +159 -0
  17. package/dist/local-memory/pending-match.js +29 -0
  18. package/dist/local-memory/pending-query.js +100 -0
  19. package/dist/local-memory/pending-schema.js +29 -0
  20. package/dist/local-memory/pending-source.js +126 -0
  21. package/dist/local-memory/pending.js +34 -84
  22. package/dist/local-memory/prompt.js +14 -11
  23. package/dist/local-memory/read-cli.js +97 -12
  24. package/dist/local-memory/runtime.js +24 -17
  25. package/dist/local-memory/work-log-execution.js +31 -0
  26. package/dist/local-memory/work-log.js +58 -0
  27. package/dist/local-memory/write.js +27 -58
  28. package/dist/machine-info.js +54 -17
  29. package/dist/main.js +4 -69
  30. package/dist/native-thread-context.js +59 -0
  31. package/dist/pre-admission-rejection.js +64 -0
  32. package/dist/project-skills/execution-adapter.js +3 -2
  33. package/dist/project-skills/runtime-launch.js +14 -0
  34. package/dist/prompt.js +15 -15
  35. package/dist/provider-env.js +3 -6
  36. package/dist/runner.js +14 -16
  37. package/dist/runtime-probe.js +34 -5
  38. package/dist/runtimes/acp-probe-transport.js +69 -0
  39. package/dist/runtimes/claude-session-start-hook.js +24 -0
  40. package/dist/runtimes/claude-thread-runner.js +266 -0
  41. package/dist/runtimes/claude.js +23 -4
  42. package/dist/runtimes/codex-app-server-runner.js +43 -4
  43. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  44. package/dist/runtimes/deepseek-harness-runner.js +224 -0
  45. package/dist/runtimes/deepseek-harness.js +64 -11
  46. package/dist/runtimes/harness-probe-retry.js +42 -0
  47. package/dist/runtimes/kimi-acp-runner.js +63 -10
  48. package/dist/runtimes/opencode-runner.js +7 -1
  49. package/dist/serve.js +59 -101
  50. package/dist/session.js +4 -0
  51. package/dist/storage-identity.js +0 -3
  52. package/dist/storage-serve-adapter.js +6 -72
  53. package/dist/supervised-runtime.js +28 -18
  54. package/dist/thread-input-delivery.js +217 -0
  55. package/package.json +2 -3
  56. package/dist/codex-home-launch.js +0 -7
  57. package/dist/codex-home-mode-controller.js +0 -68
  58. package/dist/daemon-migration-controller.js +0 -211
  59. package/dist/daemon-migration-wiring.js +0 -22
  60. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  61. package/dist/runtimes/codex-home-migration.js +0 -112
  62. package/dist/runtimes/codex-home.js +0 -236
  63. package/dist/storage-control.js +0 -212
  64. package/dist/storage-copy.js +0 -628
  65. package/dist/storage-credential-guard.js +0 -21
  66. package/dist/storage-doctor.js +0 -228
  67. package/dist/storage-manifest.js +0 -1027
  68. package/dist/storage-migration-controller.js +0 -440
  69. package/dist/storage-migration-manifest.js +0 -105
  70. package/dist/storage-migration-policy.js +0 -100
  71. package/dist/storage-runtime-controller.js +0 -381
  72. package/dist/storage-runtime-readiness.js +0 -240
  73. package/dist/storage-safe-fs.js +0 -302
  74. package/dist/storage-status-delivery.js +0 -224
  75. package/dist/storage-status-replay.js +0 -140
  76. package/dist/storage-target-confirmation.js +0 -129
  77. package/dist/storage-worker-command.js +0 -37
  78. package/dist/storage-worker-executor.js +0 -449
  79. package/dist/storage-worker-handoff.js +0 -151
  80. package/dist/storage-worker-operation.js +0 -294
  81. package/dist/storage-worker-status.js +0 -259
@@ -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`);
@@ -0,0 +1,92 @@
1
+ /** 短命执行配置:不读取全局 Harness home,不新增 daemon 状态。 */
2
+ import { mkdtemp, mkdir, writeFile, access } from "node:fs/promises";
3
+ import { join, isAbsolute } from "node:path";
4
+ import { stringify } from "yaml";
5
+ export const harnessPermissionMode = (permission) => permission === "sandboxed"
6
+ ? "read-only"
7
+ : permission === "workspace_write"
8
+ ? "workspace-write"
9
+ : "danger-full-access";
10
+ // A variable's returned value is interpolated once: literal {{...}} in NowWork
11
+ // instructions must not become unknown Harness template variables.
12
+ const instructionsPlugin = `export const name = 'nowwork-instructions';
13
+ export const inject = ['systemPrompt'];
14
+ export function apply(ctx, config) {
15
+ ctx.systemPrompt.variable('nowwork_instructions', () => config.text);
16
+ ctx.systemPrompt.section({name: 'nowwork-instructions', order: 100000, text: '{{nowwork_instructions}}'});
17
+ }
18
+ `;
19
+ export async function prepareHarnessProfile(input) {
20
+ if (![input.stateRoot, input.cwd, ...input.skillRoots].every(isAbsolute)) {
21
+ throw new Error("DeepSeek Harness execution paths must be absolute");
22
+ }
23
+ if (input.patchPath !== undefined) {
24
+ if (!isAbsolute(input.patchPath))
25
+ throw new Error("DeepSeek Harness patch must be absolute");
26
+ await access(input.patchPath);
27
+ }
28
+ await mkdir(input.stateRoot, { recursive: true, mode: 0o700 });
29
+ const home = await mkdtemp(join(input.stateRoot, ".deepseek-harness-"));
30
+ const pluginPath = join(home, "nowwork-instructions.mjs");
31
+ const policyPath = join(home, "nowwork-policy.yml");
32
+ const mode = harnessPermissionMode(input.effectivePermission);
33
+ await writeFile(pluginPath, instructionsPlugin, { mode: 0o600 });
34
+ await writeFile(policyPath, stringify([
35
+ {
36
+ id: "sandbox-policy",
37
+ disabled: false,
38
+ config: { mode, workspaceRoot: input.cwd },
39
+ },
40
+ { id: "approval", disabled: false, config: { policy: "never" } },
41
+ {
42
+ id: "permission",
43
+ disabled: false,
44
+ config: {
45
+ defaultPreset: "nowwork",
46
+ presets: { nowwork: { sandbox: mode, approval: "never" } },
47
+ },
48
+ },
49
+ {
50
+ id: "skill-filesystem",
51
+ disabled: false,
52
+ config: {
53
+ includeDefaultRoots: false,
54
+ customSkillDirs: [...input.skillRoots],
55
+ watch: false,
56
+ },
57
+ },
58
+ {
59
+ id: "session-persistence-jsonl",
60
+ disabled: false,
61
+ config: { root: join(home, "sessions") },
62
+ },
63
+ {
64
+ insert: [
65
+ {
66
+ id: "nowwork-instructions",
67
+ name: pluginPath,
68
+ config: { text: input.systemPrompt },
69
+ },
70
+ ],
71
+ },
72
+ ]), { mode: 0o600 });
73
+ const env = Object.fromEntries(Object.entries(input.env).filter(([key]) => !key.toUpperCase().startsWith("DSH_")));
74
+ return {
75
+ home,
76
+ policyPath,
77
+ args: [
78
+ "--profile",
79
+ "acp",
80
+ ...(input.patchPath ? ["--patch", input.patchPath] : []),
81
+ "--patch",
82
+ policyPath,
83
+ ],
84
+ env: {
85
+ ...env,
86
+ DSH_HOME: home,
87
+ DSH_AGENTS_HOME: join(home, "agents-home"),
88
+ DSH_PERMISSION_MODE: mode,
89
+ DSH_TELEMETRY_DISABLED: "1",
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,224 @@
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 { prepareHarnessProfile } from "./deepseek-harness-profile.js";
11
+ import { mapAcpUpdate } from "./kimi-acp-runner.js";
12
+ import { prepareThreadDelivery } from "../thread-input-delivery.js";
13
+ const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
14
+ export const HarnessInputSchema = z
15
+ .object({
16
+ systemPrompt: z.string(),
17
+ wakePrompt: z.string().min(1),
18
+ stateRoot: absolutePath,
19
+ skillRoots: z.array(absolutePath).default([]),
20
+ effectivePermission: z.enum([
21
+ "sandboxed",
22
+ "workspace_write",
23
+ "full_access",
24
+ ]),
25
+ })
26
+ .strict();
27
+ const emit = async (event) => {
28
+ if (!process.stdout.write(`${JSON.stringify(event)}\n`))
29
+ await once(process.stdout, "drain");
30
+ };
31
+ async function settleWithin(promise, ms) {
32
+ let timer;
33
+ try {
34
+ return await Promise.race([
35
+ promise,
36
+ new Promise((resolve) => {
37
+ timer = setTimeout(() => resolve(undefined), ms);
38
+ }),
39
+ ]);
40
+ }
41
+ finally {
42
+ if (timer)
43
+ clearTimeout(timer);
44
+ }
45
+ }
46
+ export async function runHarness(input, bin, patchPath) {
47
+ const delivery = await prepareThreadDelivery({
48
+ env: process.env,
49
+ resumed: false,
50
+ sessionId: "cold-harness",
51
+ wakePrompt: input.wakePrompt,
52
+ });
53
+ const profile = await prepareHarnessProfile({
54
+ ...input,
55
+ cwd: process.cwd(),
56
+ env: process.env,
57
+ ...(patchPath ? { patchPath } : {}),
58
+ });
59
+ const child = spawn(bin, profile.args, {
60
+ cwd: profile.home,
61
+ env: profile.env,
62
+ stdio: ["pipe", "pipe", "pipe"],
63
+ });
64
+ // Boot away from the repository: the CLI must not load a project's .env into
65
+ // trusted process credentials. ACP session/new carries the real workspace.
66
+ let spawnError;
67
+ const closed = new Promise((resolve) => {
68
+ child.once("error", (error) => {
69
+ spawnError = error;
70
+ resolve();
71
+ });
72
+ child.once("close", () => resolve());
73
+ });
74
+ if (!child.stdin || !child.stdout || !child.stderr)
75
+ throw new Error("DeepSeek Harness stdio unavailable");
76
+ // Provider diagnostics may include secrets or prompts; do not stream raw stderr.
77
+ child.stderr.resume();
78
+ let context;
79
+ let sessionId;
80
+ let cancelled = false;
81
+ let stage = "initialize";
82
+ let exitCode = 1;
83
+ let stopping;
84
+ const stop = () => (stopping ??= (async () => {
85
+ child.stdin?.end();
86
+ if (await settleWithin(closed.then(() => true), 2000))
87
+ return;
88
+ child.kill("SIGTERM");
89
+ if (await settleWithin(closed.then(() => true), 1000))
90
+ return;
91
+ child.kill("SIGKILL");
92
+ if (!(await settleWithin(closed.then(() => true), 2000)))
93
+ throw new Error("DeepSeek Harness process stop unconfirmed");
94
+ })());
95
+ const cancel = () => {
96
+ cancelled = true;
97
+ void (async () => {
98
+ if (context && sessionId)
99
+ await settleWithin(context
100
+ .notify(methods.agent.session.cancel, { sessionId })
101
+ .catch(() => undefined), 1000);
102
+ await stop();
103
+ })().catch(() => {
104
+ /* finally reports stop failure; supervisor owns process tree */
105
+ });
106
+ };
107
+ process.once("SIGTERM", cancel);
108
+ process.once("SIGINT", cancel);
109
+ const app = client({ name: "nowwork-harness" })
110
+ .onRequest(methods.client.session.requestPermission, () => ({
111
+ outcome: { outcome: "cancelled" },
112
+ }))
113
+ .onNotification(methods.client.session.update, async ({ params }) => {
114
+ if (params.sessionId !== sessionId)
115
+ return;
116
+ for (const event of mapAcpUpdate("deepseek-harness", params.update))
117
+ await emit(event);
118
+ });
119
+ try {
120
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
121
+ const result = await app.connectWith(stream, async (ctx) => {
122
+ context = ctx;
123
+ const initialized = await ctx.request(methods.agent.initialize, {
124
+ protocolVersion: PROTOCOL_VERSION,
125
+ clientCapabilities: {},
126
+ clientInfo: { name: "nowwork", version: "1" },
127
+ });
128
+ if (initialized.protocolVersion !== PROTOCOL_VERSION)
129
+ throw new Error("Incompatible ACP protocol");
130
+ if (cancelled)
131
+ throw new Error("cancelled");
132
+ stage = "session_new";
133
+ const session = await ctx.request(methods.agent.session.new, {
134
+ cwd: process.cwd(),
135
+ mcpServers: [],
136
+ });
137
+ sessionId = session.sessionId;
138
+ if (!sessionId || cancelled)
139
+ throw new Error("Harness session unavailable");
140
+ await emit({ type: "thread.started", thread_id: sessionId });
141
+ stage = "prompt";
142
+ const response = await ctx.request(methods.agent.session.prompt, {
143
+ sessionId,
144
+ prompt: [{ type: "text", text: delivery.prompt }],
145
+ });
146
+ if (initialized.agentCapabilities?.sessionCapabilities?.close != null) {
147
+ stage = "session_close";
148
+ const close = await settleWithin(ctx
149
+ .request(methods.agent.session.close, { sessionId })
150
+ .then(() => true), 5000);
151
+ if (!close)
152
+ throw new Error("Harness session close timed out");
153
+ }
154
+ return response;
155
+ });
156
+ const usage = result.usage;
157
+ if (!cancelled)
158
+ await emit({
159
+ type: "turn.completed",
160
+ stop_reason: result.stopReason,
161
+ ...(usage
162
+ ? {
163
+ usage: {
164
+ input_tokens: usage.inputTokens,
165
+ output_tokens: usage.outputTokens,
166
+ cache_read_input_tokens: usage.cachedReadTokens ?? 0,
167
+ cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
168
+ },
169
+ }
170
+ : {}),
171
+ });
172
+ exitCode =
173
+ result.stopReason === "end_turn"
174
+ ? 0
175
+ : result.stopReason === "cancelled"
176
+ ? 130
177
+ : 1;
178
+ }
179
+ catch {
180
+ if (!cancelled)
181
+ process.stderr.write(spawnError
182
+ ? "DeepSeek Harness launch failed; verify the installed dsh executable.\n"
183
+ : `DeepSeek Harness ACP execution failed at ${stage}; verify the pinned version, machine credentials and profile patch.\n`);
184
+ }
185
+ finally {
186
+ try {
187
+ await stop();
188
+ }
189
+ finally {
190
+ process.off("SIGTERM", cancel);
191
+ process.off("SIGINT", cancel);
192
+ }
193
+ }
194
+ if (cancelled)
195
+ return 130;
196
+ return child.exitCode !== 0 ? 1 : exitCode;
197
+ }
198
+ async function main() {
199
+ const { values } = parseArgs({
200
+ options: { bin: { type: "string" }, patch: { type: "string" } },
201
+ });
202
+ if (!values.bin)
203
+ throw new Error("DeepSeek Harness --bin is required");
204
+ process.stdin.setEncoding("utf8");
205
+ let text = "";
206
+ for await (const chunk of process.stdin) {
207
+ text += String(chunk);
208
+ if (Buffer.byteLength(text) > 16 * 1024 * 1024)
209
+ throw new Error("Harness input exceeds 16 MiB");
210
+ }
211
+ const input = HarnessInputSchema.parse(JSON.parse(text));
212
+ return runHarness(input, values.bin, values.patch);
213
+ }
214
+ if (process.argv[1] &&
215
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
216
+ main()
217
+ .then((code) => {
218
+ process.exitCode = code;
219
+ })
220
+ .catch(() => {
221
+ process.stderr.write("DeepSeek Harness configuration or cleanup failed; check execution paths and machine patch.\n");
222
+ process.exitCode = 1;
223
+ });
224
+ }
@@ -1,20 +1,73 @@
1
1
  import spawn from "cross-spawn";
2
2
  import { isAbsolute } from "node:path";
3
- import { probeKimiAcp } from "./kimi-acp-runner.js";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
7
+ import { probeKimiAcp, AcpProbeCleanupError } from "./kimi-acp-runner.js";
4
8
  export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
9
+ export const DEEPSEEK_HARNESS_PATCH_ENV = "NOWCREW_DEEPSEEK_HARNESS_PATCH";
10
+ export function deepSeekHarnessLauncher(env = process.env) {
11
+ const configPath = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
12
+ const patchPath = env[DEEPSEEK_HARNESS_PATCH_ENV]?.trim();
13
+ if (configPath && !isAbsolute(configPath))
14
+ throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
15
+ if (patchPath && !isAbsolute(patchPath))
16
+ throw new Error(`${DEEPSEEK_HARNESS_PATCH_ENV} must be an absolute path`);
17
+ if (configPath && patchPath)
18
+ throw new Error("DeepSeek Harness legacy config and profile patch are mutually exclusive");
19
+ return configPath
20
+ ? { kind: "legacy", bin: "dsh-acp-demo", configPath }
21
+ : { kind: "profile", bin: "dsh", ...(patchPath ? { patchPath } : {}) };
22
+ }
5
23
  export function deepSeekHarnessConfigPath(env = process.env) {
6
24
  const path = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
7
25
  return path && isAbsolute(path) ? path : null;
8
26
  }
9
27
  /** 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);
28
+ export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
29
+ if (options.signal?.aborted)
30
+ return false;
31
+ let root;
32
+ try {
33
+ const launcher = options.configPath === undefined
34
+ ? deepSeekHarnessLauncher()
35
+ : deepSeekHarnessLauncher({
36
+ [DEEPSEEK_HARNESS_CONFIG_ENV]: options.configPath,
37
+ });
38
+ if (launcher.kind === "legacy")
39
+ return await probeKimiAcp({
40
+ ...options,
41
+ provider: "deepseek-harness",
42
+ configPath: launcher.configPath,
43
+ effectivePermission: "sandboxed",
44
+ }, spawnProcess);
45
+ root = await mkdtemp(join(tmpdir(), "nowwork-dsh-probe-"));
46
+ const profile = await prepareHarnessProfile({
47
+ stateRoot: root,
48
+ cwd: root,
49
+ systemPrompt: "",
50
+ skillRoots: [],
51
+ effectivePermission: "sandboxed",
52
+ env: process.env,
53
+ ...(launcher.patchPath ? { patchPath: launcher.patchPath } : {}),
54
+ });
55
+ return await probeKimiAcp({
56
+ ...options,
57
+ provider: "deepseek-harness",
58
+ effectivePermission: "sandboxed",
59
+ launchArgs: profile.args,
60
+ launchEnv: profile.env,
61
+ launchCwd: root,
62
+ }, spawnProcess);
63
+ }
64
+ catch (error) {
65
+ if (error instanceof AcpProbeCleanupError)
66
+ throw error;
67
+ return false;
68
+ }
69
+ finally {
70
+ if (root)
71
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
72
+ }
20
73
  }
@@ -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
+ }