@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.
- package/dist/agent-ability/materializer.js +1 -20
- package/dist/agent-ability/resolver.js +10 -7
- package/dist/agent-ability/runtime.js +0 -3
- package/dist/automatic-install-target.js +10 -15
- package/dist/computer-cli.js +1 -10
- package/dist/computer-service.js +1 -41
- package/dist/config.js +0 -44
- package/dist/daemon-migration-controller.js +18 -16
- package/dist/execution-runner.js +1 -3
- package/dist/local-execution-prompt.js +9 -14
- package/dist/local-executor.js +3 -56
- package/dist/machine-info.js +2 -3
- package/dist/main.js +0 -83
- package/dist/managed-service-lifecycle.js +2 -47
- package/dist/managed-service-startup.js +0 -2
- package/dist/profile-layout.js +4 -10
- package/dist/project-skills/controller.js +0 -2
- package/dist/prompt.js +1 -19
- package/dist/runner.js +0 -3
- package/dist/runtimes/codex-app-server-runner.js +14 -54
- package/dist/runtimes/codex-contract.js +5 -20
- package/dist/runtimes/codex.js +0 -4
- package/dist/serve.js +3 -68
- package/dist/supervised-runtime.js +0 -3
- package/dist/workspace.js +1 -24
- package/package.json +9 -10
- package/dist/local-memory/index-budget.js +0 -74
- package/dist/local-memory/pending.js +0 -111
- package/dist/local-memory/runtime.js +0 -87
- package/dist/local-memory/types.js +0 -1
- package/dist/local-memory/write.js +0 -448
- package/dist/runtimes/codex-command-safety.js +0 -75
- package/dist/runtimes/codex-turn-retry.js +0 -69
- package/dist/storage-config.js +0 -175
- package/dist/storage-control.js +0 -193
- package/dist/storage-copy.js +0 -609
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-identity.js +0 -29
- package/dist/storage-layout.js +0 -143
- package/dist/storage-manifest.js +0 -855
- package/dist/storage-migration-controller.js +0 -417
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -339
- package/dist/storage-runtime-readiness.js +0 -184
- package/dist/storage-runtime.js +0 -39
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-serve-adapter.js +0 -65
- package/dist/storage-status-delivery.js +0 -209
- package/dist/storage-status-replay.js +0 -125
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -27
- package/dist/storage-worker-executor.js +0 -384
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -284
- 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,
|
|
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);
|
package/dist/profile-layout.js
CHANGED
|
@@ -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
|
|
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}
|
|
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 {
|
|
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(
|
|
41
|
+
export function codexAppServerArgs(projectRootMarkers) {
|
|
46
42
|
return [
|
|
47
|
-
...(
|
|
43
|
+
...(projectRootMarkers === undefined
|
|
48
44
|
? []
|
|
49
|
-
: ["-c", `project_root_markers=${JSON.stringify(
|
|
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
|
|
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
|
|
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
|
-
|
|
624
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
throw codexProjectWorkspaceUnsupported();
|
|
59
59
|
const match = /(?:^|[/\s])(\d+\.\d+\.\d+)(?=$|[\s(])/u.exec(parsed.data.userAgent);
|
|
60
|
-
|
|
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. */
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -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
|
}
|