@nowcrew/daemon 0.6.65 → 0.6.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-ability/materializer.js +1 -20
- package/dist/agent-ability/resolver.js +10 -7
- package/dist/agent-ability/runtime.js +0 -3
- package/dist/automatic-install-target.js +10 -15
- package/dist/computer-cli.js +1 -10
- package/dist/computer-service.js +1 -41
- package/dist/config.js +0 -44
- package/dist/daemon-migration-controller.js +18 -16
- package/dist/execution-runner.js +1 -3
- package/dist/local-execution-prompt.js +9 -14
- package/dist/local-executor.js +3 -56
- package/dist/machine-info.js +2 -3
- package/dist/main.js +0 -83
- package/dist/managed-service-lifecycle.js +2 -47
- package/dist/managed-service-startup.js +0 -2
- package/dist/profile-layout.js +4 -10
- package/dist/project-skills/controller.js +0 -2
- package/dist/prompt.js +1 -19
- package/dist/runner.js +0 -3
- package/dist/runtimes/codex-app-server-runner.js +14 -54
- package/dist/runtimes/codex-contract.js +5 -20
- package/dist/runtimes/codex.js +0 -4
- package/dist/serve.js +3 -68
- package/dist/supervised-runtime.js +0 -3
- package/dist/workspace.js +1 -24
- package/package.json +9 -10
- package/dist/local-memory/index-budget.js +0 -74
- package/dist/local-memory/pending.js +0 -111
- package/dist/local-memory/runtime.js +0 -87
- package/dist/local-memory/types.js +0 -1
- package/dist/local-memory/write.js +0 -448
- package/dist/runtimes/codex-command-safety.js +0 -75
- package/dist/runtimes/codex-turn-retry.js +0 -69
- package/dist/storage-config.js +0 -175
- package/dist/storage-control.js +0 -193
- package/dist/storage-copy.js +0 -609
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-identity.js +0 -29
- package/dist/storage-layout.js +0 -143
- package/dist/storage-manifest.js +0 -855
- package/dist/storage-migration-controller.js +0 -417
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -339
- package/dist/storage-runtime-readiness.js +0 -184
- package/dist/storage-runtime.js +0 -39
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-serve-adapter.js +0 -65
- package/dist/storage-status-delivery.js +0 -209
- package/dist/storage-status-replay.js +0 -125
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -27
- package/dist/storage-worker-executor.js +0 -384
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -284
- package/dist/storage-worker-status.js +0 -105
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { containsLikelySecret } from "../agent-memory/policy.js";
|
|
6
|
-
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
|
|
7
|
-
import { persistPendingMemoryCandidate } from "./pending.js";
|
|
8
|
-
const CategorySchema = z.enum([
|
|
9
|
-
"preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
|
|
10
|
-
]);
|
|
11
|
-
const SingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
|
|
12
|
-
const ChangeSchema = z.object({
|
|
13
|
-
operation: z.enum(["create", "update", "supersede"]),
|
|
14
|
-
memoryId: SingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
15
|
-
baseRevision: z.number().int().nonnegative().nullable(),
|
|
16
|
-
supersedesMemoryId: SingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
17
|
-
title: SingleLineText.max(120),
|
|
18
|
-
kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
|
|
19
|
-
summary: SingleLineText,
|
|
20
|
-
triggers: z.array(SingleLineText.max(80)).min(1).max(16),
|
|
21
|
-
priority: z.enum(["high", "medium", "low"]),
|
|
22
|
-
detailCategory: CategorySchema,
|
|
23
|
-
detailBody: z.string().trim().min(1).max(12_000),
|
|
24
|
-
reviewAfter: SingleLineText.nullable(),
|
|
25
|
-
}).strict();
|
|
26
|
-
const StatusChangeSchema = z.object({
|
|
27
|
-
memoryId: SingleLineText,
|
|
28
|
-
expectedRevision: z.number().int().positive(),
|
|
29
|
-
expectedStatus: z.enum(["candidate", "active", "pinned", "stale", "superseded", "archived"]),
|
|
30
|
-
targetStatus: z.enum(["candidate", "active", "pinned", "stale", "archived"]),
|
|
31
|
-
reason: z.enum(["user_explicit", "manual_review"]),
|
|
32
|
-
}).strict();
|
|
33
|
-
const ResultSchema = z.object({
|
|
34
|
-
schemaVersion: z.literal(1),
|
|
35
|
-
purpose: z.literal("memory_prune"),
|
|
36
|
-
baseCatalogRevision: z.number().int().nonnegative(),
|
|
37
|
-
outcome: z.enum(["no_change", "proposed", "conflict", "failed"]),
|
|
38
|
-
changes: z.array(ChangeSchema).max(50),
|
|
39
|
-
statusChanges: z.array(StatusChangeSchema).max(50),
|
|
40
|
-
}).strict();
|
|
41
|
-
const GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
|
|
42
|
-
const INDEX_HEADING = /^##\s+Knowledge Index\s*$/mu;
|
|
43
|
-
const NEXT_SECTION = /^##\s+(?!#)/mu;
|
|
44
|
-
const CARD_HEADING = /^###\s+(.+?)\s*$/gmu;
|
|
45
|
-
const FIELD_LINE = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/iu;
|
|
46
|
-
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
47
|
-
const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
|
|
48
|
-
function codePointLength(value) {
|
|
49
|
-
return Array.from(value).length;
|
|
50
|
-
}
|
|
51
|
-
function detailPath(category) {
|
|
52
|
-
return `notes/${category}.md`;
|
|
53
|
-
}
|
|
54
|
-
function parseStatus(value) {
|
|
55
|
-
return value === "candidate" || value === "active" || value === "pinned"
|
|
56
|
-
|| value === "stale" || value === "superseded" || value === "archived"
|
|
57
|
-
? value
|
|
58
|
-
: "active";
|
|
59
|
-
}
|
|
60
|
-
function parseCard(titleLine, body) {
|
|
61
|
-
const fields = {};
|
|
62
|
-
for (const line of body.split(/\r?\n/u)) {
|
|
63
|
-
const match = FIELD_LINE.exec(line);
|
|
64
|
-
if (match?.[1] !== undefined && match[2] !== undefined)
|
|
65
|
-
fields[match[1].toLowerCase()] = match[2];
|
|
66
|
-
}
|
|
67
|
-
const title = titleLine.trim().replace(/^\[|\]$/gu, "").trim();
|
|
68
|
-
const file = fields.file?.trim();
|
|
69
|
-
if (!title || !file)
|
|
70
|
-
return null;
|
|
71
|
-
const priority = fields.priority === "high" || fields.priority === "low" ? fields.priority : "medium";
|
|
72
|
-
const revision = Number(fields.revision);
|
|
73
|
-
return {
|
|
74
|
-
id: fields.id?.trim() || `legacy:${file}:${title}`,
|
|
75
|
-
title,
|
|
76
|
-
file,
|
|
77
|
-
type: fields.type === "procedural" || fields.type === "episodic" || fields.type === "preference"
|
|
78
|
-
? fields.type
|
|
79
|
-
: "semantic",
|
|
80
|
-
status: parseStatus(fields.status),
|
|
81
|
-
summary: fields.summary?.trim() ?? "",
|
|
82
|
-
triggers: [...new Set((fields.triggers ?? "").split(/[,,、]/u).map((value) => value.trim()).filter(Boolean))],
|
|
83
|
-
priority,
|
|
84
|
-
updated: fields.updated?.trim() ?? "",
|
|
85
|
-
anchor: fields.anchor?.trim() || null,
|
|
86
|
-
revision: Number.isInteger(revision) && revision > 0 ? revision : 1,
|
|
87
|
-
reviewAfter: fields.review_after?.trim() || null,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function indexSection(memory) {
|
|
91
|
-
const heading = INDEX_HEADING.exec(memory);
|
|
92
|
-
if (heading === null || heading.index === undefined)
|
|
93
|
-
return "";
|
|
94
|
-
const start = heading.index + heading[0].length;
|
|
95
|
-
const rest = memory.slice(start);
|
|
96
|
-
const next = NEXT_SECTION.exec(rest);
|
|
97
|
-
return next?.index === undefined ? rest : rest.slice(0, next.index);
|
|
98
|
-
}
|
|
99
|
-
function parseCards(section) {
|
|
100
|
-
const withoutComments = section.replace(/<!--[\s\S]*?-->/gu, "");
|
|
101
|
-
const headings = [...withoutComments.matchAll(CARD_HEADING)];
|
|
102
|
-
return headings.flatMap((heading, index) => {
|
|
103
|
-
if (heading.index === undefined || heading[1] === undefined)
|
|
104
|
-
return [];
|
|
105
|
-
const bodyStart = heading.index + heading[0].length;
|
|
106
|
-
const bodyEnd = headings[index + 1]?.index ?? withoutComments.length;
|
|
107
|
-
const card = parseCard(heading[1], withoutComments.slice(bodyStart, bodyEnd));
|
|
108
|
-
return card === null ? [] : [card];
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
export function parseLocalMemoryIndex(memory) {
|
|
112
|
-
const section = indexSection(memory);
|
|
113
|
-
const generated = GENERATED_INDEX_PATTERN.exec(section)?.[0];
|
|
114
|
-
return parseCards(generated ?? section);
|
|
115
|
-
}
|
|
116
|
-
function parseUnmarkedMemoryIndex(memory) {
|
|
117
|
-
const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
|
|
118
|
-
return parseCards(section);
|
|
119
|
-
}
|
|
120
|
-
function renderGeneratedIndex(entries) {
|
|
121
|
-
const cards = entries
|
|
122
|
-
.filter((entry) => entry.status === "pinned" || entry.status === "active")
|
|
123
|
-
.map(renderLocalMemoryIndexCard)
|
|
124
|
-
.join("\n");
|
|
125
|
-
return `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->\n${cards}<!-- NOWWORK:MEMORY_INDEX:END -->`;
|
|
126
|
-
}
|
|
127
|
-
function replaceGeneratedIndex(memory, entries) {
|
|
128
|
-
const generated = renderGeneratedIndex(entries);
|
|
129
|
-
if (GENERATED_INDEX_PATTERN.test(memory))
|
|
130
|
-
return memory.replace(GENERATED_INDEX_PATTERN, generated);
|
|
131
|
-
const heading = INDEX_HEADING.exec(memory);
|
|
132
|
-
if (heading?.index !== undefined) {
|
|
133
|
-
const afterHeading = heading.index + heading[0].length;
|
|
134
|
-
return `${memory.slice(0, afterHeading)}\n${generated}${memory.slice(afterHeading)}`;
|
|
135
|
-
}
|
|
136
|
-
return `${memory.trimEnd()}\n\n## Knowledge Index\n${generated}\n`;
|
|
137
|
-
}
|
|
138
|
-
function noteSection(id, revision, title, body) {
|
|
139
|
-
return `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id} revision=${revision} -->\n`
|
|
140
|
-
+ `## ${title}\n\n${body.trim()}\n`
|
|
141
|
-
+ `<!-- NOWWORK:MEMORY_ENTRY:END id=${id} -->\n`;
|
|
142
|
-
}
|
|
143
|
-
function notePattern(id) {
|
|
144
|
-
const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
145
|
-
return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
|
|
146
|
-
}
|
|
147
|
-
function replaceNote(content, id, replacement) {
|
|
148
|
-
const pattern = notePattern(id);
|
|
149
|
-
return pattern.test(content) ? content.replace(pattern, replacement) : null;
|
|
150
|
-
}
|
|
151
|
-
function containsReservedText(change) {
|
|
152
|
-
return RESERVED_MEMORY_MARKER.test([change.title, change.summary, ...change.triggers, change.detailBody].join("\n"));
|
|
153
|
-
}
|
|
154
|
-
function validateCandidate(result) {
|
|
155
|
-
if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
|
|
156
|
-
throw new Error("local_memory_no_change_has_operations");
|
|
157
|
-
}
|
|
158
|
-
for (const change of result.changes) {
|
|
159
|
-
const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
|
|
160
|
-
if (containsLikelySecret(text))
|
|
161
|
-
throw new Error("local_memory_secret_candidate");
|
|
162
|
-
if (CONTROL_CHARACTER_PATTERN.test(text))
|
|
163
|
-
throw new Error("local_memory_control_character");
|
|
164
|
-
if (containsReservedText(change))
|
|
165
|
-
throw new Error("local_memory_reserved_marker");
|
|
166
|
-
if (change.operation === "create"
|
|
167
|
-
&& (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
|
|
168
|
-
throw new Error("local_memory_create_revision_invalid");
|
|
169
|
-
}
|
|
170
|
-
if (change.operation === "update"
|
|
171
|
-
&& (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
|
|
172
|
-
throw new Error("local_memory_update_revision_missing");
|
|
173
|
-
}
|
|
174
|
-
if (change.operation === "supersede"
|
|
175
|
-
&& (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
|
|
176
|
-
throw new Error("local_memory_supersede_revision_missing");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
export function parseLocalMemoryResult(raw) {
|
|
181
|
-
let value;
|
|
182
|
-
try {
|
|
183
|
-
value = JSON.parse(raw);
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
throw new Error("local_memory_result_json_invalid");
|
|
187
|
-
}
|
|
188
|
-
const parsed = ResultSchema.safeParse(value);
|
|
189
|
-
if (!parsed.success)
|
|
190
|
-
throw new Error(`local_memory_result_invalid:${parsed.error.issues[0]?.path.join(".") ?? "unknown"}`);
|
|
191
|
-
validateCandidate(parsed.data);
|
|
192
|
-
return parsed.data;
|
|
193
|
-
}
|
|
194
|
-
function entryFromChange(change, id, revision, status) {
|
|
195
|
-
return {
|
|
196
|
-
id,
|
|
197
|
-
title: change.title,
|
|
198
|
-
file: detailPath(change.detailCategory),
|
|
199
|
-
type: change.kind,
|
|
200
|
-
status,
|
|
201
|
-
summary: change.summary,
|
|
202
|
-
triggers: [...new Set(change.triggers)],
|
|
203
|
-
priority: change.priority,
|
|
204
|
-
updated: new Date().toISOString().slice(0, 10),
|
|
205
|
-
anchor: id,
|
|
206
|
-
revision,
|
|
207
|
-
reviewAfter: change.reviewAfter,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
function matchesCreateCandidate(entry, change) {
|
|
211
|
-
const triggers = [...new Set(change.triggers)];
|
|
212
|
-
return entry.title === change.title
|
|
213
|
-
&& entry.file === detailPath(change.detailCategory)
|
|
214
|
-
&& entry.type === change.kind
|
|
215
|
-
&& entry.summary === change.summary
|
|
216
|
-
&& entry.priority === change.priority
|
|
217
|
-
&& entry.reviewAfter === change.reviewAfter
|
|
218
|
-
&& entry.triggers.length === triggers.length
|
|
219
|
-
&& entry.triggers.every((trigger, index) => trigger === triggers[index]);
|
|
220
|
-
}
|
|
221
|
-
function stableId(executionId, index, change) {
|
|
222
|
-
const input = [executionId, String(index), change.operation, change.title, change.summary, change.detailBody].join("\0");
|
|
223
|
-
return `mem_${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 12)}`;
|
|
224
|
-
}
|
|
225
|
-
function normalize(value) {
|
|
226
|
-
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
227
|
-
}
|
|
228
|
-
function activeOrPinnedIds(entries) {
|
|
229
|
-
return new Set(entries.filter((entry) => entry.status === "active" || entry.status === "pinned").map((entry) => entry.id));
|
|
230
|
-
}
|
|
231
|
-
function emptyResult() {
|
|
232
|
-
return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
|
|
233
|
-
}
|
|
234
|
-
function isWithin(root, candidate) {
|
|
235
|
-
const path = relative(root, candidate);
|
|
236
|
-
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
|
|
237
|
-
}
|
|
238
|
-
async function assertWritablePaths(agentDir, memoryPath) {
|
|
239
|
-
const agentRoot = await realpath(agentDir);
|
|
240
|
-
const notesPath = join(agentDir, "notes");
|
|
241
|
-
const notesRoot = await realpath(notesPath);
|
|
242
|
-
if (notesRoot !== resolve(agentRoot, "notes") || !isWithin(agentRoot, notesRoot))
|
|
243
|
-
throw new Error("local_memory_notes_path_invalid");
|
|
244
|
-
const memoryStat = await lstat(memoryPath);
|
|
245
|
-
if (!memoryStat.isFile() || memoryStat.isSymbolicLink())
|
|
246
|
-
throw new Error("local_memory_memory_path_invalid");
|
|
247
|
-
}
|
|
248
|
-
async function readNote(path) {
|
|
249
|
-
try {
|
|
250
|
-
const stat = await lstat(path);
|
|
251
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
252
|
-
throw new Error("local_memory_note_path_invalid");
|
|
253
|
-
return await readFile(path, "utf8");
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
if (error.code === "ENOENT")
|
|
257
|
-
return "";
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async function atomicWrite(path, content) {
|
|
262
|
-
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
263
|
-
try {
|
|
264
|
-
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
265
|
-
await rename(temporary, path);
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
function projectIndex(memory, entries, protectedIds) {
|
|
273
|
-
const empty = replaceGeneratedIndex(memory, []);
|
|
274
|
-
if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
275
|
-
throw new Error("local_memory_memory_over_budget");
|
|
276
|
-
const emptyIndex = renderGeneratedIndex([]);
|
|
277
|
-
const remainingTotal = LOCAL_MEMORY_TOTAL_CODE_POINT_CAP - codePointLength(empty);
|
|
278
|
-
const selection = selectLocalMemoryIndexEntries(entries, {
|
|
279
|
-
indexCodePointCap: Math.min(Math.max(0, LOCAL_MEMORY_INDEX_CODE_POINT_CAP - codePointLength(emptyIndex)), remainingTotal),
|
|
280
|
-
entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP,
|
|
281
|
-
}, { protectedIds });
|
|
282
|
-
return {
|
|
283
|
-
memory: replaceGeneratedIndex(memory, selection.entries),
|
|
284
|
-
omittedIds: selection.omittedIds,
|
|
285
|
-
reason: selection.reason,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
const applyTails = new Map();
|
|
289
|
-
async function withApplyLease(agentDir, operation) {
|
|
290
|
-
const key = resolve(agentDir);
|
|
291
|
-
const previous = applyTails.get(key) ?? Promise.resolve();
|
|
292
|
-
let release;
|
|
293
|
-
const current = new Promise((resolveCurrent) => { release = resolveCurrent; });
|
|
294
|
-
const tail = previous.then(() => current);
|
|
295
|
-
applyTails.set(key, tail);
|
|
296
|
-
await previous;
|
|
297
|
-
try {
|
|
298
|
-
return await operation();
|
|
299
|
-
}
|
|
300
|
-
finally {
|
|
301
|
-
release();
|
|
302
|
-
void tail.then(() => {
|
|
303
|
-
if (applyTails.get(key) === tail)
|
|
304
|
-
applyTails.delete(key);
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
export async function applyLocalMemoryResult(input) {
|
|
309
|
-
validateCandidate(input.result);
|
|
310
|
-
if (input.result.outcome !== "proposed")
|
|
311
|
-
return emptyResult();
|
|
312
|
-
const memoryPath = join(input.agentDir, "MEMORY.md");
|
|
313
|
-
await assertWritablePaths(input.agentDir, memoryPath);
|
|
314
|
-
return withApplyLease(input.agentDir, async () => {
|
|
315
|
-
const memory = await readFile(memoryPath, "utf8");
|
|
316
|
-
if (parseUnmarkedMemoryIndex(memory).length > 0)
|
|
317
|
-
throw new Error("local_memory_legacy_index_requires_migration");
|
|
318
|
-
let entries = [...parseLocalMemoryIndex(memory)];
|
|
319
|
-
for (const entry of entries) {
|
|
320
|
-
if (!isWithin(resolve(input.agentDir, "notes"), resolve(input.agentDir, entry.file))) {
|
|
321
|
-
throw new Error("local_memory_note_path_invalid");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const contentIds = new Set(input.result.changes.flatMap((change) => [
|
|
325
|
-
...(change.operation === "update" && change.memoryId !== null ? [change.memoryId] : []),
|
|
326
|
-
...(change.supersedesMemoryId === null ? [] : [change.supersedesMemoryId]),
|
|
327
|
-
]));
|
|
328
|
-
for (const statusChange of input.result.statusChanges) {
|
|
329
|
-
if (contentIds.has(statusChange.memoryId))
|
|
330
|
-
throw new Error("local_memory_status_conflict");
|
|
331
|
-
const current = entries.find((entry) => entry.id === statusChange.memoryId);
|
|
332
|
-
if (current === undefined || current.revision !== statusChange.expectedRevision || current.status !== statusChange.expectedStatus) {
|
|
333
|
-
throw new Error("local_memory_status_conflict");
|
|
334
|
-
}
|
|
335
|
-
entries = entries.map((entry) => entry.id === statusChange.memoryId
|
|
336
|
-
? { ...entry, status: statusChange.targetStatus, revision: entry.revision + 1 }
|
|
337
|
-
: entry);
|
|
338
|
-
}
|
|
339
|
-
const createdIds = [];
|
|
340
|
-
const updatedIds = [];
|
|
341
|
-
const statusChangedIds = input.result.statusChanges.map((change) => change.memoryId);
|
|
342
|
-
const deferredIds = [];
|
|
343
|
-
const pendingCandidates = [];
|
|
344
|
-
const noteUpdates = new Map();
|
|
345
|
-
const randomId = input.randomId ?? randomUUID;
|
|
346
|
-
const executionId = input.executionId ?? "unknown";
|
|
347
|
-
const defer = (change, index) => {
|
|
348
|
-
const candidateId = input.executionId === undefined ? `mem_${randomId()}` : stableId(executionId, index, change);
|
|
349
|
-
const pendingId = `pending-${candidateId.slice("mem_".length)}`;
|
|
350
|
-
pendingCandidates.push({
|
|
351
|
-
schemaVersion: 1,
|
|
352
|
-
pendingId,
|
|
353
|
-
createdAt: new Date().toISOString(),
|
|
354
|
-
reason: "index_over_budget",
|
|
355
|
-
source: { executionId, pruneTraceId: input.pruneTraceId ?? null },
|
|
356
|
-
candidate: change,
|
|
357
|
-
});
|
|
358
|
-
deferredIds.push(pendingId);
|
|
359
|
-
};
|
|
360
|
-
for (const [index, change] of input.result.changes.entries()) {
|
|
361
|
-
const id = change.operation === "update"
|
|
362
|
-
? change.memoryId
|
|
363
|
-
: input.executionId === undefined ? `mem_${randomId()}` : stableId(executionId, index, change);
|
|
364
|
-
const existingCreate = change.operation === "create" ? entries.find((entry) => entry.id === id) : undefined;
|
|
365
|
-
const current = change.operation === "update" ? entries.find((entry) => entry.id === id) : undefined;
|
|
366
|
-
if (change.operation === "update") {
|
|
367
|
-
if (current === undefined)
|
|
368
|
-
throw new Error("local_memory_revision_conflict_missing_id");
|
|
369
|
-
if (change.baseRevision !== current.revision)
|
|
370
|
-
throw new Error("local_memory_revision_conflict");
|
|
371
|
-
}
|
|
372
|
-
const superseded = change.operation === "supersede"
|
|
373
|
-
? entries.find((entry) => entry.id === change.supersedesMemoryId)
|
|
374
|
-
: undefined;
|
|
375
|
-
if (change.operation === "supersede") {
|
|
376
|
-
if (superseded === undefined)
|
|
377
|
-
throw new Error("local_memory_supersede_missing_id");
|
|
378
|
-
if (change.baseRevision !== superseded.revision)
|
|
379
|
-
throw new Error("local_memory_supersede_revision_conflict");
|
|
380
|
-
}
|
|
381
|
-
if (change.operation === "create") {
|
|
382
|
-
if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
|
|
383
|
-
throw new Error("local_memory_duplicate_candidate");
|
|
384
|
-
}
|
|
385
|
-
if (existingCreate === undefined && entries.some((entry) => normalize(entry.title) === normalize(change.title)
|
|
386
|
-
|| normalize(entry.summary) === normalize(change.summary))) {
|
|
387
|
-
throw new Error("local_memory_duplicate_candidate");
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
|
|
391
|
-
const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
|
|
392
|
-
const candidateEntries = existingCreate !== undefined
|
|
393
|
-
? entries
|
|
394
|
-
: change.supersedesMemoryId === null
|
|
395
|
-
? current === undefined ? [...entries, entry] : entries.map((item) => item.id === id ? entry : item)
|
|
396
|
-
: [
|
|
397
|
-
...entries.map((item) => item.id === change.supersedesMemoryId
|
|
398
|
-
? { ...item, status: "superseded", revision: item.revision + 1 }
|
|
399
|
-
: item),
|
|
400
|
-
entry,
|
|
401
|
-
];
|
|
402
|
-
const protectedIds = new Set(activeOrPinnedIds(entries));
|
|
403
|
-
if (change.supersedesMemoryId !== null)
|
|
404
|
-
protectedIds.delete(change.supersedesMemoryId);
|
|
405
|
-
const projection = projectIndex(memory, candidateEntries, protectedIds);
|
|
406
|
-
const omitted = (entry.status === "active" || entry.status === "pinned") && projection.omittedIds.includes(id);
|
|
407
|
-
if (change.operation === "update" && omitted)
|
|
408
|
-
throw new Error(projection.reason ?? "local_memory_index_over_budget");
|
|
409
|
-
if ((change.operation === "create" || change.operation === "supersede") && omitted) {
|
|
410
|
-
defer(change, index);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
const notePath = resolve(input.agentDir, entry.file);
|
|
414
|
-
if (!isWithin(resolve(input.agentDir, "notes"), notePath))
|
|
415
|
-
throw new Error("local_memory_note_path_invalid");
|
|
416
|
-
const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
|
|
417
|
-
const replacement = noteSection(id, revision, change.title, change.detailBody);
|
|
418
|
-
const existingSection = replaceNote(previous, id, replacement);
|
|
419
|
-
const next = existingSection ?? (current === undefined
|
|
420
|
-
? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`
|
|
421
|
-
: null);
|
|
422
|
-
if (next === null)
|
|
423
|
-
throw new Error("local_memory_note_anchor_missing");
|
|
424
|
-
noteUpdates.set(notePath, next);
|
|
425
|
-
entries = candidateEntries;
|
|
426
|
-
if (change.operation === "update")
|
|
427
|
-
updatedIds.push(id);
|
|
428
|
-
else if (existingCreate === undefined)
|
|
429
|
-
createdIds.push(id);
|
|
430
|
-
}
|
|
431
|
-
const finalProjection = projectIndex(memory, entries, activeOrPinnedIds(entries));
|
|
432
|
-
if (finalProjection.reason !== null)
|
|
433
|
-
throw new Error(finalProjection.reason);
|
|
434
|
-
for (const candidate of pendingCandidates)
|
|
435
|
-
await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
|
|
436
|
-
for (const [path, content] of noteUpdates)
|
|
437
|
-
await atomicWrite(path, content);
|
|
438
|
-
if (noteUpdates.size > 0 || statusChangedIds.length > 0)
|
|
439
|
-
await atomicWrite(memoryPath, finalProjection.memory);
|
|
440
|
-
return {
|
|
441
|
-
createdIds,
|
|
442
|
-
updatedIds,
|
|
443
|
-
statusChangedIds,
|
|
444
|
-
deferredIds,
|
|
445
|
-
pendingIds: deferredIds,
|
|
446
|
-
};
|
|
447
|
-
});
|
|
448
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex app-server 重试门控用的「只读 crew 命令」判定(spec Part 1,R3-1/R4-1)。
|
|
3
|
-
*
|
|
4
|
-
* 白名单 + 封闭文法,不是黑名单:任何解析不确定都返回 false。只回答"这是不是白名单里某条
|
|
5
|
-
* crew 只读命令的一次干净调用",不做"读/写"语义判断。任何非 crew 命令(sed/rg/git/python…)
|
|
6
|
-
* 一律 false——白名单之外的一切都视为不可安全重试。零 IO,可独立单测。
|
|
7
|
-
*/
|
|
8
|
-
/** 封闭列表,由 cli/src/run.ts 命令面反推;新增 CLI 只读命令时同步维护。漏加只会更保守。 */
|
|
9
|
-
export const READ_ONLY_CREW_COMMANDS = new Set([
|
|
10
|
-
"whoami",
|
|
11
|
-
"server info",
|
|
12
|
-
"message read",
|
|
13
|
-
"message check",
|
|
14
|
-
"thread read",
|
|
15
|
-
"task list",
|
|
16
|
-
"schedule list",
|
|
17
|
-
"schedule runs",
|
|
18
|
-
"reminder list",
|
|
19
|
-
"reminder log",
|
|
20
|
-
"resolve",
|
|
21
|
-
"channel members",
|
|
22
|
-
"integration env",
|
|
23
|
-
"integration list",
|
|
24
|
-
]);
|
|
25
|
-
const SINGLE_VERB = new Set(["whoami", "resolve"]);
|
|
26
|
-
// 一层 shell 包装:bash/sh/zsh -c / -lc / -cl,后跟被单引号或双引号整体包裹的正文。
|
|
27
|
-
const WRAPPER = /^(?:\/usr\/bin\/|\/bin\/)?(?:bash|sh|zsh)\s+-(?:l?c|cl)\s+(.+)$/su;
|
|
28
|
-
// 整串禁字符:出现任一即拒绝(含单个 &、引号、$、括号、换行、制表、反斜杠)。
|
|
29
|
-
const FORBIDDEN = /[&;|<>$`(){}\\'"\n\r\t]/u;
|
|
30
|
-
const OPTION_TOKEN = /^--?[A-Za-z][A-Za-z0-9-]*(?:=[A-Za-z0-9._:/@#%+-]+)?$/u;
|
|
31
|
-
const VALUE_TOKEN = /^[A-Za-z0-9._:/@#%+][A-Za-z0-9._:/@#%+-]+$/u;
|
|
32
|
-
function unwrap(command) {
|
|
33
|
-
const match = WRAPPER.exec(command);
|
|
34
|
-
if (match === null)
|
|
35
|
-
return command;
|
|
36
|
-
const quoted = match[1] ?? "";
|
|
37
|
-
const quote = quoted[0];
|
|
38
|
-
if (quote !== "'" && quote !== "\"")
|
|
39
|
-
return null;
|
|
40
|
-
if (quoted.length < 2 || quoted[quoted.length - 1] !== quote)
|
|
41
|
-
return null;
|
|
42
|
-
const body = quoted.slice(1, -1);
|
|
43
|
-
if (body.includes(quote))
|
|
44
|
-
return null;
|
|
45
|
-
if (quote === "\"" && /[$\\]/u.test(body))
|
|
46
|
-
return null;
|
|
47
|
-
return body;
|
|
48
|
-
}
|
|
49
|
-
export function isReadOnlyCrewCommand(command) {
|
|
50
|
-
const body = unwrap(command);
|
|
51
|
-
if (body === null)
|
|
52
|
-
return false;
|
|
53
|
-
if (FORBIDDEN.test(body))
|
|
54
|
-
return false;
|
|
55
|
-
const trimmed = body.trim();
|
|
56
|
-
if (trimmed === "" || /\s{2,}/u.test(trimmed))
|
|
57
|
-
return false;
|
|
58
|
-
const tokens = trimmed.split(" ");
|
|
59
|
-
if (tokens[0] !== "crew")
|
|
60
|
-
return false;
|
|
61
|
-
const verb = tokens[1];
|
|
62
|
-
if (verb === undefined)
|
|
63
|
-
return false;
|
|
64
|
-
let consumed;
|
|
65
|
-
if (SINGLE_VERB.has(verb)) {
|
|
66
|
-
consumed = 2;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
const pair = `${verb} ${tokens[2] ?? ""}`;
|
|
70
|
-
if (!READ_ONLY_CREW_COMMANDS.has(pair))
|
|
71
|
-
return false;
|
|
72
|
-
consumed = 3;
|
|
73
|
-
}
|
|
74
|
-
return tokens.slice(consumed).every((token) => OPTION_TOKEN.test(token) || VALUE_TOKEN.test(token));
|
|
75
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex app-server turn 级重试决策(spec Part 1)。
|
|
3
|
-
*
|
|
4
|
-
* attempt 只上报两个正交事实:retryReason(为什么值得重试)与 unsafeToRetry(本 turn 是否已有
|
|
5
|
-
* 白名单之外的工具活动)。外层循环通过 decideTurnRetry 单一出口判断,所有原因共用一个预算。
|
|
6
|
-
* 2026-08-10 事故根因:模型自己调 collaboration.interrupt_agent,Codex 把父 turn 也置为
|
|
7
|
-
* interrupted;旧代码直接 exit 130。同时旧的网关过载重试是无条件的,会重放 crew message send。
|
|
8
|
-
*/
|
|
9
|
-
import { isReadOnlyCrewCommand } from "./codex-command-safety.js";
|
|
10
|
-
/**
|
|
11
|
-
* Codex app-server v2 `codex app-server generate-json-schema` ThreadItem 枚举的全部 18 种类型,
|
|
12
|
-
* 逐一裁定安全性(2026-08-28 最终评审 F1:实测真实 app-server 0.150.1 发现 userMessage 缺失导致
|
|
13
|
-
* 所有 turn 被判不安全,回归影响过载重试与 interrupted 重试两条路径)。新增协议类型时,必须在此
|
|
14
|
-
* 表格里显式落座,不允许静默归类。
|
|
15
|
-
*
|
|
16
|
-
* safe(协议/叙述类,无副作用,共 9 种):
|
|
17
|
-
* - userMessage Codex 对我们自己发送的 prompt 的回显,每个 turn 必有
|
|
18
|
-
* - hookPrompt hook 触发的系统提示回显
|
|
19
|
-
* - agentMessage 模型最终回复文本
|
|
20
|
-
* - reasoning 模型推理过程(不透出给下游)
|
|
21
|
-
* - plan 模型计划更新
|
|
22
|
-
* - contextCompaction 上下文压缩通知
|
|
23
|
-
* - sleep 模型主动等待
|
|
24
|
-
* - enteredReviewMode 进入 review 模式通知
|
|
25
|
-
* - exitedReviewMode 退出 review 模式通知
|
|
26
|
-
* conditional(1 种,视具体内容而定):
|
|
27
|
-
* - commandExecution 仅当 isReadOnlyCrewCommand(command) 为 true 时安全
|
|
28
|
-
* unsafe(有副作用或无法判断,共 8 种 + 未知类型):
|
|
29
|
-
* - fileChange 写文件
|
|
30
|
-
* - mcpToolCall 调用外部 MCP 工具
|
|
31
|
-
* - dynamicToolCall 调用动态工具
|
|
32
|
-
* - collabAgentToolCall 协作工具(spawnAgent/interrupt_agent 等)
|
|
33
|
-
* - subAgentActivity 子 agent 活动
|
|
34
|
-
* - webSearch 联网搜索
|
|
35
|
-
* - imageView 查看图片
|
|
36
|
-
* - imageGeneration 生成图片
|
|
37
|
-
* - (任何未知/缺失 type) 一律视为不安全
|
|
38
|
-
*/
|
|
39
|
-
const NARRATIVE_ITEM_TYPES = new Set([
|
|
40
|
-
"userMessage",
|
|
41
|
-
"hookPrompt",
|
|
42
|
-
"agentMessage",
|
|
43
|
-
"reasoning",
|
|
44
|
-
"plan",
|
|
45
|
-
"contextCompaction",
|
|
46
|
-
"sleep",
|
|
47
|
-
"enteredReviewMode",
|
|
48
|
-
"exitedReviewMode",
|
|
49
|
-
]);
|
|
50
|
-
/** 白名单判定:只有叙述类 item 和白名单内的 crew 只读命令是安全的;其它一切(含未知类型)不安全。 */
|
|
51
|
-
export function itemMakesTurnUnsafe(item) {
|
|
52
|
-
if (item?.type === undefined)
|
|
53
|
-
return true;
|
|
54
|
-
if (NARRATIVE_ITEM_TYPES.has(item.type))
|
|
55
|
-
return false;
|
|
56
|
-
if (item.type === "commandExecution") {
|
|
57
|
-
return typeof item.command !== "string" || !isReadOnlyCrewCommand(item.command);
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
export function decideTurnRetry(result, turnRetries, maxRetries) {
|
|
62
|
-
if (result.retryReason === undefined)
|
|
63
|
-
return { kind: "return" };
|
|
64
|
-
if (result.unsafeToRetry)
|
|
65
|
-
return { kind: "skipped", reason: result.retryReason };
|
|
66
|
-
if (turnRetries < maxRetries)
|
|
67
|
-
return { kind: "retry", reason: result.retryReason };
|
|
68
|
-
return { kind: "exhausted", reason: result.retryReason };
|
|
69
|
-
}
|