@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.
Files changed (40) hide show
  1. package/dist/agent-ability/materializer.js +4 -4
  2. package/dist/config.js +3 -3
  3. package/dist/execution-runner.js +17 -13
  4. package/dist/host-execution-coordinator.js +169 -52
  5. package/dist/local-executor.js +107 -34
  6. package/dist/project-skills/reconciler.js +5 -2
  7. package/dist/remote/claude-bridge.js +558 -0
  8. package/dist/remote/claude-channel.js +164 -0
  9. package/dist/remote/codex-client.js +451 -0
  10. package/dist/remote/codex-runtime.js +77 -0
  11. package/dist/remote/config.js +135 -0
  12. package/dist/remote/gateway.js +879 -0
  13. package/dist/remote/identity.js +39 -0
  14. package/dist/remote/owner.js +77 -0
  15. package/dist/remote/protocol.js +211 -0
  16. package/dist/remote/remote-cli.js +254 -0
  17. package/dist/remote/runtime-probe.js +182 -0
  18. package/dist/remote/session-discovery.js +249 -0
  19. package/dist/remote/wrapper.js +40 -0
  20. package/dist/remote-web/assets/index-B_6VM_tw.js +94 -0
  21. package/dist/remote-web/assets/index-L6EiQbJn.css +1 -0
  22. package/dist/remote-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  23. package/dist/remote-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  24. package/dist/remote-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  25. package/dist/remote-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  26. package/dist/remote-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  27. package/dist/remote-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  28. package/dist/remote-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  29. package/dist/remote-web/icons/nowwork-192.png +0 -0
  30. package/dist/remote-web/icons/nowwork-512.png +0 -0
  31. package/dist/remote-web/icons/nowwork.svg +7 -0
  32. package/dist/remote-web/index.html +20 -0
  33. package/dist/remote-web/manifest.webmanifest +13 -0
  34. package/dist/remote-web/sw.js +12 -0
  35. package/dist/runner.js +6 -1
  36. package/dist/runtime-startup-gate.js +47 -23
  37. package/dist/runtimes/codex-app-server-runner.js +6 -0
  38. package/dist/serve.js +55 -3
  39. package/dist/shared-execution-slots.js +75 -16
  40. 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, runtimeProjectionLifetime, } from "../project-skills/agent-projection-coordinator.js";
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
- throw new Error(`ability_skill_name_conflict:${skill.name}`);
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.runExclusiveUntil(expectedAgentsRoot, handle, async () => {
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
- }, runtimeProjectionLifetime);
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: 1,
19
- maxStartingPerRuntime: 1,
20
- startupGapMs: 3_000,
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.
@@ -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
- onRuntimeReady: async () => {
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 && !dependencies.cancellation?.isRequested()) {
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 HOST_STARTUP_GAP_MS = 3_000;
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
- await lease.close();
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
- releasePromise = acquisition.then(async () => { if (owned !== null)
91
- await owned.close(); });
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 acquireExecution = async (released) => {
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 < executionSlots; step += 1) {
128
- const slot = (offset + step) % executionSlots;
129
- const lease = await leaseFor(join(root, "slots", String(slot)));
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) % executionSlots;
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
- const directory = join(root, "startup");
141
- const lease = await leaseFor(directory);
142
- if (lease === null) {
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
- const timestampPath = join(directory, "last-launch-at");
147
- let previous = Number.NEGATIVE_INFINITY;
217
+ let granted = false;
148
218
  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;
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
- await writeLaunchTimestamp(timestampPath, now());
167
- return lease;
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
- for (let slot = 0; slot < executionSlots; slot += 1) {
191
- const lease = await leaseFor(join(root, "slots", String(slot)));
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
- let released = false;
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: local.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,