@nowcrew/daemon 0.5.31 → 0.5.33

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.
@@ -0,0 +1,61 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { systemCommandRunner } from "./computer-service.js";
4
+ const RELEASED_VERSION_RE = /^\d+\.\d+\.\d+$/;
5
+ async function readPackageVersion(packageRoot) {
6
+ const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
7
+ return typeof body.version === "string" ? body.version : "";
8
+ }
9
+ export async function installExactDaemonUpdate(input) {
10
+ if (!RELEASED_VERSION_RE.test(input.targetVersion)) {
11
+ return { ok: false, errorCode: "ineligible" };
12
+ }
13
+ const local = input.localSlots.tryAcquireExclusive();
14
+ if (local === null)
15
+ return { ok: false, errorCode: "runtime_busy" };
16
+ const host = await input.hostCoordinator.tryAcquireExclusiveExecution();
17
+ if (host === null) {
18
+ local.release();
19
+ return { ok: false, errorCode: "runtime_busy" };
20
+ }
21
+ let released = false;
22
+ const release = async () => {
23
+ if (released)
24
+ return;
25
+ released = true;
26
+ await host.release();
27
+ local.release();
28
+ };
29
+ const runner = input.runner ?? systemCommandRunner;
30
+ try {
31
+ await input.onInstalling?.();
32
+ }
33
+ catch {
34
+ await release();
35
+ return { ok: false, errorCode: "install_failed" };
36
+ }
37
+ const result = await runner(process.platform === "win32" ? "npm.cmd" : "npm", [
38
+ "install",
39
+ "--global",
40
+ "--ignore-scripts",
41
+ "--no-audit",
42
+ "--no-fund",
43
+ `@nowcrew/daemon@${input.targetVersion}`,
44
+ ]).catch(() => null);
45
+ if (result === null || result.exitCode !== 0) {
46
+ await release();
47
+ return { ok: false, errorCode: "install_failed" };
48
+ }
49
+ let installedVersion = "";
50
+ try {
51
+ installedVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
52
+ }
53
+ catch {
54
+ // Version verification is authoritative; unreadable package metadata is a mismatch.
55
+ }
56
+ if (installedVersion !== input.targetVersion) {
57
+ await release();
58
+ return { ok: false, errorCode: "version_mismatch" };
59
+ }
60
+ return { ok: true, release };
61
+ }
@@ -52,6 +52,8 @@ export function createJournalLeaseRegistry() {
52
52
  return { leases: new Map() };
53
53
  }
54
54
  const codeOf = (error) => error instanceof Error && "code" in error ? error.code : undefined;
55
+ const missingDuringOwnerRead = (error) => codeOf(error) === "ENOENT"
56
+ || (error instanceof Error && codeOf(error.cause) === "ENOENT");
55
57
  const ownerFileName = (token) => `owner.${token}.json`;
56
58
  const releasedLockName = (token) => `.journal.released.${token}.lock`;
57
59
  const RELEASED_LOCK_PATTERN = /^\.journal\.released\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.lock$/i;
@@ -99,8 +101,6 @@ export async function inspectJournalLock(options) {
99
101
  const lockDirectory = join(options.directory, ".journal.lock");
100
102
  const orphanGraceMs = options.orphanGraceMs ?? 30_000;
101
103
  const now = options.now ?? (() => new Date());
102
- const missingDuringOwnerRead = (error) => codeOf(error) === "ENOENT"
103
- || (error instanceof Error && codeOf(error.cause) === "ENOENT");
104
104
  for (let attempt = 0; attempt < 2; attempt += 1) {
105
105
  let names;
106
106
  try {
@@ -230,7 +230,15 @@ export function createJournalLease(options) {
230
230
  throw error;
231
231
  }
232
232
  if (names.length === 0) {
233
- const lockStat = await fileSystem.stat(lockDirectory);
233
+ let lockStat;
234
+ try {
235
+ lockStat = await fileSystem.stat(lockDirectory);
236
+ }
237
+ catch (error) {
238
+ if (codeOf(error) === "ENOENT")
239
+ return null;
240
+ throw error;
241
+ }
234
242
  if (now().valueOf() - lockStat.mtimeMs < orphanGraceMs) {
235
243
  throw new JournalLockedError("Execution journal lock owner installation is in progress", { journalPath });
236
244
  }
@@ -247,7 +255,16 @@ export function createJournalLease(options) {
247
255
  if (names.length !== 1 || !/^owner\.[0-9a-f-]+\.json$/i.test(names[0])) {
248
256
  throw new JournalLockCorruptionError(lockDirectory, new Error("lock directory must contain one owner"));
249
257
  }
250
- return { owner: await parseLockOwner(lockDirectory, names[0], fileSystem), fileName: names[0] };
258
+ try {
259
+ return { owner: await parseLockOwner(lockDirectory, names[0], fileSystem), fileName: names[0] };
260
+ }
261
+ catch (error) {
262
+ // The current owner commits release by renaming the entire lock directory.
263
+ // A contender may therefore observe its filename just before it disappears.
264
+ if (missingDuringOwnerRead(error))
265
+ return null;
266
+ throw error;
267
+ }
251
268
  };
252
269
  const validateInstalledOwner = async (lease) => {
253
270
  const observed = await readOwner();
@@ -98,6 +98,7 @@ export const ExecutionStartSchema = z.object({
98
98
  agent: z.object({
99
99
  id: z.string().min(1),
100
100
  handle: AgentHandleSchema,
101
+ memoryEnabled: z.boolean().optional(),
101
102
  }).strict(),
102
103
  workspace: z.object({
103
104
  taskKey: z.string().min(1).max(200),
@@ -13,6 +13,7 @@ import { executionBackendCapability } from "./execution-backend.js";
13
13
  import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
14
14
  import { RuntimeCancelledError } from "./runtime-cancellation.js";
15
15
  import { supervisorLaunch } from "./supervised-runtime.js";
16
+ import { appendAgentMemoryContext } from "./agent-memory/policy.js";
16
17
  export { supervisorLaunch } from "./supervised-runtime.js";
17
18
  const ACTIVITY_KIND = {
18
19
  init: "working",
@@ -388,6 +389,7 @@ export async function runExecution(config, input, dependencies) {
388
389
  let timeout;
389
390
  let timedOut = false;
390
391
  let completion;
392
+ let memoryCaptureFinalText = null;
391
393
  let boundImDecision = spec.reporting.allowBoundImDecision
392
394
  ? "silent"
393
395
  : undefined;
@@ -399,6 +401,16 @@ export async function runExecution(config, input, dependencies) {
399
401
  if (dependencies.slot !== undefined) {
400
402
  await cancellable(dependencies.slot.ready, dependencies.cancellation);
401
403
  }
404
+ let recalledMemory = "";
405
+ if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
406
+ try {
407
+ recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt), dependencies.cancellation);
408
+ }
409
+ catch (error) {
410
+ if (error instanceof ExecutionCancelledError)
411
+ throw error;
412
+ }
413
+ }
402
414
  const credential = await cancellable(mint(config.serverUrl, config.machineToken, spec.agent.handle, undefined, { executionId: spec.executionId, agentRunId: spec.executionId }), dependencies.cancellation);
403
415
  const providerConfig = launchProviderConfig(credential.config);
404
416
  let activitySequence = 0;
@@ -545,6 +557,12 @@ export async function runExecution(config, input, dependencies) {
545
557
  }
546
558
  },
547
559
  };
560
+ const systemPromptBudget = config.executionLimits.maxPromptBytes
561
+ - Buffer.byteLength(spec.instructions.wakePrompt, "utf8");
562
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, systemPromptBudget);
563
+ const boundedSystemPrompt = (context) => appendAgentMemoryContext(typeof systemPromptWithLocalFacts === "string"
564
+ ? systemPromptWithLocalFacts
565
+ : systemPromptWithLocalFacts(context), recalledMemory, systemPromptBudget);
548
566
  const localInput = {
549
567
  executionId: spec.executionId,
550
568
  handle: spec.agent.handle,
@@ -554,8 +572,7 @@ export async function runExecution(config, input, dependencies) {
554
572
  ...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
555
573
  ...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
556
574
  ...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
557
- systemPrompt: withLocalExecutionFacts(spec.instructions.systemPrompt, config.executionLimits.maxPromptBytes
558
- - Buffer.byteLength(spec.instructions.wakePrompt, "utf8")),
575
+ systemPrompt: boundedSystemPrompt,
559
576
  wakePrompt: spec.instructions.wakePrompt,
560
577
  runtime: {
561
578
  name: spec.runtime.name,
@@ -593,6 +610,8 @@ export async function runExecution(config, input, dependencies) {
593
610
  if (timeout !== undefined)
594
611
  clearTimeout(timeout);
595
612
  const finishedAt = now().toISOString();
613
+ if (result.exitCode === 0 && result.finalText?.trim())
614
+ memoryCaptureFinalText = result.finalText;
596
615
  if (spec.reporting.allowBoundImDecision) {
597
616
  const path = join(result.workspaceRunDir, `.bound-im-decision-${spec.executionId}.json`);
598
617
  const selected = await readBoundImDecision(path);
@@ -677,6 +696,17 @@ export async function runExecution(config, input, dependencies) {
677
696
  completion = boundExecutionFrame(completion, config.executionLimits.maxEventBytes);
678
697
  await telemetry.closeAndDrain();
679
698
  await dependencies.journal.complete(spec.executionId, completion);
699
+ if (completion.outcome === "succeeded"
700
+ && spec.agent.memoryEnabled === true
701
+ && memoryCaptureFinalText !== null
702
+ && dependencies.agentMemory !== undefined) {
703
+ try {
704
+ await dependencies.agentMemory.capture(spec.agent.handle, spec.executionId, spec.instructions.wakePrompt, memoryCaptureFinalText);
705
+ }
706
+ catch {
707
+ // External memory is optional and must never alter the durable execution outcome.
708
+ }
709
+ }
680
710
  await dependencies.report(completion);
681
711
  return { kind: "completed", frame: completion };
682
712
  }
@@ -185,6 +185,28 @@ export function createHostExecutionCoordinator(options = {}) {
185
185
  return {
186
186
  reserveExecution: (prerequisite) => tracked(reservation(prerequisite, acquireExecution)),
187
187
  reserveStartup: (prerequisite) => tracked(reservation(prerequisite, acquireStartup)),
188
+ tryAcquireExclusiveExecution: async () => {
189
+ const leases = [];
190
+ for (let slot = 0; slot < executionSlots; slot += 1) {
191
+ const lease = await leaseFor(join(root, "slots", String(slot)));
192
+ if (lease === null) {
193
+ await Promise.all(leases.map((owned) => owned.close()));
194
+ return null;
195
+ }
196
+ leases.push(lease);
197
+ }
198
+ let releasePromise = null;
199
+ return {
200
+ release: () => {
201
+ if (releasePromise !== null)
202
+ 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));
206
+ return releasePromise;
207
+ },
208
+ };
209
+ },
188
210
  drain: async () => {
189
211
  while (pendingReleases.size > 0)
190
212
  await Promise.all([...pendingReleases]);
@@ -216,6 +238,7 @@ export function hostCoordinatedSlotManager(local, host) {
216
238
  };
217
239
  },
218
240
  snapshot: local.snapshot,
241
+ tryAcquireExclusive: local.tryAcquireExclusive,
219
242
  };
220
243
  }
221
244
  export function hostCoordinatedStartupGate(local, host) {
@@ -266,8 +266,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
266
266
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
267
267
  const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
268
268
  await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
269
+ const inheritedEnv = { ...process.env };
270
+ for (const key of Object.keys(inheritedEnv)) {
271
+ if (key.startsWith("CREW_AGENT_MEMORY_"))
272
+ delete inheritedEnv[key];
273
+ }
269
274
  const baseEnv = {
270
- ...process.env,
275
+ ...inheritedEnv,
271
276
  ...sanitizeEnvVars(providerConfig.envVars),
272
277
  ...input.launch.systemEnv,
273
278
  PATH: `${workspace.crewDir}${delimiter}${augmentedPath()}`,
@@ -26,6 +26,7 @@ export const DAEMON_CAPABILITIES = [
26
26
  "execution_attachments_v1",
27
27
  "execution_answer_stream_v1",
28
28
  "execution_machine_queue_v1",
29
+ "execution_agent_memory_policy_v1",
29
30
  ];
30
31
  export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
31
32
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
@@ -100,9 +101,10 @@ async function listAgentHandles(agentsRoot) {
100
101
  }
101
102
  }
102
103
  export async function collectMachineHello(agentsRoot, executionLimits, runtimePlatform = process.platform, dependencies = {}) {
103
- const [runtimes, agentHandles] = await Promise.all([
104
+ const [runtimes, agentHandles, additionalCapabilities] = await Promise.all([
104
105
  (dependencies.detectInstalled ?? detectRuntimes)(),
105
106
  listAgentHandles(agentsRoot),
107
+ dependencies.additionalCapabilities?.() ?? Promise.resolve([]),
106
108
  ]);
107
109
  const backend = executionBackendCapability(runtimePlatform, dependencies.jobObjectProbe);
108
110
  const executionRuntimes = backend.supported
@@ -115,7 +117,7 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
115
117
  daemonVersion: daemonVersion(),
116
118
  runtimes,
117
119
  executionRuntimes,
118
- capabilities: DAEMON_CAPABILITIES,
120
+ capabilities: [...DAEMON_CAPABILITIES, ...additionalCapabilities],
119
121
  ...(backend.supported ? {
120
122
  executionProtocol: EXECUTION_PROTOCOL,
121
123
  executionLimits: Object.freeze({
package/dist/prompt.js CHANGED
@@ -11,6 +11,14 @@ export const WORKLOG_INJECT_CAP = 8000;
11
11
  export const MEMORY_INJECT_CAP = 6000;
12
12
  const WORKLOG_HEAD = 4800;
13
13
  const WORKLOG_TAIL = 3000;
14
+ const EXTERNAL_RESULT_READABILITY = `
15
+
16
+ ## 外部 IM 结果可读性(CRITICAL)
17
+ - 本节只约束通过 reply-origin 或 notify-bound-im 外发的正文;NowWork 内部确认、进度和协作消息保持原有表达方式。
18
+ - 前三行依次说清结论、影响和需要谁做什么;不要用背景铺垫或复述任务开场。
19
+ - 除非用户明确要求完整明细,正文最多三个短小节、最多六个要点;总结日志、命令输出和长表格,不要整段粘贴。
20
+ - 有足够重点时,只加粗三到五处真正决定性的数字、最终状态、风险、截止时间、负责人或行动项;不足三处时宁缺毋滥。加粗范围要短,不要整段加粗,不要把每个数字都加粗,不得强化未经验证的判断。
21
+ - 使用标题、列表、引用和加粗形成无颜色也清楚的层级;不得输出 \`<font>\` 或其它未确认可用于企微流式消息的 HTML 标色标签。`;
14
22
  export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
15
23
  if (workLog.length <= cap)
16
24
  return workLog;
@@ -142,17 +150,17 @@ ${taskAndScheduleCommands}`;
142
150
  const externalReplyRule = scheduled
143
151
  ? ctx.scheduledExternalNotificationPolicy === "agent_decides"
144
152
  ? `
145
- - **绑定会话通知由你选择**:只有本轮完整结果确实值得打扰绑定会话时,运行 \`crew message notify-bound-im --channel ${ctx.channelId}\` 一次,然后仍只返回一个完整最终报告。该命令只记录本轮决策,不会自行发消息,也不能指定收件人。`
153
+ - **绑定会话通知由你选择**:只有本轮完整结果确实值得打扰绑定会话时,运行 \`crew message notify-bound-im --channel ${ctx.channelId}\` 一次,然后仍只返回一个完整最终报告。该命令只记录本轮决策,不会自行发消息,也不能指定收件人。${EXTERNAL_RESULT_READABILITY}`
146
154
  : ""
147
155
  : ctx.wakeOrigin === "wecom"
148
156
  ? `
149
- - **本轮来自企微,结束本轮前必须给出一条完整回复**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。最终只用一次 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数)发送有实质内容的完整结果;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定,并在本轮未形成可交付内容时发送统一兜底回复。`
157
+ - **本轮来自企微,结束本轮前必须给出一条完整回复**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。最终只用一次 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数)发送有实质内容的完整结果;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定,并在本轮未形成可交付内容时发送统一兜底回复。${EXTERNAL_RESULT_READABILITY}`
150
158
  : ctx.wakeOrigin
151
159
  ? `
152
- - **本轮来自${ctx.wakeOrigin === "feishu" ? "飞书" : ctx.wakeOrigin},结束本轮前必须明确选择外部回复决策**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。完整结果确实要回复外部会话时,用 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数);判断无需回复时,用 \`crew message skip-origin --reason "简短原因"\`。两者必须选择一个;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定。`
160
+ - **本轮来自${ctx.wakeOrigin === "feishu" ? "飞书" : ctx.wakeOrigin},结束本轮前必须明确选择外部回复决策**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。完整结果确实要回复外部会话时,用 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数);判断无需回复时,用 \`crew message skip-origin --reason "简短原因"\`。两者必须选择一个;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定。${EXTERNAL_RESULT_READABILITY}`
153
161
  : `
154
162
  - **本轮是 NowWork 内部唤醒**:普通 \`crew message send\` 只写入 NowWork。内部唤醒不得使用 \`--reply-origin\`,该参数只回答直接触发本轮的企微原消息。
155
- - **绑定会话主动通知**:用户明确要求同步,或最终结果有实质结论、变更或需群用户行动的阻塞时,才用一次 \`crew message send --notify-bound-im\`(同时带当前 channel/thread/content 参数)请求通知当前频道绑定的外部会话。Server 会校验绑定 owner 授权、绑定 Agent 和单轮边界;不能指定收件人。确认、进度、中间结果、无变化和重复内容一律留在 NowWork。`;
163
+ - **绑定会话主动通知**:用户明确要求同步,或最终结果有实质结论、变更或需群用户行动的阻塞时,才用一次 \`crew message send --notify-bound-im\`(同时带当前 channel/thread/content 参数)请求通知当前频道绑定的外部会话。Server 会校验绑定 owner 授权、绑定 Agent 和单轮边界;不能指定收件人。确认、进度、中间结果、无变化和重复内容一律留在 NowWork。${EXTERNAL_RESULT_READABILITY}`;
156
164
  const interactiveTaskRules = scheduled ? "" : `
157
165
  - **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
158
166
  - 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
@@ -24,7 +24,24 @@ const STDERR_TAIL_CAP = 1_200;
24
24
  const STDERR_LINE_CAPTURE_CAP = 1_200;
25
25
  const STDERR_LINE_OMITTED = "[stderr line omitted: exceeded capture limit]\n";
26
26
  const MAX_INITIALIZE_ATTEMPTS = 2;
27
+ // 模型网关瞬态故障(过载/限流)导致 turn 失败时,整轮重试(15s→45s 递进退避):codex 自身
28
+ // 的重试窗口只有 ~10-30s,网关过载往往持续数分钟,这里再兜一层,否则 agent 直接失败
29
+ // 不回复(2026-08-10 事故,普通会话与定时任务都中招)。
30
+ const MAX_TRANSIENT_TURN_RETRIES = 2;
31
+ const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
32
+ const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
27
33
  const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
34
+ const TRANSIENT_TURN_ERROR_PATTERNS = [
35
+ /at capacity/i,
36
+ /overloaded/i,
37
+ /rate.?limit/i,
38
+ /too many requests/i,
39
+ ];
40
+ /** 模型侧瞬态失败(网关过载/限流)→ 换个时间整轮重试大概率成功。
41
+ * interrupted 不算:那是取消信号或 codex collab 连带中断,重跑语义不明确。 */
42
+ export function isTransientTurnFailure(detail) {
43
+ return TRANSIENT_TURN_ERROR_PATTERNS.some((pattern) => pattern.test(detail));
44
+ }
28
45
  class CodexRpcTimeoutError extends Error {
29
46
  method;
30
47
  timeoutMs;
@@ -469,6 +486,9 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
469
486
  return {
470
487
  code: completed.turn?.status === "interrupted" ? 130 : 1,
471
488
  initializeTimedOut: false,
489
+ transientTurnFailure: !cancelling
490
+ && completed.turn?.status === "failed"
491
+ && isTransientTurnFailure(detail),
472
492
  };
473
493
  }
474
494
  catch (error) {
@@ -501,13 +521,27 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
501
521
  export async function runCodexAppServer(bin, options = {}) {
502
522
  const input = await readRunnerInput();
503
523
  const initializeTimeoutMs = Math.min(options.initializeTimeoutMs ?? INITIALIZE_RPC_TIMEOUT_MS, INITIALIZE_RPC_TIMEOUT_MS);
504
- for (let attempt = 1; attempt <= MAX_INITIALIZE_ATTEMPTS; attempt += 1) {
524
+ const turnRetryDelayMs = options.turnRetryDelayMs ?? TRANSIENT_TURN_RETRY_DELAY_MS;
525
+ let initializeTimeouts = 0;
526
+ let turnRetries = 0;
527
+ for (let attempt = 1;; attempt += 1) {
505
528
  const result = await runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs);
506
- if (!result.initializeTimedOut || attempt === MAX_INITIALIZE_ATTEMPTS)
507
- return result.code;
508
- process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=initialize_timeout\n`);
529
+ if (result.initializeTimedOut) {
530
+ initializeTimeouts += 1;
531
+ if (initializeTimeouts >= MAX_INITIALIZE_ATTEMPTS)
532
+ return result.code;
533
+ process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=initialize_timeout\n`);
534
+ continue;
535
+ }
536
+ if (result.transientTurnFailure && turnRetries < MAX_TRANSIENT_TURN_RETRIES) {
537
+ const delayMs = turnRetryDelayMs * TRANSIENT_TURN_RETRY_BACKOFF_FACTOR ** turnRetries;
538
+ turnRetries += 1;
539
+ process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=transient_turn_failure\n`);
540
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
541
+ continue;
542
+ }
543
+ return result.code;
509
544
  }
510
- return 1;
511
545
  }
512
546
  function configFromArgv(argv) {
513
547
  const { values } = parseArgs({
@@ -515,24 +549,34 @@ function configFromArgv(argv) {
515
549
  options: {
516
550
  bin: { type: "string" },
517
551
  "initialize-timeout-ms": { type: "string" },
552
+ "turn-retry-delay-ms": { type: "string" },
518
553
  },
519
554
  });
520
555
  if (!values.bin)
521
556
  throw new Error("--bin is required");
522
- const rawTimeout = values["initialize-timeout-ms"];
523
- if (rawTimeout === undefined)
524
- return { bin: values.bin };
525
- const initializeTimeoutMs = Number(rawTimeout);
526
- if (!Number.isInteger(initializeTimeoutMs) || initializeTimeoutMs <= 0) {
527
- throw new Error("--initialize-timeout-ms must be a positive integer");
528
- }
529
- return { bin: values.bin, initializeTimeoutMs };
557
+ const positiveInteger = (raw, flag) => {
558
+ if (raw === undefined)
559
+ return undefined;
560
+ const value = Number(raw);
561
+ if (!Number.isInteger(value) || value <= 0) {
562
+ throw new Error(`${flag} must be a positive integer`);
563
+ }
564
+ return value;
565
+ };
566
+ const initializeTimeoutMs = positiveInteger(values["initialize-timeout-ms"], "--initialize-timeout-ms");
567
+ const turnRetryDelayMs = positiveInteger(values["turn-retry-delay-ms"], "--turn-retry-delay-ms");
568
+ return {
569
+ bin: values.bin,
570
+ ...(initializeTimeoutMs === undefined ? {} : { initializeTimeoutMs }),
571
+ ...(turnRetryDelayMs === undefined ? {} : { turnRetryDelayMs }),
572
+ };
530
573
  }
531
574
  if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
532
575
  const config = configFromArgv(process.argv.slice(2));
533
- runCodexAppServer(config.bin, config.initializeTimeoutMs === undefined
534
- ? {}
535
- : { initializeTimeoutMs: config.initializeTimeoutMs })
576
+ runCodexAppServer(config.bin, {
577
+ ...(config.initializeTimeoutMs === undefined ? {} : { initializeTimeoutMs: config.initializeTimeoutMs }),
578
+ ...(config.turnRetryDelayMs === undefined ? {} : { turnRetryDelayMs: config.turnRetryDelayMs }),
579
+ })
536
580
  .then((code) => { process.exitCode = code; })
537
581
  .catch((error) => {
538
582
  process.stderr.write(`Codex app-server runner failed: ${safeErrorMessage(error, [])}\n`);
package/dist/serve.js CHANGED
@@ -28,8 +28,13 @@ 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 { createAgentMemoryBridge } from "./agent-memory/bridge.js";
31
32
  import { createRuntimeStartupGate } from "./runtime-startup-gate.js";
32
33
  import { createHostExecutionCoordinator, hostCoordinatedSlotManager, hostCoordinatedStartupGate, } from "./host-execution-coordinator.js";
34
+ import { detectDaemonUpdateEligibility, } from "./daemon-update-eligibility.js";
35
+ import { createDaemonUpdateController } from "./daemon-update-controller.js";
36
+ import { installExactDaemonUpdate } from "./daemon-updater.js";
37
+ import { scheduleServiceRestart } from "./computer-service.js";
33
38
  // normalize.ts 的活动种类 → activity 枚举
34
39
  const ACTIVITY_MAP = {
35
40
  init: "working", text: "thinking", reading: "reading", sending: "sending",
@@ -58,12 +63,15 @@ export function serve(config, opts = {}) {
58
63
  let reconnectTimer = null;
59
64
  let stopPromise = null;
60
65
  const executionJournal = opts.execution?.journal ?? createExecutionJournal(config.agentsRoot);
66
+ const agentMemory = opts.execution?.dependencies?.agentMemory
67
+ ?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
61
68
  const executionTelemetry = createExecutionTelemetryJournal(config.agentsRoot);
62
69
  const executeProtocol = opts.execution?.runExecution ?? runExecution;
63
70
  let detectedExecutionRuntimes = [];
64
71
  let runtimeFacts = null;
65
72
  const hostCoordinator = opts.execution?.hostCoordinator ?? createHostExecutionCoordinator();
66
- const sharedSlots = hostCoordinatedSlotManager(createSharedSlotManager(config.executionLimits), hostCoordinator);
73
+ const localSlots = createSharedSlotManager(config.executionLimits);
74
+ const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
67
75
  const runtimeStartupGate = hostCoordinatedStartupGate(createRuntimeStartupGate(config.executionLimits), hostCoordinator);
68
76
  const knownExecutionHashes = new Map();
69
77
  const executionReservations = new Map();
@@ -71,6 +79,24 @@ export function serve(config, opts = {}) {
71
79
  let executionFrameQueue = Promise.resolve();
72
80
  const cancellations = new Map();
73
81
  const legacyRuns = new Map();
82
+ const updateEligibility = opts.update?.eligibility
83
+ ?? (() => detectDaemonUpdateEligibility(opts.profileName));
84
+ const updateController = createDaemonUpdateController({
85
+ eligibility: updateEligibility,
86
+ install: opts.update?.install ?? ((input) => installExactDaemonUpdate({
87
+ ...input,
88
+ localSlots,
89
+ hostCoordinator,
90
+ })),
91
+ scheduleRestart: opts.update?.scheduleRestart ?? scheduleServiceRestart,
92
+ sendStatus: (frame) => {
93
+ try {
94
+ if (ws?.readyState === WebSocket.OPEN)
95
+ ws.send(JSON.stringify(frame));
96
+ }
97
+ catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
98
+ },
99
+ });
74
100
  const safeExecutionSend = (frame) => {
75
101
  try {
76
102
  if (ws?.readyState !== WebSocket.OPEN)
@@ -189,7 +215,16 @@ export function serve(config, opts = {}) {
189
215
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
190
216
  void drainSpool();
191
217
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
192
- const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits);
218
+ const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits, process.platform, {
219
+ additionalCapabilities: async () => {
220
+ try {
221
+ return (await updateEligibility()).eligible ? ["daemon_update_v1"] : [];
222
+ }
223
+ catch {
224
+ return [];
225
+ }
226
+ },
227
+ });
193
228
  runtimeFacts = helloPromise.then((hello) => hello.executionRuntimes, (error) => {
194
229
  dslog("execution.runtime_detection_failed", "runtime 探测失败", {
195
230
  level: "ERROR", error_message: error.message,
@@ -247,6 +282,14 @@ export function serve(config, opts = {}) {
247
282
  if (typeof decoded !== "object" || decoded === null)
248
283
  return;
249
284
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
285
+ if (rawType === "daemon:update") {
286
+ void updateController.handle(decoded).catch((error) => {
287
+ dslog("daemon.update_failed", "daemon 更新处理失败", {
288
+ level: "ERROR", error_message: error.message,
289
+ });
290
+ });
291
+ return;
292
+ }
250
293
  if (rawType.startsWith("execution:")) {
251
294
  const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
252
295
  if (!parsedExecution.success) {
@@ -394,6 +437,7 @@ export function serve(config, opts = {}) {
394
437
  }
395
438
  const execution = executeProtocol(config, spec, {
396
439
  ...opts.execution?.dependencies,
440
+ ...(agentMemory === undefined ? {} : { agentMemory }),
397
441
  journal: executionJournal,
398
442
  facts: {
399
443
  availableRuntimes,
@@ -2,6 +2,7 @@ export function createSharedSlotManager(limits) {
2
2
  const activeByHandle = new Map();
3
3
  const queue = [];
4
4
  let activeTotal = 0;
5
+ let exclusive = false;
5
6
  const queuedFor = (handle) => queue.filter((entry) => !entry.released && entry.handle === handle).length;
6
7
  const promote = () => {
7
8
  while (activeTotal < limits.maxParallelTotal) {
@@ -28,6 +29,15 @@ export function createSharedSlotManager(limits) {
28
29
  activeTotal,
29
30
  queuedTotal: queue.length,
30
31
  };
32
+ if (exclusive) {
33
+ return {
34
+ accepted: false,
35
+ facts,
36
+ ready: Promise.resolve(),
37
+ isQueued: () => false,
38
+ release: () => { },
39
+ };
40
+ }
31
41
  const canStartImmediately = activeTotal < limits.maxParallelTotal
32
42
  && activeForAgent < limits.maxParallelPerAgent
33
43
  && queue.length === 0;
@@ -79,5 +89,20 @@ export function createSharedSlotManager(limits) {
79
89
  };
80
90
  },
81
91
  snapshot: () => ({ activeTotal, queuedTotal: queue.length }),
92
+ tryAcquireExclusive: () => {
93
+ if (exclusive || activeTotal !== 0 || queue.length !== 0)
94
+ return null;
95
+ exclusive = true;
96
+ let released = false;
97
+ return {
98
+ release: () => {
99
+ if (released)
100
+ return;
101
+ released = true;
102
+ exclusive = false;
103
+ promote();
104
+ },
105
+ };
106
+ },
82
107
  };
83
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.5.31",
3
+ "version": "0.5.33",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",