@nowcrew/daemon 0.6.88-beta.5 → 0.6.88-beta.7

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 (31) hide show
  1. package/dist/local-memory/candidate.js +43 -0
  2. package/dist/local-memory/pending-match.js +29 -0
  3. package/dist/local-memory/pending-query.js +100 -0
  4. package/dist/local-memory/pending-schema.js +29 -0
  5. package/dist/local-memory/pending-source.js +126 -0
  6. package/dist/local-memory/pending.js +34 -84
  7. package/dist/local-memory/prompt.js +13 -11
  8. package/dist/local-memory/read-cli.js +57 -8
  9. package/dist/local-memory/runtime.js +2 -2
  10. package/dist/local-memory/write.js +16 -48
  11. package/dist/main.js +0 -0
  12. package/dist/project-skills/capability.js +109 -0
  13. package/dist/project-skills/controller-convergence.js +57 -0
  14. package/dist/project-skills/projection-set-switch.js +419 -0
  15. package/dist/project-skills/projection-state-domain.js +170 -0
  16. package/dist/project-skills/projection-state-store.js +842 -0
  17. package/dist/project-skills/projection-state-transaction.js +322 -0
  18. package/dist/project-skills/projection-state.js +3 -0
  19. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  20. package/dist/project-skills/runtime-root-domain.js +268 -0
  21. package/dist/project-skills/runtime-root-gc.js +293 -0
  22. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  23. package/dist/project-skills/runtime-root-leases.js +487 -0
  24. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  25. package/dist/project-skills/runtime-root-startup.js +49 -0
  26. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  27. package/dist/project-skills/runtime-root-state-index.js +356 -0
  28. package/dist/project-skills/runtime-root-store.js +722 -0
  29. package/dist/project-skills/serve-capability.js +28 -0
  30. package/dist/project-skills/serve-startup.js +37 -0
  31. package/package.json +9 -10
@@ -5,6 +5,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { parseArgs, TextDecoder } from "node:util";
7
7
  import { z } from "zod";
8
+ import { readPendingMemoryBody, searchPendingMemoryPage } from "./pending-query.js";
8
9
  const OUTPUT_BYTES = 12 * 1024;
9
10
  const FILE_BYTES = 32 * 1024 * 1024;
10
11
  const FileSchema = z.string().min(1).refine((file) => {
@@ -33,14 +34,33 @@ const CommandSchema = z.discriminatedUnion("command", [
33
34
  offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
34
35
  limit: z.coerce.number().int().min(1).max(4000).default(2000),
35
36
  }).strict(),
37
+ z.object({
38
+ command: z.literal("search-pending"),
39
+ query: z.string().trim().min(1).max(256),
40
+ cursor: z.string().min(1).max(4096).optional(),
41
+ limit: z.coerce.number().int().min(1).max(40).default(10),
42
+ }).strict(),
43
+ z.object({
44
+ command: z.literal("read-pending"),
45
+ id: z.string().min(1).max(250).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
46
+ ifHash: Common.ifHash,
47
+ offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
48
+ limit: z.coerce.number().int().min(1).max(4000).default(2000),
49
+ }).strict(),
36
50
  ]);
37
51
  const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
38
52
  crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
39
53
  crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
40
- 搜索关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
54
+ crew-memory search-pending --query <关键词> [--cursor <游标>] [--limit 10]
55
+ crew-memory read-pending --id <候选ID> [--offset 0] [--limit 2000] [--if-hash <sha256>]
56
+ search 关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
41
57
  读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
42
58
  后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
43
- 搜索结果是原始资料定位,不代表候选已生效;按当前有效索引读取明细,不扫描 pending。`;
59
+ search/read 读取 MEMORY.md 和 notes 原文,不过滤失效段落,也不扫描 pending。
60
+ search-pending 只查因容量延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
61
+ complete=true 才表示全部候选已检索完成且无遗漏;warnings 或非零退出码表示读取未完成,不能当作零命中。
62
+ read-pending 只返回候选正文;offset 非零时必须传 --if-hash,update/supersede 候选不可读取。
63
+ 候选只供相关问题参考,未生效,不能覆盖有效规则;先读取当前有效索引与明细。`;
44
64
  function inside(root, path) {
45
65
  const value = relative(root, path);
46
66
  return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
@@ -152,8 +172,8 @@ function searchSource(text, sha256, command) {
152
172
  nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null,
153
173
  };
154
174
  }
155
- function readPage(text, sha256, command) {
156
- const metadata = { file: command.file, sha256, offset: command.offset, nextOffset: Number.MAX_SAFE_INTEGER, eof: false, content: "" };
175
+ function readPage(text, source, command) {
176
+ const metadata = { ...source, offset: command.offset, nextOffset: Number.MAX_SAFE_INTEGER, eof: false, content: "" };
157
177
  let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify(metadata), "utf8");
158
178
  let position = 0;
159
179
  let offset = 0;
@@ -175,8 +195,16 @@ function readPage(text, sha256, command) {
175
195
  count += 1;
176
196
  }
177
197
  const eof = position === text.length;
198
+ if (!eof && count === 0)
199
+ throw new Error("memory_output_too_large");
178
200
  return { ...metadata, content, eof, nextOffset: eof ? null : offset };
179
201
  }
202
+ function writeResult(result, io) {
203
+ const output = JSON.stringify(result);
204
+ if (Buffer.byteLength(output, "utf8") > OUTPUT_BYTES)
205
+ throw new Error("memory_output_too_large");
206
+ io.out(output);
207
+ }
180
208
  export async function runMemoryReadCommand(argv, io) {
181
209
  let command;
182
210
  try {
@@ -186,6 +214,7 @@ export async function runMemoryReadCommand(argv, io) {
186
214
  query: { type: "string", short: "q" }, file: { type: "string" },
187
215
  limit: { type: "string" }, offset: { type: "string" },
188
216
  "after-line": { type: "string" }, "if-hash": { type: "string" },
217
+ id: { type: "string" }, cursor: { type: "string" },
189
218
  json: { type: "boolean" }, help: { type: "boolean", short: "h" },
190
219
  },
191
220
  });
@@ -201,24 +230,44 @@ export async function runMemoryReadCommand(argv, io) {
201
230
  ...(afterLine === undefined ? {} : { afterLine }),
202
231
  ...(ifHash === undefined ? {} : { ifHash }),
203
232
  });
233
+ if (command.command === "read-pending" && command.offset > 0 && command.ifHash === undefined) {
234
+ throw new Error("memory_hash_required");
235
+ }
204
236
  }
205
237
  catch {
206
- io.err(`memory_arguments_invalid: 参数不合法。\n${USAGE}`);
238
+ io.err(`memory_arguments_invalid: 参数不合法,记忆读取未完成。\n${USAGE}`);
207
239
  return 2;
208
240
  }
209
241
  try {
242
+ if (!io.env.CREW_HOME)
243
+ throw new Error("memory_home_missing");
244
+ if (command.command === "search-pending") {
245
+ const result = await searchPendingMemoryPage({ agentDir: io.env.CREW_HOME, query: command.query,
246
+ ...(command.cursor === undefined ? {} : { cursor: command.cursor }), limit: command.limit });
247
+ writeResult(result, io);
248
+ if (result.warnings.length > 0 || (!result.hasMore && !result.complete)) {
249
+ throw new Error("memory_pending_search_incomplete");
250
+ }
251
+ return 0;
252
+ }
253
+ if (command.command === "read-pending") {
254
+ const { content, ...metadata } = await readPendingMemoryBody({ agentDir: io.env.CREW_HOME,
255
+ pendingId: command.id, ...(command.ifHash === undefined ? {} : { ifHash: command.ifHash }) });
256
+ writeResult(readPage(content, metadata, command), io);
257
+ return 0;
258
+ }
210
259
  const source = await readSource(io.env.CREW_HOME, command.file);
211
260
  if (command.ifHash !== undefined && command.ifHash !== source.sha256)
212
261
  throw new Error("memory_file_changed");
213
262
  const result = command.command === "search"
214
263
  ? searchSource(source.text, source.sha256, command)
215
- : readPage(source.text, source.sha256, command);
216
- io.out(JSON.stringify(result));
264
+ : readPage(source.text, { file: command.file, sha256: source.sha256 }, command);
265
+ writeResult(result, io);
217
266
  return 0;
218
267
  }
219
268
  catch (error) {
220
269
  const code = error.code;
221
- const known = error instanceof Error && /^memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
270
+ const known = error instanceof Error && /^(?:local_)?memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
222
271
  io.err(JSON.stringify({ error: known, message: "记忆读取未完成;检查路径、文件版本或大小,不要将此错误当作没有记忆。" }));
223
272
  return 1;
224
273
  }
@@ -69,8 +69,8 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
69
69
  \`\`\`
70
70
 
71
71
  每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
72
- 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
73
- 写入候选文件不等于已经记住,只有 daemon 校验并提交成功后记忆才生效。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在提交成功后清理,失败时保留日志和候选用于排查或重试。
72
+ 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。
73
+ 写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
74
74
  `;
75
75
  }
76
76
  /** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
@@ -2,29 +2,12 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { z } from "zod";
5
- import { containsLikelySecret } from "../agent-memory/policy.js";
5
+ import { dslog } from "../slog.js";
6
+ import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
6
7
  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
8
  import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } 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
9
  const StatusChangeSchema = z.object({
27
- memoryId: SingleLineText,
10
+ memoryId: LocalMemorySingleLineText,
28
11
  expectedRevision: z.number().int().positive(),
29
12
  expectedStatus: z.enum(["candidate", "active", "pinned", "stale", "superseded", "archived"]),
30
13
  targetStatus: z.enum(["candidate", "active", "pinned", "stale", "archived"]),
@@ -35,7 +18,7 @@ const ResultSchema = z.object({
35
18
  purpose: z.literal("memory_prune"),
36
19
  baseCatalogRevision: z.number().int().nonnegative(),
37
20
  outcome: z.enum(["no_change", "proposed", "conflict", "failed"]),
38
- changes: z.array(ChangeSchema).max(50),
21
+ changes: z.array(LocalMemoryChangeSchema).max(50),
39
22
  statusChanges: z.array(StatusChangeSchema).max(50),
40
23
  }).strict();
41
24
  const GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
@@ -43,8 +26,6 @@ const INDEX_HEADING = /^##\s+Knowledge Index\s*$/mu;
43
26
  const NEXT_SECTION = /^##\s+(?!#)/mu;
44
27
  const CARD_HEADING = /^###\s+(.+?)\s*$/gmu;
45
28
  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
29
  function codePointLength(value) {
49
30
  return Array.from(value).length;
50
31
  }
@@ -148,34 +129,12 @@ function replaceNote(content, id, replacement) {
148
129
  const pattern = notePattern(id);
149
130
  return pattern.test(content) ? content.replace(pattern, replacement) : null;
150
131
  }
151
- function containsReservedText(change) {
152
- return RESERVED_MEMORY_MARKER.test([change.title, change.summary, ...change.triggers, change.detailBody].join("\n"));
153
- }
154
132
  function validateCandidate(result) {
155
133
  if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
156
134
  throw new Error("local_memory_no_change_has_operations");
157
135
  }
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
- }
136
+ for (const change of result.changes)
137
+ validateLocalMemoryChange(change);
179
138
  }
180
139
  export function parseLocalMemoryResult(raw) {
181
140
  let value;
@@ -477,7 +436,16 @@ export async function applyLocalMemoryResult(input) {
477
436
  // Pending create candidates are durable overflow, not dead ends. Promote only
478
437
  // when the current catalog has room; supersede/update candidates stay pending
479
438
  // until a fresh revision-aware decision can safely replay them.
480
- const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir });
439
+ const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
440
+ if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
441
+ throw error;
442
+ // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
443
+ dslog("local_memory.pending_promotion_skipped", "pending目录超出扫描预算,本轮跳过自动提升", {
444
+ level: "WARN", execution_id: input.executionId, prune_trace_id: input.pruneTraceId,
445
+ reason: "memory_pending_directory_over_budget",
446
+ });
447
+ return [];
448
+ });
481
449
  for (const pending of pendingCandidatesOnDisk) {
482
450
  const change = pending.candidate;
483
451
  if (change.operation !== "create")
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
+ }