@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.
- package/dist/cloud-report-upload.js +1 -10
- package/dist/execution-journal-lock.js +51 -2
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +4 -1
- package/dist/host-execution-coordinator.js +10 -0
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +78 -55
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +117 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/initialization-identity.js +36 -0
- package/dist/local-memory/inventory-cli.js +52 -8
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/inventory-v2.js +362 -0
- package/dist/local-memory/legacy-adapter.js +233 -0
- package/dist/local-memory/migration-access.js +242 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +432 -0
- package/dist/local-memory/migration-schema.js +58 -0
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +414 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/pending-query.js +5 -2
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +16 -2
- package/dist/local-memory/prompt.js +21 -5
- package/dist/local-memory/read-cli.js +98 -76
- package/dist/local-memory/release-artifact-guard.js +55 -0
- package/dist/local-memory/rollout.js +79 -11
- package/dist/local-memory/runtime.js +191 -17
- package/dist/local-memory/source-path.js +13 -0
- package/dist/local-memory/storage-format.js +249 -0
- package/dist/local-memory/write.js +190 -21
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +96 -15
- package/dist/local-memory-telemetry.js +31 -3
- package/dist/machine-info.js +6 -1
- package/dist/machine-runtime-environment.js +1 -0
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/memory-migration-controller.js +89 -0
- package/dist/memory-migration-inspect.js +116 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +367 -0
- package/dist/memory-prune-diagnostics.js +74 -4
- package/dist/message-file-preview.js +11 -1
- package/dist/prompt.js +9 -17
- package/dist/runner.js +1 -1
- package/dist/runtime-tool-update-bridge.js +126 -0
- package/dist/runtime-tool-update-controller.js +151 -0
- package/dist/runtime-tool-update-protocol.js +24 -0
- package/dist/runtime-tool-update-serve.js +54 -0
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +47 -3
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- package/dist/workspace.js +23 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
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 };
|