@botlearn-course/daemon 0.0.9 → 0.0.11
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 +3 -3
- package/dist/agent-service-client.d.ts +0 -1
- package/dist/agent-service-client.js +1 -36
- package/dist/agent-service-sandbox.d.ts +1 -5
- package/dist/agent-service-sandbox.js +12 -119
- package/dist/agent-service-ws-protocol.d.ts +1 -1
- package/dist/agent-service-ws-protocol.js +2 -6
- package/dist/file-candidates.d.ts +3 -4
- package/dist/file-candidates.js +21 -10
- package/dist/run-dispatcher.d.ts +2 -3
- package/dist/run-dispatcher.js +7 -13
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,9 +99,9 @@ runtime 凭据。
|
|
|
99
99
|
- `auth.json` 是敏感凭据文件(0600),token 只存本机、只发给你 login 时指定的 Course API;
|
|
100
100
|
日志、transcript、doctor 输出、错误信息在写出前都会经过脱敏(token / api key / 密码等
|
|
101
101
|
字段与常见 token 形态一律替换为 `[REDACTED]`)。
|
|
102
|
-
- 文件扫描只访问 workspace 内路径;包含
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
- 文件扫描只访问 workspace 内路径;包含 `..`、绝对路径或反斜杠的路径会被拒绝。daemon
|
|
103
|
+
只上报相对路径、大小、sha256 和有界脱敏 preview,文件正文始终留在本地或托管 sandbox
|
|
104
|
+
的持久 workspace,不上传 Course Service 或对象存储。扫描或 metadata 上报失败不影响 run 终态。
|
|
105
105
|
- 每个 run 在独立 workspace 内执行并有超时上限;runtime 进程在取消/超时后会被终止
|
|
106
106
|
(SIGTERM → SIGKILL)。
|
|
107
107
|
- `report_progress` MCP 子进程以 clean environment 启动,不继承 Course 控制字段或模型凭据;
|
|
@@ -12,7 +12,6 @@ export declare class AgentServiceRunClient {
|
|
|
12
12
|
getRun(): Promise<RunStartPayload>;
|
|
13
13
|
postEvent(agentRunId: string, event: RunEvent): Promise<void>;
|
|
14
14
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord>;
|
|
15
|
-
uploadFileContent(agentRunId: string, fileId: string, absPath: string, mimeType?: string): Promise<RunFileRecord>;
|
|
16
15
|
getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
17
16
|
heartbeat(): Promise<void>;
|
|
18
17
|
private assertRunId;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CourseClientError } from "./course-client.js";
|
|
2
|
-
import {
|
|
3
|
-
import { assertNoInjectedCredentials, redactSecretString, redactSecretsDeep, } from "./redaction.js";
|
|
2
|
+
import { redactSecretString, redactSecretsDeep, } from "./redaction.js";
|
|
4
3
|
const ERROR_BODY_MAX_CHARS = 500;
|
|
5
4
|
/** Run-scoped client used only inside an Agent Service sandbox. */
|
|
6
5
|
export class AgentServiceRunClient {
|
|
@@ -66,40 +65,6 @@ export class AgentServiceRunClient {
|
|
|
66
65
|
}
|
|
67
66
|
throw new Error("unreachable");
|
|
68
67
|
}
|
|
69
|
-
async uploadFileContent(agentRunId, fileId, absPath, mimeType) {
|
|
70
|
-
this.assertRunId(agentRunId);
|
|
71
|
-
const data = await readFile(absPath);
|
|
72
|
-
assertNoInjectedCredentials(data, [this.runToken]);
|
|
73
|
-
const path = `/course/v1/agent-service/runs/${agentRunId}/files/${fileId}/content`;
|
|
74
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
75
|
-
let response;
|
|
76
|
-
try {
|
|
77
|
-
response = await fetch(this.url(path), {
|
|
78
|
-
method: "PUT",
|
|
79
|
-
headers: {
|
|
80
|
-
authorization: `Bearer ${this.runToken}`,
|
|
81
|
-
...(this.traceId ? { "x-trace-id": this.traceId } : {}),
|
|
82
|
-
"content-type": mimeType ?? "application/octet-stream",
|
|
83
|
-
},
|
|
84
|
-
body: data,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
if (attempt === 2)
|
|
89
|
-
throw err;
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (response.ok)
|
|
94
|
-
return (await response.json());
|
|
95
|
-
const text = await response.text().catch(() => "");
|
|
96
|
-
const error = new CourseClientError(response.status, `PUT ${path} -> ${response.status} ${redactSecretString(text.slice(0, ERROR_BODY_MAX_CHARS))}`);
|
|
97
|
-
if (attempt === 2 || (response.status !== 429 && response.status < 500))
|
|
98
|
-
throw error;
|
|
99
|
-
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
100
|
-
}
|
|
101
|
-
throw new Error("unreachable");
|
|
102
|
-
}
|
|
103
68
|
async getRunRuntimeProfile(agentRunId) {
|
|
104
69
|
this.assertRunId(agentRunId);
|
|
105
70
|
return this.request("GET", `/course/v1/agent-service/runs/${agentRunId}/runtime-profile`);
|
|
@@ -34,9 +34,7 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
34
34
|
/** activation-scoped 模型短凭据;只保存在内存中,绝不进入 state.json。 */
|
|
35
35
|
private readonly activationContexts;
|
|
36
36
|
private readonly pendingAcks;
|
|
37
|
-
private readonly
|
|
38
|
-
private readonly pendingFileCommits;
|
|
39
|
-
private readonly fileGrants;
|
|
37
|
+
private readonly pendingFileReports;
|
|
40
38
|
private readonly inflightCommands;
|
|
41
39
|
private socket;
|
|
42
40
|
private sandboxGeneration;
|
|
@@ -61,7 +59,6 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
61
59
|
stop(): void;
|
|
62
60
|
postEvent(agentRunId: string, event: RunEvent): Promise<void>;
|
|
63
61
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord>;
|
|
64
|
-
uploadFileContent(agentRunId: string, fileId: string, absPath: string, mimeType?: string): Promise<RunFileRecord>;
|
|
65
62
|
getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
66
63
|
private connectOnce;
|
|
67
64
|
private handleHello;
|
|
@@ -91,7 +88,6 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
91
88
|
private matchesTurnScope;
|
|
92
89
|
private assertTurnScope;
|
|
93
90
|
private ackEventOrFile;
|
|
94
|
-
private acceptFileGrant;
|
|
95
91
|
private replaySpool;
|
|
96
92
|
private sendHeartbeat;
|
|
97
93
|
private sendCommandAck;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { ensureDaemonHome } from "./auth-store.js";
|
|
5
4
|
import { AGENT_SERVICE_WS_SCHEMA, AGENT_SERVICE_WS_SUBPROTOCOL, createSandboxFrame, parseSandboxFrame, UnsupportedSandboxProtocolError, } from "./agent-service-ws-protocol.js";
|
|
6
5
|
import { log as defaultLog } from "./log.js";
|
|
7
6
|
import { availableRunCapabilities } from "./runtime-capabilities.js";
|
|
8
7
|
import { activationRuntimeEnv, runtimeChildEnv } from "./runtime-env.js";
|
|
9
|
-
import {
|
|
8
|
+
import { redactSecretString } from "./redaction.js";
|
|
10
9
|
import { RunDispatcher, } from "./run-dispatcher.js";
|
|
11
10
|
import { ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, } from "./workspace.js";
|
|
12
11
|
import { WebSocketClient, } from "./websocket-client.js";
|
|
@@ -226,9 +225,7 @@ export class AgentServiceSandboxClient {
|
|
|
226
225
|
/** activation-scoped 模型短凭据;只保存在内存中,绝不进入 state.json。 */
|
|
227
226
|
activationContexts = new Map();
|
|
228
227
|
pendingAcks = new Map();
|
|
229
|
-
|
|
230
|
-
pendingFileCommits = new Map();
|
|
231
|
-
fileGrants = new Map();
|
|
228
|
+
pendingFileReports = new Map();
|
|
232
229
|
inflightCommands = new Set();
|
|
233
230
|
socket = null;
|
|
234
231
|
sandboxGeneration = 0;
|
|
@@ -332,10 +329,6 @@ export class AgentServiceSandboxClient {
|
|
|
332
329
|
this.turnScopes.delete(payload.agent_run_id);
|
|
333
330
|
if (this.currentTurnSessionId === scope.sessionId)
|
|
334
331
|
this.currentTurnSessionId = null;
|
|
335
|
-
for (const [key, grant] of this.fileGrants) {
|
|
336
|
-
if (this.sameTurnScope(scope, grant.scope))
|
|
337
|
-
this.fileGrants.delete(key);
|
|
338
|
-
}
|
|
339
332
|
this.persist();
|
|
340
333
|
}
|
|
341
334
|
completePendingActivationCleanup(sessionId) {
|
|
@@ -369,12 +362,6 @@ export class AgentServiceSandboxClient {
|
|
|
369
362
|
this.activeSessionId = null;
|
|
370
363
|
if (this.currentTurnSessionId === sessionId)
|
|
371
364
|
this.currentTurnSessionId = null;
|
|
372
|
-
for (const [key, grant] of this.fileGrants) {
|
|
373
|
-
if (grant.scope.sessionId === sessionId &&
|
|
374
|
-
grant.scope.workerAttempt === pending.workerAttempt &&
|
|
375
|
-
grant.scope.activationId === pending.activationId)
|
|
376
|
-
this.fileGrants.delete(key);
|
|
377
|
-
}
|
|
378
365
|
this.persist();
|
|
379
366
|
}
|
|
380
367
|
recoverPendingActivationCleanups() {
|
|
@@ -483,7 +470,7 @@ export class AgentServiceSandboxClient {
|
|
|
483
470
|
throw new Error("Agent Service sandbox file requires size_bytes and sha256");
|
|
484
471
|
}
|
|
485
472
|
const frame = createSandboxFrame({
|
|
486
|
-
type: "turn.file.
|
|
473
|
+
type: "turn.file.report",
|
|
487
474
|
sandboxId: this.options.sandboxId,
|
|
488
475
|
sandboxGeneration: this.sandboxGeneration,
|
|
489
476
|
connectionEpoch: this.connectionEpoch,
|
|
@@ -494,73 +481,15 @@ export class AgentServiceSandboxClient {
|
|
|
494
481
|
activationId: scope.activationId,
|
|
495
482
|
payload: file,
|
|
496
483
|
});
|
|
497
|
-
const
|
|
498
|
-
this.
|
|
484
|
+
const reported = new Promise((resolve, reject) => {
|
|
485
|
+
this.pendingFileReports.set(frame.frame_id, {
|
|
499
486
|
scope: { ...scope },
|
|
500
487
|
resolve,
|
|
501
488
|
reject,
|
|
502
489
|
});
|
|
503
490
|
});
|
|
504
491
|
await this.sendFrame(frame);
|
|
505
|
-
|
|
506
|
-
this.fileGrants.set(`${agentRunId}:${grant.record.id}`, grant);
|
|
507
|
-
return grant.record;
|
|
508
|
-
}
|
|
509
|
-
async uploadFileContent(agentRunId, fileId, absPath, mimeType) {
|
|
510
|
-
const scope = this.turnScopes.get(agentRunId);
|
|
511
|
-
if (!scope)
|
|
512
|
-
throw new Error("Agent Service sandbox file has no active turn scope");
|
|
513
|
-
const key = `${agentRunId}:${fileId}`;
|
|
514
|
-
const grant = this.fileGrants.get(key);
|
|
515
|
-
if (!grant)
|
|
516
|
-
throw new Error("Agent Service sandbox file has no upload grant");
|
|
517
|
-
if (!this.sameTurnScope(scope, grant.scope)) {
|
|
518
|
-
throw new Error("Agent Service sandbox file grant turn scope mismatch");
|
|
519
|
-
}
|
|
520
|
-
const data = await readFile(absPath);
|
|
521
|
-
assertNoInjectedCredentials(data, [
|
|
522
|
-
this.state.reconnectToken,
|
|
523
|
-
grant.uploadGrant,
|
|
524
|
-
...Object.values(this.activationContexts.get(scope.sessionId)?.runtimeEnv ?? {}),
|
|
525
|
-
]);
|
|
526
|
-
const response = await fetch(grant.uploadUrl, {
|
|
527
|
-
method: "PUT",
|
|
528
|
-
headers: {
|
|
529
|
-
authorization: `Bearer ${grant.uploadGrant}`,
|
|
530
|
-
"content-type": mimeType ?? grant.record.mime_type ?? "application/octet-stream",
|
|
531
|
-
},
|
|
532
|
-
body: data,
|
|
533
|
-
});
|
|
534
|
-
if (!response.ok) {
|
|
535
|
-
const text = await response.text().catch(() => "");
|
|
536
|
-
throw new Error(`Agent Service file upload failed (${response.status}): ${redactSecretString(text.slice(0, 500))}`);
|
|
537
|
-
}
|
|
538
|
-
const frame = createSandboxFrame({
|
|
539
|
-
type: "turn.file.committed",
|
|
540
|
-
sandboxId: this.options.sandboxId,
|
|
541
|
-
sandboxGeneration: this.sandboxGeneration,
|
|
542
|
-
connectionEpoch: this.connectionEpoch,
|
|
543
|
-
seq: this.nextOutboundSeq(),
|
|
544
|
-
runtimeSessionId: scope.sessionId,
|
|
545
|
-
agentRunId,
|
|
546
|
-
workerAttempt: scope.workerAttempt,
|
|
547
|
-
activationId: scope.activationId,
|
|
548
|
-
payload: { file_id: fileId, sha256: grant.record.sha256 },
|
|
549
|
-
});
|
|
550
|
-
const committed = new Promise((resolve, reject) => {
|
|
551
|
-
this.pendingFileCommits.set(frame.frame_id, {
|
|
552
|
-
scope: { ...scope },
|
|
553
|
-
resolve,
|
|
554
|
-
reject,
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
await this.sendFrame(frame);
|
|
558
|
-
try {
|
|
559
|
-
return await committed;
|
|
560
|
-
}
|
|
561
|
-
finally {
|
|
562
|
-
this.fileGrants.delete(key);
|
|
563
|
-
}
|
|
492
|
+
return await reported;
|
|
564
493
|
}
|
|
565
494
|
async getRunRuntimeProfile(agentRunId) {
|
|
566
495
|
const embedded = this.runProfiles.get(agentRunId);
|
|
@@ -724,9 +653,6 @@ export class AgentServiceSandboxClient {
|
|
|
724
653
|
case "event.ack":
|
|
725
654
|
this.ackEventOrFile(frame);
|
|
726
655
|
return;
|
|
727
|
-
case "turn.file.upload_grant":
|
|
728
|
-
this.acceptFileGrant(frame);
|
|
729
|
-
return;
|
|
730
656
|
case "ping":
|
|
731
657
|
await this.sendControlFrame("pong", { ping_frame_id: frame.frame_id });
|
|
732
658
|
return;
|
|
@@ -1229,13 +1155,13 @@ export class AgentServiceSandboxClient {
|
|
|
1229
1155
|
const frameId = frame.payload.frame_id;
|
|
1230
1156
|
if (typeof frameId !== "string")
|
|
1231
1157
|
throw new Error("event.ack has no frame_id");
|
|
1232
|
-
const pendingFile = this.
|
|
1158
|
+
const pendingFile = this.pendingFileReports.get(frameId);
|
|
1233
1159
|
if (pendingFile) {
|
|
1234
|
-
this.assertTurnScope(frame, pendingFile.scope, "file
|
|
1235
|
-
this.
|
|
1160
|
+
this.assertTurnScope(frame, pendingFile.scope, "file report ACK");
|
|
1161
|
+
this.pendingFileReports.delete(frameId);
|
|
1236
1162
|
const file = frame.payload.file;
|
|
1237
1163
|
if (!file || typeof file !== "object" || Array.isArray(file)) {
|
|
1238
|
-
pendingFile.reject(new Error("file
|
|
1164
|
+
pendingFile.reject(new Error("file report ACK has no file record"));
|
|
1239
1165
|
}
|
|
1240
1166
|
else {
|
|
1241
1167
|
pendingFile.resolve(file);
|
|
@@ -1264,35 +1190,6 @@ export class AgentServiceSandboxClient {
|
|
|
1264
1190
|
pending.resolve();
|
|
1265
1191
|
}
|
|
1266
1192
|
}
|
|
1267
|
-
acceptFileGrant(frame) {
|
|
1268
|
-
const requestFrameId = frame.payload.request_frame_id;
|
|
1269
|
-
if (typeof requestFrameId !== "string")
|
|
1270
|
-
throw new Error("file grant has no request_frame_id");
|
|
1271
|
-
const pending = this.pendingFilePrepares.get(requestFrameId);
|
|
1272
|
-
if (!pending)
|
|
1273
|
-
return;
|
|
1274
|
-
this.assertTurnScope(frame, pending.scope, "file upload grant");
|
|
1275
|
-
this.pendingFilePrepares.delete(requestFrameId);
|
|
1276
|
-
const file = frame.payload.file;
|
|
1277
|
-
const uploadUrl = frame.payload.upload_url;
|
|
1278
|
-
const uploadGrant = frame.payload.upload_grant;
|
|
1279
|
-
if (!file ||
|
|
1280
|
-
typeof file !== "object" ||
|
|
1281
|
-
Array.isArray(file) ||
|
|
1282
|
-
typeof uploadUrl !== "string" ||
|
|
1283
|
-
!uploadUrl ||
|
|
1284
|
-
typeof uploadGrant !== "string" ||
|
|
1285
|
-
!uploadGrant) {
|
|
1286
|
-
pending.reject(new Error("invalid Agent Service file upload grant"));
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
pending.resolve({
|
|
1290
|
-
scope: pending.scope,
|
|
1291
|
-
record: file,
|
|
1292
|
-
uploadUrl,
|
|
1293
|
-
uploadGrant,
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
1193
|
async replaySpool() {
|
|
1297
1194
|
for (const session of Object.values(this.state.sessions)) {
|
|
1298
1195
|
for (const original of session.spool) {
|
|
@@ -1386,13 +1283,9 @@ export class AgentServiceSandboxClient {
|
|
|
1386
1283
|
this.rejectPendingFiles(error);
|
|
1387
1284
|
}
|
|
1388
1285
|
rejectPendingFiles(error) {
|
|
1389
|
-
for (const pending of this.
|
|
1390
|
-
pending.reject(error);
|
|
1391
|
-
this.pendingFilePrepares.clear();
|
|
1392
|
-
for (const pending of this.pendingFileCommits.values())
|
|
1286
|
+
for (const pending of this.pendingFileReports.values())
|
|
1393
1287
|
pending.reject(error);
|
|
1394
|
-
this.
|
|
1395
|
-
this.fileGrants.clear();
|
|
1288
|
+
this.pendingFileReports.clear();
|
|
1396
1289
|
}
|
|
1397
1290
|
persist() {
|
|
1398
1291
|
saveState(this.options.sandboxId, this.state);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.2";
|
|
2
2
|
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v2";
|
|
3
|
-
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "
|
|
3
|
+
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "sandbox.drained" | "pong" | "protocol.error";
|
|
4
4
|
export declare class UnsupportedSandboxProtocolError extends Error {
|
|
5
5
|
readonly schemaVersion: unknown;
|
|
6
6
|
constructor(schemaVersion: unknown);
|
|
@@ -12,7 +12,6 @@ const FRAME_TYPES = new Set([
|
|
|
12
12
|
"sandbox.drain",
|
|
13
13
|
"sandbox.shutdown",
|
|
14
14
|
"event.ack",
|
|
15
|
-
"turn.file.upload_grant",
|
|
16
15
|
"auth.rotate",
|
|
17
16
|
"ping",
|
|
18
17
|
"sandbox.ready",
|
|
@@ -21,8 +20,7 @@ const FRAME_TYPES = new Set([
|
|
|
21
20
|
"session.opened",
|
|
22
21
|
"session.closed",
|
|
23
22
|
"turn.event",
|
|
24
|
-
"turn.file.
|
|
25
|
-
"turn.file.committed",
|
|
23
|
+
"turn.file.report",
|
|
26
24
|
"sandbox.drained",
|
|
27
25
|
"pong",
|
|
28
26
|
"protocol.error",
|
|
@@ -40,10 +38,8 @@ const TURN_TYPES = new Set([
|
|
|
40
38
|
"turn.start",
|
|
41
39
|
"turn.cancel",
|
|
42
40
|
"event.ack",
|
|
43
|
-
"turn.file.upload_grant",
|
|
44
41
|
"turn.event",
|
|
45
|
-
"turn.file.
|
|
46
|
-
"turn.file.committed",
|
|
42
|
+
"turn.file.report",
|
|
47
43
|
]);
|
|
48
44
|
const FRAME_KEYS = new Set([
|
|
49
45
|
"schema_version",
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import type { Logger } from "./log.js";
|
|
2
|
-
import type { RunFileCandidate
|
|
2
|
+
import type { RunFileCandidate } from "./types.js";
|
|
3
3
|
export interface ScanLimits {
|
|
4
4
|
maxFiles?: number;
|
|
5
5
|
maxFileBytes?: number;
|
|
6
6
|
maxPreviewChars?: number;
|
|
7
7
|
maxDepth?: number;
|
|
8
|
+
maxReportMs?: number;
|
|
8
9
|
}
|
|
9
10
|
export interface ScannedFile extends RunFileCandidate {
|
|
10
11
|
absPath: string;
|
|
11
12
|
}
|
|
12
13
|
export interface FileReportingClient {
|
|
13
|
-
postFile(agentRunId: string, file: RunFileCandidate): Promise<
|
|
14
|
-
uploadFileContent?(agentRunId: string, fileId: string, absPath: string, mimeType?: string): Promise<RunFileRecord>;
|
|
14
|
+
postFile(agentRunId: string, file: RunFileCandidate): Promise<unknown>;
|
|
15
15
|
}
|
|
16
16
|
export interface FileReportResult {
|
|
17
17
|
reported: number;
|
|
18
|
-
uploaded: number;
|
|
19
18
|
failed: number;
|
|
20
19
|
truncated: boolean;
|
|
21
20
|
}
|
package/dist/file-candidates.js
CHANGED
|
@@ -9,6 +9,7 @@ const DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
|
9
9
|
// 与后端 MAX_PREVIEW_CHARS 一致。
|
|
10
10
|
const DEFAULT_MAX_PREVIEW_CHARS = 4000;
|
|
11
11
|
const DEFAULT_MAX_DEPTH = 8;
|
|
12
|
+
const DEFAULT_MAX_REPORT_MS = 3_000;
|
|
12
13
|
// 文本启发式:前 4KiB 不含 NUL 字节即视为文本,可取 preview。
|
|
13
14
|
const TEXT_SNIFF_BYTES = 4096;
|
|
14
15
|
// 与后端 _is_safe_relative_path 同规:非空、非绝对、无反斜杠、无 .. 段、无空段。
|
|
@@ -147,18 +148,24 @@ export async function reportFileCandidates(client, agentRunId, workspaceDir, log
|
|
|
147
148
|
log.warn("workspace file scan truncated", { agentRunId, reported: files.length });
|
|
148
149
|
}
|
|
149
150
|
let reported = 0;
|
|
150
|
-
let uploaded = 0;
|
|
151
151
|
let failed = 0;
|
|
152
|
-
|
|
152
|
+
const reportDeadline = Date.now() + (limits?.maxReportMs ?? DEFAULT_MAX_REPORT_MS);
|
|
153
|
+
for (const { absPath: _absPath, ...candidate } of files) {
|
|
154
|
+
const remainingMs = reportDeadline - Date.now();
|
|
155
|
+
if (remainingMs <= 0) {
|
|
156
|
+
failed += files.length - reported - failed;
|
|
157
|
+
log.warn("workspace file metadata report timed out", { agentRunId, reported });
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
let timeout;
|
|
153
161
|
try {
|
|
154
|
-
|
|
162
|
+
await Promise.race([
|
|
163
|
+
client.postFile(agentRunId, candidate),
|
|
164
|
+
new Promise((_resolve, reject) => {
|
|
165
|
+
timeout = setTimeout(() => reject(new Error("workspace file metadata report timed out")), remainingMs);
|
|
166
|
+
}),
|
|
167
|
+
]);
|
|
155
168
|
reported += 1;
|
|
156
|
-
if (client.uploadFileContent) {
|
|
157
|
-
if (!record)
|
|
158
|
-
throw new Error("file candidate response is missing its id");
|
|
159
|
-
await client.uploadFileContent(agentRunId, record.id, absPath, candidate.mime_type);
|
|
160
|
-
uploaded += 1;
|
|
161
|
-
}
|
|
162
169
|
}
|
|
163
170
|
catch (err) {
|
|
164
171
|
failed += 1;
|
|
@@ -168,6 +175,10 @@ export async function reportFileCandidates(client, agentRunId, workspaceDir, log
|
|
|
168
175
|
error: err instanceof Error ? err.message : String(err),
|
|
169
176
|
});
|
|
170
177
|
}
|
|
178
|
+
finally {
|
|
179
|
+
if (timeout !== undefined)
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
}
|
|
171
182
|
}
|
|
172
|
-
return { reported,
|
|
183
|
+
return { reported, failed, truncated };
|
|
173
184
|
}
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ScanLimits } from "./file-candidates.js";
|
|
2
2
|
import { type Logger } from "./log.js";
|
|
3
|
-
import { type CourseRuntimeProfile, type CourseRuntime, type RunEvent, type
|
|
3
|
+
import { type CourseRuntimeProfile, type CourseRuntime, type RunEvent, type RunStartPayload } from "./types.js";
|
|
4
4
|
export interface RunDispatcherOptions {
|
|
5
5
|
defaultRuntimeId?: string;
|
|
6
6
|
log?: Logger;
|
|
@@ -22,8 +22,7 @@ export interface PersistentSessionExecution {
|
|
|
22
22
|
}
|
|
23
23
|
export interface RunReportingClient {
|
|
24
24
|
postEvent(agentRunId: string, event: RunEvent): Promise<void>;
|
|
25
|
-
postFile(agentRunId: string, file: import("./types.js").RunFileCandidate): Promise<
|
|
26
|
-
uploadFileContent?(agentRunId: string, fileId: string, absPath: string, mimeType?: string): Promise<RunFileRecord>;
|
|
25
|
+
postFile(agentRunId: string, file: import("./types.js").RunFileCandidate): Promise<unknown>;
|
|
27
26
|
getRunRuntimeProfile?(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
28
27
|
}
|
|
29
28
|
/**
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -183,8 +183,8 @@ export class RunDispatcher {
|
|
|
183
183
|
let runtimeUsage = {};
|
|
184
184
|
let modelStartedAt;
|
|
185
185
|
let modelFinishedAt;
|
|
186
|
-
let
|
|
187
|
-
let
|
|
186
|
+
let fileReportStartedAt;
|
|
187
|
+
let fileReportFinishedAt;
|
|
188
188
|
let progressEvents = 0;
|
|
189
189
|
let transcript;
|
|
190
190
|
let lastProgressKey = null;
|
|
@@ -219,9 +219,9 @@ export class RunDispatcher {
|
|
|
219
219
|
...(modelStartedAt !== undefined && modelFinishedAt !== undefined
|
|
220
220
|
? { model_wall_time_ms: Math.max(0, modelFinishedAt - modelStartedAt) }
|
|
221
221
|
: {}),
|
|
222
|
-
...(
|
|
222
|
+
...(fileReportStartedAt !== undefined && fileReportFinishedAt !== undefined
|
|
223
223
|
? {
|
|
224
|
-
|
|
224
|
+
file_report_time_ms: Math.max(0, fileReportFinishedAt - fileReportStartedAt),
|
|
225
225
|
}
|
|
226
226
|
: {}),
|
|
227
227
|
};
|
|
@@ -658,18 +658,12 @@ export class RunDispatcher {
|
|
|
658
658
|
: DEFAULT_MAX_OUTPUT_CHARS;
|
|
659
659
|
const output = redactSecretString(truncateText(finalText, maxOutputChars));
|
|
660
660
|
activeTranscript.writeFinal(output);
|
|
661
|
-
|
|
662
|
-
let fileReport;
|
|
661
|
+
fileReportStartedAt = this.now();
|
|
663
662
|
try {
|
|
664
|
-
|
|
663
|
+
await reportFileCandidates(this.client, runId, workspaceDir, this.log, this.scanLimits);
|
|
665
664
|
}
|
|
666
665
|
finally {
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
if (this.client.uploadFileContent && (fileReport.failed > 0 || fileReport.truncated)) {
|
|
670
|
-
throw new RuntimeExecutionError(fileReport.truncated
|
|
671
|
-
? "workspace file collection was truncated"
|
|
672
|
-
: `failed to persist ${fileReport.failed} workspace file(s)`);
|
|
666
|
+
fileReportFinishedAt = this.now();
|
|
673
667
|
}
|
|
674
668
|
await send({ type: "run.message", role: "assistant", text: output });
|
|
675
669
|
await sendTerminal({
|
package/dist/types.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface RunEvent {
|
|
|
58
58
|
}
|
|
59
59
|
/** `POST /daemon/runs/{id}/files` 的文件候选(与后端 DaemonRunFileIn 一致)。 */
|
|
60
60
|
export interface RunFileCandidate {
|
|
61
|
-
event?: "created" | "modified" | "deleted"
|
|
61
|
+
event?: "created" | "modified" | "deleted";
|
|
62
62
|
/** workspace 相对路径,正斜杠;不得包含 `..`、绝对路径或反斜杠(后端 400)。 */
|
|
63
63
|
path: string;
|
|
64
64
|
name?: string;
|
package/package.json
CHANGED