@nowcrew/daemon 0.6.88-beta.18 → 0.6.88-beta.19

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.
@@ -264,6 +264,7 @@ export const ExecutionRejectedSchema = z.object({
264
264
  reason: RejectionReasonSchema,
265
265
  message: z.string().optional(),
266
266
  at: TimestampSchema,
267
+ preAdmissionProof: z.object({ specHash: z.string().regex(/^[a-f0-9]{64}$/u) }).strict().optional(),
267
268
  }).strict();
268
269
  export const ExecutionStartedSchema = z.object({
269
270
  type: z.literal("execution:started"),
@@ -408,7 +408,8 @@ export async function runExecution(config, input, dependencies) {
408
408
  ? { permission: effectivePermission(spec, config) }
409
409
  : admission(spec, config, dependencies, initialAt);
410
410
  if ("rejected" in checked) {
411
- const frame = ExecutionRejectedSchema.parse(boundExecutionFrame(checked.rejected, config.executionLimits.maxEventBytes));
411
+ const proof = await dependencies.confirmPreAdmission?.(checked.rejected).catch(() => false);
412
+ const frame = ExecutionRejectedSchema.parse(boundExecutionFrame({ ...checked.rejected, ...(proof ? { preAdmissionProof: { specHash } } : {}) }, config.executionLimits.maxEventBytes));
412
413
  await dependencies.report(frame);
413
414
  return { kind: "rejected", frame };
414
415
  }
@@ -0,0 +1,64 @@
1
+ import { ExecutionCompletedSchema, ExecutionRejectedSchema } from "./execution-protocol.js";
2
+ import { hashExecutionSpec } from "./execution-runner.js";
3
+ import { dslog } from "./slog.js";
4
+ /** Queue rejection is admission work too: own the ID before any asynchronous journal write. */
5
+ export async function rejectQueuedAdmission(context, spec, executionClass, facts) {
6
+ const id = spec.executionId;
7
+ const hash = hashExecutionSpec(spec);
8
+ if (context.knownExecutionHashes.has(id) || context.executionRuns.has(id)) {
9
+ // A same-ID request won while the caller awaited journal.get. Its journal may
10
+ // still be pending; silence lets its real result arrive instead of rejecting it.
11
+ const existing = await context.journal.get(id);
12
+ if (existing?.specHash === hash)
13
+ context.replay(existing);
14
+ return;
15
+ }
16
+ context.knownExecutionHashes.set(id, hash);
17
+ const rejected = ExecutionRejectedSchema.parse({
18
+ type: "execution:rejected", protocolVersion: 1, executionId: id,
19
+ reason: "resource_limit", message: "Local machine execution queue is full", at: new Date().toISOString(),
20
+ });
21
+ dslog("execution.machine_queue_rejected", "机器执行队列已满", {
22
+ level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, ...facts,
23
+ });
24
+ const rejecting = persistPreAdmissionRejection(context.journal, spec, rejected)
25
+ .then(async (entry) => {
26
+ if (entry)
27
+ await context.report({ ...rejected, preAdmissionProof: { specHash: hash } });
28
+ else {
29
+ const existing = await context.journal.get(id);
30
+ if (existing?.specHash === hash)
31
+ context.replay(existing);
32
+ }
33
+ })
34
+ .catch(error => {
35
+ // A failed durable write has no safe rejection proof; a restart/sync will
36
+ // reconcile an accepted tombstone if this admission created one.
37
+ dslog("execution.admission_rejection_failed", "准入拒绝未取得持久停止证明", {
38
+ level: "ERROR", execution_id: id, error_message: String(error),
39
+ });
40
+ })
41
+ .finally(() => {
42
+ if (context.executionRuns.get(id) === rejecting) {
43
+ context.executionRuns.delete(id);
44
+ context.knownExecutionHashes.delete(id);
45
+ }
46
+ });
47
+ context.executionRuns.set(id, rejecting);
48
+ await rejecting;
49
+ }
50
+ /** The caller synchronously owns this admission ID. Never complete somebody else's record. */
51
+ export async function persistPreAdmissionRejection(journal, spec, rejection) {
52
+ const accepted = await journal.accept(spec.executionId, hashExecutionSpec(spec), {
53
+ runtime: spec.runtime.name, ...(spec.runtime.model ? { model: spec.runtime.model } : {}), resumed: false,
54
+ });
55
+ if (accepted.kind !== "created")
56
+ return null;
57
+ return journal.complete(spec.executionId, ExecutionCompletedSchema.parse({
58
+ type: "execution:completed", protocolVersion: 1, executionId: spec.executionId,
59
+ outcome: "failed", errorCode: rejection.reason, errorMessage: rejection.message ?? "Admission rejected before runtime start",
60
+ runtime: spec.runtime.name, ...(spec.runtime.model ? { model: spec.runtime.model } : {}), resumed: false,
61
+ startedAt: accepted.acceptedAt,
62
+ finishedAt: new Date(Math.max(Date.now(), Date.parse(accepted.acceptedAt))).toISOString(),
63
+ }));
64
+ }
package/dist/serve.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * 断线指数退避重连。一个 agent:start 进来就跑一次现有的 runAgent。
4
4
  */
5
5
  import { WebSocket } from "ws";
6
+ import { persistPreAdmissionRejection, rejectQueuedAdmission } from "./pre-admission-rejection.js";
6
7
  import { join } from "node:path";
7
8
  import { randomUUID } from "node:crypto";
8
9
  import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
@@ -210,6 +211,11 @@ export function serve(config, opts = {}) {
210
211
  return;
211
212
  }
212
213
  safeExecutionSend(frame);
214
+ if (frame.type === "execution:rejected" && frame.preAdmissionProof) {
215
+ const entry = await executionJournal.get(frame.executionId);
216
+ if (entry?.completion)
217
+ completionRetransmitter.track(entry.completion);
218
+ }
213
219
  };
214
220
  const replayExecutionTelemetry = async () => {
215
221
  const frames = await executionTelemetry.replay();
@@ -530,18 +536,8 @@ export function serve(config, opts = {}) {
530
536
  : spec.context.scheduledRunId === undefined ? "normal" : "scheduled";
531
537
  let reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
532
538
  if (!reservation.accepted) {
533
- dslog("execution.machine_queue_rejected", "机器执行队列已满", {
534
- level: "WARN",
535
- execution_id: spec.executionId,
536
- agent_handle: spec.agent.handle,
537
- execution_class: executionClass,
538
- ...reservation.facts,
539
- });
540
- safeExecutionSend(ExecutionRejectedSchema.parse({
541
- type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
542
- reason: "resource_limit", message: "Local machine execution queue is full",
543
- at: new Date().toISOString(),
544
- }));
539
+ await rejectQueuedAdmission({ journal: executionJournal, knownExecutionHashes, executionRuns,
540
+ report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts);
545
541
  return;
546
542
  }
547
543
  knownExecutionHashes.set(spec.executionId, hash);
@@ -711,6 +707,13 @@ export function serve(config, opts = {}) {
711
707
  ...reservation.facts,
712
708
  },
713
709
  report: reportExecutionFrame,
710
+ confirmPreAdmission: async (rejection) => {
711
+ const prior = await executionJournal.get(spec.executionId);
712
+ if (prior !== null || stopped || knownExecutionHashes.get(spec.executionId) !== hash
713
+ || executionRuns.get(spec.executionId) !== execution)
714
+ return false;
715
+ return await persistPreAdmissionRejection(executionJournal, spec, rejection) !== null;
716
+ },
714
717
  startupGate: runtimeStartupGate,
715
718
  startupTimeoutMs: config.executionLimits.startupTimeoutMs,
716
719
  onRuntimePhase: (phase, runtime) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.88-beta.18",
3
+ "version": "0.6.88-beta.19",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",