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