@nowcrew/daemon 0.6.88-beta.20 → 0.6.88-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/computer-profile.js +9 -4
- package/dist/config.js +0 -9
- package/dist/execution-runner.js +0 -1
- package/dist/local-executor.js +1 -8
- package/dist/local-memory/read-cli.js +48 -14
- package/dist/machine-info.js +0 -1
- package/dist/provider-env.js +0 -2
- package/dist/runner.js +0 -1
- package/dist/serve.js +1 -13
- package/package.json +1 -2
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
package/README.md
CHANGED
|
@@ -362,9 +362,11 @@ changing the task cwd. Claude receives
|
|
|
362
362
|
so a machine policy that disables project settings also disables this source even when the projection is
|
|
363
363
|
healthy.
|
|
364
364
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
Ordinary Codex always uses the shared user Home: the inherited `CODEX_HOME`, or `~/.codex` when
|
|
366
|
+
unset. There is no Computer-level Home mode or private-Home migration command. Existing user-level
|
|
367
|
+
discovery is unchanged: Claude may still load `~/.claude/skills`, and Codex may load `$CODEX_HOME/skills`
|
|
368
|
+
and `~/.agents/skills`. The DeepSeek Codex variant keeps its Agent-private `.codex-deepseek` Home for
|
|
369
|
+
provider credentials. Project bindings do not hide or rewrite any of those sources.
|
|
368
370
|
|
|
369
371
|
Binding changes reconcile immediately when the Computer is online. On reconnect the server sends the
|
|
370
372
|
complete binding set for every Agent assigned to that Computer, including empty sets that clear stale
|
package/dist/computer-profile.js
CHANGED
|
@@ -25,7 +25,6 @@ const ProfileFields = z.object({
|
|
|
25
25
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
26
26
|
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
27
27
|
registryUrl: z.string().url().optional(),
|
|
28
|
-
codexHomeMode: z.enum(["shared", "private"]).optional(),
|
|
29
28
|
}).strict();
|
|
30
29
|
function validateProfileRegistry(value, context) {
|
|
31
30
|
const fields = value;
|
|
@@ -60,7 +59,13 @@ const StoredProtectedProfileSchema = ProfileFields.omit({ machineToken: true }).
|
|
|
60
59
|
ciphertext: z.string().min(1).max(16384),
|
|
61
60
|
}).strict(),
|
|
62
61
|
}).strict().superRefine(validateProfileRegistry);
|
|
63
|
-
|
|
62
|
+
// 旧版本可能已保存 shared 字段;读取时丢弃退休设置,其余字段仍严格校验。
|
|
63
|
+
const StoredProfileSchema = z.preprocess((raw) => {
|
|
64
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
65
|
+
return raw;
|
|
66
|
+
const { codexHomeMode: _retiredMode, ...fields } = raw;
|
|
67
|
+
return fields;
|
|
68
|
+
}, z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]));
|
|
64
69
|
export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
|
|
65
70
|
export class ProfileAgentsRootConflictError extends Error {
|
|
66
71
|
profile;
|
|
@@ -257,7 +262,6 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
|
257
262
|
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
258
263
|
...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
|
|
259
264
|
...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
|
|
260
|
-
...(profile.codexHomeMode === undefined ? {} : { codexHomeMode: profile.codexHomeMode }),
|
|
261
265
|
machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
|
|
262
266
|
});
|
|
263
267
|
await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
|
|
@@ -404,5 +408,6 @@ export function applyProfileToEnv(profile, env) {
|
|
|
404
408
|
env.CREW_NPM_REGISTRY = profile.registryUrl;
|
|
405
409
|
else
|
|
406
410
|
delete env.CREW_NPM_REGISTRY;
|
|
407
|
-
|
|
411
|
+
// 不再向运行时传播退休的模式开关;CODEX_HOME 沿用用户环境。
|
|
412
|
+
delete env.CREW_CODEX_HOME_MODE;
|
|
408
413
|
}
|
package/dist/config.js
CHANGED
|
@@ -111,14 +111,6 @@ function localMemoryV2Handles(env, serverUrl) {
|
|
|
111
111
|
return new Set(handles);
|
|
112
112
|
}
|
|
113
113
|
export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
|
|
114
|
-
function codexHomeModeEnv(env) {
|
|
115
|
-
const value = env.CREW_CODEX_HOME_MODE;
|
|
116
|
-
if (value === undefined || value === "shared")
|
|
117
|
-
return "shared";
|
|
118
|
-
if (value === "private")
|
|
119
|
-
return "private";
|
|
120
|
-
throw new ConfigError("CREW_CODEX_HOME_MODE must be shared or private");
|
|
121
|
-
}
|
|
122
114
|
function codexCollaborationToolsEnv(env) {
|
|
123
115
|
const value = (env.CREW_CODEX_COLLABORATION_TOOLS ?? "").trim().toLowerCase();
|
|
124
116
|
if (value === "" || value === "0" || value === "false" || value === "off")
|
|
@@ -168,7 +160,6 @@ export function loadConfig(env = process.env) {
|
|
|
168
160
|
: defaultQaReportRoot,
|
|
169
161
|
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
170
162
|
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
171
|
-
codexHomeMode: codexHomeModeEnv(env),
|
|
172
163
|
codexCollaborationTools: codexCollaborationToolsEnv(env),
|
|
173
164
|
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
|
174
165
|
resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
|
package/dist/execution-runner.js
CHANGED
|
@@ -878,7 +878,6 @@ export async function runExecution(config, input, dependencies) {
|
|
|
878
878
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
879
879
|
cliPath: config.cliPath,
|
|
880
880
|
providerConfig,
|
|
881
|
-
codexHomeMode: config.codexHomeMode,
|
|
882
881
|
...(providerConfig.description ? { description: providerConfig.description } : {}),
|
|
883
882
|
...(spec.reporting.allowBoundImDecision ? {
|
|
884
883
|
systemEnv: {
|
package/dist/local-executor.js
CHANGED
|
@@ -7,8 +7,6 @@ import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
|
|
|
7
7
|
import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
|
|
8
8
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
9
9
|
import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
10
|
-
import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
|
|
11
|
-
import { materializePrivateCodexHome } from "./codex-home-launch.js";
|
|
12
10
|
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
13
11
|
import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
|
|
14
12
|
import { augmentedPath } from "./runtime-path.js";
|
|
@@ -262,8 +260,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
262
260
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
263
261
|
delete inheritedEnv[key];
|
|
264
262
|
}
|
|
265
|
-
const sourceCodexHome = inheritedEnv.CODEX_HOME
|
|
266
|
-
?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
|
|
267
263
|
let materializedDeepSeekHome;
|
|
268
264
|
try {
|
|
269
265
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
@@ -344,7 +340,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
344
340
|
const launchSessionId = resumeSessionId ?? (rotated
|
|
345
341
|
? await rotateAgentSession(workspace.sessionDir)
|
|
346
342
|
: workspace.agentSessionId);
|
|
347
|
-
await materializePrivateCodexHome({ runtime: runtime.name, homeDir: workspace.homeDir, ...(input.launch.codexHomeMode === undefined ? {} : { mode: input.launch.codexHomeMode }), ...(sourceCodexHome === undefined ? {} : { sourceHome: sourceCodexHome }), ...(launchSessionId === undefined ? {} : { sessionId: launchSessionId }), ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }), ...(dependencies.materializeDefaultCodexHome === undefined ? {} : { materialize: dependencies.materializeDefaultCodexHome }) });
|
|
348
343
|
let promptContext = {
|
|
349
344
|
workspace: executionWorkspace,
|
|
350
345
|
resuming,
|
|
@@ -512,9 +507,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
512
507
|
? { KIMI_MODEL_THINKING_EFFORT: runtime.reasoning }
|
|
513
508
|
: {}),
|
|
514
509
|
};
|
|
515
|
-
const providerEnv = applyProviderEnv(baseEnv, runtime.name,
|
|
516
|
-
? providerConfig
|
|
517
|
-
: { ...providerConfig, codexHomeMode: input.launch.codexHomeMode }, workspace.homeDir, materializedDeepSeekHome);
|
|
510
|
+
const providerEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir, materializedDeepSeekHome);
|
|
518
511
|
const childEnv = runtime.name === "deepseek-harness"
|
|
519
512
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
520
513
|
: providerEnv;
|
|
@@ -8,6 +8,10 @@ import { z } from "zod";
|
|
|
8
8
|
import { readPendingMemoryBody, searchPendingMemoryPage } from "./pending-query.js";
|
|
9
9
|
const OUTPUT_BYTES = 12 * 1024;
|
|
10
10
|
const FILE_BYTES = 32 * 1024 * 1024;
|
|
11
|
+
const READ_LIMIT_MAX = 4_000;
|
|
12
|
+
const READ_LIMIT_SAFE = 2_000;
|
|
13
|
+
const SEARCH_LIMIT_MAX = 40;
|
|
14
|
+
const SEARCH_LIMIT_SAFE = 10;
|
|
11
15
|
const FileSchema = z.string().min(1).refine((file) => {
|
|
12
16
|
if (isAbsolute(file) || /[\\\u0000-\u001f\u007f]/u.test(file))
|
|
13
17
|
return false;
|
|
@@ -26,26 +30,26 @@ const CommandSchema = z.discriminatedUnion("command", [
|
|
|
26
30
|
z.object({
|
|
27
31
|
command: z.literal("search"), ...Common,
|
|
28
32
|
query: z.string().trim().min(1).max(256),
|
|
29
|
-
limit: z.coerce.number().int().min(1).max(
|
|
33
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
30
34
|
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
31
35
|
}).strict(),
|
|
32
36
|
z.object({
|
|
33
37
|
command: z.literal("read"), ...Common,
|
|
34
38
|
offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
35
|
-
limit: z.coerce.number().int().min(1).max(
|
|
39
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(2000),
|
|
36
40
|
}).strict(),
|
|
37
41
|
z.object({
|
|
38
42
|
command: z.literal("search-pending"),
|
|
39
43
|
query: z.string().trim().min(1).max(256),
|
|
40
44
|
cursor: z.string().min(1).max(4096).optional(),
|
|
41
|
-
limit: z.coerce.number().int().min(1).max(
|
|
45
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
42
46
|
}).strict(),
|
|
43
47
|
z.object({
|
|
44
48
|
command: z.literal("read-pending"),
|
|
45
49
|
id: z.string().min(1).max(250).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
46
50
|
ifHash: Common.ifHash,
|
|
47
51
|
offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
48
|
-
limit: z.coerce.number().int().min(1).max(
|
|
52
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(2000),
|
|
49
53
|
}).strict(),
|
|
50
54
|
]);
|
|
51
55
|
const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
|
|
@@ -53,10 +57,10 @@ crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--lim
|
|
|
53
57
|
crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
|
|
54
58
|
crew-memory search-pending --query <关键词> [--cursor <游标>] [--limit 10]
|
|
55
59
|
crew-memory read-pending --id <候选ID> [--offset 0] [--limit 2000] [--if-hash <sha256>]
|
|
56
|
-
read/read-pending
|
|
60
|
+
read/read-pending 的有效页长为 1—4000,默认 2000;search/search-pending 的有效页长为 1—40,默认 10。超出上限时工具自动使用安全页长并返回 warning(recovered=true),不把它当作读取失败。
|
|
57
61
|
search 关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
|
|
58
62
|
读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
|
|
59
|
-
读取的 --limit
|
|
63
|
+
读取的 --limit 超出范围时,工具会自动降到 2000 并在结果中标记 warning;保留原文件或候选 ID 及 offset,按 nextOffset 分页,不要一次读完整文件。
|
|
60
64
|
后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
|
|
61
65
|
search/read 读取 MEMORY.md 和 notes 原文,不过滤失效段落,也不扫描 pending。
|
|
62
66
|
search-pending 只查因容量延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
|
|
@@ -131,7 +135,15 @@ function originalLowercaseIndex(text, index) {
|
|
|
131
135
|
}
|
|
132
136
|
return text.length;
|
|
133
137
|
}
|
|
134
|
-
function
|
|
138
|
+
function normalizeLimit(requestedLimit, max, safe) {
|
|
139
|
+
if (requestedLimit <= max)
|
|
140
|
+
return { limit: requestedLimit };
|
|
141
|
+
return {
|
|
142
|
+
limit: safe,
|
|
143
|
+
warning: { code: "memory_limit_clamped", requestedLimit, appliedLimit: safe, recovered: true },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function searchSource(text, sha256, command, warning) {
|
|
135
147
|
const terms = command.query.toLowerCase().split(/\s+/u);
|
|
136
148
|
const matches = [];
|
|
137
149
|
let line = 0;
|
|
@@ -172,10 +184,22 @@ function searchSource(text, sha256, command) {
|
|
|
172
184
|
return {
|
|
173
185
|
file: command.file, sha256, matches, matchCount, hasMore,
|
|
174
186
|
nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null,
|
|
187
|
+
...(warning === undefined ? {} : { warning, requestedLimit: warning.requestedLimit, limit: warning.appliedLimit }),
|
|
175
188
|
};
|
|
176
189
|
}
|
|
177
|
-
function readPage(text, source, command) {
|
|
178
|
-
const metadata = {
|
|
190
|
+
function readPage(text, source, command, warning) {
|
|
191
|
+
const metadata = {
|
|
192
|
+
...source,
|
|
193
|
+
...(warning === undefined ? {} : {
|
|
194
|
+
warning,
|
|
195
|
+
requestedLimit: warning.requestedLimit,
|
|
196
|
+
limit: warning.appliedLimit,
|
|
197
|
+
}),
|
|
198
|
+
offset: command.offset,
|
|
199
|
+
nextOffset: Number.MAX_SAFE_INTEGER,
|
|
200
|
+
eof: false,
|
|
201
|
+
content: "",
|
|
202
|
+
};
|
|
179
203
|
let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify(metadata), "utf8");
|
|
180
204
|
let position = 0;
|
|
181
205
|
let offset = 0;
|
|
@@ -244,26 +268,36 @@ export async function runMemoryReadCommand(argv, io) {
|
|
|
244
268
|
if (!io.env.CREW_HOME)
|
|
245
269
|
throw new Error("memory_home_missing");
|
|
246
270
|
if (command.command === "search-pending") {
|
|
271
|
+
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
247
272
|
const result = await searchPendingMemoryPage({ agentDir: io.env.CREW_HOME, query: command.query,
|
|
248
|
-
...(command.cursor === undefined ? {} : { cursor: command.cursor }), limit:
|
|
249
|
-
writeResult(result,
|
|
273
|
+
...(command.cursor === undefined ? {} : { cursor: command.cursor }), limit: normalized.limit });
|
|
274
|
+
writeResult({ ...result, ...(normalized.warning === undefined ? {} : {
|
|
275
|
+
warning: normalized.warning, requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit,
|
|
276
|
+
}) }, io);
|
|
250
277
|
if (result.warnings.length > 0 || (!result.hasMore && !result.complete)) {
|
|
251
278
|
throw new Error("memory_pending_search_incomplete");
|
|
252
279
|
}
|
|
253
280
|
return 0;
|
|
254
281
|
}
|
|
255
282
|
if (command.command === "read-pending") {
|
|
283
|
+
const normalized = normalizeLimit(command.limit, READ_LIMIT_MAX, READ_LIMIT_SAFE);
|
|
256
284
|
const { content, ...metadata } = await readPendingMemoryBody({ agentDir: io.env.CREW_HOME,
|
|
257
285
|
pendingId: command.id, ...(command.ifHash === undefined ? {} : { ifHash: command.ifHash }) });
|
|
258
|
-
writeResult(readPage(content, metadata, command), io);
|
|
286
|
+
writeResult(readPage(content, metadata, { ...command, limit: normalized.limit }, normalized.warning), io);
|
|
259
287
|
return 0;
|
|
260
288
|
}
|
|
261
289
|
const source = await readSource(io.env.CREW_HOME, command.file);
|
|
262
290
|
if (command.ifHash !== undefined && command.ifHash !== source.sha256)
|
|
263
291
|
throw new Error("memory_file_changed");
|
|
264
292
|
const result = command.command === "search"
|
|
265
|
-
?
|
|
266
|
-
|
|
293
|
+
? (() => {
|
|
294
|
+
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
295
|
+
return searchSource(source.text, source.sha256, { ...command, limit: normalized.limit }, normalized.warning);
|
|
296
|
+
})()
|
|
297
|
+
: (() => {
|
|
298
|
+
const normalized = normalizeLimit(command.limit, READ_LIMIT_MAX, READ_LIMIT_SAFE);
|
|
299
|
+
return readPage(source.text, { file: command.file, sha256: source.sha256 }, { ...command, limit: normalized.limit }, normalized.warning);
|
|
300
|
+
})();
|
|
267
301
|
writeResult(result, io);
|
|
268
302
|
return 0;
|
|
269
303
|
}
|
package/dist/machine-info.js
CHANGED
|
@@ -241,7 +241,6 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
241
241
|
runtimes,
|
|
242
242
|
executionRuntimes,
|
|
243
243
|
capabilities,
|
|
244
|
-
...(dependencies.codexHomeMode === undefined ? {} : { codexHomeMode: dependencies.codexHomeMode }),
|
|
245
244
|
...(dependencies.profileName === undefined ? {} : {
|
|
246
245
|
layoutVersion: 1,
|
|
247
246
|
profileName: dependencies.profileName,
|
package/dist/provider-env.js
CHANGED
|
@@ -67,8 +67,6 @@ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverri
|
|
|
67
67
|
return env;
|
|
68
68
|
}
|
|
69
69
|
if (runtime === "codex") {
|
|
70
|
-
if (cfg.codexHomeMode === "private")
|
|
71
|
-
return { ...base, CODEX_HOME: join(homeDir, ".codex") };
|
|
72
70
|
return base;
|
|
73
71
|
}
|
|
74
72
|
if (runtime !== "claude" || cfg.provider !== "custom")
|
package/dist/runner.js
CHANGED
|
@@ -91,7 +91,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
91
91
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
92
92
|
cliPath: config.cliPath,
|
|
93
93
|
providerConfig,
|
|
94
|
-
codexHomeMode: config.codexHomeMode,
|
|
95
94
|
...(providerConfig.description ? { description: providerConfig.description } : {}),
|
|
96
95
|
systemEnv: {
|
|
97
96
|
...(providerConfig.fastMode ? { CREW_FAST_MODE: "1" } : {}),
|
package/dist/serve.js
CHANGED
|
@@ -52,7 +52,6 @@ import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
|
52
52
|
import { probeRuntimeHealth } from "./runtime-health.js";
|
|
53
53
|
import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
|
|
54
54
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
55
|
-
import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
|
|
56
55
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
57
56
|
import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
|
|
58
57
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
@@ -128,11 +127,6 @@ export function serve(config, opts = {}) {
|
|
|
128
127
|
},
|
|
129
128
|
});
|
|
130
129
|
const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
|
|
131
|
-
const codexHomeModeController = createCodexHomeModeController({ ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), scheduleRestart: opts.update?.scheduleRestart ?? scheduleServiceRestart, getServiceSpec: async () => { const eligibility = await updateEligibility(); return eligibility.eligible ? eligibility.serviceSpec : null; }, isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0, sendStatus: (frame) => { try {
|
|
132
|
-
if (ws?.readyState === WebSocket.OPEN)
|
|
133
|
-
ws.send(JSON.stringify(frame));
|
|
134
|
-
}
|
|
135
|
-
catch { /* reconnect */ } } });
|
|
136
130
|
const projectionCoordinator = createAgentProjectionCoordinator();
|
|
137
131
|
const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
|
|
138
132
|
const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
|
|
@@ -311,9 +305,8 @@ export function serve(config, opts = {}) {
|
|
|
311
305
|
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
312
306
|
// First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
|
|
313
307
|
detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
|
|
314
|
-
additionalCapabilities: async () =>
|
|
308
|
+
additionalCapabilities: async () => managedDaemonCapabilities(updateEligibility),
|
|
315
309
|
capabilities: capabilities.machineHello,
|
|
316
|
-
codexHomeMode: config.codexHomeMode,
|
|
317
310
|
});
|
|
318
311
|
runtimeFacts = helloPromise
|
|
319
312
|
.then(async (hello) => {
|
|
@@ -409,10 +402,6 @@ export function serve(config, opts = {}) {
|
|
|
409
402
|
});
|
|
410
403
|
return;
|
|
411
404
|
}
|
|
412
|
-
if (rawType === "daemon:codex-home-mode") {
|
|
413
|
-
void codexHomeModeController.handle(decoded).catch((error) => dslog("daemon.codex_home_mode_failed", "Codex Home 模式切换失败", { level: "ERROR", error_message: error.message }));
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
405
|
if (rawType.startsWith("execution:")) {
|
|
417
406
|
const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
|
|
418
407
|
if (!parsedExecution.success) {
|
|
@@ -1339,7 +1328,6 @@ export function serve(config, opts = {}) {
|
|
|
1339
1328
|
const pending = [
|
|
1340
1329
|
webSocketClosed,
|
|
1341
1330
|
updateController.drain(),
|
|
1342
|
-
codexHomeModeController.drain(),
|
|
1343
1331
|
executionFrameQueue,
|
|
1344
1332
|
...executionRuns.values(),
|
|
1345
1333
|
...[...legacyRuns.values()].map((run) => run.done),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.88-beta.
|
|
3
|
+
"version": "0.6.88-beta.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
|
|
44
43
|
"test": "vitest run",
|
|
45
44
|
"typecheck": "tsc --noEmit"
|
|
46
45
|
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { awaitWithCancellation } from "./runtime-cancellation.js";
|
|
2
|
-
import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
|
|
3
|
-
export async function materializePrivateCodexHome(input) {
|
|
4
|
-
if (input.runtime !== "codex" || input.mode !== "private")
|
|
5
|
-
return;
|
|
6
|
-
await awaitWithCancellation((input.materialize ?? materializeDefaultCodexHome)(input.homeDir, input.sourceHome, input.sessionId), input.cancellation);
|
|
7
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { daemonHome, loadProfile, saveProfile } from "./computer-profile.js";
|
|
3
|
-
const MessageSchema = z.object({
|
|
4
|
-
type: z.literal("daemon:codex-home-mode"),
|
|
5
|
-
operationId: z.string().uuid(),
|
|
6
|
-
mode: z.enum(["shared", "private"]),
|
|
7
|
-
}).strict();
|
|
8
|
-
export function createCodexHomeModeController(input) {
|
|
9
|
-
let running = null;
|
|
10
|
-
const handled = new Set();
|
|
11
|
-
const fail = (operationId, errorCode) => {
|
|
12
|
-
input.sendStatus({ type: "daemon:codex-home-mode-status", operationId, status: "failed", errorCode });
|
|
13
|
-
};
|
|
14
|
-
const execute = async (message) => {
|
|
15
|
-
if (input.profileName === undefined) {
|
|
16
|
-
fail(message.operationId, "profile_unavailable");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (input.isBusy()) {
|
|
20
|
-
fail(message.operationId, "busy");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
input.sendStatus({ type: "daemon:codex-home-mode-status", operationId: message.operationId, status: "saving" });
|
|
24
|
-
try {
|
|
25
|
-
const home = input.profileHome ?? daemonHome();
|
|
26
|
-
const profile = await loadProfile(input.profileName, home);
|
|
27
|
-
await saveProfile({ ...profile, codexHomeMode: message.mode }, home);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
fail(message.operationId, "save_failed");
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
input.sendStatus({ type: "daemon:codex-home-mode-status", operationId: message.operationId, status: "restarting" });
|
|
34
|
-
try {
|
|
35
|
-
const serviceSpec = await input.getServiceSpec();
|
|
36
|
-
if (serviceSpec === null) {
|
|
37
|
-
fail(message.operationId, "restart_failed");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const handoff = input.scheduleRestart(serviceSpec);
|
|
41
|
-
void handoff.failed.then(() => fail(message.operationId, "restart_failed"), () => fail(message.operationId, "restart_failed"));
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
fail(message.operationId, "restart_failed");
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
return {
|
|
48
|
-
handle: async (raw) => {
|
|
49
|
-
const parsed = MessageSchema.safeParse(raw);
|
|
50
|
-
if (!parsed.success)
|
|
51
|
-
return false;
|
|
52
|
-
if (handled.has(parsed.data.operationId))
|
|
53
|
-
return true;
|
|
54
|
-
if (running !== null) {
|
|
55
|
-
fail(parsed.data.operationId, "busy");
|
|
56
|
-
handled.add(parsed.data.operationId);
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
running = execute(parsed.data).finally(() => {
|
|
60
|
-
handled.add(parsed.data.operationId);
|
|
61
|
-
running = null;
|
|
62
|
-
});
|
|
63
|
-
await running;
|
|
64
|
-
return true;
|
|
65
|
-
},
|
|
66
|
-
drain: async () => { await running; },
|
|
67
|
-
};
|
|
68
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from "node:util";
|
|
3
|
-
import { migrateCodexHome } from "./codex-home-migration.js";
|
|
4
|
-
const { values } = parseArgs({
|
|
5
|
-
args: process.argv.slice(2),
|
|
6
|
-
strict: true,
|
|
7
|
-
options: {
|
|
8
|
-
"agent-home": { type: "string" },
|
|
9
|
-
"source-home": { type: "string" },
|
|
10
|
-
"backup-root": { type: "string" },
|
|
11
|
-
yes: { type: "boolean", default: false },
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
function required(value, name) {
|
|
15
|
-
if (!value?.trim())
|
|
16
|
-
throw new Error(`Missing --${name}`);
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
if (!values.yes)
|
|
20
|
-
throw new Error("Refusing Codex home migration without --yes");
|
|
21
|
-
const result = await migrateCodexHome({
|
|
22
|
-
agentHome: required(values["agent-home"], "agent-home"),
|
|
23
|
-
sourceHome: required(values["source-home"], "source-home"),
|
|
24
|
-
backupRoot: required(values["backup-root"], "backup-root"),
|
|
25
|
-
});
|
|
26
|
-
process.stdout.write(`${result.status}${result.backupDir ? ` backup=${result.backupDir}` : ""}\n`);
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { lstat, mkdir, readlink, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { atomicPrivateWrite } from "../atomic-private-write.js";
|
|
7
|
-
import { defaultCodexHome, materializeDefaultCodexHome } from "./codex-home.js";
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
const SQLITE_FILES = ["state_5.sqlite", "state_5.sqlite-wal", "state_5.sqlite-shm"];
|
|
10
|
-
const MARKER_NAME = ".codex-home-migration.json";
|
|
11
|
-
async function exists(path) {
|
|
12
|
-
try {
|
|
13
|
-
await lstat(path);
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
if (error.code === "ENOENT")
|
|
18
|
-
return false;
|
|
19
|
-
throw error;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
async function assertNoOpenSqliteFiles(codexHome) {
|
|
23
|
-
for (const name of SQLITE_FILES) {
|
|
24
|
-
const path = join(codexHome, name);
|
|
25
|
-
if (!(await exists(path)))
|
|
26
|
-
continue;
|
|
27
|
-
try {
|
|
28
|
-
const result = await execFileAsync("lsof", ["-t", "--", path], { encoding: "utf8" });
|
|
29
|
-
if (result.stdout.trim()) {
|
|
30
|
-
throw new Error(`Codex SQLite file is still open: ${path}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
const code = error.code;
|
|
35
|
-
if (code === 1)
|
|
36
|
-
continue;
|
|
37
|
-
if (code === "ENOENT")
|
|
38
|
-
throw new Error("lsof is required to verify Codex home quiescence");
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function symlinkTarget(path) {
|
|
44
|
-
try {
|
|
45
|
-
const info = await lstat(path);
|
|
46
|
-
if (!info.isSymbolicLink())
|
|
47
|
-
return null;
|
|
48
|
-
return await readlink(path);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error.code === "ENOENT")
|
|
52
|
-
return null;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export async function migrateCodexHome(input, dependencies = {}) {
|
|
57
|
-
const agentHome = resolve(input.agentHome);
|
|
58
|
-
const sourceHome = resolve(input.sourceHome);
|
|
59
|
-
const backupRoot = resolve(input.backupRoot);
|
|
60
|
-
const codexHome = defaultCodexHome(agentHome);
|
|
61
|
-
const sessions = join(codexHome, "sessions");
|
|
62
|
-
const index = join(codexHome, "session_index.jsonl");
|
|
63
|
-
const expectedSessions = resolve(sourceHome, "sessions");
|
|
64
|
-
const expectedIndex = resolve(sourceHome, "session_index.jsonl");
|
|
65
|
-
const marker = join(codexHome, MARKER_NAME);
|
|
66
|
-
const sessionLink = await symlinkTarget(sessions);
|
|
67
|
-
const indexLink = await symlinkTarget(index);
|
|
68
|
-
if (sessionLink === null && indexLink === null && await exists(marker)) {
|
|
69
|
-
return { status: "already-migrated" };
|
|
70
|
-
}
|
|
71
|
-
if (sessionLink === null || indexLink === null) {
|
|
72
|
-
throw new Error("Refusing migration: Codex home is not in the complete legacy symlink layout");
|
|
73
|
-
}
|
|
74
|
-
if (resolve(codexHome, sessionLink) !== expectedSessions || resolve(codexHome, indexLink) !== expectedIndex) {
|
|
75
|
-
throw new Error("Refusing migration: legacy symlinks do not point to the configured source home");
|
|
76
|
-
}
|
|
77
|
-
await (dependencies.ensureQuiesced ?? assertNoOpenSqliteFiles)(codexHome);
|
|
78
|
-
const stamp = (dependencies.now ?? (() => new Date()))().toISOString().replaceAll(/[^0-9]/g, "").slice(0, 14);
|
|
79
|
-
const backupDir = join(backupRoot, `${stamp}-${dependencies.id?.() ?? randomUUID()}`);
|
|
80
|
-
await mkdir(backupDir, { recursive: true, mode: 0o700 });
|
|
81
|
-
await symlink(sessionLink, join(backupDir, "sessions"), "dir");
|
|
82
|
-
await symlink(indexLink, join(backupDir, "session_index.jsonl"), "file");
|
|
83
|
-
const moved = [];
|
|
84
|
-
try {
|
|
85
|
-
for (const name of SQLITE_FILES) {
|
|
86
|
-
const current = join(codexHome, name);
|
|
87
|
-
if (await exists(current)) {
|
|
88
|
-
await rename(current, join(backupDir, name));
|
|
89
|
-
moved.push(name);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
await materializeDefaultCodexHome(agentHome, sourceHome);
|
|
93
|
-
await atomicPrivateWrite(marker, JSON.stringify({
|
|
94
|
-
version: 1,
|
|
95
|
-
migrated_at: new Date().toISOString(),
|
|
96
|
-
source_home: sourceHome,
|
|
97
|
-
backup_dir: backupDir,
|
|
98
|
-
}) + "\n");
|
|
99
|
-
return { status: "migrated", backupDir };
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
for (const name of moved.reverse()) {
|
|
103
|
-
await rename(join(backupDir, name), join(codexHome, name)).catch(() => undefined);
|
|
104
|
-
}
|
|
105
|
-
await rm(sessions, { recursive: true, force: true });
|
|
106
|
-
await unlink(index).catch(() => undefined);
|
|
107
|
-
await symlink(sessionLink, sessions, "dir").catch(() => undefined);
|
|
108
|
-
await symlink(indexLink, index, "file").catch(() => undefined);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
export const codexHomeMigrationMarkerName = MARKER_NAME;
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { chmod, copyFile, lstat, mkdir, open, readdir, readlink, rename, rm, symlink, unlink, link, writeFile, } from "node:fs/promises";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
const PRIVATE_CODEX_HOME = ".codex";
|
|
5
|
-
export function defaultCodexHome(homeDir) {
|
|
6
|
-
return resolve(homeDir, PRIVATE_CODEX_HOME);
|
|
7
|
-
}
|
|
8
|
-
async function linkIfMissing(source, target) {
|
|
9
|
-
try {
|
|
10
|
-
await lstat(target);
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (error.code !== "ENOENT")
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
await lstat(source);
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
if (error.code === "ENOENT")
|
|
22
|
-
return;
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
await symlink(source, target, "file");
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (error.code !== "EEXIST")
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function isBelow(root, candidate) {
|
|
34
|
-
const child = relative(root, candidate);
|
|
35
|
-
return child !== "" && child !== ".." && !child.startsWith(`..${sep}`) && !child.startsWith(sep);
|
|
36
|
-
}
|
|
37
|
-
async function isExactSymlink(target, expectedSource) {
|
|
38
|
-
const linkTarget = await readlink(target);
|
|
39
|
-
return resolve(dirname(target), linkTarget) === resolve(expectedSource);
|
|
40
|
-
}
|
|
41
|
-
async function ensurePrivateSessions(target, legacySource) {
|
|
42
|
-
try {
|
|
43
|
-
const current = await lstat(target);
|
|
44
|
-
if (current.isSymbolicLink()) {
|
|
45
|
-
if (legacySource === undefined || !(await isExactSymlink(target, legacySource))) {
|
|
46
|
-
throw new Error(`Refusing to replace unexpected Codex sessions symlink: ${target}`);
|
|
47
|
-
}
|
|
48
|
-
const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
49
|
-
const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
50
|
-
try {
|
|
51
|
-
await mkdir(replacement, { mode: 0o700 });
|
|
52
|
-
await rename(target, legacyBackup);
|
|
53
|
-
try {
|
|
54
|
-
await rename(replacement, target);
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
await rename(legacyBackup, target).catch(() => undefined);
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
await rm(replacement, { recursive: true, force: true }).catch(() => undefined);
|
|
63
|
-
await rm(legacyBackup, { recursive: true, force: true }).catch(() => undefined);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (!current.isDirectory()) {
|
|
67
|
-
throw new Error(`Codex sessions target is not a directory: ${target}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (error.code !== "ENOENT")
|
|
72
|
-
throw error;
|
|
73
|
-
await mkdir(target, { mode: 0o700 });
|
|
74
|
-
}
|
|
75
|
-
await chmod(target, 0o700);
|
|
76
|
-
}
|
|
77
|
-
async function atomicCopyFile(source, target, mode) {
|
|
78
|
-
const parent = dirname(target);
|
|
79
|
-
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
80
|
-
const temporary = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
81
|
-
try {
|
|
82
|
-
await copyFile(source, temporary, constants.COPYFILE_EXCL);
|
|
83
|
-
await chmod(temporary, mode);
|
|
84
|
-
try {
|
|
85
|
-
await link(temporary, target);
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
if (error.code !== "EEXIST")
|
|
90
|
-
throw error;
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
await unlink(temporary).catch(() => undefined);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async function ensurePrivateIndex(target, source) {
|
|
99
|
-
let current;
|
|
100
|
-
try {
|
|
101
|
-
current = await lstat(target);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
if (error.code !== "ENOENT")
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
if (current?.isSymbolicLink()) {
|
|
108
|
-
if (source === undefined || !(await isExactSymlink(target, source))) {
|
|
109
|
-
throw new Error(`Refusing to replace unexpected Codex session index symlink: ${target}`);
|
|
110
|
-
}
|
|
111
|
-
const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
112
|
-
const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
113
|
-
try {
|
|
114
|
-
try {
|
|
115
|
-
await lstat(source);
|
|
116
|
-
await copyFile(source, replacement, constants.COPYFILE_EXCL);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
if (error.code !== "ENOENT")
|
|
120
|
-
throw error;
|
|
121
|
-
await writeFile(replacement, "", { encoding: "utf8", flag: "wx" });
|
|
122
|
-
}
|
|
123
|
-
await chmod(replacement, 0o600);
|
|
124
|
-
await rename(target, legacyBackup);
|
|
125
|
-
try {
|
|
126
|
-
await rename(replacement, target);
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
await rename(legacyBackup, target).catch(() => undefined);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
await rm(replacement, { force: true }).catch(() => undefined);
|
|
135
|
-
await rm(legacyBackup, { force: true }).catch(() => undefined);
|
|
136
|
-
}
|
|
137
|
-
current = undefined;
|
|
138
|
-
}
|
|
139
|
-
if (current !== undefined && !current.isFile()) {
|
|
140
|
-
throw new Error(`Codex session index target is not a regular file: ${target}`);
|
|
141
|
-
}
|
|
142
|
-
if (current !== undefined) {
|
|
143
|
-
await chmod(target, 0o600);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (source !== undefined) {
|
|
147
|
-
try {
|
|
148
|
-
await lstat(source);
|
|
149
|
-
if (await atomicCopyFile(source, target, 0o600))
|
|
150
|
-
return;
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (error.code !== "ENOENT")
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const handle = await open(target, "wx", 0o600).catch((error) => {
|
|
159
|
-
if (error.code === "EEXIST")
|
|
160
|
-
return null;
|
|
161
|
-
throw error;
|
|
162
|
-
});
|
|
163
|
-
await handle?.close();
|
|
164
|
-
}
|
|
165
|
-
async function findRollout(root, sessionId) {
|
|
166
|
-
const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
|
|
167
|
-
if (error.code === "ENOENT")
|
|
168
|
-
return [];
|
|
169
|
-
throw error;
|
|
170
|
-
});
|
|
171
|
-
const matches = [];
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
const path = join(root, entry.name);
|
|
174
|
-
if (entry.isDirectory()) {
|
|
175
|
-
const nested = await findRollout(path, sessionId);
|
|
176
|
-
if (nested !== null)
|
|
177
|
-
matches.push(nested);
|
|
178
|
-
}
|
|
179
|
-
else if (entry.isFile() && entry.name.includes(sessionId)) {
|
|
180
|
-
matches.push(path);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (matches.length > 1)
|
|
184
|
-
throw new Error(`Multiple Codex rollout files match session ${sessionId}`);
|
|
185
|
-
return matches[0] ?? null;
|
|
186
|
-
}
|
|
187
|
-
async function hydrateResumeRollout(sourceSessions, privateSessions, sessionId) {
|
|
188
|
-
if (sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
189
|
-
throw new Error("Invalid Codex resume session id");
|
|
190
|
-
}
|
|
191
|
-
const sourcePath = await findRollout(sourceSessions, sessionId);
|
|
192
|
-
if (sourcePath === null)
|
|
193
|
-
return;
|
|
194
|
-
const relativePath = relative(sourceSessions, sourcePath);
|
|
195
|
-
const destination = resolve(privateSessions, relativePath);
|
|
196
|
-
if (!isBelow(resolve(privateSessions), destination)) {
|
|
197
|
-
throw new Error("Codex resume rollout escaped the private sessions directory");
|
|
198
|
-
}
|
|
199
|
-
let existing;
|
|
200
|
-
try {
|
|
201
|
-
existing = await lstat(destination);
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
if (error.code !== "ENOENT")
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
if (existing !== undefined) {
|
|
208
|
-
if (!existing.isFile() || existing.isSymbolicLink()) {
|
|
209
|
-
throw new Error(`Refusing to replace existing Codex rollout: ${destination}`);
|
|
210
|
-
}
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const mode = (await lstat(sourcePath)).mode & 0o777;
|
|
214
|
-
await atomicCopyFile(sourcePath, destination, mode);
|
|
215
|
-
}
|
|
216
|
-
/** Isolate Codex high-volume state per Agent while preserving auth and exact resume. */
|
|
217
|
-
export async function materializeDefaultCodexHome(agentHome, sourceHome, resumeSessionId) {
|
|
218
|
-
const codexHome = defaultCodexHome(agentHome);
|
|
219
|
-
await mkdir(codexHome, { recursive: true, mode: 0o700 });
|
|
220
|
-
await chmod(codexHome, 0o700);
|
|
221
|
-
const source = sourceHome === undefined ? null : resolve(sourceHome);
|
|
222
|
-
if (source !== null && source !== codexHome) {
|
|
223
|
-
await Promise.all([
|
|
224
|
-
linkIfMissing(join(source, "auth.json"), join(codexHome, "auth.json")),
|
|
225
|
-
linkIfMissing(join(source, "config.toml"), join(codexHome, "config.toml")),
|
|
226
|
-
]);
|
|
227
|
-
}
|
|
228
|
-
const sourceSessions = source === null || source === codexHome ? undefined : join(source, "sessions");
|
|
229
|
-
await ensurePrivateSessions(join(codexHome, "sessions"), sourceSessions);
|
|
230
|
-
const sourceIndex = source === null || source === codexHome ? undefined : join(source, "session_index.jsonl");
|
|
231
|
-
await ensurePrivateIndex(join(codexHome, "session_index.jsonl"), sourceIndex);
|
|
232
|
-
if (sourceSessions !== undefined && resumeSessionId) {
|
|
233
|
-
await hydrateResumeRollout(sourceSessions, join(codexHome, "sessions"), resumeSessionId);
|
|
234
|
-
}
|
|
235
|
-
return codexHome;
|
|
236
|
-
}
|