@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.
@@ -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(),
@@ -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
- ? dependencies.facts.activeForAgent >= config.executionLimits.maxParallelPerAgent
232
+ ? agentAtCapacity || machineAtCapacity
226
233
  : dependencies.slot?.state === "queued"
227
- ? dependencies.facts.activeForAgent < config.executionLimits.maxParallelPerAgent
228
- || dependencies.facts.queuedForAgent >= config.executionLimits.maxQueuedPerAgent
234
+ ? dependencies.facts.queuedForAgent >= config.executionLimits.maxQueuedPerAgent
235
+ || dependencies.facts.queuedTotal >= config.executionLimits.maxQueuedTotal
229
236
  : false;
230
- if (slotInvalid || (dependencies.slot === undefined && (dependencies.facts.activeForAgent >= config.executionLimits.maxParallelPerAgent
231
- || dependencies.facts.queuedForAgent >= config.executionLimits.maxQueuedPerAgent))) {
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.processStartedAt !== null) {
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.processStartedAt,
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.processStartedAt !== null) {
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.processStartedAt,
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: ({ entry, handle, abort }) => {
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
- await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
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: withLocalExecutionFacts(spec.instructions.systemPrompt, config.executionLimits.maxPromptBytes
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
+ }
@@ -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
- ...process.env,
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]),
@@ -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({ ...executionLimits }),
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")