@botlearn-course/daemon 0.0.20-beta.1 → 0.0.20-beta.10

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.
@@ -1,9 +1,9 @@
1
- export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.2";
2
- export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v2";
1
+ export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.4";
2
+ export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v4";
3
3
  export declare const WORKSPACE_FILE_READ_BINARY_CAPABILITY: "workspace_file_read_binary_v1";
4
4
  export declare const WORKSPACE_FILE_CHUNK_BYTES: number;
5
5
  export declare const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
6
- 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" | "workspace.file.read" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.open_failed" | "session.closed" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
6
+ 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" | "workspace.file.read" | "workspace.checkpoint" | "workspace.thaw" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.open_failed" | "session.closed" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
7
7
  export declare class UnsupportedSandboxProtocolError extends Error {
8
8
  readonly schemaVersion: unknown;
9
9
  constructor(schemaVersion: unknown);
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.2";
3
- export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v2";
2
+ export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.4";
3
+ export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v4";
4
4
  export const WORKSPACE_FILE_READ_BINARY_CAPABILITY = "workspace_file_read_binary_v1";
5
5
  export const WORKSPACE_FILE_CHUNK_BYTES = 64 * 1024;
6
6
  export const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
@@ -18,6 +18,8 @@ const FRAME_TYPES = new Set([
18
18
  "event.ack",
19
19
  "auth.rotate",
20
20
  "workspace.file.read",
21
+ "workspace.checkpoint",
22
+ "workspace.thaw",
21
23
  "ping",
22
24
  "sandbox.ready",
23
25
  "sandbox.heartbeat",
@@ -42,6 +44,7 @@ const SESSION_TYPES = new Set([
42
44
  "session.open_failed",
43
45
  "session.closed",
44
46
  "workspace.file.read",
47
+ "workspace.checkpoint",
45
48
  "workspace.file.result",
46
49
  ]);
47
50
  /** TURN 帧:必须带 runtime_session_id + agent_run_id + worker_attempt + activation_id 四元组。 */
package/dist/cli.js CHANGED
@@ -372,7 +372,25 @@ async function cmdAgentServiceSession(args) {
372
372
  deepseekTuiVersion: process.env.BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION?.trim() || undefined,
373
373
  });
374
374
  clearAgentServiceControlEnv();
375
- await client.run();
375
+ let stopping = false;
376
+ const stop = () => {
377
+ if (stopping)
378
+ return;
379
+ stopping = true;
380
+ void client.stopGracefully().catch((error) => {
381
+ console.error(error instanceof Error ? error.message : "sandbox shutdown failed");
382
+ client.stop();
383
+ });
384
+ };
385
+ process.once("SIGTERM", stop);
386
+ process.once("SIGINT", stop);
387
+ try {
388
+ await client.run();
389
+ }
390
+ finally {
391
+ process.removeListener("SIGTERM", stop);
392
+ process.removeListener("SIGINT", stop);
393
+ }
376
394
  return 0;
377
395
  }
378
396
  // ---------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createReadStream } from "node:fs";
2
+ import { constants } from "node:fs";
3
3
  import { open, readdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { performance } from "node:perf_hooks";
@@ -20,30 +20,50 @@ function isSafeRelativePath(rel) {
20
20
  const segments = rel.split("/");
21
21
  return segments.every((seg) => seg !== "" && seg !== "..");
22
22
  }
23
- async function sha256File(absPath) {
23
+ async function sha256File(handle) {
24
24
  const hash = createHash("sha256");
25
- const stream = createReadStream(absPath);
25
+ const stream = handle.createReadStream({ autoClose: false, start: 0 });
26
26
  for await (const chunk of stream)
27
27
  hash.update(chunk);
28
28
  return hash.digest("hex");
29
29
  }
30
- async function readPreview(absPath, maxPreviewChars) {
31
- const handle = await open(absPath, "r");
30
+ async function readPreview(handle, maxPreviewChars) {
31
+ const sniff = Buffer.alloc(TEXT_SNIFF_BYTES);
32
+ const { bytesRead } = await handle.read(sniff, 0, TEXT_SNIFF_BYTES, 0);
33
+ if (sniff.subarray(0, bytesRead).includes(0))
34
+ return null;
35
+ // 预览最多 maxPreviewChars 字符;UTF-8 下 4 字节/字符封顶,读够即可。
36
+ const want = maxPreviewChars * 4;
37
+ let buf = sniff.subarray(0, bytesRead);
38
+ if (bytesRead === TEXT_SNIFF_BYTES && want > TEXT_SNIFF_BYTES) {
39
+ const more = Buffer.alloc(want - TEXT_SNIFF_BYTES);
40
+ const extra = await handle.read(more, 0, more.length, TEXT_SNIFF_BYTES);
41
+ buf = Buffer.concat([buf, more.subarray(0, extra.bytesRead)]);
42
+ }
43
+ const text = buf.toString("utf8").replace(/�+$/, "");
44
+ return redactSecretString(text.slice(0, maxPreviewChars));
45
+ }
46
+ function sameFileVersion(left, right) {
47
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
48
+ left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
49
+ }
50
+ async function scanStableFile(absPath, maxPreviewChars, maxFileBytes) {
51
+ const handle = await open(absPath, constants.O_RDONLY | constants.O_NOFOLLOW);
32
52
  try {
33
- const sniff = Buffer.alloc(TEXT_SNIFF_BYTES);
34
- const { bytesRead } = await handle.read(sniff, 0, TEXT_SNIFF_BYTES, 0);
35
- if (sniff.subarray(0, bytesRead).includes(0))
36
- return null;
37
- // 预览最多 maxPreviewChars 字符;UTF-8 下 4 字节/字符封顶,读够即可。
38
- const want = maxPreviewChars * 4;
39
- let buf = sniff.subarray(0, bytesRead);
40
- if (bytesRead === TEXT_SNIFF_BYTES && want > TEXT_SNIFF_BYTES) {
41
- const more = Buffer.alloc(want - TEXT_SNIFF_BYTES);
42
- const extra = await handle.read(more, 0, more.length, TEXT_SNIFF_BYTES);
43
- buf = Buffer.concat([buf, more.subarray(0, extra.bytesRead)]);
53
+ const before = await handle.stat({ bigint: true });
54
+ if (!before.isFile() || before.size > BigInt(Number.MAX_SAFE_INTEGER)) {
55
+ throw new Error("workspace_candidate_invalid_file");
44
56
  }
45
- const text = buf.toString("utf8").replace(/�+$/, "");
46
- return redactSecretString(text.slice(0, maxPreviewChars));
57
+ const sizeBytes = Number(before.size);
58
+ if (sizeBytes > maxFileBytes)
59
+ return { sizeBytes, tooLarge: true };
60
+ const sha256 = await sha256File(handle);
61
+ const previewText = await readPreview(handle, maxPreviewChars);
62
+ const after = await handle.stat({ bigint: true });
63
+ if (!sameFileVersion(before, after)) {
64
+ throw new Error("workspace_candidate_changed_during_scan");
65
+ }
66
+ return { sizeBytes, tooLarge: false, sha256, previewText };
47
67
  }
48
68
  finally {
49
69
  await handle.close();
@@ -92,23 +112,18 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
92
112
  const rel = path.relative(workspaceDir, absPath).split(path.sep).join("/");
93
113
  if (!isSafeRelativePath(rel))
94
114
  continue;
95
- let sizeBytes;
115
+ let scanned;
96
116
  try {
97
- const handle = await open(absPath, "r");
98
- try {
99
- sizeBytes = (await handle.stat()).size;
100
- }
101
- finally {
102
- await handle.close();
103
- }
117
+ scanned = await scanStableFile(absPath, maxPreviewChars, maxFileBytes);
104
118
  }
105
119
  catch {
106
120
  truncated = true;
107
121
  continue;
108
122
  }
123
+ const { sizeBytes } = scanned;
109
124
  observedFileCount += 1;
110
125
  observedTotalBytes += sizeBytes;
111
- if (sizeBytes > maxFileBytes) {
126
+ if (scanned.tooLarge) {
112
127
  truncated = true;
113
128
  continue;
114
129
  }
@@ -116,24 +131,16 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
116
131
  truncated = true;
117
132
  continue;
118
133
  }
119
- let sha256;
120
- let previewText;
121
- try {
122
- sha256 = await sha256File(absPath);
123
- previewText = await readPreview(absPath, maxPreviewChars);
124
- }
125
- catch {
126
- truncated = true;
127
- continue;
128
- }
129
134
  files.push({
130
135
  absPath,
131
136
  event: "created",
132
137
  path: rel,
133
138
  name: entry.name,
134
139
  size_bytes: sizeBytes,
135
- sha256,
136
- ...(previewText !== null && previewText !== "" ? { preview_text: previewText } : {}),
140
+ sha256: scanned.sha256,
141
+ ...(scanned.previewText !== null && scanned.previewText !== ""
142
+ ? { preview_text: scanned.previewText }
143
+ : {}),
137
144
  });
138
145
  }
139
146
  }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export * from "./workspace.js";
14
14
  export * from "./workspace-entry-set.js";
15
15
  export * from "./workspace-materialization.js";
16
16
  export * from "./workspace-snapshot-staging.js";
17
+ export * from "./workspace-snapshot-policy.js";
18
+ export * from "./workspace-snapshot-control.js";
17
19
  export * from "./workspace-restore.js";
18
20
  export * from "./transcript.js";
19
21
  export * from "./file-candidates.js";
package/dist/index.js CHANGED
@@ -14,6 +14,8 @@ export * from "./workspace.js";
14
14
  export * from "./workspace-entry-set.js";
15
15
  export * from "./workspace-materialization.js";
16
16
  export * from "./workspace-snapshot-staging.js";
17
+ export * from "./workspace-snapshot-policy.js";
18
+ export * from "./workspace-snapshot-control.js";
17
19
  export * from "./workspace-restore.js";
18
20
  export * from "./transcript.js";
19
21
  export * from "./file-candidates.js";
@@ -23,7 +23,7 @@ export interface PreparedPersistentTurn {
23
23
  export interface PersistentSessionExecution {
24
24
  prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
25
25
  persistNativeSession(sessionId: string): void;
26
- finishTurn(payload: RunStartPayload): void;
26
+ finishTurn(payload: RunStartPayload): Promise<void>;
27
27
  }
28
28
  export interface RunReportingClient {
29
29
  postEvent(agentRunId: string, event: RunEvent): Promise<void | RunEventReceipt>;
@@ -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 instanceof RuntimeProfileApplyError
955
- ? err.code
956
- : err instanceof InputAttachmentMaterializationError
955
+ const errorType = isWorkspaceQuotaExceededError(err)
956
+ ? "workspace_user_quota_exceeded"
957
+ : err instanceof RuntimeProfileApplyError
957
958
  ? err.code
958
- : err instanceof RuntimeExecutionError
959
- ? err.errorType
960
- : "runtime_error";
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
  : {});
@@ -979,7 +982,7 @@ export class RunDispatcher {
979
982
  });
980
983
  }
981
984
  try {
982
- this.persistentSession?.finishTurn(payload);
985
+ await this.persistentSession?.finishTurn(payload);
983
986
  }
984
987
  finally {
985
988
  this.inflight.delete(runId);
@@ -42,11 +42,15 @@ export function clearAgentServiceControlEnv(env = process.env) {
42
42
  export function runtimeChildEnv(env = process.env) {
43
43
  const childEnv = { ...env };
44
44
  const runtimeHome = childEnv.BOTLEARN_RUNTIME_HOME;
45
+ const activationId = childEnv.BOTLEARN_AGENT_SERVICE_ACTIVATION_ID;
45
46
  clearAgentServiceControlEnv(childEnv);
46
47
  for (const key of AGENT_SERVICE_SUPERVISOR_ENV_KEYS)
47
48
  delete childEnv[key];
48
49
  if (runtimeHome)
49
50
  childEnv.HOME = runtimeHome;
51
+ if (activationId && /^[A-Za-z0-9_-]{1,120}$/u.test(activationId)) {
52
+ childEnv.BOTLEARN_RUNTIME_ACTIVATION_SCOPE = activationId;
53
+ }
50
54
  return childEnv;
51
55
  }
52
56
  /**
@@ -146,10 +150,6 @@ export function runtimeChildLaunch(binary, args, env = process.env, options = {}
146
150
  "-n",
147
151
  "-H",
148
152
  "-E",
149
- "-u",
150
- runtimeUser,
151
- "-g",
152
- runtimeGroup,
153
153
  "--",
154
154
  MANAGED_RUNTIME_LAUNCHER,
155
155
  binary,
@@ -0,0 +1,19 @@
1
+ export interface WorkspaceWriterFreezeProof {
2
+ readonly proofVersion: "workspace-writer-freeze-proof/1";
3
+ readonly sandboxId: string;
4
+ readonly sandboxGeneration: number;
5
+ readonly runtimeSessionId: string;
6
+ readonly checkpointId: string;
7
+ readonly allWritersFrozen: true;
8
+ }
9
+ export declare function freezeRuntimeWriters(): Promise<void>;
10
+ export declare function thawRuntimeWriters(): Promise<void>;
11
+ export declare function assertWorkspaceWriterFreezeSupported(): void;
12
+ export declare function quiesceRuntimeWriters(activationId: string | null): Promise<void>;
13
+ export declare function issueWorkspaceWriterFreezeProof(scope: {
14
+ sandboxId: string;
15
+ sandboxGeneration: number;
16
+ runtimeSessionId: string;
17
+ checkpointId: string;
18
+ }): WorkspaceWriterFreezeProof;
19
+ export declare function isWorkspaceWriterFreezeProof(value: object): value is WorkspaceWriterFreezeProof;
@@ -0,0 +1,130 @@
1
+ import { execFile } from "node:child_process";
2
+ import { accessSync, constants as fsConstants } from "node:fs";
3
+ import { readFile, readdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ const RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
8
+ const RUNTIME_CGROUP_ROOT = "/sys/fs/cgroup/botlearn-runtime";
9
+ const RUNTIME_CGROUP_UNAVAILABLE = "/run/botlearn-runtime-cgroup-unavailable";
10
+ const proofs = new WeakSet();
11
+ const sleep = (milliseconds) => new Promise((resolve) => {
12
+ setTimeout(resolve, milliseconds);
13
+ });
14
+ async function setDirectCgroupsFrozen(frozen) {
15
+ try {
16
+ accessSync(RUNTIME_CGROUP_UNAVAILABLE, fsConstants.F_OK);
17
+ await execFileAsync(RUNTIME_LAUNCHER, [frozen ? "--freeze-all" : "--thaw-all"], {
18
+ timeout: 10_000,
19
+ });
20
+ return;
21
+ }
22
+ catch (error) {
23
+ if (error.code !== "ENOENT")
24
+ throw error;
25
+ }
26
+ let entries;
27
+ try {
28
+ entries = await readdir(RUNTIME_CGROUP_ROOT, { withFileTypes: true });
29
+ }
30
+ catch (error) {
31
+ if (error.code === "ENOENT")
32
+ return;
33
+ throw error;
34
+ }
35
+ for (const entry of entries) {
36
+ if (!entry.isDirectory())
37
+ continue;
38
+ const directory = path.join(RUNTIME_CGROUP_ROOT, entry.name);
39
+ await writeFile(path.join(directory, "cgroup.freeze"), frozen ? "1" : "0");
40
+ for (let attempt = 0; attempt < 100; attempt += 1) {
41
+ const events = await readFile(path.join(directory, "cgroup.events"), "utf8");
42
+ if (events.includes(`frozen ${frozen ? 1 : 0}`))
43
+ break;
44
+ if (attempt === 99)
45
+ throw new Error("workspace_runtime_freeze_timeout");
46
+ await sleep(50);
47
+ }
48
+ }
49
+ }
50
+ async function setRuntimeWritersFrozen(frozen) {
51
+ const mode = process.env.BOTLEARN_RUNTIME_LAUNCH_MODE;
52
+ if (mode === "direct-uid") {
53
+ await setDirectCgroupsFrozen(frozen);
54
+ return;
55
+ }
56
+ if (mode === "sudo") {
57
+ await execFileAsync("/usr/bin/sudo", ["-n", "--", RUNTIME_LAUNCHER, frozen ? "--freeze-all" : "--thaw-all"], { timeout: 10_000 });
58
+ return;
59
+ }
60
+ if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
61
+ return;
62
+ }
63
+ throw new Error("workspace_runtime_cgroup_unavailable");
64
+ }
65
+ export async function freezeRuntimeWriters() {
66
+ await setRuntimeWritersFrozen(true);
67
+ }
68
+ export async function thawRuntimeWriters() {
69
+ await setRuntimeWritersFrozen(false);
70
+ }
71
+ export function assertWorkspaceWriterFreezeSupported() {
72
+ const mode = process.env.BOTLEARN_RUNTIME_LAUNCH_MODE;
73
+ if (mode === "direct-uid") {
74
+ try {
75
+ accessSync(RUNTIME_CGROUP_UNAVAILABLE, fsConstants.F_OK);
76
+ accessSync(RUNTIME_LAUNCHER, fsConstants.X_OK);
77
+ }
78
+ catch (error) {
79
+ if (error.code !== "ENOENT")
80
+ throw error;
81
+ accessSync(RUNTIME_CGROUP_ROOT, fsConstants.R_OK | fsConstants.W_OK);
82
+ }
83
+ return;
84
+ }
85
+ if (mode === "sudo") {
86
+ accessSync(RUNTIME_LAUNCHER, fsConstants.X_OK);
87
+ return;
88
+ }
89
+ if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
90
+ return;
91
+ }
92
+ throw new Error("workspace_runtime_cgroup_unavailable");
93
+ }
94
+ function managedQuiescenceCommand(activationId) {
95
+ const mode = process.env.BOTLEARN_RUNTIME_LAUNCH_MODE;
96
+ if (mode === "direct-uid") {
97
+ return [RUNTIME_LAUNCHER, [activationId ? "--quiesce" : "--quiesce-all", ...(activationId ? [activationId] : [])]];
98
+ }
99
+ if (mode === "sudo") {
100
+ return [
101
+ "/usr/bin/sudo",
102
+ ["-n", "--", RUNTIME_LAUNCHER, activationId ? "--quiesce" : "--quiesce-all", ...(activationId ? [activationId] : [])],
103
+ ];
104
+ }
105
+ return null;
106
+ }
107
+ export async function quiesceRuntimeWriters(activationId) {
108
+ const command = managedQuiescenceCommand(activationId);
109
+ if (command === null) {
110
+ // Local/BYOA runtimes execute as the daemon user and are not eligible for the managed
111
+ // durable capability. Unit/e2e fake runtimes have no descendant writer to reap.
112
+ if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
113
+ return;
114
+ }
115
+ throw new Error("workspace_runtime_cgroup_unavailable");
116
+ }
117
+ await execFileAsync(command[0], command[1], { timeout: 10_000 });
118
+ }
119
+ export function issueWorkspaceWriterFreezeProof(scope) {
120
+ const proof = Object.freeze({
121
+ proofVersion: "workspace-writer-freeze-proof/1",
122
+ ...scope,
123
+ allWritersFrozen: true,
124
+ });
125
+ proofs.add(proof);
126
+ return proof;
127
+ }
128
+ export function isWorkspaceWriterFreezeProof(value) {
129
+ return proofs.has(value);
130
+ }
@@ -1000,6 +1000,17 @@ function normalizeDeepseekEvent(eventName, payload, seq) {
1000
1000
  status: deepseekToolFailed(payload) ? "error" : "completed",
1001
1001
  };
1002
1002
  }
1003
+ if (eventName === "item.delta" && isDeepseekReasoningEvent(eventName, payload)) {
1004
+ const text = extractDeepseekDelta(payload);
1005
+ return text
1006
+ ? {
1007
+ raw: { event: eventName, payload },
1008
+ kind: "thinking",
1009
+ seq,
1010
+ text,
1011
+ }
1012
+ : null;
1013
+ }
1003
1014
  if (eventName === "item.delta" && isAgentMessageDelta(payload)) {
1004
1015
  return {
1005
1016
  raw: { event: eventName, payload },
@@ -13,12 +13,13 @@ export interface ToolObservationPayload extends Record<string, unknown> {
13
13
  redacted: boolean;
14
14
  }
15
15
  /**
16
- * Project a provider tool envelope into durable teacher evidence.
16
+ * Project a provider tool envelope into durable learner-preview and teaching-audit evidence.
17
17
  *
18
18
  * Only allowlisted operational argument/result fields survive, credentials are removed,
19
- * and host paths become workspace-relative. `detail_preview` stays a bounded feed-sized
20
- * excerpt; `detail_full` carries the same allowlisted projection at audit fidelity (deeper
21
- * walk, larger arrays, 64k chars). Message text, request IDs, and arbitrary envelope
22
- * metadata never cross this boundary.
19
+ * and paths inside the current workspace become workspace-relative. Other semantic content,
20
+ * including execution-host paths outside that workspace, stays intact. `detail_preview` is
21
+ * a bounded feed-sized excerpt; `detail_full` carries the same allowlisted projection at
22
+ * audit fidelity (deeper walk, larger arrays, 64k chars). Message text, request IDs, and
23
+ * arbitrary envelope metadata never cross this boundary.
23
24
  */
24
25
  export declare function buildToolObservation(block: RuntimeBlock, runtime: string, workspaceDir: string): ToolObservationPayload | null;
@@ -39,13 +39,14 @@ const RESULT_FIELDS = new Set([
39
39
  "bytes",
40
40
  ]);
41
41
  /**
42
- * Project a provider tool envelope into durable teacher evidence.
42
+ * Project a provider tool envelope into durable learner-preview and teaching-audit evidence.
43
43
  *
44
44
  * Only allowlisted operational argument/result fields survive, credentials are removed,
45
- * and host paths become workspace-relative. `detail_preview` stays a bounded feed-sized
46
- * excerpt; `detail_full` carries the same allowlisted projection at audit fidelity (deeper
47
- * walk, larger arrays, 64k chars). Message text, request IDs, and arbitrary envelope
48
- * metadata never cross this boundary.
45
+ * and paths inside the current workspace become workspace-relative. Other semantic content,
46
+ * including execution-host paths outside that workspace, stays intact. `detail_preview` is
47
+ * a bounded feed-sized excerpt; `detail_full` carries the same allowlisted projection at
48
+ * audit fidelity (deeper walk, larger arrays, 64k chars). Message text, request IDs, and
49
+ * arbitrary envelope metadata never cross this boundary.
49
50
  */
50
51
  export function buildToolObservation(block, runtime, workspaceDir) {
51
52
  if (block.kind !== "tool_call" && block.kind !== "tool_result")
@@ -17,5 +17,5 @@ export interface ReasoningTracePayload extends Record<string, unknown> {
17
17
  * envelope stays local to the sandbox transcript.
18
18
  */
19
19
  export declare function extractReasoningText(raw: unknown): string;
20
- /** Project accumulated reasoning text into one durable, redacted, bounded trace payload. */
20
+ /** Project reasoning into one bounded payload that filters credentials and preserves semantics. */
21
21
  export declare function buildReasoningTracePayload(text: string, runtime: string, alreadyTruncated?: boolean): ReasoningTracePayload;
@@ -40,7 +40,7 @@ function collectReasoningStrings(value, depth, out) {
40
40
  collectReasoningStrings(item, depth + 1, out);
41
41
  }
42
42
  }
43
- /** Project accumulated reasoning text into one durable, redacted, bounded trace payload. */
43
+ /** Project reasoning into one bounded payload that filters credentials and preserves semantics. */
44
44
  export function buildReasoningTracePayload(text, runtime, alreadyTruncated = false) {
45
45
  const redactedText = redactSecretString(text);
46
46
  const truncated = alreadyTruncated || redactedText.length > REASONING_TRACE_MAX_CHARS;
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,20 @@
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
+ source: string;
7
+ mountOptions: Set<string>;
8
+ superOptions: Set<string>;
9
+ }
10
+ export interface WorkspaceCapacityProbes {
11
+ statfsBytes(workspaceRoot: string): bigint;
12
+ backingFileBytes(imagePath: string): bigint;
13
+ }
14
+ export declare function parseWorkspaceMountInfo(text: string): MountInfoEntry[];
15
+ export declare function nasWorkspaceEnabled(env?: NodeJS.ProcessEnv): boolean;
16
+ export declare function workspaceFilesystemFlushCommand(env?: NodeJS.ProcessEnv): [string, string[]] | null;
17
+ export declare function flushWorkspaceFilesystem(env?: NodeJS.ProcessEnv): Promise<void>;
18
+ export declare function isWorkspaceQuotaExceededError(error: unknown): boolean;
19
+ export declare function assertWorkspaceFilesystem(workspaceRoot: string, env?: NodeJS.ProcessEnv, mountInfo?: string, capacityProbes?: WorkspaceCapacityProbes): void;
20
+ export {};