@nowcrew/daemon 0.5.30 → 0.5.31
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 +13 -1
- package/dist/config.js +13 -1
- package/dist/execution-journal.js +96 -5
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +52 -29
- package/dist/host-execution-coordinator.js +241 -0
- package/dist/local-executor.js +78 -0
- package/dist/machine-info.js +16 -1
- package/dist/remote/claude-bridge.js +402 -0
- package/dist/remote/claude-channel.js +164 -0
- package/dist/remote/codex-client.js +408 -0
- package/dist/remote/codex-runtime.js +77 -0
- package/dist/remote/config.js +83 -0
- package/dist/remote/gateway.js +572 -0
- package/dist/remote/protocol.js +178 -0
- package/dist/remote/remote-cli.js +233 -0
- package/dist/remote/session-discovery.js +249 -0
- package/dist/remote/wrapper.js +40 -0
- package/dist/runner.js +4 -0
- package/dist/runtime-startup-gate.js +91 -0
- package/dist/serve.js +70 -8
- package/dist/shared-execution-slots.js +48 -33
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
+
function optionValue(args, long, short) {
|
|
4
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5
|
+
const value = args[index];
|
|
6
|
+
if (value.startsWith(`${long}=`))
|
|
7
|
+
return value.slice(long.length + 1);
|
|
8
|
+
if (value === long || (short && value === short)) {
|
|
9
|
+
const next = args[index + 1];
|
|
10
|
+
return next && !next.startsWith("-") ? next : null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function resolveClaudeWrapperSession(args, createId = randomUUID) {
|
|
16
|
+
if (args.includes("--continue") || args.includes("-c")) {
|
|
17
|
+
throw new Error("NowCrew remote requires an explicit session ID; use --resume <session-id> instead of --continue");
|
|
18
|
+
}
|
|
19
|
+
if (args.includes("--fork-session")) {
|
|
20
|
+
throw new Error("NowCrew remote cannot identify a fork chosen inside Claude; start a new session or resume without --fork-session");
|
|
21
|
+
}
|
|
22
|
+
const sessionId = optionValue(args, "--session-id") ?? optionValue(args, "--resume", "-r") ?? createId();
|
|
23
|
+
if (!UUID.test(sessionId)) {
|
|
24
|
+
throw new Error("Claude remote sessions require a UUID in --session-id or --resume");
|
|
25
|
+
}
|
|
26
|
+
const hasSessionSelector = args.some((value) => value === "--session-id" || value.startsWith("--session-id=")
|
|
27
|
+
|| value === "--resume" || value.startsWith("--resume=") || value === "-r");
|
|
28
|
+
return {
|
|
29
|
+
sessionId,
|
|
30
|
+
args: hasSessionSelector ? [...args] : ["--session-id", sessionId, ...args],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function buildCodexWrappedInvocation(userArgs, socketPath = "") {
|
|
34
|
+
const hasRemote = userArgs.some((value) => value === "--remote" || value.startsWith("--remote="));
|
|
35
|
+
return {
|
|
36
|
+
bin: "codex",
|
|
37
|
+
args: hasRemote ? [...userArgs] : ["--remote", socketPath ? `unix://${socketPath}` : "unix://", ...userArgs],
|
|
38
|
+
env: process.env,
|
|
39
|
+
};
|
|
40
|
+
}
|
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")
|
|
@@ -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
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -28,6 +28,8 @@ 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 { createRuntimeStartupGate } from "./runtime-startup-gate.js";
|
|
32
|
+
import { createHostExecutionCoordinator, hostCoordinatedSlotManager, hostCoordinatedStartupGate, } from "./host-execution-coordinator.js";
|
|
31
33
|
// normalize.ts 的活动种类 → activity 枚举
|
|
32
34
|
const ACTIVITY_MAP = {
|
|
33
35
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
@@ -60,7 +62,9 @@ export function serve(config, opts = {}) {
|
|
|
60
62
|
const executeProtocol = opts.execution?.runExecution ?? runExecution;
|
|
61
63
|
let detectedExecutionRuntimes = [];
|
|
62
64
|
let runtimeFacts = null;
|
|
63
|
-
const
|
|
65
|
+
const hostCoordinator = opts.execution?.hostCoordinator ?? createHostExecutionCoordinator();
|
|
66
|
+
const sharedSlots = hostCoordinatedSlotManager(createSharedSlotManager(config.executionLimits), hostCoordinator);
|
|
67
|
+
const runtimeStartupGate = hostCoordinatedStartupGate(createRuntimeStartupGate(config.executionLimits), hostCoordinator);
|
|
64
68
|
const knownExecutionHashes = new Map();
|
|
65
69
|
const executionReservations = new Map();
|
|
66
70
|
const executionRuns = new Map();
|
|
@@ -115,8 +119,11 @@ export function serve(config, opts = {}) {
|
|
|
115
119
|
if ((entry.state === "completed" || entry.state === "interrupted") && entry.completion !== null) {
|
|
116
120
|
return { executionId: entry.executionId, state: entry.state, completion: entry.completion, updatedAt: entry.updatedAt };
|
|
117
121
|
}
|
|
122
|
+
if (entry.state === "running" && entry.runtimeReadyAt !== null) {
|
|
123
|
+
return { executionId: entry.executionId, state: "running", updatedAt: entry.runtimeReadyAt };
|
|
124
|
+
}
|
|
118
125
|
if (entry.state === "accepted" || entry.state === "running") {
|
|
119
|
-
return { executionId: entry.executionId, state:
|
|
126
|
+
return { executionId: entry.executionId, state: "accepted", updatedAt: entry.acceptedAt };
|
|
120
127
|
}
|
|
121
128
|
return null;
|
|
122
129
|
};
|
|
@@ -137,10 +144,10 @@ export function serve(config, opts = {}) {
|
|
|
137
144
|
state: acceptanceState, effectivePermission: entry.effectivePermission ?? "workspace_write",
|
|
138
145
|
at: entry.acceptedAt,
|
|
139
146
|
});
|
|
140
|
-
if (entry.state === "running" && entry.
|
|
147
|
+
if (entry.state === "running" && entry.runtimeReadyAt !== null) {
|
|
141
148
|
safeExecutionSend({
|
|
142
149
|
type: "execution:started", protocolVersion: 1,
|
|
143
|
-
executionId: entry.executionId, at: entry.
|
|
150
|
+
executionId: entry.executionId, at: entry.runtimeReadyAt,
|
|
144
151
|
});
|
|
145
152
|
}
|
|
146
153
|
}
|
|
@@ -335,8 +342,30 @@ export function serve(config, opts = {}) {
|
|
|
335
342
|
}
|
|
336
343
|
return;
|
|
337
344
|
}
|
|
338
|
-
knownExecutionHashes.set(spec.executionId, hash);
|
|
339
345
|
const reservation = sharedSlots.reserve(spec.agent.handle, "execution");
|
|
346
|
+
if (!reservation.accepted) {
|
|
347
|
+
dslog("execution.machine_queue_rejected", "机器执行队列已满", {
|
|
348
|
+
level: "WARN",
|
|
349
|
+
execution_id: spec.executionId,
|
|
350
|
+
agent_handle: spec.agent.handle,
|
|
351
|
+
...reservation.facts,
|
|
352
|
+
});
|
|
353
|
+
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
354
|
+
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
355
|
+
reason: "resource_limit", message: "Local machine execution queue is full",
|
|
356
|
+
at: new Date().toISOString(),
|
|
357
|
+
}));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
knownExecutionHashes.set(spec.executionId, hash);
|
|
361
|
+
const machineQueueEnteredAt = Date.now();
|
|
362
|
+
if (reservation.isQueued()) {
|
|
363
|
+
dslog("execution.machine_queued", "execution 已进入机器队列", {
|
|
364
|
+
execution_id: spec.executionId,
|
|
365
|
+
agent_handle: spec.agent.handle,
|
|
366
|
+
...reservation.facts,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
340
369
|
executionReservations.set(spec.executionId, reservation);
|
|
341
370
|
const cancellation = cancellationFor(spec.executionId);
|
|
342
371
|
const cleanupExecutionReservation = () => {
|
|
@@ -371,8 +400,22 @@ export function serve(config, opts = {}) {
|
|
|
371
400
|
...reservation.facts,
|
|
372
401
|
},
|
|
373
402
|
report: reportExecutionFrame,
|
|
403
|
+
startupGate: runtimeStartupGate,
|
|
404
|
+
startupTimeoutMs: config.executionLimits.startupTimeoutMs,
|
|
374
405
|
...(reservation.state === undefined ? {} : {
|
|
375
|
-
slot: {
|
|
406
|
+
slot: {
|
|
407
|
+
state: reservation.state,
|
|
408
|
+
ready: reservation.ready.then(() => {
|
|
409
|
+
const snapshot = sharedSlots.snapshot();
|
|
410
|
+
dslog("execution.machine_slot_ready", "execution 获得机器执行名额", {
|
|
411
|
+
execution_id: spec.executionId,
|
|
412
|
+
agent_handle: spec.agent.handle,
|
|
413
|
+
queue_ms: Date.now() - machineQueueEnteredAt,
|
|
414
|
+
active_total: snapshot.activeTotal,
|
|
415
|
+
queued_total: snapshot.queuedTotal,
|
|
416
|
+
});
|
|
417
|
+
}),
|
|
418
|
+
},
|
|
376
419
|
}),
|
|
377
420
|
cancellation,
|
|
378
421
|
}).finally(() => {
|
|
@@ -539,12 +582,26 @@ export function serve(config, opts = {}) {
|
|
|
539
582
|
}
|
|
540
583
|
running.add(key);
|
|
541
584
|
legacyReservation = sharedSlots.reserve(msg.agentHandle, "legacy");
|
|
585
|
+
if (legacyReservation.isQueued()) {
|
|
586
|
+
const snapshot = sharedSlots.snapshot();
|
|
587
|
+
dslog("run.machine_queued", "legacy run 已进入机器队列", {
|
|
588
|
+
...runKeys,
|
|
589
|
+
active_total: snapshot.activeTotal,
|
|
590
|
+
queued_total: snapshot.queuedTotal,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
542
593
|
await awaitWithCancellation(legacyReservation.ready, controller.cancellation);
|
|
543
594
|
const queueMs = Date.now() - queueWaitStart;
|
|
595
|
+
const machineSnapshot = sharedSlots.snapshot();
|
|
544
596
|
const threadLabel = threadId ?? null;
|
|
545
597
|
const from = msg.wake?.senderHandle ?? "?";
|
|
546
598
|
const incoming = msg.wake?.content ?? "";
|
|
547
|
-
dslog("run.start", `开始运行 ${msg.agentHandle}`, {
|
|
599
|
+
dslog("run.start", `开始运行 ${msg.agentHandle}`, {
|
|
600
|
+
...runKeys,
|
|
601
|
+
queue_ms: queueMs,
|
|
602
|
+
active_total: machineSnapshot.activeTotal,
|
|
603
|
+
queued_total: machineSnapshot.queuedTotal,
|
|
604
|
+
});
|
|
548
605
|
log(`\n${"─".repeat(56)}`);
|
|
549
606
|
log(`🔔 唤醒 agent=${msg.agentHandle} reason=${msg.reason ?? "?"}`);
|
|
550
607
|
log(` channel = ${msg.channelId}`);
|
|
@@ -650,7 +707,11 @@ export function serve(config, opts = {}) {
|
|
|
650
707
|
...(!scheduled && threadId ? { wakeMessageId: threadId } : {}),
|
|
651
708
|
...(!scheduled && msg.wake?.seq !== undefined ? { wakeContextUpToSeq: msg.wake.seq } : {}),
|
|
652
709
|
...(attemptWake ? { wake: attemptWake } : {}),
|
|
653
|
-
}, reportActivity, reportConsole, {
|
|
710
|
+
}, reportActivity, reportConsole, {
|
|
711
|
+
cancellation: controller.cancellation,
|
|
712
|
+
startupGate: runtimeStartupGate,
|
|
713
|
+
startupTimeoutMs: config.executionLimits.startupTimeoutMs,
|
|
714
|
+
});
|
|
654
715
|
});
|
|
655
716
|
const result = mergeRunAgentResults(guarded.results);
|
|
656
717
|
// 本轮 token 用量上报:runner 已从 result 事件提取(含缓存读/写细分),
|
|
@@ -848,6 +909,7 @@ export function serve(config, opts = {}) {
|
|
|
848
909
|
for (const run of legacyRuns.values())
|
|
849
910
|
run.controller.request();
|
|
850
911
|
await deadline.waitFor(Promise.all(pending));
|
|
912
|
+
await deadline.waitFor(hostCoordinator.drain());
|
|
851
913
|
await executionJournal.close({ signal: deadline.signal });
|
|
852
914
|
}
|
|
853
915
|
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
|
}
|