@nowcrew/daemon 0.6.36 → 0.6.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -19,6 +19,9 @@ export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
19
19
  maxStartingPerRuntime: 10,
20
20
  startupGapMs: 500,
21
21
  startupTimeoutMs: 120_000,
22
+ maxQueueWaitMs: 5 * 60_000,
23
+ maxFirstOutputWaitMs: 2 * 60_000,
24
+ firstOutputGraceMs: 3 * 60_000,
22
25
  });
23
26
  // Fits the largest mandatory v1 lifecycle envelope (UUID + timestamps + outcome facts) with margin.
24
27
  export const MIN_EXECUTION_EVENT_BYTES = 512;
@@ -85,6 +88,9 @@ export function loadConfig(env = process.env) {
85
88
  maxStartingPerRuntime: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_STARTING_PER_RUNTIME", DEFAULT_EXECUTION_LIMITS.maxStartingPerRuntime),
86
89
  startupGapMs: positiveIntegerEnv(env, "CREW_EXECUTION_START_GAP_MS", DEFAULT_EXECUTION_LIMITS.startupGapMs),
87
90
  startupTimeoutMs: positiveIntegerEnv(env, "CREW_EXECUTION_STARTUP_TIMEOUT_MS", DEFAULT_EXECUTION_LIMITS.startupTimeoutMs),
91
+ maxQueueWaitMs: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_QUEUE_WAIT_MS", DEFAULT_EXECUTION_LIMITS.maxQueueWaitMs),
92
+ maxFirstOutputWaitMs: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_FIRST_OUTPUT_WAIT_MS", DEFAULT_EXECUTION_LIMITS.maxFirstOutputWaitMs),
93
+ firstOutputGraceMs: positiveIntegerEnv(env, "CREW_EXECUTION_FIRST_OUTPUT_GRACE_MS", DEFAULT_EXECUTION_LIMITS.firstOutputGraceMs),
88
94
  });
89
95
  return {
90
96
  serverUrl,
@@ -9,6 +9,9 @@ const MIN_SIGNED_32_INTEGER = -2_147_483_648;
9
9
  const SequenceSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
10
10
  const TokenCountSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
11
11
  const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_INTEGER);
12
+ const ProviderErrorCategorySchema = z.enum(["auth", "quota", "rate_limit", "timeout", "network", "upstream", "cli", "unknown"]);
13
+ const ProviderHttpStatusSchema = z.number().int().min(100).max(599);
14
+ const ProviderErrorCodeSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9_.:-]+$/u);
12
15
  const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"]);
13
16
  const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
14
17
  const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
@@ -308,6 +311,9 @@ const RawExecutionCompletedSchema = z.object({
308
311
  terminationSignal: z.string().min(1).optional(),
309
312
  errorCode: z.string().min(1).optional(),
310
313
  errorMessage: z.string().min(1).optional(),
314
+ providerErrorCategory: ProviderErrorCategorySchema.optional(),
315
+ providerHttpStatus: ProviderHttpStatusSchema.optional(),
316
+ providerErrorCode: ProviderErrorCodeSchema.optional(),
311
317
  runtime: RuntimeSchema,
312
318
  model: z.string().optional(),
313
319
  resumed: z.boolean(),
@@ -296,6 +296,12 @@ class ExecutionCancelledError extends Error {
296
296
  this.name = "ExecutionCancelledError";
297
297
  }
298
298
  }
299
+ class ExecutionQueueTimeoutError extends Error {
300
+ constructor(timeoutMs) {
301
+ super(`Execution queue wait exceeded ${timeoutMs}ms`);
302
+ this.name = "ExecutionQueueTimeoutError";
303
+ }
304
+ }
299
305
  async function cancellable(promise, cancellation) {
300
306
  if (cancellation === undefined)
301
307
  return promise;
@@ -306,11 +312,28 @@ async function cancellable(promise, cancellation) {
306
312
  cancellation.requested.then(() => { throw new ExecutionCancelledError(); }),
307
313
  ]);
308
314
  }
315
+ async function cancellableWithTimeout(promise, timeoutMs, cancellation, timeoutError) {
316
+ let timer;
317
+ try {
318
+ return await Promise.race([
319
+ cancellable(promise, cancellation),
320
+ new Promise((_resolve, reject) => {
321
+ timer = setTimeout(() => reject(timeoutError), timeoutMs);
322
+ }),
323
+ ]);
324
+ }
325
+ finally {
326
+ if (timer !== undefined)
327
+ clearTimeout(timer);
328
+ }
329
+ }
309
330
  function failedCompletion(spec, error, startedAt, finishedAt) {
310
331
  const message = error instanceof Error ? error.message : String(error);
311
- const errorCode = error instanceof ProjectContextUnavailableError
312
- ? error.code
313
- : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
332
+ const errorCode = error instanceof ExecutionQueueTimeoutError
333
+ ? "queue_timeout"
334
+ : error instanceof ProjectContextUnavailableError
335
+ ? error.code
336
+ : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
314
337
  return ExecutionCompletedSchema.parse({
315
338
  type: "execution:completed",
316
339
  protocolVersion: 1,
@@ -455,7 +478,9 @@ export async function runExecution(config, input, dependencies) {
455
478
  let startedAt = accepted.acceptedAt;
456
479
  let runtimeCancel = null;
457
480
  let timeout;
481
+ let firstOutputTimer;
458
482
  let timedOut = false;
483
+ let firstOutputTimedOut = false;
459
484
  let completion;
460
485
  let abandonedRuntimeRunningCompletion = null;
461
486
  let memoryCaptureFinalText = null;
@@ -468,7 +493,7 @@ export async function runExecution(config, input, dependencies) {
468
493
  });
469
494
  try {
470
495
  if (dependencies.slot !== undefined) {
471
- await cancellable(dependencies.slot.ready, dependencies.cancellation);
496
+ await cancellableWithTimeout(dependencies.slot.ready, config.executionLimits.maxQueueWaitMs, dependencies.cancellation, new ExecutionQueueTimeoutError(config.executionLimits.maxQueueWaitMs));
472
497
  }
473
498
  let projectContext;
474
499
  let sessionContextFingerprint;
@@ -508,7 +533,18 @@ export async function runExecution(config, input, dependencies) {
508
533
  if (dependencies.cancellation?.isRequested())
509
534
  return;
510
535
  const cancel = runtimeCancel;
511
- if (cancel !== null && timeout === undefined) {
536
+ if (cancel !== null && timeout === undefined && !dependencies.cancellation?.isRequested()) {
537
+ if (effectiveTimeoutMs > config.executionLimits.maxFirstOutputWaitMs + config.executionLimits.firstOutputGraceMs) {
538
+ firstOutputTimer = setTimeout(() => {
539
+ firstOutputTimedOut = true;
540
+ try {
541
+ void cancel().catch(rejectCancellationFailure);
542
+ }
543
+ catch (error) {
544
+ rejectCancellationFailure(error);
545
+ }
546
+ }, config.executionLimits.maxFirstOutputWaitMs + config.executionLimits.firstOutputGraceMs);
547
+ }
512
548
  timeout = setTimeout(() => {
513
549
  timedOut = true;
514
550
  try {
@@ -566,6 +602,10 @@ export async function runExecution(config, input, dependencies) {
566
602
  },
567
603
  ...(spec.reporting.streamActivity ? {
568
604
  onActivity: (activity) => {
605
+ if (firstOutputTimer !== undefined) {
606
+ clearTimeout(firstOutputTimer);
607
+ firstOutputTimer = undefined;
608
+ }
569
609
  const frame = DaemonToServerExecutionFrameSchema.parse({
570
610
  type: "execution:activity",
571
611
  protocolVersion: 1,
@@ -584,6 +624,10 @@ export async function runExecution(config, input, dependencies) {
584
624
  } : {}),
585
625
  ...(spec.reporting.streamConsole ? {
586
626
  onConsole: (chunk) => {
627
+ if (firstOutputTimer !== undefined) {
628
+ clearTimeout(firstOutputTimer);
629
+ firstOutputTimer = undefined;
630
+ }
587
631
  const frame = DaemonToServerExecutionFrameSchema.parse({
588
632
  type: "execution:console",
589
633
  protocolVersion: 1,
@@ -603,6 +647,10 @@ export async function runExecution(config, input, dependencies) {
603
647
  } : {}),
604
648
  ...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
605
649
  onExternalOutput: (text) => {
650
+ if (firstOutputTimer !== undefined) {
651
+ clearTimeout(firstOutputTimer);
652
+ firstOutputTimer = undefined;
653
+ }
606
654
  const frame = DaemonToServerExecutionFrameSchema.parse({
607
655
  type: "execution:output",
608
656
  protocolVersion: 1,
@@ -778,6 +826,8 @@ export async function runExecution(config, input, dependencies) {
778
826
  ]);
779
827
  if (timeout !== undefined)
780
828
  clearTimeout(timeout);
829
+ if (firstOutputTimer !== undefined)
830
+ clearTimeout(firstOutputTimer);
781
831
  const finishedAt = now().toISOString();
782
832
  if (result.exitCode === 0 && result.finalText?.trim())
783
833
  memoryCaptureFinalText = result.finalText;
@@ -787,7 +837,19 @@ export async function runExecution(config, input, dependencies) {
787
837
  await resetBoundImDecision(path);
788
838
  boundImDecision = selected?.decision ?? "silent";
789
839
  }
790
- completion = ExecutionCompletedSchema.parse(timedOut ? {
840
+ completion = ExecutionCompletedSchema.parse(firstOutputTimedOut ? {
841
+ type: "execution:completed",
842
+ protocolVersion: 1,
843
+ executionId: spec.executionId,
844
+ outcome: "cancelled",
845
+ errorCode: "runtime_no_first_output",
846
+ errorMessage: "Runtime produced no activity before the first-output deadline",
847
+ runtime: result.runtime,
848
+ ...(result.model === null ? {} : { model: result.model }),
849
+ resumed: result.resumed,
850
+ startedAt,
851
+ finishedAt,
852
+ } : timedOut ? {
791
853
  type: "execution:completed",
792
854
  protocolVersion: 1,
793
855
  executionId: spec.executionId,
@@ -818,6 +880,11 @@ export async function runExecution(config, input, dependencies) {
818
880
  ...(!spec.reporting.captureFinal || result.finalText === null
819
881
  ? {}
820
882
  : { finalText: result.finalText }),
883
+ ...(result.providerError === undefined ? {} : {
884
+ providerErrorCategory: result.providerError.category,
885
+ ...(result.providerError.httpStatus === undefined ? {} : { providerHttpStatus: result.providerError.httpStatus }),
886
+ ...(result.providerError.code === undefined ? {} : { providerErrorCode: result.providerError.code }),
887
+ }),
821
888
  // externalAnswer 是本轮完成的权威结果,与 answerStream(增量流式的可选增强)解耦转发;
822
889
  // 否则能力协商降级后模型仍可能沿用同线程记住的 marker 契约,回复却永远发不到频道
823
890
  // (incident: 2026-07-30, 2026-08-03)。
@@ -832,6 +899,8 @@ export async function runExecution(config, input, dependencies) {
832
899
  catch (error) {
833
900
  if (timeout !== undefined)
834
901
  clearTimeout(timeout);
902
+ if (firstOutputTimer !== undefined)
903
+ clearTimeout(firstOutputTimer);
835
904
  const cancelled = error instanceof ExecutionCancelledError || error instanceof RuntimeCancelledError;
836
905
  if (cancelled) {
837
906
  await closeLaunchGate();
@@ -12,6 +12,7 @@ import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint,
12
12
  import { augmentedPath } from "./runtime-path.js";
13
13
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
14
14
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
15
+ import { providerErrorFromExit } from "./provider-error.js";
15
16
  import { createConsoleFormatter } from "./console-formatter.js";
16
17
  import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
17
18
  export { withLocalExecutionFacts, } from "./local-execution-prompt.js";
@@ -872,6 +873,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
872
873
  : redactProjectSkillRuntimeRootText(spawnError, activeProjectSkillRuntimeRoot),
873
874
  terminationSignal ? `terminated by ${terminationSignal}` : undefined,
874
875
  ].filter(Boolean).join(" ").trim();
876
+ const providerError = providerErrorFromExit(exitCode, errorTail);
875
877
  const finish = exitActivity(runtime.name, exitCode, errorTail);
876
878
  if (finish) {
877
879
  activities.push(finish);
@@ -914,6 +916,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
914
916
  resumed: resuming,
915
917
  sessionId,
916
918
  errorMessage: errorTail || null,
919
+ ...(providerError === undefined ? {} : { providerError }),
917
920
  finalText: input.captureFinal && finalText !== null
918
921
  ? stripExternalAnswerMarkers(finalText)
919
922
  : null,
@@ -0,0 +1,61 @@
1
+ export function providerErrorFromExit(exitCode, errorText) {
2
+ if (exitCode === 0)
3
+ return undefined;
4
+ return errorText ? classifyProviderError(errorText) : { category: "cli", code: "process_exit" };
5
+ }
6
+ const STATUS_PATTERN = /\b(?:http(?:\/\d(?:\.\d)?)?\s*)?(?:status(?:_code)?\s*[:=]?\s*|response\s+status\s+|error\s+)([1-5]\d{2})\b/iu;
7
+ const KNOWN_STATUS_PATTERN = /\b(401|403|408|429|500|502|503|504)\b/u;
8
+ const CODE_BY_STATUS = Object.freeze({
9
+ 401: "unauthorized", 403: "forbidden", 408: "request_timeout", 429: "rate_limited",
10
+ 500: "internal_server_error", 502: "bad_gateway", 503: "service_unavailable", 504: "gateway_timeout",
11
+ });
12
+ function statusFromText(text) {
13
+ const explicit = STATUS_PATTERN.exec(text)?.[1] ?? KNOWN_STATUS_PATTERN.exec(text)?.[1];
14
+ if (explicit === undefined)
15
+ return undefined;
16
+ const value = Number(explicit);
17
+ return Number.isInteger(value) && value >= 100 && value <= 599 ? value : undefined;
18
+ }
19
+ function categoryFromStatus(status) {
20
+ if (status === 401 || status === 403)
21
+ return "auth";
22
+ if (status === 429)
23
+ return "rate_limit";
24
+ if (status === 408 || status === 504)
25
+ return "timeout";
26
+ if (status >= 500)
27
+ return "upstream";
28
+ return "unknown";
29
+ }
30
+ export function classifyProviderError(raw) {
31
+ const text = raw.trim().slice(-4_000);
32
+ const httpStatus = statusFromText(text);
33
+ if (httpStatus !== undefined) {
34
+ const code = CODE_BY_STATUS[httpStatus];
35
+ return {
36
+ category: categoryFromStatus(httpStatus),
37
+ httpStatus,
38
+ ...(code === undefined ? {} : { code }),
39
+ };
40
+ }
41
+ const lower = text.toLowerCase();
42
+ if (/quota|credit|billing|insufficient.*(?:fund|balance)/u.test(lower)) {
43
+ return { category: "quota", code: "quota_exhausted" };
44
+ }
45
+ if (/unauthori[sz]ed|authentication failed|invalid api[_ -]?key|\bauth(?:entication)? failed\b/u.test(lower)) {
46
+ return { category: "auth", code: "auth_failed" };
47
+ }
48
+ if (/rate[ -]?limit|too many requests|throttl/u.test(lower)) {
49
+ return { category: "rate_limit", code: "rate_limited" };
50
+ }
51
+ if (/timed? ?out|timeout|deadline exceeded/u.test(lower)) {
52
+ return { category: "timeout", code: "timeout" };
53
+ }
54
+ if (/econn|enotfound|dns|socket|network|tls|certificate/u.test(lower)) {
55
+ return { category: "network", code: "network_error" };
56
+ }
57
+ if (/unknown option|invalid option|command not found|usage:/u.test(lower)) {
58
+ return { category: "cli", code: "cli_error" };
59
+ }
60
+ return { category: "unknown" };
61
+ }
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { z } from "zod";
4
4
  import { augmentedPath } from "./runtime-path.js";
5
+ import { classifyProviderError } from "./provider-error.js";
5
6
  export const RUNTIME_HEALTH_PROBE_CAPABILITY = "runtime_health_probe_v1";
6
7
  export const RUNTIME_HEALTH_NAMES = ["claude", "codex", "opencode", "kimi"];
7
8
  const RuntimeProbeTargetSchema = z.object({
@@ -140,7 +141,19 @@ export async function probeRuntimeHealth(request, dependencies = {}) {
140
141
  }
141
142
  catch (error) {
142
143
  const code = isAbort(error) ? "timeout" : failureCode(error);
143
- return { ...base, installation: "available", model: code, latencyMs: latencyMs(), detailCode: code };
144
+ const providerError = isAbort(error)
145
+ ? { category: "timeout", code: "timeout" }
146
+ : classifyProviderError(failureText(error));
147
+ return {
148
+ ...base,
149
+ installation: "available",
150
+ model: code,
151
+ latencyMs: latencyMs(),
152
+ detailCode: code,
153
+ providerErrorCategory: providerError.category,
154
+ ...(providerError.httpStatus === undefined ? {} : { providerHttpStatus: providerError.httpStatus }),
155
+ ...(providerError.code === undefined ? {} : { providerErrorCode: providerError.code }),
156
+ };
144
157
  }
145
158
  }));
146
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.36",
3
+ "version": "0.6.37",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",