@nowcrew/daemon 0.6.32 → 0.6.34
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/remote/claude-bridge.js +558 -0
- package/dist/remote/claude-channel.js +164 -0
- package/dist/remote/codex-client.js +451 -0
- package/dist/remote/codex-runtime.js +77 -0
- package/dist/remote/config.js +135 -0
- package/dist/remote/gateway.js +879 -0
- package/dist/remote/identity.js +39 -0
- package/dist/remote/owner.js +77 -0
- package/dist/remote/protocol.js +211 -0
- package/dist/remote/remote-cli.js +254 -0
- package/dist/remote/runtime-probe.js +182 -0
- package/dist/remote/session-discovery.js +249 -0
- package/dist/remote/wrapper.js +40 -0
- package/dist/remote-web/assets/index-B_6VM_tw.js +94 -0
- package/dist/remote-web/assets/index-L6EiQbJn.css +1 -0
- package/dist/remote-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/remote-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/remote-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/remote-web/icons/nowwork-192.png +0 -0
- package/dist/remote-web/icons/nowwork-512.png +0 -0
- package/dist/remote-web/icons/nowwork.svg +7 -0
- package/dist/remote-web/index.html +20 -0
- package/dist/remote-web/manifest.webmanifest +13 -0
- package/dist/remote-web/sw.js +12 -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,
|