@nowcrew/daemon 0.6.91-beta.7 → 0.6.91-beta.9
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/computer-main.js +0 -0
- package/dist/console.js +5 -4
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +3 -1
- package/dist/i18n.js +4 -3
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +14 -35
- package/dist/local-memory/execution-workspace.js +36 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/migration-access.js +118 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-plan.js +278 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-store.js +424 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/prompt.js +20 -4
- package/dist/local-memory/read-cli.js +36 -1
- package/dist/local-memory/rollout.js +53 -8
- package/dist/local-memory/runtime.js +5 -2
- package/dist/local-memory/storage-format.js +152 -0
- package/dist/local-memory/write.js +108 -17
- package/dist/machine-info.js +3 -0
- package/dist/main.js +0 -0
- package/dist/memory-migration-controller.js +84 -0
- package/dist/memory-migration-inspect.js +74 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +305 -0
- package/dist/message-file-preview.js +11 -1
- package/dist/prompt.js +5 -1
- package/dist/runner.js +1 -1
- package/dist/runtime-startup-gate.js +1 -1
- package/dist/serve.js +36 -3
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- package/package.json +8 -9
|
@@ -7,30 +7,57 @@ import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.j
|
|
|
7
7
|
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
8
8
|
import { parseLocalMemoryIndex } from "./write.js";
|
|
9
9
|
import { dslog } from "../slog.js";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
|
|
12
|
+
import { MigrationStateSchema } from "./migration-schema.js";
|
|
10
13
|
export const LOCAL_MEMORY_V2_DEFERRED_GUIDANCE = `
|
|
11
14
|
|
|
12
15
|
## 本轮本地记忆兼容状态
|
|
13
16
|
开发环境已开放 V2,但该 Agent 的旧记忆尚未通过格式兼容检查,本轮未启用结构化 V2。
|
|
14
17
|
本节覆盖前文和旧会话中直接改写共享记忆的要求:本轮 MEMORY.md 与 notes 只读,不得改写、迁移或清空。
|
|
15
18
|
用户要求新增或更新长期规则时,内容和授权均只记录到本任务的 $CREW_TASK_LOG;保留日志,不要清空,不要宣称长期记忆已生效。旧资料兼容处理完成前,记忆收尾会明确报告需要处理。`;
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
export const LOCAL_MEMORY_LEGACY_GUIDANCE = `
|
|
20
|
+
|
|
21
|
+
## 本轮本地记忆兼容状态
|
|
22
|
+
本 Agent 尚未完成 V2 格式转换,本轮继续原记忆流程。用户明确要求新增或修改长期规则时,可按原规则最小范围更新共享记忆;记忆收尾继续依据任务日志更新原 MEMORY.md 和 notes,确认成功后再清理日志。
|
|
23
|
+
本节覆盖旧会话中因等待 V2 转换而冻结共享记忆写入的要求,但不是新的用户写入授权;本任务如明确要求不更新记忆,继续遵守。
|
|
24
|
+
保留原身份、目标及无关资料,不自行迁移或重置文件,不宣称已启用 V2。`;
|
|
25
|
+
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune, legacyFallback = false) {
|
|
26
|
+
dslog("local_memory.v2_deferred", legacyFallback ? "旧格式继续原记忆流程,转换成功后再启用V2" : "记忆格式异常,保留原资料和待处理日志", {
|
|
27
|
+
execution_id: executionId, agent_handle: handle, reason: legacyFallback ? "legacy_compatible" : "requires_migration"
|
|
19
28
|
});
|
|
20
|
-
if (memoryPrune)
|
|
29
|
+
if (memoryPrune && !legacyFallback)
|
|
21
30
|
throw new Error("local_memory_v2_requires_migration");
|
|
22
31
|
}
|
|
23
32
|
/** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
|
|
24
33
|
export async function resolveLocalMemoryV2Rollout(input) {
|
|
25
34
|
const environment = localMemoryV2Environment(input.serverUrl);
|
|
26
|
-
if (input.selfTraining
|
|
35
|
+
if (input.selfTraining)
|
|
36
|
+
return { enabled: false, deferred: false };
|
|
37
|
+
// 已提交的目标格式独立于新的迁移资格;关闭名单不能退回不兼容writer。
|
|
38
|
+
if (input.workspace.memory.includes("NOWWORK:MEMORY_INDEX:BEGIN schema=2")) {
|
|
39
|
+
try {
|
|
40
|
+
if (detectLocalMemoryStorageVersion(input.workspace.memory) !== 2)
|
|
41
|
+
throw new Error("invalid_storage");
|
|
42
|
+
parseFullMemoryIndex(input.workspace.memory);
|
|
43
|
+
const root = await realpath(input.workspace.dir);
|
|
44
|
+
const record = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
45
|
+
const state = MigrationStateSchema.parse(JSON.parse(record.content));
|
|
46
|
+
if (state.phase !== "committed" || state.storageVersion !== 2 || state.identity.agentId !== input.handle
|
|
47
|
+
|| state.identity.rootFingerprint !== createHash("sha256").update(root).digest("hex"))
|
|
48
|
+
throw new Error("invalid_state");
|
|
49
|
+
return { enabled: true, deferred: false };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { enabled: false, deferred: true };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (environment === "production")
|
|
27
56
|
return { enabled: false, deferred: false };
|
|
28
57
|
if (environment === "custom")
|
|
29
58
|
return { enabled: input.requested, deferred: false };
|
|
30
59
|
const deferred = { enabled: false, deferred: true };
|
|
31
60
|
const { workspace } = input;
|
|
32
|
-
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
33
|
-
return deferred;
|
|
34
61
|
try {
|
|
35
62
|
const root = await realpath(workspace.dir);
|
|
36
63
|
const notesStat = await lstat(join(root, "notes"));
|
|
@@ -42,8 +69,26 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
42
69
|
// 完整种子且无旧明细才可按空记忆接入;marker 不能替代这项核对。
|
|
43
70
|
const seededRole = /^## Role\n([\s\S]*?)\n\n## Core Goals\n/mu.exec(memory.content)?.[1];
|
|
44
71
|
if (seededRole !== undefined && memory.content === memorySeed(input.handle, seededRole)) {
|
|
45
|
-
|
|
72
|
+
if (Array.from(memory.content).length <= LOCAL_MEMORY_TOTAL_CODE_POINT_CAP
|
|
73
|
+
&& (await readdir(join(root, "notes"))).length === 0)
|
|
74
|
+
return { enabled: true, deferred: false };
|
|
46
75
|
}
|
|
76
|
+
if (!/NOWWORK:MEMORY_INDEX:/u.test(memory.content)) {
|
|
77
|
+
// 普通旧格式不是损坏V2。引用必须仍为根内可读文件,格式/容量例外保留原流程。
|
|
78
|
+
const entries = parseLocalMemoryIndex(memory.content);
|
|
79
|
+
// 已有受控ID或锚点的记录,不能因HTML标记丢失就退回旧writer。
|
|
80
|
+
if (entries.some(entry => !entry.id.startsWith("legacy:") || entry.anchor !== null))
|
|
81
|
+
return deferred;
|
|
82
|
+
let remaining = inventoryLimits.totalBytes - memory.bytes;
|
|
83
|
+
for (const file of new Set(entries.map(entry => entry.file))) {
|
|
84
|
+
if (!file.startsWith("notes/"))
|
|
85
|
+
return deferred;
|
|
86
|
+
remaining -= (await readMemoryInventorySource(root, file, Math.min(remaining, inventoryLimits.fileBytes))).bytes;
|
|
87
|
+
}
|
|
88
|
+
return { enabled: false, deferred: true, legacyFallback: true };
|
|
89
|
+
}
|
|
90
|
+
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
91
|
+
return deferred;
|
|
47
92
|
const generated = /<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->[\s\S]*?<!-- NOWWORK:MEMORY_INDEX:END -->/u.exec(memory.content)?.[0];
|
|
48
93
|
if (!generated || Array.from(generated).length > LOCAL_MEMORY_INDEX_CODE_POINT_CAP)
|
|
49
94
|
return deferred;
|
|
@@ -48,7 +48,10 @@ export function localMemoryResultPath(runDir, executionId) {
|
|
|
48
48
|
const suffix = createHash("sha256").update(executionId, "utf8").digest("hex").slice(0, 32);
|
|
49
49
|
return join(runDir, `.memory-result.v1-${suffix}.json`);
|
|
50
50
|
}
|
|
51
|
-
export function renderLocalMemoryResultInstructions(resultPath) {
|
|
51
|
+
export function renderLocalMemoryResultInstructions(resultPath, storageVersion = 1) {
|
|
52
|
+
const storagePolicy = storageVersion === 2
|
|
53
|
+
? "完整目录保存在 MEMORY.md,模型只看到有界预览;未展示记录仍然有效。涉及旧规则时先用 crew-memory search 查询完整目录,按当前 ID/revision 更新,不因预览未命中就重复新建。20条预览不是有效记录上限;完整存储超出资源上限会拒绝提交,不得自行删除或降级旧记录。已有 pending 仍是未应用候选,须重新核对授权、重复与版本后通过 changes 申请保存,不因迁移自动生效。"
|
|
54
|
+
: "daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。";
|
|
52
55
|
return `
|
|
53
56
|
|
|
54
57
|
## Structured memory prune (daemon-managed)
|
|
@@ -70,7 +73,7 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
|
|
|
70
73
|
\`\`\`
|
|
71
74
|
|
|
72
75
|
每个 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。
|
|
73
|
-
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token
|
|
76
|
+
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。${storagePolicy}
|
|
74
77
|
写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
|
|
75
78
|
`;
|
|
76
79
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
|
|
2
|
+
export const LOCAL_MEMORY_STORAGE_BYTE_CAP = 16 * 1024 * 1024;
|
|
3
|
+
export const LOCAL_MEMORY_STORAGE_ENTRY_CAP = 10_000;
|
|
4
|
+
export const LOCAL_MEMORY_NOTE_BYTE_CAP = 32 * 1024 * 1024;
|
|
5
|
+
export const LOCAL_MEMORY_READ_BYTE_CAP = 64 * 1024 * 1024;
|
|
6
|
+
export const LOCAL_MEMORY_GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=[12]\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
|
|
7
|
+
const length = (text) => {
|
|
8
|
+
let count = 0;
|
|
9
|
+
for (const _character of text)
|
|
10
|
+
count += 1;
|
|
11
|
+
return count;
|
|
12
|
+
};
|
|
13
|
+
const TYPES = new Set(["procedural", "semantic", "episodic", "preference"]);
|
|
14
|
+
const STATUSES = new Set(["candidate", "active", "pinned", "stale", "superseded", "archived"]);
|
|
15
|
+
const PRIORITIES = new Set(["high", "medium", "low"]);
|
|
16
|
+
const FIELDS = new Set(["id", "file", "type", "status", "summary", "triggers", "priority", "revision", "anchor", "updated", "review_after"]);
|
|
17
|
+
/** A malformed/unknown marker is never treated as an unmarked legacy file. */
|
|
18
|
+
export function detectLocalMemoryStorageVersion(memory) {
|
|
19
|
+
const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
|
|
20
|
+
if (markerCount === 0)
|
|
21
|
+
return 0;
|
|
22
|
+
const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
|
|
23
|
+
if (markerCount !== 2 || block?.index === undefined)
|
|
24
|
+
throw new Error("local_memory_storage_version_invalid");
|
|
25
|
+
const headings = [...memory.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
|
|
26
|
+
const heading = headings[0];
|
|
27
|
+
if (headings.length !== 1 || heading?.index === undefined)
|
|
28
|
+
throw new Error("local_memory_storage_index_invalid");
|
|
29
|
+
const sectionStart = heading.index + heading[0].length;
|
|
30
|
+
const nextSection = /^##[\t ]+(?!#)/mu.exec(memory.slice(sectionStart));
|
|
31
|
+
const sectionEnd = nextSection?.index === undefined ? memory.length : sectionStart + nextSection.index;
|
|
32
|
+
if (block.index < sectionStart || block.index + block[0].length > sectionEnd) {
|
|
33
|
+
throw new Error("local_memory_storage_index_invalid");
|
|
34
|
+
}
|
|
35
|
+
return /schema=2\s*-->/u.test(block[0].slice(0, block[0].indexOf("\n") + 1) || block[0]) ? 2 : 1;
|
|
36
|
+
}
|
|
37
|
+
function validateEntry(entry) {
|
|
38
|
+
const singleLine = (value) => !/[\r\n\u0000-\u001f\u007f]/u.test(value)
|
|
39
|
+
&& !/<!--/u.test(value);
|
|
40
|
+
if (!entry.title.trim() || !entry.summary.trim()
|
|
41
|
+
|| ![entry.title, entry.summary, entry.updated, ...entry.triggers].every(singleLine)
|
|
42
|
+
|| !/^[A-Za-z0-9_-]+$/u.test(entry.id) || entry.anchor !== entry.id
|
|
43
|
+
|| !/^notes\/(?:preferences|project-rules|testing|debugging|git|incidents|lessons)\.md$/u.test(entry.file)
|
|
44
|
+
|| !TYPES.has(entry.type) || !STATUSES.has(entry.status) || !PRIORITIES.has(entry.priority)
|
|
45
|
+
|| !Number.isSafeInteger(entry.revision) || entry.revision < 1
|
|
46
|
+
|| !entry.updated.trim()
|
|
47
|
+
|| (entry.reviewAfter !== null && !singleLine(entry.reviewAfter))) {
|
|
48
|
+
throw new Error("local_memory_storage_entry_invalid");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function validateEntries(entries) {
|
|
52
|
+
if (entries.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
|
|
53
|
+
throw new Error("local_memory_storage_entry_limit");
|
|
54
|
+
const ids = new Set();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
validateEntry(entry);
|
|
57
|
+
if (ids.has(entry.id))
|
|
58
|
+
throw new Error("local_memory_storage_duplicate_id");
|
|
59
|
+
ids.add(entry.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function assertLocalMemoryStorageSize(memory) {
|
|
63
|
+
if (Buffer.byteLength(memory, "utf8") > LOCAL_MEMORY_STORAGE_BYTE_CAP)
|
|
64
|
+
throw new Error("local_memory_storage_byte_limit");
|
|
65
|
+
}
|
|
66
|
+
/** schema=2 stores every lifecycle state, unlike the schema=1 prompt projection. */
|
|
67
|
+
export function parseFullMemoryIndex(memory) {
|
|
68
|
+
assertLocalMemoryStorageSize(memory);
|
|
69
|
+
if (detectLocalMemoryStorageVersion(memory) !== 2)
|
|
70
|
+
throw new Error("local_memory_storage_version_invalid");
|
|
71
|
+
const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory)[0];
|
|
72
|
+
const body = block.replace(/^<!--[^>]*-->\s*/u, "").replace(/<!--[^>]*-->$/u, "");
|
|
73
|
+
const headings = [...body.matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
|
|
74
|
+
if (headings.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
|
|
75
|
+
throw new Error("local_memory_storage_entry_limit");
|
|
76
|
+
if (body.slice(0, headings[0]?.index ?? body.length).trim())
|
|
77
|
+
throw new Error("local_memory_storage_entry_invalid");
|
|
78
|
+
const entries = headings.map((heading, index) => {
|
|
79
|
+
const fields = {};
|
|
80
|
+
const tail = body.slice(heading.index + heading[0].length, headings[index + 1]?.index ?? body.length);
|
|
81
|
+
for (const line of tail.split(/\r?\n/u)) {
|
|
82
|
+
if (!line.trim())
|
|
83
|
+
continue;
|
|
84
|
+
const field = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/u.exec(line);
|
|
85
|
+
if (!field || !FIELDS.has(field[1]) || fields[field[1]] !== undefined)
|
|
86
|
+
throw new Error("local_memory_storage_entry_invalid");
|
|
87
|
+
fields[field[1]] = field[2];
|
|
88
|
+
}
|
|
89
|
+
if ([...FIELDS].filter(field => field !== "review_after").some(field => fields[field] === undefined)) {
|
|
90
|
+
throw new Error("local_memory_storage_entry_invalid");
|
|
91
|
+
}
|
|
92
|
+
if (!/^[1-9][0-9]*$/u.test(fields.revision))
|
|
93
|
+
throw new Error("local_memory_storage_entry_invalid");
|
|
94
|
+
return {
|
|
95
|
+
id: fields.id, title: heading[1], file: fields.file, type: fields.type,
|
|
96
|
+
status: fields.status, summary: fields.summary,
|
|
97
|
+
triggers: fields.triggers.split(/[,,、]/u).map(value => value.trim()).filter(Boolean),
|
|
98
|
+
priority: fields.priority, revision: Number(fields.revision),
|
|
99
|
+
anchor: fields.anchor, updated: fields.updated, reviewAfter: fields.review_after ?? null,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
validateEntries(entries);
|
|
103
|
+
const section = memory.slice(memory.indexOf("## Knowledge Index") + "## Knowledge Index".length);
|
|
104
|
+
const remaining = section.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "").split(/^##[\t ]+(?!#)/mu)[0] ?? "";
|
|
105
|
+
if (/^###[\t ]+/mu.test(remaining.replace(/<!--[\s\S]*?-->/gu, "")))
|
|
106
|
+
throw new Error("local_memory_legacy_index_requires_migration");
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
export function renderFullMemoryIndex(entries) {
|
|
110
|
+
validateEntries(entries);
|
|
111
|
+
const block = `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=2 -->\n${entries.map(renderLocalMemoryIndexCard).join("\n")}<!-- NOWWORK:MEMORY_INDEX:END -->`;
|
|
112
|
+
assertLocalMemoryStorageSize(block);
|
|
113
|
+
return block;
|
|
114
|
+
}
|
|
115
|
+
export function replaceFullMemoryIndex(memory, entries) {
|
|
116
|
+
if (detectLocalMemoryStorageVersion(memory) !== 2)
|
|
117
|
+
throw new Error("local_memory_storage_version_invalid");
|
|
118
|
+
const updated = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => renderFullMemoryIndex(entries));
|
|
119
|
+
assertLocalMemoryStorageSize(updated);
|
|
120
|
+
return updated;
|
|
121
|
+
}
|
|
122
|
+
export function selectLocalMemoryPreview(memory, query = "", options = {}) {
|
|
123
|
+
const entries = parseFullMemoryIndex(memory);
|
|
124
|
+
const totalActiveEntries = entries.filter(entry => entry.status === "active" || entry.status === "pinned").length;
|
|
125
|
+
const emptyIndex = renderGeneratedIndex([]);
|
|
126
|
+
// Reserve the largest count label before selecting; all discovery text counts toward the same cap.
|
|
127
|
+
const hint = (shown) => `\nMemory preview: ${shown}/${totalActiveEntries} active entries. Use crew-memory search --query <text> and crew-memory read --file <file> for more.\n`;
|
|
128
|
+
const base = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${emptyIndex}${hint(totalActiveEntries)}`);
|
|
129
|
+
const remaining = Math.min(LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, options.maxCodePoints ?? Infinity) - length(base);
|
|
130
|
+
const remainingBytes = (options.maxBytes ?? Infinity) - Buffer.byteLength(base, "utf8");
|
|
131
|
+
if (remaining < 0 || remainingBytes < 0 || Number.isNaN(remaining) || Number.isNaN(remainingBytes)) {
|
|
132
|
+
throw new Error("local_memory_identity_budget_exceeded");
|
|
133
|
+
}
|
|
134
|
+
const terms = query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
|
|
135
|
+
const selection = selectLocalMemoryIndexEntries(entries, {
|
|
136
|
+
entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP,
|
|
137
|
+
indexCodePointCap: Math.min(LOCAL_MEMORY_INDEX_CODE_POINT_CAP - length(emptyIndex), remaining),
|
|
138
|
+
indexByteCap: remainingBytes,
|
|
139
|
+
}, {
|
|
140
|
+
protectedIds: new Set(entries.filter(entry => entry.status === "pinned").map(entry => entry.id)),
|
|
141
|
+
relevance: entry => {
|
|
142
|
+
const searchable = `${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
|
|
143
|
+
return terms.filter(term => searchable.includes(term)).length;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
if (selection.reason === "index_over_budget_pinned")
|
|
147
|
+
throw new Error("local_memory_pinned_budget_exceeded");
|
|
148
|
+
return {
|
|
149
|
+
...selection, totalActiveEntries,
|
|
150
|
+
memory: memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${renderGeneratedIndex(selection.entries)}${hint(selection.entries.length)}`),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -6,6 +6,7 @@ import { dslog } from "../slog.js";
|
|
|
6
6
|
import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
|
|
7
7
|
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, selectLocalMemoryIndexEntries, } from "./index-budget.js";
|
|
8
8
|
import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
|
|
9
|
+
import { assertLocalMemoryStorageSize, detectLocalMemoryStorageVersion, LOCAL_MEMORY_GENERATED_INDEX_PATTERN, LOCAL_MEMORY_NOTE_BYTE_CAP, LOCAL_MEMORY_READ_BYTE_CAP, parseFullMemoryIndex, replaceFullMemoryIndex, selectLocalMemoryPreview, } from "./storage-format.js";
|
|
9
10
|
const StatusChangeSchema = z.object({
|
|
10
11
|
memoryId: LocalMemorySingleLineText,
|
|
11
12
|
expectedRevision: z.number().int().positive(),
|
|
@@ -90,12 +91,14 @@ function parseCards(section) {
|
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
export function parseLocalMemoryIndex(memory) {
|
|
94
|
+
if (detectLocalMemoryStorageVersion(memory) === 2)
|
|
95
|
+
return parseFullMemoryIndex(memory);
|
|
93
96
|
const section = indexSection(memory);
|
|
94
97
|
const generated = GENERATED_INDEX_PATTERN.exec(section)?.[0];
|
|
95
98
|
return parseCards(generated ?? section);
|
|
96
99
|
}
|
|
97
100
|
function parseUnmarkedMemoryIndex(memory) {
|
|
98
|
-
const section = indexSection(memory).replace(
|
|
101
|
+
const section = indexSection(memory).replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "");
|
|
99
102
|
return parseCards(section);
|
|
100
103
|
}
|
|
101
104
|
function replaceGeneratedIndex(memory, entries) {
|
|
@@ -122,11 +125,36 @@ function noteSection(id, revision, title, body) {
|
|
|
122
125
|
}
|
|
123
126
|
function notePattern(id) {
|
|
124
127
|
const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
125
|
-
return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
|
|
128
|
+
return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
|
|
129
|
+
}
|
|
130
|
+
function uniqueNoteSection(content, id) {
|
|
131
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
132
|
+
const begins = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)`, "gu"))];
|
|
133
|
+
const ends = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->`, "gu"))];
|
|
134
|
+
if (begins.length === 0 && ends.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
const section = notePattern(id).exec(content)?.[0];
|
|
137
|
+
if (begins.length !== 1 || ends.length !== 1 || section === undefined
|
|
138
|
+
|| (section.match(/NOWWORK:MEMORY_ENTRY:(?:BEGIN|END)/gu) ?? []).length !== 2) {
|
|
139
|
+
throw new Error("local_memory_note_anchor_invalid");
|
|
140
|
+
}
|
|
141
|
+
return section;
|
|
126
142
|
}
|
|
127
143
|
function replaceNote(content, id, replacement) {
|
|
128
144
|
const pattern = notePattern(id);
|
|
129
|
-
return
|
|
145
|
+
return uniqueNoteSection(content, id) !== null ? content.replace(pattern, () => replacement) : null;
|
|
146
|
+
}
|
|
147
|
+
function updateNoteRevision(content, id, revision) {
|
|
148
|
+
const section = uniqueNoteSection(content, id);
|
|
149
|
+
const beginning = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id} `;
|
|
150
|
+
const ending = `<!-- NOWWORK:MEMORY_ENTRY:END id=${id} -->`;
|
|
151
|
+
if (section === null || content.split(beginning).length !== 2 || content.split(ending).length !== 2) {
|
|
152
|
+
throw new Error("local_memory_note_anchor_missing");
|
|
153
|
+
}
|
|
154
|
+
const updated = section.replace(/^(<!--\s*NOWWORK:MEMORY_ENTRY:BEGIN\s+id=[A-Za-z0-9_-]+\s+revision=)[1-9][0-9]*(\s*-->)/u, (_match, prefix, suffix) => `${prefix}${revision}${suffix}`);
|
|
155
|
+
if (!updated.startsWith(`${beginning}revision=${revision} -->`))
|
|
156
|
+
throw new Error("local_memory_note_anchor_missing");
|
|
157
|
+
return content.replace(notePattern(id), () => updated);
|
|
130
158
|
}
|
|
131
159
|
function validateCandidate(result) {
|
|
132
160
|
if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
|
|
@@ -199,14 +227,14 @@ function detailCategoryFromFile(file) {
|
|
|
199
227
|
return DETAIL_CATEGORIES.has(category) ? category : null;
|
|
200
228
|
}
|
|
201
229
|
function readNoteBody(content, id) {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
230
|
+
const section = uniqueNoteSection(content, id);
|
|
231
|
+
if (section === null)
|
|
204
232
|
return null;
|
|
205
|
-
const bodyStart =
|
|
206
|
-
const bodyEnd =
|
|
233
|
+
const bodyStart = section.indexOf("\n\n");
|
|
234
|
+
const bodyEnd = section.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
|
|
207
235
|
if (bodyStart < 0 || bodyEnd < 0)
|
|
208
236
|
return null;
|
|
209
|
-
return
|
|
237
|
+
return section.slice(bodyStart + 2, bodyEnd).trim();
|
|
210
238
|
}
|
|
211
239
|
function archivedChange(entry, detailBody) {
|
|
212
240
|
const detailCategory = detailCategoryFromFile(entry.file);
|
|
@@ -249,7 +277,12 @@ async function readNote(path) {
|
|
|
249
277
|
const stat = await lstat(path);
|
|
250
278
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
251
279
|
throw new Error("local_memory_note_path_invalid");
|
|
252
|
-
|
|
280
|
+
if (stat.size > LOCAL_MEMORY_NOTE_BYTE_CAP)
|
|
281
|
+
throw new Error("local_memory_note_byte_limit");
|
|
282
|
+
const content = await readFile(path, "utf8");
|
|
283
|
+
if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
|
|
284
|
+
throw new Error("local_memory_note_byte_limit");
|
|
285
|
+
return content;
|
|
253
286
|
}
|
|
254
287
|
catch (error) {
|
|
255
288
|
if (error.code === "ENOENT")
|
|
@@ -269,6 +302,11 @@ async function atomicWrite(path, content) {
|
|
|
269
302
|
}
|
|
270
303
|
}
|
|
271
304
|
function projectIndex(memory, entries, protectedIds) {
|
|
305
|
+
if (detectLocalMemoryStorageVersion(memory) === 2) {
|
|
306
|
+
const updated = replaceFullMemoryIndex(memory, entries);
|
|
307
|
+
selectLocalMemoryPreview(updated);
|
|
308
|
+
return { memory: updated, omittedIds: [], reason: null };
|
|
309
|
+
}
|
|
272
310
|
const empty = replaceGeneratedIndex(memory, []);
|
|
273
311
|
if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
274
312
|
throw new Error("local_memory_memory_over_budget");
|
|
@@ -311,7 +349,12 @@ export async function applyLocalMemoryResult(input) {
|
|
|
311
349
|
const memoryPath = join(input.agentDir, "MEMORY.md");
|
|
312
350
|
await assertWritablePaths(input.agentDir, memoryPath);
|
|
313
351
|
return withApplyLease(input.agentDir, async () => {
|
|
352
|
+
const memoryStat = await lstat(memoryPath);
|
|
353
|
+
if (memoryStat.size > 16 * 1024 * 1024)
|
|
354
|
+
throw new Error("local_memory_storage_byte_limit");
|
|
314
355
|
const memory = await readFile(memoryPath, "utf8");
|
|
356
|
+
assertLocalMemoryStorageSize(memory);
|
|
357
|
+
const fullStorage = detectLocalMemoryStorageVersion(memory) === 2;
|
|
315
358
|
if (parseUnmarkedMemoryIndex(memory).length > 0)
|
|
316
359
|
throw new Error("local_memory_legacy_index_requires_migration");
|
|
317
360
|
let entries = [...parseLocalMemoryIndex(memory)];
|
|
@@ -342,6 +385,26 @@ export async function applyLocalMemoryResult(input) {
|
|
|
342
385
|
const pendingCandidates = [];
|
|
343
386
|
const resolvedPendingIds = new Set();
|
|
344
387
|
const noteUpdates = new Map();
|
|
388
|
+
const noteCache = new Map();
|
|
389
|
+
let totalReadBytes = Buffer.byteLength(memory, "utf8");
|
|
390
|
+
const readStoredNote = async (path) => {
|
|
391
|
+
const cached = noteUpdates.get(path) ?? noteCache.get(path);
|
|
392
|
+
if (cached !== undefined)
|
|
393
|
+
return cached;
|
|
394
|
+
const stat = await lstat(path).catch((error) => {
|
|
395
|
+
if (error.code === "ENOENT")
|
|
396
|
+
return null;
|
|
397
|
+
throw error;
|
|
398
|
+
});
|
|
399
|
+
if (stat !== null && totalReadBytes + stat.size > LOCAL_MEMORY_READ_BYTE_CAP)
|
|
400
|
+
throw new Error("local_memory_read_byte_limit");
|
|
401
|
+
const content = await readNote(path);
|
|
402
|
+
totalReadBytes += Buffer.byteLength(content, "utf8");
|
|
403
|
+
if (totalReadBytes > LOCAL_MEMORY_READ_BYTE_CAP)
|
|
404
|
+
throw new Error("local_memory_read_byte_limit");
|
|
405
|
+
noteCache.set(path, content);
|
|
406
|
+
return content;
|
|
407
|
+
};
|
|
345
408
|
let allowActiveEviction = false;
|
|
346
409
|
const randomId = input.randomId ?? randomUUID;
|
|
347
410
|
const executionId = input.executionId ?? "unknown";
|
|
@@ -383,10 +446,21 @@ export async function applyLocalMemoryResult(input) {
|
|
|
383
446
|
if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
|
|
384
447
|
throw new Error("local_memory_duplicate_candidate");
|
|
385
448
|
}
|
|
386
|
-
if (existingCreate === undefined && entries.some((entry) =>
|
|
387
|
-
|| normalize(entry.summary) === normalize(change.summary))) {
|
|
449
|
+
if (existingCreate === undefined && entries.some((entry) => (!fullStorage || entry.status === "active" || entry.status === "pinned")
|
|
450
|
+
&& (normalize(entry.title) === normalize(change.title) || normalize(entry.summary) === normalize(change.summary)))) {
|
|
388
451
|
throw new Error("local_memory_duplicate_candidate");
|
|
389
452
|
}
|
|
453
|
+
if (existingCreate !== undefined) {
|
|
454
|
+
if (existingCreate.revision !== 1 || existingCreate.status !== "active") {
|
|
455
|
+
throw new Error("local_memory_create_replay_conflict");
|
|
456
|
+
}
|
|
457
|
+
const stored = await readStoredNote(resolve(input.agentDir, existingCreate.file));
|
|
458
|
+
if (uniqueNoteSection(stored, id)?.trimEnd() !== noteSection(id, 1, change.title, change.detailBody).trimEnd()) {
|
|
459
|
+
throw new Error("local_memory_create_replay_conflict");
|
|
460
|
+
}
|
|
461
|
+
// A proven duplicate is a no-op; it must never rewrite a newer body or status.
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
390
464
|
}
|
|
391
465
|
const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
|
|
392
466
|
const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
|
|
@@ -400,7 +474,7 @@ export async function applyLocalMemoryResult(input) {
|
|
|
400
474
|
: item),
|
|
401
475
|
entry,
|
|
402
476
|
];
|
|
403
|
-
const allowEvictionForChange = change.operation === "create" && change.priority === "high";
|
|
477
|
+
const allowEvictionForChange = !fullStorage && change.operation === "create" && change.priority === "high";
|
|
404
478
|
const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
|
|
405
479
|
if (allowEvictionForChange)
|
|
406
480
|
allowActiveEviction = true;
|
|
@@ -417,7 +491,7 @@ export async function applyLocalMemoryResult(input) {
|
|
|
417
491
|
const notePath = resolve(input.agentDir, entry.file);
|
|
418
492
|
if (!isWithin(resolve(input.agentDir, "notes"), notePath))
|
|
419
493
|
throw new Error("local_memory_note_path_invalid");
|
|
420
|
-
const previous =
|
|
494
|
+
const previous = await readStoredNote(notePath);
|
|
421
495
|
const replacement = noteSection(id, revision, change.title, change.detailBody);
|
|
422
496
|
const existingSection = replaceNote(previous, id, replacement);
|
|
423
497
|
const next = existingSection ?? (current === undefined
|
|
@@ -435,7 +509,7 @@ export async function applyLocalMemoryResult(input) {
|
|
|
435
509
|
// Pending create candidates are durable overflow, not dead ends. Promote only
|
|
436
510
|
// when the current catalog has room; supersede/update candidates stay pending
|
|
437
511
|
// until a fresh revision-aware decision can safely replay them.
|
|
438
|
-
const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
|
|
512
|
+
const pendingCandidatesOnDisk = fullStorage ? [] : await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
|
|
439
513
|
if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
|
|
440
514
|
throw error;
|
|
441
515
|
// 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
|
|
@@ -460,7 +534,7 @@ export async function applyLocalMemoryResult(input) {
|
|
|
460
534
|
|| normalize(entry.summary) === normalize(change.summary)));
|
|
461
535
|
if (duplicate !== undefined) {
|
|
462
536
|
const duplicatePath = resolve(input.agentDir, duplicate.file);
|
|
463
|
-
const duplicateBody = readNoteBody(
|
|
537
|
+
const duplicateBody = readNoteBody(await readStoredNote(duplicatePath), duplicate.id);
|
|
464
538
|
if (duplicateBody === change.detailBody
|
|
465
539
|
|| duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
|
|
466
540
|
resolvedPendingIds.add(pending.pendingId);
|
|
@@ -478,7 +552,7 @@ export async function applyLocalMemoryResult(input) {
|
|
|
478
552
|
const notePath = resolve(input.agentDir, entry.file);
|
|
479
553
|
if (!isWithin(resolve(input.agentDir, "notes"), notePath))
|
|
480
554
|
throw new Error("local_memory_note_path_invalid");
|
|
481
|
-
const previous =
|
|
555
|
+
const previous = await readStoredNote(notePath);
|
|
482
556
|
const replacement = noteSection(id, 1, change.title, change.detailBody);
|
|
483
557
|
const existingSection = replaceNote(previous, id, replacement);
|
|
484
558
|
const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
|
|
@@ -487,6 +561,17 @@ export async function applyLocalMemoryResult(input) {
|
|
|
487
561
|
createdIds.push(id);
|
|
488
562
|
resolvedPendingIds.add(pending.pendingId);
|
|
489
563
|
}
|
|
564
|
+
if (fullStorage) {
|
|
565
|
+
const changedMetadataIds = new Set([
|
|
566
|
+
...statusChangedIds,
|
|
567
|
+
...input.result.changes.flatMap(change => change.supersedesMemoryId === null ? [] : [change.supersedesMemoryId]),
|
|
568
|
+
]);
|
|
569
|
+
for (const id of changedMetadataIds) {
|
|
570
|
+
const entry = entries.find(item => item.id === id);
|
|
571
|
+
const notePath = resolve(input.agentDir, entry.file);
|
|
572
|
+
noteUpdates.set(notePath, updateNoteRevision(await readStoredNote(notePath), id, entry.revision));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
490
575
|
let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
|
|
491
576
|
if (finalProjection.reason !== null
|
|
492
577
|
&& (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
|
|
@@ -498,7 +583,7 @@ export async function applyLocalMemoryResult(input) {
|
|
|
498
583
|
.filter((entry) => entry?.status === "active");
|
|
499
584
|
for (const entry of omittedActiveEntries) {
|
|
500
585
|
const notePath = resolve(input.agentDir, entry.file);
|
|
501
|
-
const content =
|
|
586
|
+
const content = await readStoredNote(notePath);
|
|
502
587
|
const detailBody = readNoteBody(content, entry.id);
|
|
503
588
|
const change = detailBody === null ? null : archivedChange(entry, detailBody);
|
|
504
589
|
if (change === null)
|
|
@@ -519,6 +604,12 @@ export async function applyLocalMemoryResult(input) {
|
|
|
519
604
|
if (finalProjection.reason !== null)
|
|
520
605
|
throw new Error(finalProjection.reason);
|
|
521
606
|
}
|
|
607
|
+
// Complete every resource check before the first persistent write.
|
|
608
|
+
assertLocalMemoryStorageSize(finalProjection.memory);
|
|
609
|
+
for (const content of noteUpdates.values()) {
|
|
610
|
+
if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
|
|
611
|
+
throw new Error("local_memory_note_byte_limit");
|
|
612
|
+
}
|
|
522
613
|
for (const candidate of pendingCandidates)
|
|
523
614
|
await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
|
|
524
615
|
for (const [path, content] of noteUpdates)
|
package/dist/machine-info.js
CHANGED
|
@@ -25,6 +25,7 @@ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
|
|
|
25
25
|
import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
|
|
26
26
|
import { daemonHome } from "./computer-profile.js";
|
|
27
27
|
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
28
|
+
import { MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY } from "./memory-migration-protocol.js";
|
|
28
29
|
export { executableRuntimes } from "./runtime-capabilities.js";
|
|
29
30
|
const execFileP = promisify(execFile);
|
|
30
31
|
export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
|
|
@@ -39,6 +40,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
|
|
|
39
40
|
});
|
|
40
41
|
export const DAEMON_CAPABILITIES = Object.freeze([
|
|
41
42
|
MARKETPLACE_CAPABILITY,
|
|
43
|
+
MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY,
|
|
42
44
|
"scheduled_job_v1",
|
|
43
45
|
"reply_origin_v1",
|
|
44
46
|
"origin_decision_v1",
|
|
@@ -73,6 +75,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
|
|
|
73
75
|
return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
|
|
74
76
|
&& (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
|
|
75
77
|
&& (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
|
|
78
|
+
&& (nativeWorkspacePlatform || ![MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY].includes(capability))
|
|
76
79
|
&& (nativeWorkspacePlatform
|
|
77
80
|
|| (capability !== "project_skills_v1"
|
|
78
81
|
&& capability !== "project_skill_projection_v2"
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createRuntimeCancellation } from "./runtime-cancellation.js";
|
|
3
|
+
import { MemoryMigrationStartSchema } from "./memory-migration-protocol.js";
|
|
4
|
+
import { runMemoryMigration } from "./memory-migration-runtime.js";
|
|
5
|
+
import { inspectMemoryForMigration } from "./memory-migration-inspect.js";
|
|
6
|
+
const Inspect = MemoryMigrationStartSchema.pick({ reqId: true, protocolVersion: true, workspaceId: true,
|
|
7
|
+
agentId: true, handle: true, machineId: true }).extend({ type: z.literal("memory:migration:inspect"), executionId: z.string().uuid().optional() }).strict();
|
|
8
|
+
export function createMemoryMigrationController(input) {
|
|
9
|
+
let identity = null;
|
|
10
|
+
const runs = new Map();
|
|
11
|
+
return {
|
|
12
|
+
setIdentity(value) { identity = value; },
|
|
13
|
+
cancel(executionId) { const run = runs.get(executionId); if (!run)
|
|
14
|
+
return false; run.cancel(); return true; },
|
|
15
|
+
async handle(raw) {
|
|
16
|
+
const type = raw?.type;
|
|
17
|
+
if (type !== "memory:migration:start" && type !== "memory:migration:inspect")
|
|
18
|
+
return false;
|
|
19
|
+
const parsed = type === "memory:migration:start" ? MemoryMigrationStartSchema.safeParse(raw) : Inspect.safeParse(raw);
|
|
20
|
+
if (!parsed.success) {
|
|
21
|
+
const reqId = raw.reqId;
|
|
22
|
+
if (typeof reqId === "string" && reqId.length <= 200)
|
|
23
|
+
input.send({ type: "fs:result", reqId, ok: false, error: "memory_migration_invalid_request" });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const request = parsed.data;
|
|
27
|
+
if (!identity || request.workspaceId !== identity.workspaceId || request.machineId !== identity.machineId) {
|
|
28
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_identity_mismatch" });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
let journal;
|
|
32
|
+
try {
|
|
33
|
+
journal = input.journal();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_journal_unavailable" });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (request.type === "memory:migration:inspect") {
|
|
40
|
+
try {
|
|
41
|
+
const data = await inspectMemoryForMigration(input.config.agentsRoot, request, journal);
|
|
42
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: true, data });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_inspection_failed" });
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const existing = runs.get(request.executionId);
|
|
50
|
+
if (existing) {
|
|
51
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: true, data: { kind: "running" } });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (input.isAdmissionClosed?.()) {
|
|
55
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_storage_busy" });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const cancellation = createRuntimeCancellation();
|
|
59
|
+
const promise = runMemoryMigration(request, {
|
|
60
|
+
agentsRoot: input.config.agentsRoot, coordinator: input.coordinator, journal,
|
|
61
|
+
cancellation: cancellation.cancellation, send: input.send,
|
|
62
|
+
control: async (step, body) => {
|
|
63
|
+
const base = `/daemon/memory-migrations/${encodeURIComponent(request.agentId)}`;
|
|
64
|
+
const path = step === "history" ? `${base}/history` : `${base}/${request.migrationId}/${step === "needs-review" ? "review" : step}`;
|
|
65
|
+
const response = await fetch(new URL(path, input.config.serverUrl), {
|
|
66
|
+
method: step === "history" ? "GET" : "POST", signal: AbortSignal.timeout(10_000),
|
|
67
|
+
headers: { authorization: `Bearer ${input.config.machineToken}`, "content-type": "application/json" },
|
|
68
|
+
...(step === "history" ? {} : { body: JSON.stringify(body) }),
|
|
69
|
+
});
|
|
70
|
+
const result = await response.json();
|
|
71
|
+
if (!response.ok || result.success !== true)
|
|
72
|
+
throw new Error("migration_control_unconfirmed");
|
|
73
|
+
return result.data;
|
|
74
|
+
},
|
|
75
|
+
}).then(data => input.send({ type: "fs:result", reqId: request.reqId, ok: true, data }))
|
|
76
|
+
.catch(() => input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_failed" }))
|
|
77
|
+
.finally(() => runs.delete(request.executionId));
|
|
78
|
+
runs.set(request.executionId, { promise, cancel: cancellation.request });
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
async stop() { for (const run of runs.values())
|
|
82
|
+
run.cancel(); await Promise.allSettled([...runs.values()].map(run => run.promise)); },
|
|
83
|
+
};
|
|
84
|
+
}
|