@nowcrew/daemon 0.6.45 → 0.6.47

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/README.md CHANGED
@@ -376,18 +376,29 @@ CREW_SCHEDULED_MAX_PARALLEL=4
376
376
  CREW_EXECUTION_MAX_QUEUED_PER_AGENT=32
377
377
  CREW_EXECUTION_MAX_PARALLEL_TOTAL=10
378
378
  CREW_EXECUTION_MAX_QUEUED_TOTAL=128
379
- CREW_EXECUTION_MAX_STARTING_TOTAL=1
380
- CREW_EXECUTION_MAX_STARTING_PER_RUNTIME=1
381
- CREW_EXECUTION_START_GAP_MS=3000
379
+ CREW_EXECUTION_MAX_STARTING_TOTAL=10
380
+ CREW_EXECUTION_MAX_STARTING_PER_RUNTIME=10
381
+ CREW_EXECUTION_START_GAP_MS=500
382
382
  CREW_EXECUTION_STARTUP_TIMEOUT_MS=120000
383
+ CREW_EXECUTION_MAX_QUEUE_WAIT_MS=300000
384
+ CREW_EXECUTION_MAX_FIRST_OUTPUT_WAIT_MS=120000
385
+ CREW_EXECUTION_FIRST_OUTPUT_GRACE_MS=180000
383
386
  ```
384
387
 
385
388
  `CREW_MAX_PARALLEL` limits normal executions per Agent. `CREW_SCHEDULED_MAX_PARALLEL` separately
386
389
  limits scheduled executions per Agent. `MAX_PARALLEL_TOTAL` is the machine-wide process cap shared by
387
390
  protocol-v1 and legacy work.
388
- Runtime startup is a separate FIFO gate: by default only one Claude, Codex, or Kimi process may be
389
- initializing at a time, and launches are spaced by three seconds. A process leaves the startup gate
391
+ Runtime startup is a separate FIFO gate: by default up to ten Claude, Codex, or Kimi processes may be
392
+ initializing at a time, and launches are spaced by 500ms. A process leaves the startup gate
390
393
  only after its runtime-specific ready event; startup timeout cancels the owned process tree.
394
+ An accepted execution may wait up to five minutes for a local and host slot. After runtime readiness,
395
+ the daemon waits up to two minutes for the first model event, with a three-minute grace window for
396
+ slow providers. When one of these local liveness limits expires, the supervisor is stopped and the
397
+ terminal result is written to the execution journal before it is reported, so the execution remains
398
+ replayable instead of being silently dropped. Queue and first-output timeouts are retried locally at
399
+ most twice (three total attempts). Each retry releases its current machine/host slot and reserves a
400
+ new one, which puts it behind work already waiting in the queue. A third timeout is terminal and is
401
+ not requeued; other failures are not automatically retried.
391
402
 
392
403
  Local policy can reduce server-requested access and limits; it cannot grant more access than requested.
393
404
  Provider credentials and configured environment are prepared locally and never carried in execution
package/dist/config.js CHANGED
@@ -15,10 +15,13 @@ export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
15
15
  maxQueuedPerAgent: 32,
16
16
  maxParallelTotal: 10,
17
17
  maxQueuedTotal: 128,
18
- maxStartingTotal: 1,
19
- maxStartingPerRuntime: 1,
20
- startupGapMs: 3_000,
18
+ maxStartingTotal: 10,
19
+ maxStartingPerRuntime: 10,
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,
@@ -748,6 +748,30 @@ export function createExecutionJournal(agentsRoot, options = {}) {
748
748
  return { kind: "created", ...entry };
749
749
  });
750
750
  },
751
+ retry: async (executionId) => {
752
+ validateExecutionId(executionId);
753
+ return serializedExecution(executionId, "retry", async () => {
754
+ const entry = await requireRecord(executionId);
755
+ if (entry.state !== "accepted" && entry.state !== "running") {
756
+ throw new JournalTransitionError(`Cannot retry ${entry.state} execution`);
757
+ }
758
+ const timestamp = now().toISOString();
759
+ const updated = JournalEntrySchema.parse({
760
+ ...entry,
761
+ state: "accepted",
762
+ pid: null,
763
+ completion: null,
764
+ completionAcknowledged: false,
765
+ acceptedAt: timestamp,
766
+ processStartedAt: null,
767
+ runtimeReadyAt: null,
768
+ processIdentity: null,
769
+ updatedAt: timestamp,
770
+ });
771
+ await writeRecord(updated);
772
+ return updated;
773
+ });
774
+ },
751
775
  startGuarded: async (executionId, processStartedAt, startDormant, hooks) => {
752
776
  validateExecutionId(executionId);
753
777
  TimestampSchema.parse(processStartedAt);
@@ -4,7 +4,7 @@ import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, Execution
4
4
  import { JournalConflictError } from "./execution-journal.js";
5
5
  import { boundExecutionFrame } from "./execution-event-limit.js";
6
6
  import { mintAgentToken } from "./token.js";
7
- import { executeLocal, withLocalExecutionFacts, } from "./local-executor.js";
7
+ import { executeLocal, RuntimeRunningCallbackTimeoutError, withLocalExecutionFacts, } from "./local-executor.js";
8
8
  import { startDormantSupervisor, } from "./execution-supervisor.js";
9
9
  import { CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
10
10
  import { CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
@@ -25,6 +25,7 @@ import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.j
25
25
  import { dslog } from "./slog.js";
26
26
  import { identityRosterPromptFacts } from "./identity-roster-telemetry.js";
27
27
  export { supervisorLaunch } from "./supervised-runtime.js";
28
+ const MAX_LOCAL_EXECUTION_ATTEMPTS = 3;
28
29
  const ACTIVITY_KIND = {
29
30
  init: "working",
30
31
  text: "thinking",
@@ -298,6 +299,12 @@ class ExecutionCancelledError extends Error {
298
299
  this.name = "ExecutionCancelledError";
299
300
  }
300
301
  }
302
+ class ExecutionQueueTimeoutError extends Error {
303
+ constructor(timeoutMs) {
304
+ super(`Execution queue wait exceeded ${timeoutMs}ms`);
305
+ this.name = "ExecutionQueueTimeoutError";
306
+ }
307
+ }
301
308
  async function cancellable(promise, cancellation) {
302
309
  if (cancellation === undefined)
303
310
  return promise;
@@ -308,9 +315,24 @@ async function cancellable(promise, cancellation) {
308
315
  cancellation.requested.then(() => { throw new ExecutionCancelledError(); }),
309
316
  ]);
310
317
  }
318
+ async function cancellableWithTimeout(promise, timeoutMs, cancellation, timeoutError) {
319
+ let timer;
320
+ try {
321
+ return await Promise.race([
322
+ cancellable(promise, cancellation),
323
+ new Promise((_resolve, reject) => {
324
+ timer = setTimeout(() => reject(timeoutError), timeoutMs);
325
+ }),
326
+ ]);
327
+ }
328
+ finally {
329
+ if (timer !== undefined)
330
+ clearTimeout(timer);
331
+ }
332
+ }
311
333
  function failedCompletion(spec, error, startedAt, finishedAt) {
312
334
  const message = error instanceof Error ? error.message : String(error);
313
- const errorCode = error instanceof HostReservationCancelledError
335
+ const errorCode = error instanceof ExecutionQueueTimeoutError || error instanceof HostReservationCancelledError
314
336
  ? "queue_timeout"
315
337
  : error instanceof ProjectContextUnavailableError
316
338
  ? error.code
@@ -331,6 +353,10 @@ function failedCompletion(spec, error, startedAt, finishedAt) {
331
353
  }
332
354
  export async function runExecution(config, input, dependencies) {
333
355
  const now = dependencies.now ?? (() => new Date());
356
+ const retryAttempt = dependencies.retryAttempt ?? 1;
357
+ if (!Number.isInteger(retryAttempt) || retryAttempt < 1 || retryAttempt > MAX_LOCAL_EXECUTION_ATTEMPTS) {
358
+ throw new RangeError(`retryAttempt must be between 1 and ${MAX_LOCAL_EXECUTION_ATTEMPTS}`);
359
+ }
334
360
  const initialAt = now().toISOString();
335
361
  const parsed = ExecutionStartSchema.safeParse(input);
336
362
  if (!parsed.success) {
@@ -373,31 +399,38 @@ export async function runExecution(config, input, dependencies) {
373
399
  await dependencies.report(boundExecutionFrame(replay.completion, config.executionLimits.maxEventBytes));
374
400
  return { kind: "existing", entry: replay };
375
401
  }
376
- await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
377
- type: "execution:accepted",
378
- protocolVersion: 1,
379
- executionId: spec.executionId,
380
- state: dependencies.slot?.state ?? "ready",
381
- effectivePermission: replayPermission,
382
- at: replay.acceptedAt,
383
- }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
384
- if (replay.state === "running" && replay.runtimeReadyAt !== null) {
402
+ if (retryAttempt === 1) {
385
403
  await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
386
- type: "execution:started",
404
+ type: "execution:accepted",
387
405
  protocolVersion: 1,
388
406
  executionId: spec.executionId,
389
- at: replay.runtimeReadyAt,
407
+ state: dependencies.slot?.state ?? "ready",
408
+ effectivePermission: replayPermission,
409
+ at: replay.acceptedAt,
390
410
  }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
411
+ if (replay.state === "running" && replay.runtimeReadyAt !== null) {
412
+ await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
413
+ type: "execution:started",
414
+ protocolVersion: 1,
415
+ executionId: spec.executionId,
416
+ at: replay.runtimeReadyAt,
417
+ }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
418
+ }
419
+ return { kind: "existing", entry: replay };
391
420
  }
392
- return { kind: "existing", entry: replay };
393
421
  }
394
- const checked = admission(spec, config, dependencies, initialAt);
422
+ // A retry already owns a freshly admitted slot. Re-running the initial capacity
423
+ // admission against the first attempt's stale facts would reject a valid tail retry.
424
+ const checked = retryAttempt > 1
425
+ ? { permission: effectivePermission(spec, config) }
426
+ : admission(spec, config, dependencies, initialAt);
395
427
  if ("rejected" in checked) {
396
428
  const frame = ExecutionRejectedSchema.parse(boundExecutionFrame(checked.rejected, config.executionLimits.maxEventBytes));
397
429
  await dependencies.report(frame);
398
430
  return { kind: "rejected", frame };
399
431
  }
400
432
  const { permission } = checked;
433
+ const effectiveTimeoutMs = spec.runtime.timeoutMs ?? config.executionLimits.maxTimeoutMs;
401
434
  const accepted = await dependencies.journal.accept(spec.executionId, specHash, {
402
435
  runtime: spec.runtime.name,
403
436
  ...(spec.runtime.model === undefined ? {} : { model: spec.runtime.model }),
@@ -410,32 +443,36 @@ export async function runExecution(config, input, dependencies) {
410
443
  await dependencies.report(boundExecutionFrame(accepted.completion, config.executionLimits.maxEventBytes));
411
444
  return { kind: "existing", entry: accepted };
412
445
  }
446
+ if (retryAttempt === 1) {
447
+ await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
448
+ type: "execution:accepted",
449
+ protocolVersion: 1,
450
+ executionId: spec.executionId,
451
+ state: dependencies.slot?.state ?? "ready",
452
+ effectivePermission: accepted.effectivePermission ?? permission,
453
+ at: accepted.acceptedAt,
454
+ }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
455
+ if (accepted.state === "running" && accepted.runtimeReadyAt !== null) {
456
+ await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
457
+ type: "execution:started",
458
+ protocolVersion: 1,
459
+ executionId: spec.executionId,
460
+ at: accepted.runtimeReadyAt,
461
+ }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
462
+ }
463
+ return { kind: "existing", entry: accepted };
464
+ }
465
+ }
466
+ if (retryAttempt === 1) {
413
467
  await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
414
468
  type: "execution:accepted",
415
469
  protocolVersion: 1,
416
470
  executionId: spec.executionId,
417
471
  state: dependencies.slot?.state ?? "ready",
418
- effectivePermission: accepted.effectivePermission ?? permission,
472
+ effectivePermission: permission,
419
473
  at: accepted.acceptedAt,
420
474
  }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
421
- if (accepted.state === "running" && accepted.runtimeReadyAt !== null) {
422
- await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
423
- type: "execution:started",
424
- protocolVersion: 1,
425
- executionId: spec.executionId,
426
- at: accepted.runtimeReadyAt,
427
- }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
428
- }
429
- return { kind: "existing", entry: accepted };
430
475
  }
431
- await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
432
- type: "execution:accepted",
433
- protocolVersion: 1,
434
- executionId: spec.executionId,
435
- state: dependencies.slot?.state ?? "ready",
436
- effectivePermission: permission,
437
- at: accepted.acceptedAt,
438
- }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
439
476
  const mint = dependencies.mintAgentToken ?? mintAgentToken;
440
477
  const execute = dependencies.executeLocal ?? executeLocal;
441
478
  const startSupervisor = dependencies.startSupervisor ?? startDormantSupervisor;
@@ -466,6 +503,11 @@ export async function runExecution(config, input, dependencies) {
466
503
  };
467
504
  let startedAt = accepted.acceptedAt;
468
505
  let runtimeCancel = null;
506
+ let timeout;
507
+ let timedOut = false;
508
+ let firstOutputTimer;
509
+ let firstOutputTimedOut = false;
510
+ let abandonedRuntimeRunningCompletion = null;
469
511
  let completion;
470
512
  let memoryCaptureFinalText = null;
471
513
  let boundImDecision = spec.reporting.allowBoundImDecision
@@ -477,7 +519,7 @@ export async function runExecution(config, input, dependencies) {
477
519
  });
478
520
  try {
479
521
  if (dependencies.slot !== undefined) {
480
- await cancellable(dependencies.slot.ready, dependencies.cancellation);
522
+ await cancellableWithTimeout(dependencies.slot.ready, config.executionLimits.maxQueueWaitMs, dependencies.cancellation, new ExecutionQueueTimeoutError(config.executionLimits.maxQueueWaitMs));
481
523
  }
482
524
  let projectContext;
483
525
  let sessionContextFingerprint;
@@ -508,23 +550,90 @@ export async function runExecution(config, input, dependencies) {
508
550
  let consoleSequence = 0;
509
551
  let externalOutputSequence = 0;
510
552
  const callbacks = {
511
- onRuntimeReady: async () => {
553
+ onRuntimeStarting: () => {
554
+ dependencies.onRuntimePhase?.("starting", spec.runtime.name);
555
+ },
556
+ onRuntimeRunning: async () => {
557
+ const callbackStartedAt = Date.now();
558
+ let callbackStage = "journal";
512
559
  if (dependencies.cancellation?.isRequested())
513
560
  return;
514
- const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
515
- if (ready.runtimeReadyAt === null) {
516
- throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
561
+ const cancel = runtimeCancel;
562
+ if (cancel !== null && timeout === undefined && !dependencies.cancellation?.isRequested()) {
563
+ const firstOutputDeadline = config.executionLimits.maxFirstOutputWaitMs
564
+ + config.executionLimits.firstOutputGraceMs;
565
+ if (effectiveTimeoutMs > firstOutputDeadline) {
566
+ firstOutputTimer = setTimeout(() => {
567
+ firstOutputTimedOut = true;
568
+ try {
569
+ void cancel().catch(rejectCancellationFailure);
570
+ }
571
+ catch (error) {
572
+ rejectCancellationFailure(error);
573
+ }
574
+ }, firstOutputDeadline);
575
+ }
576
+ timeout = setTimeout(() => {
577
+ timedOut = true;
578
+ try {
579
+ void cancel().catch(rejectCancellationFailure);
580
+ }
581
+ catch (error) {
582
+ rejectCancellationFailure(error);
583
+ }
584
+ }, effectiveTimeoutMs);
585
+ }
586
+ dependencies.onRuntimePhase?.("running", spec.runtime.name);
587
+ const journalStartedAt = Date.now();
588
+ try {
589
+ const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
590
+ const journalMs = Date.now() - journalStartedAt;
591
+ if (ready.runtimeReadyAt === null) {
592
+ throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
593
+ }
594
+ if (abandonedRuntimeRunningCompletion !== null) {
595
+ await dependencies.journal.complete(spec.executionId, abandonedRuntimeRunningCompletion);
596
+ await dependencies.report(abandonedRuntimeRunningCompletion);
597
+ return;
598
+ }
599
+ startedAt = ready.runtimeReadyAt;
600
+ callbackStage = "started_report";
601
+ const reportStartedAt = Date.now();
602
+ const reportDelivered = await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
603
+ type: "execution:started",
604
+ protocolVersion: 1,
605
+ executionId: spec.executionId,
606
+ at: startedAt,
607
+ }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
608
+ dslog("execution.runtime_running_callback", "runtime-running callback 已完成", {
609
+ execution_id: spec.executionId,
610
+ runtime: spec.runtime.name,
611
+ outcome: "succeeded",
612
+ journal_ms: journalMs,
613
+ started_report_ms: Date.now() - reportStartedAt,
614
+ started_report_delivered: reportDelivered,
615
+ callback_total_ms: Date.now() - callbackStartedAt,
616
+ });
617
+ }
618
+ catch (error) {
619
+ dslog("execution.runtime_running_callback", "runtime-running callback 失败", {
620
+ level: "ERROR",
621
+ execution_id: spec.executionId,
622
+ runtime: spec.runtime.name,
623
+ outcome: "failed",
624
+ failed_stage: callbackStage,
625
+ callback_total_ms: Date.now() - callbackStartedAt,
626
+ error_message: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500),
627
+ });
628
+ throw error;
517
629
  }
518
- startedAt = ready.runtimeReadyAt;
519
- await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
520
- type: "execution:started",
521
- protocolVersion: 1,
522
- executionId: spec.executionId,
523
- at: startedAt,
524
- }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
525
630
  },
526
631
  ...(spec.reporting.streamActivity ? {
527
632
  onActivity: (activity) => {
633
+ if (firstOutputTimer !== undefined) {
634
+ clearTimeout(firstOutputTimer);
635
+ firstOutputTimer = undefined;
636
+ }
528
637
  const frame = DaemonToServerExecutionFrameSchema.parse({
529
638
  type: "execution:activity",
530
639
  protocolVersion: 1,
@@ -543,6 +652,10 @@ export async function runExecution(config, input, dependencies) {
543
652
  } : {}),
544
653
  ...(spec.reporting.streamConsole ? {
545
654
  onConsole: (chunk) => {
655
+ if (firstOutputTimer !== undefined) {
656
+ clearTimeout(firstOutputTimer);
657
+ firstOutputTimer = undefined;
658
+ }
546
659
  const frame = DaemonToServerExecutionFrameSchema.parse({
547
660
  type: "execution:console",
548
661
  protocolVersion: 1,
@@ -562,6 +675,10 @@ export async function runExecution(config, input, dependencies) {
562
675
  } : {}),
563
676
  ...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
564
677
  onExternalOutput: (text) => {
678
+ if (firstOutputTimer !== undefined) {
679
+ clearTimeout(firstOutputTimer);
680
+ firstOutputTimer = undefined;
681
+ }
565
682
  const frame = DaemonToServerExecutionFrameSchema.parse({
566
683
  type: "execution:output",
567
684
  protocolVersion: 1,
@@ -735,6 +852,10 @@ export async function runExecution(config, input, dependencies) {
735
852
  execute(localInput, callbacks, localDependencies),
736
853
  cancellationFailure,
737
854
  ]);
855
+ if (timeout !== undefined)
856
+ clearTimeout(timeout);
857
+ if (firstOutputTimer !== undefined)
858
+ clearTimeout(firstOutputTimer);
738
859
  const finishedAt = now().toISOString();
739
860
  if (result.exitCode === 0 && result.finalText?.trim())
740
861
  memoryCaptureFinalText = result.finalText;
@@ -744,7 +865,33 @@ export async function runExecution(config, input, dependencies) {
744
865
  await resetBoundImDecision(path);
745
866
  boundImDecision = selected?.decision ?? "silent";
746
867
  }
747
- completion = ExecutionCompletedSchema.parse({
868
+ completion = ExecutionCompletedSchema.parse(firstOutputTimedOut ? {
869
+ type: "execution:completed",
870
+ protocolVersion: 1,
871
+ executionId: spec.executionId,
872
+ outcome: "cancelled",
873
+ errorCode: "runtime_no_first_output",
874
+ errorMessage: "Runtime produced no activity before the first-output deadline",
875
+ runtime: result.runtime,
876
+ ...(result.model === null ? {} : { model: result.model }),
877
+ resumed: result.resumed,
878
+ ...(boundImDecision ? { boundImDecision } : {}),
879
+ startedAt,
880
+ finishedAt,
881
+ } : timedOut ? {
882
+ type: "execution:completed",
883
+ protocolVersion: 1,
884
+ executionId: spec.executionId,
885
+ outcome: "cancelled",
886
+ errorCode: "timeout",
887
+ errorMessage: "Execution exceeded its local timeout",
888
+ runtime: result.runtime,
889
+ ...(result.model === null ? {} : { model: result.model }),
890
+ resumed: result.resumed,
891
+ ...(boundImDecision ? { boundImDecision } : {}),
892
+ startedAt,
893
+ finishedAt,
894
+ } : {
748
895
  type: "execution:completed",
749
896
  protocolVersion: 1,
750
897
  executionId: spec.executionId,
@@ -779,6 +926,10 @@ export async function runExecution(config, input, dependencies) {
779
926
  });
780
927
  }
781
928
  catch (error) {
929
+ if (timeout !== undefined)
930
+ clearTimeout(timeout);
931
+ if (firstOutputTimer !== undefined)
932
+ clearTimeout(firstOutputTimer);
782
933
  const cancelled = error instanceof ExecutionCancelledError || error instanceof RuntimeCancelledError;
783
934
  if (cancelled) {
784
935
  await closeLaunchGate();
@@ -795,7 +946,7 @@ export async function runExecution(config, input, dependencies) {
795
946
  else {
796
947
  await proveActiveSupervisorStopped(error, spec.agent.projectSkillBindingGeneration !== undefined);
797
948
  }
798
- completion = cancelled
949
+ const failureCompletion = cancelled
799
950
  ? ExecutionCompletedSchema.parse({
800
951
  type: "execution:completed",
801
952
  protocolVersion: 1,
@@ -810,9 +961,55 @@ export async function runExecution(config, input, dependencies) {
810
961
  finishedAt: now().toISOString(),
811
962
  })
812
963
  : failedCompletion(spec, error, startedAt, now().toISOString());
964
+ if (error instanceof RuntimeRunningCallbackTimeoutError) {
965
+ abandonedRuntimeRunningCompletion = boundExecutionFrame(failureCompletion, config.executionLimits.maxEventBytes);
966
+ await telemetry.closeAndDrain();
967
+ throw error;
968
+ }
969
+ completion = failureCompletion;
813
970
  }
814
971
  completion = boundExecutionFrame(completion, config.executionLimits.maxEventBytes);
815
972
  await telemetry.closeAndDrain();
973
+ const retryReason = completion.errorCode === "queue_timeout"
974
+ ? "queue_timeout"
975
+ : completion.errorCode === "runtime_no_first_output"
976
+ ? "runtime_no_first_output"
977
+ : null;
978
+ if (retryReason !== null
979
+ && retryAttempt < MAX_LOCAL_EXECUTION_ATTEMPTS
980
+ && dependencies.onRetry !== undefined
981
+ && dependencies.journal.retry !== undefined
982
+ && !dependencies.cancellation?.isRequested()) {
983
+ try {
984
+ await dependencies.journal.retry(spec.executionId);
985
+ const nextSlot = await dependencies.onRetry({ attempt: retryAttempt, reason: retryReason });
986
+ if (nextSlot !== undefined) {
987
+ return runExecution(config, spec, {
988
+ ...dependencies,
989
+ slot: nextSlot,
990
+ retryAttempt: retryAttempt + 1,
991
+ });
992
+ }
993
+ }
994
+ catch (error) {
995
+ dslog("execution.retry_requeue_failed", "execution 超时重排队失败,保留本次终态", {
996
+ level: "ERROR",
997
+ execution_id: spec.executionId,
998
+ attempt: retryAttempt,
999
+ reason: retryReason,
1000
+ error_message: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500),
1001
+ });
1002
+ }
1003
+ }
1004
+ if (retryReason !== null && retryAttempt >= MAX_LOCAL_EXECUTION_ATTEMPTS) {
1005
+ dslog("execution.retry_exhausted", "execution 超时重试次数已耗尽", {
1006
+ level: "WARN",
1007
+ execution_id: spec.executionId,
1008
+ attempt: retryAttempt,
1009
+ max_attempts: MAX_LOCAL_EXECUTION_ATTEMPTS,
1010
+ reason: retryReason,
1011
+ });
1012
+ }
816
1013
  await dependencies.journal.complete(spec.executionId, completion);
817
1014
  if (completion.outcome === "succeeded"
818
1015
  && spec.agent.memoryEnabled === true
@@ -4,11 +4,23 @@ export const DEFAULT_FIRST_PROGRESS_TIMEOUT_MS = 120_000;
4
4
  * the runtime's configured total timeout remains authoritative; long-running tools are not killed
5
5
  * merely because they produce no output.
6
6
  */
7
- export function startFirstProgressWatchdog(_onTimeout, timeoutMs = DEFAULT_FIRST_PROGRESS_TIMEOUT_MS) {
7
+ export function startFirstProgressWatchdog(onTimeout, timeoutMs = DEFAULT_FIRST_PROGRESS_TIMEOUT_MS) {
8
8
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
9
9
  throw new RangeError("First-progress timeout must be a positive finite number");
10
10
  }
11
- // Progress is observational only. Runtime tasks must not be terminated because
12
- // an upstream provider is temporarily silent.
13
- return { observe: () => undefined, stop: () => undefined };
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 };
14
26
  }
package/dist/serve.js CHANGED
@@ -516,7 +516,7 @@ export function serve(config, opts = {}) {
516
516
  const executionClass = pruneTraceId !== null
517
517
  ? "memory_prune"
518
518
  : spec.context.scheduledRunId === undefined ? "normal" : "scheduled";
519
- const reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
519
+ let reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
520
520
  if (!reservation.accepted) {
521
521
  dslog("execution.machine_queue_rejected", "机器执行队列已满", {
522
522
  level: "WARN",
@@ -533,7 +533,7 @@ export function serve(config, opts = {}) {
533
533
  return;
534
534
  }
535
535
  knownExecutionHashes.set(spec.executionId, hash);
536
- const machineQueueEnteredAt = Date.now();
536
+ let machineQueueEnteredAt = Date.now();
537
537
  const executionConcurrencyIdentity = {
538
538
  executionId: spec.executionId,
539
539
  agentHandle: spec.agent.handle,
@@ -571,6 +571,70 @@ export function serve(config, opts = {}) {
571
571
  });
572
572
  };
573
573
  const cancellation = cancellationFor(spec.executionId);
574
+ const slotForReservation = (attemptReservation, queueEnteredAt) => ({
575
+ state: attemptReservation.state ?? (attemptReservation.isQueued() ? "queued" : "ready"),
576
+ ready: attemptReservation.ready.then(() => {
577
+ markExecutionTaskKeyActive();
578
+ attemptReservation.markPreparing();
579
+ concurrencyTelemetry.transition(executionConcurrencyIdentity, "preparing", {
580
+ queue_ms: Date.now() - queueEnteredAt,
581
+ });
582
+ const snapshot = sharedSlots.snapshot();
583
+ dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
584
+ execution_id: spec.executionId,
585
+ agent_handle: spec.agent.handle,
586
+ execution_class: executionClass,
587
+ queue_ms: Date.now() - queueEnteredAt,
588
+ active_total: snapshot.runningTotal,
589
+ admitted_total: snapshot.admittedTotal,
590
+ preparing_total: snapshot.preparingTotal,
591
+ starting_total: snapshot.startingTotal,
592
+ running_total: snapshot.runningTotal,
593
+ queued_total: snapshot.queuedTotal,
594
+ memory_prune_admitted_total: snapshot.memoryPruneAdmittedTotal,
595
+ memory_prune_queued_total: snapshot.memoryPruneQueuedTotal,
596
+ memory_prune_preparing_total: snapshot.memoryPrunePreparingTotal,
597
+ memory_prune_starting_total: snapshot.memoryPruneStartingTotal,
598
+ memory_prune_running_total: snapshot.memoryPruneRunningTotal,
599
+ });
600
+ }),
601
+ });
602
+ const onRetry = async ({ attempt, reason, }) => {
603
+ if (stopped || cancellation.isRequested())
604
+ return undefined;
605
+ const previousReservation = reservation;
606
+ previousReservation.release();
607
+ concurrencyTelemetry.release(executionConcurrencyIdentity, `retry:${reason}`);
608
+ const nextReservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
609
+ if (!nextReservation.accepted) {
610
+ executionReservations.delete(spec.executionId);
611
+ dslog("execution.retry_queue_rejected", "execution 超时重排队被机器队列拒绝", {
612
+ level: "WARN",
613
+ execution_id: spec.executionId,
614
+ agent_handle: spec.agent.handle,
615
+ execution_class: executionClass,
616
+ attempt,
617
+ reason,
618
+ ...nextReservation.facts,
619
+ });
620
+ return undefined;
621
+ }
622
+ reservation = nextReservation;
623
+ machineQueueEnteredAt = Date.now();
624
+ executionReservations.set(spec.executionId, nextReservation);
625
+ if (nextReservation.isQueued()) {
626
+ concurrencyTelemetry.transition(executionConcurrencyIdentity, "queued");
627
+ dslog("execution.retry_queued", "execution 超时后重新进入机器队列", {
628
+ execution_id: spec.executionId,
629
+ agent_handle: spec.agent.handle,
630
+ execution_class: executionClass,
631
+ attempt: attempt + 1,
632
+ reason,
633
+ ...nextReservation.facts,
634
+ });
635
+ }
636
+ return slotForReservation(nextReservation, machineQueueEnteredAt);
637
+ };
574
638
  const cleanupExecutionReservation = (reason = "execution_settled") => {
575
639
  if (executionTaskKeyFinished)
576
640
  return;
@@ -652,36 +716,8 @@ export function serve(config, opts = {}) {
652
716
  memory_prune_running_total: snapshot.memoryPruneRunningTotal,
653
717
  });
654
718
  },
655
- ...(reservation.state === undefined ? {} : {
656
- slot: {
657
- state: reservation.state,
658
- ready: reservation.ready.then(() => {
659
- markExecutionTaskKeyActive();
660
- reservation.markPreparing();
661
- concurrencyTelemetry.transition(executionConcurrencyIdentity, "preparing", {
662
- queue_ms: Date.now() - machineQueueEnteredAt,
663
- });
664
- const snapshot = sharedSlots.snapshot();
665
- dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
666
- execution_id: spec.executionId,
667
- agent_handle: spec.agent.handle,
668
- execution_class: executionClass,
669
- queue_ms: Date.now() - machineQueueEnteredAt,
670
- active_total: snapshot.runningTotal,
671
- admitted_total: snapshot.admittedTotal,
672
- preparing_total: snapshot.preparingTotal,
673
- starting_total: snapshot.startingTotal,
674
- running_total: snapshot.runningTotal,
675
- queued_total: snapshot.queuedTotal,
676
- memory_prune_admitted_total: snapshot.memoryPruneAdmittedTotal,
677
- memory_prune_queued_total: snapshot.memoryPruneQueuedTotal,
678
- memory_prune_preparing_total: snapshot.memoryPrunePreparingTotal,
679
- memory_prune_starting_total: snapshot.memoryPruneStartingTotal,
680
- memory_prune_running_total: snapshot.memoryPruneRunningTotal,
681
- });
682
- }),
683
- },
684
- }),
719
+ slot: slotForReservation(reservation, machineQueueEnteredAt),
720
+ onRetry,
685
721
  cancellation,
686
722
  }).finally(() => {
687
723
  cleanupExecutionReservation("execution_settled");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.45",
3
+ "version": "0.6.47",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",