@nowcrew/daemon 0.5.18 → 0.5.19
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 +23 -0
- package/dist/attachments.js +161 -0
- package/dist/execution-event-limit.js +1 -1
- package/dist/execution-protocol.js +19 -1
- package/dist/execution-runner.js +38 -5
- package/dist/external-output.js +86 -0
- package/dist/list-models.js +41 -5
- package/dist/local-executor.js +43 -5
- package/dist/machine-info.js +5 -1
- package/dist/origin-decision.js +3 -1
- package/dist/prompt.js +4 -1
- package/dist/runner.js +3 -3
- package/dist/runtime-capabilities.js +38 -0
- package/dist/runtime-path.js +56 -0
- package/dist/runtimes/claude.js +1 -0
- package/dist/runtimes/codex-app-server-runner.js +340 -0
- package/dist/runtimes/codex.js +2 -0
- package/dist/runtimes/kimi-acp-runner.js +117 -17
- package/dist/runtimes/kimi.js +2 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/serve.js +38 -6
- package/dist/session.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -80,6 +80,25 @@ the local workspace/environment, and launches only a built-in runtime adapter (`
|
|
|
80
80
|
`kimi`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
|
|
81
81
|
behavior. Those are server responsibilities.
|
|
82
82
|
|
|
83
|
+
The stable adapters keep prompts out of provider argv wherever the provider protocol allows it:
|
|
84
|
+
|
|
85
|
+
| Runtime | Transport | System/wake input | Native resume |
|
|
86
|
+
| --- | --- | --- | --- |
|
|
87
|
+
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
88
|
+
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume` |
|
|
89
|
+
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
|
|
90
|
+
|
|
91
|
+
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
92
|
+
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
93
|
+
CLI and carries the same native session id; unrelated ACP failures stay failures. The CLI fallback is
|
|
94
|
+
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
95
|
+
durable Job Object ownership is available.
|
|
96
|
+
|
|
97
|
+
Codex and Kimi sessions use the same bounded per-task persistence and keyed lease as Claude when they
|
|
98
|
+
run through protocol v1. A first-progress watchdog covers a provider that accepts a turn but remains
|
|
99
|
+
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
100
|
+
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
101
|
+
|
|
83
102
|
Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
|
|
84
103
|
found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
|
|
85
104
|
adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
|
|
@@ -104,6 +123,10 @@ Before accepting work, the daemon writes a local journal entry under:
|
|
|
104
123
|
running supervisors are identity-checked and terminated before interruption is reported. A lock prevents
|
|
105
124
|
two daemon processes from sharing one journal.
|
|
106
125
|
|
|
126
|
+
Protocol v1 is advertised only when the platform has a durable process-tree backend. POSIX uses an
|
|
127
|
+
owned process group. Native Windows intentionally fails admission until a Job Object backend has passed
|
|
128
|
+
crash-cleanup tests; `taskkill /T` is not treated as equivalent ownership.
|
|
129
|
+
|
|
107
130
|
## Local Policy
|
|
108
131
|
|
|
109
132
|
Useful environment controls:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, resolve, sep } from "node:path";
|
|
3
|
+
export const ATTACHMENT_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
4
|
+
export const ATTACHMENT_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
function truncateUtf8(value, maxBytes) {
|
|
6
|
+
let bytes = 0;
|
|
7
|
+
let output = "";
|
|
8
|
+
for (const character of value) {
|
|
9
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
10
|
+
if (bytes + size > maxBytes)
|
|
11
|
+
break;
|
|
12
|
+
output += character;
|
|
13
|
+
bytes += size;
|
|
14
|
+
}
|
|
15
|
+
return output;
|
|
16
|
+
}
|
|
17
|
+
function safeFilename(value) {
|
|
18
|
+
const leaf = basename(value.replace(/\\/gu, "/"))
|
|
19
|
+
.replace(/[\u0000-\u001f\u007f]/gu, "_")
|
|
20
|
+
.replace(/^\.+/u, "")
|
|
21
|
+
.trim();
|
|
22
|
+
return truncateUtf8(leaf, 180) || "attachment";
|
|
23
|
+
}
|
|
24
|
+
function collisionName(filename, index) {
|
|
25
|
+
if (index === 1)
|
|
26
|
+
return filename;
|
|
27
|
+
const extension = extname(filename);
|
|
28
|
+
const stem = extension ? filename.slice(0, -extension.length) : filename;
|
|
29
|
+
return `${stem}-${index}${extension}`;
|
|
30
|
+
}
|
|
31
|
+
function containedPath(directory, filename) {
|
|
32
|
+
const target = resolve(directory, filename);
|
|
33
|
+
if (!target.startsWith(`${resolve(directory)}${sep}`)) {
|
|
34
|
+
throw new Error("Attachment path escapes the execution directory");
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
function checkedRedirect(value, base) {
|
|
39
|
+
const url = new URL(value, base);
|
|
40
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
41
|
+
throw new Error("Attachment redirects must use credential-free HTTPS URLs");
|
|
42
|
+
}
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
async function readBounded(response, maxBytes) {
|
|
46
|
+
const declared = Number(response.headers.get("content-length") ?? "");
|
|
47
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
48
|
+
throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
|
|
49
|
+
}
|
|
50
|
+
if (!response.body)
|
|
51
|
+
throw new Error("Attachment response body is empty");
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let size = 0;
|
|
55
|
+
while (true) {
|
|
56
|
+
const part = await reader.read();
|
|
57
|
+
if (part.done)
|
|
58
|
+
break;
|
|
59
|
+
const chunk = Buffer.from(part.value);
|
|
60
|
+
size += chunk.length;
|
|
61
|
+
if (size > maxBytes) {
|
|
62
|
+
await reader.cancel();
|
|
63
|
+
throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
|
|
64
|
+
}
|
|
65
|
+
chunks.push(chunk);
|
|
66
|
+
}
|
|
67
|
+
return Buffer.concat(chunks, size);
|
|
68
|
+
}
|
|
69
|
+
async function downloadAttachment(input) {
|
|
70
|
+
const initial = new URL(`/agent/attachments/${encodeURIComponent(input.attachment.id)}/download`, input.serverUrl);
|
|
71
|
+
if (!["http:", "https:"].includes(initial.protocol)
|
|
72
|
+
|| initial.username || initial.password) {
|
|
73
|
+
throw new Error("Invalid NowWork attachment download URL");
|
|
74
|
+
}
|
|
75
|
+
let current = initial;
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
let response = await input.fetchImpl(current, {
|
|
80
|
+
redirect: "manual",
|
|
81
|
+
headers: { authorization: `Bearer ${input.token}` },
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
for (let redirects = 0; [301, 302, 303, 307, 308].includes(response.status); redirects += 1) {
|
|
85
|
+
if (redirects >= input.maxRedirects)
|
|
86
|
+
throw new Error("Attachment redirect limit exceeded");
|
|
87
|
+
const location = response.headers.get("location");
|
|
88
|
+
if (!location)
|
|
89
|
+
throw new Error("Attachment redirect is missing a location");
|
|
90
|
+
current = checkedRedirect(location, current);
|
|
91
|
+
response = await input.fetchImpl(current, { redirect: "manual", signal: controller.signal });
|
|
92
|
+
}
|
|
93
|
+
if (!response.ok)
|
|
94
|
+
throw new Error(`Attachment download failed with status ${response.status}`);
|
|
95
|
+
const data = await readBounded(response, input.maxFileBytes);
|
|
96
|
+
if (data.length !== input.attachment.sizeBytes) {
|
|
97
|
+
throw new Error(`Attachment size mismatch: expected ${input.attachment.sizeBytes}, received ${data.length}`);
|
|
98
|
+
}
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (controller.signal.aborted) {
|
|
103
|
+
throw new Error(`Attachment download timed out after ${input.timeoutMs}ms`);
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function materializeAttachments(input) {
|
|
112
|
+
const maxFileBytes = input.maxFileBytes ?? ATTACHMENT_MAX_FILE_BYTES;
|
|
113
|
+
const maxTotalBytes = input.maxTotalBytes ?? ATTACHMENT_MAX_TOTAL_BYTES;
|
|
114
|
+
const total = input.attachments.reduce((sum, attachment) => sum + attachment.sizeBytes, 0);
|
|
115
|
+
if (input.attachments.some((attachment) => attachment.sizeBytes > maxFileBytes)
|
|
116
|
+
|| total > maxTotalBytes) {
|
|
117
|
+
throw new Error("Attachment metadata exceeds the configured byte limit");
|
|
118
|
+
}
|
|
119
|
+
const attachmentsRoot = resolve(input.runDir, "attachments");
|
|
120
|
+
const executionKey = input.executionId.replace(/[^A-Za-z0-9._-]/gu, "_");
|
|
121
|
+
const directory = resolve(attachmentsRoot, executionKey);
|
|
122
|
+
if (!directory.startsWith(`${attachmentsRoot}${sep}`)) {
|
|
123
|
+
throw new Error("Attachment directory escapes the run directory");
|
|
124
|
+
}
|
|
125
|
+
await rm(directory, { recursive: true, force: true });
|
|
126
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
127
|
+
const used = new Set();
|
|
128
|
+
const materialized = [];
|
|
129
|
+
try {
|
|
130
|
+
for (const attachment of input.attachments) {
|
|
131
|
+
const base = safeFilename(attachment.filename);
|
|
132
|
+
let collision = 1;
|
|
133
|
+
let filename = collisionName(base, collision);
|
|
134
|
+
while (used.has(filename.toLowerCase())) {
|
|
135
|
+
collision += 1;
|
|
136
|
+
filename = collisionName(base, collision);
|
|
137
|
+
}
|
|
138
|
+
used.add(filename.toLowerCase());
|
|
139
|
+
const path = containedPath(directory, filename);
|
|
140
|
+
const data = await downloadAttachment({
|
|
141
|
+
serverUrl: input.serverUrl,
|
|
142
|
+
token: input.token,
|
|
143
|
+
attachment,
|
|
144
|
+
fetchImpl: input.fetchImpl ?? fetch,
|
|
145
|
+
maxFileBytes,
|
|
146
|
+
maxRedirects: input.maxRedirects ?? 3,
|
|
147
|
+
timeoutMs: input.timeoutMs ?? 10_000,
|
|
148
|
+
});
|
|
149
|
+
await writeFile(path, data, { flag: "wx", mode: 0o600 });
|
|
150
|
+
materialized.push({ ...attachment, filename, path });
|
|
151
|
+
}
|
|
152
|
+
return { directory, attachments: materialized };
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
await rm(directory, { recursive: true, force: true });
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export async function cleanupMaterializedAttachments(directory) {
|
|
160
|
+
await rm(directory, { recursive: true, force: true });
|
|
161
|
+
}
|
|
@@ -36,7 +36,7 @@ export function boundExecutionFrame(input, maxBytes) {
|
|
|
36
36
|
if (input.type === "execution:activity") {
|
|
37
37
|
frame = withBoundedString(frame, "detail", maxBytes, false);
|
|
38
38
|
}
|
|
39
|
-
else if (input.type === "execution:console") {
|
|
39
|
+
else if (input.type === "execution:console" || input.type === "execution:output") {
|
|
40
40
|
frame = withBoundedString(frame, "text", maxBytes, false);
|
|
41
41
|
}
|
|
42
42
|
else if (input.type === "execution:rejected") {
|
|
@@ -48,7 +48,7 @@ export const LegacyAgentStartSchema = z.object({
|
|
|
48
48
|
content: z.string().optional(),
|
|
49
49
|
senderHandle: z.string().optional(),
|
|
50
50
|
threadId: z.string().optional(),
|
|
51
|
-
origin: z.enum(["wecom"]).optional(),
|
|
51
|
+
origin: z.enum(["wecom", "feishu"]).optional(),
|
|
52
52
|
}).passthrough().optional(),
|
|
53
53
|
scheduledRun: z.object({
|
|
54
54
|
jobId: z.string().min(1),
|
|
@@ -85,6 +85,12 @@ export const ConsoleStreamSchema = z.enum([
|
|
|
85
85
|
"result",
|
|
86
86
|
"error",
|
|
87
87
|
]);
|
|
88
|
+
export const ExecutionAttachmentSchema = z.object({
|
|
89
|
+
id: z.string().min(1).max(200),
|
|
90
|
+
filename: z.string().min(1).max(255),
|
|
91
|
+
mime: z.string().min(1).max(200),
|
|
92
|
+
sizeBytes: z.number().int().nonnegative().max(25 * 1024 * 1024),
|
|
93
|
+
}).strict();
|
|
88
94
|
export const ExecutionStartSchema = z.object({
|
|
89
95
|
type: z.literal("execution:start"),
|
|
90
96
|
protocolVersion: ProtocolVersionSchema,
|
|
@@ -115,6 +121,8 @@ export const ExecutionStartSchema = z.object({
|
|
|
115
121
|
channelId: z.string().min(1),
|
|
116
122
|
threadId: z.string().min(1).optional(),
|
|
117
123
|
wakeMessageId: z.string().min(1).optional(),
|
|
124
|
+
externalResponseSessionId: ExecutionIdSchema.optional(),
|
|
125
|
+
attachments: z.array(ExecutionAttachmentSchema).max(20).optional(),
|
|
118
126
|
}).strict(),
|
|
119
127
|
reporting: z.object({
|
|
120
128
|
captureFinal: z.boolean(),
|
|
@@ -185,6 +193,15 @@ export const ExecutionConsoleSchema = z.object({
|
|
|
185
193
|
seq: SequenceSchema,
|
|
186
194
|
at: TimestampSchema,
|
|
187
195
|
}).strict();
|
|
196
|
+
export const ExecutionOutputSchema = z.object({
|
|
197
|
+
type: z.literal("execution:output"),
|
|
198
|
+
protocolVersion: ProtocolVersionSchema,
|
|
199
|
+
executionId: ExecutionIdSchema,
|
|
200
|
+
channel: z.literal("external_answer"),
|
|
201
|
+
text: z.string().min(1),
|
|
202
|
+
seq: SequenceSchema,
|
|
203
|
+
at: TimestampSchema,
|
|
204
|
+
}).strict();
|
|
188
205
|
export const ExecutionUsageSchema = z.object({
|
|
189
206
|
inputTokens: TokenCountSchema,
|
|
190
207
|
outputTokens: TokenCountSchema,
|
|
@@ -312,6 +329,7 @@ const RawDaemonToServerExecutionFrameSchema = z.discriminatedUnion("type", [
|
|
|
312
329
|
ExecutionStartedSchema,
|
|
313
330
|
ExecutionActivitySchema,
|
|
314
331
|
ExecutionConsoleSchema,
|
|
332
|
+
ExecutionOutputSchema,
|
|
315
333
|
RawExecutionCompletedSchema,
|
|
316
334
|
ExecutionSnapshotSchema,
|
|
317
335
|
]);
|
package/dist/execution-runner.js
CHANGED
|
@@ -8,7 +8,7 @@ import { mintAgentToken } from "./token.js";
|
|
|
8
8
|
import { executeLocal, withLocalExecutionFacts, } from "./local-executor.js";
|
|
9
9
|
import { startDormantSupervisor, } from "./execution-supervisor.js";
|
|
10
10
|
import { buildClaudeArgs, CLAUDE_EFFORT_LEVELS } from "./runtimes/claude.js";
|
|
11
|
-
import {
|
|
11
|
+
import { CODEX_EFFORT_LEVELS } from "./runtimes/codex.js";
|
|
12
12
|
import { KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
13
13
|
import { executionBackendCapability } from "./execution-backend.js";
|
|
14
14
|
import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
|
|
@@ -183,7 +183,7 @@ function launchProviderConfig(config) {
|
|
|
183
183
|
...(config.description === undefined ? {} : { description: config.description }),
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
|
-
function supervisorLaunch(request) {
|
|
186
|
+
export function supervisorLaunch(request) {
|
|
187
187
|
const common = {
|
|
188
188
|
wakePrompt: request.wakePrompt,
|
|
189
189
|
dangerous: request.effectivePermission === "full_access",
|
|
@@ -211,11 +211,23 @@ function supervisorLaunch(request) {
|
|
|
211
211
|
}
|
|
212
212
|
if (request.runtime === "codex") {
|
|
213
213
|
return {
|
|
214
|
-
command:
|
|
215
|
-
args:
|
|
214
|
+
command: process.execPath,
|
|
215
|
+
args: [
|
|
216
|
+
fileURLToPath(new URL("./runtimes/codex-app-server-runner.js", import.meta.url)),
|
|
217
|
+
"--bin", request.bin,
|
|
218
|
+
],
|
|
216
219
|
cwd: request.cwd,
|
|
217
220
|
env: request.env,
|
|
218
|
-
stdinText:
|
|
221
|
+
stdinText: JSON.stringify({
|
|
222
|
+
systemPrompt: request.systemPrompt,
|
|
223
|
+
wakePrompt: request.wakePrompt,
|
|
224
|
+
effectivePermission: request.effectivePermission,
|
|
225
|
+
...(request.model === undefined ? {} : { model: request.model }),
|
|
226
|
+
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
227
|
+
...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
|
|
228
|
+
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
229
|
+
resume: request.resume,
|
|
230
|
+
}),
|
|
219
231
|
};
|
|
220
232
|
}
|
|
221
233
|
if (request.effectivePermission !== "full_access") {
|
|
@@ -227,6 +239,8 @@ function supervisorLaunch(request) {
|
|
|
227
239
|
fileURLToPath(new URL("./runtimes/kimi-acp-runner.js", import.meta.url)),
|
|
228
240
|
"--bin", request.bin,
|
|
229
241
|
...(request.model === undefined ? [] : ["--model", request.model]),
|
|
242
|
+
...(request.sessionId === undefined ? [] : ["--session", request.sessionId]),
|
|
243
|
+
...(request.resume ? ["--resume"] : []),
|
|
230
244
|
],
|
|
231
245
|
cwd: request.cwd,
|
|
232
246
|
env: request.env,
|
|
@@ -442,6 +456,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
442
456
|
const providerConfig = launchProviderConfig(credential.config);
|
|
443
457
|
let activitySequence = 0;
|
|
444
458
|
let consoleSequence = 0;
|
|
459
|
+
let externalOutputSequence = 0;
|
|
445
460
|
const callbacks = {
|
|
446
461
|
...(spec.reporting.streamActivity ? {
|
|
447
462
|
onActivity: (activity) => {
|
|
@@ -479,6 +494,23 @@ export async function runExecution(config, input, dependencies) {
|
|
|
479
494
|
catch { /* best-effort console omitted when its envelope cannot fit */ }
|
|
480
495
|
},
|
|
481
496
|
} : {}),
|
|
497
|
+
...(spec.context.externalResponseSessionId ? {
|
|
498
|
+
onExternalOutput: (text) => {
|
|
499
|
+
const frame = DaemonToServerExecutionFrameSchema.parse({
|
|
500
|
+
type: "execution:output",
|
|
501
|
+
protocolVersion: 1,
|
|
502
|
+
executionId: spec.executionId,
|
|
503
|
+
channel: "external_answer",
|
|
504
|
+
text,
|
|
505
|
+
seq: externalOutputSequence++,
|
|
506
|
+
at: now().toISOString(),
|
|
507
|
+
});
|
|
508
|
+
try {
|
|
509
|
+
telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
|
|
510
|
+
}
|
|
511
|
+
catch { /* final completion remains the authoritative repair */ }
|
|
512
|
+
},
|
|
513
|
+
} : {}),
|
|
482
514
|
};
|
|
483
515
|
const localDependencies = {
|
|
484
516
|
launchRuntime: async (request) => {
|
|
@@ -543,6 +575,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
543
575
|
taskKey: spec.workspace.taskKey,
|
|
544
576
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
545
577
|
...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
|
|
578
|
+
...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
|
|
546
579
|
systemPrompt: withLocalExecutionFacts(spec.instructions.systemPrompt, config.executionLimits.maxPromptBytes
|
|
547
580
|
- Buffer.byteLength(spec.instructions.wakePrompt, "utf8")),
|
|
548
581
|
wakePrompt: spec.instructions.wakePrompt,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const EXTERNAL_ANSWER_OPEN = "<nowwork_external_answer>";
|
|
2
|
+
export const EXTERNAL_ANSWER_CLOSE = "</nowwork_external_answer>";
|
|
3
|
+
function retainedMarkerPrefix(value, marker) {
|
|
4
|
+
const maximum = Math.min(value.length, marker.length - 1);
|
|
5
|
+
for (let length = maximum; length > 0; length -= 1) {
|
|
6
|
+
if (marker.startsWith(value.slice(-length)))
|
|
7
|
+
return length;
|
|
8
|
+
}
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
export class ExternalAnswerDecoder {
|
|
12
|
+
state = "outside";
|
|
13
|
+
pending = "";
|
|
14
|
+
push(text) {
|
|
15
|
+
if (!text)
|
|
16
|
+
return [];
|
|
17
|
+
const output = [];
|
|
18
|
+
this.consume(this.pending + text, output);
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
finish() {
|
|
22
|
+
this.pending = "";
|
|
23
|
+
this.state = "outside";
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
consume(value, output) {
|
|
27
|
+
this.pending = "";
|
|
28
|
+
const marker = this.state === "outside" ? EXTERNAL_ANSWER_OPEN : EXTERNAL_ANSWER_CLOSE;
|
|
29
|
+
const markerAt = value.indexOf(marker);
|
|
30
|
+
if (markerAt >= 0) {
|
|
31
|
+
if (this.state === "inside" && markerAt > 0)
|
|
32
|
+
output.push(value.slice(0, markerAt));
|
|
33
|
+
this.state = this.state === "outside" ? "inside" : "outside";
|
|
34
|
+
const remaining = value.slice(markerAt + marker.length);
|
|
35
|
+
if (remaining)
|
|
36
|
+
this.consume(remaining, output);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const retained = retainedMarkerPrefix(value, marker);
|
|
40
|
+
const safe = retained > 0 ? value.slice(0, -retained) : value;
|
|
41
|
+
this.pending = retained > 0 ? value.slice(-retained) : "";
|
|
42
|
+
if (this.state === "inside" && safe)
|
|
43
|
+
output.push(safe);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
47
|
+
if (runtime === "claude") {
|
|
48
|
+
const candidate = (event ?? {});
|
|
49
|
+
const delta = candidate.event?.delta;
|
|
50
|
+
if (candidate.type !== "stream_event"
|
|
51
|
+
|| candidate.event?.type !== "content_block_delta"
|
|
52
|
+
|| delta?.type !== "text_delta"
|
|
53
|
+
|| typeof delta.text !== "string")
|
|
54
|
+
return [];
|
|
55
|
+
return decoder.push(delta.text);
|
|
56
|
+
}
|
|
57
|
+
const candidate = (event ?? {});
|
|
58
|
+
if (runtime === "codex") {
|
|
59
|
+
return candidate.type === "item.completed"
|
|
60
|
+
&& candidate.item?.type === "agent_message"
|
|
61
|
+
&& typeof candidate.item.text === "string"
|
|
62
|
+
? decoder.push(candidate.item.text)
|
|
63
|
+
: [];
|
|
64
|
+
}
|
|
65
|
+
return candidate.type === undefined
|
|
66
|
+
&& candidate.role === "assistant"
|
|
67
|
+
&& typeof candidate.content === "string"
|
|
68
|
+
? decoder.push(candidate.content)
|
|
69
|
+
: [];
|
|
70
|
+
}
|
|
71
|
+
export function stripExternalAnswerMarkers(value) {
|
|
72
|
+
const sections = [];
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
while (cursor < value.length) {
|
|
75
|
+
const openAt = value.indexOf(EXTERNAL_ANSWER_OPEN, cursor);
|
|
76
|
+
if (openAt < 0)
|
|
77
|
+
break;
|
|
78
|
+
const contentAt = openAt + EXTERNAL_ANSWER_OPEN.length;
|
|
79
|
+
const closeAt = value.indexOf(EXTERNAL_ANSWER_CLOSE, contentAt);
|
|
80
|
+
if (closeAt < 0)
|
|
81
|
+
return value.slice(contentAt).trim();
|
|
82
|
+
sections.push(value.slice(contentAt, closeAt));
|
|
83
|
+
cursor = closeAt + EXTERNAL_ANSWER_CLOSE.length;
|
|
84
|
+
}
|
|
85
|
+
return (sections.length > 0 ? sections.join("") : value).trim();
|
|
86
|
+
}
|
package/dist/list-models.js
CHANGED
|
@@ -2,8 +2,15 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { isWin } from "./platform.js";
|
|
4
4
|
const execFileRaw = promisify(execFile);
|
|
5
|
+
const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
6
|
+
const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
5
7
|
// win32 上 npm CLI 是 .cmd shim,execFile 需 shell 才能执行;参数全是固定字面量,无注入面。
|
|
6
|
-
const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
8
|
+
const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
9
|
+
shell: isWin(),
|
|
10
|
+
timeout: MODEL_PROBE_TIMEOUT_MS,
|
|
11
|
+
killSignal: "SIGKILL",
|
|
12
|
+
maxBuffer: MODEL_PROBE_MAX_BUFFER_BYTES,
|
|
13
|
+
});
|
|
7
14
|
export async function listRuntimeModels(runtime) {
|
|
8
15
|
switch (runtime) {
|
|
9
16
|
case "codex":
|
|
@@ -21,15 +28,44 @@ export async function listRuntimeModels(runtime) {
|
|
|
21
28
|
function parseCodexModels(stdout) {
|
|
22
29
|
try {
|
|
23
30
|
const parsed = JSON.parse(stdout);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.models)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return normalizeCodexModels(parsed.models);
|
|
28
35
|
}
|
|
29
36
|
catch {
|
|
30
37
|
return [];
|
|
31
38
|
}
|
|
32
39
|
}
|
|
40
|
+
function normalizeCodexModels(rows) {
|
|
41
|
+
const normalized = rows.flatMap((value) => {
|
|
42
|
+
if (typeof value !== "object" || value === null)
|
|
43
|
+
return [];
|
|
44
|
+
const row = value;
|
|
45
|
+
if (row.visibility != null && row.visibility !== "list")
|
|
46
|
+
return [];
|
|
47
|
+
const id = nonEmptyString(row.slug) ?? nonEmptyString(row.id);
|
|
48
|
+
if (id === null)
|
|
49
|
+
return [];
|
|
50
|
+
return [{
|
|
51
|
+
id,
|
|
52
|
+
label: nonEmptyString(row.display_name) ?? nonEmptyString(row.name) ?? id,
|
|
53
|
+
explicitDefault: row.default === true,
|
|
54
|
+
}];
|
|
55
|
+
});
|
|
56
|
+
const hasExplicitDefault = normalized.some((model) => model.explicitDefault);
|
|
57
|
+
return normalized.map((model, index) => ({
|
|
58
|
+
id: model.id,
|
|
59
|
+
label: model.label,
|
|
60
|
+
...((model.explicitDefault || (!hasExplicitDefault && index === 0)) ? { default: true } : {}),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function nonEmptyString(value) {
|
|
64
|
+
if (typeof value !== "string")
|
|
65
|
+
return null;
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
68
|
+
}
|
|
33
69
|
function parseCursorModels(stdout) {
|
|
34
70
|
return stdout
|
|
35
71
|
.split(/\r?\n/)
|
package/dist/local-executor.js
CHANGED
|
@@ -6,10 +6,14 @@ import { spawnClaude } from "./runtimes/claude.js";
|
|
|
6
6
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
7
7
|
import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
8
8
|
import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
9
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
9
10
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
10
11
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
11
12
|
import { toConsoleLines } from "./console.js";
|
|
12
13
|
import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
14
|
+
import { decodeExternalOutputEvent, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
|
|
15
|
+
import { cleanupMaterializedAttachments as cleanupAttachments, materializeAttachments, } from "./attachments.js";
|
|
16
|
+
import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
|
|
13
17
|
function truncateUtf8(value, maxBytes) {
|
|
14
18
|
if (maxBytes <= 0)
|
|
15
19
|
return "";
|
|
@@ -118,6 +122,7 @@ async function launchLegacyRuntime(request) {
|
|
|
118
122
|
return wrapChild(spawnCodex({
|
|
119
123
|
...common,
|
|
120
124
|
wakePrompt: `${request.systemPrompt}\n\n${request.wakePrompt}`,
|
|
125
|
+
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
121
126
|
}));
|
|
122
127
|
}
|
|
123
128
|
return wrapChild(spawnKimi({
|
|
@@ -150,7 +155,10 @@ async function withKeyedLease(key, operation) {
|
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
153
|
-
|
|
158
|
+
const stableProtocolRuntime = dependencies.launchRuntime !== undefined;
|
|
159
|
+
const supportsNativeResume = input.runtime.name === "claude"
|
|
160
|
+
|| (stableProtocolRuntime && runtimeCapability(input.runtime.name).nativeResume);
|
|
161
|
+
if (!supportsNativeResume || !input.session.enabled) {
|
|
154
162
|
return executeLocalUnlocked(input, callbacks, dependencies);
|
|
155
163
|
}
|
|
156
164
|
const leaseKey = JSON.stringify([
|
|
@@ -176,8 +184,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
176
184
|
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
177
185
|
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
178
186
|
});
|
|
187
|
+
let materialized = null;
|
|
179
188
|
try {
|
|
180
|
-
const supportsNativeResume = runtime.name === "claude"
|
|
189
|
+
const supportsNativeResume = runtime.name === "claude"
|
|
190
|
+
|| (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
|
|
181
191
|
const prior = input.session.enabled && supportsNativeResume
|
|
182
192
|
? await readSession(workspace.sessionDir)
|
|
183
193
|
: null;
|
|
@@ -197,13 +207,23 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
197
207
|
nearBudget,
|
|
198
208
|
};
|
|
199
209
|
const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
|
|
200
|
-
|
|
210
|
+
if (input.attachments && input.attachments.length > 0) {
|
|
211
|
+
materialized = await (dependencies.materializeAttachments ?? materializeAttachments)({
|
|
212
|
+
serverUrl: input.launch.serverUrl,
|
|
213
|
+
token: input.launch.token,
|
|
214
|
+
runDir: workspace.runDir,
|
|
215
|
+
executionId: input.executionId,
|
|
216
|
+
attachments: input.attachments,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
220
|
+
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
201
221
|
await writeFile(workspace.systemPromptPath, systemPrompt, "utf8");
|
|
202
222
|
const baseEnv = {
|
|
203
223
|
...process.env,
|
|
204
224
|
...sanitizeEnvVars(providerConfig.envVars),
|
|
205
225
|
...input.launch.systemEnv,
|
|
206
|
-
PATH: `${workspace.crewDir}${delimiter}${
|
|
226
|
+
PATH: `${workspace.crewDir}${delimiter}${augmentedPath()}`,
|
|
207
227
|
CREW_SERVER_URL: input.launch.serverUrl,
|
|
208
228
|
CREW_TOKEN: input.launch.token,
|
|
209
229
|
CREW_CHANNEL: input.channelId,
|
|
@@ -235,6 +255,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
235
255
|
...(runtime.reasoning === undefined ? {} : { reasoning: runtime.reasoning }),
|
|
236
256
|
...(launchSessionId === null ? {} : { sessionId: launchSessionId }),
|
|
237
257
|
resume: resuming,
|
|
258
|
+
...(attachmentPlan.nativeImagePaths.length > 0
|
|
259
|
+
? { imagePaths: attachmentPlan.nativeImagePaths }
|
|
260
|
+
: {}),
|
|
238
261
|
});
|
|
239
262
|
const activities = [];
|
|
240
263
|
let sessionId = launchSessionId;
|
|
@@ -242,6 +265,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
242
265
|
let observedModel = currentModel;
|
|
243
266
|
let finalText = null;
|
|
244
267
|
let sentViaCrew = false;
|
|
268
|
+
const externalOutput = new ExternalAnswerDecoder();
|
|
245
269
|
const readline = createInterface({ input: child.stdout });
|
|
246
270
|
readline.on("line", (line) => {
|
|
247
271
|
const event = parseLine(line);
|
|
@@ -266,6 +290,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
266
290
|
&& "type" in event && event.type === "kimi.acp.text_delta";
|
|
267
291
|
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
268
292
|
}
|
|
293
|
+
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
294
|
+
callbacks.onExternalOutput?.(text);
|
|
295
|
+
}
|
|
269
296
|
for (const chunk of toConsoleLines(event))
|
|
270
297
|
callbacks.onConsole?.(chunk);
|
|
271
298
|
});
|
|
@@ -313,11 +340,22 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
313
340
|
resumed: resuming,
|
|
314
341
|
sessionId,
|
|
315
342
|
errorMessage: errorTail || null,
|
|
316
|
-
finalText: input.captureFinal
|
|
343
|
+
finalText: input.captureFinal && finalText !== null
|
|
344
|
+
? stripExternalAnswerMarkers(finalText)
|
|
345
|
+
: null,
|
|
317
346
|
sentViaCrew,
|
|
318
347
|
};
|
|
319
348
|
}
|
|
320
349
|
finally {
|
|
350
|
+
if (materialized) {
|
|
351
|
+
try {
|
|
352
|
+
await (dependencies.cleanupMaterializedAttachments ?? cleanupAttachments)(materialized.directory);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
356
|
+
process.stderr.write(`[execution] failed to remove attachments ${materialized.directory}: ${detail}\n`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
321
359
|
try {
|
|
322
360
|
await rm(workspace.systemPromptPath, { force: true });
|
|
323
361
|
}
|
package/dist/machine-info.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { hostname, arch, platform as osPlatform } from "node:os";
|
|
6
6
|
import { lookupCmd } from "./platform.js";
|
|
7
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
7
8
|
import { execFile } from "node:child_process";
|
|
8
9
|
import { promisify } from "node:util";
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
@@ -21,6 +22,8 @@ export const DAEMON_CAPABILITIES = [
|
|
|
21
22
|
"reply_origin_v1",
|
|
22
23
|
"origin_decision_v1",
|
|
23
24
|
"execution_telemetry_ack_v1",
|
|
25
|
+
"execution_external_output_v1",
|
|
26
|
+
"execution_attachments_v1",
|
|
24
27
|
];
|
|
25
28
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
26
29
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
@@ -36,7 +39,8 @@ const RUNTIME_BINS = [
|
|
|
36
39
|
];
|
|
37
40
|
async function isInstalled(bin) {
|
|
38
41
|
try {
|
|
39
|
-
|
|
42
|
+
// 用增强 PATH 探测:daemon 继承的 PATH 可能缺 ~/.local/bin 等用户级目录(Claude 原生安装器落点)。
|
|
43
|
+
await execFileP(lookupCmd(), [bin], { env: { ...process.env, PATH: augmentedPath() } });
|
|
40
44
|
return true;
|
|
41
45
|
}
|
|
42
46
|
catch {
|
package/dist/origin-decision.js
CHANGED
|
@@ -27,7 +27,9 @@ export async function readOriginDecisionFile(path) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export function shouldRetryOriginDecision(wakeOrigin, decision, attempt) {
|
|
30
|
-
|
|
30
|
+
if (attempt !== 0 || wakeOrigin === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
return wakeOrigin === "wecom" ? decision?.decision !== "reply" : decision === null;
|
|
31
33
|
}
|
|
32
34
|
export async function runWithOriginDecisionGuard(wakeOrigin, runAttempt) {
|
|
33
35
|
const first = await runAttempt(0);
|