@nowcrew/daemon 0.6.93-beta.1 → 0.6.93-beta.10
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/cloud-report-upload.js +1 -10
- package/dist/execution-runner.js +1 -0
- package/dist/local-executor.js +25 -30
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +9 -2
- package/dist/local-memory/initialization-identity.js +18 -5
- package/dist/local-memory/inventory-cli.js +33 -17
- package/dist/local-memory/inventory-v2.js +282 -0
- package/dist/local-memory/migration-access.js +31 -8
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +37 -3
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +7 -35
- package/dist/local-memory/pending-query.js +5 -2
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +16 -2
- package/dist/local-memory/prompt.js +2 -2
- package/dist/local-memory/read-cli.js +4 -3
- package/dist/local-memory/rollout.js +32 -10
- package/dist/local-memory/runtime.js +46 -13
- package/dist/local-memory/write.js +63 -2
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +55 -13
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/memory-migration-controller.js +2 -0
- package/dist/memory-migration-inspect.js +40 -7
- package/dist/prompt.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +2 -0
- package/dist/workspace.js +3 -0
- package/package.json +1 -1
|
@@ -86,8 +86,13 @@ export async function prepareEnvironmentExecutionSnapshot(input) {
|
|
|
86
86
|
directory,
|
|
87
87
|
prompt: `## Per-directory environment configuration\nThis execution uses environment revision ${parsed.data.revision}. `
|
|
88
88
|
+ `Directory layers are independent: Agent root is inherited by training and each repo; sibling repos do not inherit each other. `
|
|
89
|
-
+ `
|
|
90
|
-
+
|
|
89
|
+
+ `The runtime starts in the task directory \`$CREW_TASK_DIR\`, which uses Agent-root variables. `
|
|
90
|
+
+ `Training variables are available only when the harness \`--cwd\` points to \`$QA_RUNTIME_ROOT\` (the training directory); `
|
|
91
|
+
+ `do not pass \`$PWD\` unless it is that directory. For a training command, use: `
|
|
92
|
+
+ `\`node ${JSON.stringify(join(directory, "run.mjs"))} --cwd \"$QA_RUNTIME_ROOT\" -- <command> [arguments...]\`. `
|
|
93
|
+
+ `For a repository command, pass that repository's absolute root to \`--cwd\`. `
|
|
94
|
+
+ `Keep task outputs under \`$CREW_TASK_DIR\` and pass absolute values such as \`$CREW_TASK_DIR/outputs\` to --report-root, --output-dir, and --runtime-dir; `
|
|
95
|
+
+ `do not let changing the harness cwd relocate task outputs. `
|
|
91
96
|
+ `Changing directory alone does not reload process variables. Do not source mutable .env files or print environment values.`,
|
|
92
97
|
};
|
|
93
98
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir, hostname, platform } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
3
|
const ALIYUN_MARKER = /alibaba|aliyun|alicloud/iu;
|
|
5
4
|
function readMarker(path) {
|
|
6
5
|
try {
|
|
@@ -29,15 +28,7 @@ export function detectCloudProvider(env = process.env, platformName = platform()
|
|
|
29
28
|
}
|
|
30
29
|
export function findOssUploadScript(env = process.env, exists = existsSync) {
|
|
31
30
|
const explicit = env.CREW_OSS_UPLOAD_SCRIPT?.trim();
|
|
32
|
-
|
|
33
|
-
return exists(explicit) ? explicit : undefined;
|
|
34
|
-
const home = env.HOME?.trim() || homedir();
|
|
35
|
-
const candidates = [
|
|
36
|
-
join(home, ".claude", "skills", "oss-upload", "scripts", "oss_upload.py"),
|
|
37
|
-
join(home, ".codex", "skills", "oss-upload", "scripts", "oss_upload.py"),
|
|
38
|
-
join(home, ".agents", "skills", "oss-upload", "scripts", "oss_upload.py"),
|
|
39
|
-
];
|
|
40
|
-
return candidates.find(exists);
|
|
31
|
+
return explicit && exists(explicit) ? explicit : undefined;
|
|
41
32
|
}
|
|
42
33
|
export function cloudReportUploadFacts(env = process.env, platformName = platform(), hostName = hostname(), readMarkerValue, exists) {
|
|
43
34
|
const provider = detectCloudProvider(env, platformName, hostName, readMarkerValue);
|
package/dist/execution-runner.js
CHANGED
|
@@ -881,6 +881,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
881
881
|
serverUrl: config.serverUrl,
|
|
882
882
|
token: credential.token,
|
|
883
883
|
agentId: credential.agentId,
|
|
884
|
+
...(dependencies.controlIdentity === undefined ? {} : dependencies.controlIdentity),
|
|
884
885
|
agentsRoot: config.agentsRoot,
|
|
885
886
|
reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
|
|
886
887
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
package/dist/local-executor.js
CHANGED
|
@@ -39,6 +39,7 @@ import { projectSkillRuntimeDirectories, } from "./project-skills/runtime-launch
|
|
|
39
39
|
import { logIdentityRosterRuntimePrepared } from "./identity-roster-telemetry.js";
|
|
40
40
|
import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, logMemoryPruneDiagnosticsFailure, memoryPruneSnapshotFields, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
41
41
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
42
|
+
import { LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS } from "./local-memory-diagnostics.js";
|
|
42
43
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
43
44
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
44
45
|
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
@@ -49,6 +50,9 @@ import { prepareExecutionMemoryWorkspace } from "./local-memory/execution-worksp
|
|
|
49
50
|
import { localMemoryV2Environment } from "./config.js";
|
|
50
51
|
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
51
52
|
import { MEMORY_STATE_GUARD_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY } from "./memory-migration-protocol.js";
|
|
53
|
+
import { resolveLocalMemoryWriterMode } from "./local-memory/writer-mode.js";
|
|
54
|
+
import { logLocalMemoryCommitApplied } from "./local-memory/commit-telemetry.js";
|
|
55
|
+
import { shouldUseMigrationDeferredPending } from "./local-memory/migration-pending.js";
|
|
52
56
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
53
57
|
constructor(runtime, timeoutMs) {
|
|
54
58
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -62,7 +66,6 @@ const STDERR_TAIL_CAP = 2_000;
|
|
|
62
66
|
// the old failure mode where a callback could hold execution slots forever.
|
|
63
67
|
const RUNTIME_RUNNING_CALLBACK_TIMEOUT_MS = 30_000;
|
|
64
68
|
const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
|
|
65
|
-
const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
|
|
66
69
|
const CLAUDE_INSTRUCTION_WARNING = "claude_additional_directory_instructions_unverified";
|
|
67
70
|
const CODEX_COLLABORATION_EXEC_WARNING = "codex_collaboration_disabled_on_exec";
|
|
68
71
|
function withoutClaudeInstructionFlag(env) {
|
|
@@ -219,6 +222,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
219
222
|
const memoryRollout = preparedMemory.mode;
|
|
220
223
|
input = { ...input, localMemoryV2Enabled: memoryRollout.enabled };
|
|
221
224
|
const localMemoryV2Initialized = preparedMemory.initialized;
|
|
225
|
+
let localMemoryWriterMode = resolveLocalMemoryWriterMode(workspace, memoryRollout);
|
|
222
226
|
let materialized = null;
|
|
223
227
|
let knownAttachmentDirectory = null;
|
|
224
228
|
const startupReservationState = { current: null };
|
|
@@ -229,6 +233,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
229
233
|
let memoryPruneBeforeSnapshot = null;
|
|
230
234
|
let memoryPruneSharedWriteKey = null;
|
|
231
235
|
let localMemoryResultPath = null;
|
|
236
|
+
let migrationDeferredPending = false;
|
|
232
237
|
let environmentSnapshot;
|
|
233
238
|
let executionWorkspace = workspace;
|
|
234
239
|
let localMemoryTelemetry = null;
|
|
@@ -367,21 +372,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
367
372
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
368
373
|
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
369
374
|
memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
|
|
370
|
-
|
|
371
|
-
|
|
375
|
+
localMemoryWriterMode = resolveLocalMemoryWriterMode(executionWorkspace, memoryRollout);
|
|
376
|
+
migrationDeferredPending = shouldUseMigrationDeferredPending({ pruneTraceId: memoryPruneTraceId,
|
|
377
|
+
writerMode: localMemoryWriterMode, rolloutEnabled: memoryRollout.enabled,
|
|
378
|
+
legacyFallback: memoryRollout.legacyFallback,
|
|
379
|
+
failure: memoryRollout.failure, memory: executionWorkspace.memory });
|
|
380
|
+
if (memoryRollout.deferred || migrationDeferredPending)
|
|
381
|
+
reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune", memoryRollout.legacyFallback, memoryRollout.failure, migrationDeferredPending);
|
|
382
|
+
if (memoryPruneTraceId !== null && localMemoryWriterMode === "blocked" && !migrationDeferredPending) {
|
|
383
|
+
throw new Error(memoryRollout.failure === "inspection_failed" ? "local_memory_inspection_failed" : "local_memory_v2_requires_migration");
|
|
384
|
+
}
|
|
372
385
|
if (memoryPruneTraceId !== null) {
|
|
373
386
|
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
374
387
|
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
375
388
|
baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
376
389
|
}
|
|
377
|
-
if (
|
|
390
|
+
if ((localMemoryWriterMode === "v2" && memoryPruneTraceId !== null
|
|
378
391
|
&& isLocalMemoryV2Eligible({
|
|
379
392
|
enabledForHandle: true,
|
|
380
393
|
agentHomePreexisted: workspace.agentHomePreexisted === true,
|
|
381
394
|
memorySeedCreated: executionWorkspace.memorySeedCreated === true && (localMemoryV2Initialized || workspace.localMemoryV2Initialized !== false),
|
|
382
395
|
localMemoryV2Initialized,
|
|
383
396
|
memoryPrune: true,
|
|
384
|
-
})) {
|
|
397
|
+
})) || migrationDeferredPending) {
|
|
385
398
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
386
399
|
}
|
|
387
400
|
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
@@ -393,6 +406,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
393
406
|
|| detectLocalMemoryStorageVersion(executionWorkspace.memory) === 2),
|
|
394
407
|
deferred: memoryRollout.deferred,
|
|
395
408
|
...(memoryRollout.legacyFallback ? { legacyFallback: true } : {}),
|
|
409
|
+
...(migrationDeferredPending ? { migrationDeferred: true } : {}),
|
|
396
410
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
397
411
|
});
|
|
398
412
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
@@ -894,10 +908,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
894
908
|
exitCode,
|
|
895
909
|
workLogPath: executionWorkspace.workLogPath,
|
|
896
910
|
expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
|
|
897
|
-
...(
|
|
911
|
+
...(localMemoryWriterMode === "v2" ? {
|
|
898
912
|
writerCapabilities: [MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY],
|
|
899
913
|
requiredStorageVersion: 2,
|
|
900
914
|
} : {}),
|
|
915
|
+
...(migrationDeferredPending ? { migrationDeferredPending: true } : {}),
|
|
901
916
|
});
|
|
902
917
|
const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
|
|
903
918
|
if (committed.status === "failed" || committed.status === "missing") {
|
|
@@ -912,32 +927,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
912
927
|
throw new Error(`local_memory_commit_failed: ${reason}`);
|
|
913
928
|
}
|
|
914
929
|
if (committed.status === "applied") {
|
|
915
|
-
if (
|
|
916
|
-
|
|
917
|
-
|| committed.committedState !== true)) {
|
|
930
|
+
if (localMemoryWriterMode === "v2" && (committed.storageVersionBefore !== 2 || committed.storageVersionAfter !== 2
|
|
931
|
+
|| committed.committedState !== true)) {
|
|
918
932
|
throw new Error("local_memory_commit_failed: local_memory_v2_commit_state_invalid");
|
|
919
933
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
count: ids?.length ?? 0,
|
|
923
|
-
truncated: (ids?.length ?? 0) > 100,
|
|
924
|
-
});
|
|
925
|
-
const created = boundedIds(committed.createdIds);
|
|
926
|
-
const updated = boundedIds(committed.updatedIds);
|
|
927
|
-
const statusChanged = boundedIds(committed.statusChangedIds);
|
|
928
|
-
const pending = boundedIds(committed.pendingIds);
|
|
929
|
-
dslog("local_memory.commit_applied", "结构化本地记忆提交已验证", {
|
|
930
|
-
execution_id: input.executionId, agent_handle: input.handle, prune_trace_id: memoryPruneTraceId,
|
|
931
|
-
storage_version_before: committed.storageVersionBefore,
|
|
932
|
-
storage_version_after: committed.storageVersionAfter,
|
|
933
|
-
committed_state: committed.committedState,
|
|
934
|
-
writer_mode: committed.storageVersionAfter === 2 && committed.committedState ? "v2" : "legacy",
|
|
935
|
-
created_id_count: created.count, created_ids: created.value, created_ids_truncated: created.truncated,
|
|
936
|
-
updated_id_count: updated.count, updated_ids: updated.value, updated_ids_truncated: updated.truncated,
|
|
937
|
-
status_changed_id_count: statusChanged.count, status_changed_ids: statusChanged.value,
|
|
938
|
-
status_changed_ids_truncated: statusChanged.truncated,
|
|
939
|
-
pending_id_count: pending.count, pending_ids: pending.value, pending_ids_truncated: pending.truncated,
|
|
940
|
-
});
|
|
934
|
+
logLocalMemoryCommitApplied({ executionId: input.executionId, agentHandle: input.handle,
|
|
935
|
+
pruneTraceId: memoryPruneTraceId, committed });
|
|
941
936
|
}
|
|
942
937
|
memoryPruneFailurePhase = "runtime_finalize";
|
|
943
938
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { dslog } from "../slog.js";
|
|
2
|
+
function boundedIds(ids) {
|
|
3
|
+
return {
|
|
4
|
+
value: (ids ?? []).slice(0, 100).join(","),
|
|
5
|
+
count: ids?.length ?? 0,
|
|
6
|
+
truncated: (ids?.length ?? 0) > 100,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function logLocalMemoryCommitApplied(input) {
|
|
10
|
+
const { committed } = input;
|
|
11
|
+
const created = boundedIds(committed.createdIds);
|
|
12
|
+
const updated = boundedIds(committed.updatedIds);
|
|
13
|
+
const statusChanged = boundedIds(committed.statusChangedIds);
|
|
14
|
+
const pending = boundedIds(committed.pendingIds);
|
|
15
|
+
dslog("local_memory.commit_applied", "结构化本地记忆提交已验证", {
|
|
16
|
+
execution_id: input.executionId, agent_handle: input.agentHandle, prune_trace_id: input.pruneTraceId,
|
|
17
|
+
storage_version_before: committed.storageVersionBefore,
|
|
18
|
+
storage_version_after: committed.storageVersionAfter,
|
|
19
|
+
committed_state: committed.committedState,
|
|
20
|
+
writer_mode: committed.pendingReason === "legacy_requires_migration"
|
|
21
|
+
? "pending_only"
|
|
22
|
+
: committed.storageVersionAfter === 2 && committed.committedState ? "v2" : "legacy",
|
|
23
|
+
created_id_count: created.count, created_ids: created.value, created_ids_truncated: created.truncated,
|
|
24
|
+
updated_id_count: updated.count, updated_ids: updated.value, updated_ids_truncated: updated.truncated,
|
|
25
|
+
status_changed_id_count: statusChanged.count, status_changed_ids: statusChanged.value,
|
|
26
|
+
status_changed_ids_truncated: statusChanged.truncated,
|
|
27
|
+
pending_id_count: pending.count, pending_ids: pending.value, pending_ids_truncated: pending.truncated,
|
|
28
|
+
pending_reason: committed.pendingReason,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -27,7 +27,10 @@ export async function prepareExecutionMemoryWorkspace(input, isolated, cancellat
|
|
|
27
27
|
const initialMode = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle, serverUrl: input.launch.serverUrl,
|
|
28
28
|
agentId: input.launch.agentId, requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
|
|
29
29
|
...(input.launch.description ? { description: input.launch.description } : {}) });
|
|
30
|
-
const
|
|
30
|
+
const initializationBinding = input.launch.workspaceId !== undefined && input.launch.machineId !== undefined
|
|
31
|
+
? { workspaceId: input.launch.workspaceId, machineId: input.launch.machineId }
|
|
32
|
+
: undefined;
|
|
33
|
+
const initialized = await initializeLocalMemoryV2ForWorkspace(workspace, initialMode.enabled, input.launch.agentId, initializationBinding);
|
|
31
34
|
const memory = initialized && !workspace.localMemoryV2Initialized
|
|
32
35
|
? await readFile(join(workspace.dir, "MEMORY.md"), "utf8")
|
|
33
36
|
: workspace.memory;
|
|
@@ -44,7 +47,11 @@ export async function prepareExecutionMemoryWorkspace(input, isolated, cancellat
|
|
|
44
47
|
: withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, {
|
|
45
48
|
create: true,
|
|
46
49
|
waitMs: EXECUTION_MEMORY_ACCESS_WAIT_MS,
|
|
47
|
-
|
|
50
|
+
initializedRecoveryIdentity: {
|
|
51
|
+
agentId: input.launch.agentId,
|
|
52
|
+
...(input.launch.workspaceId === undefined ? {} : { workspaceId: input.launch.workspaceId }),
|
|
53
|
+
...(input.launch.machineId === undefined ? {} : { machineId: input.launch.machineId }),
|
|
54
|
+
},
|
|
48
55
|
}), cancellation);
|
|
49
56
|
}
|
|
50
57
|
catch (error) {
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
-
const
|
|
4
|
-
export async function localMemoryInitializationIdentity(agentDir, agentId) {
|
|
3
|
+
export const LEGACY_LOCAL_INITIALIZATION_IDENTITY = "local-initialized";
|
|
4
|
+
export async function localMemoryInitializationIdentity(agentDir, agentId, binding) {
|
|
5
5
|
const root = await realpath(agentDir);
|
|
6
6
|
const rootFingerprint = createHash("sha256").update(root).digest("hex");
|
|
7
|
-
const
|
|
7
|
+
const workspaceId = binding?.workspaceId ?? LEGACY_LOCAL_INITIALIZATION_IDENTITY;
|
|
8
|
+
const machineId = binding?.machineId ?? LEGACY_LOCAL_INITIALIZATION_IDENTITY;
|
|
9
|
+
// Keep the legacy digest byte-for-byte stable so an interrupted pre-upgrade
|
|
10
|
+
// initialization can still be inspected. Every newly authenticated daemon
|
|
11
|
+
// initialization includes both control-plane identities in its operation id.
|
|
12
|
+
const operationSeed = workspaceId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
|
|
13
|
+
&& machineId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
|
|
14
|
+
? `${agentId}\0${root}`
|
|
15
|
+
: `${workspaceId}\0${machineId}\0${agentId}\0${root}`;
|
|
16
|
+
const operation = createHash("sha256").update(operationSeed, "utf8").digest("hex").slice(0, 24);
|
|
8
17
|
return {
|
|
9
|
-
workspaceId
|
|
18
|
+
workspaceId,
|
|
10
19
|
agentId,
|
|
11
20
|
migrationId: `initialize-${operation}`,
|
|
12
21
|
sourceStorageVersion: 0,
|
|
13
22
|
targetStorageVersion: 2,
|
|
14
23
|
rootFingerprint,
|
|
15
24
|
ownerIdentity: String((await lstat(root)).uid),
|
|
16
|
-
machineId
|
|
25
|
+
machineId,
|
|
17
26
|
executionId: `initialize-${operation}`,
|
|
18
27
|
};
|
|
19
28
|
}
|
|
29
|
+
export function isLegacyLocalMemoryInitializationIdentity(identity) {
|
|
30
|
+
return identity.workspaceId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
|
|
31
|
+
&& identity.machineId === LEGACY_LOCAL_INITIALIZATION_IDENTITY;
|
|
32
|
+
}
|
|
20
33
|
export function isLocalMemoryInitializationIdentity(actual, expected) {
|
|
21
34
|
return Object.keys(expected)
|
|
22
35
|
.every(key => actual[key] === expected[key]);
|
|
@@ -2,9 +2,9 @@ const CommandSchema = z.object({
|
|
|
2
2
|
"agent-dir": z.string().min(1).refine(isAbsolute),
|
|
3
3
|
"dry-run": z.literal(true),
|
|
4
4
|
}).strict();
|
|
5
|
-
const USAGE = "
|
|
5
|
+
const USAGE = "只读记忆盘点、schema0/1 转换预演与 schema2 完整性校验,不写文件,不支持 --apply。\n"
|
|
6
6
|
+ "pnpm --filter @nowcrew/daemon exec tsx src/local-memory/inventory-cli.ts --agent-dir <单个Agent绝对路径> --dry-run\n"
|
|
7
|
-
+ "报告包含原始记忆,需作为敏感本地资料保存;退出码0
|
|
7
|
+
+ "报告包含原始记忆,需作为敏感本地资料保存;退出码0仅代表盘点、格式预演或完整性校验无阻塞,不代表批准迁移。\n";
|
|
8
8
|
export async function runMemoryInventoryCommand(args, io) {
|
|
9
9
|
if (args.length === 1 && args[0] === "--help") {
|
|
10
10
|
io.out(USAGE);
|
|
@@ -24,20 +24,32 @@ export async function runMemoryInventoryCommand(args, io) {
|
|
|
24
24
|
try {
|
|
25
25
|
const scan = await scanMemorySources(command["agent-dir"]);
|
|
26
26
|
const scanComplete = scan.issues.length === 0;
|
|
27
|
-
const
|
|
27
|
+
const memory = scan.sources.find((source) => source.path === "MEMORY.md")?.content ?? "";
|
|
28
|
+
let storageVersion = null;
|
|
29
|
+
let detectionIssue = null;
|
|
30
|
+
try {
|
|
31
|
+
storageVersion = detectLocalMemoryStorageVersion(memory);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
detectionIssue = {
|
|
35
|
+
code: error instanceof Error && /^[a-z0-9_]+$/u.test(error.message)
|
|
36
|
+
? error.message
|
|
37
|
+
: "local_memory_storage_version_invalid",
|
|
38
|
+
path: "MEMORY.md",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const v2 = storageVersion === 2
|
|
42
|
+
? await inspectV2MemoryInventory(scan.root, scan.sources, scanComplete)
|
|
43
|
+
: null;
|
|
44
|
+
const analysis = detectionIssue
|
|
45
|
+
? { entries: [], issues: [detectionIssue], preview: null, rawMemory: memory }
|
|
46
|
+
: v2?.analysis ?? previewMemoryInventory(scan.sources, scanComplete);
|
|
28
47
|
const complete = scanComplete && !analysis.issues.some((issue) => issue.code === "missing_detail");
|
|
29
48
|
const issues = [...scan.issues, ...analysis.issues];
|
|
30
49
|
const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
|
|
31
|
-
const
|
|
50
|
+
const reportSources = v2?.stateSource ? [...scan.sources, v2.stateSource] : scan.sources;
|
|
51
|
+
const files = reportSources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
|
|
32
52
|
const orphanNotes = files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path);
|
|
33
|
-
const storageVersion = (() => {
|
|
34
|
-
try {
|
|
35
|
-
return detectLocalMemoryStorageVersion(scan.sources.find((source) => source.path === "MEMORY.md")?.content ?? "");
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
})();
|
|
41
53
|
const indexedEntries = analysis.entries.filter((entry) => entry.format === "managed").length;
|
|
42
54
|
const legacyEntries = analysis.entries.length - indexedEntries;
|
|
43
55
|
const report = {
|
|
@@ -47,15 +59,18 @@ export async function runMemoryInventoryCommand(args, io) {
|
|
|
47
59
|
// A dry-run does not create a shadow index or server state. Existing
|
|
48
60
|
// schema1 cards therefore remain legacy; indexedEntries is only coverage
|
|
49
61
|
// information, not proof that the Agent entered shadow mode.
|
|
50
|
-
storageMode: storageVersion === 2 ? "v2_write" : "
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
storageMode: storageVersion === 2 ? (v2?.writeReady ? "v2_write" : "repair_required")
|
|
63
|
+
: detectionIssue ? "repair_required" : "legacy",
|
|
64
|
+
contextMode: storageVersion === 2 ? (v2?.writeReady ? "bounded_preview" : "blocked")
|
|
65
|
+
: detectionIssue ? "blocked" : "legacy_context",
|
|
66
|
+
readerMode: storageVersion === 2 ? "v2" : detectionIssue ? "blocked" : "legacy",
|
|
53
67
|
detectedIndexVersion: storageVersion,
|
|
54
68
|
migrationCoverage: { detectedIndexVersion: storageVersion, storageVersion, indexedEntries, legacyEntries, unindexedFiles: orphanNotes.length,
|
|
55
69
|
totalEntries: analysis.entries.length, complete },
|
|
56
|
-
coverage: ["MEMORY.md", "notes/**", ".crew/memory/
|
|
70
|
+
coverage: ["MEMORY.md", "notes/**", ...(storageVersion === 2 ? [".crew/memory/migration.v1.json"] : []),
|
|
71
|
+
".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
|
|
57
72
|
limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
|
|
58
|
-
limits: inventoryLimits, totalBytes:
|
|
73
|
+
limits: inventoryLimits, totalBytes: reportSources.reduce((total, source) => total + source.bytes, 0), files, ...analysis, issues,
|
|
59
74
|
pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
|
|
60
75
|
orphanNotes,
|
|
61
76
|
resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
|
|
@@ -80,4 +95,5 @@ import { parseArgs } from "node:util";
|
|
|
80
95
|
import { z } from "zod";
|
|
81
96
|
import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
|
|
82
97
|
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
98
|
+
import { inspectV2MemoryInventory } from "./inventory-v2.js";
|
|
83
99
|
import { detectLocalMemoryStorageVersion } from "./storage-format.js";
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import { renderLocalMemoryIndexCard } from "./index-budget.js";
|
|
4
|
+
import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.js";
|
|
5
|
+
import { MigrationStateSchema } from "./migration-schema.js";
|
|
6
|
+
import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
|
|
7
|
+
import { parseFullMemoryIndex, selectLocalMemoryPreview } from "./storage-format.js";
|
|
8
|
+
const STATE_PATH = ".crew/memory/migration.v1.json";
|
|
9
|
+
const DETAIL_MARKER_PATTERN = /<!--\s*NOWWORK:MEMORY_ENTRY:(?:BEGIN\s+id=([A-Za-z0-9_-]+)\s+revision=([1-9][0-9]*)|END\s+id=([A-Za-z0-9_-]+))\s*-->/gu;
|
|
10
|
+
const CONTROL_MARKER_PATTERN = /NOWWORK:MEMORY_(?:ENTRY|INDEX):/u;
|
|
11
|
+
function errorCode(error, fallback) {
|
|
12
|
+
if (error instanceof Error && /^[a-z0-9_]+$/u.test(error.message))
|
|
13
|
+
return error.message;
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
function sourceFields(entry) {
|
|
17
|
+
return {
|
|
18
|
+
id: entry.id,
|
|
19
|
+
file: entry.file,
|
|
20
|
+
type: entry.type,
|
|
21
|
+
status: entry.status,
|
|
22
|
+
summary: entry.summary,
|
|
23
|
+
triggers: entry.triggers.join(", "),
|
|
24
|
+
priority: entry.priority,
|
|
25
|
+
revision: String(entry.revision),
|
|
26
|
+
anchor: entry.anchor ?? "",
|
|
27
|
+
updated: entry.updated,
|
|
28
|
+
...(entry.reviewAfter === null ? {} : { review_after: entry.reviewAfter }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function harmlessDetailRemainder(text) {
|
|
32
|
+
let cursor = 0;
|
|
33
|
+
let lineStart = true;
|
|
34
|
+
const whitespace = /\s+/uy;
|
|
35
|
+
while (cursor < text.length) {
|
|
36
|
+
whitespace.lastIndex = cursor;
|
|
37
|
+
const spacing = whitespace.exec(text);
|
|
38
|
+
if (spacing !== null) {
|
|
39
|
+
const lastNewline = spacing[0].lastIndexOf("\n");
|
|
40
|
+
lineStart = lastNewline === spacing[0].length - 1;
|
|
41
|
+
cursor = whitespace.lastIndex;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (text.startsWith("<!--", cursor)) {
|
|
45
|
+
const end = text.indexOf("-->", cursor + 4);
|
|
46
|
+
if (end === -1)
|
|
47
|
+
return false;
|
|
48
|
+
cursor = end + 3;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (lineStart && text[cursor] === "#") {
|
|
52
|
+
const end = text.indexOf("\n", cursor + 1);
|
|
53
|
+
if (end === -1)
|
|
54
|
+
return true;
|
|
55
|
+
cursor = end + 1;
|
|
56
|
+
lineStart = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
export function* scanManagedDetailMarkers(detail) {
|
|
64
|
+
for (const marker of detail.matchAll(DETAIL_MARKER_PATTERN)) {
|
|
65
|
+
const beginId = marker[1];
|
|
66
|
+
yield {
|
|
67
|
+
start: marker.index,
|
|
68
|
+
end: marker.index + marker[0].length,
|
|
69
|
+
kind: beginId === undefined ? "end" : "begin",
|
|
70
|
+
id: beginId ?? marker[3],
|
|
71
|
+
revision: beginId === undefined ? null : Number(marker[2]),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Parse one note once, then let every index entry use constant-time lookups. */
|
|
76
|
+
export function analyzeManagedDetailFile(detail, expectedIds, scan = scanManagedDetailMarkers) {
|
|
77
|
+
const revisionsById = new Map();
|
|
78
|
+
const remainder = [];
|
|
79
|
+
let cursor = 0;
|
|
80
|
+
let markerInvalid = false;
|
|
81
|
+
let unindexed = false;
|
|
82
|
+
let open = null;
|
|
83
|
+
for (const marker of scan(detail)) {
|
|
84
|
+
const between = detail.slice(cursor, marker.start);
|
|
85
|
+
if (CONTROL_MARKER_PATTERN.test(between))
|
|
86
|
+
markerInvalid = true;
|
|
87
|
+
if (open === null)
|
|
88
|
+
remainder.push(between);
|
|
89
|
+
cursor = marker.end;
|
|
90
|
+
if (marker.kind === "begin") {
|
|
91
|
+
if (open !== null || marker.revision === null)
|
|
92
|
+
markerInvalid = true;
|
|
93
|
+
else
|
|
94
|
+
open = { id: marker.id, revision: marker.revision };
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (open === null || marker.id !== open.id) {
|
|
98
|
+
markerInvalid = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const revisions = revisionsById.get(open.id) ?? [];
|
|
102
|
+
revisions.push(open.revision);
|
|
103
|
+
revisionsById.set(open.id, revisions);
|
|
104
|
+
if (!expectedIds.has(open.id))
|
|
105
|
+
unindexed = true;
|
|
106
|
+
open = null;
|
|
107
|
+
}
|
|
108
|
+
const tail = detail.slice(cursor);
|
|
109
|
+
if (CONTROL_MARKER_PATTERN.test(tail) || open !== null)
|
|
110
|
+
markerInvalid = true;
|
|
111
|
+
if (open === null)
|
|
112
|
+
remainder.push(tail);
|
|
113
|
+
const uncovered = remainder.join("");
|
|
114
|
+
const coverageIssue = markerInvalid
|
|
115
|
+
? "managed_anchor_invalid"
|
|
116
|
+
: unindexed
|
|
117
|
+
? "unindexed_detail"
|
|
118
|
+
: harmlessDetailRemainder(uncovered)
|
|
119
|
+
? null
|
|
120
|
+
: "unindexed_text_requires_mapping";
|
|
121
|
+
return { revisionsById, coverageIssue };
|
|
122
|
+
}
|
|
123
|
+
function inspectDetailEntry(entry, analysis) {
|
|
124
|
+
if (analysis === undefined)
|
|
125
|
+
return "missing_detail";
|
|
126
|
+
if (analysis.coverageIssue === "managed_anchor_invalid")
|
|
127
|
+
return "managed_anchor_invalid";
|
|
128
|
+
const revisions = analysis.revisionsById.get(entry.id) ?? [];
|
|
129
|
+
if (revisions.length !== 1)
|
|
130
|
+
return "managed_anchor_invalid";
|
|
131
|
+
return revisions[0] > entry.revision ? "managed_revision_invalid" : null;
|
|
132
|
+
}
|
|
133
|
+
export function inspectManagedDetailFiles(entries, sources, analyze = analyzeManagedDetailFile) {
|
|
134
|
+
const sourceMap = new Map(sources.map(source => [source.path, source]));
|
|
135
|
+
const expectedByFile = new Map();
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const expected = expectedByFile.get(entry.file) ?? new Set();
|
|
138
|
+
expected.add(entry.id);
|
|
139
|
+
expectedByFile.set(entry.file, expected);
|
|
140
|
+
}
|
|
141
|
+
const paths = new Set([
|
|
142
|
+
...expectedByFile.keys(),
|
|
143
|
+
...sources.filter(source => source.path.startsWith("notes/")).map(source => source.path),
|
|
144
|
+
]);
|
|
145
|
+
const analyses = new Map();
|
|
146
|
+
const fileIssues = [];
|
|
147
|
+
for (const path of paths) {
|
|
148
|
+
const source = sourceMap.get(path);
|
|
149
|
+
if (source === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
const analysis = analyze(source.content, expectedByFile.get(path) ?? new Set());
|
|
152
|
+
analyses.set(path, analysis);
|
|
153
|
+
if (analysis.coverageIssue !== null)
|
|
154
|
+
fileIssues.push({ code: analysis.coverageIssue, path });
|
|
155
|
+
}
|
|
156
|
+
const entryIssues = new Map();
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
const issue = inspectDetailEntry(entry, analyses.get(entry.file));
|
|
159
|
+
if (issue !== null)
|
|
160
|
+
entryIssues.set(entry.id, issue);
|
|
161
|
+
}
|
|
162
|
+
return { entryIssues, fileIssues };
|
|
163
|
+
}
|
|
164
|
+
async function inspectCommittedState(root, budget) {
|
|
165
|
+
let source;
|
|
166
|
+
try {
|
|
167
|
+
source = await readMemoryInventorySource(root, STATE_PATH, Math.min(inventoryLimits.fileBytes, budget));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
valid: false,
|
|
172
|
+
source: null,
|
|
173
|
+
issue: {
|
|
174
|
+
code: error.code === "ENOENT"
|
|
175
|
+
? "memory_migration_state_missing"
|
|
176
|
+
: errorCode(error, "memory_migration_state_invalid"),
|
|
177
|
+
path: STATE_PATH,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const state = MigrationStateSchema.parse(JSON.parse(source.content));
|
|
183
|
+
const fingerprint = createHash("sha256").update(await realpath(root)).digest("hex");
|
|
184
|
+
const ownerIdentity = String((await lstat(root)).uid);
|
|
185
|
+
if (state.phase !== "committed" || state.storageVersion !== 2
|
|
186
|
+
|| state.identity.rootFingerprint !== fingerprint
|
|
187
|
+
|| state.identity.ownerIdentity !== ownerIdentity) {
|
|
188
|
+
throw new Error("memory_migration_state_invalid");
|
|
189
|
+
}
|
|
190
|
+
assertMigrationStateIntegrity(state, state.identity);
|
|
191
|
+
const confirmed = await readMemoryInventorySource(root, STATE_PATH, Math.min(inventoryLimits.fileBytes, budget));
|
|
192
|
+
if (confirmed.sha256 !== source.sha256) {
|
|
193
|
+
return { valid: false, source, issue: { code: "source_changed", path: STATE_PATH } };
|
|
194
|
+
}
|
|
195
|
+
return { valid: true, source, issue: null };
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
valid: false,
|
|
200
|
+
source,
|
|
201
|
+
issue: {
|
|
202
|
+
code: "memory_migration_state_invalid",
|
|
203
|
+
path: STATE_PATH,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Validate an already-committed schema2 store without sending it through the schema1 migration preview. */
|
|
209
|
+
export async function inspectV2MemoryInventory(root, sources, readComplete) {
|
|
210
|
+
const sourceMap = new Map(sources.map(source => [source.path, source]));
|
|
211
|
+
const memory = sourceMap.get("MEMORY.md")?.content ?? "";
|
|
212
|
+
const issues = [];
|
|
213
|
+
const addIssue = (code, path) => {
|
|
214
|
+
if (!issues.some(issue => issue.code === code && issue.path === path))
|
|
215
|
+
issues.push({ code, path });
|
|
216
|
+
};
|
|
217
|
+
let entries = [];
|
|
218
|
+
try {
|
|
219
|
+
entries = parseFullMemoryIndex(memory);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
addIssue(errorCode(error, "local_memory_storage_entry_invalid"), "MEMORY.md");
|
|
223
|
+
}
|
|
224
|
+
const scannedBytes = sources.reduce((total, source) => total + source.bytes, 0);
|
|
225
|
+
const state = await inspectCommittedState(root, Math.max(0, inventoryLimits.totalBytes - scannedBytes));
|
|
226
|
+
if (state.issue)
|
|
227
|
+
addIssue(state.issue.code, state.issue.path);
|
|
228
|
+
const detailInspection = inspectManagedDetailFiles(entries, sources);
|
|
229
|
+
for (const issue of detailInspection.fileIssues)
|
|
230
|
+
addIssue(issue.code, issue.path);
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
const issue = detailInspection.entryIssues.get(entry.id);
|
|
233
|
+
if (issue !== undefined)
|
|
234
|
+
addIssue(issue, entry.file);
|
|
235
|
+
}
|
|
236
|
+
let preview = null;
|
|
237
|
+
if (readComplete && !issues.some(issue => issue.path === "MEMORY.md")) {
|
|
238
|
+
try {
|
|
239
|
+
const selected = selectLocalMemoryPreview(memory);
|
|
240
|
+
preview = {
|
|
241
|
+
memory: selected.memory,
|
|
242
|
+
budget: {
|
|
243
|
+
entries: selected.entries.length,
|
|
244
|
+
omittedEntries: selected.omittedIds.length,
|
|
245
|
+
// The bounded projection itself fits; reason records normal long-tail omission.
|
|
246
|
+
fits: true,
|
|
247
|
+
reason: selected.reason,
|
|
248
|
+
},
|
|
249
|
+
writerCompatible: state.valid && issues.length === 0,
|
|
250
|
+
notes: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
addIssue(errorCode(error, "local_memory_preview_invalid"), "MEMORY.md");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const shownDetails = new Set();
|
|
258
|
+
const writeReady = readComplete && state.valid && issues.length === 0;
|
|
259
|
+
return {
|
|
260
|
+
stateValid: state.valid,
|
|
261
|
+
writeReady,
|
|
262
|
+
stateSource: state.source,
|
|
263
|
+
analysis: {
|
|
264
|
+
entries: entries.map(entry => {
|
|
265
|
+
const detail = shownDetails.has(entry.file) ? null : sourceMap.get(entry.file)?.content ?? null;
|
|
266
|
+
shownDetails.add(entry.file);
|
|
267
|
+
return {
|
|
268
|
+
title: entry.title,
|
|
269
|
+
format: "managed",
|
|
270
|
+
disposition: detailInspection.entryIssues.has(entry.id) || !writeReady ? "blocked" : "review_required",
|
|
271
|
+
sourceFields: sourceFields(entry),
|
|
272
|
+
rawCard: renderLocalMemoryIndexCard(entry),
|
|
273
|
+
detail,
|
|
274
|
+
proposed: entry,
|
|
275
|
+
};
|
|
276
|
+
}),
|
|
277
|
+
issues,
|
|
278
|
+
preview,
|
|
279
|
+
rawMemory: memory,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|