@nowcrew/daemon 0.6.72 → 0.6.74
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/external-output.js +34 -4
- package/dist/list-models.js +3 -0
- package/dist/local-executor.js +14 -34
- package/dist/memory-prune-diagnostics.js +33 -0
- package/dist/runtimes/claude-models.js +123 -0
- package/dist/runtimes/codex-app-server-runner.js +33 -6
- package/dist/storage-manifest.js +9 -0
- package/package.json +1 -1
package/dist/external-output.js
CHANGED
|
@@ -8,9 +8,19 @@ function retainedMarkerPrefix(value, marker) {
|
|
|
8
8
|
}
|
|
9
9
|
return 0;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* 跨 chunk 的契约标签流式解码器:push() 逐段返回标签内文本(供 answerStream 实时转发);
|
|
13
|
+
* 同时记住整轮里**最后一个完整闭合**的标签块(`lastClosedBlock`),供完结时作为契约答案——
|
|
14
|
+
* 只有闭合了的块才算数:半截块(被工具调用拆开的前半段)与叙述里提到的开标签
|
|
15
|
+
* ("我会把结论包在 <marker> 里,先去跑测试")都不会被当成答案(review of a027bf15, C1)。
|
|
16
|
+
* 所有 runtime 的文本都经此解码器(local-executor 事件循环),因此"后到者胜"在
|
|
17
|
+
* 覆盖式与增量式 runtime 上语义一致。
|
|
18
|
+
*/
|
|
11
19
|
export class ExternalAnswerDecoder {
|
|
12
20
|
state = "outside";
|
|
13
21
|
pending = "";
|
|
22
|
+
block = "";
|
|
23
|
+
closed = null;
|
|
14
24
|
push(text) {
|
|
15
25
|
if (!text)
|
|
16
26
|
return [];
|
|
@@ -21,16 +31,32 @@ export class ExternalAnswerDecoder {
|
|
|
21
31
|
finish() {
|
|
22
32
|
this.pending = "";
|
|
23
33
|
this.state = "outside";
|
|
34
|
+
this.block = "";
|
|
24
35
|
return [];
|
|
25
36
|
}
|
|
37
|
+
/** 整轮里最后一个完整闭合的标签块(已 trim);没有闭合过任何块则为 null。 */
|
|
38
|
+
get lastClosedBlock() {
|
|
39
|
+
return this.closed;
|
|
40
|
+
}
|
|
26
41
|
consume(value, output) {
|
|
27
42
|
this.pending = "";
|
|
28
43
|
const marker = this.state === "outside" ? EXTERNAL_ANSWER_OPEN : EXTERNAL_ANSWER_CLOSE;
|
|
29
44
|
const markerAt = value.indexOf(marker);
|
|
30
45
|
if (markerAt >= 0) {
|
|
31
|
-
if (this.state === "inside"
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
if (this.state === "inside") {
|
|
47
|
+
const head = value.slice(0, markerAt);
|
|
48
|
+
if (head)
|
|
49
|
+
output.push(head);
|
|
50
|
+
const completed = `${this.block}${head}`.trim();
|
|
51
|
+
if (completed)
|
|
52
|
+
this.closed = completed;
|
|
53
|
+
this.block = "";
|
|
54
|
+
this.state = "outside";
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.block = "";
|
|
58
|
+
this.state = "inside";
|
|
59
|
+
}
|
|
34
60
|
const remaining = value.slice(markerAt + marker.length);
|
|
35
61
|
if (remaining)
|
|
36
62
|
this.consume(remaining, output);
|
|
@@ -39,14 +65,18 @@ export class ExternalAnswerDecoder {
|
|
|
39
65
|
const retained = retainedMarkerPrefix(value, marker);
|
|
40
66
|
const safe = retained > 0 ? value.slice(0, -retained) : value;
|
|
41
67
|
this.pending = retained > 0 ? value.slice(-retained) : "";
|
|
42
|
-
if (this.state === "inside" && safe)
|
|
68
|
+
if (this.state === "inside" && safe) {
|
|
43
69
|
output.push(safe);
|
|
70
|
+
this.block += safe;
|
|
71
|
+
}
|
|
44
72
|
}
|
|
45
73
|
}
|
|
46
74
|
export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
47
75
|
if (runtime === "claude") {
|
|
48
76
|
const candidate = (event ?? {});
|
|
49
77
|
const delta = candidate.event?.delta;
|
|
78
|
+
if (candidate.parent_tool_use_id != null)
|
|
79
|
+
return [];
|
|
50
80
|
if (candidate.type !== "stream_event"
|
|
51
81
|
|| candidate.event?.type !== "content_block_delta"
|
|
52
82
|
|| delta?.type !== "text_delta"
|
package/dist/list-models.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { isWin } from "./platform.js";
|
|
4
|
+
import { listClaudeModels } from "./runtimes/claude-models.js";
|
|
4
5
|
import { listHermesModels } from "./runtimes/hermes-models.js";
|
|
5
6
|
const execFileRaw = promisify(execFile);
|
|
6
7
|
const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
@@ -14,6 +15,8 @@ const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
|
14
15
|
});
|
|
15
16
|
export async function listRuntimeModels(runtime, options = {}) {
|
|
16
17
|
switch (runtime) {
|
|
18
|
+
case "claude":
|
|
19
|
+
return listClaudeModels();
|
|
17
20
|
case "codex":
|
|
18
21
|
return parseCodexModels((await execFileP("codex", ["debug", "models"])).stdout);
|
|
19
22
|
case "cursor":
|
package/dist/local-executor.js
CHANGED
|
@@ -27,42 +27,11 @@ import { ProjectProjectionError, } from "./project-skills/reconciler.js";
|
|
|
27
27
|
import { formatProjectSkillRuntimeWarning } from "./project-skills/runtime-warning.js";
|
|
28
28
|
import { projectSkillRuntimeDirectories, redactProjectSkillRuntimeRootText, redactProjectSkillRuntimeRootValue, } from "./project-skills/runtime-launch.js";
|
|
29
29
|
import { logIdentityRosterRuntimePrepared } from "./identity-roster-telemetry.js";
|
|
30
|
-
import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
30
|
+
import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, logMemoryPruneDiagnosticsFailure, memoryPruneSnapshotFields, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
31
31
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
32
32
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
33
33
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
34
34
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
|
|
35
|
-
function memoryPruneFileFactFields(fileFacts) {
|
|
36
|
-
const fields = {};
|
|
37
|
-
for (const [label, fact] of Object.entries(fileFacts)) {
|
|
38
|
-
fields[`${label}_exists`] = fact.exists;
|
|
39
|
-
fields[`${label}_size`] = fact.size;
|
|
40
|
-
fields[`${label}_mtime_ms`] = fact.mtime_ms;
|
|
41
|
-
fields[`${label}_sha256`] = fact.sha256;
|
|
42
|
-
fields[`${label}_hash_skipped_reason`] = fact.hash_skipped_reason;
|
|
43
|
-
fields[`${label}_error`] = fact.error;
|
|
44
|
-
fields[`${label}_markdown_heading_count`] = fact.markdown_heading_count;
|
|
45
|
-
fields[`${label}_lessons_reference_count`] = fact.lessons_reference_count;
|
|
46
|
-
fields[`${label}_same_as_shared_path`] = fact.same_as_shared_path;
|
|
47
|
-
}
|
|
48
|
-
return fields;
|
|
49
|
-
}
|
|
50
|
-
function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
|
|
51
|
-
return memoryPruneFileFactFields(fileFacts);
|
|
52
|
-
}
|
|
53
|
-
function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
54
|
-
const errorCode = error?.code;
|
|
55
|
-
dslog("memory_prune.diagnostics_failed", "长期记忆收尾诊断失败", {
|
|
56
|
-
level: "WARN",
|
|
57
|
-
execution_id: input.executionId,
|
|
58
|
-
agent_handle: input.handle,
|
|
59
|
-
task_key: input.taskKey,
|
|
60
|
-
prune_trace_id: traceId,
|
|
61
|
-
diagnostics_phase: phase,
|
|
62
|
-
error_name: error instanceof Error ? error.name : "unknown",
|
|
63
|
-
error_code: errorCode,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
35
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
67
36
|
constructor(runtime, timeoutMs) {
|
|
68
37
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -732,6 +701,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
732
701
|
let observedModel = currentModel;
|
|
733
702
|
let finalText = null;
|
|
734
703
|
let sentViaCrew = false;
|
|
704
|
+
// 契约答案解码器:除了给 answerStream 实时转发,还记住整轮最后一个**闭合**的标签块。
|
|
705
|
+
// finalText 只保留"最后一条"助手消息,模型打完 <nowwork_external_answer> 标签后若又干了
|
|
706
|
+
// 别的(如顺手 git commit),收尾消息会覆盖 finalText,只从 finalText 提取就把已履约的
|
|
707
|
+
// 结论整段丢掉——server 回落 finalText 后频道里只剩无标签的尾声
|
|
708
|
+
// (incident: 2026-09-02, boss-alarm 排查结论未投递)。所有 runtime 的文本都喂给它。
|
|
735
709
|
const externalOutput = new ExternalAnswerDecoder();
|
|
736
710
|
// 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
|
|
737
711
|
const consoleFormatter = createConsoleFormatter();
|
|
@@ -1011,8 +985,14 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1011
985
|
finalText: input.captureFinal && finalText !== null
|
|
1012
986
|
? stripExternalAnswerMarkers(finalText)
|
|
1013
987
|
: null,
|
|
1014
|
-
|
|
1015
|
-
|
|
988
|
+
// 契约答案 = 整轮最后一个闭合的标签块(六个 runtime 语义一致的"后到者胜",半截块与
|
|
989
|
+
// 叙述里的开标签不算数);从未闭合过时才回落到对 finalText 的宽容提取,保住"收尾被
|
|
990
|
+
// 截断"这一老兜底。解码器存的是原文,这里补脱敏。注意这放宽了"externalAnswer 非空
|
|
991
|
+
// ⇒ finalText 非空"的旧不变式;是否上报仍由 execution-runner 按 exitCode===0 把关。
|
|
992
|
+
externalAnswer: input.captureFinal
|
|
993
|
+
? (externalOutput.lastClosedBlock !== null
|
|
994
|
+
? redactProjectSkillRuntimeRootText(externalOutput.lastClosedBlock, activeProjectSkillRuntimeRoot)
|
|
995
|
+
: finalText !== null ? extractExternalAnswer(finalText) : null)
|
|
1016
996
|
: null,
|
|
1017
997
|
sentViaCrew,
|
|
1018
998
|
};
|
|
@@ -3,6 +3,7 @@ import { createReadStream } from "node:fs";
|
|
|
3
3
|
import { lstat, readdir } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
import { dslog } from "./slog.js";
|
|
6
7
|
const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
|
|
7
8
|
export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
8
9
|
export const MEMORY_PRUNE_NOTES_MANIFEST_LIMIT = 128;
|
|
@@ -391,3 +392,35 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
|
391
392
|
outcome = "no_files_changed";
|
|
392
393
|
return { outcome, ...facts };
|
|
393
394
|
}
|
|
395
|
+
// —— 收尾诊断的 SLS 字段拍平与失败日志(自 local-executor 迁入,签名用结构子集避免类型回环) ——
|
|
396
|
+
function memoryPruneFileFactFields(fileFacts) {
|
|
397
|
+
const fields = {};
|
|
398
|
+
for (const [label, fact] of Object.entries(fileFacts)) {
|
|
399
|
+
fields[`${label}_exists`] = fact.exists;
|
|
400
|
+
fields[`${label}_size`] = fact.size;
|
|
401
|
+
fields[`${label}_mtime_ms`] = fact.mtime_ms;
|
|
402
|
+
fields[`${label}_sha256`] = fact.sha256;
|
|
403
|
+
fields[`${label}_hash_skipped_reason`] = fact.hash_skipped_reason;
|
|
404
|
+
fields[`${label}_error`] = fact.error;
|
|
405
|
+
fields[`${label}_markdown_heading_count`] = fact.markdown_heading_count;
|
|
406
|
+
fields[`${label}_lessons_reference_count`] = fact.lessons_reference_count;
|
|
407
|
+
fields[`${label}_same_as_shared_path`] = fact.same_as_shared_path;
|
|
408
|
+
}
|
|
409
|
+
return fields;
|
|
410
|
+
}
|
|
411
|
+
export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
|
|
412
|
+
return memoryPruneFileFactFields(fileFacts);
|
|
413
|
+
}
|
|
414
|
+
export function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
415
|
+
const errorCode = error?.code;
|
|
416
|
+
dslog("memory_prune.diagnostics_failed", "长期记忆收尾诊断失败", {
|
|
417
|
+
level: "WARN",
|
|
418
|
+
execution_id: input.executionId,
|
|
419
|
+
agent_handle: input.handle,
|
|
420
|
+
task_key: input.taskKey,
|
|
421
|
+
prune_trace_id: traceId,
|
|
422
|
+
diagnostics_phase: phase,
|
|
423
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
424
|
+
error_code: errorCode,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude 模型清单探测:上报 CLI 族别名(fable/opus/sonnet/haiku,恒指本机已装版本的最新
|
|
3
|
+
* 对应模型),并把每个别名解析成具体模型 id 放进展示名——用户反馈"只写 latest 根本不知道
|
|
4
|
+
* 实际是什么模型"。
|
|
5
|
+
*
|
|
6
|
+
* 解析方式:claude 没有模型枚举命令,但 `--print` 会话启动时的 `system/init` 事件明文携带
|
|
7
|
+
* 解析后的 `model`(2.1.258 实测 opus→claude-opus-5、fable→claude-fable-5-1)。探测把
|
|
8
|
+
* ANTHROPIC_BASE_URL 指向本机不可达端口(零 API 调用、零 token),`--setting-sources=`
|
|
9
|
+
* 关掉用户 hooks/项目配置,读到 init 行即 SIGKILL。结果按 CLI 版本缓存:只有全部别名都
|
|
10
|
+
* 解析成功才缓存,失败的下次请求重试。
|
|
11
|
+
*/
|
|
12
|
+
import { execFile, spawn } from "node:child_process";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { isWin } from "../platform.js";
|
|
17
|
+
const execFileP = promisify(execFile);
|
|
18
|
+
const PROBE_TIMEOUT_MS = 8_000;
|
|
19
|
+
// 本机丢弃端口:连不上也无所谓,init 事件在任何网络请求之前就已输出。
|
|
20
|
+
const UNROUTABLE_API_BASE = "http://127.0.0.1:9";
|
|
21
|
+
const CLAUDE_ALIASES = [
|
|
22
|
+
["fable", "Fable"],
|
|
23
|
+
["sonnet", "Sonnet"],
|
|
24
|
+
["opus", "Opus"],
|
|
25
|
+
["haiku", "Haiku"],
|
|
26
|
+
];
|
|
27
|
+
const DEFAULT_ALIAS = "sonnet";
|
|
28
|
+
const cacheByVersion = new Map();
|
|
29
|
+
/** "2.1.258 (Claude Code)" → "2.1.258";空/异常输出 → ""。 */
|
|
30
|
+
export function normalizeClaudeVersion(raw) {
|
|
31
|
+
return raw.replace(/\s*\(Claude Code\)\s*$/, "").trim();
|
|
32
|
+
}
|
|
33
|
+
export async function listClaudeModels(deps = defaultDeps) {
|
|
34
|
+
const version = normalizeClaudeVersion(await deps.probeVersion());
|
|
35
|
+
const cached = version ? cacheByVersion.get(version) : undefined;
|
|
36
|
+
if (cached)
|
|
37
|
+
return cached;
|
|
38
|
+
const resolved = await Promise.all(CLAUDE_ALIASES.map(async ([alias]) => {
|
|
39
|
+
try {
|
|
40
|
+
return await deps.resolveAlias(alias);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
const suffix = version ? ` · Claude Code ${version}` : "";
|
|
47
|
+
const models = CLAUDE_ALIASES.map(([alias, name], index) => {
|
|
48
|
+
const concrete = resolved[index];
|
|
49
|
+
return {
|
|
50
|
+
id: alias,
|
|
51
|
+
label: `${name} (latest${concrete ? `: ${concrete}` : ""})${suffix}`,
|
|
52
|
+
...(alias === DEFAULT_ALIAS ? { default: true } : {}),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
if (version && resolved.every((model) => model !== null))
|
|
56
|
+
cacheByVersion.set(version, models);
|
|
57
|
+
return models;
|
|
58
|
+
}
|
|
59
|
+
/** 仅测试用:清空版本缓存。 */
|
|
60
|
+
export function resetClaudeModelCache() {
|
|
61
|
+
cacheByVersion.clear();
|
|
62
|
+
}
|
|
63
|
+
async function probeVersion() {
|
|
64
|
+
const { stdout } = await execFileP("claude", ["--version"], {
|
|
65
|
+
shell: isWin(),
|
|
66
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
67
|
+
killSignal: "SIGKILL",
|
|
68
|
+
});
|
|
69
|
+
return stdout;
|
|
70
|
+
}
|
|
71
|
+
function resolveAlias(alias) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const child = spawn("claude", [
|
|
74
|
+
"--model", alias,
|
|
75
|
+
"--print", "--output-format", "stream-json", "--verbose",
|
|
76
|
+
"--setting-sources=",
|
|
77
|
+
"ping",
|
|
78
|
+
], {
|
|
79
|
+
cwd: tmpdir(),
|
|
80
|
+
env: {
|
|
81
|
+
...process.env,
|
|
82
|
+
ANTHROPIC_BASE_URL: UNROUTABLE_API_BASE,
|
|
83
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
84
|
+
},
|
|
85
|
+
shell: isWin(),
|
|
86
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
87
|
+
});
|
|
88
|
+
let settled = false;
|
|
89
|
+
const finish = (value) => {
|
|
90
|
+
if (settled)
|
|
91
|
+
return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
try {
|
|
95
|
+
child.kill("SIGKILL");
|
|
96
|
+
}
|
|
97
|
+
catch { /* 已退出 */ }
|
|
98
|
+
resolve(value);
|
|
99
|
+
};
|
|
100
|
+
const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
|
|
101
|
+
child.on("error", () => finish(null));
|
|
102
|
+
child.on("exit", () => finish(null));
|
|
103
|
+
const lines = createInterface({ input: child.stdout });
|
|
104
|
+
lines.on("line", (line) => {
|
|
105
|
+
const model = initModelOf(line);
|
|
106
|
+
if (model !== null)
|
|
107
|
+
finish(model);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** stream-json 一行 → system/init 事件里的 model;其他行返回 null。 */
|
|
112
|
+
export function initModelOf(line) {
|
|
113
|
+
try {
|
|
114
|
+
const event = JSON.parse(line);
|
|
115
|
+
if (event.type !== "system" || event.subtype !== "init")
|
|
116
|
+
return null;
|
|
117
|
+
return typeof event.model === "string" && event.model.trim() ? event.model.trim() : null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const defaultDeps = { probeVersion, resolveAlias };
|
|
@@ -31,6 +31,7 @@ const ERROR_MESSAGE_CAP = 2_000;
|
|
|
31
31
|
const STDERR_TAIL_CAP = 1_200;
|
|
32
32
|
const STDERR_LINE_CAPTURE_CAP = 1_200;
|
|
33
33
|
const STDERR_LINE_OMITTED = "[stderr line omitted: exceeded capture limit]\n";
|
|
34
|
+
const API_RETRY_DETAIL_CAP = 240;
|
|
34
35
|
const MAX_INITIALIZE_ATTEMPTS = 2;
|
|
35
36
|
// 模型网关瞬态故障(过载/限流)导致 turn 失败时,整轮重试(15s→45s 递进退避):codex 自身
|
|
36
37
|
// 的重试窗口只有 ~10-30s,网关过载往往持续数分钟,这里再兜一层,否则 agent 直接失败
|
|
@@ -188,6 +189,21 @@ function logStage(stage, status, attempt, startedAt, detail) {
|
|
|
188
189
|
process.stderr.write(`[codex-app-server] stage=${stage} status=${status} attempt=${attempt}`
|
|
189
190
|
+ ` elapsed_ms=${Date.now() - startedAt}${suffix}\n`);
|
|
190
191
|
}
|
|
192
|
+
function logApiRetry(turnAttempt, apiRetryAttempt, detail, secrets) {
|
|
193
|
+
const safeDetail = detail === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: safeErrorMessage(detail, secrets).slice(0, API_RETRY_DETAIL_CAP);
|
|
196
|
+
process.stderr.write(`[codex-app-server] stage=api_retry status=retry attempt=${apiRetryAttempt}`
|
|
197
|
+
+ ` turn_attempt=${turnAttempt}`
|
|
198
|
+
+ (safeDetail === undefined ? "" : ` error=${JSON.stringify(safeDetail)}`)
|
|
199
|
+
+ "\n");
|
|
200
|
+
}
|
|
201
|
+
function logApiRetrySummary(turnAttempt, apiRetryCount) {
|
|
202
|
+
if (apiRetryCount === 0)
|
|
203
|
+
return;
|
|
204
|
+
process.stderr.write(`[codex-app-server] stage=api_retry status=summary turn_attempt=${turnAttempt}`
|
|
205
|
+
+ ` api_retry_count=${apiRetryCount}\n`);
|
|
206
|
+
}
|
|
191
207
|
function jsonLine(event) {
|
|
192
208
|
if (process.stdout.write(`${JSON.stringify(event)}\n`))
|
|
193
209
|
return Promise.resolve();
|
|
@@ -445,6 +461,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
445
461
|
let threadId = null;
|
|
446
462
|
let announcedThreadId = null;
|
|
447
463
|
let turnId = null;
|
|
464
|
+
let apiRetryCount = 0;
|
|
448
465
|
let turnHasUnsafeSideEffect = false;
|
|
449
466
|
const startedItemIds = new Set();
|
|
450
467
|
let lastUsage;
|
|
@@ -489,10 +506,17 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
489
506
|
}
|
|
490
507
|
if (method === "turn/completed")
|
|
491
508
|
completionResolve(params);
|
|
492
|
-
if (method === "error"
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
509
|
+
if (method === "error") {
|
|
510
|
+
const notification = (params !== null && typeof params === "object" ? params : {});
|
|
511
|
+
const detail = notification.error?.message;
|
|
512
|
+
if (notification.willRetry === true) {
|
|
513
|
+
apiRetryCount += 1;
|
|
514
|
+
logApiRetry(attempt, apiRetryCount, detail, secrets);
|
|
515
|
+
}
|
|
516
|
+
else if (notification.willRetry === false && detail) {
|
|
517
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}`
|
|
518
|
+
+ ` turn_attempt=${attempt} api_retry_count=${apiRetryCount}\n`, secrets));
|
|
519
|
+
}
|
|
496
520
|
}
|
|
497
521
|
}, completionReject, secrets);
|
|
498
522
|
let treeStopPromise = null;
|
|
@@ -608,10 +632,12 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
608
632
|
},
|
|
609
633
|
}),
|
|
610
634
|
});
|
|
635
|
+
logApiRetrySummary(attempt, apiRetryCount);
|
|
611
636
|
if (completed.turn?.status === "completed")
|
|
612
637
|
return { code: 0, initializeTimedOut: false, unsafeToRetry: false };
|
|
613
638
|
const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
|
|
614
|
-
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}
|
|
639
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}`
|
|
640
|
+
+ ` turn_attempt=${attempt} api_retry_count=${apiRetryCount}\n`, secrets));
|
|
615
641
|
const status = completed.turn?.status;
|
|
616
642
|
const retryReason = cancelling ? undefined
|
|
617
643
|
: status === "interrupted" ? "collab_interrupted"
|
|
@@ -633,7 +659,8 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
633
659
|
else {
|
|
634
660
|
logStage(activeStage, "error", attempt, stageStartedAt);
|
|
635
661
|
}
|
|
636
|
-
process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, secrets)}
|
|
662
|
+
process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, secrets)}`
|
|
663
|
+
+ ` turn_attempt=${attempt} api_retry_count=${apiRetryCount}\n`);
|
|
637
664
|
return { code: 1, initializeTimedOut, unsafeToRetry: false };
|
|
638
665
|
}
|
|
639
666
|
finally {
|
package/dist/storage-manifest.js
CHANGED
|
@@ -324,6 +324,15 @@ async function* scanSourceEntries(input) {
|
|
|
324
324
|
type = "file";
|
|
325
325
|
else if (stat.isSymbolicLink())
|
|
326
326
|
type = "symlink";
|
|
327
|
+
// Host-local runtime and credential trees are intentionally excluded from
|
|
328
|
+
// the migration. Do not descend into them: their contents are volatile,
|
|
329
|
+
// never copied, and would otherwise make the source digest drift between
|
|
330
|
+
// preflight and apply.
|
|
331
|
+
if (stat.isDirectory()
|
|
332
|
+
&& transformation === undefined
|
|
333
|
+
&& (classifiedReason === "local_runtime" || classifiedReason === "credential_material")) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
327
336
|
if (stat.isDirectory())
|
|
328
337
|
pending.push(path);
|
|
329
338
|
if (exclusion !== undefined) {
|