@nowcrew/daemon 0.6.20 → 0.6.21

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,16 @@
1
+ import { dslog } from "./slog.js";
2
+ export function completionRetransmitterOptions(execution) {
3
+ return {
4
+ ...(execution?.completionRetryDelaysMs === undefined ? {} : { retryDelaysMs: execution.completionRetryDelaysMs }),
5
+ ...(execution?.completionRetryMaxAttempts === undefined ? {} : { maxAttempts: execution.completionRetryMaxAttempts }),
6
+ ...(execution?.completionRetryMaxAgeMs === undefined ? {} : { maxAgeMs: execution.completionRetryMaxAgeMs }),
7
+ onAttempt: ({ kind, executionId, attempt, nextDelayMs }) => {
8
+ dslog(kind === "sent" ? "execution.completion_sent" : "execution.completion_retried", kind === "sent" ? "execution completion 已发送" : "execution completion 未确认,已重传", { execution_id: executionId, attempt, next_delay_ms: nextDelayMs });
9
+ },
10
+ onRetryExhausted: ({ executionId, attempts, reason }) => {
11
+ dslog("execution.completion_retry_exhausted", "execution completion 重试已耗尽,仍未收到 ACK", {
12
+ level: "ERROR", execution_id: executionId, attempts, reason,
13
+ });
14
+ },
15
+ };
16
+ }
@@ -1,7 +1,10 @@
1
1
  const DEFAULT_DELAYS = Object.freeze([1_000, 2_000, 4_000, 8_000, 16_000, 30_000]);
2
2
  export function createCompletionRetransmitter(options) {
3
3
  const delays = options.retryDelaysMs?.length ? [...options.retryDelaysMs] : [...DEFAULT_DELAYS];
4
+ const maxAttempts = options.maxAttempts ?? 20;
5
+ const maxAgeMs = options.maxAgeMs ?? 10 * 60 * 1_000;
4
6
  const pending = new Map();
7
+ const exhausted = new Set();
5
8
  let active = false;
6
9
  let stopped = false;
7
10
  const delayFor = (attempts) => delays[Math.min(Math.max(attempts - 1, 0), delays.length - 1)];
@@ -10,11 +13,31 @@ export function createCompletionRetransmitter(options) {
10
13
  clearTimeout(entry.timer);
11
14
  entry.timer = null;
12
15
  };
16
+ const expire = (entry, reason) => {
17
+ clear(entry);
18
+ pending.delete(entry.frame.executionId);
19
+ exhausted.add(entry.frame.executionId);
20
+ options.onRetryExhausted?.({
21
+ executionId: entry.frame.executionId,
22
+ attempts: entry.attempts,
23
+ reason,
24
+ });
25
+ };
26
+ const ageRemainingMs = (entry) => maxAgeMs - (Date.now() - entry.startedAtMs);
13
27
  function schedule(entry) {
14
28
  clear(entry);
15
29
  if (!active || stopped)
16
30
  return;
17
- const delay = delayFor(entry.attempts);
31
+ if (entry.attempts >= maxAttempts) {
32
+ expire(entry, "max_attempts");
33
+ return;
34
+ }
35
+ const remainingMs = ageRemainingMs(entry);
36
+ if (remainingMs <= 0) {
37
+ expire(entry, "max_age");
38
+ return;
39
+ }
40
+ const delay = Math.min(delayFor(entry.attempts), remainingMs);
18
41
  entry.timer = setTimeout(() => {
19
42
  entry.timer = null;
20
43
  attempt(entry);
@@ -24,10 +47,18 @@ export function createCompletionRetransmitter(options) {
24
47
  function attempt(entry) {
25
48
  if (!active || stopped)
26
49
  return;
50
+ if (entry.attempts >= maxAttempts) {
51
+ expire(entry, "max_attempts");
52
+ return;
53
+ }
54
+ if (ageRemainingMs(entry) <= 0) {
55
+ expire(entry, "max_age");
56
+ return;
57
+ }
27
58
  const accepted = options.send(entry.frame);
28
59
  if (accepted) {
29
60
  entry.attempts += 1;
30
- const nextDelayMs = delayFor(entry.attempts);
61
+ const nextDelayMs = Math.min(delayFor(entry.attempts), Math.max(ageRemainingMs(entry), 0));
31
62
  options.onAttempt?.({
32
63
  kind: entry.attempts === 1 ? "sent" : "retried",
33
64
  executionId: entry.frame.executionId,
@@ -35,13 +66,17 @@ export function createCompletionRetransmitter(options) {
35
66
  nextDelayMs,
36
67
  });
37
68
  }
69
+ if (entry.attempts >= maxAttempts) {
70
+ expire(entry, "max_attempts");
71
+ return;
72
+ }
38
73
  schedule(entry);
39
74
  }
40
75
  return {
41
76
  track(frame) {
42
- if (stopped || pending.has(frame.executionId))
77
+ if (stopped || pending.has(frame.executionId) || exhausted.has(frame.executionId))
43
78
  return;
44
- const entry = { frame, attempts: 0, timer: null };
79
+ const entry = { frame, startedAtMs: Date.now(), attempts: 0, timer: null };
45
80
  pending.set(frame.executionId, entry);
46
81
  if (active)
47
82
  attempt(entry);
@@ -278,6 +278,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
278
278
  if (key.startsWith("CREW_AGENT_MEMORY_"))
279
279
  delete inheritedEnv[key];
280
280
  }
281
+ const sourceCodexHome = inheritedEnv.CODEX_HOME
282
+ ?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
281
283
  try {
282
284
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
283
285
  throw new Error("DeepSeek API key is not configured for this Agent");
@@ -285,11 +287,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
285
287
  if (isDeepSeekCodex) {
286
288
  await awaitWithCancellation((dependencies.materializeDeepSeekCodexHome ?? materializeDeepSeekCodexHome)(workspace.homeDir), dependencies.cancellation);
287
289
  }
288
- else if (runtime.name === "codex") {
289
- const sourceCodexHome = inheritedEnv.CODEX_HOME
290
- ?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
291
- await awaitWithCancellation((dependencies.materializeDefaultCodexHome ?? materializeDefaultCodexHome)(workspace.homeDir, sourceCodexHome), dependencies.cancellation);
292
- }
293
290
  const supportsNativeResume = runtime.name === "claude"
294
291
  || (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
295
292
  const storedCurrentPrior = input.session.enabled && supportsNativeResume
@@ -351,6 +348,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
351
348
  const launchSessionId = resumeSessionId ?? (rotated
352
349
  ? await rotateAgentSession(workspace.sessionDir)
353
350
  : workspace.agentSessionId);
351
+ if (runtime.name === "codex") {
352
+ await awaitWithCancellation((dependencies.materializeDefaultCodexHome ?? materializeDefaultCodexHome)(workspace.homeDir, sourceCodexHome, launchSessionId), dependencies.cancellation);
353
+ }
354
354
  const promptContext = {
355
355
  workspace: executionWorkspace,
356
356
  resuming,
package/dist/main.js CHANGED
File without changes
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { migrateCodexHome } from "./codex-home-migration.js";
4
+ const { values } = parseArgs({
5
+ args: process.argv.slice(2),
6
+ strict: true,
7
+ options: {
8
+ "agent-home": { type: "string" },
9
+ "source-home": { type: "string" },
10
+ "backup-root": { type: "string" },
11
+ yes: { type: "boolean", default: false },
12
+ },
13
+ });
14
+ function required(value, name) {
15
+ if (!value?.trim())
16
+ throw new Error(`Missing --${name}`);
17
+ return value;
18
+ }
19
+ if (!values.yes)
20
+ throw new Error("Refusing Codex home migration without --yes");
21
+ const result = await migrateCodexHome({
22
+ agentHome: required(values["agent-home"], "agent-home"),
23
+ sourceHome: required(values["source-home"], "source-home"),
24
+ backupRoot: required(values["backup-root"], "backup-root"),
25
+ });
26
+ process.stdout.write(`${result.status}${result.backupDir ? ` backup=${result.backupDir}` : ""}\n`);
@@ -0,0 +1,112 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { lstat, mkdir, readlink, rename, rm, symlink, unlink, } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { atomicPrivateWrite } from "../atomic-private-write.js";
7
+ import { defaultCodexHome, materializeDefaultCodexHome } from "./codex-home.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const SQLITE_FILES = ["state_5.sqlite", "state_5.sqlite-wal", "state_5.sqlite-shm"];
10
+ const MARKER_NAME = ".codex-home-migration.json";
11
+ async function exists(path) {
12
+ try {
13
+ await lstat(path);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ if (error.code === "ENOENT")
18
+ return false;
19
+ throw error;
20
+ }
21
+ }
22
+ async function assertNoOpenSqliteFiles(codexHome) {
23
+ for (const name of SQLITE_FILES) {
24
+ const path = join(codexHome, name);
25
+ if (!(await exists(path)))
26
+ continue;
27
+ try {
28
+ const result = await execFileAsync("lsof", ["-t", "--", path], { encoding: "utf8" });
29
+ if (result.stdout.trim()) {
30
+ throw new Error(`Codex SQLite file is still open: ${path}`);
31
+ }
32
+ }
33
+ catch (error) {
34
+ const code = error.code;
35
+ if (code === 1)
36
+ continue;
37
+ if (code === "ENOENT")
38
+ throw new Error("lsof is required to verify Codex home quiescence");
39
+ throw error;
40
+ }
41
+ }
42
+ }
43
+ async function symlinkTarget(path) {
44
+ try {
45
+ const info = await lstat(path);
46
+ if (!info.isSymbolicLink())
47
+ return null;
48
+ return await readlink(path);
49
+ }
50
+ catch (error) {
51
+ if (error.code === "ENOENT")
52
+ return null;
53
+ throw error;
54
+ }
55
+ }
56
+ export async function migrateCodexHome(input, dependencies = {}) {
57
+ const agentHome = resolve(input.agentHome);
58
+ const sourceHome = resolve(input.sourceHome);
59
+ const backupRoot = resolve(input.backupRoot);
60
+ const codexHome = defaultCodexHome(agentHome);
61
+ const sessions = join(codexHome, "sessions");
62
+ const index = join(codexHome, "session_index.jsonl");
63
+ const expectedSessions = resolve(sourceHome, "sessions");
64
+ const expectedIndex = resolve(sourceHome, "session_index.jsonl");
65
+ const marker = join(codexHome, MARKER_NAME);
66
+ const sessionLink = await symlinkTarget(sessions);
67
+ const indexLink = await symlinkTarget(index);
68
+ if (sessionLink === null && indexLink === null && await exists(marker)) {
69
+ return { status: "already-migrated" };
70
+ }
71
+ if (sessionLink === null || indexLink === null) {
72
+ throw new Error("Refusing migration: Codex home is not in the complete legacy symlink layout");
73
+ }
74
+ if (resolve(codexHome, sessionLink) !== expectedSessions || resolve(codexHome, indexLink) !== expectedIndex) {
75
+ throw new Error("Refusing migration: legacy symlinks do not point to the configured source home");
76
+ }
77
+ await (dependencies.ensureQuiesced ?? assertNoOpenSqliteFiles)(codexHome);
78
+ const stamp = (dependencies.now ?? (() => new Date()))().toISOString().replaceAll(/[^0-9]/g, "").slice(0, 14);
79
+ const backupDir = join(backupRoot, `${stamp}-${dependencies.id?.() ?? randomUUID()}`);
80
+ await mkdir(backupDir, { recursive: true, mode: 0o700 });
81
+ await symlink(sessionLink, join(backupDir, "sessions"), "dir");
82
+ await symlink(indexLink, join(backupDir, "session_index.jsonl"), "file");
83
+ const moved = [];
84
+ try {
85
+ for (const name of SQLITE_FILES) {
86
+ const current = join(codexHome, name);
87
+ if (await exists(current)) {
88
+ await rename(current, join(backupDir, name));
89
+ moved.push(name);
90
+ }
91
+ }
92
+ await materializeDefaultCodexHome(agentHome, sourceHome);
93
+ await atomicPrivateWrite(marker, JSON.stringify({
94
+ version: 1,
95
+ migrated_at: new Date().toISOString(),
96
+ source_home: sourceHome,
97
+ backup_dir: backupDir,
98
+ }) + "\n");
99
+ return { status: "migrated", backupDir };
100
+ }
101
+ catch (error) {
102
+ for (const name of moved.reverse()) {
103
+ await rename(join(backupDir, name), join(codexHome, name)).catch(() => undefined);
104
+ }
105
+ await rm(sessions, { recursive: true, force: true });
106
+ await unlink(index).catch(() => undefined);
107
+ await symlink(sessionLink, sessions, "dir").catch(() => undefined);
108
+ await symlink(indexLink, index, "file").catch(() => undefined);
109
+ throw error;
110
+ }
111
+ }
112
+ export const codexHomeMigrationMarkerName = MARKER_NAME;
@@ -1,15 +1,11 @@
1
- import { chmod, lstat, mkdir, symlink } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
1
+ import { chmod, copyFile, lstat, mkdir, open, readdir, readlink, rename, rm, symlink, unlink, link, writeFile, } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
4
  const PRIVATE_CODEX_HOME = ".codex";
4
- const SHARED_CODEX_ENTRIES = [
5
- { name: "auth.json", type: "file" },
6
- { name: "sessions", type: "dir" },
7
- { name: "session_index.jsonl", type: "file" },
8
- ];
9
5
  export function defaultCodexHome(homeDir) {
10
6
  return resolve(homeDir, PRIVATE_CODEX_HOME);
11
7
  }
12
- async function linkIfMissing(source, target, type) {
8
+ async function linkIfMissing(source, target) {
13
9
  try {
14
10
  await lstat(target);
15
11
  return;
@@ -27,24 +23,211 @@ async function linkIfMissing(source, target, type) {
27
23
  throw error;
28
24
  }
29
25
  try {
30
- await symlink(source, target, type);
26
+ await symlink(source, target, "file");
31
27
  }
32
28
  catch (error) {
33
29
  if (error.code !== "EEXIST")
34
30
  throw error;
35
31
  }
36
32
  }
37
- /**
38
- * Isolate Codex's high-volume runtime state per Agent while preserving the
39
- * machine login and native resume files used by existing sessions.
40
- */
41
- export async function materializeDefaultCodexHome(agentHome, sourceHome) {
33
+ function isBelow(root, candidate) {
34
+ const child = relative(root, candidate);
35
+ return child !== "" && child !== ".." && !child.startsWith(`..${sep}`) && !child.startsWith(sep);
36
+ }
37
+ async function isExactSymlink(target, expectedSource) {
38
+ const linkTarget = await readlink(target);
39
+ return resolve(dirname(target), linkTarget) === resolve(expectedSource);
40
+ }
41
+ async function ensurePrivateSessions(target, legacySource) {
42
+ try {
43
+ const current = await lstat(target);
44
+ if (current.isSymbolicLink()) {
45
+ if (legacySource === undefined || !(await isExactSymlink(target, legacySource))) {
46
+ throw new Error(`Refusing to replace unexpected Codex sessions symlink: ${target}`);
47
+ }
48
+ const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
49
+ const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
50
+ try {
51
+ await mkdir(replacement, { mode: 0o700 });
52
+ await rename(target, legacyBackup);
53
+ try {
54
+ await rename(replacement, target);
55
+ }
56
+ catch (error) {
57
+ await rename(legacyBackup, target).catch(() => undefined);
58
+ throw error;
59
+ }
60
+ }
61
+ finally {
62
+ await rm(replacement, { recursive: true, force: true }).catch(() => undefined);
63
+ await rm(legacyBackup, { recursive: true, force: true }).catch(() => undefined);
64
+ }
65
+ }
66
+ else if (!current.isDirectory()) {
67
+ throw new Error(`Codex sessions target is not a directory: ${target}`);
68
+ }
69
+ }
70
+ catch (error) {
71
+ if (error.code !== "ENOENT")
72
+ throw error;
73
+ await mkdir(target, { mode: 0o700 });
74
+ }
75
+ await chmod(target, 0o700);
76
+ }
77
+ async function atomicCopyFile(source, target, mode) {
78
+ const parent = dirname(target);
79
+ await mkdir(parent, { recursive: true, mode: 0o700 });
80
+ const temporary = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
81
+ try {
82
+ await copyFile(source, temporary, constants.COPYFILE_EXCL);
83
+ await chmod(temporary, mode);
84
+ try {
85
+ await link(temporary, target);
86
+ return true;
87
+ }
88
+ catch (error) {
89
+ if (error.code !== "EEXIST")
90
+ throw error;
91
+ return false;
92
+ }
93
+ }
94
+ finally {
95
+ await unlink(temporary).catch(() => undefined);
96
+ }
97
+ }
98
+ async function ensurePrivateIndex(target, source) {
99
+ let current;
100
+ try {
101
+ current = await lstat(target);
102
+ }
103
+ catch (error) {
104
+ if (error.code !== "ENOENT")
105
+ throw error;
106
+ }
107
+ if (current?.isSymbolicLink()) {
108
+ if (source === undefined || !(await isExactSymlink(target, source))) {
109
+ throw new Error(`Refusing to replace unexpected Codex session index symlink: ${target}`);
110
+ }
111
+ const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
112
+ const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
113
+ try {
114
+ try {
115
+ await lstat(source);
116
+ await copyFile(source, replacement, constants.COPYFILE_EXCL);
117
+ }
118
+ catch (error) {
119
+ if (error.code !== "ENOENT")
120
+ throw error;
121
+ await writeFile(replacement, "", { encoding: "utf8", flag: "wx" });
122
+ }
123
+ await chmod(replacement, 0o600);
124
+ await rename(target, legacyBackup);
125
+ try {
126
+ await rename(replacement, target);
127
+ }
128
+ catch (error) {
129
+ await rename(legacyBackup, target).catch(() => undefined);
130
+ throw error;
131
+ }
132
+ }
133
+ finally {
134
+ await rm(replacement, { force: true }).catch(() => undefined);
135
+ await rm(legacyBackup, { force: true }).catch(() => undefined);
136
+ }
137
+ current = undefined;
138
+ }
139
+ if (current !== undefined && !current.isFile()) {
140
+ throw new Error(`Codex session index target is not a regular file: ${target}`);
141
+ }
142
+ if (current !== undefined) {
143
+ await chmod(target, 0o600);
144
+ return;
145
+ }
146
+ if (source !== undefined) {
147
+ try {
148
+ await lstat(source);
149
+ if (await atomicCopyFile(source, target, 0o600))
150
+ return;
151
+ return;
152
+ }
153
+ catch (error) {
154
+ if (error.code !== "ENOENT")
155
+ throw error;
156
+ }
157
+ }
158
+ const handle = await open(target, "wx", 0o600).catch((error) => {
159
+ if (error.code === "EEXIST")
160
+ return null;
161
+ throw error;
162
+ });
163
+ await handle?.close();
164
+ }
165
+ async function findRollout(root, sessionId) {
166
+ const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
167
+ if (error.code === "ENOENT")
168
+ return [];
169
+ throw error;
170
+ });
171
+ const matches = [];
172
+ for (const entry of entries) {
173
+ const path = join(root, entry.name);
174
+ if (entry.isDirectory()) {
175
+ const nested = await findRollout(path, sessionId);
176
+ if (nested !== null)
177
+ matches.push(nested);
178
+ }
179
+ else if (entry.isFile() && entry.name.includes(sessionId)) {
180
+ matches.push(path);
181
+ }
182
+ }
183
+ if (matches.length > 1)
184
+ throw new Error(`Multiple Codex rollout files match session ${sessionId}`);
185
+ return matches[0] ?? null;
186
+ }
187
+ async function hydrateResumeRollout(sourceSessions, privateSessions, sessionId) {
188
+ if (sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
189
+ throw new Error("Invalid Codex resume session id");
190
+ }
191
+ const sourcePath = await findRollout(sourceSessions, sessionId);
192
+ if (sourcePath === null)
193
+ return;
194
+ const relativePath = relative(sourceSessions, sourcePath);
195
+ const destination = resolve(privateSessions, relativePath);
196
+ if (!isBelow(resolve(privateSessions), destination)) {
197
+ throw new Error("Codex resume rollout escaped the private sessions directory");
198
+ }
199
+ let existing;
200
+ try {
201
+ existing = await lstat(destination);
202
+ }
203
+ catch (error) {
204
+ if (error.code !== "ENOENT")
205
+ throw error;
206
+ }
207
+ if (existing !== undefined) {
208
+ if (!existing.isFile() || existing.isSymbolicLink()) {
209
+ throw new Error(`Refusing to replace existing Codex rollout: ${destination}`);
210
+ }
211
+ return;
212
+ }
213
+ const mode = (await lstat(sourcePath)).mode & 0o777;
214
+ await atomicCopyFile(sourcePath, destination, mode);
215
+ }
216
+ /** Isolate Codex high-volume state per Agent while preserving auth and exact resume. */
217
+ export async function materializeDefaultCodexHome(agentHome, sourceHome, resumeSessionId) {
42
218
  const codexHome = defaultCodexHome(agentHome);
43
219
  await mkdir(codexHome, { recursive: true, mode: 0o700 });
44
220
  await chmod(codexHome, 0o700);
45
221
  const source = sourceHome === undefined ? null : resolve(sourceHome);
46
- if (source === null || source === codexHome)
47
- return codexHome;
48
- await Promise.all(SHARED_CODEX_ENTRIES.map(({ name, type }) => linkIfMissing(join(source, name), join(codexHome, name), type)));
222
+ if (source !== null && source !== codexHome) {
223
+ await linkIfMissing(join(source, "auth.json"), join(codexHome, "auth.json"));
224
+ }
225
+ const sourceSessions = source === null || source === codexHome ? undefined : join(source, "sessions");
226
+ await ensurePrivateSessions(join(codexHome, "sessions"), sourceSessions);
227
+ const sourceIndex = source === null || source === codexHome ? undefined : join(source, "session_index.jsonl");
228
+ await ensurePrivateIndex(join(codexHome, "session_index.jsonl"), sourceIndex);
229
+ if (sourceSessions !== undefined && resumeSessionId) {
230
+ await hydrateResumeRollout(sourceSessions, join(codexHome, "sessions"), resumeSessionId);
231
+ }
49
232
  return codexHome;
50
233
  }
package/dist/serve.js CHANGED
@@ -26,6 +26,7 @@ import { createShutdownDeadline, readTestShutdownConfiguration } from "./shutdow
26
26
  import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
27
27
  import { createSharedSlotManager } from "./shared-execution-slots.js";
28
28
  import { createCompletionRetransmitter } from "./completion-retransmitter.js";
29
+ import { completionRetransmitterOptions } from "./completion-retransmitter-logging.js";
29
30
  import { createAgentMemoryBridge } from "./agent-memory/bridge.js";
30
31
  import { createRuntimeStartupGate } from "./runtime-startup-gate.js";
31
32
  import { createHostExecutionCoordinator, hostCoordinatedSlotManager, hostCoordinatedStartupGate, } from "./host-execution-coordinator.js";
@@ -185,12 +186,7 @@ export function serve(config, opts = {}) {
185
186
  };
186
187
  const completionRetransmitter = createCompletionRetransmitter({
187
188
  send: safeExecutionSend,
188
- ...(opts.execution?.completionRetryDelaysMs === undefined ? {} : {
189
- retryDelaysMs: opts.execution.completionRetryDelaysMs,
190
- }),
191
- onAttempt: ({ kind, executionId, attempt, nextDelayMs }) => {
192
- dslog(kind === "sent" ? "execution.completion_sent" : "execution.completion_retried", kind === "sent" ? "execution completion 已发送" : "execution completion 未确认,已重传", { execution_id: executionId, attempt, next_delay_ms: nextDelayMs });
193
- },
189
+ ...completionRetransmitterOptions(opts.execution),
194
190
  });
195
191
  const reportExecutionFrame = async (frame) => {
196
192
  if (frame.type === "execution:activity" || frame.type === "execution:console") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,13 +16,6 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "scripts": {
20
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
21
- "build": "tsc -p tsconfig.json",
22
- "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
23
- "test": "vitest run",
24
- "typecheck": "tsc --noEmit"
25
- },
26
19
  "dependencies": {
27
20
  "@agentclientprotocol/sdk": "1.2.1",
28
21
  "@nowcrew/cli": "^0.4.13",
@@ -41,5 +34,12 @@
41
34
  "tsx": "^4.19.0",
42
35
  "typescript": "^5.6.0",
43
36
  "vitest": "^2.1.0"
37
+ },
38
+ "scripts": {
39
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
40
+ "build": "tsc -p tsconfig.json",
41
+ "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
42
+ "test": "vitest run",
43
+ "typecheck": "tsc --noEmit"
44
44
  }
45
- }
45
+ }