@botlearn-course/daemon 0.0.20-beta.5 → 0.0.20-beta.6
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 +7 -0
- package/dist/agent-service-sandbox.d.ts +1 -0
- package/dist/agent-service-sandbox.js +22 -14
- package/dist/run-dispatcher.js +9 -6
- package/dist/types.d.ts +2 -2
- package/dist/workspace-filesystem.d.ts +14 -0
- package/dist/workspace-filesystem.js +100 -0
- package/dist/workspace.d.ts +4 -4
- package/dist/workspace.js +26 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,13 @@ daemon,由 root-owned 固定 launcher 清空附加组后直接降权为 `user`
|
|
|
47
47
|
`agent-service session --bootstrap-stdin` 同样属于受 supervisor 保护的内部协议,不应直接暴露给
|
|
48
48
|
课程任务或 workspace shell。
|
|
49
49
|
|
|
50
|
+
veFaaS Origin 的托管 workspace 使用用户级 NAS backing image。root bootstrap 完成 NFS、marker、
|
|
51
|
+
ext4、loop mount 和固定 3 GiB 校验后,daemon 才发送 `sandbox.ready`。RuntimeSession 使用稳定路径
|
|
52
|
+
`/workspace/sessions/{runtime_session_id}/workspace`,Sandbox generation 只参与写入隔离。磁盘写满
|
|
53
|
+
统一上报 `workspace_user_quota_exceeded`。pause 和 cleanup 进入 drain 后,daemon 先停止 runtime
|
|
54
|
+
writer,再通过 root launcher 执行 ext4 sync、loop unmount、backing image 和 NFS parent fsync;完成后
|
|
55
|
+
才发送 `sandbox.drained` 并退出。
|
|
56
|
+
|
|
50
57
|
## 支持的 runtime
|
|
51
58
|
|
|
52
59
|
run 的 `runtime.id` 决定用哪个 adapter 执行;未指定时默认 `codex`。runtime 不可用时 run 会
|
|
@@ -78,6 +78,7 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
78
78
|
run(): Promise<void>;
|
|
79
79
|
stop(): void;
|
|
80
80
|
stopGracefully(): Promise<void>;
|
|
81
|
+
private drainWorkspace;
|
|
81
82
|
postEvent(agentRunId: string, event: RunEvent): Promise<RunEventReceipt>;
|
|
82
83
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord>;
|
|
83
84
|
recordWorkspaceScanMeasurement(agentRunId: string, measurement: WorkspaceScanMeasurement): Promise<void>;
|
|
@@ -20,7 +20,8 @@ import { validateWorkspaceEntrySet, WorkspaceEntrySetError, } from "./workspace-
|
|
|
20
20
|
import { WORKSPACE_SNAPSHOT_POLICY_V1, workspaceSnapshotPolicyCapability, } from "./workspace-snapshot-policy.js";
|
|
21
21
|
import { assertWorkspaceRestoreCapacity, restoreWorkspaceSnapshot, WorkspaceRestoreError, } from "./workspace-restore.js";
|
|
22
22
|
import { readWorkspaceMaterializationMarker } from "./workspace-materialization.js";
|
|
23
|
-
import {
|
|
23
|
+
import { assertWorkspaceFilesystem, flushWorkspaceFilesystem, nasWorkspaceEnabled, } from "./workspace-filesystem.js";
|
|
24
|
+
import { applyRuntimeWorkspaceQuota } from "./workspace-quota.js";
|
|
24
25
|
import { WebSocketClient, } from "./websocket-client.js";
|
|
25
26
|
const RECONNECT_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
|
|
26
27
|
const MAX_SPOOL_FRAMES = 1024;
|
|
@@ -601,6 +602,18 @@ export class AgentServiceSandboxClient {
|
|
|
601
602
|
async stopGracefully() {
|
|
602
603
|
this.dispatcher.cancelAll();
|
|
603
604
|
await quiesceRuntimeWriters(null);
|
|
605
|
+
await flushWorkspaceFilesystem();
|
|
606
|
+
this.stop();
|
|
607
|
+
}
|
|
608
|
+
async drainWorkspace(fence) {
|
|
609
|
+
if (!(await this.dispatcher.drain(10_000)) || !this.isCurrentLifecycleFence(fence)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
await quiesceRuntimeWriters(null);
|
|
613
|
+
await flushWorkspaceFilesystem();
|
|
614
|
+
if (!this.isCurrentLifecycleFence(fence))
|
|
615
|
+
return;
|
|
616
|
+
await this.sendControlFrame("sandbox.drained", {});
|
|
604
617
|
this.stop();
|
|
605
618
|
}
|
|
606
619
|
async postEvent(agentRunId, event) {
|
|
@@ -823,7 +836,7 @@ export class AgentServiceSandboxClient {
|
|
|
823
836
|
this.rejectPending(new Error("Agent Service sandbox generation changed"));
|
|
824
837
|
for (const sessionId of Object.keys(this.state.sessions)) {
|
|
825
838
|
revokeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration);
|
|
826
|
-
removeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration);
|
|
839
|
+
removeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration, nasWorkspaceEnabled());
|
|
827
840
|
}
|
|
828
841
|
this.state.sessions = {};
|
|
829
842
|
this.state.sandboxGeneration = frame.sandbox_generation;
|
|
@@ -853,7 +866,7 @@ export class AgentServiceSandboxClient {
|
|
|
853
866
|
this.runtimeLogPolicy = runtimeLogPolicy(frame.payload.runtime_log_policy);
|
|
854
867
|
await cleanupRuntimeSessionWorkspaceCopyStaging();
|
|
855
868
|
this.persist();
|
|
856
|
-
|
|
869
|
+
assertWorkspaceFilesystem(process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT ?? "");
|
|
857
870
|
assertWorkspaceWriterFreezeSupported();
|
|
858
871
|
await this.sendControlFrame("sandbox.ready", {
|
|
859
872
|
protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
|
|
@@ -862,6 +875,7 @@ export class AgentServiceSandboxClient {
|
|
|
862
875
|
"workspace_writer_freeze_v1",
|
|
863
876
|
"workspace_snapshot_v2",
|
|
864
877
|
"workspace_restore_v1",
|
|
878
|
+
...(nasWorkspaceEnabled() ? ["workspace_nas_v1"] : []),
|
|
865
879
|
...(workspaceCopyV1Supported() ? ["workspace_copy_v1"] : []),
|
|
866
880
|
],
|
|
867
881
|
workspace_snapshot_policy: workspaceSnapshotPolicyCapability(),
|
|
@@ -1008,11 +1022,7 @@ export class AgentServiceSandboxClient {
|
|
|
1008
1022
|
return;
|
|
1009
1023
|
case "sandbox.drain":
|
|
1010
1024
|
await this.sendCommandAck(frame, "ok");
|
|
1011
|
-
this.scheduleLifecycle("sandbox.drain",
|
|
1012
|
-
if (await this.dispatcher.drain(10_000) && this.isCurrentLifecycleFence(fence)) {
|
|
1013
|
-
await this.sendControlFrame("sandbox.drained", {});
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1025
|
+
this.scheduleLifecycle("sandbox.drain", (fence) => this.drainWorkspace(fence));
|
|
1016
1026
|
return;
|
|
1017
1027
|
case "protocol.error":
|
|
1018
1028
|
throw new Error(`server protocol error: ${String(frame.payload.code ?? "unknown")}`);
|
|
@@ -1038,11 +1048,7 @@ export class AgentServiceSandboxClient {
|
|
|
1038
1048
|
return;
|
|
1039
1049
|
}
|
|
1040
1050
|
if (state === "drain") {
|
|
1041
|
-
this.scheduleLifecycle("sandbox.sync.drain",
|
|
1042
|
-
if (await this.dispatcher.drain(10_000) && this.isCurrentLifecycleFence(fence)) {
|
|
1043
|
-
await this.sendControlFrame("sandbox.drained", {});
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1051
|
+
this.scheduleLifecycle("sandbox.sync.drain", (fence) => this.drainWorkspace(fence));
|
|
1046
1052
|
}
|
|
1047
1053
|
// idle/run/cancel:实际工作全部由显式命令帧下发(合同 §1.3)。
|
|
1048
1054
|
}
|
|
@@ -1317,7 +1323,9 @@ export class AgentServiceSandboxClient {
|
|
|
1317
1323
|
await finalizeRuntimeSessionWorkspaceCopy(markerPath);
|
|
1318
1324
|
}
|
|
1319
1325
|
try {
|
|
1320
|
-
|
|
1326
|
+
if (!nasWorkspaceEnabled()) {
|
|
1327
|
+
await applyRuntimeWorkspaceQuota(sessionId, this.sandboxGeneration);
|
|
1328
|
+
}
|
|
1321
1329
|
}
|
|
1322
1330
|
catch (error) {
|
|
1323
1331
|
if (requestedRestore) {
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -13,6 +13,7 @@ import { buildToolObservation } from "./tool-observation.js";
|
|
|
13
13
|
import { buildReasoningTracePayload, extractReasoningText, REASONING_TRACE_MAX_CHARS, } from "./trace-projection.js";
|
|
14
14
|
import { RuntimeExecutionError, } from "./types.js";
|
|
15
15
|
import { ensureRunWorkspace, transcriptPath } from "./workspace.js";
|
|
16
|
+
import { isWorkspaceQuotaExceededError } from "./workspace-filesystem.js";
|
|
16
17
|
// 与 runtimes/index.ts 的 DEFAULT_RUNTIME_ID 保持一致(此处不 import registry,避免拉入全部 adapter)。
|
|
17
18
|
const DEFAULT_RUNTIME_ID = "codex";
|
|
18
19
|
const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
@@ -951,13 +952,15 @@ export class RunDispatcher {
|
|
|
951
952
|
}
|
|
952
953
|
else {
|
|
953
954
|
const info = errorInfo(err);
|
|
954
|
-
const errorType = err
|
|
955
|
-
?
|
|
956
|
-
: err instanceof
|
|
955
|
+
const errorType = isWorkspaceQuotaExceededError(err)
|
|
956
|
+
? "workspace_user_quota_exceeded"
|
|
957
|
+
: err instanceof RuntimeProfileApplyError
|
|
957
958
|
? err.code
|
|
958
|
-
: err instanceof
|
|
959
|
-
? err.
|
|
960
|
-
:
|
|
959
|
+
: err instanceof InputAttachmentMaterializationError
|
|
960
|
+
? err.code
|
|
961
|
+
: err instanceof RuntimeExecutionError
|
|
962
|
+
? err.errorType
|
|
963
|
+
: "runtime_error";
|
|
961
964
|
await sendFailure(errorType, info.error_message, err, err instanceof RuntimeProfileApplyError
|
|
962
965
|
? { code: err.code, profile_apply_status: "failed" }
|
|
963
966
|
: {});
|
package/dist/types.d.ts
CHANGED
|
@@ -233,9 +233,9 @@ export interface CourseRuntime {
|
|
|
233
233
|
}
|
|
234
234
|
/** runtime 执行失败(dispatcher 折叠为 run.failed)。 */
|
|
235
235
|
export declare class RuntimeExecutionError extends Error {
|
|
236
|
-
readonly errorType: "runtime_error" | "runtime_unavailable" | "timeout";
|
|
236
|
+
readonly errorType: "runtime_error" | "runtime_unavailable" | "timeout" | "workspace_user_quota_exceeded";
|
|
237
237
|
readonly failure?: Partial<RuntimeFailureSummary> | undefined;
|
|
238
|
-
constructor(message: string, errorType?: "runtime_error" | "runtime_unavailable" | "timeout", failure?: Partial<RuntimeFailureSummary> | undefined);
|
|
238
|
+
constructor(message: string, errorType?: "runtime_error" | "runtime_unavailable" | "timeout" | "workspace_user_quota_exceeded", failure?: Partial<RuntimeFailureSummary> | undefined);
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
241
|
* 本地诊断用的失败摘要。完整结构只进脱敏日志/transcript;wire 仅允许
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const WORKSPACE_IMAGE_FORMAT: "botlearn-workspace-image/1";
|
|
2
|
+
export declare const WORKSPACE_IMAGE_LOGICAL_BYTES: number;
|
|
3
|
+
interface MountInfoEntry {
|
|
4
|
+
target: string;
|
|
5
|
+
filesystemType: string;
|
|
6
|
+
mountOptions: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseWorkspaceMountInfo(text: string): MountInfoEntry[];
|
|
9
|
+
export declare function nasWorkspaceEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
10
|
+
export declare function workspaceFilesystemFlushCommand(env?: NodeJS.ProcessEnv): [string, string[]] | null;
|
|
11
|
+
export declare function flushWorkspaceFilesystem(env?: NodeJS.ProcessEnv): Promise<void>;
|
|
12
|
+
export declare function isWorkspaceQuotaExceededError(error: unknown): boolean;
|
|
13
|
+
export declare function assertWorkspaceFilesystem(workspaceRoot: string, env?: NodeJS.ProcessEnv, mountInfo?: string): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFileSync, statfsSync } from "node:fs";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { assertRuntimeWorkspaceQuota } from "./workspace-quota.js";
|
|
5
|
+
export const WORKSPACE_IMAGE_FORMAT = "botlearn-workspace-image/1";
|
|
6
|
+
export const WORKSPACE_IMAGE_LOGICAL_BYTES = 3 * 1024 * 1024 * 1024;
|
|
7
|
+
const RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
function decodeMountInfoPath(value) {
|
|
10
|
+
return value.replace(/\\([0-7]{3})/gu, (_match, octal) => String.fromCharCode(Number.parseInt(octal, 8)));
|
|
11
|
+
}
|
|
12
|
+
export function parseWorkspaceMountInfo(text) {
|
|
13
|
+
return text.split("\n").filter(Boolean).map((line) => {
|
|
14
|
+
const [left, right] = line.split(" - ", 2);
|
|
15
|
+
const fields = left?.split(" ") ?? [];
|
|
16
|
+
const filesystem = right?.split(" ") ?? [];
|
|
17
|
+
if (fields.length < 6 || filesystem.length < 3) {
|
|
18
|
+
throw new Error("workspace_mount_proof_invalid");
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
target: decodeMountInfoPath(fields[4]),
|
|
22
|
+
filesystemType: filesystem[0],
|
|
23
|
+
mountOptions: new Set(fields[5].split(",")),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function nasWorkspaceEnabled(env = process.env) {
|
|
28
|
+
return env.BOTLEARN_WORKSPACE_IMAGE_FORMAT !== undefined;
|
|
29
|
+
}
|
|
30
|
+
export function workspaceFilesystemFlushCommand(env = process.env) {
|
|
31
|
+
if (!nasWorkspaceEnabled(env))
|
|
32
|
+
return null;
|
|
33
|
+
if (env.BOTLEARN_RUNTIME_LAUNCH_MODE === "direct-uid") {
|
|
34
|
+
return [RUNTIME_LAUNCHER, ["--workspace-filesystem-flush"]];
|
|
35
|
+
}
|
|
36
|
+
if (env.BOTLEARN_RUNTIME_LAUNCH_MODE === "sudo") {
|
|
37
|
+
return [
|
|
38
|
+
"/usr/bin/sudo",
|
|
39
|
+
["-n", "--", RUNTIME_LAUNCHER, "--workspace-filesystem-flush"],
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
if (env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || env.NODE_ENV === "test") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
throw new Error("workspace_nas_flush_failed");
|
|
46
|
+
}
|
|
47
|
+
export async function flushWorkspaceFilesystem(env = process.env) {
|
|
48
|
+
const command = workspaceFilesystemFlushCommand(env);
|
|
49
|
+
if (command === null)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
await execFileAsync(command[0], command[1], { timeout: 30_000 });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error("workspace_nas_flush_failed");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function isWorkspaceQuotaExceededError(error) {
|
|
59
|
+
if (!(error instanceof Error))
|
|
60
|
+
return false;
|
|
61
|
+
const candidate = error;
|
|
62
|
+
const code = String(candidate.code ?? candidate.failure?.error_code ?? "");
|
|
63
|
+
const detail = [
|
|
64
|
+
candidate.message,
|
|
65
|
+
candidate.failure?.error_message,
|
|
66
|
+
candidate.failure?.stderr_tail,
|
|
67
|
+
].map((value) => String(value ?? "")).join(" ");
|
|
68
|
+
return code === "ENOSPC"
|
|
69
|
+
|| code === "EDQUOT"
|
|
70
|
+
|| /\b(?:ENOSPC|EDQUOT|no space left on device|disk quota exceeded)\b/iu.test(detail)
|
|
71
|
+
|| (candidate.cause !== undefined && isWorkspaceQuotaExceededError(candidate.cause));
|
|
72
|
+
}
|
|
73
|
+
export function assertWorkspaceFilesystem(workspaceRoot, env = process.env, mountInfo) {
|
|
74
|
+
if (!nasWorkspaceEnabled(env)) {
|
|
75
|
+
assertRuntimeWorkspaceQuota(workspaceRoot);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (env.BOTLEARN_WORKSPACE_IMAGE_FORMAT !== WORKSPACE_IMAGE_FORMAT
|
|
79
|
+
|| env.BOTLEARN_WORKSPACE_LOGICAL_BYTES !== String(WORKSPACE_IMAGE_LOGICAL_BYTES)
|
|
80
|
+
|| !/^[0-9a-f]{64}$/u.test(env.BOTLEARN_WORKSPACE_SCOPE_DIGEST ?? "")) {
|
|
81
|
+
throw new Error("workspace_quota_mismatch");
|
|
82
|
+
}
|
|
83
|
+
if (env.NODE_ENV === "test" && mountInfo === undefined)
|
|
84
|
+
return;
|
|
85
|
+
const mount = parseWorkspaceMountInfo(mountInfo ?? readFileSync("/proc/self/mountinfo", "utf8"))
|
|
86
|
+
.find((candidate) => candidate.target === workspaceRoot);
|
|
87
|
+
if (!mount
|
|
88
|
+
|| mount.filesystemType !== "ext4"
|
|
89
|
+
|| !mount.mountOptions.has("rw")
|
|
90
|
+
|| !mount.mountOptions.has("nodev")
|
|
91
|
+
|| !mount.mountOptions.has("nosuid")) {
|
|
92
|
+
throw new Error("workspace_loop_mount_failed");
|
|
93
|
+
}
|
|
94
|
+
if (env.NODE_ENV === "test")
|
|
95
|
+
return;
|
|
96
|
+
const filesystem = statfsSync(workspaceRoot, { bigint: true });
|
|
97
|
+
if (filesystem.blocks * filesystem.bsize !== BigInt(WORKSPACE_IMAGE_LOGICAL_BYTES)) {
|
|
98
|
+
throw new Error("workspace_quota_mismatch");
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -20,9 +20,9 @@ export declare function runtimeProfileDir(agentRunId: string): string;
|
|
|
20
20
|
export declare function runtimeProfileRunRootDir(agentRunId: string): string;
|
|
21
21
|
export declare function runtimeSessionRootDir(runtimeSessionId: string, sandboxGeneration: number): string;
|
|
22
22
|
/**
|
|
23
|
-
* per-session workspace(ADR-015 §7):managed sandbox
|
|
24
|
-
* `<
|
|
25
|
-
*
|
|
23
|
+
* per-session workspace(ADR-015 §7):NAS managed sandbox 使用稳定路径
|
|
24
|
+
* `<root>/sessions/<runtime_session_id>/workspace`;旧 provider 保留 generation 路径。
|
|
25
|
+
* 两种路径都由 daemon 在 session.open 时创建。
|
|
26
26
|
*/
|
|
27
27
|
export declare function runtimeSessionWorkspaceDir(runtimeSessionId: string, sandboxGeneration: number): string;
|
|
28
28
|
export declare function runtimeSessionTranscriptPath(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): string;
|
|
@@ -102,7 +102,7 @@ export declare function exposeRuntimeSessionWorkspace(runtimeSessionId: string,
|
|
|
102
102
|
/** Revoke a managed workspace without deleting the session's durable local materialization. */
|
|
103
103
|
export declare function revokeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
104
104
|
/** session.close 时删除 workspace 与本地 session 物化状态(transcripts 等)。幂等。 */
|
|
105
|
-
export declare function removeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
105
|
+
export declare function removeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number, preserveManagedWorkspace?: boolean): void;
|
|
106
106
|
export declare function ensureRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): {
|
|
107
107
|
rootDir: string;
|
|
108
108
|
workspaceDir: string;
|
package/dist/workspace.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { TextDecoder } from "node:util";
|
|
6
6
|
import { daemonHome } from "./auth-store.js";
|
|
7
|
+
import { nasWorkspaceEnabled } from "./workspace-filesystem.js";
|
|
7
8
|
/**
|
|
8
9
|
* 每 run 隔离工作区(spec §4):
|
|
9
10
|
*
|
|
@@ -53,9 +54,9 @@ export function runtimeSessionRootDir(runtimeSessionId, sandboxGeneration) {
|
|
|
53
54
|
return path.join(daemonHome(), "agent-service-sessions", runtimeSessionId, `generation-${sandboxGeneration}`);
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
* per-session workspace(ADR-015 §7):managed sandbox
|
|
57
|
-
* `<
|
|
58
|
-
*
|
|
57
|
+
* per-session workspace(ADR-015 §7):NAS managed sandbox 使用稳定路径
|
|
58
|
+
* `<root>/sessions/<runtime_session_id>/workspace`;旧 provider 保留 generation 路径。
|
|
59
|
+
* 两种路径都由 daemon 在 session.open 时创建。
|
|
59
60
|
*/
|
|
60
61
|
export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration) {
|
|
61
62
|
assertSafeId(runtimeSessionId, "runtime_session_id");
|
|
@@ -64,6 +65,9 @@ export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration)
|
|
|
64
65
|
}
|
|
65
66
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
66
67
|
if (managedRoot) {
|
|
68
|
+
if (nasWorkspaceEnabled()) {
|
|
69
|
+
return path.join(managedRoot, "sessions", runtimeSessionId, "workspace");
|
|
70
|
+
}
|
|
67
71
|
return path.join(managedRoot, runtimeSessionId, `generation-${sandboxGeneration}`);
|
|
68
72
|
}
|
|
69
73
|
return path.join(runtimeSessionRootDir(runtimeSessionId, sandboxGeneration), "workspace");
|
|
@@ -71,7 +75,11 @@ export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration)
|
|
|
71
75
|
function managedRuntimeSessionParentDir(runtimeSessionId) {
|
|
72
76
|
assertSafeId(runtimeSessionId, "runtime_session_id");
|
|
73
77
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
74
|
-
|
|
78
|
+
if (!managedRoot)
|
|
79
|
+
return null;
|
|
80
|
+
return nasWorkspaceEnabled()
|
|
81
|
+
? path.join(managedRoot, "sessions", runtimeSessionId)
|
|
82
|
+
: path.join(managedRoot, runtimeSessionId);
|
|
75
83
|
}
|
|
76
84
|
export function runtimeSessionTranscriptPath(runtimeSessionId, sandboxGeneration, agentRunId) {
|
|
77
85
|
assertSafeId(agentRunId, "agent_run_id");
|
|
@@ -228,12 +236,16 @@ async function childDirectories(root) {
|
|
|
228
236
|
export async function cleanupRuntimeSessionWorkspaceCopyStaging() {
|
|
229
237
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
230
238
|
if (managedRoot) {
|
|
231
|
-
|
|
239
|
+
const sessionRoot = nasWorkspaceEnabled()
|
|
240
|
+
? path.join(managedRoot, "sessions")
|
|
241
|
+
: managedRoot;
|
|
242
|
+
for (const sessionName of await childDirectories(sessionRoot)) {
|
|
232
243
|
if (!SAFE_ID_PATTERN.test(sessionName))
|
|
233
244
|
continue;
|
|
234
|
-
const sessionParent = path.join(
|
|
245
|
+
const sessionParent = path.join(sessionRoot, sessionName);
|
|
235
246
|
for (const name of await childDirectories(sessionParent)) {
|
|
236
|
-
if (MANAGED_COPY_STAGING_PATTERN.test(name)
|
|
247
|
+
if (MANAGED_COPY_STAGING_PATTERN.test(name)
|
|
248
|
+
|| LOCAL_COPY_STAGING_PATTERN.test(name)) {
|
|
237
249
|
await fs.rm(path.join(sessionParent, name), { recursive: true, force: true });
|
|
238
250
|
}
|
|
239
251
|
}
|
|
@@ -700,16 +712,19 @@ export function revokeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneratio
|
|
|
700
712
|
}
|
|
701
713
|
}
|
|
702
714
|
/** session.close 时删除 workspace 与本地 session 物化状态(transcripts 等)。幂等。 */
|
|
703
|
-
export function removeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneration) {
|
|
704
|
-
//
|
|
715
|
+
export function removeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneration, preserveManagedWorkspace = false) {
|
|
716
|
+
// 校验路径段安全后删除本地物化状态;generation 换代可保留 NAS 正文。
|
|
705
717
|
runtimeSessionRootDir(runtimeSessionId, sandboxGeneration);
|
|
706
718
|
rmSync(path.join(daemonHome(), "agent-service-sessions", runtimeSessionId), {
|
|
707
719
|
recursive: true,
|
|
708
720
|
force: true,
|
|
709
721
|
});
|
|
710
722
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
711
|
-
if (managedRoot) {
|
|
712
|
-
|
|
723
|
+
if (managedRoot && !preserveManagedWorkspace) {
|
|
724
|
+
const sessionRoot = nasWorkspaceEnabled()
|
|
725
|
+
? path.join(managedRoot, "sessions", runtimeSessionId)
|
|
726
|
+
: path.join(managedRoot, runtimeSessionId);
|
|
727
|
+
rmSync(sessionRoot, { recursive: true, force: true });
|
|
713
728
|
}
|
|
714
729
|
}
|
|
715
730
|
export function ensureRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneration, agentRunId) {
|
package/package.json
CHANGED