@nowcrew/daemon 0.6.87 → 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 +45 -10
- package/dist/agent-memory/policy.js +3 -1
- package/dist/cloud-report-receipt.js +83 -0
- package/dist/cloud-report-uploader.js +36 -25
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +9 -5
- package/dist/list-models.js +2 -0
- package/dist/local-execution-prompt.js +19 -25
- package/dist/local-executor.js +67 -85
- 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 +40 -0
- package/dist/local-memory/read-cli.js +281 -0
- package/dist/local-memory/runtime.js +56 -7
- package/dist/local-memory/write.js +16 -48
- package/dist/local-memory-diagnostics.js +79 -12
- package/dist/native-thread-context.js +59 -0
- package/dist/prompt.js +39 -24
- 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/dist/workspace.js +15 -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
|
|
@@ -418,6 +431,28 @@ Local policy can reduce server-requested access and limits; it cannot grant more
|
|
|
418
431
|
Provider credentials and configured environment are prepared locally and never carried in execution
|
|
419
432
|
control frames.
|
|
420
433
|
|
|
434
|
+
## 完整本地记忆读取
|
|
435
|
+
|
|
436
|
+
运行时注入的 `MEMORY.md` 是有界预览,未命中不代表完整文件没有相关记忆。
|
|
437
|
+
普通 Agent 工作区会在现有 PATH 目录中提供只读 `crew-memory` 工具,无需 Server 凭证:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
crew-memory search --query '提交前检查'
|
|
441
|
+
crew-memory search --query '提交前检查' --after-line 120
|
|
442
|
+
crew-memory read --offset 2400 --limit 2000
|
|
443
|
+
crew-memory read --file notes/testing.md --offset 0 --limit 2000
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
命令默认返回 JSON。搜索遍历完整指定文件,返回 `line`、`offset`、命中摘要和
|
|
447
|
+
`nextAfterLine`;读取按 Unicode 字符偏移分页,使用 `nextOffset` 继续直到 `eof=true`。
|
|
448
|
+
后续页可传 `--if-hash <上一页的sha256>`,文件变化时明确停止,不混读不同版本。
|
|
449
|
+
每页 JSON 有 12 KiB 上限,输入文件超过 32 MiB 时明确报错,不将未完成搜索伪装为零命中。
|
|
450
|
+
|
|
451
|
+
工具仅支持 `MEMORY.md` 和 `notes/` 下明确指定的 Markdown 文件,不遍历 pending,
|
|
452
|
+
不写入或迁移记忆。搜索结果是原始资料定位,读取明细前仍需确认当前有效索引和正文片段。
|
|
453
|
+
这些修改不改变现有 V2 写入上限、候选状态或归档策略,也不提供自动语义召回保证。
|
|
454
|
+
工具随 daemon 包发布;旧 daemon 没有该入口时,Agent 使用原生文件搜索与分段读取回退。
|
|
455
|
+
|
|
421
456
|
## Optional TencentDB Agent Memory Bridge
|
|
422
457
|
|
|
423
458
|
Execution protocol v1 can opt NowWork Agents into the private TencentDB Agent Memory Panel.
|
|
@@ -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;
|
|
@@ -13,22 +14,17 @@ const fileFingerprint = async (path) => {
|
|
|
13
14
|
const stat = await lstat(path);
|
|
14
15
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
15
16
|
};
|
|
16
|
-
function isReportFile(root, path) {
|
|
17
|
+
function isReportFile(root, path, kind) {
|
|
17
18
|
const parts = relative(root, path).split(sep).filter(Boolean);
|
|
18
19
|
const filename = parts.at(-1) ?? "";
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
if (parts[0] !== "qa-report-server-v2")
|
|
23
|
-
return false;
|
|
24
|
-
if (parts[1] === "static" && parts.length === 3)
|
|
25
|
-
return extname(filename).toLowerCase() === ".html";
|
|
26
|
-
if (parts[1] === "qa-results" && parts.length >= 4) {
|
|
20
|
+
if (kind === "static")
|
|
21
|
+
return parts.length === 1 && extname(filename).toLowerCase() === ".html";
|
|
22
|
+
if (parts.length >= 2) {
|
|
27
23
|
return REPORT_FILENAMES.has(filename) || extname(filename).toLowerCase() === ".html";
|
|
28
24
|
}
|
|
29
25
|
return false;
|
|
30
26
|
}
|
|
31
|
-
async function walkReports(root, directory, depth, output) {
|
|
27
|
+
async function walkReports(root, directory, depth, output, kind) {
|
|
32
28
|
if (depth > MAX_DEPTH || output.length >= MAX_FILES)
|
|
33
29
|
return;
|
|
34
30
|
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
@@ -39,28 +35,26 @@ async function walkReports(root, directory, depth, output) {
|
|
|
39
35
|
continue;
|
|
40
36
|
const path = join(directory, entry.name);
|
|
41
37
|
if (entry.isDirectory()) {
|
|
42
|
-
await walkReports(root, path, depth + 1, output);
|
|
38
|
+
await walkReports(root, path, depth + 1, output, kind);
|
|
43
39
|
}
|
|
44
|
-
else if (entry.isFile() && isReportFile(root, path)) {
|
|
40
|
+
else if (entry.isFile() && isReportFile(root, path, kind)) {
|
|
45
41
|
output.push(resolve(path));
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
|
-
export async function snapshotReportFiles(root) {
|
|
45
|
+
export async function snapshotReportFiles(root, qaReportRoot) {
|
|
50
46
|
const files = [];
|
|
51
47
|
const workspaceRoot = resolve(root);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
join(
|
|
55
|
-
|
|
56
|
-
]) {
|
|
57
|
-
await walkReports(workspaceRoot, directory, 0, files);
|
|
48
|
+
const hub = resolve(qaReportRoot ?? join(workspaceRoot, "qa-report-server-v2"));
|
|
49
|
+
for (const kind of ["tasks", "qa-results", "static"]) {
|
|
50
|
+
const directory = join(kind === "tasks" ? workspaceRoot : hub, kind);
|
|
51
|
+
await walkReports(directory, directory, 0, files, kind);
|
|
58
52
|
}
|
|
59
53
|
const entries = await Promise.all(files.map(async (path) => [path, await fileFingerprint(path)]));
|
|
60
54
|
return new Map(entries);
|
|
61
55
|
}
|
|
62
|
-
export async function discoverReportFiles(root, before) {
|
|
63
|
-
const after = await snapshotReportFiles(root);
|
|
56
|
+
export async function discoverReportFiles(root, before, qaReportRoot) {
|
|
57
|
+
const after = await snapshotReportFiles(root, qaReportRoot);
|
|
64
58
|
return [...after.entries()]
|
|
65
59
|
.filter(([path, fingerprint]) => before.get(path) !== fingerprint)
|
|
66
60
|
.map(([path]) => path)
|
|
@@ -97,7 +91,7 @@ async function verifyOssUrl(url) {
|
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
93
|
export async function uploadCloudReports(input) {
|
|
100
|
-
const files = await discoverReportFiles(input.root, input.before);
|
|
94
|
+
const files = await discoverReportFiles(input.root, input.before, input.qaReportRoot);
|
|
101
95
|
if (files.length === 0)
|
|
102
96
|
return { urls: [], failures: [] };
|
|
103
97
|
const run = input.execFile ?? (async (file, args) => {
|
|
@@ -131,10 +125,10 @@ export async function uploadCloudReports(input) {
|
|
|
131
125
|
}
|
|
132
126
|
return { urls, failures };
|
|
133
127
|
}
|
|
134
|
-
export async function prepareCloudReportSnapshot(facts, root) {
|
|
128
|
+
export async function prepareCloudReportSnapshot(facts, root, qaReportRoot) {
|
|
135
129
|
if (!facts.enabled)
|
|
136
130
|
return null;
|
|
137
|
-
return snapshotReportFiles(root);
|
|
131
|
+
return snapshotReportFiles(root, qaReportRoot);
|
|
138
132
|
}
|
|
139
133
|
export async function prepareCloudReportExecution(input) {
|
|
140
134
|
const facts = cloudReportUploadFacts(input.env);
|
|
@@ -147,7 +141,7 @@ export async function prepareCloudReportExecution(input) {
|
|
|
147
141
|
return { facts, snapshot: null };
|
|
148
142
|
}
|
|
149
143
|
try {
|
|
150
|
-
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root) };
|
|
144
|
+
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root, input.qaReportRoot) };
|
|
151
145
|
}
|
|
152
146
|
catch (error) {
|
|
153
147
|
dslog("cloud_report_upload.snapshot_failed", "报告文件快照失败", {
|
|
@@ -173,10 +167,19 @@ export async function deliverCloudReports(input) {
|
|
|
173
167
|
if (!input.facts.enabled || input.facts.ossUploadScript === undefined || input.before === null) {
|
|
174
168
|
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
175
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
|
+
}
|
|
176
178
|
try {
|
|
177
179
|
const result = await (input.upload ?? uploadCloudReports)({
|
|
178
180
|
script: input.facts.ossUploadScript,
|
|
179
181
|
root: input.root,
|
|
182
|
+
...(input.qaReportRoot === undefined ? {} : { qaReportRoot: input.qaReportRoot }),
|
|
180
183
|
before: input.before,
|
|
181
184
|
});
|
|
182
185
|
if (result.failures.length > 0) {
|
|
@@ -185,6 +188,14 @@ export async function deliverCloudReports(input) {
|
|
|
185
188
|
uploaded_count: result.urls.length, failed_count: result.failures.length,
|
|
186
189
|
});
|
|
187
190
|
}
|
|
191
|
+
else {
|
|
192
|
+
dslog(result.urls.length === 0 ? "cloud_report_upload.no_files" : "cloud_report_upload.completed", result.urls.length === 0 ? "未发现新增或变化的报告文件" : "报告上传完成", {
|
|
193
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
194
|
+
uploaded_count: result.urls.length,
|
|
195
|
+
report_root: input.root,
|
|
196
|
+
qa_report_root: input.qaReportRoot ?? join(input.root, "qa-report-server-v2"),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
188
199
|
return {
|
|
189
200
|
finalText: input.captureFinal && result.urls.length > 0
|
|
190
201
|
? appendReportLinks(input.finalText ?? "", result.urls)
|
|
@@ -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
|
@@ -834,13 +834,17 @@ export async function runExecution(config, input, dependencies) {
|
|
|
834
834
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
835
835
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
836
836
|
});
|
|
837
|
-
const boundedSystemPrompt = (context) =>
|
|
838
|
-
|
|
839
|
-
|
|
837
|
+
const boundedSystemPrompt = (context) => {
|
|
838
|
+
const base = typeof systemPromptWithLocalFacts === "string"
|
|
839
|
+
? systemPromptWithLocalFacts
|
|
840
|
+
: systemPromptWithLocalFacts(context);
|
|
841
|
+
return appendAgentMemoryContext(base, recalledMemory, Math.max(Buffer.byteLength(base, "utf8"), baseSystemPromptBudget - (context.reservedPromptBytes ?? 0)));
|
|
842
|
+
};
|
|
840
843
|
const localInput = {
|
|
841
844
|
executionId: spec.executionId,
|
|
842
845
|
handle: spec.agent.handle,
|
|
843
846
|
channelId: spec.context.channelId,
|
|
847
|
+
...(spec.context.threadId === undefined ? {} : { threadId: spec.context.threadId }),
|
|
844
848
|
keyMode: "opaque",
|
|
845
849
|
taskKey: spec.workspace.taskKey,
|
|
846
850
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
@@ -882,13 +886,13 @@ export async function runExecution(config, input, dependencies) {
|
|
|
882
886
|
} : {}),
|
|
883
887
|
},
|
|
884
888
|
session: {
|
|
885
|
-
enabled:
|
|
889
|
+
enabled: spec.instructions.maintenancePurpose !== undefined ? false : config.resume,
|
|
886
890
|
warmMs: config.resumeWarmMs,
|
|
887
891
|
budgetTokens: config.sessionBudgetTokens,
|
|
888
892
|
softTokens: config.sessionSoftTokens,
|
|
889
893
|
maxTurns: config.sessionMaxTurns,
|
|
890
894
|
},
|
|
891
|
-
...(
|
|
895
|
+
...(spec.instructions.maintenancePurpose === undefined ? {} : { maintenancePurpose: spec.instructions.maintenancePurpose }),
|
|
892
896
|
localMemoryV2Enabled,
|
|
893
897
|
};
|
|
894
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",
|
|
@@ -34,45 +34,39 @@ export function truncateUtf8(value, maxBytes) {
|
|
|
34
34
|
}
|
|
35
35
|
return value.slice(0, end);
|
|
36
36
|
}
|
|
37
|
-
function takeCompletePromptSections(sections, maxBytes) {
|
|
38
|
-
return sections.reduce((state, section) => {
|
|
39
|
-
if (!state.full)
|
|
40
|
-
return state;
|
|
41
|
-
const sectionBytes = Buffer.byteLength(section, "utf8");
|
|
42
|
-
if (state.bytes + sectionBytes > maxBytes)
|
|
43
|
-
return { ...state, full: false };
|
|
44
|
-
return { value: `${state.value}${section}`, bytes: state.bytes + sectionBytes, full: true };
|
|
45
|
-
}, { value: "", bytes: 0, full: true }).value;
|
|
46
|
-
}
|
|
47
37
|
/** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
|
|
48
38
|
export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
|
|
49
|
-
return ({ workspace, resuming, projectContext, trainingProjectContext }) => {
|
|
50
|
-
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory);
|
|
39
|
+
return ({ workspace, resuming, projectContext, trainingProjectContext, reservedPromptBytes = 0 }) => {
|
|
51
40
|
const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
|
|
52
41
|
const collaborationPrompt = options.collaborationTools === true ? buildCollaborationToolsPrompt() : "";
|
|
53
42
|
const mandatoryWorkspacePrompt = `${buildRuntimeWorkspacePrompt(projectContext)}${buildTrainingGitProjectPrompt(trainingProjectContext)}`;
|
|
54
43
|
const crewHome = options.isolateLocalMemory ? workspace.runDir : workspace.dir;
|
|
55
|
-
const
|
|
44
|
+
const localFacts = `\n\n## Local execution facts
|
|
56
45
|
- $CREW_HOME: ${crewHome}
|
|
57
|
-
- $CREW_TASK_LOG: ${workspace.workLogPath}
|
|
58
|
-
...(memory ? [`\n\n## Injected MEMORY.md (bounded local context)\n${memory}`] : []),
|
|
59
|
-
...(workLog ? [`\n\n## Injected work-log (bounded local context)\n${workLog}`] : []),
|
|
60
|
-
];
|
|
46
|
+
- $CREW_TASK_LOG: ${workspace.workLogPath}`;
|
|
61
47
|
const serverPromptBytes = Buffer.byteLength(serverPrompt, "utf8");
|
|
62
48
|
const collaborationBytes = Buffer.byteLength(collaborationPrompt, "utf8");
|
|
63
49
|
const mandatoryBytes = Buffer.byteLength(mandatoryWorkspacePrompt, "utf8");
|
|
64
50
|
const requiredBytes = serverPromptBytes + collaborationBytes + mandatoryBytes;
|
|
65
|
-
//
|
|
51
|
+
// 先保留全部必需内容,再按实际剩余字节一次性构造预览。
|
|
66
52
|
if (collaborationPrompt !== "" && requiredBytes > maxBytes) {
|
|
67
53
|
throw new Error("collaboration_tools_prompt_budget_exceeded");
|
|
68
54
|
}
|
|
69
|
-
if (mandatoryWorkspacePrompt !== "") {
|
|
70
|
-
|
|
71
|
-
throw new Error("project_workspace_prompt_budget_exceeded");
|
|
72
|
-
const optionalFacts = takeCompletePromptSections(localFactSections, maxBytes - requiredBytes);
|
|
73
|
-
return `${serverPrompt}${collaborationPrompt}${optionalFacts}${mandatoryWorkspacePrompt}`;
|
|
55
|
+
if (mandatoryWorkspacePrompt !== "" && requiredBytes > maxBytes) {
|
|
56
|
+
throw new Error("project_workspace_prompt_budget_exceeded");
|
|
74
57
|
}
|
|
75
|
-
const remaining = maxBytes - requiredBytes;
|
|
76
|
-
|
|
58
|
+
const remaining = maxBytes - requiredBytes - Math.max(0, reservedPromptBytes);
|
|
59
|
+
const factsBytes = Buffer.byteLength(localFacts, "utf8");
|
|
60
|
+
if (factsBytes > remaining)
|
|
61
|
+
return `${serverPrompt}${collaborationPrompt}${mandatoryWorkspacePrompt}`;
|
|
62
|
+
const memoryHeading = "\n\n## Injected MEMORY.md (bounded local context)\n";
|
|
63
|
+
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"));
|
|
64
|
+
const memorySection = memory === "" ? "" : memoryHeading + memory;
|
|
65
|
+
const workLogSection = workLog === "" ? "" : `\n\n## Injected work-log (bounded local context)\n${workLog}`;
|
|
66
|
+
const workLogBudget = remaining - factsBytes - Buffer.byteLength(memorySection, "utf8");
|
|
67
|
+
const boundedWorkLog = mandatoryWorkspacePrompt === ""
|
|
68
|
+
? truncateUtf8(workLogSection, workLogBudget)
|
|
69
|
+
: Buffer.byteLength(workLogSection, "utf8") <= workLogBudget ? workLogSection : "";
|
|
70
|
+
return `${serverPrompt}${collaborationPrompt}${localFacts}${memorySection}${boundedWorkLog}${mandatoryWorkspacePrompt}`;
|
|
77
71
|
};
|
|
78
72
|
}
|