@botlearn-course/daemon 0.0.19 → 0.0.20-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-service-sandbox.d.ts +9 -1
- package/dist/agent-service-sandbox.js +490 -16
- package/dist/agent-service-ws-protocol.d.ts +3 -3
- package/dist/agent-service-ws-protocol.js +6 -2
- package/dist/cli.js +19 -1
- package/dist/file-candidates.d.ts +28 -1
- package/dist/file-candidates.js +57 -11
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/run-dispatcher.d.ts +3 -2
- package/dist/run-dispatcher.js +62 -5
- package/dist/runtime-env.js +4 -4
- package/dist/runtime-quiescence.d.ts +16 -0
- package/dist/runtime-quiescence.js +42 -0
- package/dist/runtimes/engine.js +1 -1
- package/dist/tool-observation.d.ts +7 -4
- package/dist/tool-observation.js +40 -18
- package/dist/trace-projection.d.ts +21 -0
- package/dist/trace-projection.js +56 -0
- package/dist/types.d.ts +1 -1
- package/dist/workspace-entry-set.d.ts +31 -0
- package/dist/workspace-entry-set.js +164 -0
- package/dist/workspace-materialization.d.ts +16 -0
- package/dist/workspace-materialization.js +136 -0
- package/dist/workspace-quota.d.ts +4 -0
- package/dist/workspace-quota.js +42 -0
- package/dist/workspace-restore.d.ts +42 -0
- package/dist/workspace-restore.js +347 -0
- package/dist/workspace-snapshot-control.d.ts +29 -0
- package/dist/workspace-snapshot-control.js +169 -0
- package/dist/workspace-snapshot-policy.d.ts +24 -0
- package/dist/workspace-snapshot-policy.js +45 -0
- package/dist/workspace-snapshot-staging.d.ts +27 -0
- package/dist/workspace-snapshot-staging.js +275 -0
- package/dist/workspace.d.ts +56 -0
- package/dist/workspace.js +553 -1
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, createWriteStream, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { ensureDaemonHome } from "./auth-store.js";
|
|
4
7
|
import { AGENT_SERVICE_WS_SCHEMA, AGENT_SERVICE_WS_SUBPROTOCOL, createSandboxFrame, createWorkspaceFileChunk, parseSandboxFrame, UnsupportedSandboxProtocolError, WORKSPACE_FILE_READ_BINARY_CAPABILITY, } from "./agent-service-ws-protocol.js";
|
|
@@ -8,8 +11,16 @@ import { activationRuntimeEnv, runtimeChildEnv } from "./runtime-env.js";
|
|
|
8
11
|
import { redactSecretString } from "./redaction.js";
|
|
9
12
|
import { parseRuntimeSkillProviderGrantSet, prepareRuntimeSkillProvider, RuntimeSkillProviderError, } from "./runtime-skills.js";
|
|
10
13
|
import { RunDispatcher, } from "./run-dispatcher.js";
|
|
11
|
-
import { ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, runtimeSessionWorkspaceDir, } from "./workspace.js";
|
|
14
|
+
import { cleanupRuntimeSessionWorkspaceCopyStaging, copyRuntimeSessionWorkspace, ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, finalizeRuntimeSessionWorkspaceCopy, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, runtimeSessionWorkspaceDir, WORKSPACE_COPY_POLICY_V1, WorkspaceCopyError, workspaceCopyV1Supported, } from "./workspace.js";
|
|
12
15
|
import { readWorkspaceFile, WorkspaceFileReadError } from "./workspace-file-read.js";
|
|
16
|
+
import { checkpointWorkspace, WorkspaceSnapshotControlError, } from "./workspace-snapshot-control.js";
|
|
17
|
+
import { issueWorkspaceQuiescenceProof, quiesceRuntimeWriters, } from "./runtime-quiescence.js";
|
|
18
|
+
import { WorkspaceSnapshotStagingError } from "./workspace-snapshot-staging.js";
|
|
19
|
+
import { validateWorkspaceEntrySet, WorkspaceEntrySetError, } from "./workspace-entry-set.js";
|
|
20
|
+
import { WORKSPACE_SNAPSHOT_POLICY_V1, workspaceSnapshotPolicyCapability, } from "./workspace-snapshot-policy.js";
|
|
21
|
+
import { assertWorkspaceRestoreCapacity, restoreWorkspaceSnapshot, WorkspaceRestoreError, } from "./workspace-restore.js";
|
|
22
|
+
import { readWorkspaceMaterializationMarker } from "./workspace-materialization.js";
|
|
23
|
+
import { applyRuntimeWorkspaceQuota, assertRuntimeWorkspaceQuota } from "./workspace-quota.js";
|
|
13
24
|
import { WebSocketClient, } from "./websocket-client.js";
|
|
14
25
|
const RECONNECT_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
|
|
15
26
|
const MAX_SPOOL_FRAMES = 1024;
|
|
@@ -19,6 +30,14 @@ const LEGACY_EVENT_ACK_WINDOW = 1;
|
|
|
19
30
|
const INPUT_ATTACHMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
20
31
|
/** 出站 seq 基址:seq = connection_epoch * SEQ_EPOCH_BASE + n,跨重连单调(合同 §1.1)。 */
|
|
21
32
|
const SEQ_EPOCH_BASE = 1_000_000_000;
|
|
33
|
+
function agentServiceHttpBase(wsUrl) {
|
|
34
|
+
const url = new URL(wsUrl);
|
|
35
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
36
|
+
url.pathname = "/internal/v1";
|
|
37
|
+
url.search = "";
|
|
38
|
+
url.hash = "";
|
|
39
|
+
return url.toString().replace(/\/$/, "");
|
|
40
|
+
}
|
|
22
41
|
const RUNTIME_LOG_WINDOW_MS = 60_000;
|
|
23
42
|
const WORKSPACE_FILE_TRANSFER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
24
43
|
const DISABLED_RUNTIME_LOG_POLICY = {
|
|
@@ -212,6 +231,7 @@ function emptySessionState() {
|
|
|
212
231
|
return {
|
|
213
232
|
courseRunId: null,
|
|
214
233
|
workspaceRef: null,
|
|
234
|
+
workspaceCopyReceipt: null,
|
|
215
235
|
runtimeId: null,
|
|
216
236
|
nativeSessionId: null,
|
|
217
237
|
contextRevision: 0,
|
|
@@ -230,6 +250,7 @@ function normalizeSessionState(value) {
|
|
|
230
250
|
return {
|
|
231
251
|
courseRunId: typeof value.courseRunId === "string" ? value.courseRunId : null,
|
|
232
252
|
workspaceRef: typeof value.workspaceRef === "string" ? value.workspaceRef : null,
|
|
253
|
+
workspaceCopyReceipt: (value.workspaceCopyReceipt && typeof value.workspaceCopyReceipt === "object") ? value.workspaceCopyReceipt : null,
|
|
233
254
|
runtimeId: typeof value.runtimeId === "string" ? value.runtimeId : null,
|
|
234
255
|
nativeSessionId: typeof value.nativeSessionId === "string" ? value.nativeSessionId : null,
|
|
235
256
|
contextRevision: Number(value.contextRevision ?? 0),
|
|
@@ -338,12 +359,15 @@ export class AgentServiceSandboxClient {
|
|
|
338
359
|
dispatcher;
|
|
339
360
|
/** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
|
|
340
361
|
turnScopes = new Map();
|
|
362
|
+
turnWorkspaceCheckpoints = new Map();
|
|
363
|
+
completedWorkspaceCheckpoints = new Set();
|
|
341
364
|
/** 每 run 内嵌 profile(turn payload 自带时优先)。 */
|
|
342
365
|
runProfiles = new Map();
|
|
343
366
|
/** activate 时装配的 per-session profile,turn.start 复用。 */
|
|
344
367
|
sessionProfiles = new Map();
|
|
345
368
|
/** activation-scoped 模型短凭据;只保存在内存中,绝不进入 state.json。 */
|
|
346
369
|
activationContexts = new Map();
|
|
370
|
+
activationCleanupTasks = new Map();
|
|
347
371
|
pendingAcks = new Map();
|
|
348
372
|
pendingFileReports = new Map();
|
|
349
373
|
inflightCommands = new Set();
|
|
@@ -436,7 +460,7 @@ export class AgentServiceSandboxClient {
|
|
|
436
460
|
session.nativeSessionId = nativeSessionId.trim() || null;
|
|
437
461
|
this.persist();
|
|
438
462
|
}
|
|
439
|
-
finishTurn(payload) {
|
|
463
|
+
async finishTurn(payload) {
|
|
440
464
|
const scope = this.turnScopes.get(payload.agent_run_id);
|
|
441
465
|
if (!scope)
|
|
442
466
|
return;
|
|
@@ -452,7 +476,7 @@ export class AgentServiceSandboxClient {
|
|
|
452
476
|
};
|
|
453
477
|
this.persist();
|
|
454
478
|
try {
|
|
455
|
-
this.completePendingActivationCleanup(scope.sessionId);
|
|
479
|
+
await this.completePendingActivationCleanup(scope.sessionId);
|
|
456
480
|
}
|
|
457
481
|
catch (error) {
|
|
458
482
|
this.log.error("failed to revoke a terminal runtime session workspace", {
|
|
@@ -470,11 +494,27 @@ export class AgentServiceSandboxClient {
|
|
|
470
494
|
this.currentTurnSessionId = null;
|
|
471
495
|
this.persist();
|
|
472
496
|
}
|
|
473
|
-
completePendingActivationCleanup(sessionId) {
|
|
497
|
+
async completePendingActivationCleanup(sessionId) {
|
|
498
|
+
const existing = this.activationCleanupTasks.get(sessionId);
|
|
499
|
+
if (existing)
|
|
500
|
+
return await existing;
|
|
501
|
+
const task = this.runPendingActivationCleanup(sessionId);
|
|
502
|
+
this.activationCleanupTasks.set(sessionId, task);
|
|
503
|
+
try {
|
|
504
|
+
await task;
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
if (this.activationCleanupTasks.get(sessionId) === task) {
|
|
508
|
+
this.activationCleanupTasks.delete(sessionId);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async runPendingActivationCleanup(sessionId) {
|
|
474
513
|
const session = this.state.sessions[sessionId];
|
|
475
514
|
const pending = session?.pendingActivationCleanup;
|
|
476
515
|
if (!session || !pending)
|
|
477
516
|
return;
|
|
517
|
+
await quiesceRuntimeWriters(pending.activationId);
|
|
478
518
|
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
479
519
|
const activation = this.activationContexts.get(sessionId);
|
|
480
520
|
if (activation?.activationId === pending.activationId) {
|
|
@@ -503,9 +543,9 @@ export class AgentServiceSandboxClient {
|
|
|
503
543
|
this.currentTurnSessionId = null;
|
|
504
544
|
this.persist();
|
|
505
545
|
}
|
|
506
|
-
recoverPendingActivationCleanups() {
|
|
546
|
+
async recoverPendingActivationCleanups() {
|
|
507
547
|
for (const sessionId of Object.keys(this.state.sessions)) {
|
|
508
|
-
this.completePendingActivationCleanup(sessionId);
|
|
548
|
+
await this.completePendingActivationCleanup(sessionId);
|
|
509
549
|
}
|
|
510
550
|
}
|
|
511
551
|
async run() {
|
|
@@ -566,6 +606,11 @@ export class AgentServiceSandboxClient {
|
|
|
566
606
|
this.removeOperationalLogSink();
|
|
567
607
|
this.socket?.close(1000, "daemon_stopping");
|
|
568
608
|
}
|
|
609
|
+
async stopGracefully() {
|
|
610
|
+
this.dispatcher.cancelAll();
|
|
611
|
+
await quiesceRuntimeWriters(null);
|
|
612
|
+
this.stop();
|
|
613
|
+
}
|
|
569
614
|
async postEvent(agentRunId, event) {
|
|
570
615
|
const scope = this.turnScopes.get(agentRunId);
|
|
571
616
|
if (!scope)
|
|
@@ -576,6 +621,9 @@ export class AgentServiceSandboxClient {
|
|
|
576
621
|
if (!this.sandboxGeneration || !this.connectionEpoch) {
|
|
577
622
|
throw new Error("Agent Service sandbox is not authenticated");
|
|
578
623
|
}
|
|
624
|
+
// Terminal truth is never delayed by object storage. Course Service keeps the
|
|
625
|
+
// checkpoint obligation pending and, after this turn is ACKed and fully quiesced,
|
|
626
|
+
// re-delivers it as a session-scoped workspace.checkpoint command.
|
|
579
627
|
if (event.type === "run.block") {
|
|
580
628
|
await this.waitForEventCapacity();
|
|
581
629
|
}
|
|
@@ -655,6 +703,29 @@ export class AgentServiceSandboxClient {
|
|
|
655
703
|
await this.sendFrame(frame);
|
|
656
704
|
return await reported;
|
|
657
705
|
}
|
|
706
|
+
async recordWorkspaceScanMeasurement(agentRunId, measurement) {
|
|
707
|
+
if (!this.turnScopes.has(agentRunId)) {
|
|
708
|
+
throw new Error("Agent Service sandbox scan measurement has no active turn scope");
|
|
709
|
+
}
|
|
710
|
+
await this.sendFrame(createSandboxFrame({
|
|
711
|
+
type: "sandbox.log",
|
|
712
|
+
sandboxId: this.options.sandboxId,
|
|
713
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
714
|
+
connectionEpoch: this.connectionEpoch,
|
|
715
|
+
seq: this.nextOutboundSeq(),
|
|
716
|
+
payload: {
|
|
717
|
+
level: "info",
|
|
718
|
+
stream: "daemon",
|
|
719
|
+
message: "workspace.scan.measurement",
|
|
720
|
+
fields: {
|
|
721
|
+
file_count: measurement.fileCount,
|
|
722
|
+
total_bytes: measurement.totalBytes,
|
|
723
|
+
duration_ms: measurement.durationMs,
|
|
724
|
+
result: measurement.truncated ? "truncated" : "complete",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
658
729
|
async getRunRuntimeProfile(agentRunId) {
|
|
659
730
|
const embedded = this.runProfiles.get(agentRunId);
|
|
660
731
|
if (embedded)
|
|
@@ -744,6 +815,10 @@ export class AgentServiceSandboxClient {
|
|
|
744
815
|
}
|
|
745
816
|
}
|
|
746
817
|
async handleHello(frame) {
|
|
818
|
+
// A reconnect can race a copy that started on the previous socket. Wait for
|
|
819
|
+
// the serialized lifecycle attempt to converge before deleting crash-left
|
|
820
|
+
// staging and advertising capability on the new connection.
|
|
821
|
+
await this.lifecycleChain;
|
|
747
822
|
if (frame.sandbox_id !== this.options.sandboxId) {
|
|
748
823
|
throw new SandboxClosedError(4403, "sandbox_mismatch");
|
|
749
824
|
}
|
|
@@ -753,6 +828,7 @@ export class AgentServiceSandboxClient {
|
|
|
753
828
|
if (frame.sandbox_generation !== this.state.sandboxGeneration) {
|
|
754
829
|
// Generation change wipes every session shard (contract §1.4).
|
|
755
830
|
this.dispatcher.cancelAll();
|
|
831
|
+
await quiesceRuntimeWriters(null);
|
|
756
832
|
this.rejectPending(new Error("Agent Service sandbox generation changed"));
|
|
757
833
|
for (const sessionId of Object.keys(this.state.sessions)) {
|
|
758
834
|
revokeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration);
|
|
@@ -771,7 +847,7 @@ export class AgentServiceSandboxClient {
|
|
|
771
847
|
this.connectionEpoch = frame.connection_epoch;
|
|
772
848
|
this.inboundSeq = frame.seq;
|
|
773
849
|
this.outboundSeq = frame.connection_epoch * SEQ_EPOCH_BASE;
|
|
774
|
-
this.recoverPendingActivationCleanups();
|
|
850
|
+
await this.recoverPendingActivationCleanups();
|
|
775
851
|
const heartbeatSeconds = Number(frame.payload.heartbeat_seconds ?? 15);
|
|
776
852
|
this.heartbeatMs = Math.max(1_000, Math.min(60_000, heartbeatSeconds * 1000));
|
|
777
853
|
const staleSeconds = Number(frame.payload.stale_after_seconds ?? 45);
|
|
@@ -784,10 +860,18 @@ export class AgentServiceSandboxClient {
|
|
|
784
860
|
: LEGACY_EVENT_ACK_WINDOW;
|
|
785
861
|
this.maxUnackedEvents = Math.min(MAX_EVENT_ACK_WINDOW, ackWindow);
|
|
786
862
|
this.runtimeLogPolicy = runtimeLogPolicy(frame.payload.runtime_log_policy);
|
|
863
|
+
await cleanupRuntimeSessionWorkspaceCopyStaging();
|
|
787
864
|
this.persist();
|
|
865
|
+
assertRuntimeWorkspaceQuota(process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT ?? "");
|
|
788
866
|
await this.sendControlFrame("sandbox.ready", {
|
|
789
867
|
protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
|
|
790
|
-
capabilities: [
|
|
868
|
+
capabilities: [
|
|
869
|
+
WORKSPACE_FILE_READ_BINARY_CAPABILITY,
|
|
870
|
+
"workspace_snapshot_v1",
|
|
871
|
+
"workspace_restore_v1",
|
|
872
|
+
...(workspaceCopyV1Supported() ? ["workspace_copy_v1"] : []),
|
|
873
|
+
],
|
|
874
|
+
workspace_snapshot_policy: workspaceSnapshotPolicyCapability(),
|
|
791
875
|
daemon_version: this.options.daemonVersion,
|
|
792
876
|
runtime_versions: {
|
|
793
877
|
course_daemon: this.options.daemonVersion,
|
|
@@ -919,8 +1003,11 @@ export class AgentServiceSandboxClient {
|
|
|
919
1003
|
});
|
|
920
1004
|
});
|
|
921
1005
|
return;
|
|
1006
|
+
case "workspace.checkpoint":
|
|
1007
|
+
this.scheduleLifecycle("workspace.checkpoint", () => this.handleWorkspaceCheckpoint(frame));
|
|
1008
|
+
return;
|
|
922
1009
|
case "sandbox.shutdown":
|
|
923
|
-
this.
|
|
1010
|
+
await this.stopGracefully();
|
|
924
1011
|
return;
|
|
925
1012
|
case "sandbox.drain":
|
|
926
1013
|
await this.sendCommandAck(frame, "ok");
|
|
@@ -947,7 +1034,7 @@ export class AgentServiceSandboxClient {
|
|
|
947
1034
|
}
|
|
948
1035
|
const state = frame.payload.state;
|
|
949
1036
|
if (state === "shutdown") {
|
|
950
|
-
this.
|
|
1037
|
+
await this.stopGracefully();
|
|
951
1038
|
return;
|
|
952
1039
|
}
|
|
953
1040
|
if (state === "drain") {
|
|
@@ -964,11 +1051,33 @@ export class AgentServiceSandboxClient {
|
|
|
964
1051
|
const session = this.state.sessions[sessionId] ?? emptySessionState();
|
|
965
1052
|
const courseRunId = frame.payload.course_run_id;
|
|
966
1053
|
const requestedWorkspaceRef = frame.payload.workspace_ref;
|
|
1054
|
+
const requestedCopy = frame.payload.workspace_copy;
|
|
1055
|
+
const requestedRestore = frame.payload.workspace_restore;
|
|
1056
|
+
const requestedCopyRecord = requestedCopy && typeof requestedCopy === "object"
|
|
1057
|
+
? requestedCopy
|
|
1058
|
+
: undefined;
|
|
1059
|
+
const copyId = requestedCopyRecord
|
|
1060
|
+
? requestedCopyRecord.copy_id
|
|
1061
|
+
: undefined;
|
|
1062
|
+
const requestedCopySource = requestedCopyRecord
|
|
1063
|
+
? requestedCopyRecord.source_runtime_session_id
|
|
1064
|
+
: undefined;
|
|
967
1065
|
if (typeof courseRunId !== "string" ||
|
|
968
1066
|
courseRunId.length < 1 ||
|
|
969
1067
|
(requestedWorkspaceRef !== null &&
|
|
970
1068
|
requestedWorkspaceRef !== undefined &&
|
|
971
|
-
(typeof requestedWorkspaceRef !== "string" || requestedWorkspaceRef.length < 1))
|
|
1069
|
+
(typeof requestedWorkspaceRef !== "string" || requestedWorkspaceRef.length < 1)) ||
|
|
1070
|
+
(requestedCopy !== null &&
|
|
1071
|
+
requestedCopy !== undefined &&
|
|
1072
|
+
(typeof requestedCopy !== "object" ||
|
|
1073
|
+
typeof copyId !== "string" ||
|
|
1074
|
+
copyId.length < 1 ||
|
|
1075
|
+
typeof requestedCopySource !== "string" ||
|
|
1076
|
+
requestedCopySource.length < 1))
|
|
1077
|
+
|| (requestedRestore !== null && requestedRestore !== undefined &&
|
|
1078
|
+
(typeof requestedRestore !== "object" || Array.isArray(requestedRestore)))
|
|
1079
|
+
|| (requestedCopy !== null && requestedCopy !== undefined &&
|
|
1080
|
+
requestedRestore !== null && requestedRestore !== undefined)) {
|
|
972
1081
|
throw new Error("invalid session.open payload");
|
|
973
1082
|
}
|
|
974
1083
|
if (session.courseRunId !== null && session.courseRunId !== courseRunId) {
|
|
@@ -979,18 +1088,365 @@ export class AgentServiceSandboxClient {
|
|
|
979
1088
|
session.workspaceRef !== requestedWorkspaceRef) {
|
|
980
1089
|
throw new Error("runtime session workspace_ref cannot be rebound");
|
|
981
1090
|
}
|
|
1091
|
+
const isNewSession = session.courseRunId === null;
|
|
1092
|
+
let markerPath = null;
|
|
1093
|
+
try {
|
|
1094
|
+
if (requestedRestore && typeof requestedRestore === "object") {
|
|
1095
|
+
const raw = requestedRestore;
|
|
1096
|
+
const descriptorRaw = raw.descriptor;
|
|
1097
|
+
const controlFence = raw.control_fence;
|
|
1098
|
+
let restoreControlToken = raw.control_token;
|
|
1099
|
+
if (!controlFence || typeof controlFence !== "object" || Array.isArray(controlFence)) {
|
|
1100
|
+
throw new Error("invalid workspace restore control fence");
|
|
1101
|
+
}
|
|
1102
|
+
if (typeof restoreControlToken !== "string" || restoreControlToken.length < 32) {
|
|
1103
|
+
throw new Error("invalid workspace restore control token");
|
|
1104
|
+
}
|
|
1105
|
+
if (!descriptorRaw || !Array.isArray(raw.entries)) {
|
|
1106
|
+
throw new Error("invalid workspace restore payload");
|
|
1107
|
+
}
|
|
1108
|
+
const descriptor = {
|
|
1109
|
+
sandboxId: this.options.sandboxId,
|
|
1110
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
1111
|
+
runtimeSessionId: sessionId,
|
|
1112
|
+
continuityState: descriptorRaw.continuity_state,
|
|
1113
|
+
continuityErrorCode: descriptorRaw.continuity_error_code,
|
|
1114
|
+
snapshotId: descriptorRaw.snapshot_id,
|
|
1115
|
+
revision: Number(descriptorRaw.revision),
|
|
1116
|
+
contentSha256: descriptorRaw.content_sha256,
|
|
1117
|
+
expectedFileCount: Number(descriptorRaw.expected_file_count),
|
|
1118
|
+
expectedEntryCount: Number(descriptorRaw.expected_entry_count),
|
|
1119
|
+
expectedTotalBytes: Number(descriptorRaw.expected_total_bytes),
|
|
1120
|
+
};
|
|
1121
|
+
const workspaceDirectory = runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration);
|
|
1122
|
+
const requiredFreeBytes = Math.max(WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes, descriptor.expectedTotalBytes + WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes);
|
|
1123
|
+
// Fail before issuing or consuming any object grant. The download staging is a
|
|
1124
|
+
// sibling on this same bounded backing filesystem and is renamed, not copied,
|
|
1125
|
+
// into the atomic restore staging tree.
|
|
1126
|
+
assertWorkspaceRestoreCapacity({
|
|
1127
|
+
workspaceDirectory,
|
|
1128
|
+
descriptor,
|
|
1129
|
+
requiredFreeBytes,
|
|
1130
|
+
entryCopies: 2,
|
|
1131
|
+
});
|
|
1132
|
+
const entries = [];
|
|
1133
|
+
const predownloadRoot = path.join(path.dirname(workspaceDirectory), `.botlearn-predownload-${sessionId}-${randomUUID()}`);
|
|
1134
|
+
mkdirSync(predownloadRoot, { recursive: true, mode: 0o700 });
|
|
1135
|
+
try {
|
|
1136
|
+
let nextCursor = null;
|
|
1137
|
+
do {
|
|
1138
|
+
if (nextCursor !== null && nextCursor.length > 2048) {
|
|
1139
|
+
throw new Error("invalid workspace restore cursor");
|
|
1140
|
+
}
|
|
1141
|
+
const pageSignal = AbortSignal.timeout(60_000);
|
|
1142
|
+
const response = await fetch(`${agentServiceHttpBase(this.options.wsUrl)}/runtime-sessions/${sessionId}/restore-grants`, {
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
headers: {
|
|
1145
|
+
Authorization: `Bearer ${restoreControlToken}`,
|
|
1146
|
+
"Content-Type": "application/json",
|
|
1147
|
+
},
|
|
1148
|
+
body: JSON.stringify({
|
|
1149
|
+
policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId,
|
|
1150
|
+
fence: controlFence,
|
|
1151
|
+
cursor: nextCursor,
|
|
1152
|
+
// Fetch one canonical entry at a time so a grant is consumed as soon as
|
|
1153
|
+
// it is signed, including deployments using the minimum 60-second TTL.
|
|
1154
|
+
max_entries: 1,
|
|
1155
|
+
}),
|
|
1156
|
+
signal: pageSignal,
|
|
1157
|
+
});
|
|
1158
|
+
if (!response.ok) {
|
|
1159
|
+
let code = "workspace_restore_unavailable";
|
|
1160
|
+
let retryable = response.status >= 500 || response.status === 408 ||
|
|
1161
|
+
response.status === 429;
|
|
1162
|
+
try {
|
|
1163
|
+
const rejected = await response.json();
|
|
1164
|
+
const detail = rejected.detail;
|
|
1165
|
+
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
|
|
1166
|
+
const stableCode = detail.code;
|
|
1167
|
+
if (typeof stableCode === "string" && stableCode.length <= 120) {
|
|
1168
|
+
code = stableCode;
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof detail.retryable === "boolean") {
|
|
1171
|
+
retryable = detail.retryable;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
// Content-free fallback retains the stable local failure code.
|
|
1177
|
+
}
|
|
1178
|
+
throw new WorkspaceRestoreError(code, retryable);
|
|
1179
|
+
}
|
|
1180
|
+
const page = await response.json();
|
|
1181
|
+
if (!Array.isArray(page.entries))
|
|
1182
|
+
throw new Error("invalid workspace restore page");
|
|
1183
|
+
if (typeof page.control_token !== "string" || page.control_token.length < 32) {
|
|
1184
|
+
throw new Error("invalid refreshed workspace restore control token");
|
|
1185
|
+
}
|
|
1186
|
+
restoreControlToken = page.control_token;
|
|
1187
|
+
const pageFiles = [];
|
|
1188
|
+
for (const value of page.entries) {
|
|
1189
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1190
|
+
throw new Error("invalid workspace restore entry");
|
|
1191
|
+
}
|
|
1192
|
+
const item = value;
|
|
1193
|
+
if (item.entry_type === "directory") {
|
|
1194
|
+
entries.push({ type: "directory", path: String(item.path), mode: "0700" });
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (item.entry_type !== "file" || !item.grant || typeof item.grant !== "object") {
|
|
1198
|
+
throw new Error("invalid workspace restore file grant");
|
|
1199
|
+
}
|
|
1200
|
+
const entry = {
|
|
1201
|
+
type: "file",
|
|
1202
|
+
path: String(item.path),
|
|
1203
|
+
mode: item.mode,
|
|
1204
|
+
sizeBytes: Number(item.size_bytes),
|
|
1205
|
+
sha256: String(item.sha256),
|
|
1206
|
+
};
|
|
1207
|
+
entries.push(entry);
|
|
1208
|
+
pageFiles.push({ entry, grant: item.grant });
|
|
1209
|
+
}
|
|
1210
|
+
// Validate all paths and duplicates before using any page path locally.
|
|
1211
|
+
validateWorkspaceEntrySet({ schemaVersion: "agent-workspace-entry-set/1", entries }, WORKSPACE_SNAPSHOT_POLICY_V1.limits);
|
|
1212
|
+
for (const { entry, grant } of pageFiles) {
|
|
1213
|
+
if (typeof grant.url !== "string")
|
|
1214
|
+
throw new Error("workspace restore grant missing");
|
|
1215
|
+
const destination = path.join(predownloadRoot, ...entry.path.split("/"));
|
|
1216
|
+
mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
1217
|
+
const part = `${destination}.part`;
|
|
1218
|
+
const downloadSignal = AbortSignal.timeout(60_000);
|
|
1219
|
+
const download = await fetch(grant.url, {
|
|
1220
|
+
method: "GET",
|
|
1221
|
+
headers: grant.headers,
|
|
1222
|
+
signal: downloadSignal,
|
|
1223
|
+
});
|
|
1224
|
+
if (!download.ok)
|
|
1225
|
+
throw new Error(`workspace_restore_download_${download.status}`);
|
|
1226
|
+
if (!download.body)
|
|
1227
|
+
throw new Error("workspace_restore_download_body_missing");
|
|
1228
|
+
await pipeline(Readable.fromWeb(download.body), createWriteStream(part, { mode: 0o600 }), { signal: downloadSignal });
|
|
1229
|
+
renameSync(part, destination);
|
|
1230
|
+
}
|
|
1231
|
+
const rawNext = page.next_cursor;
|
|
1232
|
+
if (rawNext !== null && rawNext !== undefined && typeof rawNext !== "string") {
|
|
1233
|
+
throw new Error("invalid workspace restore cursor");
|
|
1234
|
+
}
|
|
1235
|
+
nextCursor = rawNext ?? null;
|
|
1236
|
+
} while (nextCursor !== null);
|
|
1237
|
+
await restoreWorkspaceSnapshot({
|
|
1238
|
+
workspaceDirectory,
|
|
1239
|
+
controlDirectory: path.join(ensureDaemonHome(), "agent-service-sandboxes", this.options.sandboxId, "workspace-control"),
|
|
1240
|
+
descriptor,
|
|
1241
|
+
entrySet: { schemaVersion: "agent-workspace-entry-set/1", entries },
|
|
1242
|
+
limits: WORKSPACE_SNAPSHOT_POLICY_V1.limits,
|
|
1243
|
+
requiredFreeBytes,
|
|
1244
|
+
downloadFile: async (entry, partPath) => {
|
|
1245
|
+
renameSync(path.join(predownloadRoot, ...entry.path.split("/")), partPath);
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
finally {
|
|
1250
|
+
rmSync(predownloadRoot, { recursive: true, force: true });
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (typeof requestedCopySource === "string" && typeof copyId === "string") {
|
|
1254
|
+
if (session.workspaceCopyReceipt !== null) {
|
|
1255
|
+
const receipt = session.workspaceCopyReceipt;
|
|
1256
|
+
if (receipt.copy_id !== copyId ||
|
|
1257
|
+
receipt.source_runtime_session_id !== requestedCopySource ||
|
|
1258
|
+
receipt.target_runtime_session_id !== sessionId ||
|
|
1259
|
+
receipt.source_sandbox_id !== this.options.sandboxId ||
|
|
1260
|
+
receipt.source_generation !== this.sandboxGeneration) {
|
|
1261
|
+
throw new WorkspaceCopyError("workspace_migration_receipt_mismatch");
|
|
1262
|
+
}
|
|
1263
|
+
markerPath = path.join(runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration), WORKSPACE_COPY_POLICY_V1.markerName);
|
|
1264
|
+
}
|
|
1265
|
+
else if (!isNewSession) {
|
|
1266
|
+
throw new WorkspaceCopyError("workspace_migration_receipt_mismatch");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (isNewSession && typeof requestedCopySource === "string" && typeof copyId === "string") {
|
|
1270
|
+
const sourceSession = this.state.sessions[requestedCopySource];
|
|
1271
|
+
if (!sourceSession ||
|
|
1272
|
+
sourceSession.courseRunId === null ||
|
|
1273
|
+
this.activeSessionId === requestedCopySource ||
|
|
1274
|
+
this.currentTurnSessionId === requestedCopySource) {
|
|
1275
|
+
throw new WorkspaceCopyError("workspace_migration_source_unavailable");
|
|
1276
|
+
}
|
|
1277
|
+
revokeRuntimeSessionWorkspace(requestedCopySource, this.sandboxGeneration);
|
|
1278
|
+
const copied = await copyRuntimeSessionWorkspace({
|
|
1279
|
+
copyId,
|
|
1280
|
+
sourceRuntimeSessionId: requestedCopySource,
|
|
1281
|
+
targetRuntimeSessionId: sessionId,
|
|
1282
|
+
sandboxId: this.options.sandboxId,
|
|
1283
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
1284
|
+
});
|
|
1285
|
+
session.workspaceCopyReceipt = copied.receipt;
|
|
1286
|
+
markerPath = copied.markerPath;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
if (error instanceof WorkspaceCopyError) {
|
|
1291
|
+
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1292
|
+
error_code: error.code,
|
|
1293
|
+
});
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (requestedRestore) {
|
|
1297
|
+
const restoreError = error instanceof WorkspaceRestoreError ? error : null;
|
|
1298
|
+
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1299
|
+
error_code: restoreError?.code ?? "workspace_restore_unavailable",
|
|
1300
|
+
retryable: restoreError?.retryable ?? true,
|
|
1301
|
+
});
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
throw error;
|
|
1305
|
+
}
|
|
982
1306
|
session.courseRunId = courseRunId;
|
|
983
1307
|
session.workspaceRef = session.workspaceRef ?? (typeof requestedWorkspaceRef === "string"
|
|
984
1308
|
? requestedWorkspaceRef
|
|
985
1309
|
: `ws_${sessionId.replaceAll("-", "")}_g${this.sandboxGeneration}`);
|
|
986
|
-
|
|
1310
|
+
// Replaying session.open is part of reconnect reconciliation. An already-active
|
|
1311
|
+
// runtime may still be creating files in this workspace, so never transiently revoke
|
|
1312
|
+
// its group write/execute access before the following session.activate arrives.
|
|
1313
|
+
if (session.workspaceCopyReceipt === null && (isNewSession || session.activationId === null)) {
|
|
1314
|
+
ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
|
|
1315
|
+
}
|
|
1316
|
+
if (markerPath !== null) {
|
|
1317
|
+
await finalizeRuntimeSessionWorkspaceCopy(markerPath);
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
await applyRuntimeWorkspaceQuota(sessionId, this.sandboxGeneration);
|
|
1321
|
+
}
|
|
1322
|
+
catch (error) {
|
|
1323
|
+
if (requestedRestore) {
|
|
1324
|
+
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1325
|
+
error_code: "workspace_restore_unavailable",
|
|
1326
|
+
});
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (requestedCopy) {
|
|
1330
|
+
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1331
|
+
error_code: "workspace_migration_io_failed",
|
|
1332
|
+
});
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
987
1337
|
this.state.sessions[sessionId] = session;
|
|
988
1338
|
this.persist();
|
|
1339
|
+
const materialization = readWorkspaceMaterializationMarker(this.options.sandboxId, sessionId);
|
|
989
1340
|
await this.sendSessionFrame("session.opened", sessionId, {
|
|
990
1341
|
workspace_ref: session.workspaceRef,
|
|
991
1342
|
native_session_id: session.nativeSessionId,
|
|
1343
|
+
...(materialization
|
|
1344
|
+
? {
|
|
1345
|
+
workspace_marker_version: 1,
|
|
1346
|
+
workspace_continuity_state: materialization.workspaceContinuityState,
|
|
1347
|
+
workspace_snapshot_id: materialization.snapshotId,
|
|
1348
|
+
workspace_revision: materialization.revision,
|
|
1349
|
+
workspace_content_sha256: materialization.contentSha256,
|
|
1350
|
+
}
|
|
1351
|
+
: {}),
|
|
1352
|
+
...(session.workspaceCopyReceipt !== null
|
|
1353
|
+
? { workspace_copy_receipt: session.workspaceCopyReceipt }
|
|
1354
|
+
: {}),
|
|
992
1355
|
});
|
|
993
1356
|
}
|
|
1357
|
+
async handleWorkspaceCheckpoint(frame) {
|
|
1358
|
+
const sessionId = frame.runtime_session_id;
|
|
1359
|
+
const session = this.state.sessions[sessionId];
|
|
1360
|
+
const checkpointId = frame.payload.checkpoint_id;
|
|
1361
|
+
const controlToken = frame.payload.control_token;
|
|
1362
|
+
const baseRevision = Number(frame.payload.base_revision);
|
|
1363
|
+
const agentRunId = frame.payload.agent_run_id;
|
|
1364
|
+
const workerAttempt = frame.payload.worker_attempt;
|
|
1365
|
+
const activationId = frame.payload.activation_id;
|
|
1366
|
+
const pendingCleanup = session?.pendingActivationCleanup;
|
|
1367
|
+
if (pendingCleanup &&
|
|
1368
|
+
pendingCleanup.agentRunId === agentRunId &&
|
|
1369
|
+
pendingCleanup.workerAttempt === workerAttempt &&
|
|
1370
|
+
pendingCleanup.activationId === activationId) {
|
|
1371
|
+
await this.completePendingActivationCleanup(sessionId);
|
|
1372
|
+
}
|
|
1373
|
+
if (!session ||
|
|
1374
|
+
this.activeSessionId === sessionId ||
|
|
1375
|
+
this.currentTurnSessionId === sessionId ||
|
|
1376
|
+
typeof checkpointId !== "string" ||
|
|
1377
|
+
typeof controlToken !== "string" ||
|
|
1378
|
+
controlToken.length < 32 ||
|
|
1379
|
+
frame.payload.policy_id !== "WorkspaceSnapshotPolicyV1" ||
|
|
1380
|
+
!Number.isSafeInteger(baseRevision) ||
|
|
1381
|
+
baseRevision < 0 ||
|
|
1382
|
+
!((agentRunId == null && workerAttempt == null && activationId == null) ||
|
|
1383
|
+
(typeof agentRunId === "string" &&
|
|
1384
|
+
Number.isSafeInteger(workerAttempt) &&
|
|
1385
|
+
Number(workerAttempt) >= 1 &&
|
|
1386
|
+
typeof activationId === "string" &&
|
|
1387
|
+
activationId.length > 0))) {
|
|
1388
|
+
await this.sendCommandAck(frame, "rejected", "invalid_workspace_checkpoint");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1393
|
+
await quiesceRuntimeWriters(typeof activationId === "string" ? activationId : null);
|
|
1394
|
+
const quiescenceProof = issueWorkspaceQuiescenceProof({
|
|
1395
|
+
sandboxId: this.options.sandboxId,
|
|
1396
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
1397
|
+
runtimeSessionId: sessionId,
|
|
1398
|
+
checkpointId,
|
|
1399
|
+
});
|
|
1400
|
+
await checkpointWorkspace({
|
|
1401
|
+
wsUrl: this.options.wsUrl,
|
|
1402
|
+
reconnectToken: controlToken,
|
|
1403
|
+
workspaceDirectory: runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration),
|
|
1404
|
+
scope: {
|
|
1405
|
+
checkpointId,
|
|
1406
|
+
baseRevision,
|
|
1407
|
+
sandboxId: this.options.sandboxId,
|
|
1408
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
1409
|
+
connectionEpoch: this.connectionEpoch,
|
|
1410
|
+
runtimeSessionId: sessionId,
|
|
1411
|
+
...(typeof agentRunId === "string"
|
|
1412
|
+
? {
|
|
1413
|
+
agentRunId,
|
|
1414
|
+
workerAttempt: Number(workerAttempt),
|
|
1415
|
+
activationId: String(activationId),
|
|
1416
|
+
}
|
|
1417
|
+
: {}),
|
|
1418
|
+
},
|
|
1419
|
+
quiescenceProof,
|
|
1420
|
+
});
|
|
1421
|
+
const marker = readWorkspaceMaterializationMarker(this.options.sandboxId, sessionId);
|
|
1422
|
+
if (!marker)
|
|
1423
|
+
throw new Error("workspace checkpoint marker missing");
|
|
1424
|
+
await this.sendSessionFrame("session.opened", sessionId, {
|
|
1425
|
+
workspace_ref: session.workspaceRef,
|
|
1426
|
+
native_session_id: session.nativeSessionId,
|
|
1427
|
+
workspace_marker_version: 1,
|
|
1428
|
+
workspace_continuity_state: marker.workspaceContinuityState,
|
|
1429
|
+
workspace_snapshot_id: marker.snapshotId,
|
|
1430
|
+
workspace_revision: marker.revision,
|
|
1431
|
+
workspace_content_sha256: marker.contentSha256,
|
|
1432
|
+
});
|
|
1433
|
+
await this.sendCommandAck(frame, "ok");
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
this.log.error("workspace baseline checkpoint failed", {
|
|
1437
|
+
sandboxId: this.options.sandboxId,
|
|
1438
|
+
runtimeSessionId: sessionId,
|
|
1439
|
+
error: error instanceof Error ? redactSecretString(error.message) : "unexpected",
|
|
1440
|
+
});
|
|
1441
|
+
const deterministic = (error instanceof WorkspaceSnapshotStagingError && !error.retryable) ||
|
|
1442
|
+
error instanceof WorkspaceEntrySetError ||
|
|
1443
|
+
(error instanceof WorkspaceSnapshotControlError && !error.retryable);
|
|
1444
|
+
const errorCode = deterministic && "code" in error
|
|
1445
|
+
? String(error.code)
|
|
1446
|
+
: "workspace_checkpoint_retryable";
|
|
1447
|
+
await this.sendCommandAck(frame, "rejected", errorCode, !deterministic);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
994
1450
|
async handleSessionActivate(frame, fence) {
|
|
995
1451
|
const sessionId = frame.runtime_session_id;
|
|
996
1452
|
const activationId = frame.activation_id;
|
|
@@ -1177,6 +1633,21 @@ export class AgentServiceSandboxClient {
|
|
|
1177
1633
|
await this.sendCommandAck(frame, "rejected", "activation_mismatch");
|
|
1178
1634
|
return;
|
|
1179
1635
|
}
|
|
1636
|
+
const checkpoint = frame.payload.workspace_checkpoint;
|
|
1637
|
+
if (checkpoint !== undefined) {
|
|
1638
|
+
if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint) ||
|
|
1639
|
+
typeof checkpoint.checkpoint_id !== "string" ||
|
|
1640
|
+
checkpoint.policy_id !== "WorkspaceSnapshotPolicyV1" ||
|
|
1641
|
+
!Number.isSafeInteger(checkpoint.base_revision) ||
|
|
1642
|
+
Number(checkpoint.base_revision) < 0) {
|
|
1643
|
+
await this.sendCommandAck(frame, "rejected", "invalid_workspace_checkpoint");
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
this.turnWorkspaceCheckpoints.set(runId, {
|
|
1647
|
+
checkpointId: String(checkpoint.checkpoint_id),
|
|
1648
|
+
baseRevision: Number(checkpoint.base_revision),
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1180
1651
|
const payload = frame.payload;
|
|
1181
1652
|
if (!this.validTurnPayload(payload)) {
|
|
1182
1653
|
await this.sendCommandAck(frame, "rejected", "invalid_turn_payload");
|
|
@@ -1263,6 +1734,8 @@ export class AgentServiceSandboxClient {
|
|
|
1263
1734
|
.finally(() => {
|
|
1264
1735
|
this.inflightCommands.delete(commandId);
|
|
1265
1736
|
this.runProfiles.delete(runId);
|
|
1737
|
+
this.turnWorkspaceCheckpoints.delete(runId);
|
|
1738
|
+
this.completedWorkspaceCheckpoints.delete(runId);
|
|
1266
1739
|
});
|
|
1267
1740
|
}
|
|
1268
1741
|
async handleTurnCancel(frame) {
|
|
@@ -1313,7 +1786,7 @@ export class AgentServiceSandboxClient {
|
|
|
1313
1786
|
}
|
|
1314
1787
|
delete session.acceptedCommands[commandId];
|
|
1315
1788
|
this.persist();
|
|
1316
|
-
this.finishTurn({ agent_run_id: runId });
|
|
1789
|
+
await this.finishTurn({ agent_run_id: runId });
|
|
1317
1790
|
}
|
|
1318
1791
|
/**
|
|
1319
1792
|
* 幂等关闭一个 runtime session:终止其 runtime 子进程、删除 workspace/native state、
|
|
@@ -1372,7 +1845,7 @@ export class AgentServiceSandboxClient {
|
|
|
1372
1845
|
delete session.acceptedCommands[commandId];
|
|
1373
1846
|
this.persist();
|
|
1374
1847
|
}
|
|
1375
|
-
this.finishTurn({ agent_run_id: accepted.agentRunId });
|
|
1848
|
+
await this.finishTurn({ agent_run_id: accepted.agentRunId });
|
|
1376
1849
|
}
|
|
1377
1850
|
catch (error) {
|
|
1378
1851
|
this.log.error("failed to reconcile an interrupted accepted turn", {
|
|
@@ -1581,7 +2054,7 @@ export class AgentServiceSandboxClient {
|
|
|
1581
2054
|
spool_frames: this.spoolFrameCount(),
|
|
1582
2055
|
});
|
|
1583
2056
|
}
|
|
1584
|
-
async sendCommandAck(frame, status, error) {
|
|
2057
|
+
async sendCommandAck(frame, status, error, retryable) {
|
|
1585
2058
|
if (frame.sandbox_generation !== this.sandboxGeneration ||
|
|
1586
2059
|
frame.connection_epoch !== this.connectionEpoch)
|
|
1587
2060
|
return;
|
|
@@ -1589,6 +2062,7 @@ export class AgentServiceSandboxClient {
|
|
|
1589
2062
|
command_frame_id: frame.frame_id,
|
|
1590
2063
|
status,
|
|
1591
2064
|
...(error !== undefined ? { error } : {}),
|
|
2065
|
+
...(retryable !== undefined ? { retryable } : {}),
|
|
1592
2066
|
});
|
|
1593
2067
|
}
|
|
1594
2068
|
async handleWorkspaceFileRead(frame) {
|