@nowcrew/daemon 0.6.68 → 0.6.69

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.
Files changed (72) hide show
  1. package/dist/agent-ability/materializer.js +30 -1
  2. package/dist/agent-ability/resolver.js +27 -16
  3. package/dist/agent-ability/runtime.js +3 -0
  4. package/dist/agent-ability/types.js +6 -11
  5. package/dist/automatic-install-target.js +15 -10
  6. package/dist/background-job.js +50 -0
  7. package/dist/computer-cli.js +10 -1
  8. package/dist/computer-service.js +41 -1
  9. package/dist/config.js +44 -0
  10. package/dist/console-formatter.js +76 -1
  11. package/dist/console.js +23 -4
  12. package/dist/daemon-migration-controller.js +16 -18
  13. package/dist/execution-protocol.js +0 -1
  14. package/dist/execution-runner.js +60 -11
  15. package/dist/i18n.js +2 -0
  16. package/dist/local-execution-prompt.js +32 -13
  17. package/dist/local-executor.js +107 -55
  18. package/dist/local-memory/index-budget.js +74 -0
  19. package/dist/local-memory/pending.js +111 -0
  20. package/dist/local-memory/runtime.js +87 -0
  21. package/dist/local-memory/types.js +1 -0
  22. package/dist/local-memory/write.js +448 -0
  23. package/dist/machine-info.js +3 -4
  24. package/dist/main.js +83 -0
  25. package/dist/managed-service-lifecycle.js +47 -2
  26. package/dist/managed-service-startup.js +2 -0
  27. package/dist/profile-layout.js +10 -4
  28. package/dist/project-dependencies.js +139 -124
  29. package/dist/project-dependency-lock.js +87 -0
  30. package/dist/project-dependency-manifest.js +2 -12
  31. package/dist/project-registry-wiring.js +1 -4
  32. package/dist/project-skills/controller.js +2 -0
  33. package/dist/project-skills/runtime-launch.js +10 -7
  34. package/dist/prompt.js +19 -1
  35. package/dist/provider-env.js +3 -21
  36. package/dist/repository-identity.js +45 -0
  37. package/dist/runner.js +4 -2
  38. package/dist/runtimes/codex-app-server-runner.js +55 -23
  39. package/dist/runtimes/codex-command-safety.js +75 -0
  40. package/dist/runtimes/codex-contract.js +20 -5
  41. package/dist/runtimes/codex-deepseek-config.js +7 -17
  42. package/dist/runtimes/codex-turn-retry.js +69 -0
  43. package/dist/runtimes/codex.js +4 -3
  44. package/dist/serve.js +68 -3
  45. package/dist/storage-config.js +175 -0
  46. package/dist/storage-control.js +193 -0
  47. package/dist/storage-copy.js +628 -0
  48. package/dist/storage-credential-guard.js +21 -0
  49. package/dist/storage-doctor.js +228 -0
  50. package/dist/storage-identity.js +29 -0
  51. package/dist/storage-layout.js +143 -0
  52. package/dist/storage-manifest.js +855 -0
  53. package/dist/storage-migration-controller.js +420 -0
  54. package/dist/storage-migration-manifest.js +105 -0
  55. package/dist/storage-migration-policy.js +100 -0
  56. package/dist/storage-runtime-controller.js +339 -0
  57. package/dist/storage-runtime-readiness.js +184 -0
  58. package/dist/storage-runtime.js +39 -0
  59. package/dist/storage-safe-fs.js +302 -0
  60. package/dist/storage-serve-adapter.js +65 -0
  61. package/dist/storage-status-delivery.js +209 -0
  62. package/dist/storage-status-replay.js +125 -0
  63. package/dist/storage-target-confirmation.js +129 -0
  64. package/dist/storage-worker-command.js +37 -0
  65. package/dist/storage-worker-executor.js +430 -0
  66. package/dist/storage-worker-handoff.js +151 -0
  67. package/dist/storage-worker-operation.js +284 -0
  68. package/dist/storage-worker-status.js +259 -0
  69. package/dist/supervised-runtime.js +3 -1
  70. package/dist/workspace.js +34 -14
  71. package/package.json +1 -1
  72. package/dist/self-training-workspace.js +0 -15
@@ -0,0 +1,448 @@
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
+ }
@@ -24,7 +24,6 @@ export { executableRuntimes } from "./runtime-capabilities.js";
24
24
  const execFileP = promisify(execFile);
25
25
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
26
26
  export const TRAINING_PROJECT_DEPENDENCIES_CAPABILITY = "training_project_dependencies_v1";
27
- export const SELF_TRAINING_EXECUTION_CAPABILITY = "self_training_execution_v1";
28
27
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
29
28
  resolverReady: true,
30
29
  nativeRuntimeAdaptersReady: true,
@@ -40,7 +39,6 @@ export const DAEMON_CAPABILITIES = Object.freeze([
40
39
  "execution_machine_queue_v1",
41
40
  "execution_agent_memory_policy_v1",
42
41
  "execution_agent_memory_mapping_v1",
43
- SELF_TRAINING_EXECUTION_CAPABILITY,
44
42
  "project_skills_v1",
45
43
  PROJECT_WORKSPACES_CAPABILITY,
46
44
  TRAINING_PROJECT_DEPENDENCIES_CAPABILITY,
@@ -193,10 +191,11 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
193
191
  profileName: dependencies.profileName,
194
192
  daemonHome: dependencies.daemonHome ?? daemonHome(),
195
193
  agentsRoot,
196
- ...(dependencies.profileName.match(/^nw-(?:in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)?.[1] === undefined
194
+ ...(dependencies.profileName.match(/^nw-(?:production|in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)?.[1] === undefined
197
195
  ? {}
198
- : { localInstanceName: dependencies.profileName.match(/^nw-(?:in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)[1] }),
196
+ : { localInstanceName: dependencies.profileName.match(/^nw-(?:production|in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)[1] }),
199
197
  }),
198
+ ...(dependencies.storageIdentity ?? {}),
200
199
  ...(backend.supported ? {
201
200
  executionProtocol: EXECUTION_PROTOCOL,
202
201
  executionLimits: Object.freeze({
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { parseArgs } from "node:util";
9
9
  import { homedir } from "node:os";
10
+ import { join } from "node:path";
10
11
  import { loadConfig, ConfigError } from "./config.js";
11
12
  import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
12
13
  import { cliVersion, daemonVersion } from "./machine-info.js";
@@ -21,7 +22,18 @@ import { formatDaemonStartupError } from "./daemon-startup-error.js";
21
22
  import { assertManagedServiceStartup } from "./managed-service-startup.js";
22
23
  import { daemonGlobalInstallation } from "./daemon-installation.js";
23
24
  import { acquireDaemonInstallationLease, runWithDaemonInstallationLease, } from "./daemon-installation-lease.js";
25
+ import { loadStorageRuntime } from "./storage-runtime.js";
26
+ import { readStorageMigrationReceipt } from "./storage-migration-controller.js";
27
+ import { storageMigrationStatusReplay } from "./storage-status-replay.js";
28
+ import { runStorageWorkerCommand } from "./storage-worker-command.js";
29
+ import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
30
+ import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
24
31
  async function main() {
32
+ const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
33
+ if (workerResult !== null) {
34
+ process.exitCode = workerResult;
35
+ return;
36
+ }
25
37
  const computerResult = await runComputerCommand(process.argv.slice(2));
26
38
  if (computerResult !== null) {
27
39
  process.exitCode = computerResult;
@@ -57,8 +69,10 @@ async function main() {
57
69
  // 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
58
70
  if (values["daemon-home"])
59
71
  process.env.CREW_DAEMON_HOME = values["daemon-home"];
72
+ let activeDaemonHome;
60
73
  if (values.profile) {
61
74
  const home = daemonHome();
75
+ activeDaemonHome = home;
62
76
  const profile = await loadProfile(values.profile, home);
63
77
  if (cmd === "serve") {
64
78
  await assertProfileAgentsRootUnique(profile, home, homedir());
@@ -71,6 +85,12 @@ async function main() {
71
85
  agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
72
86
  nodePath: process.execPath,
73
87
  entryPath: builtDaemonEntry(),
88
+ environment: {
89
+ ...(process.env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: process.env.NOWWORK_HOME }),
90
+ ...(process.env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: process.env.NOWWORK_ENV_TAG }),
91
+ ...(process.env.NOWWORK_STORAGE_POLICY_PATH === undefined
92
+ ? {} : { NOWWORK_STORAGE_POLICY_PATH: process.env.NOWWORK_STORAGE_POLICY_PATH }),
93
+ },
74
94
  });
75
95
  }
76
96
  applyProfileToEnv(profile, process.env);
@@ -100,9 +120,72 @@ async function main() {
100
120
  const installationLease = installation === null
101
121
  ? null
102
122
  : await acquireDaemonInstallationLease(installation.npmPrefix);
123
+ const storageRuntime = values.profile === undefined
124
+ ? null
125
+ : await loadStorageRuntime({
126
+ profile: values.profile,
127
+ serverUrl: config.serverUrl,
128
+ userHome: homedir(),
129
+ platform: process.platform,
130
+ environment: process.env,
131
+ });
132
+ const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
133
+ ? { ready: false, reason: "not_legacy_source" }
134
+ : await probeStorageMigrationRuntime({
135
+ profile: values.profile,
136
+ serverUrl: config.serverUrl,
137
+ currentDaemonHome: activeDaemonHome,
138
+ currentAgentsRoot: config.agentsRoot,
139
+ userHome: homedir(),
140
+ platform: process.platform,
141
+ ...(process.getuid === undefined ? {} : { uid: process.getuid() }),
142
+ environment: process.env,
143
+ });
144
+ const targetConfirmation = storageRuntime?.identity.storageMigrationId === undefined
145
+ ? null
146
+ : createStorageTargetConfirmationHandler({
147
+ backupsRoot: storageRuntime.local.backupsRoot,
148
+ identity: storageRuntime.identity,
149
+ profile: storageRuntime.installation.profile,
150
+ completionTimeoutMs: 180_000,
151
+ });
152
+ const storageMigration = storageRuntime !== null
153
+ ? {
154
+ ready: false,
155
+ statusOutboxPath: join(storageRuntime.local.stateRoot, "storage-status-outbox.json"),
156
+ identity: storageRuntime.identity,
157
+ ...(targetConfirmation === null ? {} : { confirm: targetConfirmation.handle }),
158
+ ...(storageRuntime.identity.storageMigrationId === undefined ? {} : {
159
+ replayStatus: async (send) => {
160
+ const migrationId = storageRuntime.identity.storageMigrationId;
161
+ const receipt = await readStorageMigrationReceipt(storageRuntime.local.backupsRoot, migrationId);
162
+ if (receipt === null)
163
+ throw new Error(`storage migration receipt not found: ${migrationId}`);
164
+ for (const frame of storageMigrationStatusReplay(receipt, {
165
+ migrationId,
166
+ environment: storageRuntime.identity.storageEnvironment,
167
+ workspaceId: storageRuntime.identity.storageWorkspaceId,
168
+ profile: storageRuntime.installation.profile,
169
+ }))
170
+ send(frame);
171
+ },
172
+ }),
173
+ }
174
+ : migrationRuntime.ready
175
+ ? {
176
+ ready: true,
177
+ statusOutboxPath: migrationRuntime.runtime.statusOutboxPath,
178
+ identity: migrationRuntime.runtime.identity,
179
+ controller: migrationRuntime.runtime.controller,
180
+ attach: migrationRuntime.runtime.attach,
181
+ bindServerIdentity: migrationRuntime.runtime.bindServerIdentity,
182
+ replayStatus: migrationRuntime.runtime.replayStatus,
183
+ }
184
+ : undefined;
103
185
  await runWithDaemonInstallationLease(installationLease, async () => {
104
186
  const service = serve(config, {
105
187
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
188
+ ...(storageMigration === undefined ? {} : { storageMigration }),
106
189
  });
107
190
  await runServeLifecycle(service);
108
191
  });
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { acquireDaemonInstallationLease } from "./daemon-installation-lease.js";
3
- import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, uninstallService, } from "./computer-service.js";
3
+ import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, replaceInstalledServiceDescriptor, uninstallService, } from "./computer-service.js";
4
4
  import { ManagedServiceConflictError, ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, managedServiceConflict, normalizeManagedServiceRecord, serviceDescriptorSha256, withManagedServiceRegistryLock, } from "./managed-service-registry.js";
5
5
  function requirePortableManagedSpec(spec) {
6
6
  if ((spec.platform !== "darwin" && spec.platform !== "linux")
@@ -35,6 +35,17 @@ function sameManagedIdentity(left, right) {
35
35
  const { generation: _generation, createdAt: _createdAt, ...expected } = right;
36
36
  return managedServiceIdentityMatches(left, expected);
37
37
  }
38
+ function sameManagedResource(left, right) {
39
+ const { generation: _leftGeneration, createdAt: _leftCreatedAt, descriptorSha256: _leftHash, ...leftResource } = left;
40
+ const { generation: _rightGeneration, createdAt: _rightCreatedAt, descriptorSha256: _rightHash, ...rightResource } = right;
41
+ return JSON.stringify(leftResource) === JSON.stringify(rightResource);
42
+ }
43
+ function descriptorDiffIsStorageEnvironmentOnly(previous, next) {
44
+ if (!next.includes("\n <key>EnvironmentVariables</key>\n"))
45
+ return false;
46
+ const strip = (descriptor) => descriptor.replace(/\n <key>EnvironmentVariables<\/key>\n <dict>\n(?: <key>(?:NOWWORK_HOME|NOWWORK_ENV_TAG|NOWWORK_STORAGE_POLICY_PATH)<\/key><string>[^<]*<\/string>\n)+ <\/dict>\n/u, "\n");
47
+ return strip(previous) === strip(next);
48
+ }
38
49
  function assertRecordDescribesSpec(input) {
39
50
  requirePortableManagedSpec(input.spec);
40
51
  const { spec, record } = input;
@@ -51,8 +62,20 @@ function assertRecordDescribesSpec(input) {
51
62
  }
52
63
  }
53
64
  export async function installManagedService(input) {
54
- assertRecordDescribesSpec(input);
55
65
  requirePortableManagedSpec(input.spec);
66
+ const requestedDescriptor = input.spec.descriptor;
67
+ const expectedDescriptorHash = serviceDescriptorSha256(input.spec.descriptor);
68
+ const expectedPath = normalizeManagedServiceRecord({
69
+ ...input.record,
70
+ descriptorPath: input.spec.descriptorPath,
71
+ descriptorSha256: expectedDescriptorHash,
72
+ }).descriptorPath;
73
+ if (input.record.platform !== input.spec.platform
74
+ || input.record.serviceId !== input.spec.id
75
+ || input.record.profile !== input.spec.profile
76
+ || input.record.descriptorPath !== expectedPath) {
77
+ throw new ManagedServiceRegistryError(`Managed service record does not describe '${input.spec.id}'`);
78
+ }
56
79
  const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
57
80
  await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services, replace }) => {
58
81
  const exact = services.find((record) => sameManagedIdentity(record, input.record));
@@ -76,6 +99,28 @@ export async function installManagedService(input) {
76
99
  }
77
100
  return;
78
101
  }
102
+ const registered = services.find((record) => record.serviceId === input.record.serviceId
103
+ && record.profile === input.record.profile
104
+ && record.daemonHome === input.record.daemonHome);
105
+ if (registered !== undefined && sameManagedResource(registered, input.record)) {
106
+ const current = await readServiceDescriptor(input.spec);
107
+ if (current === null || serviceDescriptorSha256(current) !== registered.descriptorSha256) {
108
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
109
+ }
110
+ if (!descriptorDiffIsStorageEnvironmentOnly(current, requestedDescriptor)) {
111
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' conflicts with the requested identity`);
112
+ }
113
+ await replaceInstalledServiceDescriptor(input.spec, current, input.runner);
114
+ try {
115
+ await replace(services.map((record) => record === registered ? input.record : record));
116
+ }
117
+ catch (error) {
118
+ const rollbackSpec = { ...input.spec, descriptor: current };
119
+ await replaceInstalledServiceDescriptor(rollbackSpec, requestedDescriptor, input.runner).catch(() => undefined);
120
+ throw error;
121
+ }
122
+ return;
123
+ }
79
124
  const conflict = managedServiceConflict(services, input.record);
80
125
  if (conflict !== null) {
81
126
  throw new ManagedServiceConflictError(conflict.resource, input.record, conflict.owner);