@nowcrew/daemon 0.6.93-beta.1 → 0.6.93-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-environment-execution.js +7 -2
- package/dist/execution-journal.js +1 -3
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +1 -3
- package/dist/local-execution-prompt.js +3 -8
- package/dist/local-executor.js +35 -64
- package/dist/local-memory/index-budget.js +3 -9
- package/dist/local-memory/inventory-cli.js +1 -23
- package/dist/local-memory/inventory-source.js +8 -15
- package/dist/local-memory/prompt.js +4 -20
- package/dist/local-memory/read-cli.js +77 -75
- package/dist/local-memory/rollout.js +8 -54
- package/dist/local-memory/runtime.js +13 -148
- package/dist/local-memory/write.js +19 -125
- package/dist/local-memory-diagnostics.js +2 -41
- package/dist/local-memory-telemetry.js +2 -19
- package/dist/machine-info.js +0 -3
- package/dist/memory-prune-diagnostics.js +4 -60
- package/dist/message-file-preview.js +1 -11
- package/dist/prompt.js +1 -5
- package/dist/runner.js +1 -1
- package/dist/serve.js +3 -36
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +3 -16
- package/dist/workspace.js +2 -10
- package/package.json +1 -1
- package/dist/local-memory/execution-workspace.js +0 -55
- package/dist/local-memory/full-index-search.js +0 -47
- package/dist/local-memory/initialization-identity.js +0 -23
- package/dist/local-memory/legacy-adapter.js +0 -233
- package/dist/local-memory/migration-access.js +0 -184
- package/dist/local-memory/migration-cli.js +0 -54
- package/dist/local-memory/migration-io.js +0 -199
- package/dist/local-memory/migration-plan.js +0 -283
- package/dist/local-memory/migration-schema.js +0 -55
- package/dist/local-memory/migration-store.js +0 -429
- package/dist/local-memory/migration-tool-entry.js +0 -21
- package/dist/local-memory/storage-format.js +0 -152
- package/dist/memory-migration-controller.js +0 -84
- package/dist/memory-migration-inspect.js +0 -75
- package/dist/memory-migration-journal.js +0 -14
- package/dist/memory-migration-protocol.js +0 -14
- package/dist/memory-migration-runtime.js +0 -305
package/dist/serve.js
CHANGED
|
@@ -14,7 +14,6 @@ import { reconcileExecutionJournal } from "./execution-recovery.js";
|
|
|
14
14
|
import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
|
|
15
15
|
import { handleWorkspaceControlRequest } from "./workspace-control-handler.js";
|
|
16
16
|
import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
|
|
17
|
-
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
18
17
|
import { normalizeScheduledContext } from "./scheduled-report.js";
|
|
19
18
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
20
19
|
import { reportAgentRunComplete } from "./scheduled-run-report.js";
|
|
@@ -51,8 +50,6 @@ import { probeRuntimeHealth } from "./runtime-health.js";
|
|
|
51
50
|
import { createServeMarketplace } from "./marketplace-serve.js";
|
|
52
51
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
53
52
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
54
|
-
import { createMemoryMigrationController } from "./memory-migration-controller.js";
|
|
55
|
-
import { createMemoryMigrationJournal, replayExecutionJournals } from "./memory-migration-journal.js";
|
|
56
53
|
import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
|
|
57
54
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
58
55
|
// normalize.ts 的活动种类 → activity 枚举
|
|
@@ -74,8 +71,6 @@ export function serve(config, opts = {}) {
|
|
|
74
71
|
let reconnectTimer = null;
|
|
75
72
|
let stopPromise = null;
|
|
76
73
|
let executionJournal;
|
|
77
|
-
let migrationExecutionJournal;
|
|
78
|
-
let migrationJournalReady = false;
|
|
79
74
|
const injectedExecutionJournal = opts.execution?.journal;
|
|
80
75
|
const agentMemory = opts.execution?.dependencies?.agentMemory
|
|
81
76
|
?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
|
|
@@ -90,15 +85,6 @@ export function serve(config, opts = {}) {
|
|
|
90
85
|
startupGapMs: config.executionLimits.startupGapMs,
|
|
91
86
|
});
|
|
92
87
|
const localSlots = createSharedSlotManager(config.executionLimits);
|
|
93
|
-
const memoryMigrations = createMemoryMigrationController({ config, coordinator: hostCoordinator,
|
|
94
|
-
isAdmissionClosed: () => stopped || storageIdentityAdmission.isAdmissionClosed(),
|
|
95
|
-
journal: () => { if (!migrationJournalReady)
|
|
96
|
-
throw new Error("memory_migration_journal_unavailable"); return migrationExecutionJournal; }, send: frame => {
|
|
97
|
-
if (frame.type === "execution:completed")
|
|
98
|
-
completionRetransmitter.track(frame);
|
|
99
|
-
else if (ws?.readyState === WebSocket.OPEN)
|
|
100
|
-
ws.send(JSON.stringify(frame));
|
|
101
|
-
} });
|
|
102
88
|
const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
|
|
103
89
|
const concurrencyTelemetry = createExecutionConcurrencyTelemetry(sharedSlots, {
|
|
104
90
|
...(opts.execution?.concurrencySnapshotIntervalMs === undefined
|
|
@@ -246,7 +232,7 @@ export function serve(config, opts = {}) {
|
|
|
246
232
|
return null;
|
|
247
233
|
};
|
|
248
234
|
const sendSnapshot = async (reqId) => {
|
|
249
|
-
const entries = (await
|
|
235
|
+
const entries = (await executionJournal.replay()).map(snapshotEntry).filter((entry) => entry !== null);
|
|
250
236
|
safeExecutionSend(ExecutionSnapshotSchema.parse({
|
|
251
237
|
type: "execution:snapshot", protocolVersion: 1, reqId, entries,
|
|
252
238
|
}));
|
|
@@ -327,12 +313,6 @@ export function serve(config, opts = {}) {
|
|
|
327
313
|
machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
|
|
328
314
|
opts.onOpen?.(ws);
|
|
329
315
|
void sendSnapshot(`reconnect-${randomUUID()}`);
|
|
330
|
-
// 普通回执不等待维护日志IO;两类记录分别重放到同一发送器。
|
|
331
|
-
void migrationExecutionJournal.replay().then(entries => {
|
|
332
|
-
for (const entry of entries)
|
|
333
|
-
if (entry.completion && !entry.completionAcknowledged)
|
|
334
|
-
completionRetransmitter.track(entry.completion);
|
|
335
|
-
}).catch(() => dslog("memory_migration.journal_unavailable", "维护回执重放失败,普通回执继续", { level: "ERROR" }));
|
|
336
316
|
void executionJournal.replay()
|
|
337
317
|
.then((entries) => {
|
|
338
318
|
for (const entry of entries) {
|
|
@@ -407,8 +387,7 @@ export function serve(config, opts = {}) {
|
|
|
407
387
|
return;
|
|
408
388
|
if (frame.type === "execution:completion-ack") {
|
|
409
389
|
try {
|
|
410
|
-
|
|
411
|
-
await journal.acknowledgeCompletion(frame.executionId);
|
|
390
|
+
await executionJournal.acknowledgeCompletion(frame.executionId);
|
|
412
391
|
const attempts = completionRetransmitter.acknowledge(frame.executionId);
|
|
413
392
|
dslog("execution.completion_acknowledged", "execution completion ACK 已持久化", {
|
|
414
393
|
execution_id: frame.executionId,
|
|
@@ -434,8 +413,6 @@ export function serve(config, opts = {}) {
|
|
|
434
413
|
return;
|
|
435
414
|
}
|
|
436
415
|
if (frame.type === "execution:cancel") {
|
|
437
|
-
if (memoryMigrations.cancel(frame.executionId))
|
|
438
|
-
return;
|
|
439
416
|
if (!knownExecutionHashes.has(frame.executionId)
|
|
440
417
|
&& !executionRuns.has(frame.executionId)
|
|
441
418
|
&& await executionJournal.get(frame.executionId) === null)
|
|
@@ -758,8 +735,6 @@ export function serve(config, opts = {}) {
|
|
|
758
735
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
759
736
|
const r = msg;
|
|
760
737
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
761
|
-
if (r.workspaceId && r.machineId)
|
|
762
|
-
memoryMigrations.setIdentity({ workspaceId: r.workspaceId, machineId: r.machineId });
|
|
763
738
|
if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
|
|
764
739
|
const bindingSocket = ws;
|
|
765
740
|
bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
|
|
@@ -826,8 +801,6 @@ export function serve(config, opts = {}) {
|
|
|
826
801
|
}
|
|
827
802
|
catch { /* server 会按请求超时处理 */ } }))
|
|
828
803
|
return;
|
|
829
|
-
if (await memoryMigrations.handle(msg))
|
|
830
|
-
return;
|
|
831
804
|
if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
|
|
832
805
|
return;
|
|
833
806
|
// 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
|
|
@@ -842,7 +815,7 @@ export function serve(config, opts = {}) {
|
|
|
842
815
|
try {
|
|
843
816
|
const data2 = req.type === "raft:inspect"
|
|
844
817
|
? await inspectRaftWorkspace(req.path)
|
|
845
|
-
: await
|
|
818
|
+
: await importRaftWorkspace(req.path, join(config.agentsRoot, req.handle));
|
|
846
819
|
if (req.type === "raft:import") {
|
|
847
820
|
log(`📦 已导入 raft 工作区 → ${req.handle}: 复制 ${data2.copied.length} 项`);
|
|
848
821
|
}
|
|
@@ -1286,9 +1259,6 @@ export function serve(config, opts = {}) {
|
|
|
1286
1259
|
writeStderr: (line) => process.stderr.write(line),
|
|
1287
1260
|
});
|
|
1288
1261
|
await runProjectSkillStartupCleanup(config.agentsRoot, executionJournal, dslog);
|
|
1289
|
-
migrationExecutionJournal = createMemoryMigrationJournal(config.agentsRoot);
|
|
1290
|
-
await migrationExecutionJournal.reconcileAfterRestart().then(() => { migrationJournalReady = true; })
|
|
1291
|
-
.catch(() => dslog("memory_migration.journal_unavailable", "迁移日志恢复失败,普通执行继续,迁移请求单独报错", { level: "ERROR" }));
|
|
1292
1262
|
await connect();
|
|
1293
1263
|
})();
|
|
1294
1264
|
const stop = () => {
|
|
@@ -1306,7 +1276,6 @@ export function serve(config, opts = {}) {
|
|
|
1306
1276
|
const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
|
|
1307
1277
|
const pending = [
|
|
1308
1278
|
machineRuntime.stop(),
|
|
1309
|
-
memoryMigrations.stop(),
|
|
1310
1279
|
webSocketClosed,
|
|
1311
1280
|
updateController.drain(),
|
|
1312
1281
|
marketplace.stop(),
|
|
@@ -1325,8 +1294,6 @@ export function serve(config, opts = {}) {
|
|
|
1325
1294
|
if (executionJournal !== undefined) {
|
|
1326
1295
|
await executionJournal.close({ signal: deadline.signal });
|
|
1327
1296
|
}
|
|
1328
|
-
if (migrationExecutionJournal !== undefined)
|
|
1329
|
-
await migrationExecutionJournal.close({ signal: deadline.signal });
|
|
1330
1297
|
}
|
|
1331
1298
|
finally {
|
|
1332
1299
|
deadline.dispose();
|
package/dist/token.js
CHANGED
|
@@ -7,7 +7,6 @@ export async function mintAgentToken(serverUrl, machineToken, handle, displayNam
|
|
|
7
7
|
},
|
|
8
8
|
// 把本轮唤醒的线程根钉到 per-launch 令牌:服务端据此让 task/消息默认绑/回当前线程。
|
|
9
9
|
body: JSON.stringify({
|
|
10
|
-
memoryStateGuardVersion: 1,
|
|
11
10
|
handle,
|
|
12
11
|
...(displayName ? { displayName } : {}),
|
|
13
12
|
...(options.wakeThreadRoot ? { wakeThreadRoot: options.wakeThreadRoot } : {}),
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
/** 控制面文件/技能查看的按需处理,独立于执行生命周期。 */
|
|
2
|
-
import {
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { join } from "node:path";
|
|
4
3
|
import { readSharedMessageFile } from "./message-file-preview.js";
|
|
5
4
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
6
5
|
import { listSkills } from "./skills.js";
|
|
7
6
|
import { listRuntimeModels } from "./list-models.js";
|
|
8
|
-
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
9
|
-
async function listProtectedWorkspace(root, path) {
|
|
10
|
-
try {
|
|
11
|
-
await lstat(root);
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (error.code !== "ENOENT")
|
|
15
|
-
throw error;
|
|
16
|
-
return { root: resolve(root), path: path.replace(/^\/+/, ""), entries: [] };
|
|
17
|
-
}
|
|
18
|
-
return withMemoryAccess(root, () => listWorkspace(root, path));
|
|
19
|
-
}
|
|
20
7
|
export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, models = listRuntimeModels) {
|
|
21
8
|
if (!raw || typeof raw !== "object")
|
|
22
9
|
return null;
|
|
@@ -28,8 +15,8 @@ export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, m
|
|
|
28
15
|
try {
|
|
29
16
|
const root = join(agentsRoot, request.handle);
|
|
30
17
|
const data = request.type === "fs:read-shared" ? await readSharedMessageFile(agentsRoot, raw, registry)
|
|
31
|
-
: request.type === "fs:list" ? await
|
|
32
|
-
: request.type === "fs:read" ? await
|
|
18
|
+
: request.type === "fs:list" ? await listWorkspace(root, request.path)
|
|
19
|
+
: request.type === "fs:read" ? await readWorkspaceFile(root, request.path)
|
|
33
20
|
: request.type === "skills:list" ? await listSkills(request.runtime)
|
|
34
21
|
: { models: await models(request.runtime ?? "") };
|
|
35
22
|
return { type: "fs:result", reqId: request.reqId, ok: true, data };
|
package/dist/workspace.js
CHANGED
|
@@ -8,14 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* spawn runtime 时把 <ws>/.crew 置于 PATH 最前,于是 agent 的 `crew ...` 命中 wrapper。
|
|
10
10
|
*/
|
|
11
|
-
import { mkdir, writeFile, readFile, chmod, access, lstat
|
|
11
|
+
import { mkdir, writeFile, readFile, chmod, access, lstat } from "node:fs/promises";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import { dslog } from "./slog.js";
|
|
17
|
-
import { MigrationStateSchema } from "./local-memory/migration-schema.js";
|
|
18
|
-
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
19
17
|
/** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
|
|
20
18
|
export function safeKey(key) {
|
|
21
19
|
return key.replace(/[^\w.-]+/g, "_").replace(/^[-.]+/, "").slice(0, 80) || "default";
|
|
@@ -51,13 +49,7 @@ async function hasLocalMemoryV2Marker(agentDir) {
|
|
|
51
49
|
const stat = await lstat(markerPath);
|
|
52
50
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
53
51
|
return false;
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
const root = await realpath(agentDir);
|
|
57
|
-
const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(memoryDir, "migration.v1.json"), "utf8")));
|
|
58
|
-
return state.phase === "committed" && state.storageVersion === 2
|
|
59
|
-
&& state.identity.rootFingerprint === createHash("sha256").update(root).digest("hex")
|
|
60
|
-
&& detectLocalMemoryStorageVersion(await readFile(join(root, "MEMORY.md"), "utf8")) === 2;
|
|
52
|
+
return (await readFile(markerPath, "utf8")) === "1\n";
|
|
61
53
|
}
|
|
62
54
|
catch {
|
|
63
55
|
return false;
|
package/package.json
CHANGED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { prepareWorkspace } from "../workspace.js";
|
|
2
|
-
import { awaitWithCancellation } from "../runtime-cancellation.js";
|
|
3
|
-
import { logLocalMemoryContextPrepareFailure } from "../local-memory-telemetry.js";
|
|
4
|
-
import { resolveLocalMemoryV2Rollout } from "./rollout.js";
|
|
5
|
-
import { initializeLocalMemoryV2ForWorkspace } from "./runtime.js";
|
|
6
|
-
import { withMemoryAccess } from "./migration-access.js";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
-
// A fresh Agent may receive the daemon's full ten-wide execution burst. The
|
|
10
|
-
// first turn publishes schema2 durably; the remaining turns must wait to reuse
|
|
11
|
-
// that committed fact instead of timing out on the generic short IO window.
|
|
12
|
-
const EXECUTION_MEMORY_ACCESS_WAIT_MS = 30_000;
|
|
13
|
-
export async function prepareExecutionMemoryWorkspace(input, isolated, cancellation) {
|
|
14
|
-
try {
|
|
15
|
-
const prepare = async (context) => {
|
|
16
|
-
const prepared = await prepareWorkspace({
|
|
17
|
-
agentsRoot: input.launch.agentsRoot, handle: input.handle, cliPath: input.launch.cliPath,
|
|
18
|
-
executionId: input.executionId,
|
|
19
|
-
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
20
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
21
|
-
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
22
|
-
...(input.sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint: input.sessionContextFingerprint }),
|
|
23
|
-
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
24
|
-
...(isolated ? { skipLocalMemory: true } : {}),
|
|
25
|
-
});
|
|
26
|
-
const workspace = context?.created ? { ...prepared, agentHomePreexisted: false } : prepared;
|
|
27
|
-
const initialMode = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle, serverUrl: input.launch.serverUrl,
|
|
28
|
-
agentId: input.launch.agentId, requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
|
|
29
|
-
...(input.launch.description ? { description: input.launch.description } : {}) });
|
|
30
|
-
const initialized = await initializeLocalMemoryV2ForWorkspace(workspace, initialMode.enabled, input.launch.agentId);
|
|
31
|
-
const memory = initialized && !workspace.localMemoryV2Initialized
|
|
32
|
-
? await readFile(join(workspace.dir, "MEMORY.md"), "utf8")
|
|
33
|
-
: workspace.memory;
|
|
34
|
-
const currentWorkspace = { ...workspace, memory };
|
|
35
|
-
const mode = initialized && !workspace.localMemoryV2Initialized
|
|
36
|
-
? await resolveLocalMemoryV2Rollout({ workspace: currentWorkspace, handle: input.handle,
|
|
37
|
-
serverUrl: input.launch.serverUrl, agentId: input.launch.agentId,
|
|
38
|
-
requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
|
|
39
|
-
...(input.launch.description ? { description: input.launch.description } : {}) })
|
|
40
|
-
: initialMode;
|
|
41
|
-
return { workspace: { ...currentWorkspace, localMemoryV2Enabled: mode.enabled }, mode, initialized };
|
|
42
|
-
};
|
|
43
|
-
return await awaitWithCancellation(isolated ? prepare()
|
|
44
|
-
: withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, {
|
|
45
|
-
create: true,
|
|
46
|
-
waitMs: EXECUTION_MEMORY_ACCESS_WAIT_MS,
|
|
47
|
-
initializedRecoveryAgentId: input.launch.agentId,
|
|
48
|
-
}), cancellation);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
|
|
52
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", error);
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { parseFullMemoryIndex, LOCAL_MEMORY_GENERATED_INDEX_PATTERN } from "./storage-format.js";
|
|
2
|
-
const excerpt = (text, size) => Array.from(text).slice(0, size).join("");
|
|
3
|
-
/** 查询完整元数据;只在明确的历史入口返回非有效记录。正文仍按引用和哈希分页读取。 */
|
|
4
|
-
export function searchFullMemoryIndex(memory, sha256, input, outputBytes) {
|
|
5
|
-
const records = parseFullMemoryIndex(memory);
|
|
6
|
-
const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
|
|
7
|
-
const prefix = memory.slice(0, block.index);
|
|
8
|
-
let line = (prefix.match(/\n/gu) ?? []).length + 1;
|
|
9
|
-
let offset = Array.from(prefix).length;
|
|
10
|
-
const positions = new Map();
|
|
11
|
-
for (const part of block[0].matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
12
|
-
const id = /^- id: ([A-Za-z0-9_-]+)\r?\n?$/u.exec(part[0])?.[1];
|
|
13
|
-
if (id)
|
|
14
|
-
positions.set(id, { line, offset });
|
|
15
|
-
line += 1;
|
|
16
|
-
offset += Array.from(part[0]).length;
|
|
17
|
-
}
|
|
18
|
-
const terms = input.query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
|
|
19
|
-
const candidates = records.filter(entry => {
|
|
20
|
-
const active = entry.status === "active" || entry.status === "pinned";
|
|
21
|
-
const content = `${entry.id} ${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
|
|
22
|
-
return active !== input.history && terms.every(term => content.includes(term));
|
|
23
|
-
});
|
|
24
|
-
const matches = [];
|
|
25
|
-
let bytes = 512, hasMore = false;
|
|
26
|
-
for (const entry of candidates) {
|
|
27
|
-
const position = positions.get(entry.id);
|
|
28
|
-
if (!position)
|
|
29
|
-
throw new Error("memory_storage_position_invalid");
|
|
30
|
-
if (position.line <= input.afterLine)
|
|
31
|
-
continue;
|
|
32
|
-
const item = { ...position, id: entry.id, status: entry.status, revision: entry.revision,
|
|
33
|
-
file: entry.file, anchor: entry.anchor, heading: excerpt(entry.title, 96),
|
|
34
|
-
title: excerpt(entry.title, 120), summary: excerpt(entry.summary, 240), text: excerpt(entry.summary, 240) };
|
|
35
|
-
const size = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
36
|
-
if (matches.length >= input.limit || bytes + size > outputBytes) {
|
|
37
|
-
hasMore = true;
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
matches.push(item);
|
|
41
|
-
bytes += size;
|
|
42
|
-
}
|
|
43
|
-
if (hasMore && matches.length === 0)
|
|
44
|
-
throw new Error("memory_output_too_large");
|
|
45
|
-
return { file: "MEMORY.md", storageVersion: 2, sha256, matches, matchCount: candidates.length,
|
|
46
|
-
hasMore, complete: !hasMore, nextAfterLine: hasMore ? matches.at(-1).line : null };
|
|
47
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
-
const INITIALIZED_IDENTITY = "local-initialized";
|
|
4
|
-
export async function localMemoryInitializationIdentity(agentDir, agentId) {
|
|
5
|
-
const root = await realpath(agentDir);
|
|
6
|
-
const rootFingerprint = createHash("sha256").update(root).digest("hex");
|
|
7
|
-
const operation = createHash("sha256").update(`${agentId}\0${root}`, "utf8").digest("hex").slice(0, 24);
|
|
8
|
-
return {
|
|
9
|
-
workspaceId: INITIALIZED_IDENTITY,
|
|
10
|
-
agentId,
|
|
11
|
-
migrationId: `initialize-${operation}`,
|
|
12
|
-
sourceStorageVersion: 0,
|
|
13
|
-
targetStorageVersion: 2,
|
|
14
|
-
rootFingerprint,
|
|
15
|
-
ownerIdentity: String((await lstat(root)).uid),
|
|
16
|
-
machineId: INITIALIZED_IDENTITY,
|
|
17
|
-
executionId: `initialize-${operation}`,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
export function isLocalMemoryInitializationIdentity(actual, expected) {
|
|
21
|
-
return Object.keys(expected)
|
|
22
|
-
.every(key => actual[key] === expected[key]);
|
|
23
|
-
}
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
3
|
-
import { inventoryLimits, readMemoryInventorySource, scanMemorySources } from "./inventory-source.js";
|
|
4
|
-
import { detectLocalMemoryStorageVersion, LOCAL_MEMORY_GENERATED_INDEX_PATTERN } from "./storage-format.js";
|
|
5
|
-
const CURSOR_VERSION = 1;
|
|
6
|
-
const MAX_METADATA_PATH_BYTES = 4 * 1024;
|
|
7
|
-
const MAX_METADATA_WARNING_BYTES = 2 * 1024;
|
|
8
|
-
const excerpt = (text, size) => {
|
|
9
|
-
const characters = Array.from(text);
|
|
10
|
-
return characters.length > size ? `${characters.slice(0, size).join("")}...` : text;
|
|
11
|
-
};
|
|
12
|
-
function originalLowercaseIndex(text, index) {
|
|
13
|
-
let originalIndex = 0;
|
|
14
|
-
let lowerIndex = 0;
|
|
15
|
-
for (const character of text) {
|
|
16
|
-
lowerIndex += character.toLowerCase().length;
|
|
17
|
-
if (index < lowerIndex)
|
|
18
|
-
return originalIndex;
|
|
19
|
-
originalIndex += character.length;
|
|
20
|
-
}
|
|
21
|
-
return text.length;
|
|
22
|
-
}
|
|
23
|
-
function encodeCursor(cursor) {
|
|
24
|
-
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
25
|
-
}
|
|
26
|
-
function decodeCursor(value) {
|
|
27
|
-
let parsed;
|
|
28
|
-
try {
|
|
29
|
-
parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw new Error("memory_cursor_invalid");
|
|
33
|
-
}
|
|
34
|
-
if (!parsed || typeof parsed !== "object")
|
|
35
|
-
throw new Error("memory_cursor_invalid");
|
|
36
|
-
const record = parsed;
|
|
37
|
-
if (record.version !== CURSOR_VERSION || typeof record.file !== "string"
|
|
38
|
-
|| !/^[a-f0-9]{64}$/u.test(String(record.sha256))
|
|
39
|
-
|| typeof record.afterLine !== "number" || !Number.isSafeInteger(record.afterLine) || record.afterLine < 0) {
|
|
40
|
-
throw new Error("memory_cursor_invalid");
|
|
41
|
-
}
|
|
42
|
-
return { version: 1, file: record.file, sha256: String(record.sha256), afterLine: record.afterLine };
|
|
43
|
-
}
|
|
44
|
-
function boundStringList(values, maxBytes) {
|
|
45
|
-
const bounded = [];
|
|
46
|
-
for (const value of values) {
|
|
47
|
-
const candidate = [...bounded, value];
|
|
48
|
-
if (Buffer.byteLength(JSON.stringify(candidate), "utf8") > maxBytes)
|
|
49
|
-
break;
|
|
50
|
-
bounded.push(value);
|
|
51
|
-
}
|
|
52
|
-
return { values: bounded, truncated: bounded.length < values.length };
|
|
53
|
-
}
|
|
54
|
-
function sourceMatches(source, query, afterLine, limit, outputBytes) {
|
|
55
|
-
const terms = query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
|
|
56
|
-
const matches = [];
|
|
57
|
-
let line = 0;
|
|
58
|
-
let offset = 0;
|
|
59
|
-
let heading = "";
|
|
60
|
-
let matchCount = 0;
|
|
61
|
-
let hasMore = false;
|
|
62
|
-
let outputTooLarge = false;
|
|
63
|
-
let firstMatchLine = null;
|
|
64
|
-
let remaining = outputBytes;
|
|
65
|
-
const generatedIndex = source.path === "MEMORY.md"
|
|
66
|
-
? LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(source.content)
|
|
67
|
-
: null;
|
|
68
|
-
const generatedStart = generatedIndex === null ? -1 : Array.from(source.content.slice(0, generatedIndex.index)).length;
|
|
69
|
-
const generatedEnd = generatedIndex === null ? -1 : generatedStart + Array.from(generatedIndex[0]).length;
|
|
70
|
-
for (const match of source.content.matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
71
|
-
if (match[0] === "")
|
|
72
|
-
break;
|
|
73
|
-
line += 1;
|
|
74
|
-
const raw = match[0];
|
|
75
|
-
const lineStart = offset;
|
|
76
|
-
const lineEnd = offset + Array.from(raw).length;
|
|
77
|
-
const content = raw.replace(/\r?\n$/u, "");
|
|
78
|
-
const title = /^#{1,6}\s+(.+?)\s*$/u.exec(content)?.[1];
|
|
79
|
-
if (title !== undefined)
|
|
80
|
-
heading = excerpt(title, 96);
|
|
81
|
-
const lower = content.normalize("NFKC").toLowerCase();
|
|
82
|
-
const insideGeneratedIndex = generatedStart >= 0 && lineStart < generatedEnd && lineEnd > generatedStart;
|
|
83
|
-
if (!insideGeneratedIndex && terms.every(term => lower.includes(term))) {
|
|
84
|
-
matchCount += 1;
|
|
85
|
-
if (line > afterLine) {
|
|
86
|
-
firstMatchLine ??= line;
|
|
87
|
-
const matchIndex = lower.indexOf(terms[0] ?? "");
|
|
88
|
-
let start = Math.max(0, originalLowercaseIndex(content, matchIndex) - 60);
|
|
89
|
-
if (start > 0 && /[\uDC00-\uDFFF]/u.test(content[start]))
|
|
90
|
-
start -= 1;
|
|
91
|
-
const item = { file: source.path, sourceSha256: source.sha256, legacy: true, opaque: true,
|
|
92
|
-
line, offset, heading, text: `${start > 0 ? "..." : ""}${excerpt(content.slice(start), 240)}` };
|
|
93
|
-
const bytes = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
94
|
-
if (matches.length >= limit)
|
|
95
|
-
hasMore = true;
|
|
96
|
-
else if (bytes > remaining) {
|
|
97
|
-
hasMore = true;
|
|
98
|
-
outputTooLarge = matches.length === 0;
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
matches.push(item);
|
|
102
|
-
remaining -= bytes;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
offset += Array.from(raw).length;
|
|
107
|
-
}
|
|
108
|
-
return { matches, matchCount, hasMore, outputTooLarge, firstMatchLine,
|
|
109
|
-
bytesUsed: outputBytes - remaining,
|
|
110
|
-
nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null };
|
|
111
|
-
}
|
|
112
|
-
function issueCode(error) {
|
|
113
|
-
if (error instanceof TypeError)
|
|
114
|
-
return "invalid_utf8";
|
|
115
|
-
if (error instanceof Error)
|
|
116
|
-
return error.code ?? error.message;
|
|
117
|
-
return "read_failed";
|
|
118
|
-
}
|
|
119
|
-
async function readRequestedSources(agentDir, files, excludeFiles) {
|
|
120
|
-
const root = await realpath(agentDir);
|
|
121
|
-
const sources = [];
|
|
122
|
-
const issues = [];
|
|
123
|
-
let remaining = inventoryLimits.totalBytes;
|
|
124
|
-
for (const file of [...new Set(files)].sort((left, right) => left.localeCompare(right))) {
|
|
125
|
-
if (excludeFiles?.has(file))
|
|
126
|
-
continue;
|
|
127
|
-
try {
|
|
128
|
-
const source = await readMemoryInventorySource(root, file, remaining);
|
|
129
|
-
remaining -= source.bytes;
|
|
130
|
-
sources.push(source);
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
issues.push({ code: issueCode(error), path: file });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return { sources, issues };
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Read-only adapter for old MEMORY.md/notes content. It is only called from
|
|
140
|
-
* explicit search commands; prompt preparation never invokes it.
|
|
141
|
-
*/
|
|
142
|
-
export async function searchLegacyMemorySources(input) {
|
|
143
|
-
const scan = input.files === undefined
|
|
144
|
-
? await scanMemorySources(input.agentDir, { includeOperationalFiles: false })
|
|
145
|
-
: null;
|
|
146
|
-
const requestedResult = input.files === undefined
|
|
147
|
-
? null
|
|
148
|
-
: await readRequestedSources(input.agentDir, input.files, input.excludeFiles);
|
|
149
|
-
const requested = input.files === undefined
|
|
150
|
-
? scan.sources.filter(source => source.path === "MEMORY.md" || source.path.startsWith("notes/"))
|
|
151
|
-
: requestedResult.sources;
|
|
152
|
-
const sources = input.files === undefined
|
|
153
|
-
? requested.filter(source => !(input.excludeFiles?.has(source.path) ?? false))
|
|
154
|
-
: requested;
|
|
155
|
-
const detectedIndexVersion = await (async () => {
|
|
156
|
-
try {
|
|
157
|
-
const memory = sources.find(source => source.path === "MEMORY.md")
|
|
158
|
-
?? await readMemoryInventorySource(await realpath(input.agentDir), "MEMORY.md", inventoryLimits.fileBytes);
|
|
159
|
-
return detectLocalMemoryStorageVersion(memory.content);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
})();
|
|
165
|
-
const relevantIssues = input.files === undefined
|
|
166
|
-
? scan.issues.filter(issue => issue.path === "MEMORY.md" || issue.path.startsWith("notes/"))
|
|
167
|
-
: requestedResult.issues;
|
|
168
|
-
const cursor = input.cursor === undefined ? null : decodeCursor(input.cursor);
|
|
169
|
-
if (cursor !== null && !sources.some(source => source.path === cursor.file))
|
|
170
|
-
throw new Error("memory_cursor_invalid");
|
|
171
|
-
const ordered = [...sources].sort((left, right) => left.path.localeCompare(right.path));
|
|
172
|
-
const matches = [];
|
|
173
|
-
let matchCount = 0;
|
|
174
|
-
let hasMore = false;
|
|
175
|
-
let nextCursor = null;
|
|
176
|
-
let started = cursor === null;
|
|
177
|
-
let pageFull = false;
|
|
178
|
-
const allUnindexedFiles = ordered.map(source => source.path);
|
|
179
|
-
const boundedFiles = boundStringList(allUnindexedFiles, MAX_METADATA_PATH_BYTES);
|
|
180
|
-
const allWarnings = relevantIssues.map(issue => ({ code: issue.code, path: issue.path }));
|
|
181
|
-
const boundedWarnings = boundStringList(allWarnings.map(warning => `${warning.code}\u0000${warning.path}`), MAX_METADATA_WARNING_BYTES);
|
|
182
|
-
const warnings = boundedWarnings.values.map(value => {
|
|
183
|
-
const separator = value.indexOf("\u0000");
|
|
184
|
-
return { code: separator < 0 ? value : value.slice(0, separator), path: separator < 0 ? "" : value.slice(separator + 1) };
|
|
185
|
-
});
|
|
186
|
-
const outputReservation = Buffer.byteLength(JSON.stringify({
|
|
187
|
-
source: "legacy", legacy: true, opaque: true, readerMode: "legacy", storageVersion: detectedIndexVersion,
|
|
188
|
-
detectedIndexVersion,
|
|
189
|
-
...(ordered.length === 1 ? { file: ordered[0].path, sha256: ordered[0].sha256 } : {}),
|
|
190
|
-
matches: [], matchCount: 0, hasMore: false, complete: false, nextAfterLine: null,
|
|
191
|
-
unindexedFiles: boundedFiles.values, unindexedFileCount: allUnindexedFiles.length,
|
|
192
|
-
unindexedFilesTruncated: boundedFiles.truncated, nextCursor: null,
|
|
193
|
-
warnings, warningCount: allWarnings.length, warningsTruncated: boundedWarnings.truncated,
|
|
194
|
-
}), "utf8") + 256;
|
|
195
|
-
let remainingOutputBytes = Math.max(0, input.outputBytes - outputReservation);
|
|
196
|
-
for (const source of ordered) {
|
|
197
|
-
if (!started) {
|
|
198
|
-
if (source.path !== cursor.file)
|
|
199
|
-
continue;
|
|
200
|
-
if (source.sha256 !== cursor.sha256)
|
|
201
|
-
throw new Error("memory_file_changed");
|
|
202
|
-
started = true;
|
|
203
|
-
}
|
|
204
|
-
const afterLine = source.path === cursor?.file ? cursor.afterLine
|
|
205
|
-
: cursor === null && source.path === ordered[0]?.path ? input.afterLine ?? 0 : 0;
|
|
206
|
-
const result = sourceMatches(source, input.query, afterLine, pageFull ? 0 : Math.max(0, input.limit - matches.length), remainingOutputBytes);
|
|
207
|
-
matches.push(...result.matches);
|
|
208
|
-
matchCount += result.matchCount;
|
|
209
|
-
remainingOutputBytes -= result.bytesUsed;
|
|
210
|
-
if (result.hasMore) {
|
|
211
|
-
hasMore = true;
|
|
212
|
-
pageFull = true;
|
|
213
|
-
if (result.outputTooLarge && matches.length === 0)
|
|
214
|
-
throw new Error("memory_output_too_large");
|
|
215
|
-
if (nextCursor === null) {
|
|
216
|
-
const last = result.matches.at(-1);
|
|
217
|
-
nextCursor = encodeCursor({ version: 1, file: source.path, sha256: source.sha256,
|
|
218
|
-
afterLine: Number(last?.line ?? Math.max(0, result.firstMatchLine ?? 1) - 1) });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
const complete = !hasMore && relevantIssues.length === 0;
|
|
223
|
-
return {
|
|
224
|
-
source: "legacy", legacy: true, opaque: true, readerMode: "legacy",
|
|
225
|
-
storageVersion: detectedIndexVersion, detectedIndexVersion,
|
|
226
|
-
...(ordered.length === 1 ? { file: ordered[0].path, sha256: ordered[0].sha256 } : {}),
|
|
227
|
-
matches, matchCount, hasMore, complete, nextAfterLine: hasMore && ordered.length === 1
|
|
228
|
-
? Number(matches.at(-1)?.line ?? 0) : null,
|
|
229
|
-
unindexedFiles: boundedFiles.values, unindexedFileCount: allUnindexedFiles.length,
|
|
230
|
-
unindexedFilesTruncated: boundedFiles.truncated, nextCursor, warnings,
|
|
231
|
-
warningCount: allWarnings.length, warningsTruncated: boundedWarnings.truncated,
|
|
232
|
-
};
|
|
233
|
-
}
|