@nowcrew/daemon 0.6.101-beta.3 → 0.6.101-beta.5

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.
Files changed (64) hide show
  1. package/dist/cloud-report-upload.js +1 -10
  2. package/dist/execution-journal-lock.js +51 -2
  3. package/dist/execution-journal.js +3 -1
  4. package/dist/execution-protocol.js +1 -1
  5. package/dist/execution-runner.js +4 -1
  6. package/dist/host-execution-coordinator.js +10 -0
  7. package/dist/local-execution-prompt.js +8 -3
  8. package/dist/local-executor.js +78 -55
  9. package/dist/local-memory/commit-telemetry.js +30 -0
  10. package/dist/local-memory/execution-workspace.js +117 -0
  11. package/dist/local-memory/full-index-search.js +47 -0
  12. package/dist/local-memory/index-budget.js +9 -3
  13. package/dist/local-memory/initialization-identity.js +36 -0
  14. package/dist/local-memory/inventory-cli.js +52 -8
  15. package/dist/local-memory/inventory-source.js +15 -8
  16. package/dist/local-memory/inventory-v2.js +362 -0
  17. package/dist/local-memory/legacy-adapter.js +233 -0
  18. package/dist/local-memory/migration-access.js +242 -0
  19. package/dist/local-memory/migration-cli.js +54 -0
  20. package/dist/local-memory/migration-io.js +199 -0
  21. package/dist/local-memory/migration-pending.js +9 -0
  22. package/dist/local-memory/migration-plan.js +432 -0
  23. package/dist/local-memory/migration-schema.js +58 -0
  24. package/dist/local-memory/migration-state-invariants.js +65 -0
  25. package/dist/local-memory/migration-store.js +414 -0
  26. package/dist/local-memory/migration-tool-entry.js +21 -0
  27. package/dist/local-memory/pending-query.js +5 -2
  28. package/dist/local-memory/pending-schema.js +1 -1
  29. package/dist/local-memory/pending.js +16 -2
  30. package/dist/local-memory/prompt.js +21 -5
  31. package/dist/local-memory/read-cli.js +98 -76
  32. package/dist/local-memory/release-artifact-guard.js +55 -0
  33. package/dist/local-memory/rollout.js +79 -11
  34. package/dist/local-memory/runtime.js +191 -17
  35. package/dist/local-memory/source-path.js +13 -0
  36. package/dist/local-memory/storage-format.js +249 -0
  37. package/dist/local-memory/write.js +190 -21
  38. package/dist/local-memory/writer-mode.js +13 -0
  39. package/dist/local-memory-diagnostics.js +96 -15
  40. package/dist/local-memory-telemetry.js +31 -3
  41. package/dist/machine-info.js +6 -1
  42. package/dist/machine-runtime-environment.js +1 -0
  43. package/dist/marketplace-protocol.js +1 -1
  44. package/dist/marketplace-remote.js +1 -1
  45. package/dist/marketplace-runner.js +1 -1
  46. package/dist/memory-migration-controller.js +89 -0
  47. package/dist/memory-migration-inspect.js +116 -0
  48. package/dist/memory-migration-journal.js +14 -0
  49. package/dist/memory-migration-protocol.js +14 -0
  50. package/dist/memory-migration-runtime.js +367 -0
  51. package/dist/memory-prune-diagnostics.js +74 -4
  52. package/dist/message-file-preview.js +11 -1
  53. package/dist/prompt.js +9 -17
  54. package/dist/runner.js +1 -1
  55. package/dist/runtime-tool-update-bridge.js +126 -0
  56. package/dist/runtime-tool-update-controller.js +151 -0
  57. package/dist/runtime-tool-update-protocol.js +24 -0
  58. package/dist/runtime-tool-update-serve.js +54 -0
  59. package/dist/runtimes/claude-thread-runner.js +25 -4
  60. package/dist/serve.js +47 -3
  61. package/dist/token.js +1 -0
  62. package/dist/workspace-control-handler.js +16 -3
  63. package/dist/workspace.js +23 -4
  64. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { RuntimeToolResponseSchema } from "./runtime-tool-update-protocol.js";
7
+ export function runtimeToolEnvironment(env) {
8
+ // Keep only machine identity and lookup settings. Never pass runtime/control-plane credentials or Node injection flags.
9
+ return Object.fromEntries(Object.entries(env).filter(([key]) => /^(PATH|HOME|USER|LOGNAME|SHELL|TMPDIR|TMP|TEMP|LANG|LC_[A-Z_]+|XDG_CONFIG_HOME|XDG_CACHE_HOME)$/u.test(key)));
10
+ }
11
+ async function readReport(dir) {
12
+ const file = join(dir, "report.json");
13
+ if ((await stat(file)).size > 256_000)
14
+ throw new Error("invalid_report");
15
+ return RuntimeToolResponseSchema.parse(JSON.parse(await readFile(file, "utf8")));
16
+ }
17
+ async function stopGroup(pid) {
18
+ if (!pid)
19
+ return true;
20
+ try {
21
+ process.kill(-pid, "SIGKILL");
22
+ }
23
+ catch (error) {
24
+ return error.code === "ESRCH";
25
+ }
26
+ const deadline = Date.now() + 3000;
27
+ while (Date.now() < deadline) {
28
+ try {
29
+ process.kill(-pid, 0);
30
+ }
31
+ catch (error) {
32
+ return error.code === "ESRCH";
33
+ }
34
+ await new Promise(resolve => setTimeout(resolve, 50));
35
+ }
36
+ return false;
37
+ }
38
+ export function createRuntimeToolProgressPoller(read, progress, intervalMs = 250) {
39
+ let active = true;
40
+ let pending = null;
41
+ const interval = setInterval(() => {
42
+ if (!active || pending || !progress)
43
+ return;
44
+ pending = read().then(async (data) => { if (active)
45
+ await progress(data); })
46
+ .catch(() => { }).finally(() => { pending = null; });
47
+ }, intervalMs);
48
+ return { async stop() { active = false; clearInterval(interval); await pending; } };
49
+ }
50
+ async function supervise(dir, env, start, progress, protectProcessGroup) {
51
+ const child = spawn(process.execPath, [join(dir, "runner.mjs"), "--managed-worker"], { cwd: dir, env, detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"] });
52
+ const closed = new Promise(resolve => { child.once("error", () => resolve(null)); child.once("close", resolve); });
53
+ const polling = createRuntimeToolProgressPoller(() => readReport(dir), progress);
54
+ let expired = false;
55
+ const timer = setTimeout(() => { expired = true; void stopGroup(child.pid); }, start ? 12 * 60_000 : 120_000);
56
+ let launchFailed = false;
57
+ try {
58
+ if (start) {
59
+ if (!protectProcessGroup || !child.pid)
60
+ throw new Error("lease_protection_unavailable");
61
+ await protectProcessGroup(child.pid);
62
+ }
63
+ await new Promise((resolve, reject) => child.send({ type: "runtime-tools:go" }, error => error ? reject(error) : resolve()));
64
+ }
65
+ catch {
66
+ launchFailed = true;
67
+ await stopGroup(child.pid);
68
+ }
69
+ const code = await closed;
70
+ clearTimeout(timer);
71
+ await polling.stop();
72
+ // A successful parent exit does not prove its package-manager descendants stopped.
73
+ const stopped = await stopGroup(child.pid);
74
+ return { stopped, success: stopped && !expired && !launchFailed && code === 0 };
75
+ }
76
+ export async function runRuntimeToolBridge(config, request, acquireEnv, progress, deps = {}) {
77
+ let dir;
78
+ let started = false;
79
+ let processStopped = true;
80
+ const failure = (errorCode) => ({ processStopped, data: request.action === "start" ? { operation: {
81
+ operationId: request.operationId, state: started ? "unknown" : "failed", currentIndex: 0, targets: request.targets,
82
+ items: request.targets.map(t => ({ runtime: t.installation.runtime, state: started ? "unknown" : "failed", errorCode })),
83
+ updatedAt: new Date().toISOString(), errorCode,
84
+ } } : {} });
85
+ try {
86
+ const response = await (deps.fetch ?? fetch)(`${config.serverUrl.replace(/\/+$/, "")}/machine/runtime-tool-updates/runner/${request.runner.releaseId}`, {
87
+ headers: { authorization: `Bearer ${config.machineToken}` }, redirect: "error", signal: AbortSignal.timeout(15_000),
88
+ });
89
+ if (!response.ok || !response.body)
90
+ return failure("runner_unavailable");
91
+ const chunks = [];
92
+ let bytes = 0;
93
+ for await (const chunk of response.body) {
94
+ bytes += chunk.length;
95
+ if (bytes > 512_000)
96
+ return failure("runner_unavailable");
97
+ chunks.push(chunk);
98
+ }
99
+ const content = Buffer.concat(chunks);
100
+ if (createHash("sha256").update(content).digest("hex") !== request.runner.sha256)
101
+ return failure("runner_unavailable");
102
+ const env = runtimeToolEnvironment(await acquireEnv());
103
+ dir = await mkdtemp(join(tmpdir(), "nowwork-runtime-update-"));
104
+ await writeFile(join(dir, "runner.mjs"), content, { mode: 0o600 });
105
+ await writeFile(join(dir, "request.json"), JSON.stringify(request), { mode: 0o600 });
106
+ started = true;
107
+ processStopped = false;
108
+ const outcome = await supervise(dir, env, request.action === "start", progress, deps.protectProcessGroup);
109
+ processStopped = outcome.stopped;
110
+ if (!outcome.success)
111
+ return failure("operation_unknown");
112
+ const data = await readReport(dir);
113
+ if (request.action === "start" && data.operation?.operationId !== request.operationId)
114
+ return failure("operation_unknown");
115
+ if (request.action === "inspect" && !data.snapshot)
116
+ return failure("inspection_failed");
117
+ return { data, processStopped };
118
+ }
119
+ catch {
120
+ return failure(started ? "operation_unknown" : "runner_unavailable");
121
+ }
122
+ finally {
123
+ if (dir && processStopped)
124
+ await rm(dir, { recursive: true, force: true });
125
+ }
126
+ }
@@ -0,0 +1,151 @@
1
+ import { RuntimeToolRequestSchema, RuntimeToolResponseSchema } from "./runtime-tool-update-protocol.js";
2
+ const finalStates = new Set(["completed", "failed", "unknown", "cancelled"]);
3
+ const phaseRank = (state) => ({ queued: 0, preflight: 1, installing: 2, verifying: 3, completed: 4, failed: 4, unknown: 4, cancelled: 4 })[state];
4
+ function advancesProgress(previous, next) {
5
+ if (finalStates.has(previous.state) || finalStates.has(next.state)
6
+ || next.operationId !== previous.operationId || JSON.stringify(next.targets) !== JSON.stringify(previous.targets)
7
+ || next.currentIndex < previous.currentIndex || next.currentIndex >= previous.targets.length
8
+ || (next.currentIndex === previous.currentIndex && phaseRank(next.state) < phaseRank(previous.state)))
9
+ return false;
10
+ return previous.items.every((item, index) => {
11
+ const candidate = next.items[index];
12
+ if (!candidate || candidate.runtime !== item.runtime)
13
+ return false;
14
+ return finalStates.has(item.state) ? candidate.state === item.state && candidate.errorCode === item.errorCode
15
+ : phaseRank(candidate.state) >= phaseRank(item.state);
16
+ });
17
+ }
18
+ export function createRuntimeToolUpdateController(deps) {
19
+ const entries = new Map();
20
+ let current = null;
21
+ let stopped = false;
22
+ let orphaned = false;
23
+ const now = deps.now ?? Date.now;
24
+ const date = () => new Date(now()).toISOString();
25
+ const wait = deps.wait ?? (() => new Promise(resolve => setTimeout(resolve, 500)));
26
+ const fail = (error) => ({ ok: false, error });
27
+ const result = (data) => ({ ok: true, data });
28
+ const set = (entry, patch) => { entry.operation = { ...entry.operation, ...patch, updatedAt: date() }; };
29
+ async function execute(entry) {
30
+ const deadline = now() + 10 * 60_000;
31
+ let acceptingProgress = false;
32
+ let lease = null;
33
+ try {
34
+ while (!stopped && entry.operation.state === "queued" && now() < deadline && !lease) {
35
+ lease = await deps.acquire();
36
+ if (!lease)
37
+ await wait();
38
+ }
39
+ if (entry.operation.state === "cancelled")
40
+ return;
41
+ if (!lease || stopped || now() >= deadline) {
42
+ set(entry, { state: "failed", errorCode: "runtime_busy" });
43
+ return;
44
+ }
45
+ set(entry, { state: "preflight" });
46
+ // Mark unsafe before spawning: an exception or lost response cannot authorize another writer.
47
+ entry.processStopped = false;
48
+ acceptingProgress = true;
49
+ const output = await deps.run(entry.request, raw => {
50
+ if (!acceptingProgress)
51
+ return;
52
+ const parsed = RuntimeToolResponseSchema.safeParse(raw);
53
+ if (parsed.success && parsed.data.operation && advancesProgress(entry.operation, parsed.data.operation))
54
+ entry.operation = parsed.data.operation;
55
+ }, lease.protectProcessGroup);
56
+ acceptingProgress = false;
57
+ const data = RuntimeToolResponseSchema.parse(output.data);
58
+ entry.processStopped = output.processStopped;
59
+ if (!data.operation || data.operation.operationId !== entry.operation.operationId
60
+ || JSON.stringify(data.operation.targets) !== JSON.stringify(entry.request.targets)
61
+ || data.operation.currentIndex < entry.operation.currentIndex || !finalStates.has(data.operation.state))
62
+ throw new Error("operation_mismatch");
63
+ entry.operation = data.operation;
64
+ if (!entry.processStopped)
65
+ set(entry, { state: "unknown", errorCode: "operation_unknown" });
66
+ }
67
+ catch {
68
+ acceptingProgress = false;
69
+ set(entry, { state: "unknown", errorCode: "operation_unknown" });
70
+ }
71
+ finally {
72
+ acceptingProgress = false;
73
+ // Retain the barrier if process-group termination could not be proved.
74
+ if (entry.processStopped) {
75
+ try {
76
+ await lease?.release();
77
+ }
78
+ catch {
79
+ entry.processStopped = false;
80
+ set(entry, { state: "unknown", errorCode: "operation_unknown" });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ async handle(raw) {
87
+ const parsed = RuntimeToolRequestSchema.safeParse(raw);
88
+ if (!parsed.success || stopped)
89
+ return fail("invalid_request");
90
+ const request = parsed.data;
91
+ if (request.action === "inspect") {
92
+ try {
93
+ const data = RuntimeToolResponseSchema.parse((await deps.run(request)).data);
94
+ return data.snapshot ? result(data) : fail("inspection_failed");
95
+ }
96
+ catch {
97
+ return fail("inspection_failed");
98
+ }
99
+ }
100
+ const entry = entries.get(request.operationId);
101
+ if (request.action === "start") {
102
+ if (entry)
103
+ return result({ operation: entry.operation });
104
+ if (orphaned || [...entries.values()].some(x => x.operation.state === "unknown"))
105
+ return fail("operation_unknown");
106
+ if (current)
107
+ return fail("runtime_busy");
108
+ if (entries.size >= 100)
109
+ return fail("operation_limit");
110
+ if (new Set(request.targets.map(t => t.installation.runtime)).size !== request.targets.length)
111
+ return fail("invalid_request");
112
+ const operation = { operationId: request.operationId, state: "queued", currentIndex: 0, targets: request.targets,
113
+ items: request.targets.map(t => ({ runtime: t.installation.runtime, state: "queued", errorCode: null })), updatedAt: date(), errorCode: null };
114
+ const next = { operation, request, processStopped: true };
115
+ entries.set(request.operationId, next);
116
+ current = execute(next).finally(() => { current = null; });
117
+ return result({ operation });
118
+ }
119
+ if (!entry) {
120
+ orphaned = true;
121
+ return fail("operation_unknown");
122
+ }
123
+ if (request.action === "status")
124
+ return result({ operation: entry.operation });
125
+ if (request.action === "cancel") {
126
+ if (entry.operation.state !== "queued")
127
+ return fail("already_started");
128
+ set(entry, { state: "cancelled", items: entry.operation.items.map(x => ({ ...x, state: "cancelled" })) });
129
+ return result({ operation: entry.operation });
130
+ }
131
+ if (!entry.processStopped || current)
132
+ return fail("operation_unknown");
133
+ try {
134
+ const output = await deps.run({ action: "inspect", runner: entry.request.runner });
135
+ const snapshot = RuntimeToolResponseSchema.parse(output.data).snapshot;
136
+ if (!snapshot || !output.processStopped)
137
+ return fail("operation_unknown");
138
+ const matches = entry.operation.targets.every(target => snapshot.tools.some(tool => tool.runtime === target.installation.runtime
139
+ && tool.version === target.targetVersion && tool.path === target.installation.path && tool.source === target.installation.source
140
+ && tool.prefix === target.installation.prefix && tool.brewPath === target.installation.brewPath && tool.automatic));
141
+ set(entry, { state: matches ? "completed" : "failed", errorCode: matches ? null : "verification_failed", snapshot,
142
+ items: entry.operation.items.map(x => ({ ...x, state: matches ? "completed" : "failed", errorCode: matches ? null : "verification_failed" })) });
143
+ return result({ operation: entry.operation, snapshot });
144
+ }
145
+ catch {
146
+ return fail("inspection_failed");
147
+ }
148
+ },
149
+ async stop() { stopped = true; await current; },
150
+ };
151
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ export const RUNTIME_TOOL_UPDATES_CAPABILITY = "runtime_tool_updates_v1";
3
+ const boundedPath = z.string().min(1).max(4096);
4
+ export const RuntimeToolRunnerSchema = z.object({ releaseId: z.string().regex(/^[a-f0-9]{64}$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
5
+ export const RuntimeToolInstallationSchema = z.object({
6
+ runtime: z.enum(["codex", "claude"]), path: boundedPath, realPath: boundedPath,
7
+ version: z.string().max(100).nullable(), source: z.enum(["npm", "homebrew", "native", "unknown"]),
8
+ nodeVersion: z.string().max(100).nullable(), nodePath: boundedPath.nullable(), npmPath: boundedPath.nullable(), prefix: boundedPath.nullable(),
9
+ brewPath: boundedPath.nullable(), brewKind: z.enum(["formula", "cask"]).nullable(), brewName: z.enum(["codex", "claude-code"]).nullable(),
10
+ fingerprint: z.string().regex(/^[a-f0-9]{64}$/), alternatives: z.array(boundedPath).max(32), automatic: z.boolean(), reason: z.string().max(100).nullable(),
11
+ }).strict();
12
+ export const RuntimeToolSnapshotSchema = z.object({ checkedAt: z.string().datetime(), tools: z.array(RuntimeToolInstallationSchema).max(2) }).strict();
13
+ export const RuntimeToolTargetSchema = z.object({ installation: RuntimeToolInstallationSchema, targetVersion: z.string().regex(/^\d+\.\d+\.\d+$/).max(100) }).strict();
14
+ export const RuntimeToolStateSchema = z.enum(["queued", "preflight", "installing", "verifying", "completed", "failed", "unknown", "cancelled"]);
15
+ export const RuntimeToolOperationSchema = z.object({ operationId: z.string().uuid(), state: RuntimeToolStateSchema, currentIndex: z.number().int().min(0).max(2),
16
+ targets: z.array(RuntimeToolTargetSchema).min(1).max(2), items: z.array(z.object({ runtime: z.enum(["codex", "claude"]), state: RuntimeToolStateSchema, errorCode: z.string().max(100).nullable() }).strict()).max(2),
17
+ updatedAt: z.string().datetime(), errorCode: z.string().max(100).nullable(), snapshot: RuntimeToolSnapshotSchema.optional(),
18
+ }).strict();
19
+ export const RuntimeToolRequestSchema = z.discriminatedUnion("action", [
20
+ z.object({ action: z.literal("inspect"), runner: RuntimeToolRunnerSchema }).strict(),
21
+ z.object({ action: z.literal("start"), operationId: z.string().uuid(), targets: z.array(RuntimeToolTargetSchema).min(1).max(2), runner: RuntimeToolRunnerSchema }).strict(),
22
+ ...["status", "cancel", "verify"].map(action => z.object({ action: z.literal(action), operationId: z.string().uuid() }).strict()),
23
+ ]);
24
+ export const RuntimeToolResponseSchema = z.object({ snapshot: RuntimeToolSnapshotSchema.optional(), operation: RuntimeToolOperationSchema.optional() }).strict();
@@ -0,0 +1,54 @@
1
+ import { prepareDaemonRestart } from "./daemon-updater.js";
2
+ import { createRuntimeToolUpdateController } from "./runtime-tool-update-controller.js";
3
+ import { augmentedPath } from "./runtime-path.js";
4
+ import { runRuntimeToolBridge } from "./runtime-tool-update-bridge.js";
5
+ export async function acquireRuntimeToolLease(localSlots, host) {
6
+ const local = await prepareDaemonRestart({ localSlots });
7
+ if (!local.ok)
8
+ return null;
9
+ try {
10
+ const shared = await host.tryAcquireExclusiveExecution();
11
+ if (!shared) {
12
+ await local.release();
13
+ return null;
14
+ }
15
+ let released = false;
16
+ return { protectProcessGroup: async (pid) => {
17
+ if (!shared.protectProcessGroup)
18
+ throw new Error("lease protection unavailable");
19
+ await shared.protectProcessGroup(pid);
20
+ }, async release() { if (released)
21
+ return; await shared.release(); await local.release(); released = true; } };
22
+ }
23
+ catch (error) {
24
+ await local.release();
25
+ throw error;
26
+ }
27
+ }
28
+ export function createServeRuntimeToolUpdates(config, localSlots, host, acquireEnv, refresh) {
29
+ const controller = createRuntimeToolUpdateController({ acquire: () => acquireRuntimeToolLease(localSlots, host),
30
+ run: async (request, progress, protectProcessGroup) => {
31
+ const result = await runRuntimeToolBridge(config, request, async () => {
32
+ const env = await acquireEnv();
33
+ return { ...env, PATH: augmentedPath(env, {}, process.platform, "append") };
34
+ }, progress, { ...(protectProcessGroup ? { protectProcessGroup } : {}) });
35
+ if (request.action === "start" && result.processStopped)
36
+ refresh();
37
+ return result;
38
+ } });
39
+ return {
40
+ handle(raw, send) {
41
+ if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "runtime-tools:operation")
42
+ return false;
43
+ const { reqId, type: _type, ...request } = raw;
44
+ if (typeof reqId === "string") {
45
+ if (process.platform !== "linux" && process.platform !== "darwin")
46
+ send({ type: "fs:result", reqId, ok: false, error: "unsupported_platform" });
47
+ else
48
+ void controller.handle(request).then(result => send({ type: "fs:result", reqId, ...result })).catch(() => send({ type: "fs:result", reqId, ok: false, error: "operation_unknown" }));
49
+ }
50
+ return true;
51
+ },
52
+ stop: controller.stop,
53
+ };
54
+ }
@@ -12,6 +12,14 @@ const InputSchema = z.object({ bin: z.string().min(1), args: z.array(z.string())
12
12
  wakePrompt: z.string().min(1), sessionId: z.string().min(1).optional(), resume: z.boolean() }).strict();
13
13
  class PreflightError extends Error {
14
14
  }
15
+ // Claude waits for every merged SessionStart hook before acknowledging SDK
16
+ // initialization. Those hooks may legitimately outlive the old 30s adapter
17
+ // deadline. Cap this phase below the daemon's default 120s fail-closed startup
18
+ // window so wrapper failure propagation and supervisor-owned tree cleanup retain
19
+ // margin. The 100s cap leaves a nominal 20s outside this phase; this runner's
20
+ // local TERM/KILL cleanup is itself bounded to 2s.
21
+ // Resume and cold recovery share this one budget; hook activity never extends it.
22
+ export const CLAUDE_INITIALIZE_TIMEOUT_MS = 100_000;
15
23
  function hookCommand(path, nonce) {
16
24
  const source = import.meta.url.endsWith(".ts");
17
25
  const hook = fileURLToPath(new URL(`./claude-session-start-hook.${source ? "ts" : "js"}`, import.meta.url));
@@ -52,6 +60,9 @@ async function bounded(promise, milliseconds, label) {
52
60
  clearTimeout(timer);
53
61
  }
54
62
  }
63
+ export function waitForClaudeInitialize(initialized, timeoutMs = CLAUDE_INITIALIZE_TIMEOUT_MS) {
64
+ return bounded(initialized, timeoutMs, "claude_initialize_timeout");
65
+ }
55
66
  async function attempt(input, sessionId, resume, options) {
56
67
  const proofDir = await mkdtemp(join(tmpdir(), "nowwork-claude-session-"));
57
68
  const proofPath = join(proofDir, "proof.json");
@@ -188,7 +199,7 @@ async function attempt(input, sessionId, resume, options) {
188
199
  try {
189
200
  try {
190
201
  await send({ type: "control_request", request_id: initId, request: { subtype: "initialize" } });
191
- await bounded(initialized.promise, options.initializeTimeoutMs ?? 30_000, "claude_initialize_timeout");
202
+ await waitForClaudeInitialize(initialized.promise, options.initializeTimeoutMs);
192
203
  const proof = JSON.parse(await readFile(proofPath, "utf8"));
193
204
  if (proof.nonce !== nonce || proof.sessionId !== sessionId || proof.source !== (resume ? "resume" : "startup")) {
194
205
  throw new Error("claude_session_proof_mismatch");
@@ -238,15 +249,25 @@ async function attempt(input, sessionId, resume, options) {
238
249
  }
239
250
  export async function runClaudeThread(input, options = {}) {
240
251
  const sessionId = input.sessionId ?? randomUUID();
252
+ const initializeBudgetMs = options.initializeTimeoutMs ?? CLAUDE_INITIALIZE_TIMEOUT_MS;
253
+ const now = options.now ?? (() => performance.now());
254
+ const initializeDeadline = now() + Math.max(0, initializeBudgetMs);
255
+ const runAttempt = (attemptSessionId, resume) => {
256
+ const remainingMs = Math.max(0, Math.ceil(initializeDeadline - now()));
257
+ if (remainingMs === 0)
258
+ return Promise.reject(new PreflightError("claude_native_session_unconfirmed"));
259
+ return attempt(input, attemptSessionId, resume, { ...options, initializeTimeoutMs: remainingMs });
260
+ };
241
261
  try {
242
- return await attempt(input, sessionId, input.resume, options);
262
+ return await runAttempt(sessionId, input.resume);
243
263
  }
244
264
  catch (error) {
245
265
  // The failed attempt never received a business user turn or entered its
246
- // model/tool loop. Native startup hooks may run again on cold recovery.
266
+ // model/tool loop. Native startup hooks may run again on cold recovery,
267
+ // within the original aggregate initialize deadline.
247
268
  if (!input.resume || !(error instanceof PreflightError || error instanceof ThreadDeliveryColdStartRequired))
248
269
  throw error;
249
- return attempt(input, randomUUID(), false, options);
270
+ return runAttempt(randomUUID(), false);
250
271
  }
251
272
  }
252
273
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
package/dist/serve.js CHANGED
@@ -14,6 +14,7 @@ import { reconcileExecutionJournal } from "./execution-recovery.js";
14
14
  import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
15
15
  import { handleWorkspaceControlRequest } from "./workspace-control-handler.js";
16
16
  import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
17
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
17
18
  import { normalizeScheduledContext } from "./scheduled-report.js";
18
19
  import { formatDaemonLogLine } from "./log-format.js";
19
20
  import { reportAgentRunComplete } from "./scheduled-run-report.js";
@@ -48,8 +49,11 @@ import { initializedProjectSkillsReconciler } from "./project-skills/initialized
48
49
  import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
49
50
  import { probeRuntimeHealth } from "./runtime-health.js";
50
51
  import { createServeMarketplace } from "./marketplace-serve.js";
52
+ import { createServeRuntimeToolUpdates } from "./runtime-tool-update-serve.js";
51
53
  import { buildControlPlaneUrl } from "./control-plane-url.js";
52
54
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
55
+ import { createMemoryMigrationController } from "./memory-migration-controller.js";
56
+ import { createMemoryMigrationJournal, replayExecutionJournals } from "./memory-migration-journal.js";
53
57
  import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
54
58
  export { buildControlPlaneUrl } from "./control-plane-url.js";
55
59
  // normalize.ts 的活动种类 → activity 枚举
@@ -75,6 +79,8 @@ export function serve(config, opts = {}) {
75
79
  let reconnectTimer = null;
76
80
  let stopPromise = null;
77
81
  let executionJournal;
82
+ let migrationExecutionJournal;
83
+ let migrationJournalReady = false;
78
84
  const injectedExecutionJournal = opts.execution?.journal;
79
85
  const agentMemory = opts.execution?.dependencies?.agentMemory
80
86
  ?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
@@ -89,6 +95,15 @@ export function serve(config, opts = {}) {
89
95
  startupGapMs: config.executionLimits.startupGapMs,
90
96
  });
91
97
  const localSlots = createSharedSlotManager(config.executionLimits);
98
+ const memoryMigrations = createMemoryMigrationController({ config, coordinator: hostCoordinator,
99
+ isAdmissionClosed: () => stopped || storageIdentityAdmission.isAdmissionClosed(),
100
+ journal: () => { if (!migrationJournalReady)
101
+ throw new Error("memory_migration_journal_unavailable"); return migrationExecutionJournal; }, send: frame => {
102
+ if (frame.type === "execution:completed")
103
+ completionRetransmitter.track(frame);
104
+ else if (ws?.readyState === WebSocket.OPEN)
105
+ ws.send(JSON.stringify(frame));
106
+ } });
92
107
  const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
93
108
  const concurrencyTelemetry = createExecutionConcurrencyTelemetry(sharedSlots, {
94
109
  ...(opts.execution?.concurrencySnapshotIntervalMs === undefined
@@ -210,6 +225,7 @@ export function serve(config, opts = {}) {
210
225
  onError: (code, context) => dslog("execution.runtime_detection_failed", "机器环境或 runtime 探测失败", { level: "ERROR", error_code: code, ...context }),
211
226
  });
212
227
  machineRuntime.prewarm();
228
+ const runtimeToolUpdates = createServeRuntimeToolUpdates(config, localSlots, hostCoordinator, async () => ({ ...(await machineRuntime.provider.acquire()).env }), () => machineRuntime.refresh());
213
229
  const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
214
230
  const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
215
231
  const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
@@ -270,7 +286,7 @@ export function serve(config, opts = {}) {
270
286
  return null;
271
287
  };
272
288
  const sendSnapshot = async (reqId) => {
273
- const entries = (await executionJournal.replay()).map(snapshotEntry).filter((entry) => entry !== null);
289
+ const entries = (await replayExecutionJournals(executionJournal, migrationExecutionJournal)).map(snapshotEntry).filter((entry) => entry !== null);
274
290
  safeExecutionSend(ExecutionSnapshotSchema.parse({
275
291
  type: "execution:snapshot", protocolVersion: 1, reqId, entries,
276
292
  }));
@@ -352,6 +368,12 @@ export function serve(config, opts = {}) {
352
368
  machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
353
369
  opts.onOpen?.(ws);
354
370
  void sendSnapshot(`reconnect-${randomUUID()}`);
371
+ // 普通回执不等待维护日志IO;两类记录分别重放到同一发送器。
372
+ void migrationExecutionJournal.replay().then(entries => {
373
+ for (const entry of entries)
374
+ if (entry.completion && !entry.completionAcknowledged)
375
+ completionRetransmitter.track(entry.completion);
376
+ }).catch(() => dslog("memory_migration.journal_unavailable", "维护回执重放失败,普通回执继续", { level: "ERROR" }));
355
377
  void executionJournal.replay()
356
378
  .then((entries) => {
357
379
  for (const entry of entries) {
@@ -431,7 +453,8 @@ export function serve(config, opts = {}) {
431
453
  return;
432
454
  if (frame.type === "execution:completion-ack") {
433
455
  try {
434
- await executionJournal.acknowledgeCompletion(frame.executionId);
456
+ const journal = await executionJournal.get(frame.executionId) ? executionJournal : migrationExecutionJournal;
457
+ await journal.acknowledgeCompletion(frame.executionId);
435
458
  const attempts = completionRetransmitter.acknowledge(frame.executionId);
436
459
  dslog("execution.completion_acknowledged", "execution completion ACK 已持久化", {
437
460
  execution_id: frame.executionId,
@@ -457,6 +480,8 @@ export function serve(config, opts = {}) {
457
480
  return;
458
481
  }
459
482
  if (frame.type === "execution:cancel") {
483
+ if (memoryMigrations.cancel(frame.executionId))
484
+ return;
460
485
  if (!knownExecutionHashes.has(frame.executionId)
461
486
  && !executionRuns.has(frame.executionId)
462
487
  && await executionJournal.get(frame.executionId) === null)
@@ -689,6 +714,7 @@ export function serve(config, opts = {}) {
689
714
  const execution = executeProtocol(config, spec, {
690
715
  ...opts.execution?.dependencies,
691
716
  machineEnvironment: machineRuntime.provider,
717
+ ...(memoryMigrations.getIdentity() === null ? {} : { controlIdentity: memoryMigrations.getIdentity() }),
692
718
  capabilities: capabilities.executionRunner,
693
719
  projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
694
720
  abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
@@ -779,6 +805,8 @@ export function serve(config, opts = {}) {
779
805
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
780
806
  const r = msg;
781
807
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
808
+ if (r.workspaceId && r.machineId)
809
+ memoryMigrations.setIdentity({ workspaceId: r.workspaceId, machineId: r.machineId });
782
810
  // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。ackSupported() 依赖它判断是否重试,
783
811
  // 必须在任何 republish() 之前就位;否则本轮 republish 看到空集合→不排重试→整套 ack/重连机制哑火。
784
812
  serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
@@ -842,11 +870,19 @@ export function serve(config, opts = {}) {
842
870
  }
843
871
  return;
844
872
  }
873
+ const runtimeUpdateSocket = ws;
874
+ if (runtimeToolUpdates.handle(msg, frame => { try {
875
+ runtimeUpdateSocket?.send(JSON.stringify(frame));
876
+ }
877
+ catch { /* Server reconciles via status. */ } }))
878
+ return;
845
879
  if (marketplace.handle(msg, frame => { try {
846
880
  ws?.send(JSON.stringify(frame));
847
881
  }
848
882
  catch { /* server 会按请求超时处理 */ } }))
849
883
  return;
884
+ if (await memoryMigrations.handle(msg))
885
+ return;
850
886
  if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
851
887
  return;
852
888
  // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
@@ -861,7 +897,7 @@ export function serve(config, opts = {}) {
861
897
  try {
862
898
  const data2 = req.type === "raft:inspect"
863
899
  ? await inspectRaftWorkspace(req.path)
864
- : await importRaftWorkspace(req.path, join(config.agentsRoot, req.handle));
900
+ : await withMemoryAccess(join(config.agentsRoot, req.handle), () => importRaftWorkspace(req.path, join(config.agentsRoot, req.handle)), { create: true });
865
901
  if (req.type === "raft:import") {
866
902
  log(`📦 已导入 raft 工作区 → ${req.handle}: 复制 ${data2.copied.length} 项`);
867
903
  }
@@ -1254,6 +1290,7 @@ export function serve(config, opts = {}) {
1254
1290
  return;
1255
1291
  // server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
1256
1292
  serverCapabilities = new Set();
1293
+ memoryMigrations.clearIdentity();
1257
1294
  completionRetransmitter.pause();
1258
1295
  // 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
1259
1296
  // error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
@@ -1306,6 +1343,9 @@ export function serve(config, opts = {}) {
1306
1343
  writeStderr: (line) => process.stderr.write(line),
1307
1344
  });
1308
1345
  await runProjectSkillStartupCleanup(config.agentsRoot, executionJournal, dslog);
1346
+ migrationExecutionJournal = createMemoryMigrationJournal(config.agentsRoot);
1347
+ await migrationExecutionJournal.reconcileAfterRestart().then(() => { migrationJournalReady = true; })
1348
+ .catch(() => dslog("memory_migration.journal_unavailable", "迁移日志恢复失败,普通执行继续,迁移请求单独报错", { level: "ERROR" }));
1309
1349
  await connect();
1310
1350
  })();
1311
1351
  const stop = () => {
@@ -1323,9 +1363,11 @@ export function serve(config, opts = {}) {
1323
1363
  const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
1324
1364
  const pending = [
1325
1365
  machineRuntime.stop(),
1366
+ memoryMigrations.stop(),
1326
1367
  webSocketClosed,
1327
1368
  updateController.drain(),
1328
1369
  marketplace.stop(),
1370
+ runtimeToolUpdates.stop(),
1329
1371
  executionFrameQueue,
1330
1372
  ...executionRuns.values(),
1331
1373
  ...[...legacyRuns.values()].map((run) => run.done),
@@ -1341,6 +1383,8 @@ export function serve(config, opts = {}) {
1341
1383
  if (executionJournal !== undefined) {
1342
1384
  await executionJournal.close({ signal: deadline.signal });
1343
1385
  }
1386
+ if (migrationExecutionJournal !== undefined)
1387
+ await migrationExecutionJournal.close({ signal: deadline.signal });
1344
1388
  }
1345
1389
  finally {
1346
1390
  deadline.dispose();
package/dist/token.js CHANGED
@@ -7,6 +7,7 @@ export async function mintAgentToken(serverUrl, machineToken, handle, displayNam
7
7
  },
8
8
  // 把本轮唤醒的线程根钉到 per-launch 令牌:服务端据此让 task/消息默认绑/回当前线程。
9
9
  body: JSON.stringify({
10
+ memoryStateGuardVersion: 1,
10
11
  handle,
11
12
  ...(displayName ? { displayName } : {}),
12
13
  ...(options.wakeThreadRoot ? { wakeThreadRoot: options.wakeThreadRoot } : {}),
@@ -1,9 +1,22 @@
1
1
  /** 控制面文件/技能查看的按需处理,独立于执行生命周期。 */
2
- import { join } from "node:path";
2
+ import { lstat } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
3
4
  import { readSharedMessageFile } from "./message-file-preview.js";
4
5
  import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
5
6
  import { listSkills } from "./skills.js";
6
7
  import { listRuntimeModels } from "./list-models.js";
8
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
9
+ async function listProtectedWorkspace(root, path) {
10
+ try {
11
+ await lstat(root);
12
+ }
13
+ catch (error) {
14
+ if (error.code !== "ENOENT")
15
+ throw error;
16
+ return { root: resolve(root), path: path.replace(/^\/+/, ""), entries: [] };
17
+ }
18
+ return withMemoryAccess(root, () => listWorkspace(root, path));
19
+ }
7
20
  export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, models = listRuntimeModels) {
8
21
  if (!raw || typeof raw !== "object")
9
22
  return null;
@@ -15,8 +28,8 @@ export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, m
15
28
  try {
16
29
  const root = join(agentsRoot, request.handle);
17
30
  const data = request.type === "fs:read-shared" ? await readSharedMessageFile(agentsRoot, raw, registry)
18
- : request.type === "fs:list" ? await listWorkspace(root, request.path)
19
- : request.type === "fs:read" ? await readWorkspaceFile(root, request.path)
31
+ : request.type === "fs:list" ? await listProtectedWorkspace(root, request.path)
32
+ : request.type === "fs:read" ? await withMemoryAccess(root, () => readWorkspaceFile(root, request.path))
20
33
  : request.type === "skills:list" ? await listSkills(request.runtime)
21
34
  : { models: await models(request.runtime ?? "") };
22
35
  return { type: "fs:result", reqId: request.reqId, ok: true, data };