@nowcrew/daemon 0.6.58 → 0.6.63
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/automatic-install-target.js +10 -15
- package/dist/config.js +0 -35
- package/dist/daemon-migration-controller.js +18 -16
- package/dist/execution-runner.js +0 -1
- package/dist/local-executor.js +1 -44
- package/dist/machine-info.js +2 -3
- package/dist/main.js +0 -77
- package/dist/profile-layout.js +4 -10
- package/dist/runner.js +0 -1
- package/dist/serve.js +3 -64
- package/dist/workspace.js +1 -24
- package/package.json +10 -9
- 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/storage-config.js +0 -175
- package/dist/storage-control.js +0 -131
- package/dist/storage-copy.js +0 -444
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -175
- package/dist/storage-identity.js +0 -29
- package/dist/storage-layout.js +0 -143
- package/dist/storage-manifest.js +0 -440
- 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 -311
- package/dist/storage-runtime-readiness.js +0 -178
- package/dist/storage-runtime.js +0 -39
- package/dist/storage-safe-fs.js +0 -293
- package/dist/storage-serve-adapter.js +0 -65
- package/dist/storage-status-delivery.js +0 -201
- 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 -380
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -277
- 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
|
-
}
|
package/dist/storage-config.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { posix, win32 } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
5
|
-
import { resolveEnvTag, resolveNowworkHome, storageLayout, } from "./storage-layout.js";
|
|
6
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
7
|
-
const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
|
|
8
|
-
const PORTABLE_ABSOLUTE_PATH = z.string().min(1).max(32768).refine((value) => posix.isAbsolute(value) || win32.isAbsolute(value), "must be an absolute path");
|
|
9
|
-
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
10
|
-
const url = new URL(value);
|
|
11
|
-
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
|
|
12
|
-
}, "serverUrl must be HTTP(S) without embedded credentials");
|
|
13
|
-
const InstallationRecordSchema = z.object({
|
|
14
|
-
version: z.literal(1),
|
|
15
|
-
installationId: z.string().uuid(),
|
|
16
|
-
profile: z.string().regex(PROFILE),
|
|
17
|
-
serverUrl: ServerUrlSchema,
|
|
18
|
-
environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
|
|
19
|
-
workspaceId: z.string().uuid(),
|
|
20
|
-
storageId: z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u),
|
|
21
|
-
nowworkHome: PORTABLE_ABSOLUTE_PATH,
|
|
22
|
-
runtimeRoot: PORTABLE_ABSOLUTE_PATH,
|
|
23
|
-
credentialRef: z.string().regex(/^profile:[a-z0-9][a-z0-9_-]{0,47}$/u),
|
|
24
|
-
storageMigrationId: z.string().uuid().optional(),
|
|
25
|
-
createdAt: z.string().datetime({ offset: true }),
|
|
26
|
-
updatedAt: z.string().datetime({ offset: true }),
|
|
27
|
-
}).strict();
|
|
28
|
-
const InstallationRegistrySchema = z.object({
|
|
29
|
-
version: z.literal(1),
|
|
30
|
-
installations: z.array(InstallationRecordSchema).max(256),
|
|
31
|
-
}).strict();
|
|
32
|
-
export function findInstallationForProfile(records, profile) {
|
|
33
|
-
if (!PROFILE.test(profile))
|
|
34
|
-
throw new Error("profile name is invalid");
|
|
35
|
-
const matches = records.filter((record) => record.profile === profile);
|
|
36
|
-
if (matches.length > 1)
|
|
37
|
-
throw new Error(`Multiple installations use profile '${profile}'`);
|
|
38
|
-
return matches[0] ?? null;
|
|
39
|
-
}
|
|
40
|
-
const pathsFor = (platform) => platform === "win32" ? win32 : posix;
|
|
41
|
-
function absoluteBase(value, fallback, platform, name) {
|
|
42
|
-
const paths = pathsFor(platform);
|
|
43
|
-
const selected = value ?? fallback;
|
|
44
|
-
if (!paths.isAbsolute(selected) || selected.includes("\0") || selected.includes("\n") || selected.includes("\r")) {
|
|
45
|
-
throw new Error(`${name} must be an absolute single-line path`);
|
|
46
|
-
}
|
|
47
|
-
return paths.normalize(selected);
|
|
48
|
-
}
|
|
49
|
-
export function resolveLocalStorageRoots(input) {
|
|
50
|
-
const paths = pathsFor(input.platform);
|
|
51
|
-
const userHome = absoluteBase(input.userHome, input.userHome, input.platform, "user home");
|
|
52
|
-
if (input.platform === "win32") {
|
|
53
|
-
const local = absoluteBase(input.environment.LOCALAPPDATA, paths.join(userHome, "AppData", "Local"), input.platform, "LOCALAPPDATA");
|
|
54
|
-
const root = paths.join(local, "NowWork");
|
|
55
|
-
return Object.freeze({
|
|
56
|
-
configRoot: paths.join(root, "config"),
|
|
57
|
-
runtimeBase: paths.join(root, "runtime"),
|
|
58
|
-
cacheRoot: paths.join(root, "cache"),
|
|
59
|
-
stateRoot: paths.join(root, "state"),
|
|
60
|
-
backupsRoot: paths.join(root, "state", "backups"),
|
|
61
|
-
runtimePackagesRoot: paths.join(root, "cache", "runtime-packages"),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
if (input.platform === "darwin") {
|
|
65
|
-
const applicationSupport = paths.join(userHome, "Library", "Application Support", "NowWork");
|
|
66
|
-
const cacheRoot = paths.join(userHome, "Library", "Caches", "NowWork");
|
|
67
|
-
return Object.freeze({
|
|
68
|
-
configRoot: paths.join(applicationSupport, "config"),
|
|
69
|
-
runtimeBase: paths.join(cacheRoot, "runtime"),
|
|
70
|
-
cacheRoot,
|
|
71
|
-
stateRoot: paths.join(applicationSupport, "state"),
|
|
72
|
-
backupsRoot: paths.join(applicationSupport, "state", "backups"),
|
|
73
|
-
runtimePackagesRoot: paths.join(cacheRoot, "runtime-packages"),
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
const configHome = absoluteBase(input.environment.XDG_CONFIG_HOME, paths.join(userHome, ".config"), input.platform, "XDG_CONFIG_HOME");
|
|
77
|
-
const cacheHome = absoluteBase(input.environment.XDG_CACHE_HOME, paths.join(userHome, ".cache"), input.platform, "XDG_CACHE_HOME");
|
|
78
|
-
const stateHome = absoluteBase(input.environment.XDG_STATE_HOME, paths.join(userHome, ".local", "state"), input.platform, "XDG_STATE_HOME");
|
|
79
|
-
const runtimeHome = absoluteBase(input.environment.XDG_RUNTIME_DIR, paths.join(stateHome, "nowwork", "runtime"), input.platform, "XDG_RUNTIME_DIR");
|
|
80
|
-
const stateRoot = paths.join(stateHome, "nowwork");
|
|
81
|
-
const cacheRoot = paths.join(cacheHome, "nowwork");
|
|
82
|
-
return Object.freeze({
|
|
83
|
-
configRoot: paths.join(configHome, "nowwork"),
|
|
84
|
-
runtimeBase: input.environment.XDG_RUNTIME_DIR === undefined ? runtimeHome : paths.join(runtimeHome, "nowwork"),
|
|
85
|
-
cacheRoot,
|
|
86
|
-
stateRoot,
|
|
87
|
-
backupsRoot: paths.join(stateRoot, "backups"),
|
|
88
|
-
runtimePackagesRoot: paths.join(cacheRoot, "runtime-packages"),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
export function buildStorageConfig(input) {
|
|
92
|
-
if (!PROFILE.test(input.profile))
|
|
93
|
-
throw new Error("profile name is invalid");
|
|
94
|
-
if (!UUID.test(input.workspaceId))
|
|
95
|
-
throw new Error("workspace id must be a UUID");
|
|
96
|
-
const paths = pathsFor(input.platform);
|
|
97
|
-
const local = resolveLocalStorageRoots(input);
|
|
98
|
-
const home = resolveNowworkHome({
|
|
99
|
-
...(input.nowworkHome === undefined ? {} : { explicit: input.nowworkHome }),
|
|
100
|
-
...(input.environment.NOWWORK_HOME === undefined ? {} : { environment: input.environment.NOWWORK_HOME }),
|
|
101
|
-
userHome: input.userHome,
|
|
102
|
-
platform: input.platform,
|
|
103
|
-
});
|
|
104
|
-
const credentialRef = `profile:${input.profile}`;
|
|
105
|
-
if (home.mode === "legacy") {
|
|
106
|
-
return Object.freeze({
|
|
107
|
-
mode: "legacy",
|
|
108
|
-
profile: input.profile,
|
|
109
|
-
workspaceId: input.workspaceId.toLowerCase(),
|
|
110
|
-
credentialRef,
|
|
111
|
-
daemonHome: input.legacyDaemonHome ?? paths.join(input.userHome, ".crew", "daemon"),
|
|
112
|
-
agentsRoot: input.legacyAgentsRoot ?? paths.join(input.userHome, ".crew", "agents"),
|
|
113
|
-
layout: null,
|
|
114
|
-
local,
|
|
115
|
-
diagnostics: Object.freeze(["legacy_storage_detected"]),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
const environment = resolveEnvTag({
|
|
119
|
-
...(input.envTag === undefined ? {} : { explicit: input.envTag }),
|
|
120
|
-
...(input.environment.NOWWORK_ENV_TAG === undefined ? {} : { environment: input.environment.NOWWORK_ENV_TAG }),
|
|
121
|
-
}).environment;
|
|
122
|
-
const layout = storageLayout({
|
|
123
|
-
nowworkHome: home.root,
|
|
124
|
-
environment,
|
|
125
|
-
workspaceId: input.workspaceId,
|
|
126
|
-
runtimeBase: local.runtimeBase,
|
|
127
|
-
platform: input.platform,
|
|
128
|
-
});
|
|
129
|
-
return Object.freeze({
|
|
130
|
-
mode: "standard",
|
|
131
|
-
profile: input.profile,
|
|
132
|
-
workspaceId: input.workspaceId.toLowerCase(),
|
|
133
|
-
credentialRef,
|
|
134
|
-
daemonHome: local.configRoot,
|
|
135
|
-
agentsRoot: layout.agentItemsRoot,
|
|
136
|
-
layout,
|
|
137
|
-
local,
|
|
138
|
-
diagnostics: Object.freeze([]),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
export function installationsPath(configRoot) {
|
|
142
|
-
const paths = win32.isAbsolute(configRoot) && !posix.isAbsolute(configRoot) ? win32 : posix;
|
|
143
|
-
return paths.join(configRoot, "installations.json");
|
|
144
|
-
}
|
|
145
|
-
export async function readInstallations(input) {
|
|
146
|
-
const path = installationsPath(input.configRoot);
|
|
147
|
-
try {
|
|
148
|
-
const parsed = InstallationRegistrySchema.parse(JSON.parse(await readFile(path, "utf8")));
|
|
149
|
-
return Object.freeze(parsed.installations.map((record) => Object.freeze(record)));
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (error.code === "ENOENT")
|
|
153
|
-
return Object.freeze([]);
|
|
154
|
-
throw new Error(`Invalid installation registry: ${path}`, { cause: error });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
export async function writeInstallations(records, input) {
|
|
158
|
-
let parsed;
|
|
159
|
-
try {
|
|
160
|
-
parsed = InstallationRegistrySchema.parse({ version: 1, installations: records });
|
|
161
|
-
const installationIds = new Set();
|
|
162
|
-
const profiles = new Set();
|
|
163
|
-
for (const record of parsed.installations) {
|
|
164
|
-
if (installationIds.has(record.installationId) || profiles.has(record.profile)) {
|
|
165
|
-
throw new Error("duplicate installation identity");
|
|
166
|
-
}
|
|
167
|
-
installationIds.add(record.installationId);
|
|
168
|
-
profiles.add(record.profile);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
throw new Error("Invalid installation metadata", { cause: error });
|
|
173
|
-
}
|
|
174
|
-
await atomicPrivateWrite(installationsPath(input.configRoot), `${JSON.stringify(parsed, null, 2)}\n`);
|
|
175
|
-
}
|