@nowcrew/daemon 0.6.65 → 0.6.66

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 (57) hide show
  1. package/dist/agent-ability/materializer.js +1 -20
  2. package/dist/agent-ability/resolver.js +10 -7
  3. package/dist/agent-ability/runtime.js +0 -3
  4. package/dist/automatic-install-target.js +10 -15
  5. package/dist/computer-cli.js +1 -10
  6. package/dist/computer-service.js +1 -41
  7. package/dist/config.js +0 -44
  8. package/dist/daemon-migration-controller.js +18 -16
  9. package/dist/execution-runner.js +1 -3
  10. package/dist/local-execution-prompt.js +9 -14
  11. package/dist/local-executor.js +3 -56
  12. package/dist/machine-info.js +2 -3
  13. package/dist/main.js +0 -83
  14. package/dist/managed-service-lifecycle.js +2 -47
  15. package/dist/managed-service-startup.js +0 -2
  16. package/dist/profile-layout.js +4 -10
  17. package/dist/project-skills/controller.js +0 -2
  18. package/dist/prompt.js +1 -19
  19. package/dist/runner.js +0 -3
  20. package/dist/runtimes/codex-app-server-runner.js +14 -54
  21. package/dist/runtimes/codex-contract.js +5 -20
  22. package/dist/runtimes/codex.js +0 -4
  23. package/dist/serve.js +3 -68
  24. package/dist/supervised-runtime.js +0 -3
  25. package/dist/workspace.js +1 -24
  26. package/package.json +9 -10
  27. package/dist/local-memory/index-budget.js +0 -74
  28. package/dist/local-memory/pending.js +0 -111
  29. package/dist/local-memory/runtime.js +0 -87
  30. package/dist/local-memory/types.js +0 -1
  31. package/dist/local-memory/write.js +0 -448
  32. package/dist/runtimes/codex-command-safety.js +0 -75
  33. package/dist/runtimes/codex-turn-retry.js +0 -69
  34. package/dist/storage-config.js +0 -175
  35. package/dist/storage-control.js +0 -193
  36. package/dist/storage-copy.js +0 -609
  37. package/dist/storage-credential-guard.js +0 -21
  38. package/dist/storage-doctor.js +0 -228
  39. package/dist/storage-identity.js +0 -29
  40. package/dist/storage-layout.js +0 -143
  41. package/dist/storage-manifest.js +0 -855
  42. package/dist/storage-migration-controller.js +0 -417
  43. package/dist/storage-migration-manifest.js +0 -105
  44. package/dist/storage-migration-policy.js +0 -100
  45. package/dist/storage-runtime-controller.js +0 -339
  46. package/dist/storage-runtime-readiness.js +0 -184
  47. package/dist/storage-runtime.js +0 -39
  48. package/dist/storage-safe-fs.js +0 -302
  49. package/dist/storage-serve-adapter.js +0 -65
  50. package/dist/storage-status-delivery.js +0 -209
  51. package/dist/storage-status-replay.js +0 -125
  52. package/dist/storage-target-confirmation.js +0 -129
  53. package/dist/storage-worker-command.js +0 -27
  54. package/dist/storage-worker-executor.js +0 -384
  55. package/dist/storage-worker-handoff.js +0 -151
  56. package/dist/storage-worker-operation.js +0 -284
  57. package/dist/storage-worker-status.js +0 -105
package/dist/main.js CHANGED
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import { parseArgs } from "node:util";
9
9
  import { homedir } from "node:os";
10
- import { join } from "node:path";
11
10
  import { loadConfig, ConfigError } from "./config.js";
12
11
  import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
13
12
  import { cliVersion, daemonVersion } from "./machine-info.js";
@@ -22,18 +21,7 @@ import { formatDaemonStartupError } from "./daemon-startup-error.js";
22
21
  import { assertManagedServiceStartup } from "./managed-service-startup.js";
23
22
  import { daemonGlobalInstallation } from "./daemon-installation.js";
24
23
  import { acquireDaemonInstallationLease, runWithDaemonInstallationLease, } from "./daemon-installation-lease.js";
25
- import { loadStorageRuntime } from "./storage-runtime.js";
26
- import { readStorageMigrationReceipt } from "./storage-migration-controller.js";
27
- import { storageMigrationStatusReplay } from "./storage-status-replay.js";
28
- import { runStorageWorkerCommand } from "./storage-worker-command.js";
29
- import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
30
- import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
31
24
  async function main() {
32
- const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
33
- if (workerResult !== null) {
34
- process.exitCode = workerResult;
35
- return;
36
- }
37
25
  const computerResult = await runComputerCommand(process.argv.slice(2));
38
26
  if (computerResult !== null) {
39
27
  process.exitCode = computerResult;
@@ -69,10 +57,8 @@ async function main() {
69
57
  // 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
70
58
  if (values["daemon-home"])
71
59
  process.env.CREW_DAEMON_HOME = values["daemon-home"];
72
- let activeDaemonHome;
73
60
  if (values.profile) {
74
61
  const home = daemonHome();
75
- activeDaemonHome = home;
76
62
  const profile = await loadProfile(values.profile, home);
77
63
  if (cmd === "serve") {
78
64
  await assertProfileAgentsRootUnique(profile, home, homedir());
@@ -85,12 +71,6 @@ async function main() {
85
71
  agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
86
72
  nodePath: process.execPath,
87
73
  entryPath: builtDaemonEntry(),
88
- environment: {
89
- ...(process.env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: process.env.NOWWORK_HOME }),
90
- ...(process.env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: process.env.NOWWORK_ENV_TAG }),
91
- ...(process.env.NOWWORK_STORAGE_POLICY_PATH === undefined
92
- ? {} : { NOWWORK_STORAGE_POLICY_PATH: process.env.NOWWORK_STORAGE_POLICY_PATH }),
93
- },
94
74
  });
95
75
  }
96
76
  applyProfileToEnv(profile, process.env);
@@ -120,72 +100,9 @@ async function main() {
120
100
  const installationLease = installation === null
121
101
  ? null
122
102
  : await acquireDaemonInstallationLease(installation.npmPrefix);
123
- const storageRuntime = values.profile === undefined
124
- ? null
125
- : await loadStorageRuntime({
126
- profile: values.profile,
127
- serverUrl: config.serverUrl,
128
- userHome: homedir(),
129
- platform: process.platform,
130
- environment: process.env,
131
- });
132
- const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
133
- ? { ready: false, reason: "not_legacy_source" }
134
- : await probeStorageMigrationRuntime({
135
- profile: values.profile,
136
- serverUrl: config.serverUrl,
137
- currentDaemonHome: activeDaemonHome,
138
- currentAgentsRoot: config.agentsRoot,
139
- userHome: homedir(),
140
- platform: process.platform,
141
- ...(process.getuid === undefined ? {} : { uid: process.getuid() }),
142
- environment: process.env,
143
- });
144
- const targetConfirmation = storageRuntime?.identity.storageMigrationId === undefined
145
- ? null
146
- : createStorageTargetConfirmationHandler({
147
- backupsRoot: storageRuntime.local.backupsRoot,
148
- identity: storageRuntime.identity,
149
- profile: storageRuntime.installation.profile,
150
- completionTimeoutMs: 180_000,
151
- });
152
- const storageMigration = storageRuntime !== null
153
- ? {
154
- ready: false,
155
- statusOutboxPath: join(storageRuntime.local.stateRoot, "storage-status-outbox.json"),
156
- identity: storageRuntime.identity,
157
- ...(targetConfirmation === null ? {} : { confirm: targetConfirmation.handle }),
158
- ...(storageRuntime.identity.storageMigrationId === undefined ? {} : {
159
- replayStatus: async (send) => {
160
- const migrationId = storageRuntime.identity.storageMigrationId;
161
- const receipt = await readStorageMigrationReceipt(storageRuntime.local.backupsRoot, migrationId);
162
- if (receipt === null)
163
- throw new Error(`storage migration receipt not found: ${migrationId}`);
164
- for (const frame of storageMigrationStatusReplay(receipt, {
165
- migrationId,
166
- environment: storageRuntime.identity.storageEnvironment,
167
- workspaceId: storageRuntime.identity.storageWorkspaceId,
168
- profile: storageRuntime.installation.profile,
169
- }))
170
- send(frame);
171
- },
172
- }),
173
- }
174
- : migrationRuntime.ready
175
- ? {
176
- ready: true,
177
- statusOutboxPath: migrationRuntime.runtime.statusOutboxPath,
178
- identity: migrationRuntime.runtime.identity,
179
- controller: migrationRuntime.runtime.controller,
180
- attach: migrationRuntime.runtime.attach,
181
- bindServerIdentity: migrationRuntime.runtime.bindServerIdentity,
182
- replayStatus: migrationRuntime.runtime.replayStatus,
183
- }
184
- : undefined;
185
103
  await runWithDaemonInstallationLease(installationLease, async () => {
186
104
  const service = serve(config, {
187
105
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
188
- ...(storageMigration === undefined ? {} : { storageMigration }),
189
106
  });
190
107
  await runServeLifecycle(service);
191
108
  });
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { acquireDaemonInstallationLease } from "./daemon-installation-lease.js";
3
- import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, replaceInstalledServiceDescriptor, uninstallService, } from "./computer-service.js";
3
+ import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, uninstallService, } from "./computer-service.js";
4
4
  import { ManagedServiceConflictError, ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, managedServiceConflict, normalizeManagedServiceRecord, serviceDescriptorSha256, withManagedServiceRegistryLock, } from "./managed-service-registry.js";
5
5
  function requirePortableManagedSpec(spec) {
6
6
  if ((spec.platform !== "darwin" && spec.platform !== "linux")
@@ -35,17 +35,6 @@ function sameManagedIdentity(left, right) {
35
35
  const { generation: _generation, createdAt: _createdAt, ...expected } = right;
36
36
  return managedServiceIdentityMatches(left, expected);
37
37
  }
38
- function sameManagedResource(left, right) {
39
- const { generation: _leftGeneration, createdAt: _leftCreatedAt, descriptorSha256: _leftHash, ...leftResource } = left;
40
- const { generation: _rightGeneration, createdAt: _rightCreatedAt, descriptorSha256: _rightHash, ...rightResource } = right;
41
- return JSON.stringify(leftResource) === JSON.stringify(rightResource);
42
- }
43
- function descriptorDiffIsStorageEnvironmentOnly(previous, next) {
44
- if (!next.includes("\n <key>EnvironmentVariables</key>\n"))
45
- return false;
46
- const strip = (descriptor) => descriptor.replace(/\n <key>EnvironmentVariables<\/key>\n <dict>\n(?: <key>(?:NOWWORK_HOME|NOWWORK_ENV_TAG|NOWWORK_STORAGE_POLICY_PATH)<\/key><string>[^<]*<\/string>\n)+ <\/dict>\n/u, "\n");
47
- return strip(previous) === strip(next);
48
- }
49
38
  function assertRecordDescribesSpec(input) {
50
39
  requirePortableManagedSpec(input.spec);
51
40
  const { spec, record } = input;
@@ -62,20 +51,8 @@ function assertRecordDescribesSpec(input) {
62
51
  }
63
52
  }
64
53
  export async function installManagedService(input) {
54
+ assertRecordDescribesSpec(input);
65
55
  requirePortableManagedSpec(input.spec);
66
- const requestedDescriptor = input.spec.descriptor;
67
- const expectedDescriptorHash = serviceDescriptorSha256(input.spec.descriptor);
68
- const expectedPath = normalizeManagedServiceRecord({
69
- ...input.record,
70
- descriptorPath: input.spec.descriptorPath,
71
- descriptorSha256: expectedDescriptorHash,
72
- }).descriptorPath;
73
- if (input.record.platform !== input.spec.platform
74
- || input.record.serviceId !== input.spec.id
75
- || input.record.profile !== input.spec.profile
76
- || input.record.descriptorPath !== expectedPath) {
77
- throw new ManagedServiceRegistryError(`Managed service record does not describe '${input.spec.id}'`);
78
- }
79
56
  const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
80
57
  await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services, replace }) => {
81
58
  const exact = services.find((record) => sameManagedIdentity(record, input.record));
@@ -99,28 +76,6 @@ export async function installManagedService(input) {
99
76
  }
100
77
  return;
101
78
  }
102
- const registered = services.find((record) => record.serviceId === input.record.serviceId
103
- && record.profile === input.record.profile
104
- && record.daemonHome === input.record.daemonHome);
105
- if (registered !== undefined && sameManagedResource(registered, input.record)) {
106
- const current = await readServiceDescriptor(input.spec);
107
- if (current === null || serviceDescriptorSha256(current) !== registered.descriptorSha256) {
108
- throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
109
- }
110
- if (!descriptorDiffIsStorageEnvironmentOnly(current, requestedDescriptor)) {
111
- throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' conflicts with the requested identity`);
112
- }
113
- await replaceInstalledServiceDescriptor(input.spec, current, input.runner);
114
- try {
115
- await replace(services.map((record) => record === registered ? input.record : record));
116
- }
117
- catch (error) {
118
- const rollbackSpec = { ...input.spec, descriptor: current };
119
- await replaceInstalledServiceDescriptor(rollbackSpec, requestedDescriptor, input.runner).catch(() => undefined);
120
- throw error;
121
- }
122
- return;
123
- }
124
79
  const conflict = managedServiceConflict(services, input.record);
125
80
  if (conflict !== null) {
126
81
  throw new ManagedServiceConflictError(conflict.resource, input.record, conflict.owner);
@@ -14,7 +14,6 @@ export async function assertManagedServiceStartup(input) {
14
14
  nodePath: input.nodePath,
15
15
  entryPath: input.entryPath,
16
16
  profileHome: input.daemonHome,
17
- ...(input.environment === undefined ? {} : { environment: input.environment }),
18
17
  });
19
18
  const readRegistry = input.readManagedServices
20
19
  ?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
@@ -43,7 +42,6 @@ export async function assertManagedServiceStartup(input) {
43
42
  nodePath: input.nodePath,
44
43
  entryPath: input.entryPath,
45
44
  profileHome: input.daemonHome,
46
- ...(input.environment === undefined ? {} : { environment: input.environment }),
47
45
  legacyServiceId: true,
48
46
  });
49
47
  const legacyDescriptor = await readDescriptor(legacySpec);
@@ -2,10 +2,6 @@ import { posix, win32 } from "node:path";
2
2
  export const PROFILE_LAYOUT_VERSION = 1;
3
3
  const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
4
4
  const LOCAL_INSTANCE_NAME = /^[a-z0-9][a-z0-9_-]{0,39}$/u;
5
- /** Accept the legacy `in` tag at the boundary, but never persist it in a new layout. */
6
- export function normalizeProfileEnvironment(value) {
7
- return value === "in" ? "production" : value;
8
- }
9
5
  export function validateLocalInstanceName(value) {
10
6
  const normalized = value.trim().toLowerCase();
11
7
  if (!LOCAL_INSTANCE_NAME.test(normalized) || normalized.includes("..")) {
@@ -14,21 +10,19 @@ export function validateLocalInstanceName(value) {
14
10
  return normalized;
15
11
  }
16
12
  export function profileForLocalInstance(environment, localInstanceName) {
17
- const canonicalEnvironment = normalizeProfileEnvironment(environment);
18
- const profile = `nw-${canonicalEnvironment}-${validateLocalInstanceName(localInstanceName)}`;
13
+ const profile = `nw-${environment}-${validateLocalInstanceName(localInstanceName)}`;
19
14
  if (!PROFILE_NAME.test(profile))
20
15
  throw new Error("derived daemon profile exceeds 48 characters");
21
16
  return profile;
22
17
  }
23
18
  export function profileLayout(input) {
24
- const environment = normalizeProfileEnvironment(input.environment);
25
19
  const localInstanceName = validateLocalInstanceName(input.localInstanceName);
26
- const profile = profileForLocalInstance(environment, localInstanceName);
20
+ const profile = profileForLocalInstance(input.environment, localInstanceName);
27
21
  const paths = input.platform === "win32" ? win32 : posix;
28
- const base = `${environment}-${localInstanceName}`;
22
+ const base = `${input.environment}-${localInstanceName}`;
29
23
  return Object.freeze({
30
24
  layoutVersion: PROFILE_LAYOUT_VERSION,
31
- environment,
25
+ environment: input.environment,
32
26
  localInstanceName,
33
27
  profile,
34
28
  daemonHome: paths.join(input.userHome, ".crew", `daemon-${base}`),
@@ -183,8 +183,6 @@ export function createProjectSkillsController(deps) {
183
183
  const parsed = ProjectCommandSchema.safeParse(input);
184
184
  if (!parsed.success)
185
185
  return { ok: false, error: "invalid_project_command" };
186
- if (deps.isAdmissionClosed?.() === true)
187
- return { ok: false, error: "storage_migration_quiescing" };
188
186
  try {
189
187
  assertCurrentEpoch(epochToken);
190
188
  }
package/dist/prompt.js CHANGED
@@ -305,7 +305,7 @@ ${memoryTaskReadRule}
305
305
  - **跨任务协调看实时清单**:用 \`crew task list --mine\` 查你当前所有任务及其状态(谁在排队、谁在评审)。例:你一次只能测一个页面时,据此判断"正在测 #1、#2 排队",在 #2 的工作日志里标注"等 #1 完成"。
306
306
  - 需要更多背景时再 \`Read $CREW_HOME/notes/<topic>.md\`;不要一次把所有笔记读进上下文。
307
307
 
308
- ${communicationSection}${ctx.collaborationToolsEnabled === true ? buildCollaborationToolsPrompt() : ""}
308
+ ${communicationSection}
309
309
 
310
310
  CLI 成功时打印人类可读文本到 stdout;失败时 stderr 给出错误,并用**退出码**告诉你下一步:
311
311
  - \`4\` = 任务已被他人认领 / 不可认领 → 停手,别抢,转做别的。
@@ -385,21 +385,3 @@ export function buildOriginDecisionRetryPrompt(channelId, threadId) {
385
385
  "普通内部消息不算外部回复决策;--send-draft 也不是外部回复开关。",
386
386
  ].join("\n");
387
387
  }
388
- /**
389
- * Codex 原生 collaboration.* 工具的用法与边界(spec 2d;2026-08-28 对 Codex 0.149.1/0.150.1 实测)。
390
- * 只在 daemon 开关打开且 runtime 为 codex 时注入;是本机 runtime 能力事实,不是产品策略。
391
- * 契约来自实测,不编造 Codex 未声明的返回字段。
392
- */
393
- export function buildCollaborationToolsPrompt() {
394
- return `
395
-
396
- ## Codex 原生协作工具
397
- 本机 Codex 已开启 \`collaboration.*\` 子 agent 工具。它们完全在 Codex 进程内运行:子 agent 的工作对 NowWork 的频道、线程、任务、execution 记录和按 agent 的 token 统计**都不可见**,token 并入你本轮的用量。只用于你自己会汇总的内部交叉核对、并行调研;绝不用于人类或其它 NowWork 成员需要观察、审计或接手的事。
398
- - **\`collaboration.spawn_agent\`** —— 必填 \`task_name\`(仅小写字母/数字/下划线)、\`message\`;可选 \`fork_turns\`(\`"none"\`|\`"all"\`|\`"3"\` 这样的轮数,默认 \`"all"\`)、\`model\`、\`reasoning_effort\`。立即启动独立 turn,你不阻塞;子 agent 跑完后 idle 而非销毁。新 agent 用 **canonical 任务名路径**引用,如 \`/root/api_review\`。含 root 最多 4 个活跃 agent。
399
- - **\`collaboration.followup_task\`** —— 必填 \`target\`(canonical 路径,不是 UUID)、\`message\`。异步投递并把 idle 的目标唤醒进新 turn,可重复。
400
- - **\`collaboration.send_message\`** —— 必填 \`target\`、\`message\`。只入队,**不**启动 turn;要触发工作用 followup_task。
401
- - **\`collaboration.list_agents\`** —— 可选 \`path_prefix\`。只读,列出 live agents。
402
- - **\`collaboration.wait_agent\`** —— 可选 \`timeout_ms\`(10000–3600000,默认 30000)。阻塞你直到任一子 agent 有更新、有新输入或超时。
403
- - **没有任何返回值有保证的 schema**:不要去解析返回值里的 \`agent_id\`/\`id\`/\`accepted\` 之类字段;用 \`list_agents\` 和 \`wait_agent\` 获取状态。
404
- - **不要调用 \`collaboration.interrupt_agent\`**:它会把你自己这轮 turn 一起中断(2026-08-10 线上事故),中断后本轮结果全部丢失且不会重跑。让子 agent 自然跑完或直接忽略其结果。`;
405
- }
package/dist/runner.js CHANGED
@@ -58,7 +58,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
58
58
  ...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
59
59
  memory: capMemoryForInject(workspace.memory),
60
60
  ...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
61
- collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
62
61
  }),
63
62
  wakePrompt: ({ resuming, rotatedForBudget, nearBudget }) => {
64
63
  const distillHint = nearBudget
@@ -75,7 +74,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
75
74
  name: runtime,
76
75
  ...(providerConfig.model ? { model: providerConfig.model } : {}),
77
76
  ...(reasoning.success ? { reasoning: reasoning.data } : {}),
78
- codexCollaborationTools: config.codexCollaborationTools,
79
77
  },
80
78
  effectivePermission: config.dangerous ? "full_access" : "workspace_write",
81
79
  captureFinal: true,
@@ -107,7 +105,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
107
105
  softTokens: config.sessionSoftTokens,
108
106
  maxTurns: config.sessionMaxTurns,
109
107
  },
110
- localMemoryV2Enabled: config.localMemoryV2Handles?.has(input.handle) === true,
111
108
  }, {
112
109
  onActivity,
113
110
  onConsole,
@@ -6,8 +6,7 @@ import { pathToFileURL } from "node:url";
6
6
  import spawn from "cross-spawn";
7
7
  import { z } from "zod";
8
8
  import { signalSupervisorTree } from "../execution-supervisor.js";
9
- import { assertCodexCollaborationVersion, assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
10
- import { decideTurnRetry, itemMakesTurnUnsafe } from "./codex-turn-retry.js";
9
+ import { assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
11
10
  import { startFirstProgressWatchdog } from "./progress-watchdog.js";
12
11
  const RunnerInputSchema = z.object({
13
12
  systemPrompt: z.string().min(1),
@@ -21,7 +20,6 @@ const RunnerInputSchema = z.object({
21
20
  cwd: z.string().min(1).optional(),
22
21
  workspaceRoots: z.array(z.string().min(1)).optional(),
23
22
  skillRoots: z.array(z.string().min(1)).optional(),
24
- codexCollaborationTools: z.boolean().optional(),
25
23
  resume: z.boolean(),
26
24
  }).strict();
27
25
  const RPC_TIMEOUT_MS = 30_000;
@@ -39,17 +37,12 @@ const MAX_TRANSIENT_TURN_RETRIES = 2;
39
37
  const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
40
38
  const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
41
39
  const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
42
- /** 进程级:一旦某个 collabAgentToolCall 的 item/completed 没有对应 item/started,本执行后续永不重试(spec R4-3)。 */
43
- let collabStartedMissing = false;
44
40
  const USER_INPUT_UNAVAILABLE_MESSAGE = "NowWork cannot collect interactive user input in this headless run; ask the user in the final response instead of assuming an answer.";
45
- export function codexAppServerArgs(input) {
41
+ export function codexAppServerArgs(projectRootMarkers) {
46
42
  return [
47
- ...(input.projectRootMarkers === undefined
43
+ ...(projectRootMarkers === undefined
48
44
  ? []
49
- : ["-c", `project_root_markers=${JSON.stringify(input.projectRootMarkers)}`]),
50
- // multi_agent 按开关;multi_agent_v2 无条件关,钉死已验证的 v1 契约(spec 2b)。
51
- "-c", `features.multi_agent=${input.collaborationTools === true}`,
52
- "-c", "features.multi_agent_v2=false",
45
+ : ["-c", `project_root_markers=${JSON.stringify(projectRootMarkers)}`]),
53
46
  "app-server", "--listen", "stdio://",
54
47
  ];
55
48
  }
@@ -60,8 +53,7 @@ const TRANSIENT_TURN_ERROR_PATTERNS = [
60
53
  /too many requests/i,
61
54
  ];
62
55
  /** 模型侧瞬态失败(网关过载/限流)→ 换个时间整轮重试大概率成功。
63
- * interrupted 单独归为 collab_interrupted:2026-08-10 实锤是模型自己调 collaboration.interrupt_agent
64
- * 连带把父 turn 置为 interrupted。两类原因都只在本 turn 无白名单外工具活动时重试(codex-turn-retry.ts)。 */
56
+ * interrupted 不算:那是取消信号或 codex collab 连带中断,重跑语义不明确。 */
65
57
  export function isTransientTurnFailure(detail) {
66
58
  return TRANSIENT_TURN_ERROR_PATTERNS.some((pattern) => pattern.test(detail));
67
59
  }
@@ -421,10 +413,7 @@ async function stopChildTree(child) {
421
413
  }
422
414
  async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs) {
423
415
  const attemptStartedAt = Date.now();
424
- const child = spawn(bin, codexAppServerArgs({
425
- ...(input.projectRootMarkers === undefined ? {} : { projectRootMarkers: input.projectRootMarkers }),
426
- ...(input.codexCollaborationTools === undefined ? {} : { collaborationTools: input.codexCollaborationTools }),
427
- }), {
416
+ const child = spawn(bin, codexAppServerArgs(input.projectRootMarkers), {
428
417
  cwd: process.cwd(),
429
418
  env: process.env,
430
419
  stdio: ["pipe", "pipe", "pipe"],
@@ -445,8 +434,6 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
445
434
  let threadId = null;
446
435
  let announcedThreadId = null;
447
436
  let turnId = null;
448
- let turnHasUnsafeSideEffect = false;
449
- const startedItemIds = new Set();
450
437
  let lastUsage;
451
438
  let completionResolve;
452
439
  let completionReject;
@@ -465,20 +452,6 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
465
452
  || method === "item/agentMessage/delta" || method === "item/completed") {
466
453
  firstProgress.observe();
467
454
  }
468
- if (method === "item/started" || method === "item/completed") {
469
- const item = params !== null && typeof params === "object"
470
- ? params.item
471
- : undefined;
472
- if (method === "item/started" && typeof item?.id === "string")
473
- startedItemIds.add(item.id);
474
- if (method === "item/completed" && item?.type === "collabAgentToolCall"
475
- && (typeof item.id !== "string" || !startedItemIds.has(item.id)) && !collabStartedMissing) {
476
- collabStartedMissing = true;
477
- process.stderr.write("[codex-app-server] collab_item_started_missing\n");
478
- }
479
- if (itemMakesTurnUnsafe(item))
480
- turnHasUnsafeSideEffect = true;
481
- }
482
455
  for (const event of mapCodexNotification(method, params)) {
483
456
  const eventThreadId = typeof event.thread_id === "string" ? event.thread_id : null;
484
457
  if (event.type === "thread.started" && eventThreadId === announcedThreadId)
@@ -523,8 +496,6 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
523
496
  if (input.workspaceRoots !== undefined || input.skillRoots !== undefined) {
524
497
  assertCodexNativeWorkspaceVersion(initialized);
525
498
  }
526
- if (input.codexCollaborationTools === true)
527
- assertCodexCollaborationVersion(initialized);
528
499
  logStage(activeStage, "ok", attempt, stageStartedAt);
529
500
  rpc.notify("initialized");
530
501
  activeStage = input.resume ? "thread_resume" : "thread_start";
@@ -609,19 +580,15 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
609
580
  }),
610
581
  });
611
582
  if (completed.turn?.status === "completed")
612
- return { code: 0, initializeTimedOut: false, unsafeToRetry: false };
583
+ return { code: 0, initializeTimedOut: false };
613
584
  const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
614
585
  process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
615
- const status = completed.turn?.status;
616
- const retryReason = cancelling ? undefined
617
- : status === "interrupted" ? "collab_interrupted"
618
- : status === "failed" && isTransientTurnFailure(detail) ? "transient_turn_failure"
619
- : undefined;
620
586
  return {
621
- code: status === "interrupted" ? 130 : 1,
587
+ code: completed.turn?.status === "interrupted" ? 130 : 1,
622
588
  initializeTimedOut: false,
623
- ...(retryReason === undefined ? {} : { retryReason }),
624
- unsafeToRetry: turnHasUnsafeSideEffect || collabStartedMissing,
589
+ transientTurnFailure: !cancelling
590
+ && completed.turn?.status === "failed"
591
+ && isTransientTurnFailure(detail),
625
592
  };
626
593
  }
627
594
  catch (error) {
@@ -634,7 +601,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
634
601
  logStage(activeStage, "error", attempt, stageStartedAt);
635
602
  }
636
603
  process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, secrets)}\n`);
637
- return { code: 1, initializeTimedOut, unsafeToRetry: false };
604
+ return { code: 1, initializeTimedOut };
638
605
  }
639
606
  finally {
640
607
  firstProgress.stop();
@@ -666,20 +633,13 @@ export async function runCodexAppServer(bin, options = {}) {
666
633
  process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=initialize_timeout\n`);
667
634
  continue;
668
635
  }
669
- const decision = decideTurnRetry(result, turnRetries, MAX_TRANSIENT_TURN_RETRIES);
670
- if (decision.kind === "retry") {
636
+ if (result.transientTurnFailure && turnRetries < MAX_TRANSIENT_TURN_RETRIES) {
671
637
  const delayMs = turnRetryDelayMs * TRANSIENT_TURN_RETRY_BACKOFF_FACTOR ** turnRetries;
672
638
  turnRetries += 1;
673
- process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=${decision.reason}\n`);
639
+ process.stderr.write(`[codex-app-server] stage=retry status=start attempt=${attempt + 1} elapsed_ms=0 reason=transient_turn_failure\n`);
674
640
  await new Promise((resolve) => setTimeout(resolve, delayMs));
675
641
  continue;
676
642
  }
677
- if (decision.kind === "skipped") {
678
- process.stderr.write(`[codex-app-server] stage=retry status=skipped attempt=${attempt} reason=${decision.reason}_unsafe\n`);
679
- }
680
- else if (decision.kind === "exhausted") {
681
- process.stderr.write(`[codex-app-server] stage=retry status=exhausted attempt=${attempt} reason=${decision.reason}\n`);
682
- }
683
643
  return result.code;
684
644
  }
685
645
  }
@@ -1,7 +1,6 @@
1
- import { CODEX_COLLABORATION_MIN_VERSION, CODEX_NATIVE_WORKSPACE_MIN_VERSION } from "./codex.js";
1
+ import { CODEX_NATIVE_WORKSPACE_MIN_VERSION } from "./codex.js";
2
2
  import { z } from "zod";
3
3
  export const CODEX_PROJECT_WORKSPACE_UNSUPPORTED = "codex_project_workspace_unsupported";
4
- export const CODEX_COLLABORATION_UNSUPPORTED = "codex_collaboration_unsupported";
5
4
  export const CODEX_PLAN_ENTRY_LIMIT = 16;
6
5
  // 16 steps remain inside the server's 4,000 UTF-16-unit plan payload cap even
7
6
  // when every retained code point is a surrogate pair (for example, emoji).
@@ -52,32 +51,18 @@ const versionTuple = (version) => {
52
51
  const versionAtLeast = (actual, minimum) => actual[0] > minimum[0]
53
52
  || (actual[0] === minimum[0] && (actual[1] > minimum[1]
54
53
  || (actual[1] === minimum[1] && actual[2] >= minimum[2])));
55
- function initializeVersion(response) {
54
+ /** Native roots require the app-server version returned by initialize, not a separate shell probe. */
55
+ export function assertCodexNativeWorkspaceVersion(response) {
56
56
  const parsed = NativeInitializeResponseSchema.safeParse(response);
57
57
  if (!parsed.success)
58
- return null;
58
+ throw codexProjectWorkspaceUnsupported();
59
59
  const match = /(?:^|[/\s])(\d+\.\d+\.\d+)(?=$|[\s(])/u.exec(parsed.data.userAgent);
60
- return match === null ? null : versionTuple(match[1] ?? "");
61
- }
62
- /** Native roots require the app-server version returned by initialize, not a separate shell probe. */
63
- export function assertCodexNativeWorkspaceVersion(response) {
64
- const actual = initializeVersion(response);
60
+ const actual = match === null ? null : versionTuple(match[1] ?? "");
65
61
  const minimum = versionTuple(CODEX_NATIVE_WORKSPACE_MIN_VERSION);
66
62
  if (actual === null || minimum === null || !versionAtLeast(actual, minimum)) {
67
63
  throw codexProjectWorkspaceUnsupported();
68
64
  }
69
65
  }
70
- /** Collaboration tools are exposed only on an app-server whose contract was verified (spec 2b). */
71
- export function assertCodexCollaborationVersion(response) {
72
- const actual = initializeVersion(response);
73
- const minimum = versionTuple(CODEX_COLLABORATION_MIN_VERSION);
74
- if (actual === null || minimum === null || !versionAtLeast(actual, minimum)) {
75
- throw codexCollaborationUnsupported();
76
- }
77
- }
78
- export function codexCollaborationUnsupported() {
79
- return new Error(CODEX_COLLABORATION_UNSUPPORTED);
80
- }
81
66
  const PLAN_STATUSES = new Set(["pending", "inProgress", "completed"]);
82
67
  const clipCharacters = (value, limit) => [...value.trim().replace(/\s+/gu, " ")].slice(0, limit).join("");
83
68
  /** Treat plan updates as bounded user-visible data; any malformed retained step rejects the event. */
@@ -5,8 +5,6 @@
5
5
  import spawn from "cross-spawn";
6
6
  /** Minimum CLI whose real app-server contract supports NowCrew native project workspaces. */
7
7
  export const CODEX_NATIVE_WORKSPACE_MIN_VERSION = "0.148.0";
8
- /** Minimum CLI whose `multi_agent` collaboration tool contract was empirically verified (2026-08-28). */
9
- export const CODEX_COLLABORATION_MIN_VERSION = "0.149.1";
10
8
  // Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
11
9
  // 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
12
10
  // claude 专属的 "max")回落 CODEX_DEFAULT_EFFORT。
@@ -24,8 +22,6 @@ export function buildCodexArgs(input) {
24
22
  ? input.reasoning
25
23
  : CODEX_DEFAULT_EFFORT;
26
24
  args.push("-c", `model_reasoning_effort=${effort}`);
27
- // 纵深防御:exec 启动器没有 initialize 握手、无法断言版本,协作工具恒关(spec 2b)。
28
- args.push("-c", "features.multi_agent=false", "-c", "features.multi_agent_v2=false");
29
25
  if (input.projectRootMarkers) {
30
26
  args.push("-c", `project_root_markers=${JSON.stringify(input.projectRootMarkers)}`);
31
27
  }