@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.
- package/dist/execution-concurrency-telemetry.js +115 -0
- package/dist/execution-journal.js +103 -20
- package/dist/execution-runner.js +56 -14
- package/dist/local-executor.js +22 -2
- package/dist/project-skills/serve-startup.js +15 -0
- package/dist/runner.js +2 -2
- package/dist/serve.js +42 -6
- package/package.json +1 -1
|
@@ -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
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
877
|
+
return serializedExecution(executionId, "get", () => readRecord(executionId));
|
|
795
878
|
},
|
|
796
|
-
replay: async () =>
|
|
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 () =>
|
|
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 () =>
|
|
898
|
+
prune: async () => serializedDirectory(pruneInternal),
|
|
816
899
|
close: async (closeOptions = {}) => {
|
|
817
900
|
const signal = closeOptions.signal;
|
|
818
901
|
let started = false;
|
|
819
|
-
const closing =
|
|
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");
|
package/dist/execution-runner.js
CHANGED
|
@@ -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
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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();
|
package/dist/local-executor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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);
|