@nowcrew/daemon 0.6.87-beta.2 → 0.6.87-beta.3

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.
@@ -25,6 +25,7 @@ import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
25
25
  import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.js";
26
26
  import { dslog } from "./slog.js";
27
27
  import { identityRosterPromptFacts } from "./identity-roster-telemetry.js";
28
+ import { renderLocalMemoryV2WriteGuard } from "./local-memory/runtime.js";
28
29
  export { supervisorLaunch } from "./supervised-runtime.js";
29
30
  const MAX_LOCAL_EXECUTION_ATTEMPTS = 3;
30
31
  const ACTIVITY_KIND = {
@@ -828,7 +829,8 @@ export async function runExecution(config, input, dependencies) {
828
829
  const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
829
830
  ? systemPromptBudget
830
831
  : Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
831
- const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget, {
832
+ const localMemoryV2Enabled = config.localMemoryV2Handles?.has(spec.agent.handle) === true;
833
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${localMemoryV2Enabled ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
832
834
  ...(selfTraining ? { isolateLocalMemory: true } : {}),
833
835
  collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
834
836
  });
@@ -887,7 +889,7 @@ export async function runExecution(config, input, dependencies) {
887
889
  maxTurns: config.sessionMaxTurns,
888
890
  },
889
891
  ...(selfTraining ? { maintenancePurpose: "self_training" } : {}),
890
- localMemoryV2Enabled: config.localMemoryV2Handles?.has(spec.agent.handle) === true,
892
+ localMemoryV2Enabled,
891
893
  };
892
894
  const result = await Promise.race([
893
895
  execute(localInput, callbacks, localDependencies),
@@ -37,6 +37,7 @@ import { formatRuntimeStartupError } from "./runtime-startup-error.js";
37
37
  import { machinePackageManagerEnv, packageManagerEnvFacts } from "./package-manager-env.js";
38
38
  import { sanitizeEnvVars } from "./runtime-env.js";
39
39
  import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
40
+ import { renderPendingMemoryContext, searchPendingMemoryCandidates, } from "./local-memory/pending.js";
40
41
  export class RuntimeRunningCallbackTimeoutError extends Error {
41
42
  constructor(runtime, timeoutMs) {
42
43
  super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
@@ -381,6 +382,26 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
381
382
  }
382
383
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
383
384
  const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
385
+ let pendingMemoryContext = "";
386
+ if (input.localMemoryV2Enabled === true) {
387
+ try {
388
+ const pendingCandidates = await searchPendingMemoryCandidates({
389
+ agentDir: executionWorkspace.dir,
390
+ query: wakePrompt,
391
+ limit: 5,
392
+ });
393
+ pendingMemoryContext = renderPendingMemoryContext(pendingCandidates);
394
+ }
395
+ catch (error) {
396
+ dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
397
+ level: "WARN",
398
+ execution_id: input.executionId,
399
+ agent_handle: input.handle,
400
+ error_name: error instanceof Error ? error.name : "unknown",
401
+ error_code: error?.code,
402
+ });
403
+ }
404
+ }
384
405
  memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
385
406
  if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
386
407
  && isLocalMemoryV2Eligible({
@@ -392,14 +413,21 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
392
413
  })) {
393
414
  localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
394
415
  }
395
- const baseSystemPrompt = resolvePrompt(input.systemPrompt, promptContext);
416
+ const promptWithoutPending = resolvePrompt(input.systemPrompt, promptContext);
417
+ let baseSystemPrompt = `${promptWithoutPending}${pendingMemoryContext}`;
396
418
  const structuredMemoryInstructions = localMemoryResultPath === null
397
419
  ? null
398
420
  : renderLocalMemoryResultInstructions(localMemoryResultPath);
399
- const structuredPromptFits = structuredMemoryInstructions === null
421
+ const fitsPromptBudget = (prompt) => structuredMemoryInstructions === null
400
422
  || input.maxSystemPromptBytes === undefined
401
- || Buffer.byteLength(baseSystemPrompt, "utf8") + Buffer.byteLength(structuredMemoryInstructions, "utf8")
423
+ || Buffer.byteLength(prompt, "utf8") + Buffer.byteLength(structuredMemoryInstructions, "utf8")
402
424
  <= input.maxSystemPromptBytes;
425
+ // The structured prune contract is mandatory for V2. Drop optional archived
426
+ // recall before dropping the daemon result file when the prompt budget is tight.
427
+ if (!fitsPromptBudget(baseSystemPrompt) && pendingMemoryContext !== "") {
428
+ baseSystemPrompt = promptWithoutPending;
429
+ }
430
+ const structuredPromptFits = fitsPromptBudget(baseSystemPrompt);
403
431
  if (!structuredPromptFits)
404
432
  localMemoryResultPath = null;
405
433
  const systemPrompt = localMemoryResultPath === null
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import { lstat, mkdir, readdir, realpath, rename, rm, writeFile, readFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { z } from "zod";
5
+ export const LOCAL_MEMORY_PENDING_SCAN_FILE_CAP = 128;
6
+ export const LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP = 1_000_000;
5
7
  const CandidateSchema = z.object({
6
8
  operation: z.enum(["create", "update", "supersede"]),
7
9
  memoryId: z.string().nullable(),
@@ -59,6 +61,44 @@ async function pendingDirectory(agentDir) {
59
61
  }
60
62
  return directory;
61
63
  }
64
+ async function existingPendingDirectory(agentDir) {
65
+ const agentRoot = await realpath(agentDir);
66
+ const memoryRoot = resolve(agentRoot, ".crew", "memory");
67
+ let memoryStat;
68
+ try {
69
+ memoryStat = await lstat(memoryRoot);
70
+ }
71
+ catch (error) {
72
+ if (error.code === "ENOENT")
73
+ return null;
74
+ throw error;
75
+ }
76
+ if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {
77
+ throw new Error("local_memory_pending_path_invalid");
78
+ }
79
+ const resolvedMemoryRoot = await realpath(memoryRoot);
80
+ if (resolvedMemoryRoot !== memoryRoot || !within(agentRoot, resolvedMemoryRoot)) {
81
+ throw new Error("local_memory_pending_path_invalid");
82
+ }
83
+ const directory = join(memoryRoot, "pending");
84
+ let stat;
85
+ try {
86
+ stat = await lstat(directory);
87
+ }
88
+ catch (error) {
89
+ if (error.code === "ENOENT")
90
+ return null;
91
+ throw error;
92
+ }
93
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
94
+ throw new Error("local_memory_pending_path_invalid");
95
+ }
96
+ const resolvedDirectory = await realpath(directory);
97
+ if (resolvedDirectory !== directory || !within(agentRoot, resolvedDirectory)) {
98
+ throw new Error("local_memory_pending_path_invalid");
99
+ }
100
+ return directory;
101
+ }
62
102
  async function assertRegularFile(path) {
63
103
  try {
64
104
  const stat = await lstat(path);
@@ -91,15 +131,40 @@ export async function persistPendingMemoryCandidate(input) {
91
131
  await atomicWrite(target, `${JSON.stringify(parsed.data, null, 2)}\n`);
92
132
  return target;
93
133
  }
134
+ export async function removePendingMemoryCandidate(input) {
135
+ const directory = await existingPendingDirectory(input.agentDir);
136
+ if (directory === null)
137
+ return;
138
+ const target = join(directory, `${input.pendingId}.json`);
139
+ await assertRegularFile(target);
140
+ await rm(target, { force: true });
141
+ }
94
142
  export async function readPendingMemoryCandidates(input) {
95
- const directory = await pendingDirectory(input.agentDir);
143
+ const directory = await existingPendingDirectory(input.agentDir);
144
+ if (directory === null)
145
+ return [];
96
146
  const names = (await readdir(directory)).filter((name) => name.endsWith(".json")).sort();
147
+ const maxFiles = Math.max(0, input.maxFiles ?? LOCAL_MEMORY_PENDING_SCAN_FILE_CAP);
148
+ const maxBytes = Math.max(0, input.maxBytes ?? LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP);
97
149
  const result = [];
150
+ let scannedBytes = 0;
98
151
  for (const name of names) {
152
+ if (result.length >= maxFiles || scannedBytes >= maxBytes)
153
+ break;
99
154
  const path = join(directory, name);
100
155
  try {
101
156
  await assertRegularFile(path);
102
- const parsed = PendingSchema.safeParse(JSON.parse(await readFile(path, "utf8")));
157
+ const stat = await lstat(path);
158
+ if (!stat.isFile() || stat.isSymbolicLink())
159
+ throw new Error("local_memory_pending_path_invalid");
160
+ if (scannedBytes + stat.size > maxBytes)
161
+ break;
162
+ const raw = await readFile(path, "utf8");
163
+ const size = Buffer.byteLength(raw, "utf8");
164
+ if (scannedBytes + size > maxBytes)
165
+ break;
166
+ scannedBytes += size;
167
+ const parsed = PendingSchema.safeParse(JSON.parse(raw));
103
168
  if (parsed.success && `${parsed.data.pendingId}.json` === name)
104
169
  result.push(parsed.data);
105
170
  }
@@ -109,3 +174,72 @@ export async function readPendingMemoryCandidates(input) {
109
174
  }
110
175
  return result;
111
176
  }
177
+ function normalize(value) {
178
+ return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
179
+ }
180
+ function priorityRank(priority) {
181
+ return priority === "high" ? 0 : priority === "medium" ? 1 : 2;
182
+ }
183
+ function candidateText(candidate) {
184
+ return [
185
+ candidate.candidate.title,
186
+ candidate.candidate.summary,
187
+ ...candidate.candidate.triggers,
188
+ candidate.candidate.detailCategory,
189
+ candidate.candidate.detailBody,
190
+ ].join("\n");
191
+ }
192
+ function recallScore(candidate, query) {
193
+ const normalizedQuery = normalize(query);
194
+ if (normalizedQuery === "")
195
+ return 1;
196
+ const text = normalize(candidateText(candidate));
197
+ if (text.includes(normalizedQuery))
198
+ return 100;
199
+ const terms = normalizedQuery.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter((term) => term.length > 0);
200
+ return terms.reduce((score, term) => score + (text.includes(term) ? 1 : 0), 0);
201
+ }
202
+ export async function searchPendingMemoryCandidates(input) {
203
+ const candidates = await readPendingMemoryCandidates({ agentDir: input.agentDir });
204
+ const limit = Math.max(0, input.limit ?? 5);
205
+ if (limit === 0)
206
+ return [];
207
+ return candidates
208
+ .map((candidate) => ({ candidate, score: recallScore(candidate, input.query) }))
209
+ .filter((item) => normalize(input.query) === "" || item.score > 0)
210
+ .sort((left, right) => right.score - left.score
211
+ || priorityRank(left.candidate.candidate.priority) - priorityRank(right.candidate.candidate.priority)
212
+ || right.candidate.createdAt.localeCompare(left.candidate.createdAt)
213
+ || left.candidate.pendingId.localeCompare(right.candidate.pendingId))
214
+ .slice(0, limit)
215
+ .map((item) => item.candidate);
216
+ }
217
+ function truncateCodePoints(value, maxCodePoints) {
218
+ return Array.from(value).slice(0, Math.max(0, maxCodePoints)).join("");
219
+ }
220
+ export function renderPendingMemoryContext(candidates, maxCodePoints = 2_000) {
221
+ if (candidates.length === 0 || maxCodePoints <= 0)
222
+ return "";
223
+ const header = `\n\n## Archived memory candidates (daemon-managed)\n这些记忆已持久化,但尚未进入常驻热索引;只在当前请求相关时参考,必要时再由 memory-prune 提升。\n`;
224
+ let remaining = maxCodePoints - Array.from(header).length;
225
+ if (remaining <= 0)
226
+ return truncateCodePoints(header, maxCodePoints);
227
+ const sections = [];
228
+ for (const item of candidates) {
229
+ const change = item.candidate;
230
+ const prefix = `\n### ${change.title}\n`
231
+ + `- pending_file: .crew/memory/pending/${item.pendingId}.json\n`
232
+ + `- summary: ${change.summary}\n`
233
+ + `- triggers: ${change.triggers.join(", ")}\n`
234
+ + "- detail: ";
235
+ const prefixLength = Array.from(prefix).length;
236
+ if (prefixLength + 1 > remaining)
237
+ break;
238
+ const detail = truncateCodePoints(change.detailBody, remaining - prefixLength - 1);
239
+ const section = `${prefix}${detail}\n`;
240
+ const length = Array.from(section).length;
241
+ sections.push(section);
242
+ remaining -= length;
243
+ }
244
+ return `${header}${sections.join("")}`;
245
+ }
@@ -57,9 +57,16 @@ ${resultPath}
57
57
  JSON 顶层必须包含:
58
58
  {"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"no_change","changes":[],"statusChanges":[]}
59
59
  每个 changes 项使用 operation=create|update|supersede、title、kind、summary、triggers、priority、detailCategory、detailBody、reviewAfter;create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null,update/supersede 按现有索引填写版本和 id。
60
- 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 中等待后续治理。
60
+ 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
61
61
  `;
62
62
  }
63
+ /** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
64
+ export function renderLocalMemoryV2WriteGuard() {
65
+ return `
66
+
67
+ ## V2 shared memory write safety
68
+ 本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
69
+ }
63
70
  export async function commitLocalMemoryResultAfterExit(input) {
64
71
  if (input.exitCode !== 0)
65
72
  return { status: "skipped" };
@@ -4,7 +4,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { containsLikelySecret } from "../agent-memory/policy.js";
6
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";
7
+ import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
8
8
  const CategorySchema = z.enum([
9
9
  "preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
10
10
  ]);
@@ -228,6 +228,47 @@ function normalize(value) {
228
228
  function activeOrPinnedIds(entries) {
229
229
  return new Set(entries.filter((entry) => entry.status === "active" || entry.status === "pinned").map((entry) => entry.id));
230
230
  }
231
+ function pinnedIds(entries) {
232
+ return new Set(entries.filter((entry) => entry.status === "pinned").map((entry) => entry.id));
233
+ }
234
+ const DETAIL_CATEGORIES = new Set([
235
+ "preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
236
+ ]);
237
+ function detailCategoryFromFile(file) {
238
+ if (!file.startsWith("notes/") || !file.endsWith(".md"))
239
+ return null;
240
+ const category = file.slice("notes/".length, -".md".length);
241
+ return DETAIL_CATEGORIES.has(category) ? category : null;
242
+ }
243
+ function readNoteBody(content, id) {
244
+ const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
245
+ if (begin < 0)
246
+ return null;
247
+ const bodyStart = content.indexOf("\n\n", begin);
248
+ const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
249
+ if (bodyStart < 0 || bodyEnd < 0)
250
+ return null;
251
+ return content.slice(bodyStart + 2, bodyEnd).trim();
252
+ }
253
+ function archivedChange(entry, detailBody) {
254
+ const detailCategory = detailCategoryFromFile(entry.file);
255
+ if (detailCategory === null)
256
+ return null;
257
+ return {
258
+ operation: "create",
259
+ memoryId: null,
260
+ baseRevision: null,
261
+ supersedesMemoryId: null,
262
+ title: entry.title,
263
+ kind: entry.type,
264
+ summary: entry.summary,
265
+ triggers: entry.triggers,
266
+ priority: entry.priority,
267
+ detailCategory,
268
+ detailBody,
269
+ reviewAfter: entry.reviewAfter,
270
+ };
271
+ }
231
272
  function emptyResult() {
232
273
  return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
233
274
  }
@@ -341,7 +382,9 @@ export async function applyLocalMemoryResult(input) {
341
382
  const statusChangedIds = input.result.statusChanges.map((change) => change.memoryId);
342
383
  const deferredIds = [];
343
384
  const pendingCandidates = [];
385
+ const resolvedPendingIds = new Set();
344
386
  const noteUpdates = new Map();
387
+ let allowActiveEviction = false;
345
388
  const randomId = input.randomId ?? randomUUID;
346
389
  const executionId = input.executionId ?? "unknown";
347
390
  const defer = (change, index) => {
@@ -399,7 +442,10 @@ export async function applyLocalMemoryResult(input) {
399
442
  : item),
400
443
  entry,
401
444
  ];
402
- const protectedIds = new Set(activeOrPinnedIds(entries));
445
+ const allowEvictionForChange = change.operation === "create" && change.priority === "high";
446
+ const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
447
+ if (allowEvictionForChange)
448
+ allowActiveEviction = true;
403
449
  if (change.supersedesMemoryId !== null)
404
450
  protectedIds.delete(change.supersedesMemoryId);
405
451
  const projection = projectIndex(memory, candidateEntries, protectedIds);
@@ -428,15 +474,91 @@ export async function applyLocalMemoryResult(input) {
428
474
  else if (existingCreate === undefined)
429
475
  createdIds.push(id);
430
476
  }
431
- const finalProjection = projectIndex(memory, entries, activeOrPinnedIds(entries));
432
- if (finalProjection.reason !== null)
477
+ // Pending create candidates are durable overflow, not dead ends. Promote only
478
+ // when the current catalog has room; supersede/update candidates stay pending
479
+ // until a fresh revision-aware decision can safely replay them.
480
+ const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir });
481
+ for (const pending of pendingCandidatesOnDisk) {
482
+ const change = pending.candidate;
483
+ if (change.operation !== "create")
484
+ continue;
485
+ const id = stableId(`pending:${pending.pendingId}`, 0, change);
486
+ const existing = entries.find((entry) => entry.id === id);
487
+ if (existing !== undefined) {
488
+ resolvedPendingIds.add(pending.pendingId);
489
+ continue;
490
+ }
491
+ const duplicate = entries.find((entry) => (entry.status === "active" || entry.status === "pinned")
492
+ && (normalize(entry.title) === normalize(change.title)
493
+ || normalize(entry.summary) === normalize(change.summary)));
494
+ if (duplicate !== undefined) {
495
+ const duplicatePath = resolve(input.agentDir, duplicate.file);
496
+ const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
497
+ if (duplicateBody === change.detailBody)
498
+ resolvedPendingIds.add(pending.pendingId);
499
+ continue;
500
+ }
501
+ const entry = entryFromChange(change, id, 1, "active");
502
+ const candidateEntries = [...entries, entry];
503
+ const allowEvictionForPending = change.priority === "high";
504
+ const projection = projectIndex(memory, candidateEntries, allowEvictionForPending ? pinnedIds(entries) : activeOrPinnedIds(entries));
505
+ if (allowEvictionForPending)
506
+ allowActiveEviction = true;
507
+ if (projection.omittedIds.includes(id))
508
+ continue;
509
+ const notePath = resolve(input.agentDir, entry.file);
510
+ if (!isWithin(resolve(input.agentDir, "notes"), notePath))
511
+ throw new Error("local_memory_note_path_invalid");
512
+ const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
513
+ const replacement = noteSection(id, 1, change.title, change.detailBody);
514
+ const existingSection = replaceNote(previous, id, replacement);
515
+ const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
516
+ noteUpdates.set(notePath, next);
517
+ entries = candidateEntries;
518
+ createdIds.push(id);
519
+ resolvedPendingIds.add(pending.pendingId);
520
+ }
521
+ let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
522
+ if (finalProjection.reason !== null
523
+ && (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
433
524
  throw new Error(finalProjection.reason);
525
+ }
526
+ if (allowActiveEviction) {
527
+ const omittedActiveEntries = finalProjection.omittedIds
528
+ .map((id) => entries.find((entry) => entry.id === id))
529
+ .filter((entry) => entry?.status === "active");
530
+ for (const entry of omittedActiveEntries) {
531
+ const notePath = resolve(input.agentDir, entry.file);
532
+ const content = noteUpdates.get(notePath) ?? await readNote(notePath);
533
+ const detailBody = readNoteBody(content, entry.id);
534
+ const change = detailBody === null ? null : archivedChange(entry, detailBody);
535
+ if (change === null)
536
+ throw new Error("local_memory_note_anchor_missing");
537
+ const pendingId = `pending-${stableId(`evicted:${executionId}`, 0, change).slice("mem_".length)}`;
538
+ pendingCandidates.push({
539
+ schemaVersion: 1,
540
+ pendingId,
541
+ createdAt: new Date().toISOString(),
542
+ reason: "index_over_budget",
543
+ source: { executionId, pruneTraceId: input.pruneTraceId ?? null },
544
+ candidate: change,
545
+ });
546
+ deferredIds.push(pendingId);
547
+ entries = entries.map((item) => item.id === entry.id ? { ...item, status: "archived" } : item);
548
+ }
549
+ finalProjection = projectIndex(memory, entries, pinnedIds(entries));
550
+ if (finalProjection.reason !== null)
551
+ throw new Error(finalProjection.reason);
552
+ }
434
553
  for (const candidate of pendingCandidates)
435
554
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
436
555
  for (const [path, content] of noteUpdates)
437
556
  await atomicWrite(path, content);
438
557
  if (noteUpdates.size > 0 || statusChangedIds.length > 0)
439
558
  await atomicWrite(memoryPath, finalProjection.memory);
559
+ for (const pendingId of resolvedPendingIds) {
560
+ await removePendingMemoryCandidate({ agentDir: input.agentDir, pendingId });
561
+ }
440
562
  return {
441
563
  createdIds,
442
564
  updatedIds,
package/dist/main.js CHANGED
File without changes
@@ -0,0 +1,109 @@
1
+ import { PROJECT_SKILLS_CAPABILITY, PROJECT_SKILL_PROJECTION_V2_CAPABILITY, } from "./types.js";
2
+ export function projectSkillV2RolloutReady(platform, readiness) {
3
+ const platformReady = platform === "darwin"
4
+ || platform === "linux"
5
+ || (platform === "win32" && readiness.nativeWindowsSmokePassed);
6
+ return platformReady
7
+ && readiness.runtimeRootStorePrimitiveReady
8
+ && readiness.runtimeRootStoreRecoveryReady
9
+ && readiness.leaseRecoveryReady
10
+ && readiness.controllerReady
11
+ && readiness.executionHandlerReady
12
+ && readiness.codexAdapterReady
13
+ && readiness.claudeAdapterReady;
14
+ }
15
+ export function effectiveProjectSkillCapabilities(capabilities, projectSkillsReady, projectSkillV2Ready) {
16
+ const projectSkillCapabilitiesReady = projectSkillsReady && projectSkillV2Ready;
17
+ if (!projectSkillCapabilitiesReady
18
+ && !capabilities.includes(PROJECT_SKILLS_CAPABILITY)
19
+ && !capabilities.includes(PROJECT_SKILL_PROJECTION_V2_CAPABILITY)
20
+ && Object.isFrozen(capabilities))
21
+ return capabilities;
22
+ const filtered = capabilities.filter((capability) => capability !== PROJECT_SKILL_PROJECTION_V2_CAPABILITY
23
+ && (projectSkillCapabilitiesReady || capability !== PROJECT_SKILLS_CAPABILITY));
24
+ if (!projectSkillCapabilitiesReady || !filtered.includes(PROJECT_SKILLS_CAPABILITY)) {
25
+ return Object.freeze(filtered);
26
+ }
27
+ const v1Index = filtered.indexOf(PROJECT_SKILLS_CAPABILITY);
28
+ return Object.freeze([
29
+ ...filtered.slice(0, v1Index + 1),
30
+ PROJECT_SKILL_PROJECTION_V2_CAPABILITY,
31
+ ...filtered.slice(v1Index + 1),
32
+ ]);
33
+ }
34
+ export function withProjectSkillCapabilityStatus(value, status) {
35
+ return {
36
+ ...value,
37
+ projectSkillsStatus: status,
38
+ capabilities: status === "ready"
39
+ ? value.capabilities
40
+ : effectiveProjectSkillCapabilities(value.capabilities, false, false),
41
+ };
42
+ }
43
+ const settleReadinessGate = async (operation) => {
44
+ try {
45
+ return await operation() === true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ };
51
+ export async function createProjectSkillConnectionCapabilitySnapshot(input) {
52
+ const runtimeRootStorePrimitiveReady = await settleReadinessGate(input.probeRuntimeRootStorePrimitive);
53
+ const controllerReady = await settleReadinessGate(input.initializeController);
54
+ const recoveryReady = await settleReadinessGate(input.recoverRuntimeRootsAndLeases);
55
+ const readiness = Object.freeze({
56
+ runtimeRootStorePrimitiveReady,
57
+ runtimeRootStoreRecoveryReady: recoveryReady,
58
+ leaseRecoveryReady: recoveryReady,
59
+ controllerReady,
60
+ executionHandlerReady: true,
61
+ codexAdapterReady: true,
62
+ claudeAdapterReady: true,
63
+ nativeWindowsSmokePassed: false,
64
+ });
65
+ const projectSkillsReady = projectSkillV2RolloutReady(input.platform, readiness);
66
+ return Object.freeze({
67
+ capabilities: effectiveProjectSkillCapabilities(input.baseCapabilities, projectSkillsReady, projectSkillsReady),
68
+ projectSkillsReady,
69
+ readiness,
70
+ });
71
+ }
72
+ export async function prepareProjectSkillConnectionCapabilities(input) {
73
+ const snapshot = await createProjectSkillConnectionCapabilitySnapshot({
74
+ ...input,
75
+ baseCapabilities: [...input.baseCapabilities, ...await input.additionalCapabilities()],
76
+ });
77
+ input.applyReadiness(snapshot.projectSkillsReady);
78
+ return snapshot.capabilities;
79
+ }
80
+ export function createProjectSkillControllerReadiness(input) {
81
+ let status = "initializing";
82
+ let initialization = null;
83
+ const ensure = () => {
84
+ if (status === "ready")
85
+ return Promise.resolve(true);
86
+ if (initialization !== null)
87
+ return initialization;
88
+ status = "initializing";
89
+ const attempt = input.initialize().then(() => {
90
+ status = "ready";
91
+ return true;
92
+ }, (error) => {
93
+ status = "unavailable";
94
+ input.onUnavailable(error);
95
+ return false;
96
+ }).finally(() => {
97
+ if (initialization === attempt)
98
+ initialization = null;
99
+ input.onSettled();
100
+ });
101
+ initialization = attempt;
102
+ return attempt;
103
+ };
104
+ return Object.freeze({
105
+ status: () => status,
106
+ ensure,
107
+ applyPreflight: (ready) => { status = ready ? "ready" : "unavailable"; },
108
+ });
109
+ }
@@ -0,0 +1,57 @@
1
+ import { ProjectProjectionError, } from "./reconciler.js";
2
+ export const MAX_REMEMBERED_PROJECT_SKILL_AGENTS = 64;
3
+ const compareBinary = (left, right) => {
4
+ if (left < right)
5
+ return -1;
6
+ if (left > right)
7
+ return 1;
8
+ return 0;
9
+ };
10
+ const copySnapshot = (snapshot) => Object.freeze({
11
+ bindings: Object.freeze(snapshot.bindings.map((binding) => Object.freeze({ ...binding }))),
12
+ generation: snapshot.generation,
13
+ });
14
+ export function createProjectSkillSnapshotEpoch(maxEntries = MAX_REMEMBERED_PROJECT_SKILL_AGENTS) {
15
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
16
+ throw new ProjectProjectionError("skill_projection_failed");
17
+ }
18
+ const snapshots = new Map();
19
+ return Object.freeze({
20
+ begin() {
21
+ snapshots.clear();
22
+ },
23
+ remember(handle, snapshot) {
24
+ const previous = snapshots.get(handle);
25
+ if (previous !== undefined && snapshot.generation < previous.generation)
26
+ return;
27
+ if (previous === undefined && snapshots.size >= maxEntries) {
28
+ throw new ProjectProjectionError("skill_projection_failed");
29
+ }
30
+ snapshots.set(handle, copySnapshot(snapshot));
31
+ },
32
+ forget(handle) {
33
+ snapshots.delete(handle);
34
+ },
35
+ entries() {
36
+ return Object.freeze([...snapshots.entries()]
37
+ .sort(([left], [right]) => compareBinary(left, right))
38
+ .map(([handle, snapshot]) => Object.freeze([handle, copySnapshot(snapshot)])));
39
+ },
40
+ });
41
+ }
42
+ const stableErrorCode = (reason) => reason?.code === "skill_projection_snapshot_corrupt"
43
+ ? "skill_projection_snapshot_corrupt"
44
+ : "skill_projection_failed";
45
+ export function toPathPrivateConvergenceResults(entries, results) {
46
+ return Object.freeze(entries.map(([handle], index) => {
47
+ const result = results[index];
48
+ if (result?.status === "fulfilled") {
49
+ return Object.freeze({ handle, status: "fulfilled" });
50
+ }
51
+ return Object.freeze({
52
+ handle,
53
+ status: "rejected",
54
+ errorCode: stableErrorCode(result?.reason),
55
+ });
56
+ }));
57
+ }