@nowcrew/daemon 0.6.88-beta.2 → 0.6.88-beta.20
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/README.md +38 -10
- package/dist/agent-memory/policy.js +3 -1
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +5 -3
- package/dist/list-models.js +2 -0
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -37
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +154 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +14 -11
- package/dist/local-memory/read-cli.js +59 -8
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/machine-info.js +54 -16
- package/dist/main.js +4 -69
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +3 -4
- package/dist/runner.js +14 -15
- package/dist/runtime-probe.js +34 -5
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +224 -0
- package/dist/runtimes/deepseek-harness.js +64 -11
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/kimi-acp-runner.js +63 -10
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/serve.js +58 -88
- package/dist/session.js +4 -0
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/package.json +2 -2
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/README.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
The daemon is NowWork's local execution adapter. It holds a machine credential, keeps one control-plane
|
|
4
4
|
WebSocket open, and launches coding runtimes on that machine. Product policy remains on the server.
|
|
5
5
|
|
|
6
|
+
## 已完成搬迁后的存储运行
|
|
7
|
+
|
|
8
|
+
NAS / `NOWWORK_HOME` 搬迁和旧 daemon 布局标准化入口已退役。daemon 不再提供内部
|
|
9
|
+
`storage-migration-worker` / `storage-migration-recover` 命令,不处理 `daemon:migrate`、
|
|
10
|
+
`daemon:storage-doctor`、`daemon:storage-migrate` 或迁移确认消息,也不再声明
|
|
11
|
+
`storage_migration_v1` / `storage_migration_status_v1` / `daemon_layout_migration_v1` 能力。
|
|
12
|
+
|
|
13
|
+
现有 `installations.json` 即使仍有历史 `storageMigrationId`,也按当前 installation 解析
|
|
14
|
+
Agent、仓库和报告目录;启动、hello 和身份绑定不再读取迁移 receipt、outbox 或备份。
|
|
15
|
+
hello 继续报告布局、环境、Workspace、路径指纹与 ownership,但不再发送迁移 ID。
|
|
16
|
+
每次连接仍须完成 Server 身份绑定后才能执行任务;Workspace 或 Machine 身份不匹配时拒绝执行。
|
|
17
|
+
|
|
18
|
+
旧布局读取兼容、日常安装/升级、NAS 所有权校验、执行 journal 与 ACK/replay 继续保留。
|
|
19
|
+
退役代码不会删除历史安装字段、迁移记录、receipt、备份或旧数据目录;这些数据的保留与清理另行处理。
|
|
20
|
+
|
|
6
21
|
## Run
|
|
7
22
|
|
|
8
23
|
For a temporary foreground connection, run:
|
|
@@ -242,26 +257,26 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
|
|
|
242
257
|
|
|
243
258
|
| Runtime | Transport | System/wake input | Native resume |
|
|
244
259
|
| --- | --- | --- | --- |
|
|
245
|
-
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
246
|
-
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume
|
|
247
|
-
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load`
|
|
248
|
-
| Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load`
|
|
249
|
-
| OpenCode | `run --format json` | stdin | `--session
|
|
260
|
+
| Claude | stream-json CLI | daemon-owned system prompt file + user input | CLI session id; thread-bound: matching native SessionStart proof + SDK initialization |
|
|
261
|
+
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume`; thread-bound: returned id must match |
|
|
262
|
+
| Kimi | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
263
|
+
| Hermes | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
264
|
+
| OpenCode | `run --format json` | stdin | unbound: `--session`; thread-bound: cold |
|
|
250
265
|
| DeepSeek Harness | ACP over stdio | `session/prompt` | none (fresh session per execution) |
|
|
251
266
|
|
|
252
267
|
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
253
268
|
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
254
|
-
CLI
|
|
269
|
+
CLI; only unbound executions can carry a saved native session id. Unrelated ACP failures stay failures. The CLI fallback is
|
|
255
270
|
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
256
271
|
durable Job Object ownership is available.
|
|
257
272
|
|
|
258
273
|
Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
|
|
259
274
|
a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
|
|
260
|
-
machine-local.
|
|
275
|
+
machine-local. For unbound executions, a missing resumed ACP session is retried once as a fresh session.
|
|
261
276
|
|
|
262
277
|
OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
|
|
263
278
|
workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
|
|
264
|
-
compatibility fallback).
|
|
279
|
+
compatibility fallback). For unbound executions, a missing resumed session is retried once without `--session`; malformed,
|
|
265
280
|
empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
|
|
266
281
|
`opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
|
|
267
282
|
only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
|
|
@@ -291,8 +306,12 @@ uses fresh sessions and committed text; it does not claim resume, images, MCP, r
|
|
|
291
306
|
or transcript support. Agent environment variables cannot override `DEEPSEEK_*`, `DSH_*`, or
|
|
292
307
|
`NOWCREW_DEEPSEEK_HARNESS_CONFIG`; those values always come from the daemon service environment.
|
|
293
308
|
|
|
294
|
-
Codex, Kimi, Hermes, and OpenCode sessions
|
|
295
|
-
|
|
309
|
+
Unbound Codex, Kimi, Hermes, and OpenCode sessions retain the same configured warm/budget policy and keyed
|
|
310
|
+
lease as Claude through protocol v1. Thread-bound Claude/Codex sessions keep their native session across
|
|
311
|
+
warm-cache expiry, token estimates, and turn counts; verified restoration receives only accepted-cursor
|
|
312
|
+
deltas. New sessions and runtimes without verified restoration receive complete current-thread history
|
|
313
|
+
before the business turn. See the [thread context contract](../docs/thread-scoped-native-context.md).
|
|
314
|
+
A first-progress watchdog covers a provider that accepts a turn but remains
|
|
296
315
|
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
297
316
|
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
298
317
|
DeepSeek Harness is execution-v1-only and intentionally skips native session persistence.
|
|
@@ -354,6 +373,15 @@ Runtime launch, which recovers from a missed notification or daemon restart. Ski
|
|
|
354
373
|
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
355
374
|
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
356
375
|
|
|
376
|
+
## 目录投影测试的平台范围
|
|
377
|
+
|
|
378
|
+
`test/directory-projection.test.ts` 在所有平台运行,覆盖路径检查、复制发布、只读权限请求及失败恢复。
|
|
379
|
+
其受控文件系统夹具记录目录 `chmod` 请求,但保留宿主目录的写权限,避免把 macOS 的目录重命名规则当作 Windows 行为。
|
|
380
|
+
生产代码的只读保护不受此夹具影响。
|
|
381
|
+
|
|
382
|
+
`test/directory-projection.windows.test.ts` 仅在 Windows 上运行,使用真实文件系统验证只读目录发布与文件保护。
|
|
383
|
+
非 Windows 的全量测试会明确跳过该文件,不能据此宣称 Windows 原生行为已验证。
|
|
384
|
+
|
|
357
385
|
## Reliability
|
|
358
386
|
|
|
359
387
|
Protocol-v1 lifecycle is `accepted → started → completed`, followed by a server
|
|
@@ -17,7 +17,9 @@ function truncateUtf8(value, maxBytes) {
|
|
|
17
17
|
return output;
|
|
18
18
|
}
|
|
19
19
|
export function extractIncomingMessage(wakePrompt) {
|
|
20
|
-
|
|
20
|
+
// Keep legacy wake delimiters while accepting program-delivered-context prompts.
|
|
21
|
+
// Stop before policy/history blocks; neither is the current user's message.
|
|
22
|
+
const match = wakePrompt.match(/(?:^|\n)(?:Incoming message|来信):[ \t]*(.*?)(?=\n(?:Start with crew|先用 crew|Work directly from this thread's context|直接依据程序交付的本线程上下文|No thread anchor:|没有线程锚点[,,])|$)/su);
|
|
21
23
|
const content = match?.[1]?.trim();
|
|
22
24
|
return content ? content : null;
|
|
23
25
|
}
|
|
@@ -14,7 +14,7 @@ import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, man
|
|
|
14
14
|
export async function managedDaemonCapabilities(eligibility) {
|
|
15
15
|
try {
|
|
16
16
|
return (await eligibility()).eligible
|
|
17
|
-
? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"
|
|
17
|
+
? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"]
|
|
18
18
|
: [];
|
|
19
19
|
}
|
|
20
20
|
catch {
|
|
@@ -878,23 +878,26 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
878
878
|
},
|
|
879
879
|
acknowledgeCompletion: async (executionId) => {
|
|
880
880
|
validateExecutionId(executionId);
|
|
881
|
-
const
|
|
881
|
+
const result = await serializedExecution(executionId, "acknowledge_completion", async () => {
|
|
882
882
|
const entry = await requireRecord(executionId);
|
|
883
883
|
if (entry.state !== "completed" && entry.state !== "interrupted") {
|
|
884
884
|
throw new JournalTransitionError(`Cannot acknowledge ${entry.state} execution`);
|
|
885
885
|
}
|
|
886
|
-
if (entry.completionAcknowledged)
|
|
887
|
-
return confirmDurable(entry);
|
|
886
|
+
if (entry.completionAcknowledged) {
|
|
887
|
+
return { entry: await confirmDurable(entry), newlyAcknowledged: false };
|
|
888
|
+
}
|
|
888
889
|
const updated = JournalEntrySchema.parse({
|
|
889
890
|
...entry,
|
|
890
891
|
completionAcknowledged: true,
|
|
891
892
|
updatedAt: now().toISOString(),
|
|
892
893
|
});
|
|
893
894
|
await writeRecord(updated);
|
|
894
|
-
return updated;
|
|
895
|
+
return { entry: updated, newlyAcknowledged: true };
|
|
895
896
|
});
|
|
896
|
-
|
|
897
|
-
|
|
897
|
+
// 重复 ACK 仍确认持久化,但不能反复全目录清理并阻塞后续控制帧。
|
|
898
|
+
if (result.newlyAcknowledged)
|
|
899
|
+
await serializedDirectory(pruneInternal);
|
|
900
|
+
return result.entry;
|
|
898
901
|
},
|
|
899
902
|
get: async (executionId) => {
|
|
900
903
|
validateExecutionId(executionId);
|
|
@@ -200,7 +200,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
200
200
|
language: z.enum(["zh", "en"]),
|
|
201
201
|
systemPrompt: z.string(),
|
|
202
202
|
wakePrompt: z.string(),
|
|
203
|
-
maintenancePurpose: z.
|
|
203
|
+
maintenancePurpose: z.enum(["self_training", "memory_prune"]).optional(),
|
|
204
204
|
}).strict(),
|
|
205
205
|
runtime: z.object({
|
|
206
206
|
name: RuntimeSchema,
|
|
@@ -264,6 +264,7 @@ export const ExecutionRejectedSchema = z.object({
|
|
|
264
264
|
reason: RejectionReasonSchema,
|
|
265
265
|
message: z.string().optional(),
|
|
266
266
|
at: TimestampSchema,
|
|
267
|
+
preAdmissionProof: z.object({ specHash: z.string().regex(/^[a-f0-9]{64}$/u) }).strict().optional(),
|
|
267
268
|
}).strict();
|
|
268
269
|
export const ExecutionStartedSchema = z.object({
|
|
269
270
|
type: z.literal("execution:started"),
|
package/dist/execution-runner.js
CHANGED
|
@@ -408,7 +408,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
408
408
|
? { permission: effectivePermission(spec, config) }
|
|
409
409
|
: admission(spec, config, dependencies, initialAt);
|
|
410
410
|
if ("rejected" in checked) {
|
|
411
|
-
const
|
|
411
|
+
const proof = await dependencies.confirmPreAdmission?.(checked.rejected).catch(() => false);
|
|
412
|
+
const frame = ExecutionRejectedSchema.parse(boundExecutionFrame({ ...checked.rejected, ...(proof ? { preAdmissionProof: { specHash } } : {}) }, config.executionLimits.maxEventBytes));
|
|
412
413
|
await dependencies.report(frame);
|
|
413
414
|
return { kind: "rejected", frame };
|
|
414
415
|
}
|
|
@@ -844,6 +845,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
844
845
|
executionId: spec.executionId,
|
|
845
846
|
handle: spec.agent.handle,
|
|
846
847
|
channelId: spec.context.channelId,
|
|
848
|
+
...(spec.context.threadId === undefined ? {} : { threadId: spec.context.threadId }),
|
|
847
849
|
keyMode: "opaque",
|
|
848
850
|
taskKey: spec.workspace.taskKey,
|
|
849
851
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
@@ -885,13 +887,13 @@ export async function runExecution(config, input, dependencies) {
|
|
|
885
887
|
} : {}),
|
|
886
888
|
},
|
|
887
889
|
session: {
|
|
888
|
-
enabled:
|
|
890
|
+
enabled: spec.instructions.maintenancePurpose !== undefined ? false : config.resume,
|
|
889
891
|
warmMs: config.resumeWarmMs,
|
|
890
892
|
budgetTokens: config.sessionBudgetTokens,
|
|
891
893
|
softTokens: config.sessionSoftTokens,
|
|
892
894
|
maxTurns: config.sessionMaxTurns,
|
|
893
895
|
},
|
|
894
|
-
...(
|
|
896
|
+
...(spec.instructions.maintenancePurpose === undefined ? {} : { maintenancePurpose: spec.instructions.maintenancePurpose }),
|
|
895
897
|
localMemoryV2Enabled,
|
|
896
898
|
};
|
|
897
899
|
const result = await Promise.race([
|
package/dist/list-models.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { isWin } from "./platform.js";
|
|
4
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
4
5
|
import { listClaudeModels } from "./runtimes/claude-models.js";
|
|
5
6
|
import { listHermesModels } from "./runtimes/hermes-models.js";
|
|
6
7
|
const execFileRaw = promisify(execFile);
|
|
@@ -8,6 +9,7 @@ const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
|
8
9
|
const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
9
10
|
// win32 上 npm CLI 是 .cmd shim,execFile 需 shell 才能执行;参数全是固定字面量,无注入面。
|
|
10
11
|
const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
12
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
11
13
|
shell: isWin(),
|
|
12
14
|
timeout: MODEL_PROBE_TIMEOUT_MS,
|
|
13
15
|
killSignal: "SIGKILL",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { awaitWithCancellation } from "./runtime-cancellation.js";
|
|
2
|
+
const leaseTails = new Map();
|
|
3
|
+
/** 同一进程内的短操作或原生会话排队;等待取消也不会越过前一位持有者。 */
|
|
4
|
+
export async function withKeyedLease(key, operation, cancellation) {
|
|
5
|
+
const predecessor = leaseTails.get(key) ?? Promise.resolve();
|
|
6
|
+
// 解锁函数在等待 predecessor 前可用,保留取消等待时的队列顺序。
|
|
7
|
+
let unlock;
|
|
8
|
+
const current = new Promise(resolve => { unlock = resolve; });
|
|
9
|
+
const queued = predecessor.then(() => current);
|
|
10
|
+
leaseTails.set(key, queued);
|
|
11
|
+
try {
|
|
12
|
+
await awaitWithCancellation(predecessor, cancellation);
|
|
13
|
+
return await operation();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
unlock();
|
|
17
|
+
void queued.then(() => {
|
|
18
|
+
if (leaseTails.get(key) === queued)
|
|
19
|
+
leaseTails.delete(key);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
|
|
1
2
|
import { createInterface } from "node:readline";
|
|
2
3
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { delimiter, join } from "node:path";
|
|
@@ -15,6 +16,11 @@ import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report
|
|
|
15
16
|
import { reportDeliveryContext } from "./cloud-report-receipt.js";
|
|
16
17
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
17
18
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
19
|
+
import { NativeThreadContext, isolateMaintenanceSession } from "./native-thread-context.js";
|
|
20
|
+
import { prepareThreadDelivery } from "./thread-input-delivery.js";
|
|
21
|
+
import { resolveMemoryPruneWorkLog, workLogSha256 } from "./local-memory/work-log.js";
|
|
22
|
+
import { withKeyedLease } from "./local-execution-lease.js";
|
|
23
|
+
import { withWorkLogExecution, withWorkLogCommit, workLogExecutionKey } from "./local-memory/work-log-execution.js";
|
|
18
24
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
19
25
|
import { createConsoleFormatter } from "./console-formatter.js";
|
|
20
26
|
import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
|
|
@@ -108,6 +114,9 @@ async function launchLegacyRuntime(request) {
|
|
|
108
114
|
if (request.runtime === "deepseek-harness") {
|
|
109
115
|
throw new Error("DeepSeek Harness requires execution protocol v1");
|
|
110
116
|
}
|
|
117
|
+
if (request.runtime !== "claude")
|
|
118
|
+
request = { ...request, wakePrompt: (await prepareThreadDelivery({ env: request.env,
|
|
119
|
+
resumed: false, sessionId: "unverified-cold-legacy", wakePrompt: request.wakePrompt })).prompt };
|
|
111
120
|
const common = {
|
|
112
121
|
bin: request.bin,
|
|
113
122
|
cwd: request.cwd,
|
|
@@ -154,27 +163,12 @@ async function launchLegacyRuntime(request) {
|
|
|
154
163
|
function resolvePrompt(prompt, context) {
|
|
155
164
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
156
165
|
}
|
|
157
|
-
const nativeSessionLeaseTails = new Map();
|
|
158
166
|
const activeMemoryPrunes = new Map();
|
|
159
|
-
async function withKeyedLease(key, operation, cancellation) {
|
|
160
|
-
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
161
|
-
let release;
|
|
162
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
163
|
-
const tail = predecessor.then(() => current);
|
|
164
|
-
nativeSessionLeaseTails.set(key, tail);
|
|
165
|
-
try {
|
|
166
|
-
await awaitWithCancellation(predecessor, cancellation);
|
|
167
|
-
return await operation();
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
release();
|
|
171
|
-
void tail.then(() => {
|
|
172
|
-
if (nativeSessionLeaseTails.get(key) === tail)
|
|
173
|
-
nativeSessionLeaseTails.delete(key);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
167
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
168
|
+
input = isolateMaintenanceSession(input);
|
|
169
|
+
return withWorkLogExecution(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), () => executeLocalWithSession(input, callbacks, dependencies), dependencies.cancellation);
|
|
170
|
+
}
|
|
171
|
+
async function executeLocalWithSession(input, callbacks, dependencies) {
|
|
178
172
|
const versionedProjectSkills = input.projectSkills !== undefined
|
|
179
173
|
&& !Array.isArray(input.projectSkills);
|
|
180
174
|
if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
|
|
@@ -255,6 +249,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
255
249
|
let localMemoryResultPath = null;
|
|
256
250
|
let executionWorkspace = workspace;
|
|
257
251
|
let localMemoryTelemetry = null;
|
|
252
|
+
let nativeThreadContext = null;
|
|
258
253
|
const inheritedEnv = { ...process.env };
|
|
259
254
|
const reportRoots = {
|
|
260
255
|
root: workspace.dir,
|
|
@@ -294,12 +289,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
294
289
|
: null;
|
|
295
290
|
const currentPrior = storedCurrentPrior !== null
|
|
296
291
|
&& storedCurrentPrior.sessionContextFingerprint === input.sessionContextFingerprint
|
|
292
|
+
&& storedCurrentPrior.runtime === runtime.name
|
|
293
|
+
&& storedCurrentPrior.threadRoot === input.resumeKey
|
|
297
294
|
? storedCurrentPrior
|
|
298
295
|
: null;
|
|
299
|
-
//
|
|
300
|
-
// older legacy path stored metadata under tasks/<safe thread key>. Import
|
|
301
|
-
// that metadata once so an existing thread can cross the protocol boundary
|
|
302
|
-
// without losing its native runtime conversation.
|
|
296
|
+
// Import compatible legacy metadata across the protocol boundary.
|
|
303
297
|
const legacyRunDir = input.session.enabled
|
|
304
298
|
&& supportsNativeResume
|
|
305
299
|
&& input.keyMode === "opaque"
|
|
@@ -315,7 +309,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
315
309
|
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
316
310
|
? null
|
|
317
311
|
: await readSession(legacyRunDir);
|
|
318
|
-
const
|
|
312
|
+
const compatibleLegacyPrior = legacyPrior?.runtime === runtime.name && legacyPrior?.threadRoot === input.resumeKey
|
|
313
|
+
? legacyPrior : null;
|
|
314
|
+
const prior = currentPrior ?? compatibleLegacyPrior;
|
|
319
315
|
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
320
316
|
const usesLegacyCwd = legacyRunDir !== null
|
|
321
317
|
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
@@ -336,11 +332,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
336
332
|
? prior?.model ?? currentModel
|
|
337
333
|
: currentModel;
|
|
338
334
|
const resumeSessionId = supportsNativeResume && sessionExists
|
|
339
|
-
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
335
|
+
? pickResumeId(input.resumeKey && (!Number.isSafeInteger(prior?.threadContextSeq) || prior?.threadHistoryComplete !== true) ? null : prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, input.resumeKey ? 0 : input.session.budgetTokens, input.resumeKey ? 0 : input.session.maxTurns, providerFp)
|
|
340
336
|
: null;
|
|
341
337
|
const resuming = resumeSessionId !== null;
|
|
342
338
|
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
343
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
339
|
+
&& pickResumeId(prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
344
340
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
345
341
|
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
346
342
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
@@ -349,7 +345,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
349
345
|
? await rotateAgentSession(workspace.sessionDir)
|
|
350
346
|
: workspace.agentSessionId);
|
|
351
347
|
await materializePrivateCodexHome({ runtime: runtime.name, homeDir: workspace.homeDir, ...(input.launch.codexHomeMode === undefined ? {} : { mode: input.launch.codexHomeMode }), ...(sourceCodexHome === undefined ? {} : { sourceHome: sourceCodexHome }), ...(launchSessionId === undefined ? {} : { sessionId: launchSessionId }), ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }), ...(dependencies.materializeDefaultCodexHome === undefined ? {} : { materialize: dependencies.materializeDefaultCodexHome }) });
|
|
352
|
-
|
|
348
|
+
let promptContext = {
|
|
353
349
|
workspace: executionWorkspace,
|
|
354
350
|
resuming,
|
|
355
351
|
rotatedForBudget,
|
|
@@ -388,8 +384,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
388
384
|
}
|
|
389
385
|
}
|
|
390
386
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
391
|
-
|
|
392
|
-
memoryPruneTraceId = parseMemoryPruneTraceId(
|
|
387
|
+
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
388
|
+
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
389
|
+
if (memoryPruneTraceId !== null) {
|
|
390
|
+
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
391
|
+
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
392
|
+
baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
393
|
+
}
|
|
393
394
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
394
395
|
&& isLocalMemoryV2Eligible({
|
|
395
396
|
enabledForHandle: true,
|
|
@@ -402,12 +403,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
402
403
|
}
|
|
403
404
|
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
404
405
|
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
405
|
-
enabled: input.localMemoryV2Enabled === true, query:
|
|
406
|
+
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
406
407
|
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
407
408
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
408
409
|
});
|
|
409
410
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
410
411
|
const systemPrompt = memoryPrompt.systemPrompt;
|
|
412
|
+
const wakePrompt = baseWakePrompt + memoryPrompt.wakePromptSuffix;
|
|
411
413
|
if (!isolatedMaintenance)
|
|
412
414
|
try {
|
|
413
415
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -466,6 +468,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
466
468
|
}
|
|
467
469
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
468
470
|
const crewHome = isolatedMaintenance ? executionWorkspace.runDir : workspace.dir;
|
|
471
|
+
const contextThreadId = input.threadId ?? input.resumeKey;
|
|
472
|
+
if (contextThreadId && input.maintenancePurpose === undefined) {
|
|
473
|
+
nativeThreadContext = new NativeThreadContext(join(executionWorkspace.runDir, `.crew-context-${input.executionId}.json`), contextThreadId, runtime.name, resumeSessionId, prior);
|
|
474
|
+
await nativeThreadContext.initialize();
|
|
475
|
+
}
|
|
469
476
|
const baseEnv = {
|
|
470
477
|
...inheritedEnv,
|
|
471
478
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
@@ -478,6 +485,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
478
485
|
CREW_TOKEN: input.launch.token,
|
|
479
486
|
CREW_CHANNEL: input.channelId,
|
|
480
487
|
CREW_HOME: crewHome,
|
|
488
|
+
// Empty values override any stale inherited hints from an enclosing runtime.
|
|
489
|
+
CREW_THREAD_ROOT: contextThreadId ?? "",
|
|
490
|
+
CREW_THREAD_CONTEXT_FILE: nativeThreadContext?.path ?? "",
|
|
481
491
|
// Keep the daemon-selected storage root authoritative for runtimes.
|
|
482
492
|
// In profile-backed NowWork sessions the daemon config may differ from
|
|
483
493
|
// the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
|
|
@@ -525,7 +535,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
525
535
|
const effectiveSystemPrompt = abilityContext === undefined
|
|
526
536
|
? systemPrompt
|
|
527
537
|
: `${systemPrompt}\n\n${abilityContext.prompt}`;
|
|
528
|
-
if (Buffer.byteLength(effectiveSystemPrompt, "utf8")
|
|
538
|
+
if (Buffer.byteLength(effectiveSystemPrompt, "utf8") + Buffer.byteLength(memoryPrompt.wakePromptSuffix, "utf8")
|
|
539
|
+
> (input.maxSystemPromptBytes ?? Number.MAX_SAFE_INTEGER)) {
|
|
529
540
|
throw new Error("ability_runtime_context_size_exceeded");
|
|
530
541
|
}
|
|
531
542
|
memoryPruneFailurePhase = "prompt_write";
|
|
@@ -559,7 +570,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
559
570
|
});
|
|
560
571
|
}
|
|
561
572
|
const trainingRoots = input.trainingProjectContext?.projects.map((project) => project.root) ?? [];
|
|
562
|
-
const { codexSkillRoots, claudeAdditionalDirectories } = await projectSkillRuntimeDirectories({
|
|
573
|
+
const { codexSkillRoots, claudeAdditionalDirectories, harnessSkillRoots } = await projectSkillRuntimeDirectories({
|
|
563
574
|
runtime: runtime.name,
|
|
564
575
|
...(runtimeRoot === undefined ? {} : { runtimeRoot }),
|
|
565
576
|
agentRoot: workspace.dir,
|
|
@@ -597,7 +608,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
597
608
|
const workspaceRoots = [...legacyRoots, ...trainingRoots];
|
|
598
609
|
const launchRequest = {
|
|
599
610
|
runtime: runtime.name,
|
|
600
|
-
bin: runtime.name === "deepseek-harness" ?
|
|
611
|
+
bin: runtime.name === "deepseek-harness" ? deepSeekHarnessLauncher(runtimeEnv).bin : runtime.name,
|
|
612
|
+
...(runtime.name === "deepseek-harness" ? { harnessStateRoot: executionWorkspace.runDir, harnessSkillRoots: harnessSkillRoots ?? [] } : {}),
|
|
601
613
|
cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
|
|
602
614
|
...(runtimeRoot !== undefined
|
|
603
615
|
|| (input.projectSkills === undefined && input.abilityRelease === undefined)
|
|
@@ -801,6 +813,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
801
813
|
const event = parseLine(line);
|
|
802
814
|
if (!event)
|
|
803
815
|
return;
|
|
816
|
+
nativeThreadContext?.observe(event);
|
|
804
817
|
localMemoryTelemetry?.observe(event);
|
|
805
818
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
806
819
|
markRuntimeReady();
|
|
@@ -918,14 +931,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
918
931
|
memoryPruneRuntimeExitCode = exitCode;
|
|
919
932
|
if (localMemoryResultPath !== null) {
|
|
920
933
|
memoryPruneFailurePhase = "memory_commit";
|
|
921
|
-
const
|
|
922
|
-
|
|
934
|
+
const resultPath = localMemoryResultPath;
|
|
935
|
+
const commit = () => commitLocalMemoryResultAfterExit({
|
|
936
|
+
resultPath,
|
|
923
937
|
agentDir: executionWorkspace.dir,
|
|
924
938
|
executionId: input.executionId,
|
|
925
939
|
pruneTraceId: memoryPruneTraceId,
|
|
926
940
|
exitCode,
|
|
927
941
|
workLogPath: executionWorkspace.workLogPath,
|
|
942
|
+
expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
|
|
928
943
|
});
|
|
944
|
+
const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
|
|
929
945
|
if (committed.status === "failed" || committed.status === "missing") {
|
|
930
946
|
const reason = committed.error ?? "local_memory_result_missing";
|
|
931
947
|
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
@@ -959,9 +975,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
959
975
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
960
976
|
: (resuming ? prior?.contextTokens : undefined);
|
|
961
977
|
await writeSession(workspace.sessionDir, {
|
|
978
|
+
runtime: runtime.name,
|
|
979
|
+
...(input.resumeKey === undefined ? {} : { threadRoot: input.resumeKey }),
|
|
980
|
+
...(exitCode === 0 ? await nativeThreadContext?.checkpoint() ?? {} : {}),
|
|
962
981
|
sessionId,
|
|
963
982
|
lastRunAt: Date.now(),
|
|
964
|
-
turns:
|
|
983
|
+
turns: nativeThreadContext?.confirmed ? (prior?.turns ?? 0) + 1 : 1,
|
|
965
984
|
model: currentModel,
|
|
966
985
|
providerFingerprint: providerFp,
|
|
967
986
|
...(input.sessionContextFingerprint === undefined
|
|
@@ -995,7 +1014,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
995
1014
|
...(usage === undefined ? {} : { usage }),
|
|
996
1015
|
model: observedModel,
|
|
997
1016
|
runtime: runtime.name,
|
|
998
|
-
resumed:
|
|
1017
|
+
resumed: nativeThreadContext?.confirmed ?? false,
|
|
999
1018
|
sessionId,
|
|
1000
1019
|
errorMessage: errorTail || null,
|
|
1001
1020
|
...(providerError === undefined ? {} : { providerError }),
|
|
@@ -1007,6 +1026,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1007
1026
|
};
|
|
1008
1027
|
}
|
|
1009
1028
|
finally {
|
|
1029
|
+
await nativeThreadContext?.cleanup().catch(() => undefined);
|
|
1010
1030
|
startupReservationState.current?.release();
|
|
1011
1031
|
try {
|
|
1012
1032
|
await localMemoryTelemetry?.finish();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { containsLikelySecret } from "../agent-memory/policy.js";
|
|
3
|
+
const CategorySchema = z.enum([
|
|
4
|
+
"preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
|
|
5
|
+
]);
|
|
6
|
+
export const LocalMemorySingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
|
|
7
|
+
export const LocalMemoryChangeSchema = z.object({
|
|
8
|
+
operation: z.enum(["create", "update", "supersede"]),
|
|
9
|
+
memoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
10
|
+
baseRevision: z.number().int().nonnegative().nullable(),
|
|
11
|
+
supersedesMemoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
12
|
+
title: LocalMemorySingleLineText.max(120),
|
|
13
|
+
kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
|
|
14
|
+
summary: LocalMemorySingleLineText,
|
|
15
|
+
triggers: z.array(LocalMemorySingleLineText.max(80)).min(1).max(16),
|
|
16
|
+
priority: z.enum(["high", "medium", "low"]),
|
|
17
|
+
detailCategory: CategorySchema,
|
|
18
|
+
detailBody: z.string().trim().min(1).max(12_000),
|
|
19
|
+
reviewAfter: LocalMemorySingleLineText.nullable(),
|
|
20
|
+
}).strict();
|
|
21
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
22
|
+
const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
|
|
23
|
+
export function validateLocalMemoryChange(change) {
|
|
24
|
+
const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
|
|
25
|
+
if (containsLikelySecret(text))
|
|
26
|
+
throw new Error("local_memory_secret_candidate");
|
|
27
|
+
if (CONTROL_CHARACTER_PATTERN.test(text))
|
|
28
|
+
throw new Error("local_memory_control_character");
|
|
29
|
+
if (RESERVED_MEMORY_MARKER.test(text))
|
|
30
|
+
throw new Error("local_memory_reserved_marker");
|
|
31
|
+
if (change.operation === "create"
|
|
32
|
+
&& (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
|
|
33
|
+
throw new Error("local_memory_create_revision_invalid");
|
|
34
|
+
}
|
|
35
|
+
if (change.operation === "update"
|
|
36
|
+
&& (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
|
|
37
|
+
throw new Error("local_memory_update_revision_missing");
|
|
38
|
+
}
|
|
39
|
+
if (change.operation === "supersede"
|
|
40
|
+
&& (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
|
|
41
|
+
throw new Error("local_memory_supersede_revision_missing");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -19,6 +19,14 @@ export function renderLocalMemoryIndexCard(entry) {
|
|
|
19
19
|
+ `- updated: ${entry.updated}\n`
|
|
20
20
|
+ (entry.reviewAfter === null ? "" : `- review_after: ${entry.reviewAfter}\n`);
|
|
21
21
|
}
|
|
22
|
+
/** 写入和预演共用完整索引,预算包含起止标记与分隔换行。 */
|
|
23
|
+
export function renderGeneratedIndex(entries) {
|
|
24
|
+
const cards = entries
|
|
25
|
+
.filter(entry => PROJECTABLE_STATUSES.has(entry.status))
|
|
26
|
+
.map(renderLocalMemoryIndexCard)
|
|
27
|
+
.join("\n");
|
|
28
|
+
return `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->\n${cards}<!-- NOWWORK:MEMORY_INDEX:END -->`;
|
|
29
|
+
}
|
|
22
30
|
function compareEntries(left, right) {
|
|
23
31
|
const statusRank = (status) => status === "pinned" ? 0 : 1;
|
|
24
32
|
const priorityRank = (priority) => (priority === "high" ? 0 : priority === "medium" ? 1 : 2);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const CommandSchema = z.object({
|
|
2
|
+
"agent-dir": z.string().min(1).refine(isAbsolute),
|
|
3
|
+
"dry-run": z.literal(true),
|
|
4
|
+
}).strict();
|
|
5
|
+
const USAGE = "只读记忆盘点与转换预演,不写文件,不支持 --apply。\n"
|
|
6
|
+
+ "pnpm --filter @nowcrew/daemon exec tsx src/local-memory/inventory-cli.ts --agent-dir <单个Agent绝对路径> --dry-run\n"
|
|
7
|
+
+ "报告包含原始记忆,需作为敏感本地资料保存;退出码0仅代表盘点及格式预演无阻塞,不代表批准迁移。\n";
|
|
8
|
+
export async function runMemoryInventoryCommand(args, io) {
|
|
9
|
+
if (args.length === 1 && args[0] === "--help") {
|
|
10
|
+
io.out(USAGE);
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
let command;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = parseArgs({ args, allowPositionals: false, options: {
|
|
16
|
+
"agent-dir": { type: "string" }, "dry-run": { type: "boolean" },
|
|
17
|
+
} });
|
|
18
|
+
command = CommandSchema.parse(parsed.values);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
io.err(USAGE);
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const scan = await scanMemorySources(command["agent-dir"]);
|
|
26
|
+
const scanComplete = scan.issues.length === 0;
|
|
27
|
+
const analysis = previewMemoryInventory(scan.sources, scanComplete);
|
|
28
|
+
const complete = scanComplete && !analysis.issues.some((issue) => issue.code === "missing_detail");
|
|
29
|
+
const issues = [...scan.issues, ...analysis.issues];
|
|
30
|
+
const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
|
|
31
|
+
const files = scan.sources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
|
|
32
|
+
const report = {
|
|
33
|
+
schemaVersion: 1, mode: "dry-run", agentDir: scan.root, applySupported: false,
|
|
34
|
+
complete, scanComplete, requiresHumanReview: true,
|
|
35
|
+
coverage: ["MEMORY.md", "notes/**", ".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
|
|
36
|
+
limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
|
|
37
|
+
limits: inventoryLimits, totalBytes: scan.totalBytes, files, ...analysis, issues,
|
|
38
|
+
pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
|
|
39
|
+
orphanNotes: files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path),
|
|
40
|
+
resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
|
|
41
|
+
};
|
|
42
|
+
io.out(JSON.stringify(report, null, 2) + "\n");
|
|
43
|
+
return issues.length ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const code = error instanceof Error ? error.code ?? error.message : "inventory_failed";
|
|
47
|
+
io.err(`记忆盘点失败:${code}\n`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
52
|
+
process.exitCode = await runMemoryInventoryCommand(process.argv.slice(2), {
|
|
53
|
+
out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
import { isAbsolute, resolve } from "node:path";
|
|
57
|
+
import { pathToFileURL } from "node:url";
|
|
58
|
+
import { parseArgs } from "node:util";
|
|
59
|
+
import { z } from "zod";
|
|
60
|
+
import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
|
|
61
|
+
import { previewMemoryInventory } from "./inventory-preview.js";
|