@nowcrew/daemon 0.6.91-beta.3 → 0.6.91-beta.5

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.
@@ -1,3 +1,4 @@
1
+ import { dslog } from "./slog.js";
1
2
  import { z } from "zod";
2
3
  import { isDaemonReleaseVersion } from "./daemon-release-version.js";
3
4
  const DaemonUpdateMessageSchema = z.object({
@@ -42,15 +43,16 @@ export function createDaemonUpdateController(deps) {
42
43
  failed(message.updateId, preparation.errorCode);
43
44
  return;
44
45
  }
45
- let released = false;
46
- const releaseOnce = async () => {
47
- if (released)
48
- return;
49
- released = true;
50
- await preparation.release();
46
+ let releaseTask;
47
+ const releaseOnce = () => {
48
+ releaseTask ??= preparation.release().catch(error => {
49
+ releaseTask = undefined;
50
+ throw error;
51
+ });
52
+ return releaseTask;
51
53
  };
52
- deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
53
54
  try {
55
+ deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
54
56
  const handoff = deps.scheduleRestart(eligibility.serviceSpec);
55
57
  const state = {
56
58
  release: releaseOnce,
@@ -92,7 +94,13 @@ export function createDaemonUpdateController(deps) {
92
94
  failed(message.updateId, "update_in_progress");
93
95
  return true;
94
96
  }
95
- const done = execute(message).finally(() => {
97
+ const done = execute(message).catch(error => {
98
+ dslog("daemon.update_failed", "Daemon 更新或重启准备失败", {
99
+ level: "ERROR", update_id: message.updateId, operation: message.type,
100
+ error_message: error instanceof Error ? error.message : String(error),
101
+ });
102
+ failed(message.updateId, message.type === "daemon:restart" ? "restart_failed" : "install_failed");
103
+ }).finally(() => {
96
104
  handled.add(message.updateId);
97
105
  if (running?.id === message.updateId)
98
106
  running = null;
@@ -1,3 +1,4 @@
1
+ import { dslog } from "./slog.js";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { resolve } from "node:path";
3
4
  import { systemCommandRunner } from "./computer-service.js";
@@ -17,6 +18,8 @@ export async function prepareDaemonRestart(input) {
17
18
  const local = input.localSlots.tryAcquireExclusive();
18
19
  if (local === null)
19
20
  return { ok: false, errorCode: "runtime_busy" };
21
+ const acquiredAt = Date.now();
22
+ dslog("daemon.maintenance_barrier_acquired", "Daemon 已暂停接收执行,准备维护");
20
23
  let released = false;
21
24
  return {
22
25
  ok: true,
@@ -25,6 +28,7 @@ export async function prepareDaemonRestart(input) {
25
28
  return;
26
29
  released = true;
27
30
  local.release();
31
+ dslog("daemon.maintenance_barrier_released", "维护屏障已释放", { held_ms: Date.now() - acquiredAt });
28
32
  },
29
33
  };
30
34
  }
@@ -67,16 +71,10 @@ export async function installExactDaemonUpdate(input) {
67
71
  || installation.npmPrefix !== input.npmPrefix) {
68
72
  return { ok: false, errorCode: "ineligible" };
69
73
  }
70
- const local = input.localSlots.tryAcquireExclusive();
71
- if (local === null)
72
- return { ok: false, errorCode: "runtime_busy" };
73
- let released = false;
74
- const release = async () => {
75
- if (released)
76
- return;
77
- released = true;
78
- local.release();
79
- };
74
+ const barrier = await prepareDaemonRestart(input);
75
+ if (!barrier.ok)
76
+ return barrier;
77
+ const release = barrier.release;
80
78
  const lockedInstallation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
81
79
  if (lockedInstallation === null
82
80
  || lockedInstallation.packageRoot !== input.packageRoot
@@ -286,13 +286,19 @@ export function createHostExecutionCoordinator(options = {}) {
286
286
  ...Array.from({ length: executionSlots }, (_, slot) => join(root, "slots", String(slot))),
287
287
  join(root, "memory-prune-slots", "0"),
288
288
  ];
289
- for (const target of targets) {
290
- const lease = await acquireLease(target);
291
- if (lease === null) {
292
- await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
293
- return null;
289
+ try {
290
+ for (const target of targets) {
291
+ const lease = await acquireLease(target);
292
+ if (lease === null) {
293
+ await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map((owned) => owned.close())).then(() => undefined));
294
+ return null;
295
+ }
296
+ leases.push(lease);
294
297
  }
295
- leases.push(lease);
298
+ }
299
+ catch (error) {
300
+ await retryRelease("exclusive_execution_partial", () => Promise.all(leases.map(owned => owned.close())).then(() => undefined));
301
+ throw error;
296
302
  }
297
303
  let releasePromise = null;
298
304
  return {
@@ -2,7 +2,7 @@ import { ExecutionCompletedSchema, ExecutionRejectedSchema } from "./execution-p
2
2
  import { hashExecutionSpec } from "./execution-runner.js";
3
3
  import { dslog } from "./slog.js";
4
4
  /** Queue rejection is admission work too: own the ID before any asynchronous journal write. */
5
- export async function rejectQueuedAdmission(context, spec, executionClass, facts) {
5
+ export async function rejectQueuedAdmission(context, spec, executionClass, facts, rejectionReason = "machine_queue_full") {
6
6
  const id = spec.executionId;
7
7
  const hash = hashExecutionSpec(spec);
8
8
  if (context.knownExecutionHashes.has(id) || context.executionRuns.has(id)) {
@@ -16,10 +16,11 @@ export async function rejectQueuedAdmission(context, spec, executionClass, facts
16
16
  context.knownExecutionHashes.set(id, hash);
17
17
  const rejected = ExecutionRejectedSchema.parse({
18
18
  type: "execution:rejected", protocolVersion: 1, executionId: id,
19
- reason: "resource_limit", message: "Local machine execution queue is full", at: new Date().toISOString(),
19
+ reason: "resource_limit", message: rejectionReason === "maintenance" ? "Local machine is temporarily unavailable for maintenance"
20
+ : rejectionReason === "agent_queue_full" ? "Local agent execution queue is full" : "Local machine execution queue is full", at: new Date().toISOString(),
20
21
  });
21
- dslog("execution.machine_queue_rejected", "机器执行队列已满", {
22
- level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, ...facts,
22
+ dslog("execution.machine_queue_rejected", rejectionReason === "maintenance" ? "机器维护期间暂停接收执行" : "执行等待队列已满", {
23
+ level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, admission_reason: rejectionReason, ...facts,
23
24
  });
24
25
  const rejecting = persistPreAdmissionRejection(context.journal, spec, rejected)
25
26
  .then(async (entry) => {
package/dist/serve.js CHANGED
@@ -488,7 +488,7 @@ export function serve(config, opts = {}) {
488
488
  let reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
489
489
  if (!reservation.accepted) {
490
490
  await rejectQueuedAdmission({ journal: executionJournal, knownExecutionHashes, executionRuns,
491
- report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts);
491
+ report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts, reservation.rejectionReason);
492
492
  return;
493
493
  }
494
494
  knownExecutionHashes.set(spec.executionId, hash);
@@ -59,6 +59,7 @@ export function createSharedSlotManager(limits) {
59
59
  if (exclusive) {
60
60
  return {
61
61
  accepted: false,
62
+ rejectionReason: "maintenance",
62
63
  facts,
63
64
  ready: Promise.resolve(),
64
65
  isQueued: () => false,
@@ -77,6 +78,7 @@ export function createSharedSlotManager(limits) {
77
78
  || laneQueuedTotal >= limits.maxQueuedTotal)) {
78
79
  return {
79
80
  accepted: false,
81
+ rejectionReason: queuedForAgent >= limits.maxQueuedPerAgent ? "agent_queue_full" : "machine_queue_full",
80
82
  facts,
81
83
  ready: Promise.resolve(),
82
84
  isQueued: () => false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.91-beta.3",
3
+ "version": "0.6.91-beta.5",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",