@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export function createRuntimeStartupGate(limits, now = Date.now) {
|
|
2
|
+
const activeByRuntime = new Map();
|
|
3
|
+
const queue = [];
|
|
4
|
+
let activeTotal = 0;
|
|
5
|
+
let lastLaunchAt = Number.NEGATIVE_INFINITY;
|
|
6
|
+
const promote = () => {
|
|
7
|
+
while (activeTotal < limits.maxStartingTotal) {
|
|
8
|
+
const index = queue.findIndex((entry) => !entry.released
|
|
9
|
+
&& (activeByRuntime.get(entry.runtime) ?? 0) < limits.maxStartingPerRuntime);
|
|
10
|
+
if (index < 0)
|
|
11
|
+
return;
|
|
12
|
+
const [next] = queue.splice(index, 1);
|
|
13
|
+
if (!next || next.released)
|
|
14
|
+
continue;
|
|
15
|
+
next.promoted = true;
|
|
16
|
+
activeTotal += 1;
|
|
17
|
+
activeByRuntime.set(next.runtime, (activeByRuntime.get(next.runtime) ?? 0) + 1);
|
|
18
|
+
const grant = () => {
|
|
19
|
+
next.timer = null;
|
|
20
|
+
if (next.released)
|
|
21
|
+
return;
|
|
22
|
+
next.launchGranted = true;
|
|
23
|
+
lastLaunchAt = now();
|
|
24
|
+
next.resolve();
|
|
25
|
+
};
|
|
26
|
+
const delay = Math.max(0, lastLaunchAt + limits.startupGapMs - now());
|
|
27
|
+
if (delay === 0)
|
|
28
|
+
grant();
|
|
29
|
+
else
|
|
30
|
+
next.timer = setTimeout(grant, delay);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
reserve: (runtime) => {
|
|
35
|
+
let resolveReady;
|
|
36
|
+
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
37
|
+
const entry = {
|
|
38
|
+
runtime,
|
|
39
|
+
released: false,
|
|
40
|
+
promoted: false,
|
|
41
|
+
launchGranted: false,
|
|
42
|
+
timer: null,
|
|
43
|
+
resolve: resolveReady,
|
|
44
|
+
};
|
|
45
|
+
queue.push(entry);
|
|
46
|
+
promote();
|
|
47
|
+
return {
|
|
48
|
+
ready,
|
|
49
|
+
isQueued: () => !entry.launchGranted && !entry.released,
|
|
50
|
+
release: () => {
|
|
51
|
+
if (entry.released)
|
|
52
|
+
return;
|
|
53
|
+
entry.released = true;
|
|
54
|
+
if (entry.timer !== null)
|
|
55
|
+
clearTimeout(entry.timer);
|
|
56
|
+
if (entry.promoted) {
|
|
57
|
+
activeTotal = Math.max(0, activeTotal - 1);
|
|
58
|
+
const nextForRuntime = Math.max(0, (activeByRuntime.get(runtime) ?? 1) - 1);
|
|
59
|
+
if (nextForRuntime === 0)
|
|
60
|
+
activeByRuntime.delete(runtime);
|
|
61
|
+
else
|
|
62
|
+
activeByRuntime.set(runtime, nextForRuntime);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const index = queue.indexOf(entry);
|
|
66
|
+
if (index >= 0)
|
|
67
|
+
queue.splice(index, 1);
|
|
68
|
+
}
|
|
69
|
+
promote();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
snapshot: () => ({
|
|
74
|
+
startingTotal: activeTotal,
|
|
75
|
+
queuedTotal: queue.length,
|
|
76
|
+
startingByRuntime: {
|
|
77
|
+
claude: activeByRuntime.get("claude") ?? 0,
|
|
78
|
+
codex: activeByRuntime.get("codex") ?? 0,
|
|
79
|
+
kimi: activeByRuntime.get("kimi") ?? 0,
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function isRuntimeReadyEvent(runtime, event) {
|
|
85
|
+
if (typeof event !== "object" || event === null)
|
|
86
|
+
return false;
|
|
87
|
+
const value = event;
|
|
88
|
+
if (runtime === "claude")
|
|
89
|
+
return value.type === "system" && value.subtype === "init";
|
|
90
|
+
return value.type === "thread.started";
|
|
91
|
+
}
|
|
@@ -24,7 +24,24 @@ const STDERR_TAIL_CAP = 1_200;
|
|
|
24
24
|
const STDERR_LINE_CAPTURE_CAP = 1_200;
|
|
25
25
|
const STDERR_LINE_OMITTED = "[stderr line omitted: exceeded capture limit]\n";
|
|
26
26
|
const MAX_INITIALIZE_ATTEMPTS = 2;
|
|
27
|
+
// 模型网关瞬态故障(过载/限流)导致 turn 失败时,整轮重试(15s→45s 递进退避):codex 自身
|
|
28
|
+
// 的重试窗口只有 ~10-30s,网关过载往往持续数分钟,这里再兜一层,否则 agent 直接失败
|
|
29
|
+
// 不回复(2026-08-10 事故,普通会话与定时任务都中招)。
|
|
30
|
+
const MAX_TRANSIENT_TURN_RETRIES = 2;
|
|
31
|
+
const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
|
|
32
|
+
const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
|
|
27
33
|
const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
|
|
34
|
+
const TRANSIENT_TURN_ERROR_PATTERNS = [
|
|
35
|
+
/at capacity/i,
|
|
36
|
+
/overloaded/i,
|
|
37
|
+
/rate.?limit/i,
|
|
38
|
+
/too many requests/i,
|
|
39
|
+
];
|
|
40
|
+
/** 模型侧瞬态失败(网关过载/限流)→ 换个时间整轮重试大概率成功。
|
|
41
|
+
* interrupted 不算:那是取消信号或 codex collab 连带中断,重跑语义不明确。 */
|
|
42
|
+
export function isTransientTurnFailure(detail) {
|
|
43
|
+
return TRANSIENT_TURN_ERROR_PATTERNS.some((pattern) => pattern.test(detail));
|
|
44
|
+
}
|
|
28
45
|
class CodexRpcTimeoutError extends Error {
|
|
29
46
|
method;
|
|
30
47
|
timeoutMs;
|
|
@@ -469,6 +486,9 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
469
486
|
return {
|
|
470
487
|
code: completed.turn?.status === "interrupted" ? 130 : 1,
|
|
471
488
|
initializeTimedOut: false,
|
|
489
|
+
transientTurnFailure: !cancelling
|
|
490
|
+
&& completed.turn?.status === "failed"
|
|
491
|
+
&& isTransientTurnFailure(detail),
|
|
472
492
|
};
|
|
473
493
|
}
|
|
474
494
|
catch (error) {
|
|
@@ -501,13 +521,27 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
501
521
|
export async function runCodexAppServer(bin, options = {}) {
|
|
502
522
|
const input = await readRunnerInput();
|
|
503
523
|
const initializeTimeoutMs = Math.min(options.initializeTimeoutMs ?? INITIALIZE_RPC_TIMEOUT_MS, INITIALIZE_RPC_TIMEOUT_MS);
|
|
504
|
-
|
|
524
|
+
const turnRetryDelayMs = options.turnRetryDelayMs ?? TRANSIENT_TURN_RETRY_DELAY_MS;
|
|
525
|
+
let initializeTimeouts = 0;
|
|
526
|
+
let turnRetries = 0;
|
|
527
|
+
for (let attempt = 1;; attempt += 1) {
|
|
505
528
|
const result = await runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs);
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
529
|
+
if (result.initializeTimedOut) {
|
|
530
|
+
initializeTimeouts += 1;
|
|
531
|
+
if (initializeTimeouts >= MAX_INITIALIZE_ATTEMPTS)
|
|
532
|
+
return result.code;
|
|
533
|
+
process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=initialize_timeout\n`);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (result.transientTurnFailure && turnRetries < MAX_TRANSIENT_TURN_RETRIES) {
|
|
537
|
+
const delayMs = turnRetryDelayMs * TRANSIENT_TURN_RETRY_BACKOFF_FACTOR ** turnRetries;
|
|
538
|
+
turnRetries += 1;
|
|
539
|
+
process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=transient_turn_failure\n`);
|
|
540
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
return result.code;
|
|
509
544
|
}
|
|
510
|
-
return 1;
|
|
511
545
|
}
|
|
512
546
|
function configFromArgv(argv) {
|
|
513
547
|
const { values } = parseArgs({
|
|
@@ -515,24 +549,34 @@ function configFromArgv(argv) {
|
|
|
515
549
|
options: {
|
|
516
550
|
bin: { type: "string" },
|
|
517
551
|
"initialize-timeout-ms": { type: "string" },
|
|
552
|
+
"turn-retry-delay-ms": { type: "string" },
|
|
518
553
|
},
|
|
519
554
|
});
|
|
520
555
|
if (!values.bin)
|
|
521
556
|
throw new Error("--bin is required");
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
557
|
+
const positiveInteger = (raw, flag) => {
|
|
558
|
+
if (raw === undefined)
|
|
559
|
+
return undefined;
|
|
560
|
+
const value = Number(raw);
|
|
561
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
562
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
563
|
+
}
|
|
564
|
+
return value;
|
|
565
|
+
};
|
|
566
|
+
const initializeTimeoutMs = positiveInteger(values["initialize-timeout-ms"], "--initialize-timeout-ms");
|
|
567
|
+
const turnRetryDelayMs = positiveInteger(values["turn-retry-delay-ms"], "--turn-retry-delay-ms");
|
|
568
|
+
return {
|
|
569
|
+
bin: values.bin,
|
|
570
|
+
...(initializeTimeoutMs === undefined ? {} : { initializeTimeoutMs }),
|
|
571
|
+
...(turnRetryDelayMs === undefined ? {} : { turnRetryDelayMs }),
|
|
572
|
+
};
|
|
530
573
|
}
|
|
531
574
|
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
532
575
|
const config = configFromArgv(process.argv.slice(2));
|
|
533
|
-
runCodexAppServer(config.bin,
|
|
534
|
-
? {}
|
|
535
|
-
: {
|
|
576
|
+
runCodexAppServer(config.bin, {
|
|
577
|
+
...(config.initializeTimeoutMs === undefined ? {} : { initializeTimeoutMs: config.initializeTimeoutMs }),
|
|
578
|
+
...(config.turnRetryDelayMs === undefined ? {} : { turnRetryDelayMs: config.turnRetryDelayMs }),
|
|
579
|
+
})
|
|
536
580
|
.then((code) => { process.exitCode = code; })
|
|
537
581
|
.catch((error) => {
|
|
538
582
|
process.stderr.write(`Codex app-server runner failed: ${safeErrorMessage(error, [])}\n`);
|
package/dist/serve.js
CHANGED
|
@@ -28,6 +28,9 @@ import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
|
|
|
28
28
|
import { reconcileExecutionJournal } from "./execution-recovery.js";
|
|
29
29
|
import { createSharedSlotManager } from "./shared-execution-slots.js";
|
|
30
30
|
import { createCompletionRetransmitter } from "./completion-retransmitter.js";
|
|
31
|
+
import { createAgentMemoryBridge } from "./agent-memory/bridge.js";
|
|
32
|
+
import { createRuntimeStartupGate } from "./runtime-startup-gate.js";
|
|
33
|
+
import { createHostExecutionCoordinator, hostCoordinatedSlotManager, hostCoordinatedStartupGate, } from "./host-execution-coordinator.js";
|
|
31
34
|
// normalize.ts 的活动种类 → activity 枚举
|
|
32
35
|
const ACTIVITY_MAP = {
|
|
33
36
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
@@ -56,11 +59,15 @@ export function serve(config, opts = {}) {
|
|
|
56
59
|
let reconnectTimer = null;
|
|
57
60
|
let stopPromise = null;
|
|
58
61
|
const executionJournal = opts.execution?.journal ?? createExecutionJournal(config.agentsRoot);
|
|
62
|
+
const agentMemory = opts.execution?.dependencies?.agentMemory
|
|
63
|
+
?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
|
|
59
64
|
const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
|
|
60
65
|
const executeProtocol = opts.execution?.runExecution ?? runExecution;
|
|
61
66
|
let detectedExecutionRuntimes = [];
|
|
62
67
|
let runtimeFacts = null;
|
|
63
|
-
const
|
|
68
|
+
const hostCoordinator = opts.execution?.hostCoordinator ?? createHostExecutionCoordinator();
|
|
69
|
+
const sharedSlots = hostCoordinatedSlotManager(createSharedSlotManager(config.executionLimits), hostCoordinator);
|
|
70
|
+
const runtimeStartupGate = hostCoordinatedStartupGate(createRuntimeStartupGate(config.executionLimits), hostCoordinator);
|
|
64
71
|
const knownExecutionHashes = new Map();
|
|
65
72
|
const executionReservations = new Map();
|
|
66
73
|
const executionRuns = new Map();
|
|
@@ -115,8 +122,11 @@ export function serve(config, opts = {}) {
|
|
|
115
122
|
if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
|
|
116
123
|
return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
|
|
117
124
|
}
|
|
125
|
+
if (entry.state === "running" && entry.runtimeReadyAt !== null) {
|
|
126
|
+
return { executionId: entry.executionId, state: "running", updatedAt: entry.runtimeReadyAt };
|
|
127
|
+
}
|
|
118
128
|
if (entry.state === "accepted" || entry.state === "running") {
|
|
119
|
-
return { executionId: entry.executionId, state:
|
|
129
|
+
return { executionId: entry.executionId, state: "accepted", updatedAt: entry.acceptedAt };
|
|
120
130
|
}
|
|
121
131
|
return null;
|
|
122
132
|
};
|
|
@@ -137,10 +147,10 @@ export function serve(config, opts = {}) {
|
|
|
137
147
|
state: acceptanceState, effectivePermission: entry.effectivePermission ?? "workspace_write",
|
|
138
148
|
at: entry.acceptedAt,
|
|
139
149
|
});
|
|
140
|
-
if (entry.state === "running" && entry.
|
|
150
|
+
if (entry.state === "running" && entry.runtimeReadyAt !== null) {
|
|
141
151
|
safeExecutionSend({
|
|
142
152
|
type: "execution:started", protocolVersion: 1,
|
|
143
|
-
executionId: entry.executionId, at: entry.
|
|
153
|
+
executionId: entry.executionId, at: entry.runtimeReadyAt,
|
|
144
154
|
});
|
|
145
155
|
}
|
|
146
156
|
}
|
|
@@ -335,8 +345,30 @@ export function serve(config, opts = {}) {
|
|
|
335
345
|
}
|
|
336
346
|
return;
|
|
337
347
|
}
|
|
338
|
-
knownExecutionHashes.set(spec.executionId, hash);
|
|
339
348
|
const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
|
|
349
|
+
if (!reservation.accepted) {
|
|
350
|
+
dslog("execution.machine_queue_rejected", "机器执行队列已满", {
|
|
351
|
+
level: "WARN",
|
|
352
|
+
execution_id: spec.executionId,
|
|
353
|
+
agent_handle: spec.agent.handle,
|
|
354
|
+
...reservation.facts,
|
|
355
|
+
});
|
|
356
|
+
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
357
|
+
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
358
|
+
reason: "resource_limit", message: "Local machine execution queue is full",
|
|
359
|
+
at: new Date().toISOString(),
|
|
360
|
+
}));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
knownExecutionHashes.set(spec.executionId, hash);
|
|
364
|
+
const machineQueueEnteredAt = Date.now();
|
|
365
|
+
if (reservation.isQueued()) {
|
|
366
|
+
dslog("execution.machine_queued", "execution 已进入机器队列", {
|
|
367
|
+
execution_id: spec.executionId,
|
|
368
|
+
agent_handle: spec.agent.handle,
|
|
369
|
+
...reservation.facts,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
340
372
|
executionReservations.set(spec.executionId, reservation);
|
|
341
373
|
const cancellation = cancellationFor(spec.executionId);
|
|
342
374
|
const cleanupExecutionReservation = () => {
|
|
@@ -365,14 +397,29 @@ export function serve(config, opts = {}) {
|
|
|
365
397
|
}
|
|
366
398
|
const execution = executeProtocol(config, spec, {
|
|
367
399
|
...opts.execution?.dependencies,
|
|
400
|
+
...(agentMemory === undefined ? {} : { agentMemory }),
|
|
368
401
|
journal: executionJournal,
|
|
369
402
|
facts: {
|
|
370
403
|
availableRuntimes,
|
|
371
404
|
...reservation.facts,
|
|
372
405
|
},
|
|
373
406
|
report: reportExecutionFrame,
|
|
407
|
+
startupGate: runtimeStartupGate,
|
|
408
|
+
startupTimeoutMs: config.executionLimits.startupTimeoutMs,
|
|
374
409
|
...(reservation.state === undefined ? {} : {
|
|
375
|
-
slot: {
|
|
410
|
+
slot: {
|
|
411
|
+
state: reservation.state,
|
|
412
|
+
ready: reservation.ready.then(() => {
|
|
413
|
+
const snapshot = sharedSlots.snapshot();
|
|
414
|
+
dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
|
|
415
|
+
execution_id: spec.executionId,
|
|
416
|
+
agent_handle: spec.agent.handle,
|
|
417
|
+
queue_ms: Date.now() - machineQueueEnteredAt,
|
|
418
|
+
active_total: snapshot.activeTotal,
|
|
419
|
+
queued_total: snapshot.queuedTotal,
|
|
420
|
+
});
|
|
421
|
+
}),
|
|
422
|
+
},
|
|
376
423
|
}),
|
|
377
424
|
cancellation,
|
|
378
425
|
}).finally(() => {
|
|
@@ -539,12 +586,26 @@ export function serve(config, opts = {}) {
|
|
|
539
586
|
}
|
|
540
587
|
running.add(key);
|
|
541
588
|
legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy");
|
|
589
|
+
if (legacyReservation.isQueued()) {
|
|
590
|
+
const snapshot = sharedSlots.snapshot();
|
|
591
|
+
dslog("run.machine_queued", "legacy run 已进入机器队列", {
|
|
592
|
+
...runKeys,
|
|
593
|
+
active_total: snapshot.activeTotal,
|
|
594
|
+
queued_total: snapshot.queuedTotal,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
542
597
|
await awaitWithCancellation(legacyReservation.ready, controller.cancellation);
|
|
543
598
|
const queueMs = Date.now() - queueWaitStart;
|
|
599
|
+
const machineSnapshot = sharedSlots.snapshot();
|
|
544
600
|
const threadLabel = threadId ?? null;
|
|
545
601
|
const from = msg.wake?.senderHandle ?? "?";
|
|
546
602
|
const incoming = msg.wake?.content ?? "";
|
|
547
|
-
dslog("run.start", `开始运行 ${msg.agentHandle}`, {
|
|
603
|
+
dslog("run.start", `开始运行 ${msg.agentHandle}`, {
|
|
604
|
+
...runKeys,
|
|
605
|
+
queue_ms: queueMs,
|
|
606
|
+
active_total: machineSnapshot.activeTotal,
|
|
607
|
+
queued_total: machineSnapshot.queuedTotal,
|
|
608
|
+
});
|
|
548
609
|
log(`\n${"─".repeat(56)}`);
|
|
549
610
|
log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
|
|
550
611
|
log(` channel = ${msg.channelId}`);
|
|
@@ -650,7 +711,11 @@ export function serve(config, opts = {}) {
|
|
|
650
711
|
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
651
712
|
...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
|
|
652
713
|
...(attemptWake ? { wake: attemptWake } : {}),
|
|
653
|
-
}, reportActivity, reportConsole, {
|
|
714
|
+
}, reportActivity, reportConsole, {
|
|
715
|
+
cancellation: controller.cancellation,
|
|
716
|
+
startupGate: runtimeStartupGate,
|
|
717
|
+
startupTimeoutMs: config.executionLimits.startupTimeoutMs,
|
|
718
|
+
});
|
|
654
719
|
});
|
|
655
720
|
const result = mergeRunAgentResults(guarded.results);
|
|
656
721
|
// 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
|
|
@@ -848,6 +913,7 @@ export function serve(config, opts = {}) {
|
|
|
848
913
|
for (const run of legacyRuns.values())
|
|
849
914
|
run.controller.request();
|
|
850
915
|
await deadline.waitFor(Promise.all(pending));
|
|
916
|
+
await deadline.waitFor(hostCoordinator.drain());
|
|
851
917
|
await executionJournal.close({ signal: deadline.signal });
|
|
852
918
|
}
|
|
853
919
|
finally {
|
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
export function createSharedSlotManager(limits) {
|
|
2
2
|
const activeByHandle = new Map();
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const queue = [];
|
|
4
|
+
let activeTotal = 0;
|
|
5
|
+
const queuedFor = (handle) => queue.filter((entry) => !entry.released && entry.handle === handle).length;
|
|
6
|
+
const promote = () => {
|
|
7
|
+
while (activeTotal < limits.maxParallelTotal) {
|
|
8
|
+
const index = queue.findIndex((entry) => !entry.released
|
|
9
|
+
&& (activeByHandle.get(entry.handle) ?? 0) < limits.maxParallelPerAgent);
|
|
10
|
+
if (index < 0)
|
|
11
|
+
return;
|
|
12
|
+
const [next] = queue.splice(index, 1);
|
|
13
|
+
if (!next || next.released)
|
|
9
14
|
continue;
|
|
10
15
|
next.promoted = true;
|
|
11
|
-
|
|
16
|
+
activeTotal += 1;
|
|
17
|
+
activeByHandle.set(next.handle, (activeByHandle.get(next.handle) ?? 0) + 1);
|
|
12
18
|
next.resolve();
|
|
13
19
|
}
|
|
14
|
-
if (queue.length === 0)
|
|
15
|
-
queuesByHandle.delete(handle);
|
|
16
20
|
};
|
|
17
21
|
return {
|
|
18
22
|
reserve: (handle, kind) => {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const facts = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const activeForAgent = activeByHandle.get(handle) ?? 0;
|
|
24
|
+
const queuedForAgent = queuedFor(handle);
|
|
25
|
+
const facts = {
|
|
26
|
+
activeForAgent,
|
|
27
|
+
queuedForAgent,
|
|
28
|
+
activeTotal,
|
|
29
|
+
queuedTotal: queue.length,
|
|
30
|
+
};
|
|
31
|
+
const canStartImmediately = activeTotal < limits.maxParallelTotal
|
|
32
|
+
&& activeForAgent < limits.maxParallelPerAgent
|
|
33
|
+
&& queue.length === 0;
|
|
34
|
+
if (kind === "execution" && !canStartImmediately && (queuedForAgent >= limits.maxQueuedPerAgent
|
|
35
|
+
|| queue.length >= limits.maxQueuedTotal)) {
|
|
36
|
+
return {
|
|
37
|
+
accepted: false,
|
|
38
|
+
facts,
|
|
39
|
+
ready: Promise.resolve(),
|
|
40
|
+
isQueued: () => false,
|
|
41
|
+
release: () => { },
|
|
42
|
+
};
|
|
25
43
|
}
|
|
26
44
|
let resolveReady;
|
|
27
45
|
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
28
46
|
const entry = {
|
|
29
|
-
|
|
47
|
+
handle,
|
|
30
48
|
released: false,
|
|
31
|
-
promoted:
|
|
49
|
+
promoted: false,
|
|
32
50
|
resolve: resolveReady,
|
|
33
51
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
resolveReady();
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
const queue = queuesByHandle.get(handle) ?? [];
|
|
40
|
-
queue.push(entry);
|
|
41
|
-
queuesByHandle.set(handle, queue);
|
|
42
|
-
}
|
|
52
|
+
queue.push(entry);
|
|
53
|
+
promote();
|
|
43
54
|
return {
|
|
55
|
+
accepted: true,
|
|
44
56
|
facts,
|
|
45
57
|
state: entry.promoted ? "ready" : "queued",
|
|
46
58
|
ready,
|
|
@@ -50,19 +62,22 @@ export function createSharedSlotManager(limits) {
|
|
|
50
62
|
return;
|
|
51
63
|
entry.released = true;
|
|
52
64
|
if (entry.promoted) {
|
|
53
|
-
|
|
65
|
+
activeTotal = Math.max(0, activeTotal - 1);
|
|
66
|
+
const nextForAgent = Math.max(0, (activeByHandle.get(handle) ?? 1) - 1);
|
|
67
|
+
if (nextForAgent === 0)
|
|
68
|
+
activeByHandle.delete(handle);
|
|
69
|
+
else
|
|
70
|
+
activeByHandle.set(handle, nextForAgent);
|
|
54
71
|
}
|
|
55
72
|
else {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (queue !== undefined && index >= 0)
|
|
73
|
+
const index = queue.indexOf(entry);
|
|
74
|
+
if (index >= 0)
|
|
59
75
|
queue.splice(index, 1);
|
|
60
|
-
if (queue?.length === 0)
|
|
61
|
-
queuesByHandle.delete(handle);
|
|
62
76
|
}
|
|
63
|
-
|
|
77
|
+
promote();
|
|
64
78
|
},
|
|
65
79
|
};
|
|
66
80
|
},
|
|
81
|
+
snapshot: () => ({ activeTotal, queuedTotal: queue.length }),
|
|
67
82
|
};
|
|
68
83
|
}
|