@nowcrew/daemon 0.6.88-beta.15 → 0.6.88-beta.17
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 +15 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/local-executor.js +4 -2
- package/dist/machine-info.js +48 -15
- package/dist/main.js +4 -69
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/provider-env.js +3 -4
- package/dist/runtime-probe.js +34 -5
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +224 -0
- package/dist/runtimes/deepseek-harness.js +61 -10
- package/dist/runtimes/kimi-acp-runner.js +37 -6
- package/dist/serve.js +36 -68
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- package/package.json +1 -1
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- 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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/** 新版 Harness 的短命 ACP 桥:产品状态与重试仍属于 server。 */
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { Readable, Writable } from "node:stream";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { isAbsolute } from "node:path";
|
|
7
|
+
import spawn from "cross-spawn";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { client, methods, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
10
|
+
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
11
|
+
import { mapAcpUpdate } from "./kimi-acp-runner.js";
|
|
12
|
+
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
13
|
+
const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
|
|
14
|
+
export const HarnessInputSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
systemPrompt: z.string(),
|
|
17
|
+
wakePrompt: z.string().min(1),
|
|
18
|
+
stateRoot: absolutePath,
|
|
19
|
+
skillRoots: z.array(absolutePath).default([]),
|
|
20
|
+
effectivePermission: z.enum([
|
|
21
|
+
"sandboxed",
|
|
22
|
+
"workspace_write",
|
|
23
|
+
"full_access",
|
|
24
|
+
]),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
const emit = async (event) => {
|
|
28
|
+
if (!process.stdout.write(`${JSON.stringify(event)}\n`))
|
|
29
|
+
await once(process.stdout, "drain");
|
|
30
|
+
};
|
|
31
|
+
async function settleWithin(promise, ms) {
|
|
32
|
+
let timer;
|
|
33
|
+
try {
|
|
34
|
+
return await Promise.race([
|
|
35
|
+
promise,
|
|
36
|
+
new Promise((resolve) => {
|
|
37
|
+
timer = setTimeout(() => resolve(undefined), ms);
|
|
38
|
+
}),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (timer)
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function runHarness(input, bin, patchPath) {
|
|
47
|
+
const delivery = await prepareThreadDelivery({
|
|
48
|
+
env: process.env,
|
|
49
|
+
resumed: false,
|
|
50
|
+
sessionId: "cold-harness",
|
|
51
|
+
wakePrompt: input.wakePrompt,
|
|
52
|
+
});
|
|
53
|
+
const profile = await prepareHarnessProfile({
|
|
54
|
+
...input,
|
|
55
|
+
cwd: process.cwd(),
|
|
56
|
+
env: process.env,
|
|
57
|
+
...(patchPath ? { patchPath } : {}),
|
|
58
|
+
});
|
|
59
|
+
const child = spawn(bin, profile.args, {
|
|
60
|
+
cwd: profile.home,
|
|
61
|
+
env: profile.env,
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
// Boot away from the repository: the CLI must not load a project's .env into
|
|
65
|
+
// trusted process credentials. ACP session/new carries the real workspace.
|
|
66
|
+
let spawnError;
|
|
67
|
+
const closed = new Promise((resolve) => {
|
|
68
|
+
child.once("error", (error) => {
|
|
69
|
+
spawnError = error;
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
child.once("close", () => resolve());
|
|
73
|
+
});
|
|
74
|
+
if (!child.stdin || !child.stdout || !child.stderr)
|
|
75
|
+
throw new Error("DeepSeek Harness stdio unavailable");
|
|
76
|
+
// Provider diagnostics may include secrets or prompts; do not stream raw stderr.
|
|
77
|
+
child.stderr.resume();
|
|
78
|
+
let context;
|
|
79
|
+
let sessionId;
|
|
80
|
+
let cancelled = false;
|
|
81
|
+
let stage = "initialize";
|
|
82
|
+
let exitCode = 1;
|
|
83
|
+
let stopping;
|
|
84
|
+
const stop = () => (stopping ??= (async () => {
|
|
85
|
+
child.stdin?.end();
|
|
86
|
+
if (await settleWithin(closed.then(() => true), 2000))
|
|
87
|
+
return;
|
|
88
|
+
child.kill("SIGTERM");
|
|
89
|
+
if (await settleWithin(closed.then(() => true), 1000))
|
|
90
|
+
return;
|
|
91
|
+
child.kill("SIGKILL");
|
|
92
|
+
if (!(await settleWithin(closed.then(() => true), 2000)))
|
|
93
|
+
throw new Error("DeepSeek Harness process stop unconfirmed");
|
|
94
|
+
})());
|
|
95
|
+
const cancel = () => {
|
|
96
|
+
cancelled = true;
|
|
97
|
+
void (async () => {
|
|
98
|
+
if (context && sessionId)
|
|
99
|
+
await settleWithin(context
|
|
100
|
+
.notify(methods.agent.session.cancel, { sessionId })
|
|
101
|
+
.catch(() => undefined), 1000);
|
|
102
|
+
await stop();
|
|
103
|
+
})().catch(() => {
|
|
104
|
+
/* finally reports stop failure; supervisor owns process tree */
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
process.once("SIGTERM", cancel);
|
|
108
|
+
process.once("SIGINT", cancel);
|
|
109
|
+
const app = client({ name: "nowwork-harness" })
|
|
110
|
+
.onRequest(methods.client.session.requestPermission, () => ({
|
|
111
|
+
outcome: { outcome: "cancelled" },
|
|
112
|
+
}))
|
|
113
|
+
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
114
|
+
if (params.sessionId !== sessionId)
|
|
115
|
+
return;
|
|
116
|
+
for (const event of mapAcpUpdate("deepseek-harness", params.update))
|
|
117
|
+
await emit(event);
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
121
|
+
const result = await app.connectWith(stream, async (ctx) => {
|
|
122
|
+
context = ctx;
|
|
123
|
+
const initialized = await ctx.request(methods.agent.initialize, {
|
|
124
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
125
|
+
clientCapabilities: {},
|
|
126
|
+
clientInfo: { name: "nowwork", version: "1" },
|
|
127
|
+
});
|
|
128
|
+
if (initialized.protocolVersion !== PROTOCOL_VERSION)
|
|
129
|
+
throw new Error("Incompatible ACP protocol");
|
|
130
|
+
if (cancelled)
|
|
131
|
+
throw new Error("cancelled");
|
|
132
|
+
stage = "session_new";
|
|
133
|
+
const session = await ctx.request(methods.agent.session.new, {
|
|
134
|
+
cwd: process.cwd(),
|
|
135
|
+
mcpServers: [],
|
|
136
|
+
});
|
|
137
|
+
sessionId = session.sessionId;
|
|
138
|
+
if (!sessionId || cancelled)
|
|
139
|
+
throw new Error("Harness session unavailable");
|
|
140
|
+
await emit({ type: "thread.started", thread_id: sessionId });
|
|
141
|
+
stage = "prompt";
|
|
142
|
+
const response = await ctx.request(methods.agent.session.prompt, {
|
|
143
|
+
sessionId,
|
|
144
|
+
prompt: [{ type: "text", text: delivery.prompt }],
|
|
145
|
+
});
|
|
146
|
+
if (initialized.agentCapabilities?.sessionCapabilities?.close != null) {
|
|
147
|
+
stage = "session_close";
|
|
148
|
+
const close = await settleWithin(ctx
|
|
149
|
+
.request(methods.agent.session.close, { sessionId })
|
|
150
|
+
.then(() => true), 5000);
|
|
151
|
+
if (!close)
|
|
152
|
+
throw new Error("Harness session close timed out");
|
|
153
|
+
}
|
|
154
|
+
return response;
|
|
155
|
+
});
|
|
156
|
+
const usage = result.usage;
|
|
157
|
+
if (!cancelled)
|
|
158
|
+
await emit({
|
|
159
|
+
type: "turn.completed",
|
|
160
|
+
stop_reason: result.stopReason,
|
|
161
|
+
...(usage
|
|
162
|
+
? {
|
|
163
|
+
usage: {
|
|
164
|
+
input_tokens: usage.inputTokens,
|
|
165
|
+
output_tokens: usage.outputTokens,
|
|
166
|
+
cache_read_input_tokens: usage.cachedReadTokens ?? 0,
|
|
167
|
+
cache_creation_input_tokens: usage.cachedWriteTokens ?? 0,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: {}),
|
|
171
|
+
});
|
|
172
|
+
exitCode =
|
|
173
|
+
result.stopReason === "end_turn"
|
|
174
|
+
? 0
|
|
175
|
+
: result.stopReason === "cancelled"
|
|
176
|
+
? 130
|
|
177
|
+
: 1;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
if (!cancelled)
|
|
181
|
+
process.stderr.write(spawnError
|
|
182
|
+
? "DeepSeek Harness launch failed; verify the installed dsh executable.\n"
|
|
183
|
+
: `DeepSeek Harness ACP execution failed at ${stage}; verify the pinned version, machine credentials and profile patch.\n`);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
try {
|
|
187
|
+
await stop();
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
process.off("SIGTERM", cancel);
|
|
191
|
+
process.off("SIGINT", cancel);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (cancelled)
|
|
195
|
+
return 130;
|
|
196
|
+
return child.exitCode !== 0 ? 1 : exitCode;
|
|
197
|
+
}
|
|
198
|
+
async function main() {
|
|
199
|
+
const { values } = parseArgs({
|
|
200
|
+
options: { bin: { type: "string" }, patch: { type: "string" } },
|
|
201
|
+
});
|
|
202
|
+
if (!values.bin)
|
|
203
|
+
throw new Error("DeepSeek Harness --bin is required");
|
|
204
|
+
process.stdin.setEncoding("utf8");
|
|
205
|
+
let text = "";
|
|
206
|
+
for await (const chunk of process.stdin) {
|
|
207
|
+
text += String(chunk);
|
|
208
|
+
if (Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
209
|
+
throw new Error("Harness input exceeds 16 MiB");
|
|
210
|
+
}
|
|
211
|
+
const input = HarnessInputSchema.parse(JSON.parse(text));
|
|
212
|
+
return runHarness(input, values.bin, values.patch);
|
|
213
|
+
}
|
|
214
|
+
if (process.argv[1] &&
|
|
215
|
+
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
216
|
+
main()
|
|
217
|
+
.then((code) => {
|
|
218
|
+
process.exitCode = code;
|
|
219
|
+
})
|
|
220
|
+
.catch(() => {
|
|
221
|
+
process.stderr.write("DeepSeek Harness configuration or cleanup failed; check execution paths and machine patch.\n");
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
3
7
|
import { probeKimiAcp } from "./kimi-acp-runner.js";
|
|
4
8
|
export const DEEPSEEK_HARNESS_CONFIG_ENV = "NOWCREW_DEEPSEEK_HARNESS_CONFIG";
|
|
9
|
+
export const DEEPSEEK_HARNESS_PATCH_ENV = "NOWCREW_DEEPSEEK_HARNESS_PATCH";
|
|
10
|
+
export function deepSeekHarnessLauncher(env = process.env) {
|
|
11
|
+
const configPath = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
|
|
12
|
+
const patchPath = env[DEEPSEEK_HARNESS_PATCH_ENV]?.trim();
|
|
13
|
+
if (configPath && !isAbsolute(configPath))
|
|
14
|
+
throw new Error(`${DEEPSEEK_HARNESS_CONFIG_ENV} must be an absolute path`);
|
|
15
|
+
if (patchPath && !isAbsolute(patchPath))
|
|
16
|
+
throw new Error(`${DEEPSEEK_HARNESS_PATCH_ENV} must be an absolute path`);
|
|
17
|
+
if (configPath && patchPath)
|
|
18
|
+
throw new Error("DeepSeek Harness legacy config and profile patch are mutually exclusive");
|
|
19
|
+
return configPath
|
|
20
|
+
? { kind: "legacy", bin: "dsh-acp-demo", configPath }
|
|
21
|
+
: { kind: "profile", bin: "dsh", ...(patchPath ? { patchPath } : {}) };
|
|
22
|
+
}
|
|
5
23
|
export function deepSeekHarnessConfigPath(env = process.env) {
|
|
6
24
|
const path = env[DEEPSEEK_HARNESS_CONFIG_ENV]?.trim();
|
|
7
25
|
return path && isAbsolute(path) ? path : null;
|
|
8
26
|
}
|
|
9
27
|
/** The published DeepSeek Harness ACP executable speaks JSON-RPC directly on stdio. */
|
|
10
|
-
export function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
export async function probeDeepSeekHarnessAcp(options, spawnProcess = spawn) {
|
|
29
|
+
if (options.signal?.aborted)
|
|
30
|
+
return false;
|
|
31
|
+
let root;
|
|
32
|
+
try {
|
|
33
|
+
const launcher = options.configPath === undefined
|
|
34
|
+
? deepSeekHarnessLauncher()
|
|
35
|
+
: deepSeekHarnessLauncher({
|
|
36
|
+
[DEEPSEEK_HARNESS_CONFIG_ENV]: options.configPath,
|
|
37
|
+
});
|
|
38
|
+
if (launcher.kind === "legacy")
|
|
39
|
+
return await probeKimiAcp({
|
|
40
|
+
...options,
|
|
41
|
+
provider: "deepseek-harness",
|
|
42
|
+
configPath: launcher.configPath,
|
|
43
|
+
effectivePermission: "sandboxed",
|
|
44
|
+
}, spawnProcess);
|
|
45
|
+
root = await mkdtemp(join(tmpdir(), "nowwork-dsh-probe-"));
|
|
46
|
+
const profile = await prepareHarnessProfile({
|
|
47
|
+
stateRoot: root,
|
|
48
|
+
cwd: root,
|
|
49
|
+
systemPrompt: "",
|
|
50
|
+
skillRoots: [],
|
|
51
|
+
effectivePermission: "sandboxed",
|
|
52
|
+
env: process.env,
|
|
53
|
+
...(launcher.patchPath ? { patchPath: launcher.patchPath } : {}),
|
|
54
|
+
});
|
|
55
|
+
return await probeKimiAcp({
|
|
56
|
+
...options,
|
|
57
|
+
provider: "deepseek-harness",
|
|
58
|
+
effectivePermission: "sandboxed",
|
|
59
|
+
launchArgs: profile.args,
|
|
60
|
+
launchEnv: profile.env,
|
|
61
|
+
launchCwd: root,
|
|
62
|
+
}, spawnProcess);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
if (root)
|
|
69
|
+
await rm(root, { recursive: true, force: true }).catch(() => undefined);
|
|
70
|
+
}
|
|
20
71
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { dslog } from "../slog.js";
|
|
1
3
|
import { once } from "node:events";
|
|
2
4
|
import { isAbsolute } from "node:path";
|
|
3
5
|
import { parseArgs } from "node:util";
|
|
@@ -198,14 +200,22 @@ async function stopChild(child) {
|
|
|
198
200
|
export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
199
201
|
const provider = options.provider ?? "kimi";
|
|
200
202
|
const permission = options.effectivePermission ?? "sandboxed";
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
+
const probeId = options.probeId ?? randomUUID();
|
|
204
|
+
const started = Date.now();
|
|
205
|
+
let reason = "initializing", protocolVersion;
|
|
206
|
+
let stderrSeen = false;
|
|
207
|
+
const fields = () => ({ probe_id: probeId, runtime: provider, duration_ms: Date.now() - started,
|
|
208
|
+
reason, protocol_version: protocolVersion, stderr_seen: stderrSeen, aborted: options.signal?.aborted ?? false });
|
|
209
|
+
dslog("execution.acp_probe_started", "ACP 探测开始", { probe_id: probeId, runtime: provider, node_version: process.version });
|
|
210
|
+
const child = spawnProcess(options.bin, options.launchArgs ?? providerArgs(provider, options.configPath), {
|
|
211
|
+
cwd: options.launchCwd ?? process.cwd(),
|
|
203
212
|
// probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
|
|
204
|
-
env: { ...providerEnv(provider, permission), PATH: augmentedPath() },
|
|
213
|
+
env: { ...(options.launchEnv ?? providerEnv(provider, permission)), PATH: augmentedPath() },
|
|
205
214
|
stdio: ["pipe", "pipe", "pipe"],
|
|
206
215
|
});
|
|
207
216
|
if (child.stdin === null || child.stdout === null || child.stderr === null)
|
|
208
217
|
return false;
|
|
218
|
+
child.stderr.on("data", () => { stderrSeen = true; });
|
|
209
219
|
child.stderr.resume();
|
|
210
220
|
const app = client({ name: `nowcrew-daemon-${provider}-probe` })
|
|
211
221
|
.onRequest(methods.client.session.requestPermission, () => ({
|
|
@@ -217,23 +227,32 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
217
227
|
try {
|
|
218
228
|
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
219
229
|
const connected = app.connectWith(stream, async (context) => {
|
|
220
|
-
await context.request(methods.agent.initialize, {
|
|
230
|
+
const initialized = await context.request(methods.agent.initialize, {
|
|
221
231
|
protocolVersion: PROTOCOL_VERSION,
|
|
222
232
|
clientCapabilities: {},
|
|
223
233
|
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
224
234
|
});
|
|
225
|
-
|
|
235
|
+
protocolVersion = initialized.protocolVersion;
|
|
236
|
+
const ready = protocolVersion === PROTOCOL_VERSION;
|
|
237
|
+
if (reason === "initializing")
|
|
238
|
+
reason = ready ? "ready" : "protocol_mismatch";
|
|
239
|
+
dslog("execution.acp_probe_handshake", "ACP 握手结果", { ...fields(), ready });
|
|
240
|
+
return ready;
|
|
226
241
|
});
|
|
227
242
|
const result = await Promise.race([
|
|
228
243
|
connected,
|
|
229
244
|
new Promise((resolve) => {
|
|
230
245
|
timeout = setTimeout(() => {
|
|
246
|
+
if (reason === "initializing")
|
|
247
|
+
reason = "timeout";
|
|
231
248
|
void stopChild(child);
|
|
232
249
|
resolve(false);
|
|
233
250
|
}, PROBE_TIMEOUT_MS);
|
|
234
251
|
}),
|
|
235
252
|
new Promise((resolve) => {
|
|
236
253
|
onAbort = () => {
|
|
254
|
+
if (reason === "initializing")
|
|
255
|
+
reason = "aborted";
|
|
237
256
|
void stopChild(child);
|
|
238
257
|
resolve(false);
|
|
239
258
|
};
|
|
@@ -246,6 +265,8 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
246
265
|
return result;
|
|
247
266
|
}
|
|
248
267
|
catch {
|
|
268
|
+
if (reason === "initializing")
|
|
269
|
+
reason = options.signal?.aborted ? "aborted" : "transport_error";
|
|
249
270
|
return false;
|
|
250
271
|
}
|
|
251
272
|
finally {
|
|
@@ -253,7 +274,17 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
253
274
|
clearTimeout(timeout);
|
|
254
275
|
if (onAbort !== undefined)
|
|
255
276
|
options.signal?.removeEventListener("abort", onAbort);
|
|
256
|
-
|
|
277
|
+
dslog("execution.acp_probe_cleanup_started", "ACP 探测清理开始", fields());
|
|
278
|
+
let cleanupFinished = false;
|
|
279
|
+
try {
|
|
280
|
+
await stopChild(child);
|
|
281
|
+
cleanupFinished = true;
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
dslog("execution.acp_probe_finished", "ACP 探测结束", { ...fields(),
|
|
285
|
+
cleanup_finished: cleanupFinished, exit_code: child.exitCode, exit_signal: child.signalCode,
|
|
286
|
+
level: reason === "ready" && cleanupFinished ? "INFO" : "WARN" });
|
|
287
|
+
}
|
|
257
288
|
}
|
|
258
289
|
}
|
|
259
290
|
export async function runKimiAcp(requestedOptions) {
|
package/dist/serve.js
CHANGED
|
@@ -51,10 +51,9 @@ import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
|
51
51
|
import { probeRuntimeHealth } from "./runtime-health.js";
|
|
52
52
|
import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
|
|
53
53
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
54
|
-
import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
|
|
55
54
|
import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
|
|
56
55
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
57
|
-
import {
|
|
56
|
+
import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
|
|
58
57
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
59
58
|
// normalize.ts 的活动种类 → activity 枚举
|
|
60
59
|
const ACTIVITY_MAP = {
|
|
@@ -127,17 +126,7 @@ export function serve(config, opts = {}) {
|
|
|
127
126
|
catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
|
|
128
127
|
},
|
|
129
128
|
});
|
|
130
|
-
|
|
131
|
-
const migrationController = createServeMigrationController({
|
|
132
|
-
config,
|
|
133
|
-
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
134
|
-
isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
|
|
135
|
-
sendStatus: (frame) => { try {
|
|
136
|
-
if (ws?.readyState === WebSocket.OPEN)
|
|
137
|
-
ws.send(JSON.stringify(frame));
|
|
138
|
-
}
|
|
139
|
-
catch { /* reconnect */ } },
|
|
140
|
-
});
|
|
129
|
+
const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
|
|
141
130
|
const codexHomeModeController = createCodexHomeModeController({ ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), scheduleRestart: opts.update?.scheduleRestart ?? scheduleServiceRestart, getServiceSpec: async () => { const eligibility = await updateEligibility(); return eligibility.eligible ? eligibility.serviceSpec : null; }, isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0, sendStatus: (frame) => { try {
|
|
142
131
|
if (ws?.readyState === WebSocket.OPEN)
|
|
143
132
|
ws.send(JSON.stringify(frame));
|
|
@@ -148,7 +137,7 @@ export function serve(config, opts = {}) {
|
|
|
148
137
|
const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
|
|
149
138
|
...opts.agentAbility,
|
|
150
139
|
coordinator: projectionCoordinator,
|
|
151
|
-
isAdmissionClosed:
|
|
140
|
+
isAdmissionClosed: storageIdentityAdmission.isAdmissionClosed,
|
|
152
141
|
});
|
|
153
142
|
let projectSkillsController;
|
|
154
143
|
const projectSkillsReconciler = opts.projectSkills?.reconciler ?? createProjectSkillsReconciler({
|
|
@@ -169,7 +158,7 @@ export function serve(config, opts = {}) {
|
|
|
169
158
|
}
|
|
170
159
|
catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
|
|
171
160
|
},
|
|
172
|
-
isAdmissionClosed:
|
|
161
|
+
isAdmissionClosed: storageIdentityAdmission.isAdmissionClosed,
|
|
173
162
|
reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
|
|
174
163
|
});
|
|
175
164
|
const logRegistryFailure = (error) => dslog("project_skills.registry_failed", "本机项目注册表读取失败,本次不上报项目清单(能力不受影响)", {
|
|
@@ -177,7 +166,7 @@ export function serve(config, opts = {}) {
|
|
|
177
166
|
});
|
|
178
167
|
const machineHelloPublisher = createMachineHelloPublisher({
|
|
179
168
|
effective: (hello) => {
|
|
180
|
-
const configuredIdentity = opts.
|
|
169
|
+
const configuredIdentity = opts.storageRuntime?.identity;
|
|
181
170
|
const currentIdentity = typeof configuredIdentity === "function"
|
|
182
171
|
? configuredIdentity()
|
|
183
172
|
: configuredIdentity;
|
|
@@ -187,23 +176,7 @@ export function serve(config, opts = {}) {
|
|
|
187
176
|
});
|
|
188
177
|
const initializedProjectSkills = initializedProjectSkillsReconciler(() => projectSkillsController.ensureScanned(), projectSkillsReconciler);
|
|
189
178
|
const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
|
|
190
|
-
const
|
|
191
|
-
const storageStatusDelivery = createServeStorageStatusDelivery(opts.storageMigration, {
|
|
192
|
-
onPersistenceError: (error) => dslog("daemon.storage_status_outbox_failed", "存储迁移状态 outbox 持久化失败", { level: "ERROR", error_message: error.message }),
|
|
193
|
-
transmit: (frame) => { try {
|
|
194
|
-
if (ws?.readyState === WebSocket.OPEN)
|
|
195
|
-
ws.send(JSON.stringify(frame));
|
|
196
|
-
}
|
|
197
|
-
catch { /* retry */ } },
|
|
198
|
-
});
|
|
199
|
-
const storageMigrationRuntime = attachStorageMigrationRuntime(opts.storageMigration, {
|
|
200
|
-
isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
|
|
201
|
-
waitForExecutionFrames: () => executionFrameQueue,
|
|
202
|
-
flushLocalState: flushSlog,
|
|
203
|
-
sendStatus: storageStatusDelivery.send,
|
|
204
|
-
});
|
|
205
|
-
storageMigrationAdmissionClosed = storageMigrationRuntime.isAdmissionClosed;
|
|
206
|
-
const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...storageCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
|
|
179
|
+
const prepareCapabilities = async () => daemonCapabilityBindings(process.platform, [...configuredCapabilities, ...await managedDaemonCapabilities(updateEligibility)]);
|
|
207
180
|
const safeExecutionSend = (frame) => {
|
|
208
181
|
try {
|
|
209
182
|
if (ws?.readyState !== WebSocket.OPEN)
|
|
@@ -316,21 +289,16 @@ export function serve(config, opts = {}) {
|
|
|
316
289
|
let connectionEpochToken;
|
|
317
290
|
ws.on("open", () => {
|
|
318
291
|
connectionEpochToken = projectSkillsController.beginConnectionEpoch();
|
|
319
|
-
|
|
292
|
+
storageIdentityAdmission.beginIdentityBinding();
|
|
320
293
|
const openedSocket = ws;
|
|
294
|
+
const connectionId = randomUUID();
|
|
321
295
|
backoff = 1000;
|
|
322
296
|
connectedAt = Date.now();
|
|
323
297
|
log(`🔌 已连接控制面 ${config.serverUrl}`);
|
|
324
|
-
dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl });
|
|
298
|
+
dslog("daemon.ws_open", "已连接控制面", { server_url: config.serverUrl, connection_id: connectionId });
|
|
325
299
|
// 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
|
|
326
300
|
void drainSpool();
|
|
327
301
|
// 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
|
|
328
|
-
storageStatusDelivery.replayPending();
|
|
329
|
-
const storageReplay = replayStorageMigrationStatus(opts.storageMigration, openedSocket, () => ws === openedSocket, (error) => {
|
|
330
|
-
dslog("daemon.storage_status_replay_failed", "存储迁移状态重放失败", {
|
|
331
|
-
level: "ERROR", error_message: error.message,
|
|
332
|
-
});
|
|
333
|
-
}, storageStatusDelivery.send);
|
|
334
302
|
const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
|
|
335
303
|
const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
|
|
336
304
|
...(detectInstalled ? { detectInstalled } : {}),
|
|
@@ -345,28 +313,41 @@ export function serve(config, opts = {}) {
|
|
|
345
313
|
.then(async (hello) => {
|
|
346
314
|
if (hello.executionProtocol === undefined) {
|
|
347
315
|
executionRuntimesKnown = true;
|
|
316
|
+
dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
|
|
317
|
+
connection_id: connectionId, reason: "protocol_unavailable",
|
|
318
|
+
current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
|
|
319
|
+
socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
|
|
320
|
+
execution_runtimes: JSON.stringify(hello.executionRuntimes),
|
|
321
|
+
});
|
|
348
322
|
return [];
|
|
349
323
|
}
|
|
350
|
-
const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
|
|
324
|
+
const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable, connectionId);
|
|
351
325
|
executionRuntimesKnown = true;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
326
|
+
const currentSocket = ws === openedSocket;
|
|
327
|
+
const socketOpen = openedSocket.readyState === WebSocket.OPEN;
|
|
328
|
+
const hasNewRuntime = detected.some((runtime) => !hello.executionRuntimes.includes(runtime));
|
|
329
|
+
dslog("runtime_probe.hello_refresh", "runtime hello 刷新决策", {
|
|
330
|
+
connection_id: connectionId, current_socket: currentSocket, socket_open: socketOpen,
|
|
331
|
+
socket_ready_state: openedSocket.readyState, installed_runtimes: JSON.stringify(hello.runtimes),
|
|
332
|
+
initial_execution_runtimes: JSON.stringify(hello.executionRuntimes), execution_runtimes: JSON.stringify(detected),
|
|
333
|
+
reason: !currentSocket ? "stale_connection" : !socketOpen ? "socket_not_open" : !hasNewRuntime ? "unchanged" : "publish",
|
|
334
|
+
});
|
|
335
|
+
if (currentSocket && socketOpen && hasNewRuntime) {
|
|
355
336
|
detectedExecutionRuntimes = detected;
|
|
356
337
|
machineHelloPublisher.publish({ ...hello, executionRuntimes: [...detected] }, openedSocket);
|
|
357
338
|
}
|
|
358
339
|
return detected;
|
|
359
340
|
})
|
|
360
|
-
.catch((
|
|
341
|
+
.catch(() => {
|
|
361
342
|
executionRuntimesKnown = true;
|
|
362
343
|
dslog("execution.runtime_detection_failed", "runtime 探测失败", {
|
|
363
|
-
level: "ERROR",
|
|
344
|
+
level: "ERROR", reason: "detection_threw", connection_id: connectionId,
|
|
345
|
+
current_socket: ws === openedSocket, socket_open: openedSocket.readyState === WebSocket.OPEN,
|
|
364
346
|
});
|
|
365
347
|
return [];
|
|
366
348
|
});
|
|
367
349
|
void helloPromise
|
|
368
350
|
.then(async (hello) => {
|
|
369
|
-
await storageReplay;
|
|
370
351
|
if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
|
|
371
352
|
return;
|
|
372
353
|
detectedExecutionRuntimes = hello.executionRuntimes;
|
|
@@ -414,8 +395,6 @@ export function serve(config, opts = {}) {
|
|
|
414
395
|
if (typeof decoded !== "object" || decoded === null)
|
|
415
396
|
return;
|
|
416
397
|
const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
|
|
417
|
-
if (storageStatusDelivery.acknowledge(decoded))
|
|
418
|
-
return;
|
|
419
398
|
if (rawType === "daemon:update" || rawType === "daemon:restart") {
|
|
420
399
|
void updateController.handle(decoded).catch((error) => {
|
|
421
400
|
dslog("daemon.control_failed", "daemon 控制操作处理失败", {
|
|
@@ -424,19 +403,10 @@ export function serve(config, opts = {}) {
|
|
|
424
403
|
});
|
|
425
404
|
return;
|
|
426
405
|
}
|
|
427
|
-
if (rawType === "daemon:migrate") {
|
|
428
|
-
void handleMigrationControlMessage(migrationController, decoded).catch((error) => dslog("daemon.migration_failed", "daemon 布局迁移失败", { level: "ERROR", error_message: error.message }));
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
406
|
if (rawType === "daemon:codex-home-mode") {
|
|
432
407
|
void codexHomeModeController.handle(decoded).catch((error) => dslog("daemon.codex_home_mode_failed", "Codex Home 模式切换失败", { level: "ERROR", error_message: error.message }));
|
|
433
408
|
return;
|
|
434
409
|
}
|
|
435
|
-
if (handleStorageMigrationControl(opts.storageMigration, rawType, decoded, storageStatusDelivery.send, (kind, error) => {
|
|
436
|
-
dslog(kind === "migration" ? "daemon.storage_migration_failed" : "daemon.storage_migration_confirmation_failed", kind === "migration" ? "存储迁移控制操作失败" : "存储迁移 hello 确认处理失败", { level: "ERROR", error_message: error.message });
|
|
437
|
-
})) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
410
|
if (rawType.startsWith("execution:")) {
|
|
441
411
|
const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
|
|
442
412
|
if (!parsedExecution.success) {
|
|
@@ -498,10 +468,10 @@ export function serve(config, opts = {}) {
|
|
|
498
468
|
return;
|
|
499
469
|
}
|
|
500
470
|
const spec = frame;
|
|
501
|
-
if (
|
|
471
|
+
if (storageIdentityAdmission.isAdmissionClosed()) {
|
|
502
472
|
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
503
473
|
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
504
|
-
reason: "resource_limit", message: "Storage
|
|
474
|
+
reason: "resource_limit", message: "Storage identity binding is not complete",
|
|
505
475
|
at: new Date().toISOString(),
|
|
506
476
|
}));
|
|
507
477
|
return;
|
|
@@ -812,17 +782,17 @@ export function serve(config, opts = {}) {
|
|
|
812
782
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
813
783
|
const r = msg;
|
|
814
784
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
815
|
-
if (opts.
|
|
785
|
+
if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
|
|
816
786
|
const bindingSocket = ws;
|
|
817
|
-
|
|
787
|
+
bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
|
|
818
788
|
onBound: () => {
|
|
819
789
|
if (bindingSocket !== ws || bindingSocket?.readyState !== WebSocket.OPEN)
|
|
820
790
|
return;
|
|
821
|
-
|
|
791
|
+
storageIdentityAdmission.completeIdentityBinding();
|
|
822
792
|
machineHelloPublisher.republish();
|
|
823
793
|
},
|
|
824
794
|
onError: (error) => {
|
|
825
|
-
dslog("daemon.storage_identity_bind_failed", "
|
|
795
|
+
dslog("daemon.storage_identity_bind_failed", "存储 Workspace 身份绑定失败", { level: "ERROR", error_message: error.message });
|
|
826
796
|
if (bindingSocket === ws && bindingSocket?.readyState === WebSocket.OPEN)
|
|
827
797
|
bindingSocket.close(1011, "storage identity binding failed");
|
|
828
798
|
},
|
|
@@ -925,7 +895,7 @@ export function serve(config, opts = {}) {
|
|
|
925
895
|
}
|
|
926
896
|
if (msg.type !== "agent:start")
|
|
927
897
|
return; // ready/error 等忽略
|
|
928
|
-
if (
|
|
898
|
+
if (storageIdentityAdmission.isAdmissionClosed())
|
|
929
899
|
return;
|
|
930
900
|
// 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
|
|
931
901
|
// daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
|
|
@@ -1357,7 +1327,6 @@ export function serve(config, opts = {}) {
|
|
|
1357
1327
|
concurrencyTelemetry.stop();
|
|
1358
1328
|
runtimeProbe.stop();
|
|
1359
1329
|
completionRetransmitter.stop();
|
|
1360
|
-
storageStatusDelivery.stop();
|
|
1361
1330
|
const deadline = createShutdownDeadline(shutdownTimeoutMs);
|
|
1362
1331
|
if (reconnectTimer !== null) {
|
|
1363
1332
|
clearTimeout(reconnectTimer);
|
|
@@ -1367,7 +1336,6 @@ export function serve(config, opts = {}) {
|
|
|
1367
1336
|
const pending = [
|
|
1368
1337
|
webSocketClosed,
|
|
1369
1338
|
updateController.drain(),
|
|
1370
|
-
...(migrationController === null ? [] : [migrationController.drain()]),
|
|
1371
1339
|
codexHomeModeController.drain(),
|
|
1372
1340
|
executionFrameQueue,
|
|
1373
1341
|
...executionRuns.values(),
|
package/dist/storage-identity.js
CHANGED
|
@@ -39,8 +39,5 @@ export function storageHelloIdentity(input) {
|
|
|
39
39
|
daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
|
|
40
40
|
agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
|
|
41
41
|
...(ownership === undefined ? {} : { storageOwnership: ownership }),
|
|
42
|
-
...(installation.storageMigrationId === undefined
|
|
43
|
-
? {}
|
|
44
|
-
: { storageMigrationId: installation.storageMigrationId }),
|
|
45
42
|
});
|
|
46
43
|
}
|