@nowcrew/daemon 0.6.88-beta.1 → 0.6.88-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/README.md +23 -10
- package/dist/agent-memory/policy.js +3 -1
- package/dist/cloud-report-receipt.js +83 -0
- package/dist/cloud-report-uploader.js +9 -0
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +3 -2
- package/dist/list-models.js +2 -0
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +69 -64
- 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 +57 -8
- package/dist/local-memory/runtime.js +61 -6
- 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 +17 -56
- package/dist/native-thread-context.js +59 -0
- package/dist/prompt.js +15 -15
- package/dist/runner.js +14 -15
- package/dist/runtime-environment-telemetry.js +27 -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/kimi-acp-runner.js +6 -2
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/serve.js +7 -8
- package/dist/session.js +4 -0
- package/dist/supervised-runtime.js +11 -16
- package/dist/thread-input-delivery.js +217 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -242,26 +242,26 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
|
|
|
242
242
|
|
|
243
243
|
| Runtime | Transport | System/wake input | Native resume |
|
|
244
244
|
| --- | --- | --- | --- |
|
|
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
|
|
245
|
+
| Claude | stream-json CLI | daemon-owned system prompt file + user input | CLI session id; thread-bound: matching native SessionStart proof + SDK initialization |
|
|
246
|
+
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume`; thread-bound: returned id must match |
|
|
247
|
+
| Kimi | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
248
|
+
| Hermes | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
249
|
+
| OpenCode | `run --format json` | stdin | unbound: `--session`; thread-bound: cold |
|
|
250
250
|
| DeepSeek Harness | ACP over stdio | `session/prompt` | none (fresh session per execution) |
|
|
251
251
|
|
|
252
252
|
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
253
253
|
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
254
|
-
CLI
|
|
254
|
+
CLI; only unbound executions can carry a saved native session id. Unrelated ACP failures stay failures. The CLI fallback is
|
|
255
255
|
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
256
256
|
durable Job Object ownership is available.
|
|
257
257
|
|
|
258
258
|
Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
|
|
259
259
|
a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
|
|
260
|
-
machine-local.
|
|
260
|
+
machine-local. For unbound executions, a missing resumed ACP session is retried once as a fresh session.
|
|
261
261
|
|
|
262
262
|
OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
|
|
263
263
|
workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
|
|
264
|
-
compatibility fallback).
|
|
264
|
+
compatibility fallback). For unbound executions, a missing resumed session is retried once without `--session`; malformed,
|
|
265
265
|
empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
|
|
266
266
|
`opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
|
|
267
267
|
only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
|
|
@@ -291,8 +291,12 @@ uses fresh sessions and committed text; it does not claim resume, images, MCP, r
|
|
|
291
291
|
or transcript support. Agent environment variables cannot override `DEEPSEEK_*`, `DSH_*`, or
|
|
292
292
|
`NOWCREW_DEEPSEEK_HARNESS_CONFIG`; those values always come from the daemon service environment.
|
|
293
293
|
|
|
294
|
-
Codex, Kimi, Hermes, and OpenCode sessions
|
|
295
|
-
|
|
294
|
+
Unbound Codex, Kimi, Hermes, and OpenCode sessions retain the same configured warm/budget policy and keyed
|
|
295
|
+
lease as Claude through protocol v1. Thread-bound Claude/Codex sessions keep their native session across
|
|
296
|
+
warm-cache expiry, token estimates, and turn counts; verified restoration receives only accepted-cursor
|
|
297
|
+
deltas. New sessions and runtimes without verified restoration receive complete current-thread history
|
|
298
|
+
before the business turn. See the [thread context contract](../docs/thread-scoped-native-context.md).
|
|
299
|
+
A first-progress watchdog covers a provider that accepts a turn but remains
|
|
296
300
|
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
297
301
|
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
298
302
|
DeepSeek Harness is execution-v1-only and intentionally skips native session persistence.
|
|
@@ -354,6 +358,15 @@ Runtime launch, which recovers from a missed notification or daemon restart. Ski
|
|
|
354
358
|
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
355
359
|
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
356
360
|
|
|
361
|
+
## 目录投影测试的平台范围
|
|
362
|
+
|
|
363
|
+
`test/directory-projection.test.ts` 在所有平台运行,覆盖路径检查、复制发布、只读权限请求及失败恢复。
|
|
364
|
+
其受控文件系统夹具记录目录 `chmod` 请求,但保留宿主目录的写权限,避免把 macOS 的目录重命名规则当作 Windows 行为。
|
|
365
|
+
生产代码的只读保护不受此夹具影响。
|
|
366
|
+
|
|
367
|
+
`test/directory-projection.windows.test.ts` 仅在 Windows 上运行,使用真实文件系统验证只读目录发布与文件保护。
|
|
368
|
+
非 Windows 的全量测试会明确跳过该文件,不能据此宣称 Windows 原生行为已验证。
|
|
369
|
+
|
|
357
370
|
## Reliability
|
|
358
371
|
|
|
359
372
|
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
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
const MAX_REPORT_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
export function reportDeliveryContext(executionId, runDir, facts) {
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
provider: facts.provider,
|
|
9
|
+
executionId,
|
|
10
|
+
receiptFile: join(runDir, `.qa-report-delivery-${createHash("sha256").update(executionId).digest("hex")}.json`),
|
|
11
|
+
...(facts.ossUploadScript === undefined ? {} : { script: facts.ossUploadScript }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function inside(root, file) {
|
|
15
|
+
const part = relative(root, file);
|
|
16
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute(part);
|
|
17
|
+
}
|
|
18
|
+
export async function verifiedReportReceipt(input) {
|
|
19
|
+
if (!input.receiptFile || !input.answer)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const stat = await lstat(input.receiptFile);
|
|
23
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024)
|
|
24
|
+
return false;
|
|
25
|
+
const receipt = JSON.parse(await readFile(input.receiptFile, "utf8"));
|
|
26
|
+
if (!receipt || typeof receipt !== "object")
|
|
27
|
+
return false;
|
|
28
|
+
const r = receipt;
|
|
29
|
+
if (r.version !== 1 || r.executionId !== input.executionId || r.evidenceComplete !== true
|
|
30
|
+
|| !Array.isArray(r.runIds) || r.runIds.length === 0 || r.runIds.length > 100
|
|
31
|
+
|| r.runIds.some(id => typeof id !== "string" || !id || id.length > 256)
|
|
32
|
+
|| typeof r.file !== "string" || !isAbsolute(r.file) || !r.file.endsWith(".html")
|
|
33
|
+
|| typeof r.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.sha256)
|
|
34
|
+
|| typeof r.url !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
const url = new URL(r.url);
|
|
37
|
+
if (url.protocol !== "https:" || url.hostname !== "ai-claw-in.nowcoder.com"
|
|
38
|
+
|| !url.pathname.endsWith(".html") || url.username || url.password || url.search || url.hash)
|
|
39
|
+
return false;
|
|
40
|
+
const answerUrls = input.answer.match(/https:\/\/ai-claw-in\.nowcoder\.com\/[^\s<>"')\]]+/g) ?? [];
|
|
41
|
+
if (!answerUrls.includes(url.href))
|
|
42
|
+
return false;
|
|
43
|
+
const file = await realpath(r.file);
|
|
44
|
+
const roots = await Promise.all([input.root, input.qaReportRoot ?? join(input.root, "qa-report-server-v2")]
|
|
45
|
+
.map(root => realpath(resolve(root)).catch(() => null)));
|
|
46
|
+
if (!roots.some(root => root !== null && inside(root, file)))
|
|
47
|
+
return false;
|
|
48
|
+
const fileStat = await lstat(file);
|
|
49
|
+
if (!fileStat.isFile() || fileStat.size > MAX_REPORT_BYTES)
|
|
50
|
+
return false;
|
|
51
|
+
const expectedHash = createHash("sha256").update(await readFile(file)).digest("hex");
|
|
52
|
+
if (expectedHash !== r.sha256)
|
|
53
|
+
return false;
|
|
54
|
+
const response = await fetch(url.href, { redirect: "error", signal: AbortSignal.timeout(15_000) });
|
|
55
|
+
if (!response.ok || !/^text\/html(?:;|$)/i.test(response.headers.get("content-type") ?? "")
|
|
56
|
+
|| /attachment/i.test(response.headers.get("content-disposition") ?? "") || !response.body) {
|
|
57
|
+
await response.body?.cancel();
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
const hash = createHash("sha256");
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const chunk = await reader.read();
|
|
66
|
+
if (chunk.done)
|
|
67
|
+
break;
|
|
68
|
+
bytes += chunk.value.byteLength;
|
|
69
|
+
if (bytes > MAX_REPORT_BYTES)
|
|
70
|
+
return false;
|
|
71
|
+
hash.update(chunk.value);
|
|
72
|
+
}
|
|
73
|
+
return bytes === fileStat.size && hash.digest("hex") === expectedHash;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await reader.cancel().catch(() => undefined);
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -4,6 +4,7 @@ import { extname, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { cloudReportUploadFacts } from "./cloud-report-upload.js";
|
|
6
6
|
import { dslog } from "./slog.js";
|
|
7
|
+
import { verifiedReportReceipt } from "./cloud-report-receipt.js";
|
|
7
8
|
const execFileP = promisify(execFile);
|
|
8
9
|
const REPORT_FILENAMES = new Set(["report.md", "result.json"]);
|
|
9
10
|
const MAX_DEPTH = 8;
|
|
@@ -166,6 +167,14 @@ export async function deliverCloudReports(input) {
|
|
|
166
167
|
if (!input.facts.enabled || input.facts.ossUploadScript === undefined || input.before === null) {
|
|
167
168
|
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
168
169
|
}
|
|
170
|
+
if (input.captureFinal && await verifiedReportReceipt({
|
|
171
|
+
...input, answer: input.externalAnswer ?? input.finalText,
|
|
172
|
+
})) {
|
|
173
|
+
dslog("cloud_report_upload.already_delivered", "本次执行的 HTML 交付收据及内容已验证,跳过重复上传", {
|
|
174
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
175
|
+
});
|
|
176
|
+
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
177
|
+
}
|
|
169
178
|
try {
|
|
170
179
|
const result = await (input.upload ?? uploadCloudReports)({
|
|
171
180
|
script: input.facts.ossUploadScript,
|
|
@@ -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,
|
package/dist/execution-runner.js
CHANGED
|
@@ -844,6 +844,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
844
844
|
executionId: spec.executionId,
|
|
845
845
|
handle: spec.agent.handle,
|
|
846
846
|
channelId: spec.context.channelId,
|
|
847
|
+
...(spec.context.threadId === undefined ? {} : { threadId: spec.context.threadId }),
|
|
847
848
|
keyMode: "opaque",
|
|
848
849
|
taskKey: spec.workspace.taskKey,
|
|
849
850
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
@@ -885,13 +886,13 @@ export async function runExecution(config, input, dependencies) {
|
|
|
885
886
|
} : {}),
|
|
886
887
|
},
|
|
887
888
|
session: {
|
|
888
|
-
enabled:
|
|
889
|
+
enabled: spec.instructions.maintenancePurpose !== undefined ? false : config.resume,
|
|
889
890
|
warmMs: config.resumeWarmMs,
|
|
890
891
|
budgetTokens: config.sessionBudgetTokens,
|
|
891
892
|
softTokens: config.sessionSoftTokens,
|
|
892
893
|
maxTurns: config.sessionMaxTurns,
|
|
893
894
|
},
|
|
894
|
-
...(
|
|
895
|
+
...(spec.instructions.maintenancePurpose === undefined ? {} : { maintenancePurpose: spec.instructions.maintenancePurpose }),
|
|
895
896
|
localMemoryV2Enabled,
|
|
896
897
|
};
|
|
897
898
|
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
|
@@ -12,8 +12,14 @@ import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
|
12
12
|
import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
|
|
13
13
|
import { augmentedPath } from "./runtime-path.js";
|
|
14
14
|
import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report-uploader.js";
|
|
15
|
+
import { reportDeliveryContext } from "./cloud-report-receipt.js";
|
|
15
16
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
16
17
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
18
|
+
import { NativeThreadContext, isolateMaintenanceSession } from "./native-thread-context.js";
|
|
19
|
+
import { prepareThreadDelivery } from "./thread-input-delivery.js";
|
|
20
|
+
import { resolveMemoryPruneWorkLog, workLogSha256 } from "./local-memory/work-log.js";
|
|
21
|
+
import { withKeyedLease } from "./local-execution-lease.js";
|
|
22
|
+
import { withWorkLogExecution, withWorkLogCommit, workLogExecutionKey } from "./local-memory/work-log-execution.js";
|
|
17
23
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
18
24
|
import { createConsoleFormatter } from "./console-formatter.js";
|
|
19
25
|
import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
|
|
@@ -34,7 +40,8 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
|
|
|
34
40
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
35
41
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
36
42
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
|
-
import { machinePackageManagerEnv
|
|
43
|
+
import { machinePackageManagerEnv } from "./package-manager-env.js";
|
|
44
|
+
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
38
45
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
46
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
40
47
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
@@ -106,6 +113,9 @@ async function launchLegacyRuntime(request) {
|
|
|
106
113
|
if (request.runtime === "deepseek-harness") {
|
|
107
114
|
throw new Error("DeepSeek Harness requires execution protocol v1");
|
|
108
115
|
}
|
|
116
|
+
if (request.runtime !== "claude")
|
|
117
|
+
request = { ...request, wakePrompt: (await prepareThreadDelivery({ env: request.env,
|
|
118
|
+
resumed: false, sessionId: "unverified-cold-legacy", wakePrompt: request.wakePrompt })).prompt };
|
|
109
119
|
const common = {
|
|
110
120
|
bin: request.bin,
|
|
111
121
|
cwd: request.cwd,
|
|
@@ -152,27 +162,12 @@ async function launchLegacyRuntime(request) {
|
|
|
152
162
|
function resolvePrompt(prompt, context) {
|
|
153
163
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
154
164
|
}
|
|
155
|
-
const nativeSessionLeaseTails = new Map();
|
|
156
165
|
const activeMemoryPrunes = new Map();
|
|
157
|
-
async function withKeyedLease(key, operation, cancellation) {
|
|
158
|
-
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
159
|
-
let release;
|
|
160
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
161
|
-
const tail = predecessor.then(() => current);
|
|
162
|
-
nativeSessionLeaseTails.set(key, tail);
|
|
163
|
-
try {
|
|
164
|
-
await awaitWithCancellation(predecessor, cancellation);
|
|
165
|
-
return await operation();
|
|
166
|
-
}
|
|
167
|
-
finally {
|
|
168
|
-
release();
|
|
169
|
-
void tail.then(() => {
|
|
170
|
-
if (nativeSessionLeaseTails.get(key) === tail)
|
|
171
|
-
nativeSessionLeaseTails.delete(key);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
166
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
167
|
+
input = isolateMaintenanceSession(input);
|
|
168
|
+
return withWorkLogExecution(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), () => executeLocalWithSession(input, callbacks, dependencies), dependencies.cancellation);
|
|
169
|
+
}
|
|
170
|
+
async function executeLocalWithSession(input, callbacks, dependencies) {
|
|
176
171
|
const versionedProjectSkills = input.projectSkills !== undefined
|
|
177
172
|
&& !Array.isArray(input.projectSkills);
|
|
178
173
|
if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
|
|
@@ -253,6 +248,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
253
248
|
let localMemoryResultPath = null;
|
|
254
249
|
let executionWorkspace = workspace;
|
|
255
250
|
let localMemoryTelemetry = null;
|
|
251
|
+
let nativeThreadContext = null;
|
|
256
252
|
const inheritedEnv = { ...process.env };
|
|
257
253
|
const reportRoots = {
|
|
258
254
|
root: workspace.dir,
|
|
@@ -260,6 +256,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
260
256
|
};
|
|
261
257
|
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
262
258
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
259
|
+
const reportContext = reportDeliveryContext(input.executionId, workspace.runDir, cloudReportUpload);
|
|
263
260
|
for (const key of Object.keys(inheritedEnv)) {
|
|
264
261
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
265
262
|
delete inheritedEnv[key];
|
|
@@ -291,12 +288,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
291
288
|
: null;
|
|
292
289
|
const currentPrior = storedCurrentPrior !== null
|
|
293
290
|
&& storedCurrentPrior.sessionContextFingerprint === input.sessionContextFingerprint
|
|
291
|
+
&& storedCurrentPrior.runtime === runtime.name
|
|
292
|
+
&& storedCurrentPrior.threadRoot === input.resumeKey
|
|
294
293
|
? storedCurrentPrior
|
|
295
294
|
: null;
|
|
296
|
-
//
|
|
297
|
-
// older legacy path stored metadata under tasks/<safe thread key>. Import
|
|
298
|
-
// that metadata once so an existing thread can cross the protocol boundary
|
|
299
|
-
// without losing its native runtime conversation.
|
|
295
|
+
// Import compatible legacy metadata across the protocol boundary.
|
|
300
296
|
const legacyRunDir = input.session.enabled
|
|
301
297
|
&& supportsNativeResume
|
|
302
298
|
&& input.keyMode === "opaque"
|
|
@@ -312,7 +308,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
312
308
|
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
313
309
|
? null
|
|
314
310
|
: await readSession(legacyRunDir);
|
|
315
|
-
const
|
|
311
|
+
const compatibleLegacyPrior = legacyPrior?.runtime === runtime.name && legacyPrior?.threadRoot === input.resumeKey
|
|
312
|
+
? legacyPrior : null;
|
|
313
|
+
const prior = currentPrior ?? compatibleLegacyPrior;
|
|
316
314
|
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
317
315
|
const usesLegacyCwd = legacyRunDir !== null
|
|
318
316
|
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
@@ -333,11 +331,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
333
331
|
? prior?.model ?? currentModel
|
|
334
332
|
: currentModel;
|
|
335
333
|
const resumeSessionId = supportsNativeResume && sessionExists
|
|
336
|
-
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
334
|
+
? 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)
|
|
337
335
|
: null;
|
|
338
336
|
const resuming = resumeSessionId !== null;
|
|
339
337
|
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
340
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
338
|
+
&& pickResumeId(prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
341
339
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
342
340
|
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
343
341
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
@@ -346,7 +344,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
346
344
|
? await rotateAgentSession(workspace.sessionDir)
|
|
347
345
|
: workspace.agentSessionId);
|
|
348
346
|
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 }) });
|
|
349
|
-
|
|
347
|
+
let promptContext = {
|
|
350
348
|
workspace: executionWorkspace,
|
|
351
349
|
resuming,
|
|
352
350
|
rotatedForBudget,
|
|
@@ -385,8 +383,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
385
383
|
}
|
|
386
384
|
}
|
|
387
385
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
388
|
-
|
|
389
|
-
memoryPruneTraceId = parseMemoryPruneTraceId(
|
|
386
|
+
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
387
|
+
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
388
|
+
if (memoryPruneTraceId !== null) {
|
|
389
|
+
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
390
|
+
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
391
|
+
baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
392
|
+
}
|
|
390
393
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
391
394
|
&& isLocalMemoryV2Eligible({
|
|
392
395
|
enabledForHandle: true,
|
|
@@ -399,12 +402,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
399
402
|
}
|
|
400
403
|
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
401
404
|
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
402
|
-
enabled: input.localMemoryV2Enabled === true, query:
|
|
405
|
+
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
403
406
|
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
404
407
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
405
408
|
});
|
|
406
409
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
407
410
|
const systemPrompt = memoryPrompt.systemPrompt;
|
|
411
|
+
const wakePrompt = baseWakePrompt + memoryPrompt.wakePromptSuffix;
|
|
408
412
|
if (!isolatedMaintenance)
|
|
409
413
|
try {
|
|
410
414
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -463,6 +467,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
463
467
|
}
|
|
464
468
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
465
469
|
const crewHome = isolatedMaintenance ? executionWorkspace.runDir : workspace.dir;
|
|
470
|
+
const contextThreadId = input.threadId ?? input.resumeKey;
|
|
471
|
+
if (contextThreadId && input.maintenancePurpose === undefined) {
|
|
472
|
+
nativeThreadContext = new NativeThreadContext(join(executionWorkspace.runDir, `.crew-context-${input.executionId}.json`), contextThreadId, runtime.name, resumeSessionId, prior);
|
|
473
|
+
await nativeThreadContext.initialize();
|
|
474
|
+
}
|
|
466
475
|
const baseEnv = {
|
|
467
476
|
...inheritedEnv,
|
|
468
477
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
@@ -475,6 +484,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
475
484
|
CREW_TOKEN: input.launch.token,
|
|
476
485
|
CREW_CHANNEL: input.channelId,
|
|
477
486
|
CREW_HOME: crewHome,
|
|
487
|
+
// Empty values override any stale inherited hints from an enclosing runtime.
|
|
488
|
+
CREW_THREAD_ROOT: contextThreadId ?? "",
|
|
489
|
+
CREW_THREAD_CONTEXT_FILE: nativeThreadContext?.path ?? "",
|
|
478
490
|
// Keep the daemon-selected storage root authoritative for runtimes.
|
|
479
491
|
// In profile-backed NowWork sessions the daemon config may differ from
|
|
480
492
|
// the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
|
|
@@ -485,6 +497,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
485
497
|
// Expose only the training root; ability-specific workspace layout stays
|
|
486
498
|
// owned by the training package rather than the NowWork daemon.
|
|
487
499
|
QA_RUNTIME_ROOT: join(crewHome, "training"),
|
|
500
|
+
QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
|
|
488
501
|
CREW_TASK_DIR: executionWorkspace.runDir,
|
|
489
502
|
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
490
503
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
@@ -504,30 +517,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
504
517
|
const childEnv = runtime.name === "deepseek-harness"
|
|
505
518
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
506
519
|
: providerEnv;
|
|
507
|
-
|
|
508
|
-
execution_id: input.executionId,
|
|
509
|
-
agent_handle: input.handle,
|
|
510
|
-
runtime: runtime.name,
|
|
511
|
-
qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
|
|
512
|
-
qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
|
|
513
|
-
inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
|
|
514
|
-
...packageManagerEnvFacts(childEnv),
|
|
515
|
-
training_root: join(crewHome, "training"),
|
|
516
|
-
cloud_provider: cloudReportUpload.provider,
|
|
517
|
-
oss_report_upload_enabled: cloudReportUpload.enabled,
|
|
518
|
-
project_context_present: input.projectContext !== undefined,
|
|
519
|
-
project_ids: JSON.stringify(input.projectContext === undefined
|
|
520
|
-
? []
|
|
521
|
-
: [
|
|
522
|
-
...(input.projectContext.primary === undefined
|
|
523
|
-
? []
|
|
524
|
-
: [input.projectContext.primary.projectId]),
|
|
525
|
-
...input.projectContext.secondary.map((project) => project.projectId),
|
|
526
|
-
]),
|
|
527
|
-
ability_release_present: input.abilityRelease !== undefined,
|
|
528
|
-
ability_release_id: input.abilityRelease?.releaseId,
|
|
529
|
-
ability_root_commit: input.abilityRelease?.rootCommit,
|
|
530
|
-
});
|
|
520
|
+
logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload);
|
|
531
521
|
const warn = dependencies.warnRuntime ?? warnRuntimeLocally;
|
|
532
522
|
const launchRuntime = dependencies.launchRuntime ?? (async (launchReq) => {
|
|
533
523
|
if (launchReq.runtime === "codex" && launchReq.codexCollaborationTools === true) {
|
|
@@ -544,7 +534,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
544
534
|
const effectiveSystemPrompt = abilityContext === undefined
|
|
545
535
|
? systemPrompt
|
|
546
536
|
: `${systemPrompt}\n\n${abilityContext.prompt}`;
|
|
547
|
-
if (Buffer.byteLength(effectiveSystemPrompt, "utf8")
|
|
537
|
+
if (Buffer.byteLength(effectiveSystemPrompt, "utf8") + Buffer.byteLength(memoryPrompt.wakePromptSuffix, "utf8")
|
|
538
|
+
> (input.maxSystemPromptBytes ?? Number.MAX_SAFE_INTEGER)) {
|
|
548
539
|
throw new Error("ability_runtime_context_size_exceeded");
|
|
549
540
|
}
|
|
550
541
|
memoryPruneFailurePhase = "prompt_write";
|
|
@@ -820,6 +811,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
820
811
|
const event = parseLine(line);
|
|
821
812
|
if (!event)
|
|
822
813
|
return;
|
|
814
|
+
nativeThreadContext?.observe(event);
|
|
823
815
|
localMemoryTelemetry?.observe(event);
|
|
824
816
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
825
817
|
markRuntimeReady();
|
|
@@ -936,22 +928,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
936
928
|
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
937
929
|
memoryPruneRuntimeExitCode = exitCode;
|
|
938
930
|
if (localMemoryResultPath !== null) {
|
|
939
|
-
|
|
940
|
-
|
|
931
|
+
memoryPruneFailurePhase = "memory_commit";
|
|
932
|
+
const resultPath = localMemoryResultPath;
|
|
933
|
+
const commit = () => commitLocalMemoryResultAfterExit({
|
|
934
|
+
resultPath,
|
|
941
935
|
agentDir: executionWorkspace.dir,
|
|
942
936
|
executionId: input.executionId,
|
|
943
937
|
pruneTraceId: memoryPruneTraceId,
|
|
944
938
|
exitCode,
|
|
939
|
+
workLogPath: executionWorkspace.workLogPath,
|
|
940
|
+
expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
|
|
945
941
|
});
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
942
|
+
const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
|
|
943
|
+
if (committed.status === "failed" || committed.status === "missing") {
|
|
944
|
+
const reason = committed.error ?? "local_memory_result_missing";
|
|
945
|
+
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
946
|
+
level: "ERROR",
|
|
949
947
|
execution_id: input.executionId,
|
|
950
948
|
agent_handle: input.handle,
|
|
951
|
-
error_code:
|
|
949
|
+
error_code: reason,
|
|
952
950
|
result_path: localMemoryResultPath,
|
|
953
951
|
});
|
|
952
|
+
throw new Error(`local_memory_commit_failed: ${reason}`);
|
|
954
953
|
}
|
|
954
|
+
memoryPruneFailurePhase = "runtime_finalize";
|
|
955
955
|
}
|
|
956
956
|
const errorTail = [
|
|
957
957
|
stderrTail.trim(),
|
|
@@ -973,9 +973,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
973
973
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
974
974
|
: (resuming ? prior?.contextTokens : undefined);
|
|
975
975
|
await writeSession(workspace.sessionDir, {
|
|
976
|
+
runtime: runtime.name,
|
|
977
|
+
...(input.resumeKey === undefined ? {} : { threadRoot: input.resumeKey }),
|
|
978
|
+
...(exitCode === 0 ? await nativeThreadContext?.checkpoint() ?? {} : {}),
|
|
976
979
|
sessionId,
|
|
977
980
|
lastRunAt: Date.now(),
|
|
978
|
-
turns:
|
|
981
|
+
turns: nativeThreadContext?.confirmed ? (prior?.turns ?? 0) + 1 : 1,
|
|
979
982
|
model: currentModel,
|
|
980
983
|
providerFingerprint: providerFp,
|
|
981
984
|
...(input.sessionContextFingerprint === undefined
|
|
@@ -991,6 +994,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
991
994
|
const cloudReportResult = exitCode === 0
|
|
992
995
|
? await deliverCloudReports({
|
|
993
996
|
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
997
|
+
receiptFile: reportContext.receiptFile,
|
|
994
998
|
externalAnswer: externalOutput.lastClosedBlock
|
|
995
999
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
996
1000
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -1008,7 +1012,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1008
1012
|
...(usage === undefined ? {} : { usage }),
|
|
1009
1013
|
model: observedModel,
|
|
1010
1014
|
runtime: runtime.name,
|
|
1011
|
-
resumed:
|
|
1015
|
+
resumed: nativeThreadContext?.confirmed ?? false,
|
|
1012
1016
|
sessionId,
|
|
1013
1017
|
errorMessage: errorTail || null,
|
|
1014
1018
|
...(providerError === undefined ? {} : { providerError }),
|
|
@@ -1020,6 +1024,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1020
1024
|
};
|
|
1021
1025
|
}
|
|
1022
1026
|
finally {
|
|
1027
|
+
await nativeThreadContext?.cleanup().catch(() => undefined);
|
|
1023
1028
|
startupReservationState.current?.release();
|
|
1024
1029
|
try {
|
|
1025
1030
|
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);
|