@nowcrew/daemon 0.6.88-beta.1 → 0.6.88-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/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-executor.js +52 -42
- package/dist/local-memory/candidate.js +43 -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 +56 -7
- package/dist/local-memory/write.js +16 -48
- 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",
|
package/dist/local-executor.js
CHANGED
|
@@ -12,8 +12,11 @@ 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";
|
|
17
20
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
18
21
|
import { createConsoleFormatter } from "./console-formatter.js";
|
|
19
22
|
import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
|
|
@@ -34,7 +37,8 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
|
|
|
34
37
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
35
38
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
36
39
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
|
-
import { machinePackageManagerEnv
|
|
40
|
+
import { machinePackageManagerEnv } from "./package-manager-env.js";
|
|
41
|
+
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
38
42
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
43
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
40
44
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
@@ -106,6 +110,9 @@ async function launchLegacyRuntime(request) {
|
|
|
106
110
|
if (request.runtime === "deepseek-harness") {
|
|
107
111
|
throw new Error("DeepSeek Harness requires execution protocol v1");
|
|
108
112
|
}
|
|
113
|
+
if (request.runtime !== "claude")
|
|
114
|
+
request = { ...request, wakePrompt: (await prepareThreadDelivery({ env: request.env,
|
|
115
|
+
resumed: false, sessionId: "unverified-cold-legacy", wakePrompt: request.wakePrompt })).prompt };
|
|
109
116
|
const common = {
|
|
110
117
|
bin: request.bin,
|
|
111
118
|
cwd: request.cwd,
|
|
@@ -173,6 +180,7 @@ async function withKeyedLease(key, operation, cancellation) {
|
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
183
|
+
input = isolateMaintenanceSession(input);
|
|
176
184
|
const versionedProjectSkills = input.projectSkills !== undefined
|
|
177
185
|
&& !Array.isArray(input.projectSkills);
|
|
178
186
|
if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
|
|
@@ -253,6 +261,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
253
261
|
let localMemoryResultPath = null;
|
|
254
262
|
let executionWorkspace = workspace;
|
|
255
263
|
let localMemoryTelemetry = null;
|
|
264
|
+
let nativeThreadContext = null;
|
|
256
265
|
const inheritedEnv = { ...process.env };
|
|
257
266
|
const reportRoots = {
|
|
258
267
|
root: workspace.dir,
|
|
@@ -260,6 +269,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
260
269
|
};
|
|
261
270
|
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
262
271
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
272
|
+
const reportContext = reportDeliveryContext(input.executionId, workspace.runDir, cloudReportUpload);
|
|
263
273
|
for (const key of Object.keys(inheritedEnv)) {
|
|
264
274
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
265
275
|
delete inheritedEnv[key];
|
|
@@ -291,12 +301,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
291
301
|
: null;
|
|
292
302
|
const currentPrior = storedCurrentPrior !== null
|
|
293
303
|
&& storedCurrentPrior.sessionContextFingerprint === input.sessionContextFingerprint
|
|
304
|
+
&& storedCurrentPrior.runtime === runtime.name
|
|
305
|
+
&& storedCurrentPrior.threadRoot === input.resumeKey
|
|
294
306
|
? storedCurrentPrior
|
|
295
307
|
: 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.
|
|
308
|
+
// Import compatible legacy metadata across the protocol boundary.
|
|
300
309
|
const legacyRunDir = input.session.enabled
|
|
301
310
|
&& supportsNativeResume
|
|
302
311
|
&& input.keyMode === "opaque"
|
|
@@ -312,7 +321,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
312
321
|
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
313
322
|
? null
|
|
314
323
|
: await readSession(legacyRunDir);
|
|
315
|
-
const
|
|
324
|
+
const compatibleLegacyPrior = legacyPrior?.runtime === runtime.name && legacyPrior?.threadRoot === input.resumeKey
|
|
325
|
+
? legacyPrior : null;
|
|
326
|
+
const prior = currentPrior ?? compatibleLegacyPrior;
|
|
316
327
|
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
317
328
|
const usesLegacyCwd = legacyRunDir !== null
|
|
318
329
|
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
@@ -333,11 +344,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
333
344
|
? prior?.model ?? currentModel
|
|
334
345
|
: currentModel;
|
|
335
346
|
const resumeSessionId = supportsNativeResume && sessionExists
|
|
336
|
-
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
347
|
+
? 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
348
|
: null;
|
|
338
349
|
const resuming = resumeSessionId !== null;
|
|
339
350
|
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
340
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
351
|
+
&& pickResumeId(prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
341
352
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
342
353
|
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
343
354
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
@@ -385,8 +396,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
385
396
|
}
|
|
386
397
|
}
|
|
387
398
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
388
|
-
const
|
|
389
|
-
memoryPruneTraceId = parseMemoryPruneTraceId(
|
|
399
|
+
const baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
400
|
+
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
390
401
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
391
402
|
&& isLocalMemoryV2Eligible({
|
|
392
403
|
enabledForHandle: true,
|
|
@@ -399,12 +410,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
399
410
|
}
|
|
400
411
|
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
401
412
|
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
402
|
-
enabled: input.localMemoryV2Enabled === true, query:
|
|
413
|
+
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
403
414
|
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
404
415
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
405
416
|
});
|
|
406
417
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
407
418
|
const systemPrompt = memoryPrompt.systemPrompt;
|
|
419
|
+
const wakePrompt = baseWakePrompt + memoryPrompt.wakePromptSuffix;
|
|
408
420
|
if (!isolatedMaintenance)
|
|
409
421
|
try {
|
|
410
422
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -463,6 +475,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
463
475
|
}
|
|
464
476
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
465
477
|
const crewHome = isolatedMaintenance ? executionWorkspace.runDir : workspace.dir;
|
|
478
|
+
const contextThreadId = input.threadId ?? input.resumeKey;
|
|
479
|
+
if (contextThreadId && input.maintenancePurpose === undefined) {
|
|
480
|
+
nativeThreadContext = new NativeThreadContext(join(executionWorkspace.runDir, `.crew-context-${input.executionId}.json`), contextThreadId, runtime.name, resumeSessionId, prior);
|
|
481
|
+
await nativeThreadContext.initialize();
|
|
482
|
+
}
|
|
466
483
|
const baseEnv = {
|
|
467
484
|
...inheritedEnv,
|
|
468
485
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
@@ -475,6 +492,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
475
492
|
CREW_TOKEN: input.launch.token,
|
|
476
493
|
CREW_CHANNEL: input.channelId,
|
|
477
494
|
CREW_HOME: crewHome,
|
|
495
|
+
// Empty values override any stale inherited hints from an enclosing runtime.
|
|
496
|
+
CREW_THREAD_ROOT: contextThreadId ?? "",
|
|
497
|
+
CREW_THREAD_CONTEXT_FILE: nativeThreadContext?.path ?? "",
|
|
478
498
|
// Keep the daemon-selected storage root authoritative for runtimes.
|
|
479
499
|
// In profile-backed NowWork sessions the daemon config may differ from
|
|
480
500
|
// the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
|
|
@@ -485,6 +505,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
485
505
|
// Expose only the training root; ability-specific workspace layout stays
|
|
486
506
|
// owned by the training package rather than the NowWork daemon.
|
|
487
507
|
QA_RUNTIME_ROOT: join(crewHome, "training"),
|
|
508
|
+
QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
|
|
488
509
|
CREW_TASK_DIR: executionWorkspace.runDir,
|
|
489
510
|
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
490
511
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
@@ -504,30 +525,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
504
525
|
const childEnv = runtime.name === "deepseek-harness"
|
|
505
526
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
506
527
|
: 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
|
-
});
|
|
528
|
+
logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload);
|
|
531
529
|
const warn = dependencies.warnRuntime ?? warnRuntimeLocally;
|
|
532
530
|
const launchRuntime = dependencies.launchRuntime ?? (async (launchReq) => {
|
|
533
531
|
if (launchReq.runtime === "codex" && launchReq.codexCollaborationTools === true) {
|
|
@@ -544,7 +542,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
544
542
|
const effectiveSystemPrompt = abilityContext === undefined
|
|
545
543
|
? systemPrompt
|
|
546
544
|
: `${systemPrompt}\n\n${abilityContext.prompt}`;
|
|
547
|
-
if (Buffer.byteLength(effectiveSystemPrompt, "utf8")
|
|
545
|
+
if (Buffer.byteLength(effectiveSystemPrompt, "utf8") + Buffer.byteLength(memoryPrompt.wakePromptSuffix, "utf8")
|
|
546
|
+
> (input.maxSystemPromptBytes ?? Number.MAX_SAFE_INTEGER)) {
|
|
548
547
|
throw new Error("ability_runtime_context_size_exceeded");
|
|
549
548
|
}
|
|
550
549
|
memoryPruneFailurePhase = "prompt_write";
|
|
@@ -820,6 +819,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
820
819
|
const event = parseLine(line);
|
|
821
820
|
if (!event)
|
|
822
821
|
return;
|
|
822
|
+
nativeThreadContext?.observe(event);
|
|
823
823
|
localMemoryTelemetry?.observe(event);
|
|
824
824
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
825
825
|
markRuntimeReady();
|
|
@@ -936,22 +936,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
936
936
|
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
937
937
|
memoryPruneRuntimeExitCode = exitCode;
|
|
938
938
|
if (localMemoryResultPath !== null) {
|
|
939
|
+
memoryPruneFailurePhase = "memory_commit";
|
|
939
940
|
const committed = await commitLocalMemoryResultAfterExit({
|
|
940
941
|
resultPath: localMemoryResultPath,
|
|
941
942
|
agentDir: executionWorkspace.dir,
|
|
942
943
|
executionId: input.executionId,
|
|
943
944
|
pruneTraceId: memoryPruneTraceId,
|
|
944
945
|
exitCode,
|
|
946
|
+
workLogPath: executionWorkspace.workLogPath,
|
|
945
947
|
});
|
|
946
|
-
if (committed.status === "failed") {
|
|
947
|
-
|
|
948
|
-
|
|
948
|
+
if (committed.status === "failed" || committed.status === "missing") {
|
|
949
|
+
const reason = committed.error ?? "local_memory_result_missing";
|
|
950
|
+
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
951
|
+
level: "ERROR",
|
|
949
952
|
execution_id: input.executionId,
|
|
950
953
|
agent_handle: input.handle,
|
|
951
|
-
error_code:
|
|
954
|
+
error_code: reason,
|
|
952
955
|
result_path: localMemoryResultPath,
|
|
953
956
|
});
|
|
957
|
+
throw new Error(`local_memory_commit_failed: ${reason}`);
|
|
954
958
|
}
|
|
959
|
+
memoryPruneFailurePhase = "runtime_finalize";
|
|
955
960
|
}
|
|
956
961
|
const errorTail = [
|
|
957
962
|
stderrTail.trim(),
|
|
@@ -973,9 +978,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
973
978
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
974
979
|
: (resuming ? prior?.contextTokens : undefined);
|
|
975
980
|
await writeSession(workspace.sessionDir, {
|
|
981
|
+
runtime: runtime.name,
|
|
982
|
+
...(input.resumeKey === undefined ? {} : { threadRoot: input.resumeKey }),
|
|
983
|
+
...(exitCode === 0 ? await nativeThreadContext?.checkpoint() ?? {} : {}),
|
|
976
984
|
sessionId,
|
|
977
985
|
lastRunAt: Date.now(),
|
|
978
|
-
turns:
|
|
986
|
+
turns: nativeThreadContext?.confirmed ? (prior?.turns ?? 0) + 1 : 1,
|
|
979
987
|
model: currentModel,
|
|
980
988
|
providerFingerprint: providerFp,
|
|
981
989
|
...(input.sessionContextFingerprint === undefined
|
|
@@ -991,6 +999,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
991
999
|
const cloudReportResult = exitCode === 0
|
|
992
1000
|
? await deliverCloudReports({
|
|
993
1001
|
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
1002
|
+
receiptFile: reportContext.receiptFile,
|
|
994
1003
|
externalAnswer: externalOutput.lastClosedBlock
|
|
995
1004
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
996
1005
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -1008,7 +1017,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1008
1017
|
...(usage === undefined ? {} : { usage }),
|
|
1009
1018
|
model: observedModel,
|
|
1010
1019
|
runtime: runtime.name,
|
|
1011
|
-
resumed:
|
|
1020
|
+
resumed: nativeThreadContext?.confirmed ?? false,
|
|
1012
1021
|
sessionId,
|
|
1013
1022
|
errorMessage: errorTail || null,
|
|
1014
1023
|
...(providerError === undefined ? {} : { providerError }),
|
|
@@ -1020,6 +1029,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1020
1029
|
};
|
|
1021
1030
|
}
|
|
1022
1031
|
finally {
|
|
1032
|
+
await nativeThreadContext?.cleanup().catch(() => undefined);
|
|
1023
1033
|
startupReservationState.current?.release();
|
|
1024
1034
|
try {
|
|
1025
1035
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function normalizePendingQuery(value) {
|
|
2
|
+
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
3
|
+
}
|
|
4
|
+
function containsTerm(text, term) {
|
|
5
|
+
if (!term)
|
|
6
|
+
return false;
|
|
7
|
+
const startBoundary = /^[a-z0-9]/u.test(term);
|
|
8
|
+
const endBoundary = /[a-z0-9]$/u.test(term);
|
|
9
|
+
let position = text.indexOf(term);
|
|
10
|
+
while (position >= 0) {
|
|
11
|
+
const before = text[position - 1] ?? "";
|
|
12
|
+
const after = text[position + term.length] ?? "";
|
|
13
|
+
if ((!startBoundary || !/[a-z0-9_-]/u.test(before)) && (!endBoundary || !/[a-z0-9_-]/u.test(after)))
|
|
14
|
+
return true;
|
|
15
|
+
position = text.indexOf(term, position + 1);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function pendingRecallScore(item, query) {
|
|
20
|
+
const normalized = normalizePendingQuery(query);
|
|
21
|
+
if (!normalized)
|
|
22
|
+
return 0;
|
|
23
|
+
const candidate = item.candidate;
|
|
24
|
+
const terms = normalized.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter(Boolean);
|
|
25
|
+
const text = normalizePendingQuery([candidate.title, candidate.summary, ...candidate.triggers, candidate.detailCategory, candidate.detailBody].join("\n"));
|
|
26
|
+
const explicit = [candidate.title, ...candidate.triggers].map(normalizePendingQuery);
|
|
27
|
+
const explicitScore = explicit.reduce((score, term) => score + (containsTerm(normalized, term) ? 10 : 0), 0);
|
|
28
|
+
return explicitScore + terms.reduce((score, term) => score + (containsTerm(text, term) ? 1 : 0), 0);
|
|
29
|
+
}
|