@nowcrew/daemon 0.5.18 → 0.5.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.
Files changed (41) hide show
  1. package/README.md +23 -0
  2. package/dist/attachments.js +196 -0
  3. package/dist/computer-cli.js +72 -12
  4. package/dist/computer-profile-lock.js +395 -0
  5. package/dist/computer-profile.js +189 -20
  6. package/dist/config.js +2 -1
  7. package/dist/console.js +175 -9
  8. package/dist/execution-event-limit.js +1 -1
  9. package/dist/execution-journal-lock.js +199 -40
  10. package/dist/execution-journal.js +42 -4
  11. package/dist/execution-protocol.js +21 -1
  12. package/dist/execution-recovery.js +71 -0
  13. package/dist/execution-runner.js +68 -77
  14. package/dist/execution-supervisor.js +79 -31
  15. package/dist/external-output.js +114 -0
  16. package/dist/i18n.js +5 -5
  17. package/dist/list-models.js +41 -5
  18. package/dist/local-executor.js +103 -14
  19. package/dist/machine-info.js +6 -1
  20. package/dist/main.js +23 -8
  21. package/dist/origin-decision.js +3 -1
  22. package/dist/prompt.js +4 -1
  23. package/dist/runner.js +14 -9
  24. package/dist/runtime-cancellation.js +74 -0
  25. package/dist/runtime-capabilities.js +38 -0
  26. package/dist/runtime-path.js +60 -0
  27. package/dist/runtimes/claude.js +9 -4
  28. package/dist/runtimes/codex-app-server-runner.js +340 -0
  29. package/dist/runtimes/codex.js +10 -4
  30. package/dist/runtimes/kimi-acp-runner.js +117 -17
  31. package/dist/runtimes/kimi.js +2 -0
  32. package/dist/runtimes/progress-watchdog.js +26 -0
  33. package/dist/serve-lifecycle.js +82 -0
  34. package/dist/serve.js +212 -212
  35. package/dist/session.js +1 -1
  36. package/dist/shared-execution-slots.js +68 -0
  37. package/dist/shutdown-deadline.js +32 -0
  38. package/dist/slog.js +34 -20
  39. package/dist/supervised-runtime.js +104 -0
  40. package/dist/websocket-shutdown.js +53 -0
  41. package/package.json +3 -3
@@ -4,6 +4,9 @@ import { Readable, Writable } from "node:stream";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import spawn from "cross-spawn";
6
6
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
7
+ import { augmentedPath } from "../runtime-path.js";
8
+ import { startFirstProgressWatchdog } from "./progress-watchdog.js";
9
+ import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
7
10
  const ERROR_MESSAGE_CAP = 2_000;
8
11
  const PROBE_TIMEOUT_MS = 5_000;
9
12
  function jsonLine(event) {
@@ -60,6 +63,17 @@ function safeErrorMessage(error, prompt) {
60
63
  const redacted = prompt && raw.includes(prompt) ? raw.replaceAll(prompt, "[prompt redacted]") : raw;
61
64
  return redacted.slice(0, ERROR_MESSAGE_CAP);
62
65
  }
66
+ export function isKimiAuthenticationRequired(error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return /\bauthentication required\b/i.test(message);
69
+ }
70
+ export function kimiResumeMethod(capabilities) {
71
+ if (capabilities?.sessionCapabilities?.resume != null)
72
+ return "resume";
73
+ if (capabilities?.loadSession)
74
+ return "load";
75
+ return null;
76
+ }
63
77
  /** Full access may approve an operation, but it must never fabricate an answer to an agent question. */
64
78
  export function selectKimiPermission(params) {
65
79
  const allowOnce = params.options.filter((option) => option.kind === "allow_once");
@@ -100,11 +114,12 @@ async function stopChild(child) {
100
114
  await closed;
101
115
  }
102
116
  }
103
- /** A help banner is insufficient: Kimi's authenticate RPC re-checks its token without creating a session. */
117
+ /** Probe the ACP transport without starting a session or forcing an optional interactive login flow. */
104
118
  export async function probeKimiAcp(options, spawnProcess = spawn) {
105
119
  const child = spawnProcess(options.bin, ["acp"], {
106
120
  cwd: process.cwd(),
107
- env: process.env,
121
+ // probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
122
+ env: { ...process.env, PATH: augmentedPath() },
108
123
  stdio: ["pipe", "pipe", "pipe"],
109
124
  });
110
125
  if (child.stdin === null || child.stdout === null || child.stderr === null)
@@ -119,15 +134,11 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
119
134
  try {
120
135
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
121
136
  const connected = app.connectWith(stream, async (context) => {
122
- const initialized = await context.request(methods.agent.initialize, {
137
+ await context.request(methods.agent.initialize, {
123
138
  protocolVersion: PROTOCOL_VERSION,
124
139
  clientCapabilities: {},
125
140
  clientInfo: { name: "nowcrew-daemon", version: "1" },
126
141
  });
127
- const authMethod = initialized.authMethods?.[0];
128
- if (authMethod !== undefined) {
129
- await context.request(methods.agent.authenticate, { methodId: authMethod.id });
130
- }
131
142
  return true;
132
143
  });
133
144
  const result = await Promise.race([
@@ -157,12 +168,15 @@ export async function runKimiAcp(options) {
157
168
  env: process.env,
158
169
  stdio: ["pipe", "pipe", "pipe"],
159
170
  });
171
+ let runtimeChild = child;
160
172
  if (child.stdin === null || child.stdout === null || child.stderr === null) {
161
173
  throw new Error("Kimi ACP process did not expose stdio");
162
174
  }
163
175
  child.stderr.pipe(process.stderr, { end: false });
164
176
  let context = null;
165
177
  let sessionId = null;
178
+ let acpSemanticProgress = false;
179
+ let progressTimedOut = false;
166
180
  let cancelling = false;
167
181
  const cancel = async () => {
168
182
  if (cancelling)
@@ -171,7 +185,7 @@ export async function runKimiAcp(options) {
171
185
  if (context !== null && sessionId !== null) {
172
186
  await context.notify(methods.agent.session.cancel, { sessionId }).catch(() => undefined);
173
187
  }
174
- await stopChild(child);
188
+ await stopChild(runtimeChild);
175
189
  };
176
190
  const onSignal = () => {
177
191
  void cancel().finally(() => process.exit(130));
@@ -181,9 +195,13 @@ export async function runKimiAcp(options) {
181
195
  const app = client({ name: "nowcrew-daemon-kimi" })
182
196
  .onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params))
183
197
  .onNotification(methods.client.session.update, async ({ params }) => {
198
+ acpSemanticProgress = true;
199
+ firstProgress.observe();
184
200
  for (const event of mapKimiAcpUpdate(params.update))
185
201
  await jsonLine(event);
186
202
  });
203
+ let firstProgress = startFirstProgressWatchdog(() => undefined);
204
+ firstProgress.stop();
187
205
  try {
188
206
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
189
207
  const result = await app.connectWith(stream, async (nextContext) => {
@@ -196,15 +214,35 @@ export async function runKimiAcp(options) {
196
214
  if (process.env.CREW_KIMI_ACP_DEBUG === "1") {
197
215
  process.stderr.write(`Kimi ACP auth methods: ${JSON.stringify(initialized.authMethods ?? [])}\n`);
198
216
  }
199
- const authMethod = initialized.authMethods?.[0];
200
- if (authMethod !== undefined) {
201
- await nextContext.request(methods.agent.authenticate, { methodId: authMethod.id });
217
+ if (options.resume && options.sessionId) {
218
+ const resumeMethod = kimiResumeMethod(initialized.agentCapabilities);
219
+ if (resumeMethod === "resume") {
220
+ await nextContext.request(methods.agent.session.resume, {
221
+ sessionId: options.sessionId,
222
+ cwd: process.cwd(),
223
+ mcpServers: [],
224
+ });
225
+ }
226
+ else if (resumeMethod === "load") {
227
+ await nextContext.request(methods.agent.session.load, {
228
+ sessionId: options.sessionId,
229
+ cwd: process.cwd(),
230
+ mcpServers: [],
231
+ });
232
+ }
233
+ else {
234
+ throw new Error("Kimi ACP does not advertise session resume support");
235
+ }
236
+ sessionId = options.sessionId;
202
237
  }
203
- const session = await nextContext.request(methods.agent.session.new, {
204
- cwd: process.cwd(),
205
- mcpServers: [],
206
- });
207
- sessionId = session.sessionId;
238
+ else {
239
+ const session = await nextContext.request(methods.agent.session.new, {
240
+ cwd: process.cwd(),
241
+ mcpServers: [],
242
+ });
243
+ sessionId = session.sessionId;
244
+ }
245
+ await jsonLine({ type: "thread.started", thread_id: sessionId });
208
246
  if (options.model) {
209
247
  await nextContext.request(methods.agent.session.setConfigOption, {
210
248
  sessionId,
@@ -212,11 +250,20 @@ export async function runKimiAcp(options) {
212
250
  value: options.model,
213
251
  });
214
252
  }
253
+ firstProgress = startFirstProgressWatchdog(() => {
254
+ progressTimedOut = true;
255
+ void cancel();
256
+ });
215
257
  return nextContext.request(methods.agent.session.prompt, {
216
258
  sessionId,
217
259
  prompt: [{ type: "text", text: prompt }],
218
260
  });
219
261
  });
262
+ firstProgress.stop();
263
+ if (progressTimedOut) {
264
+ process.stderr.write("Kimi produced no semantic progress within the startup window\n");
265
+ return 1;
266
+ }
220
267
  const usage = result.usage;
221
268
  await jsonLine({
222
269
  type: "turn.completed",
@@ -233,10 +280,54 @@ export async function runKimiAcp(options) {
233
280
  return result.stopReason === "end_turn" ? 0 : result.stopReason === "cancelled" ? 130 : 1;
234
281
  }
235
282
  catch (error) {
283
+ if (progressTimedOut) {
284
+ process.stderr.write("Kimi produced no semantic progress within the startup window\n");
285
+ return 1;
286
+ }
287
+ if (!acpSemanticProgress && isKimiAuthenticationRequired(error)) {
288
+ firstProgress.stop();
289
+ await stopChild(child);
290
+ process.stderr.write("Kimi ACP requires account login; falling back to configured CLI provider transport\n");
291
+ assertKimiLegacyPromptFits(prompt);
292
+ const fallback = spawn(options.bin, buildKimiArgs({
293
+ wakePrompt: prompt,
294
+ effectivePermission: "full_access",
295
+ ...(options.model === undefined ? {} : { model: options.model }),
296
+ ...(options.resume && options.sessionId ? { sessionId: options.sessionId } : {}),
297
+ }), {
298
+ cwd: process.cwd(),
299
+ env: process.env,
300
+ stdio: ["ignore", "pipe", "pipe"],
301
+ });
302
+ runtimeChild = fallback;
303
+ if (fallback.stdout === null || fallback.stderr === null) {
304
+ throw new Error("Kimi CLI fallback did not expose output streams");
305
+ }
306
+ firstProgress = startFirstProgressWatchdog(() => {
307
+ progressTimedOut = true;
308
+ void stopChild(fallback);
309
+ });
310
+ fallback.stdout.on("data", () => firstProgress.observe());
311
+ fallback.stdout.pipe(process.stdout, { end: false });
312
+ fallback.stderr.pipe(process.stderr, { end: false });
313
+ const code = await new Promise((resolve, reject) => {
314
+ fallback.once("error", reject);
315
+ fallback.once("close", (exitCode, signal) => {
316
+ resolve(exitCode ?? (signal === null ? 1 : 128));
317
+ });
318
+ });
319
+ firstProgress.stop();
320
+ if (progressTimedOut) {
321
+ process.stderr.write("Kimi CLI fallback produced no output within the startup window\n");
322
+ return 1;
323
+ }
324
+ return code;
325
+ }
236
326
  process.stderr.write(`Kimi ACP execution failed: ${safeErrorMessage(error, prompt)}\n`);
237
327
  return 1;
238
328
  }
239
329
  finally {
330
+ firstProgress.stop();
240
331
  process.off("SIGTERM", onSignal);
241
332
  process.off("SIGINT", onSignal);
242
333
  await stopChild(child);
@@ -248,11 +339,20 @@ function optionsFromArgv(argv) {
248
339
  options: {
249
340
  bin: { type: "string" },
250
341
  model: { type: "string" },
342
+ session: { type: "string" },
343
+ resume: { type: "boolean", default: false },
251
344
  },
252
345
  });
253
346
  if (!values.bin)
254
347
  throw new Error("--bin is required");
255
- return { bin: values.bin, ...(values.model ? { model: values.model } : {}) };
348
+ if (values.resume && !values.session)
349
+ throw new Error("--resume requires --session");
350
+ return {
351
+ bin: values.bin,
352
+ ...(values.model ? { model: values.model } : {}),
353
+ ...(values.session ? { sessionId: values.session } : {}),
354
+ ...(values.resume ? { resume: true } : {}),
355
+ };
256
356
  }
257
357
  if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
258
358
  runKimiAcp(optionsFromArgv(process.argv.slice(2)))
@@ -29,6 +29,8 @@ export function buildKimiArgs(input) {
29
29
  const args = ["--output-format", "stream-json"];
30
30
  if (input.model)
31
31
  args.push("--model", input.model);
32
+ if (input.sessionId)
33
+ args.push("--session", input.sessionId);
32
34
  args.push("--prompt", input.wakePrompt);
33
35
  return args;
34
36
  }
@@ -0,0 +1,26 @@
1
+ export const DEFAULT_FIRST_PROGRESS_TIMEOUT_MS = 120_000;
2
+ /**
3
+ * Bound the silent gap after a protocol turn starts. Once any semantic notification arrives,
4
+ * the runtime's configured total timeout remains authoritative; long-running tools are not killed
5
+ * merely because they produce no output.
6
+ */
7
+ export function startFirstProgressWatchdog(onTimeout, timeoutMs = DEFAULT_FIRST_PROGRESS_TIMEOUT_MS) {
8
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
9
+ throw new RangeError("First-progress timeout must be a positive finite number");
10
+ }
11
+ let active = true;
12
+ const timer = setTimeout(() => {
13
+ if (!active)
14
+ return;
15
+ active = false;
16
+ onTimeout();
17
+ }, timeoutMs);
18
+ timer.unref?.();
19
+ const stop = () => {
20
+ if (!active)
21
+ return;
22
+ active = false;
23
+ clearTimeout(timer);
24
+ };
25
+ return { observe: stop, stop };
26
+ }
@@ -0,0 +1,82 @@
1
+ import { dslog, flushSlog } from "./slog.js";
2
+ function defaultDiagnose(diagnostic) {
3
+ const fields = {
4
+ signal: diagnostic.signal,
5
+ stage: diagnostic.stage,
6
+ active_execution_count: diagnostic.activeExecutionCount,
7
+ active_legacy_count: diagnostic.activeLegacyCount,
8
+ deadline_ms: diagnostic.deadlineMs,
9
+ ...(diagnostic.error === undefined ? {} : { error: diagnostic.error }),
10
+ };
11
+ try {
12
+ dslog(diagnostic.stage === "failed" ? "daemon.shutdown_failed" : "daemon.shutdown", `daemon shutdown ${diagnostic.stage}`, { level: diagnostic.stage === "failed" ? "ERROR" : "INFO", ...fields });
13
+ process.stderr.write(`${JSON.stringify({
14
+ level: diagnostic.stage === "failed" ? "ERROR" : "INFO",
15
+ event_type: diagnostic.stage === "failed" ? "daemon.shutdown_failed" : "daemon.shutdown",
16
+ ...fields,
17
+ })}\n`);
18
+ }
19
+ catch { /* shutdown diagnostics must never interrupt cleanup */ }
20
+ }
21
+ export async function runServeLifecycle(service, dependencies = {}) {
22
+ const signals = dependencies.signals ?? process;
23
+ const flush = dependencies.flush ?? flushSlog;
24
+ const diagnose = dependencies.diagnose ?? defaultDiagnose;
25
+ let receivedSignal = null;
26
+ let resolveSignal;
27
+ const signalReceived = new Promise((resolve) => { resolveSignal = resolve; });
28
+ const receive = (signal) => {
29
+ if (receivedSignal !== null)
30
+ return;
31
+ receivedSignal = signal;
32
+ resolveSignal(signal);
33
+ };
34
+ const onSigint = () => receive("SIGINT");
35
+ const onSigterm = () => receive("SIGTERM");
36
+ signals.on("SIGINT", onSigint);
37
+ signals.on("SIGTERM", onSigterm);
38
+ try {
39
+ try {
40
+ await service.ready;
41
+ }
42
+ catch (readyError) {
43
+ try {
44
+ await service.stop();
45
+ }
46
+ catch { /* preserve the readiness error */ }
47
+ try {
48
+ await flush();
49
+ }
50
+ catch { /* slog is best effort */ }
51
+ throw readyError;
52
+ }
53
+ const signal = await signalReceived;
54
+ const snapshot = service.shutdownSnapshot();
55
+ diagnose({ signal, stage: "stopping", ...snapshot });
56
+ let stopError;
57
+ try {
58
+ await service.stop();
59
+ diagnose({ signal, stage: "completed", ...snapshot });
60
+ }
61
+ catch (error) {
62
+ stopError = error;
63
+ diagnose({
64
+ signal,
65
+ stage: "failed",
66
+ error: error instanceof Error ? error.message : String(error),
67
+ ...snapshot,
68
+ });
69
+ }
70
+ try {
71
+ await flush();
72
+ }
73
+ catch { /* slog owns its own spool fallback */ }
74
+ if (stopError !== undefined)
75
+ throw stopError;
76
+ return { signal };
77
+ }
78
+ finally {
79
+ signals.off("SIGINT", onSigint);
80
+ signals.off("SIGTERM", onSigterm);
81
+ }
82
+ }