@nowcrew/daemon 0.5.30 → 0.5.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -1
- package/dist/agent-memory/bridge.js +37 -0
- package/dist/agent-memory/client.js +94 -0
- package/dist/agent-memory/config.js +64 -0
- package/dist/agent-memory/policy.js +98 -0
- package/dist/config.js +15 -1
- package/dist/execution-journal-lock.js +21 -4
- package/dist/execution-journal.js +96 -5
- package/dist/execution-protocol.js +2 -0
- package/dist/execution-runner.js +84 -31
- package/dist/host-execution-coordinator.js +241 -0
- package/dist/local-executor.js +84 -1
- package/dist/machine-info.js +17 -1
- package/dist/runner.js +4 -0
- package/dist/runtime-startup-gate.js +91 -0
- package/dist/runtimes/codex-app-server-runner.js +60 -16
- package/dist/serve.js +74 -8
- package/dist/shared-execution-slots.js +48 -33
- package/package.json +1 -1
|
@@ -98,6 +98,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
98
98
|
agent: z.object({
|
|
99
99
|
id: z.string().min(1),
|
|
100
100
|
handle: AgentHandleSchema,
|
|
101
|
+
memoryEnabled: z.boolean().optional(),
|
|
101
102
|
}).strict(),
|
|
102
103
|
workspace: z.object({
|
|
103
104
|
taskKey: z.string().min(1).max(200),
|
|
@@ -121,6 +122,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
121
122
|
channelId: z.string().min(1),
|
|
122
123
|
threadId: z.string().min(1).optional(),
|
|
123
124
|
wakeMessageId: z.string().min(1).optional(),
|
|
125
|
+
scheduledRunId: ExecutionIdSchema.optional(),
|
|
124
126
|
externalResponseSessionId: ExecutionIdSchema.optional(),
|
|
125
127
|
answerStream: z.boolean().optional(),
|
|
126
128
|
attachments: z.array(ExecutionAttachmentSchema).max(20).optional(),
|
package/dist/execution-runner.js
CHANGED
|
@@ -13,6 +13,7 @@ import { executionBackendCapability } from "./execution-backend.js";
|
|
|
13
13
|
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
14
14
|
import { RuntimeCancelledError } from "./runtime-cancellation.js";
|
|
15
15
|
import { supervisorLaunch } from "./supervised-runtime.js";
|
|
16
|
+
import { appendAgentMemoryContext } from "./agent-memory/policy.js";
|
|
16
17
|
export { supervisorLaunch } from "./supervised-runtime.js";
|
|
17
18
|
const ACTIVITY_KIND = {
|
|
18
19
|
init: "working",
|
|
@@ -217,18 +218,26 @@ function admission(spec, config, dependencies, at) {
|
|
|
217
218
|
}
|
|
218
219
|
if (!Number.isInteger(dependencies.facts.activeForAgent)
|
|
219
220
|
|| !Number.isInteger(dependencies.facts.queuedForAgent)
|
|
221
|
+
|| !Number.isInteger(dependencies.facts.activeTotal)
|
|
222
|
+
|| !Number.isInteger(dependencies.facts.queuedTotal)
|
|
220
223
|
|| dependencies.facts.activeForAgent < 0
|
|
221
|
-
|| dependencies.facts.queuedForAgent < 0
|
|
224
|
+
|| dependencies.facts.queuedForAgent < 0
|
|
225
|
+
|| dependencies.facts.activeTotal < 0
|
|
226
|
+
|| dependencies.facts.queuedTotal < 0) {
|
|
222
227
|
return { rejected: rejection(spec.executionId, "invalid_spec", "Invalid local resource facts", at) };
|
|
223
228
|
}
|
|
229
|
+
const agentAtCapacity = dependencies.facts.activeForAgent >= config.executionLimits.maxParallelPerAgent;
|
|
230
|
+
const machineAtCapacity = dependencies.facts.activeTotal >= config.executionLimits.maxParallelTotal;
|
|
224
231
|
const slotInvalid = dependencies.slot?.state === "ready"
|
|
225
|
-
?
|
|
232
|
+
? agentAtCapacity || machineAtCapacity
|
|
226
233
|
: dependencies.slot?.state === "queued"
|
|
227
|
-
? dependencies.facts.
|
|
228
|
-
|| dependencies.facts.
|
|
234
|
+
? dependencies.facts.queuedForAgent >= config.executionLimits.maxQueuedPerAgent
|
|
235
|
+
|| dependencies.facts.queuedTotal >= config.executionLimits.maxQueuedTotal
|
|
229
236
|
: false;
|
|
230
|
-
if (slotInvalid || (dependencies.slot === undefined && (
|
|
231
|
-
||
|
|
237
|
+
if (slotInvalid || (dependencies.slot === undefined && (agentAtCapacity
|
|
238
|
+
|| machineAtCapacity
|
|
239
|
+
|| dependencies.facts.queuedForAgent >= config.executionLimits.maxQueuedPerAgent
|
|
240
|
+
|| dependencies.facts.queuedTotal >= config.executionLimits.maxQueuedTotal))) {
|
|
232
241
|
return { rejected: rejection(spec.executionId, "resource_limit", "Local execution capacity is exhausted", at) };
|
|
233
242
|
}
|
|
234
243
|
const permission = effectivePermission(spec, config);
|
|
@@ -306,12 +315,12 @@ export async function runExecution(config, input, dependencies) {
|
|
|
306
315
|
effectivePermission: replayPermission,
|
|
307
316
|
at: replay.acceptedAt,
|
|
308
317
|
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
309
|
-
if (replay.state === "running" && replay.
|
|
318
|
+
if (replay.state === "running" && replay.runtimeReadyAt !== null) {
|
|
310
319
|
await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
|
|
311
320
|
type: "execution:started",
|
|
312
321
|
protocolVersion: 1,
|
|
313
322
|
executionId: spec.executionId,
|
|
314
|
-
at: replay.
|
|
323
|
+
at: replay.runtimeReadyAt,
|
|
315
324
|
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
316
325
|
}
|
|
317
326
|
return { kind: "existing", entry: replay };
|
|
@@ -344,12 +353,12 @@ export async function runExecution(config, input, dependencies) {
|
|
|
344
353
|
effectivePermission: accepted.effectivePermission ?? permission,
|
|
345
354
|
at: accepted.acceptedAt,
|
|
346
355
|
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
347
|
-
if (accepted.state === "running" && accepted.
|
|
356
|
+
if (accepted.state === "running" && accepted.runtimeReadyAt !== null) {
|
|
348
357
|
await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
|
|
349
358
|
type: "execution:started",
|
|
350
359
|
protocolVersion: 1,
|
|
351
360
|
executionId: spec.executionId,
|
|
352
|
-
at: accepted.
|
|
361
|
+
at: accepted.runtimeReadyAt,
|
|
353
362
|
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
354
363
|
}
|
|
355
364
|
return { kind: "existing", entry: accepted };
|
|
@@ -376,9 +385,11 @@ export async function runExecution(config, input, dependencies) {
|
|
|
376
385
|
await Promise.all([...launchAttempts]);
|
|
377
386
|
};
|
|
378
387
|
let startedAt = accepted.acceptedAt;
|
|
388
|
+
let runtimeCancel = null;
|
|
379
389
|
let timeout;
|
|
380
390
|
let timedOut = false;
|
|
381
391
|
let completion;
|
|
392
|
+
let memoryCaptureFinalText = null;
|
|
382
393
|
let boundImDecision = spec.reporting.allowBoundImDecision
|
|
383
394
|
? "silent"
|
|
384
395
|
: undefined;
|
|
@@ -390,12 +401,49 @@ export async function runExecution(config, input, dependencies) {
|
|
|
390
401
|
if (dependencies.slot !== undefined) {
|
|
391
402
|
await cancellable(dependencies.slot.ready, dependencies.cancellation);
|
|
392
403
|
}
|
|
404
|
+
let recalledMemory = "";
|
|
405
|
+
if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
|
|
406
|
+
try {
|
|
407
|
+
recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt), dependencies.cancellation);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (error instanceof ExecutionCancelledError)
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
393
414
|
const credential = await cancellable(mint(config.serverUrl, config.machineToken, spec.agent.handle, undefined, { executionId: spec.executionId, agentRunId: spec.executionId }), dependencies.cancellation);
|
|
394
415
|
const providerConfig = launchProviderConfig(credential.config);
|
|
395
416
|
let activitySequence = 0;
|
|
396
417
|
let consoleSequence = 0;
|
|
397
418
|
let externalOutputSequence = 0;
|
|
398
419
|
const callbacks = {
|
|
420
|
+
onRuntimeReady: async () => {
|
|
421
|
+
if (dependencies.cancellation?.isRequested())
|
|
422
|
+
return;
|
|
423
|
+
const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
|
|
424
|
+
if (ready.runtimeReadyAt === null) {
|
|
425
|
+
throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
|
|
426
|
+
}
|
|
427
|
+
startedAt = ready.runtimeReadyAt;
|
|
428
|
+
await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
|
|
429
|
+
type: "execution:started",
|
|
430
|
+
protocolVersion: 1,
|
|
431
|
+
executionId: spec.executionId,
|
|
432
|
+
at: startedAt,
|
|
433
|
+
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
434
|
+
const cancel = runtimeCancel;
|
|
435
|
+
if (cancel !== null && !dependencies.cancellation?.isRequested()) {
|
|
436
|
+
timeout = setTimeout(() => {
|
|
437
|
+
timedOut = true;
|
|
438
|
+
try {
|
|
439
|
+
void cancel().catch(rejectCancellationFailure);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
rejectCancellationFailure(error);
|
|
443
|
+
}
|
|
444
|
+
}, effectiveTimeoutMs);
|
|
445
|
+
}
|
|
446
|
+
},
|
|
399
447
|
...(spec.reporting.streamActivity ? {
|
|
400
448
|
onActivity: (activity) => {
|
|
401
449
|
const frame = DaemonToServerExecutionFrameSchema.parse({
|
|
@@ -453,6 +501,10 @@ export async function runExecution(config, input, dependencies) {
|
|
|
453
501
|
};
|
|
454
502
|
const localDependencies = {
|
|
455
503
|
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
504
|
+
...(dependencies.startupGate === undefined ? {} : { startupGate: dependencies.startupGate }),
|
|
505
|
+
...(dependencies.startupTimeoutMs === undefined
|
|
506
|
+
? {}
|
|
507
|
+
: { startupTimeoutMs: dependencies.startupTimeoutMs }),
|
|
456
508
|
launchRuntime: async (request) => {
|
|
457
509
|
if (launchClosed || dependencies.cancellation?.isRequested())
|
|
458
510
|
throw new ExecutionCancelledError();
|
|
@@ -463,7 +515,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
463
515
|
const processStartedAt = now().toISOString();
|
|
464
516
|
const launchControl = { cancel: null };
|
|
465
517
|
const guarded = await dependencies.journal.startGuarded(spec.executionId, processStartedAt, () => startSupervisor(supervisorLaunch(request)), {
|
|
466
|
-
beforeRelease: ({
|
|
518
|
+
beforeRelease: ({ handle, abort }) => {
|
|
467
519
|
supervisorState.active = handle;
|
|
468
520
|
let stopPromise = null;
|
|
469
521
|
let releaseStarted = false;
|
|
@@ -481,7 +533,6 @@ export async function runExecution(config, input, dependencies) {
|
|
|
481
533
|
launchControl.cancel = () => stopOnce(releaseStarted ? handle.cancel : abort);
|
|
482
534
|
supervisorState.abortOnce = () => stopOnce(abort);
|
|
483
535
|
dependencies.cancellation?.register(launchControl.cancel);
|
|
484
|
-
startedAt = entry.processStartedAt ?? processStartedAt;
|
|
485
536
|
if (dependencies.cancellation?.isRequested()) {
|
|
486
537
|
return dependencies.cancellation.waitForStop().then(() => {
|
|
487
538
|
throw new ExecutionCancelledError();
|
|
@@ -497,23 +548,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
497
548
|
throw new Error("Execution launch cancellation gate was not installed");
|
|
498
549
|
}
|
|
499
550
|
const installedCancel = launchControl.cancel;
|
|
500
|
-
|
|
501
|
-
type: "execution:started",
|
|
502
|
-
protocolVersion: 1,
|
|
503
|
-
executionId: spec.executionId,
|
|
504
|
-
at: startedAt,
|
|
505
|
-
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
506
|
-
if (!dependencies.cancellation?.isRequested()) {
|
|
507
|
-
timeout = setTimeout(() => {
|
|
508
|
-
timedOut = true;
|
|
509
|
-
try {
|
|
510
|
-
void installedCancel().catch(rejectCancellationFailure);
|
|
511
|
-
}
|
|
512
|
-
catch (error) {
|
|
513
|
-
rejectCancellationFailure(error);
|
|
514
|
-
}
|
|
515
|
-
}, effectiveTimeoutMs);
|
|
516
|
-
}
|
|
551
|
+
runtimeCancel = installedCancel;
|
|
517
552
|
return { ...guarded.handle, cancel: installedCancel };
|
|
518
553
|
}
|
|
519
554
|
finally {
|
|
@@ -522,6 +557,12 @@ export async function runExecution(config, input, dependencies) {
|
|
|
522
557
|
}
|
|
523
558
|
},
|
|
524
559
|
};
|
|
560
|
+
const systemPromptBudget = config.executionLimits.maxPromptBytes
|
|
561
|
+
- Buffer.byteLength(spec.instructions.wakePrompt, "utf8");
|
|
562
|
+
const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, systemPromptBudget);
|
|
563
|
+
const boundedSystemPrompt = (context) => appendAgentMemoryContext(typeof systemPromptWithLocalFacts === "string"
|
|
564
|
+
? systemPromptWithLocalFacts
|
|
565
|
+
: systemPromptWithLocalFacts(context), recalledMemory, systemPromptBudget);
|
|
525
566
|
const localInput = {
|
|
526
567
|
executionId: spec.executionId,
|
|
527
568
|
handle: spec.agent.handle,
|
|
@@ -531,8 +572,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
531
572
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
532
573
|
...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
|
|
533
574
|
...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
|
|
534
|
-
systemPrompt:
|
|
535
|
-
- Buffer.byteLength(spec.instructions.wakePrompt, "utf8")),
|
|
575
|
+
systemPrompt: boundedSystemPrompt,
|
|
536
576
|
wakePrompt: spec.instructions.wakePrompt,
|
|
537
577
|
runtime: {
|
|
538
578
|
name: spec.runtime.name,
|
|
@@ -570,6 +610,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
570
610
|
if (timeout !== undefined)
|
|
571
611
|
clearTimeout(timeout);
|
|
572
612
|
const finishedAt = now().toISOString();
|
|
613
|
+
if (result.exitCode === 0 && result.finalText?.trim())
|
|
614
|
+
memoryCaptureFinalText = result.finalText;
|
|
573
615
|
if (spec.reporting.allowBoundImDecision) {
|
|
574
616
|
const path = join(result.workspaceRunDir, `.bound-im-decision-${spec.executionId}.json`);
|
|
575
617
|
const selected = await readBoundImDecision(path);
|
|
@@ -654,6 +696,17 @@ export async function runExecution(config, input, dependencies) {
|
|
|
654
696
|
completion = boundExecutionFrame(completion, config.executionLimits.maxEventBytes);
|
|
655
697
|
await telemetry.closeAndDrain();
|
|
656
698
|
await dependencies.journal.complete(spec.executionId, completion);
|
|
699
|
+
if (completion.outcome === "succeeded"
|
|
700
|
+
&& spec.agent.memoryEnabled === true
|
|
701
|
+
&& memoryCaptureFinalText !== null
|
|
702
|
+
&& dependencies.agentMemory !== undefined) {
|
|
703
|
+
try {
|
|
704
|
+
await dependencies.agentMemory.capture(spec.agent.handle, spec.executionId, spec.instructions.wakePrompt, memoryCaptureFinalText);
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// External memory is optional and must never alter the durable execution outcome.
|
|
708
|
+
}
|
|
709
|
+
}
|
|
657
710
|
await dependencies.report(completion);
|
|
658
711
|
return { kind: "completed", frame: completion };
|
|
659
712
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { defaultProcessController } from "./execution-journal.js";
|
|
6
|
+
import { createJournalLease, createJournalLeaseRegistry, JournalLockedError, } from "./execution-journal-lock.js";
|
|
7
|
+
const HOST_EXECUTION_SLOT_COUNT = 4;
|
|
8
|
+
const HOST_STARTUP_GAP_MS = 3_000;
|
|
9
|
+
const RETRY_MS = 50;
|
|
10
|
+
export function defaultHostExecutionCoordinatorRoot(userHome = homedir()) {
|
|
11
|
+
// Deliberately independent of CREW_DAEMON_HOME and agentsRoot: every profile
|
|
12
|
+
// owned by this OS user must share the same physical-compute safety boundary.
|
|
13
|
+
return resolve(userHome, ".crew", "daemon", "compute-execution");
|
|
14
|
+
}
|
|
15
|
+
async function syncDirectory(path) {
|
|
16
|
+
if (process.platform === "win32")
|
|
17
|
+
return;
|
|
18
|
+
const handle = await open(path, "r");
|
|
19
|
+
try {
|
|
20
|
+
await handle.sync();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await handle.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function leaseFor(directory) {
|
|
27
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
28
|
+
const lease = createJournalLease({
|
|
29
|
+
directory,
|
|
30
|
+
currentPid: process.pid,
|
|
31
|
+
captureCurrentIdentity: async () => {
|
|
32
|
+
const identity = await defaultProcessController.inspectIdentity(process.pid);
|
|
33
|
+
if (identity === null)
|
|
34
|
+
throw new Error("current process identity is absent");
|
|
35
|
+
return identity;
|
|
36
|
+
},
|
|
37
|
+
inspectIdentity: (pid) => defaultProcessController.inspectIdentity(pid),
|
|
38
|
+
syncDirectory,
|
|
39
|
+
// A separate registry is intentional: reservations in one process must
|
|
40
|
+
// compete for the same slot instead of attaching to one re-entrant lease.
|
|
41
|
+
registry: createJournalLeaseRegistry(),
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
await lease.acquire();
|
|
45
|
+
return lease;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof JournalLockedError)
|
|
49
|
+
return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function reservation(prerequisite, acquire) {
|
|
54
|
+
let released = false;
|
|
55
|
+
let granted = false;
|
|
56
|
+
let owned = null;
|
|
57
|
+
let releasePromise = null;
|
|
58
|
+
let cancelPrerequisite;
|
|
59
|
+
const prerequisiteCancelled = new Promise((resolveCancelled) => {
|
|
60
|
+
cancelPrerequisite = () => resolveCancelled(false);
|
|
61
|
+
});
|
|
62
|
+
const acquisition = Promise.race([
|
|
63
|
+
prerequisite.then(() => true),
|
|
64
|
+
prerequisiteCancelled,
|
|
65
|
+
]).then(async (shouldAcquire) => {
|
|
66
|
+
if (!shouldAcquire)
|
|
67
|
+
return null;
|
|
68
|
+
const lease = await acquire(() => released);
|
|
69
|
+
if (lease === null)
|
|
70
|
+
return null;
|
|
71
|
+
if (released) {
|
|
72
|
+
await lease.close();
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
owned = lease;
|
|
76
|
+
granted = true;
|
|
77
|
+
return lease;
|
|
78
|
+
});
|
|
79
|
+
const neverReady = new Promise(() => { });
|
|
80
|
+
const ready = acquisition.then((lease) => lease === null ? neverReady : undefined);
|
|
81
|
+
void ready.catch(() => undefined);
|
|
82
|
+
return {
|
|
83
|
+
ready,
|
|
84
|
+
isQueued: () => !granted && !released,
|
|
85
|
+
release: () => {
|
|
86
|
+
if (releasePromise !== null)
|
|
87
|
+
return releasePromise;
|
|
88
|
+
released = true;
|
|
89
|
+
cancelPrerequisite();
|
|
90
|
+
releasePromise = acquisition.then(async () => { if (owned !== null)
|
|
91
|
+
await owned.close(); });
|
|
92
|
+
void releasePromise.catch(() => undefined);
|
|
93
|
+
return releasePromise;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function writeLaunchTimestamp(path, value) {
|
|
98
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(temporary, `${value}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
101
|
+
await rename(temporary, path);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await rm(temporary, { force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function createHostExecutionCoordinator(options = {}) {
|
|
108
|
+
const root = resolve(options.root ?? defaultHostExecutionCoordinatorRoot());
|
|
109
|
+
const executionSlots = options.executionSlots ?? HOST_EXECUTION_SLOT_COUNT;
|
|
110
|
+
const startupGapMs = options.startupGapMs ?? HOST_STARTUP_GAP_MS;
|
|
111
|
+
const retryMs = options.retryMs ?? RETRY_MS;
|
|
112
|
+
const now = options.now ?? Date.now;
|
|
113
|
+
const wait = options.wait ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
|
|
114
|
+
const pendingReleases = new Set();
|
|
115
|
+
if (!Number.isSafeInteger(executionSlots) || executionSlots <= 0) {
|
|
116
|
+
throw new RangeError("executionSlots must be a positive safe integer");
|
|
117
|
+
}
|
|
118
|
+
if (!Number.isFinite(startupGapMs) || startupGapMs < 0) {
|
|
119
|
+
throw new RangeError("startupGapMs must be a nonnegative finite number");
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(retryMs) || retryMs <= 0) {
|
|
122
|
+
throw new RangeError("retryMs must be a positive finite number");
|
|
123
|
+
}
|
|
124
|
+
const acquireExecution = async (released) => {
|
|
125
|
+
let offset = 0;
|
|
126
|
+
while (!released()) {
|
|
127
|
+
for (let step = 0; step < executionSlots; step += 1) {
|
|
128
|
+
const slot = (offset + step) % executionSlots;
|
|
129
|
+
const lease = await leaseFor(join(root, "slots", String(slot)));
|
|
130
|
+
if (lease !== null)
|
|
131
|
+
return lease;
|
|
132
|
+
}
|
|
133
|
+
offset = (offset + 1) % executionSlots;
|
|
134
|
+
await wait(retryMs);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
const acquireStartup = async (released) => {
|
|
139
|
+
while (!released()) {
|
|
140
|
+
const directory = join(root, "startup");
|
|
141
|
+
const lease = await leaseFor(directory);
|
|
142
|
+
if (lease === null) {
|
|
143
|
+
await wait(retryMs);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const timestampPath = join(directory, "last-launch-at");
|
|
147
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
148
|
+
try {
|
|
149
|
+
previous = Number.parseInt(await readFile(timestampPath, "utf8"), 10);
|
|
150
|
+
if (!Number.isFinite(previous))
|
|
151
|
+
previous = Number.NEGATIVE_INFINITY;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
155
|
+
await lease.close();
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const delay = Math.max(0, previous + startupGapMs - now());
|
|
160
|
+
if (delay > 0)
|
|
161
|
+
await wait(delay);
|
|
162
|
+
if (released()) {
|
|
163
|
+
await lease.close();
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
await writeLaunchTimestamp(timestampPath, now());
|
|
167
|
+
return lease;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
};
|
|
171
|
+
const tracked = (entry) => {
|
|
172
|
+
let trackedRelease = null;
|
|
173
|
+
return {
|
|
174
|
+
...entry,
|
|
175
|
+
release: () => {
|
|
176
|
+
if (trackedRelease !== null)
|
|
177
|
+
return trackedRelease;
|
|
178
|
+
trackedRelease = entry.release();
|
|
179
|
+
pendingReleases.add(trackedRelease);
|
|
180
|
+
void trackedRelease.then(() => pendingReleases.delete(trackedRelease), () => pendingReleases.delete(trackedRelease));
|
|
181
|
+
return trackedRelease;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
reserveExecution: (prerequisite) => tracked(reservation(prerequisite, acquireExecution)),
|
|
187
|
+
reserveStartup: (prerequisite) => tracked(reservation(prerequisite, acquireStartup)),
|
|
188
|
+
drain: async () => {
|
|
189
|
+
while (pendingReleases.size > 0)
|
|
190
|
+
await Promise.all([...pendingReleases]);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function hostCoordinatedSlotManager(local, host) {
|
|
195
|
+
return {
|
|
196
|
+
reserve: (handle, kind) => {
|
|
197
|
+
const localReservation = local.reserve(handle, kind);
|
|
198
|
+
if (!localReservation.accepted)
|
|
199
|
+
return localReservation;
|
|
200
|
+
const hostReservation = host.reserveExecution(localReservation.ready);
|
|
201
|
+
let released = false;
|
|
202
|
+
return {
|
|
203
|
+
...localReservation,
|
|
204
|
+
// File ownership is asynchronous, so accepted work is conservatively
|
|
205
|
+
// reported queued until it owns both its local and host-wide slot.
|
|
206
|
+
state: "queued",
|
|
207
|
+
ready: hostReservation.ready,
|
|
208
|
+
isQueued: hostReservation.isQueued,
|
|
209
|
+
release: () => {
|
|
210
|
+
if (released)
|
|
211
|
+
return;
|
|
212
|
+
released = true;
|
|
213
|
+
void hostReservation.release();
|
|
214
|
+
localReservation.release();
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
snapshot: local.snapshot,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function hostCoordinatedStartupGate(local, host) {
|
|
222
|
+
return {
|
|
223
|
+
reserve: (runtime) => {
|
|
224
|
+
const localReservation = local.reserve(runtime);
|
|
225
|
+
const hostReservation = host.reserveStartup(localReservation.ready);
|
|
226
|
+
let released = false;
|
|
227
|
+
return {
|
|
228
|
+
ready: hostReservation.ready,
|
|
229
|
+
isQueued: hostReservation.isQueued,
|
|
230
|
+
release: () => {
|
|
231
|
+
if (released)
|
|
232
|
+
return;
|
|
233
|
+
released = true;
|
|
234
|
+
void hostReservation.release();
|
|
235
|
+
localReservation.release();
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
snapshot: local.snapshot,
|
|
240
|
+
};
|
|
241
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -16,6 +16,8 @@ import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder
|
|
|
16
16
|
import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
|
|
17
17
|
import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
|
|
18
18
|
import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
19
|
+
import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
|
|
20
|
+
import { dslog } from "./slog.js";
|
|
19
21
|
function truncateUtf8(value, maxBytes) {
|
|
20
22
|
if (maxBytes <= 0)
|
|
21
23
|
return "";
|
|
@@ -198,6 +200,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
198
200
|
}), dependencies.cancellation);
|
|
199
201
|
let materialized = null;
|
|
200
202
|
let knownAttachmentDirectory = null;
|
|
203
|
+
let startupReservation = null;
|
|
201
204
|
try {
|
|
202
205
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
203
206
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -263,8 +266,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
263
266
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
264
267
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
265
268
|
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
269
|
+
const inheritedEnv = { ...process.env };
|
|
270
|
+
for (const key of Object.keys(inheritedEnv)) {
|
|
271
|
+
if (key.startsWith("CREW_AGENT_MEMORY_"))
|
|
272
|
+
delete inheritedEnv[key];
|
|
273
|
+
}
|
|
266
274
|
const baseEnv = {
|
|
267
|
-
...
|
|
275
|
+
...inheritedEnv,
|
|
268
276
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
269
277
|
...input.launch.systemEnv,
|
|
270
278
|
PATH: `${workspace.crewDir}${delimiter}${augmentedPath()}`,
|
|
@@ -288,6 +296,28 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
288
296
|
const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
|
|
289
297
|
if (dependencies.cancellation?.isRequested())
|
|
290
298
|
throw new RuntimeCancelledError();
|
|
299
|
+
startupReservation = dependencies.startupGate?.reserve(runtime.name) ?? null;
|
|
300
|
+
if (startupReservation !== null) {
|
|
301
|
+
const startupQueueEnteredAt = Date.now();
|
|
302
|
+
dslog("runtime.start_queued", "runtime 等待启动许可", {
|
|
303
|
+
execution_id: input.executionId,
|
|
304
|
+
runtime: runtime.name,
|
|
305
|
+
queued: startupReservation.isQueued(),
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
await awaitWithCancellation(startupReservation.ready, dependencies.cancellation);
|
|
309
|
+
dslog("runtime.start_granted", "runtime 获得启动许可", {
|
|
310
|
+
execution_id: input.executionId,
|
|
311
|
+
runtime: runtime.name,
|
|
312
|
+
startup_queue_ms: Date.now() - startupQueueEnteredAt,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
startupReservation.release();
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const runtimeLaunchAt = Date.now();
|
|
291
321
|
const child = await launchRuntime({
|
|
292
322
|
runtime: runtime.name,
|
|
293
323
|
bin: runtime.name,
|
|
@@ -321,11 +351,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
321
351
|
const externalOutput = new ExternalAnswerDecoder();
|
|
322
352
|
// 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
|
|
323
353
|
const consoleFormatter = createConsoleFormatter();
|
|
354
|
+
let runtimeReady = false;
|
|
355
|
+
let resolveRuntimeReady;
|
|
356
|
+
const runtimeReadySignal = new Promise((resolve) => { resolveRuntimeReady = resolve; });
|
|
357
|
+
let runtimeReadyNotification = Promise.resolve();
|
|
358
|
+
const markRuntimeReady = () => {
|
|
359
|
+
if (runtimeReady)
|
|
360
|
+
return;
|
|
361
|
+
runtimeReady = true;
|
|
362
|
+
startupReservation?.release();
|
|
363
|
+
dslog("runtime.start_ready", "runtime 已完成初始化", {
|
|
364
|
+
execution_id: input.executionId,
|
|
365
|
+
runtime: runtime.name,
|
|
366
|
+
startup_ms: Date.now() - runtimeLaunchAt,
|
|
367
|
+
});
|
|
368
|
+
runtimeReadyNotification = Promise.resolve(callbacks.onRuntimeReady?.(runtime.name));
|
|
369
|
+
resolveRuntimeReady();
|
|
370
|
+
};
|
|
324
371
|
const readline = createInterface({ input: child.stdout });
|
|
325
372
|
readline.on("line", (line) => {
|
|
326
373
|
const event = parseLine(line);
|
|
327
374
|
if (!event)
|
|
328
375
|
return;
|
|
376
|
+
if (isRuntimeReadyEvent(runtime.name, event))
|
|
377
|
+
markRuntimeReady();
|
|
329
378
|
const meta = extractRunMeta(event);
|
|
330
379
|
if (meta.sessionId)
|
|
331
380
|
sessionId = meta.sessionId;
|
|
@@ -358,6 +407,39 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
358
407
|
});
|
|
359
408
|
let runtimeExit;
|
|
360
409
|
try {
|
|
410
|
+
if (startupReservation !== null) {
|
|
411
|
+
let startupTimer;
|
|
412
|
+
let startupOutcome;
|
|
413
|
+
try {
|
|
414
|
+
startupOutcome = await awaitWithCancellation(Promise.race([
|
|
415
|
+
runtimeReadySignal.then(() => ({ kind: "ready" })),
|
|
416
|
+
child.exit.then((exit) => ({ kind: "exit", exit })),
|
|
417
|
+
new Promise((resolve) => {
|
|
418
|
+
startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ?? 60_000);
|
|
419
|
+
}),
|
|
420
|
+
]), dependencies.cancellation);
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
if (startupTimer !== undefined)
|
|
424
|
+
clearTimeout(startupTimer);
|
|
425
|
+
}
|
|
426
|
+
if (startupOutcome.kind !== "ready") {
|
|
427
|
+
if (startupOutcome.kind === "timeout") {
|
|
428
|
+
dslog("runtime.start_timeout", "runtime 启动超时", {
|
|
429
|
+
level: "ERROR",
|
|
430
|
+
execution_id: input.executionId,
|
|
431
|
+
runtime: runtime.name,
|
|
432
|
+
startup_timeout_ms: dependencies.startupTimeoutMs ?? 60_000,
|
|
433
|
+
});
|
|
434
|
+
await child.cancel?.();
|
|
435
|
+
}
|
|
436
|
+
startupReservation.release();
|
|
437
|
+
throw new Error(startupOutcome.kind === "timeout"
|
|
438
|
+
? `${runtime.name} startup timed out`
|
|
439
|
+
: `${runtime.name} exited before signaling ready (exit ${startupOutcome.exit.exitCode})`);
|
|
440
|
+
}
|
|
441
|
+
await runtimeReadyNotification;
|
|
442
|
+
}
|
|
361
443
|
runtimeExit = await awaitWithCancellation(child.exit, dependencies.cancellation);
|
|
362
444
|
}
|
|
363
445
|
catch (error) {
|
|
@@ -415,6 +497,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
415
497
|
};
|
|
416
498
|
}
|
|
417
499
|
finally {
|
|
500
|
+
startupReservation?.release();
|
|
418
501
|
const attachmentDirectories = new Set([
|
|
419
502
|
...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
|
|
420
503
|
...(materialized === null ? [] : [materialized.directory]),
|
package/dist/machine-info.js
CHANGED
|
@@ -25,6 +25,8 @@ export const DAEMON_CAPABILITIES = [
|
|
|
25
25
|
"execution_external_output_v1",
|
|
26
26
|
"execution_attachments_v1",
|
|
27
27
|
"execution_answer_stream_v1",
|
|
28
|
+
"execution_machine_queue_v1",
|
|
29
|
+
"execution_agent_memory_policy_v1",
|
|
28
30
|
];
|
|
29
31
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
30
32
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
@@ -117,7 +119,21 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
117
119
|
capabilities: DAEMON_CAPABILITIES,
|
|
118
120
|
...(backend.supported ? {
|
|
119
121
|
executionProtocol: EXECUTION_PROTOCOL,
|
|
120
|
-
executionLimits: Object.freeze({
|
|
122
|
+
executionLimits: Object.freeze({
|
|
123
|
+
maxPromptBytes: executionLimits.maxPromptBytes,
|
|
124
|
+
maxTimeoutMs: executionLimits.maxTimeoutMs,
|
|
125
|
+
maxEventBytes: executionLimits.maxEventBytes,
|
|
126
|
+
maxParallelPerAgent: executionLimits.maxParallelPerAgent,
|
|
127
|
+
maxQueuedPerAgent: executionLimits.maxQueuedPerAgent,
|
|
128
|
+
}),
|
|
129
|
+
executionScheduler: Object.freeze({
|
|
130
|
+
maxParallelTotal: executionLimits.maxParallelTotal,
|
|
131
|
+
maxQueuedTotal: executionLimits.maxQueuedTotal,
|
|
132
|
+
maxStartingTotal: executionLimits.maxStartingTotal,
|
|
133
|
+
maxStartingPerRuntime: executionLimits.maxStartingPerRuntime,
|
|
134
|
+
startupGapMs: executionLimits.startupGapMs,
|
|
135
|
+
startupTimeoutMs: executionLimits.startupTimeoutMs,
|
|
136
|
+
}),
|
|
121
137
|
} : {}),
|
|
122
138
|
agentHandles,
|
|
123
139
|
};
|
package/dist/runner.js
CHANGED
|
@@ -106,6 +106,10 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
106
106
|
}, { onActivity, onConsole }, {
|
|
107
107
|
launchRuntime: dependencies.launchRuntime ?? ((request) => launchSupervisedRuntime(request, dependencies.startSupervisor, dependencies.cancellation, dependencies.platform)),
|
|
108
108
|
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
109
|
+
...(dependencies.startupGate === undefined ? {} : { startupGate: dependencies.startupGate }),
|
|
110
|
+
...(dependencies.startupTimeoutMs === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { startupTimeoutMs: dependencies.startupTimeoutMs }),
|
|
109
113
|
});
|
|
110
114
|
const activities = [...local.activities];
|
|
111
115
|
if (!input.scheduled && (runtime === "codex" || runtime === "kimi")
|