@nowcrew/daemon 0.6.88-beta.2 → 0.6.88-beta.21
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 +43 -13
- package/dist/agent-memory/policy.js +3 -1
- package/dist/computer-profile.js +9 -4
- package/dist/config.js +0 -9
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +5 -4
- package/dist/list-models.js +2 -0
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +58 -45
- 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 +97 -12
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/machine-info.js +54 -17
- package/dist/main.js +4 -69
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +3 -6
- package/dist/runner.js +14 -16
- package/dist/runtime-probe.js +34 -5
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +224 -0
- package/dist/runtimes/deepseek-harness.js +64 -11
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/kimi-acp-runner.js +63 -10
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/serve.js +59 -101
- package/dist/session.js +4 -0
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/package.json +2 -3
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/README.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
The daemon is NowWork's local execution adapter. It holds a machine credential, keeps one control-plane
|
|
4
4
|
WebSocket open, and launches coding runtimes on that machine. Product policy remains on the server.
|
|
5
5
|
|
|
6
|
+
## 已完成搬迁后的存储运行
|
|
7
|
+
|
|
8
|
+
NAS / `NOWWORK_HOME` 搬迁和旧 daemon 布局标准化入口已退役。daemon 不再提供内部
|
|
9
|
+
`storage-migration-worker` / `storage-migration-recover` 命令,不处理 `daemon:migrate`、
|
|
10
|
+
`daemon:storage-doctor`、`daemon:storage-migrate` 或迁移确认消息,也不再声明
|
|
11
|
+
`storage_migration_v1` / `storage_migration_status_v1` / `daemon_layout_migration_v1` 能力。
|
|
12
|
+
|
|
13
|
+
现有 `installations.json` 即使仍有历史 `storageMigrationId`,也按当前 installation 解析
|
|
14
|
+
Agent、仓库和报告目录;启动、hello 和身份绑定不再读取迁移 receipt、outbox 或备份。
|
|
15
|
+
hello 继续报告布局、环境、Workspace、路径指纹与 ownership,但不再发送迁移 ID。
|
|
16
|
+
每次连接仍须完成 Server 身份绑定后才能执行任务;Workspace 或 Machine 身份不匹配时拒绝执行。
|
|
17
|
+
|
|
18
|
+
旧布局读取兼容、日常安装/升级、NAS 所有权校验、执行 journal 与 ACK/replay 继续保留。
|
|
19
|
+
退役代码不会删除历史安装字段、迁移记录、receipt、备份或旧数据目录;这些数据的保留与清理另行处理。
|
|
20
|
+
|
|
6
21
|
## Run
|
|
7
22
|
|
|
8
23
|
For a temporary foreground connection, run:
|
|
@@ -242,26 +257,26 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
|
|
|
242
257
|
|
|
243
258
|
| Runtime | Transport | System/wake input | Native resume |
|
|
244
259
|
| --- | --- | --- | --- |
|
|
245
|
-
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
246
|
-
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume
|
|
247
|
-
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load`
|
|
248
|
-
| Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load`
|
|
249
|
-
| OpenCode | `run --format json` | stdin | `--session
|
|
260
|
+
| Claude | stream-json CLI | daemon-owned system prompt file + user input | CLI session id; thread-bound: matching native SessionStart proof + SDK initialization |
|
|
261
|
+
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume`; thread-bound: returned id must match |
|
|
262
|
+
| Kimi | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
263
|
+
| Hermes | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
264
|
+
| OpenCode | `run --format json` | stdin | unbound: `--session`; thread-bound: cold |
|
|
250
265
|
| DeepSeek Harness | ACP over stdio | `session/prompt` | none (fresh session per execution) |
|
|
251
266
|
|
|
252
267
|
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
253
268
|
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
254
|
-
CLI
|
|
269
|
+
CLI; only unbound executions can carry a saved native session id. Unrelated ACP failures stay failures. The CLI fallback is
|
|
255
270
|
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
256
271
|
durable Job Object ownership is available.
|
|
257
272
|
|
|
258
273
|
Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
|
|
259
274
|
a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
|
|
260
|
-
machine-local.
|
|
275
|
+
machine-local. For unbound executions, a missing resumed ACP session is retried once as a fresh session.
|
|
261
276
|
|
|
262
277
|
OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
|
|
263
278
|
workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
|
|
264
|
-
compatibility fallback).
|
|
279
|
+
compatibility fallback). For unbound executions, a missing resumed session is retried once without `--session`; malformed,
|
|
265
280
|
empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
|
|
266
281
|
`opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
|
|
267
282
|
only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
|
|
@@ -291,8 +306,12 @@ uses fresh sessions and committed text; it does not claim resume, images, MCP, r
|
|
|
291
306
|
or transcript support. Agent environment variables cannot override `DEEPSEEK_*`, `DSH_*`, or
|
|
292
307
|
`NOWCREW_DEEPSEEK_HARNESS_CONFIG`; those values always come from the daemon service environment.
|
|
293
308
|
|
|
294
|
-
Codex, Kimi, Hermes, and OpenCode sessions
|
|
295
|
-
|
|
309
|
+
Unbound Codex, Kimi, Hermes, and OpenCode sessions retain the same configured warm/budget policy and keyed
|
|
310
|
+
lease as Claude through protocol v1. Thread-bound Claude/Codex sessions keep their native session across
|
|
311
|
+
warm-cache expiry, token estimates, and turn counts; verified restoration receives only accepted-cursor
|
|
312
|
+
deltas. New sessions and runtimes without verified restoration receive complete current-thread history
|
|
313
|
+
before the business turn. See the [thread context contract](../docs/thread-scoped-native-context.md).
|
|
314
|
+
A first-progress watchdog covers a provider that accepts a turn but remains
|
|
296
315
|
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
297
316
|
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
298
317
|
DeepSeek Harness is execution-v1-only and intentionally skips native session persistence.
|
|
@@ -343,9 +362,11 @@ changing the task cwd. Claude receives
|
|
|
343
362
|
so a machine policy that disables project settings also disables this source even when the projection is
|
|
344
363
|
healthy.
|
|
345
364
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
365
|
+
Ordinary Codex always uses the shared user Home: the inherited `CODEX_HOME`, or `~/.codex` when
|
|
366
|
+
unset. There is no Computer-level Home mode or private-Home migration command. Existing user-level
|
|
367
|
+
discovery is unchanged: Claude may still load `~/.claude/skills`, and Codex may load `$CODEX_HOME/skills`
|
|
368
|
+
and `~/.agents/skills`. The DeepSeek Codex variant keeps its Agent-private `.codex-deepseek` Home for
|
|
369
|
+
provider credentials. Project bindings do not hide or rewrite any of those sources.
|
|
349
370
|
|
|
350
371
|
Binding changes reconcile immediately when the Computer is online. On reconnect the server sends the
|
|
351
372
|
complete binding set for every Agent assigned to that Computer, including empty sets that clear stale
|
|
@@ -354,6 +375,15 @@ Runtime launch, which recovers from a missed notification or daemon restart. Ski
|
|
|
354
375
|
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
355
376
|
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
356
377
|
|
|
378
|
+
## 目录投影测试的平台范围
|
|
379
|
+
|
|
380
|
+
`test/directory-projection.test.ts` 在所有平台运行,覆盖路径检查、复制发布、只读权限请求及失败恢复。
|
|
381
|
+
其受控文件系统夹具记录目录 `chmod` 请求,但保留宿主目录的写权限,避免把 macOS 的目录重命名规则当作 Windows 行为。
|
|
382
|
+
生产代码的只读保护不受此夹具影响。
|
|
383
|
+
|
|
384
|
+
`test/directory-projection.windows.test.ts` 仅在 Windows 上运行,使用真实文件系统验证只读目录发布与文件保护。
|
|
385
|
+
非 Windows 的全量测试会明确跳过该文件,不能据此宣称 Windows 原生行为已验证。
|
|
386
|
+
|
|
357
387
|
## Reliability
|
|
358
388
|
|
|
359
389
|
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
|
}
|
package/dist/computer-profile.js
CHANGED
|
@@ -25,7 +25,6 @@ const ProfileFields = z.object({
|
|
|
25
25
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
26
26
|
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
27
27
|
registryUrl: z.string().url().optional(),
|
|
28
|
-
codexHomeMode: z.enum(["shared", "private"]).optional(),
|
|
29
28
|
}).strict();
|
|
30
29
|
function validateProfileRegistry(value, context) {
|
|
31
30
|
const fields = value;
|
|
@@ -60,7 +59,13 @@ const StoredProtectedProfileSchema = ProfileFields.omit({ machineToken: true }).
|
|
|
60
59
|
ciphertext: z.string().min(1).max(16384),
|
|
61
60
|
}).strict(),
|
|
62
61
|
}).strict().superRefine(validateProfileRegistry);
|
|
63
|
-
|
|
62
|
+
// 旧版本可能已保存 shared 字段;读取时丢弃退休设置,其余字段仍严格校验。
|
|
63
|
+
const StoredProfileSchema = z.preprocess((raw) => {
|
|
64
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
65
|
+
return raw;
|
|
66
|
+
const { codexHomeMode: _retiredMode, ...fields } = raw;
|
|
67
|
+
return fields;
|
|
68
|
+
}, z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]));
|
|
64
69
|
export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
|
|
65
70
|
export class ProfileAgentsRootConflictError extends Error {
|
|
66
71
|
profile;
|
|
@@ -257,7 +262,6 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
|
257
262
|
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
258
263
|
...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
|
|
259
264
|
...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
|
|
260
|
-
...(profile.codexHomeMode === undefined ? {} : { codexHomeMode: profile.codexHomeMode }),
|
|
261
265
|
machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
|
|
262
266
|
});
|
|
263
267
|
await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
|
|
@@ -404,5 +408,6 @@ export function applyProfileToEnv(profile, env) {
|
|
|
404
408
|
env.CREW_NPM_REGISTRY = profile.registryUrl;
|
|
405
409
|
else
|
|
406
410
|
delete env.CREW_NPM_REGISTRY;
|
|
407
|
-
|
|
411
|
+
// 不再向运行时传播退休的模式开关;CODEX_HOME 沿用用户环境。
|
|
412
|
+
delete env.CREW_CODEX_HOME_MODE;
|
|
408
413
|
}
|
package/dist/config.js
CHANGED
|
@@ -111,14 +111,6 @@ function localMemoryV2Handles(env, serverUrl) {
|
|
|
111
111
|
return new Set(handles);
|
|
112
112
|
}
|
|
113
113
|
export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
|
|
114
|
-
function codexHomeModeEnv(env) {
|
|
115
|
-
const value = env.CREW_CODEX_HOME_MODE;
|
|
116
|
-
if (value === undefined || value === "shared")
|
|
117
|
-
return "shared";
|
|
118
|
-
if (value === "private")
|
|
119
|
-
return "private";
|
|
120
|
-
throw new ConfigError("CREW_CODEX_HOME_MODE must be shared or private");
|
|
121
|
-
}
|
|
122
114
|
function codexCollaborationToolsEnv(env) {
|
|
123
115
|
const value = (env.CREW_CODEX_COLLABORATION_TOOLS ?? "").trim().toLowerCase();
|
|
124
116
|
if (value === "" || value === "0" || value === "false" || value === "off")
|
|
@@ -168,7 +160,6 @@ export function loadConfig(env = process.env) {
|
|
|
168
160
|
: defaultQaReportRoot,
|
|
169
161
|
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
170
162
|
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
171
|
-
codexHomeMode: codexHomeModeEnv(env),
|
|
172
163
|
codexCollaborationTools: codexCollaborationToolsEnv(env),
|
|
173
164
|
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
|
174
165
|
resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
|
|
@@ -14,7 +14,7 @@ import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, man
|
|
|
14
14
|
export async function managedDaemonCapabilities(eligibility) {
|
|
15
15
|
try {
|
|
16
16
|
return (await eligibility()).eligible
|
|
17
|
-
? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"
|
|
17
|
+
? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"]
|
|
18
18
|
: [];
|
|
19
19
|
}
|
|
20
20
|
catch {
|
|
@@ -878,23 +878,26 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
878
878
|
},
|
|
879
879
|
acknowledgeCompletion: async (executionId) => {
|
|
880
880
|
validateExecutionId(executionId);
|
|
881
|
-
const
|
|
881
|
+
const result = await serializedExecution(executionId, "acknowledge_completion", async () => {
|
|
882
882
|
const entry = await requireRecord(executionId);
|
|
883
883
|
if (entry.state !== "completed" && entry.state !== "interrupted") {
|
|
884
884
|
throw new JournalTransitionError(`Cannot acknowledge ${entry.state} execution`);
|
|
885
885
|
}
|
|
886
|
-
if (entry.completionAcknowledged)
|
|
887
|
-
return confirmDurable(entry);
|
|
886
|
+
if (entry.completionAcknowledged) {
|
|
887
|
+
return { entry: await confirmDurable(entry), newlyAcknowledged: false };
|
|
888
|
+
}
|
|
888
889
|
const updated = JournalEntrySchema.parse({
|
|
889
890
|
...entry,
|
|
890
891
|
completionAcknowledged: true,
|
|
891
892
|
updatedAt: now().toISOString(),
|
|
892
893
|
});
|
|
893
894
|
await writeRecord(updated);
|
|
894
|
-
return updated;
|
|
895
|
+
return { entry: updated, newlyAcknowledged: true };
|
|
895
896
|
});
|
|
896
|
-
|
|
897
|
-
|
|
897
|
+
// 重复 ACK 仍确认持久化,但不能反复全目录清理并阻塞后续控制帧。
|
|
898
|
+
if (result.newlyAcknowledged)
|
|
899
|
+
await serializedDirectory(pruneInternal);
|
|
900
|
+
return result.entry;
|
|
898
901
|
},
|
|
899
902
|
get: async (executionId) => {
|
|
900
903
|
validateExecutionId(executionId);
|
|
@@ -200,7 +200,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
200
200
|
language: z.enum(["zh", "en"]),
|
|
201
201
|
systemPrompt: z.string(),
|
|
202
202
|
wakePrompt: z.string(),
|
|
203
|
-
maintenancePurpose: z.
|
|
203
|
+
maintenancePurpose: z.enum(["self_training", "memory_prune"]).optional(),
|
|
204
204
|
}).strict(),
|
|
205
205
|
runtime: z.object({
|
|
206
206
|
name: RuntimeSchema,
|
|
@@ -264,6 +264,7 @@ export const ExecutionRejectedSchema = z.object({
|
|
|
264
264
|
reason: RejectionReasonSchema,
|
|
265
265
|
message: z.string().optional(),
|
|
266
266
|
at: TimestampSchema,
|
|
267
|
+
preAdmissionProof: z.object({ specHash: z.string().regex(/^[a-f0-9]{64}$/u) }).strict().optional(),
|
|
267
268
|
}).strict();
|
|
268
269
|
export const ExecutionStartedSchema = z.object({
|
|
269
270
|
type: z.literal("execution:started"),
|
package/dist/execution-runner.js
CHANGED
|
@@ -408,7 +408,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
408
408
|
? { permission: effectivePermission(spec, config) }
|
|
409
409
|
: admission(spec, config, dependencies, initialAt);
|
|
410
410
|
if ("rejected" in checked) {
|
|
411
|
-
const
|
|
411
|
+
const proof = await dependencies.confirmPreAdmission?.(checked.rejected).catch(() => false);
|
|
412
|
+
const frame = ExecutionRejectedSchema.parse(boundExecutionFrame({ ...checked.rejected, ...(proof ? { preAdmissionProof: { specHash } } : {}) }, config.executionLimits.maxEventBytes));
|
|
412
413
|
await dependencies.report(frame);
|
|
413
414
|
return { kind: "rejected", frame };
|
|
414
415
|
}
|
|
@@ -844,6 +845,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
844
845
|
executionId: spec.executionId,
|
|
845
846
|
handle: spec.agent.handle,
|
|
846
847
|
channelId: spec.context.channelId,
|
|
848
|
+
...(spec.context.threadId === undefined ? {} : { threadId: spec.context.threadId }),
|
|
847
849
|
keyMode: "opaque",
|
|
848
850
|
taskKey: spec.workspace.taskKey,
|
|
849
851
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
@@ -876,7 +878,6 @@ export async function runExecution(config, input, dependencies) {
|
|
|
876
878
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
877
879
|
cliPath: config.cliPath,
|
|
878
880
|
providerConfig,
|
|
879
|
-
codexHomeMode: config.codexHomeMode,
|
|
880
881
|
...(providerConfig.description ? { description: providerConfig.description } : {}),
|
|
881
882
|
...(spec.reporting.allowBoundImDecision ? {
|
|
882
883
|
systemEnv: {
|
|
@@ -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
|
|
1
2
|
import { createInterface } from "node:readline";
|
|
2
3
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { delimiter, join } from "node:path";
|
|
@@ -6,8 +7,6 @@ import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
|
|
|
6
7
|
import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
|
|
7
8
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
8
9
|
import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
9
|
-
import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
|
|
10
|
-
import { materializePrivateCodexHome } from "./codex-home-launch.js";
|
|
11
10
|
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
12
11
|
import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
|
|
13
12
|
import { augmentedPath } from "./runtime-path.js";
|
|
@@ -15,6 +14,11 @@ import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report
|
|
|
15
14
|
import { reportDeliveryContext } from "./cloud-report-receipt.js";
|
|
16
15
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
17
16
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
17
|
+
import { NativeThreadContext, isolateMaintenanceSession } from "./native-thread-context.js";
|
|
18
|
+
import { prepareThreadDelivery } from "./thread-input-delivery.js";
|
|
19
|
+
import { resolveMemoryPruneWorkLog, workLogSha256 } from "./local-memory/work-log.js";
|
|
20
|
+
import { withKeyedLease } from "./local-execution-lease.js";
|
|
21
|
+
import { withWorkLogExecution, withWorkLogCommit, workLogExecutionKey } from "./local-memory/work-log-execution.js";
|
|
18
22
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
19
23
|
import { createConsoleFormatter } from "./console-formatter.js";
|
|
20
24
|
import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
|
|
@@ -108,6 +112,9 @@ async function launchLegacyRuntime(request) {
|
|
|
108
112
|
if (request.runtime === "deepseek-harness") {
|
|
109
113
|
throw new Error("DeepSeek Harness requires execution protocol v1");
|
|
110
114
|
}
|
|
115
|
+
if (request.runtime !== "claude")
|
|
116
|
+
request = { ...request, wakePrompt: (await prepareThreadDelivery({ env: request.env,
|
|
117
|
+
resumed: false, sessionId: "unverified-cold-legacy", wakePrompt: request.wakePrompt })).prompt };
|
|
111
118
|
const common = {
|
|
112
119
|
bin: request.bin,
|
|
113
120
|
cwd: request.cwd,
|
|
@@ -154,27 +161,12 @@ async function launchLegacyRuntime(request) {
|
|
|
154
161
|
function resolvePrompt(prompt, context) {
|
|
155
162
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
156
163
|
}
|
|
157
|
-
const nativeSessionLeaseTails = new Map();
|
|
158
164
|
const activeMemoryPrunes = new Map();
|
|
159
|
-
async function withKeyedLease(key, operation, cancellation) {
|
|
160
|
-
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
161
|
-
let release;
|
|
162
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
163
|
-
const tail = predecessor.then(() => current);
|
|
164
|
-
nativeSessionLeaseTails.set(key, tail);
|
|
165
|
-
try {
|
|
166
|
-
await awaitWithCancellation(predecessor, cancellation);
|
|
167
|
-
return await operation();
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
release();
|
|
171
|
-
void tail.then(() => {
|
|
172
|
-
if (nativeSessionLeaseTails.get(key) === tail)
|
|
173
|
-
nativeSessionLeaseTails.delete(key);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
165
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
166
|
+
input = isolateMaintenanceSession(input);
|
|
167
|
+
return withWorkLogExecution(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), () => executeLocalWithSession(input, callbacks, dependencies), dependencies.cancellation);
|
|
168
|
+
}
|
|
169
|
+
async function executeLocalWithSession(input, callbacks, dependencies) {
|
|
178
170
|
const versionedProjectSkills = input.projectSkills !== undefined
|
|
179
171
|
&& !Array.isArray(input.projectSkills);
|
|
180
172
|
if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
|
|
@@ -255,6 +247,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
255
247
|
let localMemoryResultPath = null;
|
|
256
248
|
let executionWorkspace = workspace;
|
|
257
249
|
let localMemoryTelemetry = null;
|
|
250
|
+
let nativeThreadContext = null;
|
|
258
251
|
const inheritedEnv = { ...process.env };
|
|
259
252
|
const reportRoots = {
|
|
260
253
|
root: workspace.dir,
|
|
@@ -267,8 +260,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
267
260
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
268
261
|
delete inheritedEnv[key];
|
|
269
262
|
}
|
|
270
|
-
const sourceCodexHome = inheritedEnv.CODEX_HOME
|
|
271
|
-
?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
|
|
272
263
|
let materializedDeepSeekHome;
|
|
273
264
|
try {
|
|
274
265
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
@@ -294,12 +285,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
294
285
|
: null;
|
|
295
286
|
const currentPrior = storedCurrentPrior !== null
|
|
296
287
|
&& storedCurrentPrior.sessionContextFingerprint === input.sessionContextFingerprint
|
|
288
|
+
&& storedCurrentPrior.runtime === runtime.name
|
|
289
|
+
&& storedCurrentPrior.threadRoot === input.resumeKey
|
|
297
290
|
? storedCurrentPrior
|
|
298
291
|
: null;
|
|
299
|
-
//
|
|
300
|
-
// older legacy path stored metadata under tasks/<safe thread key>. Import
|
|
301
|
-
// that metadata once so an existing thread can cross the protocol boundary
|
|
302
|
-
// without losing its native runtime conversation.
|
|
292
|
+
// Import compatible legacy metadata across the protocol boundary.
|
|
303
293
|
const legacyRunDir = input.session.enabled
|
|
304
294
|
&& supportsNativeResume
|
|
305
295
|
&& input.keyMode === "opaque"
|
|
@@ -315,7 +305,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
315
305
|
const legacyPrior = legacyRunDir === null || (currentPrior !== null && !legacyCwdPinned)
|
|
316
306
|
? null
|
|
317
307
|
: await readSession(legacyRunDir);
|
|
318
|
-
const
|
|
308
|
+
const compatibleLegacyPrior = legacyPrior?.runtime === runtime.name && legacyPrior?.threadRoot === input.resumeKey
|
|
309
|
+
? legacyPrior : null;
|
|
310
|
+
const prior = currentPrior ?? compatibleLegacyPrior;
|
|
319
311
|
const sessionExists = workspace.sessionResume || legacyPrior !== null;
|
|
320
312
|
const usesLegacyCwd = legacyRunDir !== null
|
|
321
313
|
&& (legacyCwdPinned || (currentPrior === null && legacyPrior !== null));
|
|
@@ -336,11 +328,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
336
328
|
? prior?.model ?? currentModel
|
|
337
329
|
: currentModel;
|
|
338
330
|
const resumeSessionId = supportsNativeResume && sessionExists
|
|
339
|
-
? pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, input.session.budgetTokens, input.session.maxTurns, providerFp)
|
|
331
|
+
? pickResumeId(input.resumeKey && (!Number.isSafeInteger(prior?.threadContextSeq) || prior?.threadHistoryComplete !== true) ? null : prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, input.resumeKey ? 0 : input.session.budgetTokens, input.resumeKey ? 0 : input.session.maxTurns, providerFp)
|
|
340
332
|
: null;
|
|
341
333
|
const resuming = resumeSessionId !== null;
|
|
342
334
|
const rotatedForBudget = !resuming && supportsNativeResume && sessionExists
|
|
343
|
-
&& pickResumeId(prior, Date.now(), input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
335
|
+
&& pickResumeId(prior, Date.now(), input.resumeKey ? 0 : input.session.warmMs, resumeComparisonModel, 0, 0, providerFp) !== null;
|
|
344
336
|
const nearBudget = resuming && isNearBudget(prior, input.session.softTokens);
|
|
345
337
|
const rotated = Boolean(workspace.agentSessionId && sessionExists && !resuming);
|
|
346
338
|
// Codex and Kimi choose their own id on a fresh start, so the native id persisted in
|
|
@@ -348,8 +340,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
348
340
|
const launchSessionId = resumeSessionId ?? (rotated
|
|
349
341
|
? await rotateAgentSession(workspace.sessionDir)
|
|
350
342
|
: workspace.agentSessionId);
|
|
351
|
-
|
|
352
|
-
const promptContext = {
|
|
343
|
+
let promptContext = {
|
|
353
344
|
workspace: executionWorkspace,
|
|
354
345
|
resuming,
|
|
355
346
|
rotatedForBudget,
|
|
@@ -388,8 +379,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
388
379
|
}
|
|
389
380
|
}
|
|
390
381
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
391
|
-
|
|
392
|
-
memoryPruneTraceId = parseMemoryPruneTraceId(
|
|
382
|
+
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
383
|
+
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
384
|
+
if (memoryPruneTraceId !== null) {
|
|
385
|
+
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
386
|
+
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
387
|
+
baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
388
|
+
}
|
|
393
389
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
394
390
|
&& isLocalMemoryV2Eligible({
|
|
395
391
|
enabledForHandle: true,
|
|
@@ -402,12 +398,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
402
398
|
}
|
|
403
399
|
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
404
400
|
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
405
|
-
enabled: input.localMemoryV2Enabled === true, query:
|
|
401
|
+
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
406
402
|
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
407
403
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
408
404
|
});
|
|
409
405
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
410
406
|
const systemPrompt = memoryPrompt.systemPrompt;
|
|
407
|
+
const wakePrompt = baseWakePrompt + memoryPrompt.wakePromptSuffix;
|
|
411
408
|
if (!isolatedMaintenance)
|
|
412
409
|
try {
|
|
413
410
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -466,6 +463,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
466
463
|
}
|
|
467
464
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
468
465
|
const crewHome = isolatedMaintenance ? executionWorkspace.runDir : workspace.dir;
|
|
466
|
+
const contextThreadId = input.threadId ?? input.resumeKey;
|
|
467
|
+
if (contextThreadId && input.maintenancePurpose === undefined) {
|
|
468
|
+
nativeThreadContext = new NativeThreadContext(join(executionWorkspace.runDir, `.crew-context-${input.executionId}.json`), contextThreadId, runtime.name, resumeSessionId, prior);
|
|
469
|
+
await nativeThreadContext.initialize();
|
|
470
|
+
}
|
|
469
471
|
const baseEnv = {
|
|
470
472
|
...inheritedEnv,
|
|
471
473
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
@@ -478,6 +480,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
478
480
|
CREW_TOKEN: input.launch.token,
|
|
479
481
|
CREW_CHANNEL: input.channelId,
|
|
480
482
|
CREW_HOME: crewHome,
|
|
483
|
+
// Empty values override any stale inherited hints from an enclosing runtime.
|
|
484
|
+
CREW_THREAD_ROOT: contextThreadId ?? "",
|
|
485
|
+
CREW_THREAD_CONTEXT_FILE: nativeThreadContext?.path ?? "",
|
|
481
486
|
// Keep the daemon-selected storage root authoritative for runtimes.
|
|
482
487
|
// In profile-backed NowWork sessions the daemon config may differ from
|
|
483
488
|
// the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
|
|
@@ -502,9 +507,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
502
507
|
? { KIMI_MODEL_THINKING_EFFORT: runtime.reasoning }
|
|
503
508
|
: {}),
|
|
504
509
|
};
|
|
505
|
-
const providerEnv = applyProviderEnv(baseEnv, runtime.name,
|
|
506
|
-
? providerConfig
|
|
507
|
-
: { ...providerConfig, codexHomeMode: input.launch.codexHomeMode }, workspace.homeDir, materializedDeepSeekHome);
|
|
510
|
+
const providerEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir, materializedDeepSeekHome);
|
|
508
511
|
const childEnv = runtime.name === "deepseek-harness"
|
|
509
512
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
510
513
|
: providerEnv;
|
|
@@ -525,7 +528,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
525
528
|
const effectiveSystemPrompt = abilityContext === undefined
|
|
526
529
|
? systemPrompt
|
|
527
530
|
: `${systemPrompt}\n\n${abilityContext.prompt}`;
|
|
528
|
-
if (Buffer.byteLength(effectiveSystemPrompt, "utf8")
|
|
531
|
+
if (Buffer.byteLength(effectiveSystemPrompt, "utf8") + Buffer.byteLength(memoryPrompt.wakePromptSuffix, "utf8")
|
|
532
|
+
> (input.maxSystemPromptBytes ?? Number.MAX_SAFE_INTEGER)) {
|
|
529
533
|
throw new Error("ability_runtime_context_size_exceeded");
|
|
530
534
|
}
|
|
531
535
|
memoryPruneFailurePhase = "prompt_write";
|
|
@@ -559,7 +563,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
559
563
|
});
|
|
560
564
|
}
|
|
561
565
|
const trainingRoots = input.trainingProjectContext?.projects.map((project) => project.root) ?? [];
|
|
562
|
-
const { codexSkillRoots, claudeAdditionalDirectories } = await projectSkillRuntimeDirectories({
|
|
566
|
+
const { codexSkillRoots, claudeAdditionalDirectories, harnessSkillRoots } = await projectSkillRuntimeDirectories({
|
|
563
567
|
runtime: runtime.name,
|
|
564
568
|
...(runtimeRoot === undefined ? {} : { runtimeRoot }),
|
|
565
569
|
agentRoot: workspace.dir,
|
|
@@ -597,7 +601,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
597
601
|
const workspaceRoots = [...legacyRoots, ...trainingRoots];
|
|
598
602
|
const launchRequest = {
|
|
599
603
|
runtime: runtime.name,
|
|
600
|
-
bin: runtime.name === "deepseek-harness" ?
|
|
604
|
+
bin: runtime.name === "deepseek-harness" ? deepSeekHarnessLauncher(runtimeEnv).bin : runtime.name,
|
|
605
|
+
...(runtime.name === "deepseek-harness" ? { harnessStateRoot: executionWorkspace.runDir, harnessSkillRoots: harnessSkillRoots ?? [] } : {}),
|
|
601
606
|
cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
|
|
602
607
|
...(runtimeRoot !== undefined
|
|
603
608
|
|| (input.projectSkills === undefined && input.abilityRelease === undefined)
|
|
@@ -801,6 +806,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
801
806
|
const event = parseLine(line);
|
|
802
807
|
if (!event)
|
|
803
808
|
return;
|
|
809
|
+
nativeThreadContext?.observe(event);
|
|
804
810
|
localMemoryTelemetry?.observe(event);
|
|
805
811
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
806
812
|
markRuntimeReady();
|
|
@@ -918,14 +924,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
918
924
|
memoryPruneRuntimeExitCode = exitCode;
|
|
919
925
|
if (localMemoryResultPath !== null) {
|
|
920
926
|
memoryPruneFailurePhase = "memory_commit";
|
|
921
|
-
const
|
|
922
|
-
|
|
927
|
+
const resultPath = localMemoryResultPath;
|
|
928
|
+
const commit = () => commitLocalMemoryResultAfterExit({
|
|
929
|
+
resultPath,
|
|
923
930
|
agentDir: executionWorkspace.dir,
|
|
924
931
|
executionId: input.executionId,
|
|
925
932
|
pruneTraceId: memoryPruneTraceId,
|
|
926
933
|
exitCode,
|
|
927
934
|
workLogPath: executionWorkspace.workLogPath,
|
|
935
|
+
expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
|
|
928
936
|
});
|
|
937
|
+
const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
|
|
929
938
|
if (committed.status === "failed" || committed.status === "missing") {
|
|
930
939
|
const reason = committed.error ?? "local_memory_result_missing";
|
|
931
940
|
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
@@ -959,9 +968,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
959
968
|
? usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens
|
|
960
969
|
: (resuming ? prior?.contextTokens : undefined);
|
|
961
970
|
await writeSession(workspace.sessionDir, {
|
|
971
|
+
runtime: runtime.name,
|
|
972
|
+
...(input.resumeKey === undefined ? {} : { threadRoot: input.resumeKey }),
|
|
973
|
+
...(exitCode === 0 ? await nativeThreadContext?.checkpoint() ?? {} : {}),
|
|
962
974
|
sessionId,
|
|
963
975
|
lastRunAt: Date.now(),
|
|
964
|
-
turns:
|
|
976
|
+
turns: nativeThreadContext?.confirmed ? (prior?.turns ?? 0) + 1 : 1,
|
|
965
977
|
model: currentModel,
|
|
966
978
|
providerFingerprint: providerFp,
|
|
967
979
|
...(input.sessionContextFingerprint === undefined
|
|
@@ -995,7 +1007,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
995
1007
|
...(usage === undefined ? {} : { usage }),
|
|
996
1008
|
model: observedModel,
|
|
997
1009
|
runtime: runtime.name,
|
|
998
|
-
resumed:
|
|
1010
|
+
resumed: nativeThreadContext?.confirmed ?? false,
|
|
999
1011
|
sessionId,
|
|
1000
1012
|
errorMessage: errorTail || null,
|
|
1001
1013
|
...(providerError === undefined ? {} : { providerError }),
|
|
@@ -1007,6 +1019,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1007
1019
|
};
|
|
1008
1020
|
}
|
|
1009
1021
|
finally {
|
|
1022
|
+
await nativeThreadContext?.cleanup().catch(() => undefined);
|
|
1010
1023
|
startupReservationState.current?.release();
|
|
1011
1024
|
try {
|
|
1012
1025
|
await localMemoryTelemetry?.finish();
|