@nowcrew/daemon 0.6.29 → 0.6.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/dist/agent-ability/materializer.js +4 -4
- package/dist/config.js +3 -3
- package/dist/execution-runner.js +17 -13
- package/dist/host-execution-coordinator.js +169 -52
- package/dist/local-executor.js +107 -34
- package/dist/project-skills/reconciler.js +5 -2
- package/dist/remote/claude-bridge.js +558 -0
- package/dist/remote/claude-channel.js +164 -0
- package/dist/remote/codex-client.js +451 -0
- package/dist/remote/codex-runtime.js +77 -0
- package/dist/remote/config.js +135 -0
- package/dist/remote/gateway.js +879 -0
- package/dist/remote/identity.js +39 -0
- package/dist/remote/owner.js +77 -0
- package/dist/remote/protocol.js +211 -0
- package/dist/remote/remote-cli.js +254 -0
- package/dist/remote/runtime-probe.js +182 -0
- package/dist/remote/session-discovery.js +249 -0
- package/dist/remote/wrapper.js +40 -0
- package/dist/remote-web/assets/index-B_6VM_tw.js +94 -0
- package/dist/remote-web/assets/index-L6EiQbJn.css +1 -0
- package/dist/remote-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/remote-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/remote-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/remote-web/icons/nowwork-192.png +0 -0
- package/dist/remote-web/icons/nowwork-512.png +0 -0
- package/dist/remote-web/icons/nowwork.svg +7 -0
- package/dist/remote-web/index.html +20 -0
- package/dist/remote-web/manifest.webmanifest +13 -0
- package/dist/remote-web/sw.js +12 -0
- package/dist/runner.js +6 -1
- package/dist/runtime-startup-gate.js +47 -23
- package/dist/runtimes/codex-app-server-runner.js +6 -0
- package/dist/serve.js +55 -3
- package/dist/shared-execution-slots.js +75 -16
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityM
|
|
|
8
8
|
import { loadAgentAbilityRuntimeContext } from "./runtime-context.js";
|
|
9
9
|
import { parseSkillFrontmatter } from "../skill-frontmatter.js";
|
|
10
10
|
import { isProjectSkillName, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILL_FILE_BYTES, } from "../project-skills/types.js";
|
|
11
|
-
import { createAgentProjectionCoordinator,
|
|
11
|
+
import { createAgentProjectionCoordinator, } from "../project-skills/agent-projection-coordinator.js";
|
|
12
12
|
const exists = (path) => lstat(path).then(() => true, () => false);
|
|
13
13
|
const isRealDirectory = (path) => lstat(path)
|
|
14
14
|
.then((info) => info.isDirectory() && !info.isSymbolicLink(), () => false);
|
|
@@ -135,7 +135,7 @@ const projectNativeSkills = async (agentRoot, trainingRoot, skills) => {
|
|
|
135
135
|
const names = await copyExistingSkills(target, staging, managedRoots);
|
|
136
136
|
for (const skill of nativeSkills) {
|
|
137
137
|
if (names.has(skill.name))
|
|
138
|
-
|
|
138
|
+
continue;
|
|
139
139
|
await symlink(skill.source, join(staging, skill.name), "dir");
|
|
140
140
|
}
|
|
141
141
|
await switchDirectory(target, staging, backup);
|
|
@@ -555,7 +555,7 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
555
555
|
prepareAndLaunch(agentsRoot, handle, ability, launch) {
|
|
556
556
|
if (agentsRoot !== expectedAgentsRoot)
|
|
557
557
|
return Promise.reject(new Error("ability_agents_root_mismatch"));
|
|
558
|
-
return coordinator.
|
|
558
|
+
return coordinator.runExclusive(expectedAgentsRoot, handle, async () => {
|
|
559
559
|
const started = Date.now();
|
|
560
560
|
try {
|
|
561
561
|
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
@@ -575,7 +575,7 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
575
575
|
});
|
|
576
576
|
throw error;
|
|
577
577
|
}
|
|
578
|
-
}
|
|
578
|
+
});
|
|
579
579
|
},
|
|
580
580
|
};
|
|
581
581
|
}
|
package/dist/config.js
CHANGED
|
@@ -15,9 +15,9 @@ export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
|
|
|
15
15
|
maxQueuedPerAgent: 32,
|
|
16
16
|
maxParallelTotal: 10,
|
|
17
17
|
maxQueuedTotal: 128,
|
|
18
|
-
maxStartingTotal:
|
|
19
|
-
maxStartingPerRuntime:
|
|
20
|
-
startupGapMs:
|
|
18
|
+
maxStartingTotal: 10,
|
|
19
|
+
maxStartingPerRuntime: 10,
|
|
20
|
+
startupGapMs: 500,
|
|
21
21
|
startupTimeoutMs: 120_000,
|
|
22
22
|
});
|
|
23
23
|
// Fits the largest mandatory v1 lifecycle envelope (UUID + timestamps + outcome facts) with margin.
|
package/dist/execution-runner.js
CHANGED
|
@@ -497,22 +497,14 @@ export async function runExecution(config, input, dependencies) {
|
|
|
497
497
|
let consoleSequence = 0;
|
|
498
498
|
let externalOutputSequence = 0;
|
|
499
499
|
const callbacks = {
|
|
500
|
-
|
|
500
|
+
onRuntimeStarting: () => {
|
|
501
|
+
dependencies.onRuntimePhase?.("starting");
|
|
502
|
+
},
|
|
503
|
+
onRuntimeRunning: async () => {
|
|
501
504
|
if (dependencies.cancellation?.isRequested())
|
|
502
505
|
return;
|
|
503
|
-
const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
|
|
504
|
-
if (ready.runtimeReadyAt === null) {
|
|
505
|
-
throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
|
|
506
|
-
}
|
|
507
|
-
startedAt = ready.runtimeReadyAt;
|
|
508
|
-
await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
|
|
509
|
-
type: "execution:started",
|
|
510
|
-
protocolVersion: 1,
|
|
511
|
-
executionId: spec.executionId,
|
|
512
|
-
at: startedAt,
|
|
513
|
-
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
514
506
|
const cancel = runtimeCancel;
|
|
515
|
-
if (cancel !== null &&
|
|
507
|
+
if (cancel !== null && timeout === undefined) {
|
|
516
508
|
timeout = setTimeout(() => {
|
|
517
509
|
timedOut = true;
|
|
518
510
|
try {
|
|
@@ -523,6 +515,18 @@ export async function runExecution(config, input, dependencies) {
|
|
|
523
515
|
}
|
|
524
516
|
}, effectiveTimeoutMs);
|
|
525
517
|
}
|
|
518
|
+
dependencies.onRuntimePhase?.("running");
|
|
519
|
+
const ready = await dependencies.journal.markRuntimeReady(spec.executionId, now().toISOString());
|
|
520
|
+
if (ready.runtimeReadyAt === null) {
|
|
521
|
+
throw new Error(`Execution ${spec.executionId} runtime readiness was not persisted`);
|
|
522
|
+
}
|
|
523
|
+
startedAt = ready.runtimeReadyAt;
|
|
524
|
+
await reportBestEffort(dependencies.report, boundExecutionFrame(DaemonToServerExecutionFrameSchema.parse({
|
|
525
|
+
type: "execution:started",
|
|
526
|
+
protocolVersion: 1,
|
|
527
|
+
executionId: spec.executionId,
|
|
528
|
+
at: startedAt,
|
|
529
|
+
}), config.executionLimits.maxEventBytes), bestEffortTimeoutMs);
|
|
526
530
|
},
|
|
527
531
|
...(spec.reporting.streamActivity ? {
|
|
528
532
|
onActivity: (activity) => {
|
|
@@ -4,9 +4,12 @@ import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { defaultProcessController } from "./execution-journal.js";
|
|
6
6
|
import { createJournalLease, createJournalLeaseRegistry, JournalLockedError, } from "./execution-journal-lock.js";
|
|
7
|
+
import { dslog } from "./slog.js";
|
|
7
8
|
const HOST_EXECUTION_SLOT_COUNT = 10;
|
|
8
|
-
const
|
|
9
|
+
const HOST_STARTUP_SLOT_COUNT = 10;
|
|
10
|
+
const HOST_STARTUP_GAP_MS = 500;
|
|
9
11
|
const RETRY_MS = 50;
|
|
12
|
+
const RELEASE_RETRY_MAX_MS = 5_000;
|
|
10
13
|
export function defaultHostExecutionCoordinatorRoot(userHome = homedir()) {
|
|
11
14
|
// Deliberately independent of CREW_DAEMON_HOME and agentsRoot: every profile
|
|
12
15
|
// owned by this OS user must share the same physical-compute safety boundary.
|
|
@@ -68,11 +71,12 @@ function reservation(prerequisite, acquire) {
|
|
|
68
71
|
const lease = await acquire(() => released);
|
|
69
72
|
if (lease === null)
|
|
70
73
|
return null;
|
|
74
|
+
owned = lease;
|
|
71
75
|
if (released) {
|
|
72
|
-
|
|
76
|
+
// The release path owns cleanup so every close failure gets the same
|
|
77
|
+
// retry, diagnostics, and drain tracking as an already-granted slot.
|
|
73
78
|
return null;
|
|
74
79
|
}
|
|
75
|
-
owned = lease;
|
|
76
80
|
granted = true;
|
|
77
81
|
return lease;
|
|
78
82
|
});
|
|
@@ -87,8 +91,23 @@ function reservation(prerequisite, acquire) {
|
|
|
87
91
|
return releasePromise;
|
|
88
92
|
released = true;
|
|
89
93
|
cancelPrerequisite();
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
const closeOwned = async () => {
|
|
95
|
+
const lease = owned;
|
|
96
|
+
if (lease === null)
|
|
97
|
+
return;
|
|
98
|
+
await lease.close();
|
|
99
|
+
if (owned === lease)
|
|
100
|
+
owned = null;
|
|
101
|
+
};
|
|
102
|
+
releasePromise = acquisition.then(closeOwned, async () => {
|
|
103
|
+
// Acquisition errors are already exposed through ready. Release only
|
|
104
|
+
// owns cleanup; with no acquired lease there is nothing to retry.
|
|
105
|
+
if (owned !== null)
|
|
106
|
+
await closeOwned();
|
|
107
|
+
}).catch((error) => {
|
|
108
|
+
releasePromise = null;
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
92
111
|
void releasePromise.catch(() => undefined);
|
|
93
112
|
return releasePromise;
|
|
94
113
|
},
|
|
@@ -107,90 +126,161 @@ async function writeLaunchTimestamp(path, value) {
|
|
|
107
126
|
export function createHostExecutionCoordinator(options = {}) {
|
|
108
127
|
const root = resolve(options.root ?? defaultHostExecutionCoordinatorRoot());
|
|
109
128
|
const executionSlots = options.executionSlots ?? HOST_EXECUTION_SLOT_COUNT;
|
|
129
|
+
const startupSlots = options.startupSlots ?? HOST_STARTUP_SLOT_COUNT;
|
|
110
130
|
const startupGapMs = options.startupGapMs ?? HOST_STARTUP_GAP_MS;
|
|
111
131
|
const retryMs = options.retryMs ?? RETRY_MS;
|
|
112
132
|
const now = options.now ?? Date.now;
|
|
113
133
|
const wait = options.wait ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
|
|
134
|
+
const acquireLease = options.acquireLease ?? leaseFor;
|
|
114
135
|
const pendingReleases = new Set();
|
|
115
136
|
if (!Number.isSafeInteger(executionSlots) || executionSlots <= 0) {
|
|
116
137
|
throw new RangeError("executionSlots must be a positive safe integer");
|
|
117
138
|
}
|
|
139
|
+
if (!Number.isSafeInteger(startupSlots) || startupSlots <= 0) {
|
|
140
|
+
throw new RangeError("startupSlots must be a positive safe integer");
|
|
141
|
+
}
|
|
118
142
|
if (!Number.isFinite(startupGapMs) || startupGapMs < 0) {
|
|
119
143
|
throw new RangeError("startupGapMs must be a nonnegative finite number");
|
|
120
144
|
}
|
|
121
145
|
if (!Number.isFinite(retryMs) || retryMs <= 0) {
|
|
122
146
|
throw new RangeError("retryMs must be a positive finite number");
|
|
123
147
|
}
|
|
124
|
-
const
|
|
148
|
+
const reportReleaseError = options.onReleaseError ?? ((event) => {
|
|
149
|
+
dslog("execution.host_lease_release_retry", "Host execution lease 释放失败,准备重试", {
|
|
150
|
+
level: "ERROR",
|
|
151
|
+
resource: event.resource,
|
|
152
|
+
attempt: event.attempt,
|
|
153
|
+
retry_delay_ms: event.retryDelayMs,
|
|
154
|
+
error_message: event.error instanceof Error ? event.error.message.slice(0, 500) : String(event.error).slice(0, 500),
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
const retryRelease = async (resource, release) => {
|
|
158
|
+
let attempt = 0;
|
|
159
|
+
while (true) {
|
|
160
|
+
try {
|
|
161
|
+
await release();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
attempt += 1;
|
|
166
|
+
const retryDelayMs = Math.min(RELEASE_RETRY_MAX_MS, retryMs * 2 ** Math.min(attempt - 1, 6));
|
|
167
|
+
try {
|
|
168
|
+
reportReleaseError({ resource, attempt, retryDelayMs, error });
|
|
169
|
+
}
|
|
170
|
+
catch { /* diagnostics must never stop lease cleanup */ }
|
|
171
|
+
await wait(retryDelayMs);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const trackRelease = (release) => {
|
|
176
|
+
pendingReleases.add(release);
|
|
177
|
+
void release.finally(() => pendingReleases.delete(release)).catch(() => undefined);
|
|
178
|
+
return release;
|
|
179
|
+
};
|
|
180
|
+
const acquireExecution = async (released, lane) => {
|
|
125
181
|
let offset = 0;
|
|
182
|
+
const slotCount = lane === "memory_prune" ? 1 : executionSlots;
|
|
183
|
+
const slotRoot = lane === "memory_prune" ? "memory-prune-slots" : "slots";
|
|
126
184
|
while (!released()) {
|
|
127
|
-
for (let step = 0; step <
|
|
128
|
-
const slot = (offset + step) %
|
|
129
|
-
const lease = await
|
|
185
|
+
for (let step = 0; step < slotCount; step += 1) {
|
|
186
|
+
const slot = (offset + step) % slotCount;
|
|
187
|
+
const lease = await acquireLease(join(root, slotRoot, String(slot)));
|
|
130
188
|
if (lease !== null)
|
|
131
189
|
return lease;
|
|
132
190
|
}
|
|
133
|
-
offset = (offset + 1) %
|
|
191
|
+
offset = (offset + 1) % slotCount;
|
|
134
192
|
await wait(retryMs);
|
|
135
193
|
}
|
|
136
194
|
return null;
|
|
137
195
|
};
|
|
138
|
-
const acquireStartup = async (released) => {
|
|
196
|
+
const acquireStartup = async (released, lane) => {
|
|
197
|
+
const slotCount = lane === "memory_prune" ? 1 : startupSlots;
|
|
198
|
+
const slotRoot = lane === "memory_prune" ? "memory-prune-startup-slots" : "startup-slots";
|
|
199
|
+
// Keep the legacy directory as the short-lived launch-gap mutex. During a rolling upgrade,
|
|
200
|
+
// older daemons still hold this same lease for their full startup lifetime, so old and new
|
|
201
|
+
// versions remain mutually coordinated instead of producing an unbounded spawn burst.
|
|
202
|
+
const gapDirectory = join(root, lane === "memory_prune" ? "memory-prune-startup" : "startup");
|
|
203
|
+
let offset = 0;
|
|
139
204
|
while (!released()) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
let startupSlot = null;
|
|
206
|
+
for (let step = 0; step < slotCount; step += 1) {
|
|
207
|
+
const slot = (offset + step) % slotCount;
|
|
208
|
+
startupSlot = await acquireLease(join(root, slotRoot, String(slot)));
|
|
209
|
+
if (startupSlot !== null)
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (startupSlot === null) {
|
|
213
|
+
offset = (offset + 1) % slotCount;
|
|
143
214
|
await wait(retryMs);
|
|
144
215
|
continue;
|
|
145
216
|
}
|
|
146
|
-
|
|
147
|
-
let previous = Number.NEGATIVE_INFINITY;
|
|
217
|
+
let granted = false;
|
|
148
218
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
219
|
+
while (!released()) {
|
|
220
|
+
const gapLease = await acquireLease(gapDirectory);
|
|
221
|
+
if (gapLease === null) {
|
|
222
|
+
await wait(retryMs);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const timestampPath = join(gapDirectory, "last-launch-at");
|
|
227
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
228
|
+
try {
|
|
229
|
+
previous = Number.parseInt(await readFile(timestampPath, "utf8"), 10);
|
|
230
|
+
if (!Number.isFinite(previous))
|
|
231
|
+
previous = Number.NEGATIVE_INFINITY;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
const delay = Math.max(0, previous + startupGapMs - now());
|
|
238
|
+
if (delay > 0)
|
|
239
|
+
await wait(delay);
|
|
240
|
+
if (released())
|
|
241
|
+
return null;
|
|
242
|
+
await writeLaunchTimestamp(timestampPath, now());
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await retryRelease(`${lane}:startup_gap`, () => gapLease.close());
|
|
246
|
+
}
|
|
247
|
+
granted = true;
|
|
248
|
+
return startupSlot;
|
|
157
249
|
}
|
|
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
250
|
return null;
|
|
165
251
|
}
|
|
166
|
-
|
|
167
|
-
|
|
252
|
+
finally {
|
|
253
|
+
if (!granted)
|
|
254
|
+
await retryRelease(`${lane}:startup_slot`, () => startupSlot.close());
|
|
255
|
+
}
|
|
168
256
|
}
|
|
169
257
|
return null;
|
|
170
258
|
};
|
|
171
|
-
const tracked = (entry) => {
|
|
259
|
+
const tracked = (entry, resource) => {
|
|
172
260
|
let trackedRelease = null;
|
|
173
261
|
return {
|
|
174
262
|
...entry,
|
|
175
263
|
release: () => {
|
|
176
264
|
if (trackedRelease !== null)
|
|
177
265
|
return trackedRelease;
|
|
178
|
-
trackedRelease = entry.release
|
|
179
|
-
pendingReleases.add(trackedRelease);
|
|
180
|
-
void trackedRelease.then(() => pendingReleases.delete(trackedRelease), () => pendingReleases.delete(trackedRelease));
|
|
266
|
+
trackedRelease = trackRelease(retryRelease(resource, entry.release));
|
|
181
267
|
return trackedRelease;
|
|
182
268
|
},
|
|
183
269
|
};
|
|
184
270
|
};
|
|
185
271
|
return {
|
|
186
|
-
reserveExecution: (prerequisite) => tracked(reservation(prerequisite, acquireExecution)),
|
|
187
|
-
reserveStartup: (prerequisite) => tracked(reservation(prerequisite, acquireStartup)),
|
|
272
|
+
reserveExecution: (prerequisite, lane = "foreground") => tracked(reservation(prerequisite, (released) => acquireExecution(released, lane)), `${lane}:execution_slot`),
|
|
273
|
+
reserveStartup: (prerequisite, lane = "foreground") => tracked(reservation(prerequisite, (released) => acquireStartup(released, lane)), `${lane}:startup_slot`),
|
|
188
274
|
tryAcquireExclusiveExecution: async () => {
|
|
189
275
|
const leases = [];
|
|
190
|
-
|
|
191
|
-
|
|
276
|
+
const targets = [
|
|
277
|
+
...Array.from({ length: executionSlots }, (_, slot) => join(root, "slots", String(slot))),
|
|
278
|
+
join(root, "memory-prune-slots", "0"),
|
|
279
|
+
];
|
|
280
|
+
for (const target of targets) {
|
|
281
|
+
const lease = await acquireLease(target);
|
|
192
282
|
if (lease === null) {
|
|
193
|
-
await Promise.all(leases.map((owned) => owned.close()));
|
|
283
|
+
await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
|
|
194
284
|
return null;
|
|
195
285
|
}
|
|
196
286
|
leases.push(lease);
|
|
@@ -200,9 +290,7 @@ export function createHostExecutionCoordinator(options = {}) {
|
|
|
200
290
|
release: () => {
|
|
201
291
|
if (releasePromise !== null)
|
|
202
292
|
return releasePromise;
|
|
203
|
-
releasePromise = Promise.all(leases.map((lease) => lease.close())).then(() => undefined);
|
|
204
|
-
pendingReleases.add(releasePromise);
|
|
205
|
-
void releasePromise.then(() => pendingReleases.delete(releasePromise), () => pendingReleases.delete(releasePromise));
|
|
293
|
+
releasePromise = trackRelease(retryRelease("exclusive_execution", () => Promise.all(leases.map((lease) => lease.close())).then(() => undefined)));
|
|
206
294
|
return releasePromise;
|
|
207
295
|
},
|
|
208
296
|
};
|
|
@@ -214,13 +302,20 @@ export function createHostExecutionCoordinator(options = {}) {
|
|
|
214
302
|
};
|
|
215
303
|
}
|
|
216
304
|
export function hostCoordinatedSlotManager(local, host) {
|
|
305
|
+
const coordinated = new Set();
|
|
217
306
|
return {
|
|
218
307
|
reserve: (handle, kind, executionClass) => {
|
|
219
308
|
const localReservation = local.reserve(handle, kind, executionClass);
|
|
220
309
|
if (!localReservation.accepted)
|
|
221
310
|
return localReservation;
|
|
222
|
-
const hostReservation = host.reserveExecution(localReservation.ready);
|
|
223
|
-
|
|
311
|
+
const hostReservation = host.reserveExecution(localReservation.ready, executionClass === "memory_prune" ? "memory_prune" : "foreground");
|
|
312
|
+
const entry = {
|
|
313
|
+
local: localReservation,
|
|
314
|
+
host: hostReservation,
|
|
315
|
+
memoryPrune: executionClass === "memory_prune",
|
|
316
|
+
released: false,
|
|
317
|
+
};
|
|
318
|
+
coordinated.add(entry);
|
|
224
319
|
return {
|
|
225
320
|
...localReservation,
|
|
226
321
|
// File ownership is asynchronous, so accepted work is conservatively
|
|
@@ -229,23 +324,45 @@ export function hostCoordinatedSlotManager(local, host) {
|
|
|
229
324
|
ready: hostReservation.ready,
|
|
230
325
|
isQueued: hostReservation.isQueued,
|
|
231
326
|
release: () => {
|
|
232
|
-
if (released)
|
|
327
|
+
if (entry.released)
|
|
233
328
|
return;
|
|
234
|
-
released = true;
|
|
329
|
+
entry.released = true;
|
|
330
|
+
coordinated.delete(entry);
|
|
235
331
|
void hostReservation.release();
|
|
236
332
|
localReservation.release();
|
|
237
333
|
},
|
|
238
334
|
};
|
|
239
335
|
},
|
|
240
|
-
snapshot:
|
|
336
|
+
snapshot: () => {
|
|
337
|
+
const snapshot = local.snapshot();
|
|
338
|
+
let foregroundHostWaiting = 0;
|
|
339
|
+
let memoryPruneHostWaiting = 0;
|
|
340
|
+
for (const entry of coordinated) {
|
|
341
|
+
if (entry.released || entry.local.isQueued() || !entry.host.isQueued())
|
|
342
|
+
continue;
|
|
343
|
+
if (entry.memoryPrune)
|
|
344
|
+
memoryPruneHostWaiting += 1;
|
|
345
|
+
else
|
|
346
|
+
foregroundHostWaiting += 1;
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
...snapshot,
|
|
350
|
+
queuedTotal: snapshot.queuedTotal + foregroundHostWaiting,
|
|
351
|
+
admittedTotal: Math.max(0, snapshot.admittedTotal - foregroundHostWaiting),
|
|
352
|
+
preparingTotal: Math.max(0, snapshot.preparingTotal - foregroundHostWaiting),
|
|
353
|
+
memoryPruneQueuedTotal: snapshot.memoryPruneQueuedTotal + memoryPruneHostWaiting,
|
|
354
|
+
memoryPruneAdmittedTotal: Math.max(0, snapshot.memoryPruneAdmittedTotal - memoryPruneHostWaiting),
|
|
355
|
+
memoryPrunePreparingTotal: Math.max(0, snapshot.memoryPrunePreparingTotal - memoryPruneHostWaiting),
|
|
356
|
+
};
|
|
357
|
+
},
|
|
241
358
|
tryAcquireExclusive: local.tryAcquireExclusive,
|
|
242
359
|
};
|
|
243
360
|
}
|
|
244
361
|
export function hostCoordinatedStartupGate(local, host) {
|
|
245
362
|
return {
|
|
246
|
-
reserve: (runtime) => {
|
|
247
|
-
const localReservation = local.reserve(runtime);
|
|
248
|
-
const hostReservation = host.reserveStartup(localReservation.ready);
|
|
363
|
+
reserve: (runtime, lane = "foreground") => {
|
|
364
|
+
const localReservation = local.reserve(runtime, lane);
|
|
365
|
+
const hostReservation = host.reserveStartup(localReservation.ready, lane);
|
|
249
366
|
let released = false;
|
|
250
367
|
return {
|
|
251
368
|
ready: hostReservation.ready,
|