@nowcrew/daemon 0.6.33 → 0.6.35

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.
@@ -0,0 +1,115 @@
1
+ import { dslog } from "./slog.js";
2
+ const RUNTIMES = [
3
+ "claude",
4
+ "codex",
5
+ "kimi",
6
+ "hermes",
7
+ "opencode",
8
+ "deepseek-harness",
9
+ ];
10
+ function snapshotFields(snapshot, tracked) {
11
+ const runtimeFields = {};
12
+ for (const runtime of RUNTIMES) {
13
+ for (const phase of ["queued", "preparing", "starting", "running"]) {
14
+ runtimeFields[`${runtime.replaceAll("-", "_")}_${phase}_total`] = [...tracked.values()]
15
+ .filter((entry) => entry.runtime === runtime && entry.phase === phase).length;
16
+ }
17
+ }
18
+ return {
19
+ active_total: snapshot.activeTotal,
20
+ admitted_total: snapshot.admittedTotal,
21
+ queued_total: snapshot.queuedTotal,
22
+ preparing_total: snapshot.preparingTotal,
23
+ starting_total: snapshot.startingTotal,
24
+ running_total: snapshot.runningTotal,
25
+ memory_prune_admitted_total: snapshot.memoryPruneAdmittedTotal,
26
+ memory_prune_queued_total: snapshot.memoryPruneQueuedTotal,
27
+ memory_prune_preparing_total: snapshot.memoryPrunePreparingTotal,
28
+ memory_prune_starting_total: snapshot.memoryPruneStartingTotal,
29
+ memory_prune_running_total: snapshot.memoryPruneRunningTotal,
30
+ runtime_tracked_total: tracked.size,
31
+ ...runtimeFields,
32
+ };
33
+ }
34
+ function hasWork(snapshot, tracked) {
35
+ return tracked.size > 0
36
+ || snapshot.admittedTotal > 0
37
+ || snapshot.queuedTotal > 0
38
+ || snapshot.memoryPruneAdmittedTotal > 0
39
+ || snapshot.memoryPruneQueuedTotal > 0;
40
+ }
41
+ export function createExecutionConcurrencyTelemetry(slots, options = {}) {
42
+ const tracked = new Map();
43
+ const log = options.log ?? dslog;
44
+ const intervalMs = options.intervalMs ?? 30_000;
45
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
46
+ throw new RangeError("concurrency telemetry intervalMs must be positive");
47
+ }
48
+ let timer = null;
49
+ const writeSnapshot = (reason) => {
50
+ const current = slots.snapshot();
51
+ if (!hasWork(current, tracked))
52
+ return;
53
+ log("execution.concurrency_snapshot", "execution 并发快照", {
54
+ reason,
55
+ ...snapshotFields(current, tracked),
56
+ });
57
+ };
58
+ const ensureTimer = () => {
59
+ if (timer !== null)
60
+ return;
61
+ timer = setInterval(() => writeSnapshot("periodic"), intervalMs);
62
+ timer.unref?.();
63
+ };
64
+ const stopTimerIfIdle = () => {
65
+ if (timer === null || hasWork(slots.snapshot(), tracked))
66
+ return;
67
+ clearInterval(timer);
68
+ timer = null;
69
+ };
70
+ return {
71
+ transition: (identity, phase, fields = {}) => {
72
+ const previousPhase = tracked.get(identity.executionId)?.phase ?? null;
73
+ tracked.set(identity.executionId, { runtime: identity.runtime, phase });
74
+ ensureTimer();
75
+ log("execution.phase_changed", "execution 阶段变更", {
76
+ execution_id: identity.executionId,
77
+ agent_handle: identity.agentHandle,
78
+ runtime: identity.runtime,
79
+ execution_class: identity.executionClass,
80
+ protocol: identity.protocol,
81
+ previous_phase: previousPhase,
82
+ phase,
83
+ ...snapshotFields(slots.snapshot(), tracked),
84
+ ...fields,
85
+ });
86
+ },
87
+ release: (identity, reason, fields = {}) => {
88
+ const previousPhase = tracked.get(identity.executionId)?.phase ?? null;
89
+ tracked.delete(identity.executionId);
90
+ log("execution.phase_changed", "execution 已释放执行名额", {
91
+ execution_id: identity.executionId,
92
+ agent_handle: identity.agentHandle,
93
+ runtime: identity.runtime,
94
+ execution_class: identity.executionClass,
95
+ protocol: identity.protocol,
96
+ previous_phase: previousPhase,
97
+ phase: "released",
98
+ release_reason: reason,
99
+ ...snapshotFields(slots.snapshot(), tracked),
100
+ ...fields,
101
+ });
102
+ stopTimerIfIdle();
103
+ },
104
+ snapshot: (reason) => {
105
+ writeSnapshot(reason);
106
+ if (hasWork(slots.snapshot(), tracked))
107
+ ensureTimer();
108
+ },
109
+ stop: () => {
110
+ if (timer !== null)
111
+ clearInterval(timer);
112
+ timer = null;
113
+ },
114
+ };
115
+ }
@@ -129,10 +129,45 @@ const DEFAULT_TERMINATION_GRACE_MS = 30_000;
129
129
  const DEFAULT_KILL_VERIFICATION_DELAY_MS = 100;
130
130
  const DEFAULT_COMMAND_TIMEOUT_MS = 5_000;
131
131
  const directoryQueues = new Map();
132
- function runSerialized(directory, operation) {
133
- const previous = directoryQueues.get(directory) ?? Promise.resolve();
134
- const result = previous.then(operation, operation);
135
- directoryQueues.set(directory, result.then(() => undefined, () => undefined));
132
+ function directoryQueue(directory) {
133
+ const existing = directoryQueues.get(directory);
134
+ if (existing !== undefined)
135
+ return existing;
136
+ const created = {
137
+ exclusiveTail: Promise.resolve(),
138
+ keyedTails: new Map(),
139
+ };
140
+ directoryQueues.set(directory, created);
141
+ return created;
142
+ }
143
+ /**
144
+ * Serialize mutations for one execution while allowing unrelated execution
145
+ * records to make progress concurrently. Directory maintenance scheduled
146
+ * before this operation remains an ordering barrier.
147
+ */
148
+ function runKeyedSerialized(directory, key, operation) {
149
+ const queue = directoryQueue(directory);
150
+ const previousKey = queue.keyedTails.get(key) ?? Promise.resolve();
151
+ const result = Promise.all([queue.exclusiveTail, previousKey]).then(operation);
152
+ const tail = result.then(() => undefined, () => undefined);
153
+ queue.keyedTails.set(key, tail);
154
+ void tail.then(() => {
155
+ if (queue.keyedTails.get(key) === tail)
156
+ queue.keyedTails.delete(key);
157
+ });
158
+ return result;
159
+ }
160
+ /**
161
+ * Directory-wide work is an exclusive barrier: it waits for every mutation
162
+ * already scheduled, and later keyed mutations wait for it. Recovery, prune,
163
+ * initialization, and close use this path because they inspect directory-wide
164
+ * state or journal lease ownership.
165
+ */
166
+ function runDirectoryExclusive(directory, operation) {
167
+ const queue = directoryQueue(directory);
168
+ const blockers = [queue.exclusiveTail, ...queue.keyedTails.values()];
169
+ const result = Promise.all(blockers).then(operation);
170
+ queue.exclusiveTail = result.then(() => undefined, () => undefined);
136
171
  return result;
137
172
  }
138
173
  function runCommand(command, args, options = {}) {
@@ -312,6 +347,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
312
347
  if (!Number.isInteger(maxEntries))
313
348
  throw new RangeError("maxEntries must be an integer");
314
349
  let initialized = false;
350
+ let initializationPromise = null;
315
351
  const recordPath = (executionId) => join(directory, `${executionId}.json`);
316
352
  const runtimeReadyPath = (executionId) => join(directory, `${executionId}${RUNTIME_READY_SUFFIX}`);
317
353
  const parseRecord = (path, raw) => {
@@ -528,11 +564,57 @@ export function createExecutionJournal(agentsRoot, options = {}) {
528
564
  await pruneInternal();
529
565
  initialized = true;
530
566
  };
531
- const serialized = (operation) => runSerialized(directory, async () => {
532
- lease.assertUsable();
533
- await initialize();
534
- return operation();
535
- });
567
+ const ensureInitialized = () => {
568
+ if (initialized)
569
+ return Promise.resolve();
570
+ if (initializationPromise !== null)
571
+ return initializationPromise;
572
+ const initializing = runDirectoryExclusive(directory, async () => {
573
+ lease.assertUsable();
574
+ await initialize();
575
+ });
576
+ initializationPromise = initializing;
577
+ void initializing.catch(() => {
578
+ if (initializationPromise === initializing)
579
+ initializationPromise = null;
580
+ });
581
+ return initializing;
582
+ };
583
+ const serializedExecution = async (executionId, operationName, operation) => {
584
+ await ensureInitialized();
585
+ const queuedAt = Date.now();
586
+ return runKeyedSerialized(directory, executionId, async () => {
587
+ lease.assertUsable();
588
+ const operationStartedAt = Date.now();
589
+ let outcome = "succeeded";
590
+ try {
591
+ return await operation();
592
+ }
593
+ catch (error) {
594
+ outcome = "failed";
595
+ throw error;
596
+ }
597
+ finally {
598
+ try {
599
+ options.onOperationTiming?.({
600
+ executionId,
601
+ operation: operationName,
602
+ queueWaitMs: operationStartedAt - queuedAt,
603
+ operationMs: Date.now() - operationStartedAt,
604
+ outcome,
605
+ });
606
+ }
607
+ catch { /* diagnostics must never affect journal durability */ }
608
+ }
609
+ });
610
+ };
611
+ const serializedDirectory = async (operation) => {
612
+ await ensureInitialized();
613
+ return runDirectoryExclusive(directory, async () => {
614
+ lease.assertUsable();
615
+ return operation();
616
+ });
617
+ };
536
618
  const requireRecord = async (executionId) => {
537
619
  const entry = await readRecord(executionId);
538
620
  if (entry === null)
@@ -636,7 +718,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
636
718
  validateExecutionId(executionId);
637
719
  if (specHash.length === 0)
638
720
  throw new TypeError("specHash must not be empty");
639
- return serialized(async () => {
721
+ return serializedExecution(executionId, "accept", async () => {
640
722
  const existing = await readRecord(executionId);
641
723
  if (existing !== null) {
642
724
  if (existing.specHash !== specHash) {
@@ -669,7 +751,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
669
751
  startGuarded: async (executionId, processStartedAt, startDormant, hooks) => {
670
752
  validateExecutionId(executionId);
671
753
  TimestampSchema.parse(processStartedAt);
672
- return serialized(async () => {
754
+ return serializedExecution(executionId, "start_guarded", async () => {
673
755
  const entry = await requireRecord(executionId);
674
756
  if (entry.state !== "accepted") {
675
757
  return { kind: "existing", entry: await confirmDurable(entry) };
@@ -729,7 +811,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
729
811
  markRuntimeReady: async (executionId, runtimeReadyAt) => {
730
812
  validateExecutionId(executionId);
731
813
  TimestampSchema.parse(runtimeReadyAt);
732
- return serialized(async () => {
814
+ return serializedExecution(executionId, "mark_runtime_ready", async () => {
733
815
  const entry = await requireRecord(executionId);
734
816
  if (entry.state !== "running") {
735
817
  throw new JournalTransitionError(`Cannot mark ${entry.state} execution runtime-ready`);
@@ -751,7 +833,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
751
833
  if (completion.executionId !== executionId) {
752
834
  throw new JournalConflictError("Completion executionId does not match the journal record");
753
835
  }
754
- return serialized(async () => {
836
+ return serializedExecution(executionId, "complete", async () => {
755
837
  const entry = await requireRecord(executionId);
756
838
  if (entry.state === "completed" || entry.state === "interrupted") {
757
839
  if (sameValue(entry.completion, completion))
@@ -772,7 +854,7 @@ export function createExecutionJournal(agentsRoot, options = {}) {
772
854
  },
773
855
  acknowledgeCompletion: async (executionId) => {
774
856
  validateExecutionId(executionId);
775
- return serialized(async () => {
857
+ const updated = await serializedExecution(executionId, "acknowledge_completion", async () => {
776
858
  const entry = await requireRecord(executionId);
777
859
  if (entry.state !== "completed" && entry.state !== "interrupted") {
778
860
  throw new JournalTransitionError(`Cannot acknowledge ${entry.state} execution`);
@@ -785,20 +867,21 @@ export function createExecutionJournal(agentsRoot, options = {}) {
785
867
  updatedAt: now().toISOString(),
786
868
  });
787
869
  await writeRecord(updated);
788
- await pruneInternal();
789
870
  return updated;
790
871
  });
872
+ await serializedDirectory(pruneInternal);
873
+ return updated;
791
874
  },
792
875
  get: async (executionId) => {
793
876
  validateExecutionId(executionId);
794
- return serialized(() => readRecord(executionId));
877
+ return serializedExecution(executionId, "get", () => readRecord(executionId));
795
878
  },
796
- replay: async () => serialized(async () => (await readAll())
879
+ replay: async () => serializedDirectory(async () => (await readAll())
797
880
  .filter((entry) => entry.state === "accepted"
798
881
  || entry.state === "running"
799
882
  || !entry.completionAcknowledged)
800
883
  .sort((left, right) => left.executionId.localeCompare(right.executionId))),
801
- reconcileAfterRestart: async () => serialized(async () => {
884
+ reconcileAfterRestart: async () => serializedDirectory(async () => {
802
885
  const active = (await readAll()).filter((entry) => entry.state === "accepted" || entry.state === "running");
803
886
  const results = await Promise.allSettled(active.map(async (entry) => {
804
887
  if (entry.state === "running")
@@ -812,11 +895,11 @@ export function createExecutionJournal(agentsRoot, options = {}) {
812
895
  throw new AggregateError(failures, "One or more executions could not be reconciled");
813
896
  }
814
897
  }),
815
- prune: async () => serialized(pruneInternal),
898
+ prune: async () => serializedDirectory(pruneInternal),
816
899
  close: async (closeOptions = {}) => {
817
900
  const signal = closeOptions.signal;
818
901
  let started = false;
819
- const closing = runSerialized(directory, async () => {
902
+ const closing = runDirectoryExclusive(directory, async () => {
820
903
  started = true;
821
904
  if (signal?.aborted)
822
905
  throw signal.reason ?? new Error("Journal close aborted");
@@ -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";
@@ -21,6 +21,7 @@ import { createProjectRegistry } from "./project-skills/registry.js";
21
21
  import { ProjectContextUnavailableError, resolveProjectContext } from "./project-workspaces/resolver.js";
22
22
  import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
23
23
  import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.js";
24
+ import { dslog } from "./slog.js";
24
25
  export { supervisorLaunch } from "./supervised-runtime.js";
25
26
  const ACTIVITY_KIND = {
26
27
  init: "working",
@@ -456,6 +457,7 @@ export async function runExecution(config, input, dependencies) {
456
457
  let timeout;
457
458
  let timedOut = false;
458
459
  let completion;
460
+ let abandonedRuntimeRunningCompletion = null;
459
461
  let memoryCaptureFinalText = null;
460
462
  let boundImDecision = spec.reporting.allowBoundImDecision
461
463
  ? "silent"
@@ -498,9 +500,11 @@ export async function runExecution(config, input, dependencies) {
498
500
  let externalOutputSequence = 0;
499
501
  const callbacks = {
500
502
  onRuntimeStarting: () => {
501
- dependencies.onRuntimePhase?.("starting");
503
+ dependencies.onRuntimePhase?.("starting", spec.runtime.name);
502
504
  },
503
505
  onRuntimeRunning: async () => {
506
+ const callbackStartedAt = Date.now();
507
+ let callbackStage = "journal";
504
508
  if (dependencies.cancellation?.isRequested())
505
509
  return;
506
510
  const cancel = runtimeCancel;
@@ -515,18 +519,50 @@ export async function runExecution(config, input, dependencies) {
515
519
  }
516
520
  }, effectiveTimeoutMs);
517
521
  }
518
- dependencies.onRuntimePhase?.("running");
519
- const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
520
- if (ready.runtimeReadyAt === null) {
521
- throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
522
+ dependencies.onRuntimePhase?.("running", spec.runtime.name);
523
+ const journalStartedAt = Date.now();
524
+ try {
525
+ const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
526
+ const journalMs = Date.now() - journalStartedAt;
527
+ if (ready.runtimeReadyAt === null) {
528
+ throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
529
+ }
530
+ if (abandonedRuntimeRunningCompletion !== null) {
531
+ await dependencies.journal.complete(spec.executionId, abandonedRuntimeRunningCompletion);
532
+ await dependencies.report(abandonedRuntimeRunningCompletion);
533
+ return;
534
+ }
535
+ startedAt = ready.runtimeReadyAt;
536
+ callbackStage = "started_report";
537
+ const reportStartedAt = Date.now();
538
+ const reportDelivered = await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
539
+ type: "execution:started",
540
+ protocolVersion: 1,
541
+ executionId: spec.executionId,
542
+ at: startedAt,
543
+ }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
544
+ dslog("execution.runtime_running_callback", "runtime-running callback 已完成", {
545
+ execution_id: spec.executionId,
546
+ runtime: spec.runtime.name,
547
+ outcome: "succeeded",
548
+ journal_ms: journalMs,
549
+ started_report_ms: Date.now() - reportStartedAt,
550
+ started_report_delivered: reportDelivered,
551
+ callback_total_ms: Date.now() - callbackStartedAt,
552
+ });
553
+ }
554
+ catch (error) {
555
+ dslog("execution.runtime_running_callback", "runtime-running callback 失败", {
556
+ level: "ERROR",
557
+ execution_id: spec.executionId,
558
+ runtime: spec.runtime.name,
559
+ outcome: "failed",
560
+ failed_stage: callbackStage,
561
+ callback_total_ms: Date.now() - callbackStartedAt,
562
+ error_message: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500),
563
+ });
564
+ throw error;
522
565
  }
523
- startedAt = ready.runtimeReadyAt;
524
- await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
525
- type: "execution:started",
526
- protocolVersion: 1,
527
- executionId: spec.executionId,
528
- at: startedAt,
529
- }), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
530
566
  },
531
567
  ...(spec.reporting.streamActivity ? {
532
568
  onActivity: (activity) => {
@@ -812,7 +848,7 @@ export async function runExecution(config, input, dependencies) {
812
848
  else {
813
849
  await proveActiveSupervisorStopped(error, spec.agent.projectSkillBindingGeneration !== undefined);
814
850
  }
815
- completion = cancelled
851
+ const failureCompletion = cancelled
816
852
  ? ExecutionCompletedSchema.parse({
817
853
  type: "execution:completed",
818
854
  protocolVersion: 1,
@@ -827,6 +863,12 @@ export async function runExecution(config, input, dependencies) {
827
863
  finishedAt: now().toISOString(),
828
864
  })
829
865
  : failedCompletion(spec, error, startedAt, now().toISOString());
866
+ if (error instanceof RuntimeRunningCallbackTimeoutError) {
867
+ abandonedRuntimeRunningCompletion = boundExecutionFrame(failureCompletion, config.executionLimits.maxEventBytes);
868
+ await telemetry.closeAndDrain();
869
+ throw error;
870
+ }
871
+ completion = failureCompletion;
830
872
  }
831
873
  completion = boundExecutionFrame(completion, config.executionLimits.maxEventBytes);
832
874
  await telemetry.closeAndDrain();
@@ -60,8 +60,18 @@ function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
60
60
  error_code: errorCode,
61
61
  });
62
62
  }
63
+ export class RuntimeRunningCallbackTimeoutError extends Error {
64
+ constructor(runtime, timeoutMs) {
65
+ super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
66
+ this.name = "RuntimeRunningCallbackTimeoutError";
67
+ }
68
+ }
63
69
  const STDERR_TAIL_CAP = 2_000;
64
- const RUNTIME_RUNNING_CALLBACK_TIMEOUT_MS = 10_000;
70
+ // This is a liveness fuse, not a normal durability latency budget. Journal
71
+ // mutations are keyed per execution, so a healthy callback should finish in
72
+ // milliseconds; 30s tolerates a transient filesystem stall without reviving
73
+ // the old failure mode where a callback could hold execution slots forever.
74
+ const RUNTIME_RUNNING_CALLBACK_TIMEOUT_MS = 30_000;
65
75
  const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
66
76
  const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
67
77
  const CLAUDE_INSTRUCTION_WARNING = "claude_additional_directory_instructions_unverified";
@@ -682,19 +692,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
682
692
  if (runtimeRunning)
683
693
  return;
684
694
  runtimeRunning = true;
695
+ const callbackStartedAt = Date.now();
685
696
  let callbackTimer;
686
697
  runtimeRunningNotification = Promise.race([
687
698
  Promise.resolve().then(() => callbacks.onRuntimeRunning?.(runtime.name)),
688
699
  new Promise((_resolve, reject) => {
689
700
  const callbackTimeoutMs = dependencies.runtimeRunningCallbackTimeoutMs
690
701
  ?? RUNTIME_RUNNING_CALLBACK_TIMEOUT_MS;
691
- callbackTimer = setTimeout(() => reject(new Error(`${runtime.name} runtime-running callback timed out after ${callbackTimeoutMs}ms`)), callbackTimeoutMs);
702
+ callbackTimer = setTimeout(() => reject(new RuntimeRunningCallbackTimeoutError(runtime.name, callbackTimeoutMs)), callbackTimeoutMs);
692
703
  }),
693
704
  ]).finally(() => {
694
705
  if (callbackTimer !== undefined)
695
706
  clearTimeout(callbackTimer);
696
707
  });
697
708
  void runtimeRunningNotification.catch((error) => {
709
+ dslog("runtime.running_callback_failed", "runtime-running 回调失败", {
710
+ level: "ERROR",
711
+ execution_id: input.executionId,
712
+ runtime: runtime.name,
713
+ callback_ms: Date.now() - callbackStartedAt,
714
+ error_message: error instanceof Error
715
+ ? error.message.slice(0, 500)
716
+ : String(error).slice(0, 500),
717
+ });
698
718
  // Persisting the running phase is part of execution liveness. Propagate
699
719
  // its failure immediately: cancellation is best-effort and must not be
700
720
  // allowed to keep the execution or its slots alive indefinitely.
@@ -8,6 +8,21 @@ export async function initializeProjectSkillProtectedExecutionJournal(input) {
8
8
  input.protectedExecutionIds.add(executionId);
9
9
  const journal = input.injectedJournal ?? createExecutionJournal(input.config.agentsRoot, {
10
10
  protectedExecutionIds: () => input.protectedExecutionIds,
11
+ onOperationTiming: (event) => {
12
+ if (event.operation !== "mark_runtime_ready"
13
+ && event.queueWaitMs < 250
14
+ && event.operationMs < 250)
15
+ return;
16
+ dslog("execution.journal_operation", "execution journal 操作完成", {
17
+ level: event.outcome === "succeeded" ? "INFO" : "ERROR",
18
+ execution_id: event.executionId,
19
+ operation: event.operation,
20
+ outcome: event.outcome,
21
+ queue_wait_ms: event.queueWaitMs,
22
+ operation_ms: event.operationMs,
23
+ total_ms: event.queueWaitMs + event.operationMs,
24
+ });
25
+ },
11
26
  });
12
27
  await reconcileExecutionJournal(journal, {
13
28
  agentsRoot: input.config.agentsRoot,
package/dist/runner.js CHANGED
@@ -108,8 +108,8 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
108
108
  }, {
109
109
  onActivity,
110
110
  onConsole,
111
- onRuntimeStarting: () => dependencies.onRuntimePhase?.("starting"),
112
- onRuntimeRunning: () => dependencies.onRuntimePhase?.("running"),
111
+ onRuntimeStarting: () => dependencies.onRuntimePhase?.("starting", runtime),
112
+ onRuntimeRunning: () => dependencies.onRuntimePhase?.("running", runtime),
113
113
  }, {
114
114
  launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
115
115
  ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
package/dist/serve.js CHANGED
@@ -25,6 +25,7 @@ import { awaitWithCancellation, createRuntimeCancellation, RuntimeCancelledError
25
25
  import { createShutdownDeadline, readTestShutdownConfiguration } from "./shutdown-deadline.js";
26
26
  import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
27
27
  import { createSharedSlotManager } from "./shared-execution-slots.js";
28
+ import { createExecutionConcurrencyTelemetry } from "./execution-concurrency-telemetry.js";
28
29
  import { createCompletionRetransmitter } from "./completion-retransmitter.js";
29
30
  import { completionRetransmitterOptions } from "./completion-retransmitter-logging.js";
30
31
  import { createAgentMemoryBridge } from "./agent-memory/bridge.js";
@@ -87,6 +88,11 @@ export function serve(config, opts = {}) {
87
88
  });
88
89
  const localSlots = createSharedSlotManager(config.executionLimits);
89
90
  const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
91
+ const concurrencyTelemetry = createExecutionConcurrencyTelemetry(sharedSlots, {
92
+ ...(opts.execution?.concurrencySnapshotIntervalMs === undefined
93
+ ? {}
94
+ : { intervalMs: opts.execution.concurrencySnapshotIntervalMs }),
95
+ });
90
96
  const runtimeStartupGate = hostCoordinatedStartupGate(createRuntimeStartupGate(config.executionLimits), hostCoordinator);
91
97
  const knownExecutionHashes = new Map();
92
98
  const executionReservations = new Map();
@@ -527,7 +533,15 @@ export function serve(config, opts = {}) {
527
533
  }
528
534
  knownExecutionHashes.set(spec.executionId, hash);
529
535
  const machineQueueEnteredAt = Date.now();
536
+ const executionConcurrencyIdentity = {
537
+ executionId: spec.executionId,
538
+ agentHandle: spec.agent.handle,
539
+ runtime: spec.runtime.name,
540
+ executionClass,
541
+ protocol: "execution_v1",
542
+ };
530
543
  if (reservation.isQueued()) {
544
+ concurrencyTelemetry.transition(executionConcurrencyIdentity, "queued");
531
545
  dslog("execution.machine_queued", "execution 已进入机器队列", {
532
546
  execution_id: spec.executionId,
533
547
  agent_handle: spec.agent.handle,
@@ -556,9 +570,12 @@ export function serve(config, opts = {}) {
556
570
  });
557
571
  };
558
572
  const cancellation = cancellationFor(spec.executionId);
559
- const cleanupExecutionReservation = () => {
573
+ const cleanupExecutionReservation = (reason = "execution_settled") => {
574
+ if (executionTaskKeyFinished)
575
+ return;
560
576
  executionTaskKeyFinished = true;
561
577
  reservation.release();
578
+ concurrencyTelemetry.release(executionConcurrencyIdentity, reason);
562
579
  if (executionTaskKeyActive) {
563
580
  const remainingSameTask = Math.max(0, (activeExecutionTaskKeys.get(executionTaskKey) ?? 1) - 1);
564
581
  if (remainingSameTask === 0)
@@ -584,7 +601,7 @@ export function serve(config, opts = {}) {
584
601
  : await (runtimeFacts ?? Promise.resolve(detectedExecutionRuntimes)));
585
602
  }
586
603
  catch (error) {
587
- cleanupExecutionReservation();
604
+ cleanupExecutionReservation("runtime_detection_failed");
588
605
  safeExecutionSend(ExecutionRejectedSchema.parse({
589
606
  type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
590
607
  reason: "resource_limit", message: `Runtime detection failed: ${error.message}`,
@@ -593,7 +610,7 @@ export function serve(config, opts = {}) {
593
610
  return;
594
611
  }
595
612
  if (stopped) {
596
- cleanupExecutionReservation();
613
+ cleanupExecutionReservation("daemon_stopping");
597
614
  return;
598
615
  }
599
616
  const execution = executeProtocol(config, spec, {
@@ -610,15 +627,17 @@ export function serve(config, opts = {}) {
610
627
  report: reportExecutionFrame,
611
628
  startupGate: runtimeStartupGate,
612
629
  startupTimeoutMs: config.executionLimits.startupTimeoutMs,
613
- onRuntimePhase: (phase) => {
630
+ onRuntimePhase: (phase, runtime) => {
614
631
  if (phase === "starting")
615
632
  reservation.markStarting();
616
633
  else
617
634
  reservation.markRunning();
635
+ concurrencyTelemetry.transition(executionConcurrencyIdentity, phase);
618
636
  const snapshot = sharedSlots.snapshot();
619
637
  dslog(`execution.runtime_${phase}`, `execution runtime ${phase}`, {
620
638
  execution_id: spec.executionId,
621
639
  agent_handle: spec.agent.handle,
640
+ runtime,
622
641
  execution_class: executionClass,
623
642
  admitted_total: snapshot.admittedTotal,
624
643
  preparing_total: snapshot.preparingTotal,
@@ -636,6 +655,9 @@ export function serve(config, opts = {}) {
636
655
  ready: reservation.ready.then(() => {
637
656
  markExecutionTaskKeyActive();
638
657
  reservation.markPreparing();
658
+ concurrencyTelemetry.transition(executionConcurrencyIdentity, "preparing", {
659
+ queue_ms: Date.now() - machineQueueEnteredAt,
660
+ });
639
661
  const snapshot = sharedSlots.snapshot();
640
662
  dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
641
663
  execution_id: spec.executionId,
@@ -659,7 +681,7 @@ export function serve(config, opts = {}) {
659
681
  }),
660
682
  cancellation,
661
683
  }).finally(() => {
662
- cleanupExecutionReservation();
684
+ cleanupExecutionReservation("execution_settled");
663
685
  executionRuns.delete(spec.executionId);
664
686
  });
665
687
  executionRuns.set(spec.executionId, execution);
@@ -858,6 +880,7 @@ export function serve(config, opts = {}) {
858
880
  legacyRuns.set(runId, { controller, done: legacyDone });
859
881
  let legacyStopError;
860
882
  let legacyReservation = null;
883
+ let legacyConcurrencyIdentity = null;
861
884
  let legacyQueueDone = null;
862
885
  let finishLegacyQueue = () => { };
863
886
  try {
@@ -1004,14 +1027,23 @@ export function serve(config, opts = {}) {
1004
1027
  cancellation: controller.cancellation,
1005
1028
  startupGate: runtimeStartupGate,
1006
1029
  startupTimeoutMs: config.executionLimits.startupTimeoutMs,
1007
- onRuntimePhase: (phase) => {
1030
+ onRuntimePhase: (phase, runtime) => {
1031
+ legacyConcurrencyIdentity ??= {
1032
+ executionId: runId,
1033
+ agentHandle: msg.agentHandle,
1034
+ runtime,
1035
+ executionClass: scheduled === null ? "normal" : "scheduled",
1036
+ protocol: "legacy",
1037
+ };
1008
1038
  if (phase === "starting")
1009
1039
  legacyReservation?.markStarting();
1010
1040
  else
1011
1041
  legacyReservation?.markRunning();
1042
+ concurrencyTelemetry.transition(legacyConcurrencyIdentity, phase);
1012
1043
  const snapshot = sharedSlots.snapshot();
1013
1044
  dslog(`run.runtime_${phase}`, `legacy runtime ${phase}`, {
1014
1045
  ...runKeys,
1046
+ runtime,
1015
1047
  admitted_total: snapshot.admittedTotal,
1016
1048
  preparing_total: snapshot.preparingTotal,
1017
1049
  starting_total: snapshot.startingTotal,
@@ -1139,6 +1171,9 @@ export function serve(config, opts = {}) {
1139
1171
  finally {
1140
1172
  running.delete(key);
1141
1173
  legacyReservation?.release();
1174
+ if (legacyConcurrencyIdentity !== null) {
1175
+ concurrencyTelemetry.release(legacyConcurrencyIdentity, "legacy_run_settled");
1176
+ }
1142
1177
  finishLegacyQueue();
1143
1178
  if (legacyQueueDone && legacyTaskTails.get(key) === legacyQueueDone) {
1144
1179
  legacyTaskTails.delete(key);
@@ -1199,6 +1234,7 @@ export function serve(config, opts = {}) {
1199
1234
  return stopPromise;
1200
1235
  stopPromise = (async () => {
1201
1236
  stopped = true;
1237
+ concurrencyTelemetry.stop();
1202
1238
  runtimeProbe.stop();
1203
1239
  completionRetransmitter.stop();
1204
1240
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",