toc-ocra 0.2.8 → 0.2.9

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 CHANGED
@@ -73,6 +73,14 @@ node dist/src/index.js run
73
73
  - **服务端对账兜底**:心跳会上报本机正在运行的任务,服务端据此释放本机已经不存在的运行;Agent 崩溃重启时,会清理上次遗留的 CLI 进程。
74
74
  - **加一个 CLI**:在 `src/providers/cli/` 新增一个文件,实现 `HeadlessProvider`(启动参数 + 解析每一行输出),再在 `src/providers/registry.ts` 加一行。
75
75
 
76
+ ### 看板附件
77
+
78
+ 任务链附件在 CLI 启动前由本机代理下载到本次实际工作目录的 `.echo/attachments/message-{message_id}/`,完成大小与 SHA-256 校验后,提示词中的附件引用会替换为本机绝对路径。下载目录必须属于已允许的 CLI 工作目录,不能通过符号链接跳出;未配置设备默认目录或工位工作目录时会拒绝附件任务。每条消息最多 16 个附件,准备过程可取消、可复用已校验文件,最长等待 15 分钟。
79
+
80
+ 窗口工位使用同机代理的 `run.files.prepare` 先下载文件,然后由服务端转发路径;同机设备代理必须在线。更新后的代理通过心跳声明 `task_attachments.version=1`,服务端会拒绝把附件任务发给不支持此协议的旧代理。请更新并重启设备代理与对应后端后使用。
81
+
82
+ 下载文件暂时保留,可手工清理;`.echo` 不进入设备目录扫描,并带有仅作用于该内部目录的 Git 忽略规则。视频也可下载为文件,实际解析取决于 CLI 的能力。
83
+
76
84
  详见 [docs/architecture.md](docs/architecture.md)。
77
85
 
78
86
  ## 开发
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/plugin-service.ts
9
9
  import { existsSync as existsSync6 } from "node:fs";
10
- import { delimiter as delimiter5, join as join9 } from "node:path";
10
+ import { delimiter as delimiter5, join as join10 } from "node:path";
11
11
 
12
12
  // src/config.ts
13
13
  import { randomUUID } from "node:crypto";
@@ -15731,8 +15731,8 @@ var CliDetectionService = class {
15731
15731
  var cliDetection = new CliDetectionService();
15732
15732
 
15733
15733
  // src/runtime/agent-runtime.ts
15734
- import { randomUUID as randomUUID2 } from "node:crypto";
15735
- import { join as join8 } from "node:path";
15734
+ import { randomUUID as randomUUID3 } from "node:crypto";
15735
+ import { join as join9 } from "node:path";
15736
15736
 
15737
15737
  // src/exec/script-runner.ts
15738
15738
  import { spawn as spawn3 } from "node:child_process";
@@ -16717,8 +16717,8 @@ function commandOf(pid) {
16717
16717
 
16718
16718
  // src/runs/run-supervisor.ts
16719
16719
  import { spawn as spawn7 } from "node:child_process";
16720
- import { mkdir as mkdir4, realpath, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
16721
- import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve3 } from "node:path";
16720
+ import { mkdir as mkdir5, realpath as realpath2, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
16721
+ import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve3 } from "node:path";
16722
16722
  import { createInterface as createInterface2 } from "node:readline";
16723
16723
 
16724
16724
  // src/session/terminal-line-sanitizer.ts
@@ -16837,6 +16837,88 @@ function clipToCharacterBudget(rows, budget) {
16837
16837
  return { lines: rows, limited: false };
16838
16838
  }
16839
16839
 
16840
+ // src/runs/task-attachments.ts
16841
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
16842
+ import { constants as constants2 } from "node:fs";
16843
+ import { lstat, mkdir as mkdir4, open, realpath, rename as rename4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
16844
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative } from "node:path";
16845
+ function within(root, path) {
16846
+ const segment = relative(root, path);
16847
+ return segment === "" || !segment.startsWith("..") && !isAbsolute2(segment);
16848
+ }
16849
+ async function attachmentDirectory(workdir, messageId) {
16850
+ let directory = workdir;
16851
+ for (const segment of [".echo", "attachments", `message-${encodeURIComponent(messageId).slice(0, 180)}`]) {
16852
+ directory = join6(directory, segment);
16853
+ await mkdir4(directory, { mode: 448 }).catch((error51) => {
16854
+ if (error51.code !== "EEXIST") throw error51;
16855
+ });
16856
+ const metadata = await lstat(directory);
16857
+ if (metadata.isSymbolicLink() || !metadata.isDirectory() || !within(workdir, await realpath(directory))) {
16858
+ throw new Error("\u9644\u4EF6\u76EE\u5F55\u5FC5\u987B\u4F4D\u4E8E CLI \u5DE5\u4F5C\u76EE\u5F55\u5185\uFF0C\u4E14\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5");
16859
+ }
16860
+ if (segment === ".echo") await writeFile4(join6(directory, ".gitignore"), "*\n", { flag: "wx", mode: 384 }).catch((error51) => {
16861
+ if (error51.code !== "EEXIST") throw error51;
16862
+ });
16863
+ }
16864
+ return directory;
16865
+ }
16866
+ async function matches(path, size, sha256, signal2) {
16867
+ const metadata = await lstat(path).catch(() => void 0);
16868
+ if (!metadata?.isFile() || metadata.isSymbolicLink() || metadata.size !== size) return false;
16869
+ const file2 = await open(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
16870
+ try {
16871
+ const hash2 = createHash2("sha256");
16872
+ let read = 0;
16873
+ for await (const chunk of file2.createReadStream({ autoClose: false })) {
16874
+ signal2.throwIfAborted();
16875
+ read += chunk.length;
16876
+ if (read > size) return false;
16877
+ hash2.update(chunk);
16878
+ }
16879
+ return hash2.digest("hex") === sha256;
16880
+ } finally {
16881
+ await file2.close();
16882
+ }
16883
+ }
16884
+ async function prepareTaskAttachments(payload, workdir, download, signal2) {
16885
+ const files = payload.task_attachments ?? [];
16886
+ if (!files.length) return payload.prompt;
16887
+ const directory = await attachmentDirectory(workdir, payload.message_id || payload.run_id);
16888
+ let prompt = payload.prompt;
16889
+ const paths = [];
16890
+ for (const file2 of files) {
16891
+ signal2.throwIfAborted();
16892
+ const token = `{{echo-file:${file2.file_id}}}`;
16893
+ if (file2.prompt_token !== token) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u5360\u4F4D\u7B26\u65E0\u6548\uFF0C\u8BF7\u66F4\u65B0\u670D\u52A1\u5668");
16894
+ const characters = Array.from(basename2(file2.name.replace(/\\/g, "/")).replace(/[^\p{L}\p{N}._-]/gu, "_").replace(/\.+$/u, ""));
16895
+ while (Buffer.byteLength(characters.join("")) > 160) characters.shift();
16896
+ const name = characters.join("") || "attachment";
16897
+ const path = join6(directory, `${file2.file_id}-${name}`);
16898
+ if (!await matches(path, file2.size, file2.sha256, signal2)) {
16899
+ const temporary = join6(directory, `.download-${file2.file_id}-${randomUUID2()}.part`);
16900
+ try {
16901
+ await download(payload.run_id, file2.file_id, temporary, file2.size, file2.sha256, signal2);
16902
+ signal2.throwIfAborted();
16903
+ if (!within(workdir, await realpath(directory))) throw new Error("\u9644\u4EF6\u76EE\u5F55\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u8BD5");
16904
+ await rename4(temporary, path);
16905
+ } finally {
16906
+ await rm2(temporary, { force: true });
16907
+ }
16908
+ }
16909
+ if (!within(workdir, await realpath(path))) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u4E0D\u5728 CLI \u5DE5\u4F5C\u76EE\u5F55\u5185");
16910
+ prompt = prompt.split(token).join(path);
16911
+ paths.push(`- ${JSON.stringify(path)}\uFF08${file2.mime}\uFF0C${file2.size} \u5B57\u8282\uFF09`);
16912
+ }
16913
+ if (prompt.includes("{{echo-file:")) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u672A\u5B8C\u6574\u89E3\u6790");
16914
+ prompt += `
16915
+
16916
+ \u9644\u4EF6\u5DF2\u7531\u8BBE\u5907\u4EE3\u7406\u4E0B\u8F7D\u5230\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u3002\u8BF7\u76F4\u63A5\u8BFB\u53D6\u4EE5\u4E0B\u672C\u673A\u6587\u4EF6\uFF0C\u65E0\u9700\u4E0B\u8F7D\uFF1A
16917
+ ${paths.join("\n")}`;
16918
+ if (Buffer.byteLength(prompt) > 512 * 1024) throw new Error("\u5305\u542B\u9644\u4EF6\u8DEF\u5F84\u7684\u6D88\u606F\u8FC7\u957F");
16919
+ return prompt;
16920
+ }
16921
+
16840
16922
  // src/runs/run-supervisor.ts
16841
16923
  var COMPLETION_EXIT_GRACE_MS = 15e3;
16842
16924
  var RECENT_RUN_TTL_MS = 10 * 6e4;
@@ -16858,12 +16940,13 @@ var RunSupervisor = class {
16858
16940
  /** Run ids whose finish report has not been acknowledged yet. */
16859
16941
  unreported = /* @__PURE__ */ new Set();
16860
16942
  chatRunIds = /* @__PURE__ */ new Set();
16943
+ preparing = /* @__PURE__ */ new Map();
16861
16944
  get activeCount() {
16862
- return this.active.size;
16945
+ return this.active.size + this.preparing.size;
16863
16946
  }
16864
16947
  /** Runs to list in the heartbeat: still running, or finished but not yet reported. */
16865
16948
  activeRunIds() {
16866
- return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.unreported])];
16949
+ return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.preparing.keys(), ...this.unreported])];
16867
16950
  }
16868
16951
  activeChatRunIds() {
16869
16952
  return this.activeRunIds().filter((id) => this.chatRunIds.has(id));
@@ -16885,6 +16968,16 @@ var RunSupervisor = class {
16885
16968
  * directory, unknown CLI) are reported as a failed run, never thrown.
16886
16969
  */
16887
16970
  async start(payload) {
16971
+ if (this.preparing.has(payload.run_id)) return;
16972
+ const controller = new AbortController();
16973
+ this.preparing.set(payload.run_id, { workstationId: payload.workstation_id, controller });
16974
+ try {
16975
+ await this.startPrepared(payload, controller.signal);
16976
+ } finally {
16977
+ this.preparing.delete(payload.run_id);
16978
+ }
16979
+ }
16980
+ async startPrepared(payload, signal2) {
16888
16981
  if (this.active.has(payload.run_id) || this.recent.has(payload.run_id)) {
16889
16982
  this.logger.info("run-duplicate-ignored", { runId: payload.run_id });
16890
16983
  return;
@@ -16895,20 +16988,32 @@ var RunSupervisor = class {
16895
16988
  this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed", chat: !!payload.chat_session_id });
16896
16989
  await this.report(payload.run_id, { status: "failed", error: error51, duration_ms: 0 });
16897
16990
  };
16898
- if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) {
16991
+ if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id) || [...this.preparing].some(([id, run2]) => id !== payload.run_id && run2.workstationId === payload.workstation_id)) {
16899
16992
  return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
16900
16993
  }
16901
- if (this.active.size >= this.config.maxConcurrentRuns) {
16994
+ if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) {
16902
16995
  return refuse(`\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650\uFF08${this.config.maxConcurrentRuns}\uFF09`);
16903
16996
  }
16904
16997
  let provider;
16905
16998
  let workdir;
16906
16999
  try {
16907
17000
  provider = resolveHeadlessProvider(payload.agent_type);
17001
+ if (payload.task_attachments?.length && !payload.workdir) throw new Error("\u9644\u4EF6\u9700\u8981\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
16908
17002
  workdir = payload.chat_session_id && !payload.workdir ? await this.defaultChatWorkdir() : await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
16909
17003
  } catch (error51) {
16910
17004
  return refuse(errorMessage(error51));
16911
17005
  }
17006
+ try {
17007
+ if (payload.task_attachments?.length) payload = { ...payload, prompt: await this.prepareTaskFiles(payload, signal2, workdir) };
17008
+ signal2.throwIfAborted();
17009
+ } catch (error51) {
17010
+ if (signal2.aborted) {
17011
+ this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "cancelled", chat: !!payload.chat_session_id });
17012
+ await this.report(payload.run_id, { status: "cancelled", error: "\u9644\u4EF6\u51C6\u5907\u5DF2\u53D6\u6D88", duration_ms: 0 });
17013
+ return;
17014
+ }
17015
+ return refuse(`\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25\uFF1A${errorMessage(error51)}`);
17016
+ }
16912
17017
  const request = {
16913
17018
  prompt: payload.prompt,
16914
17019
  sessionId: payload.session_id,
@@ -16927,7 +17032,7 @@ var RunSupervisor = class {
16927
17032
  }
16928
17033
  if (!payload.prompt && !payload.attachments?.length) return refuse("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
16929
17034
  if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
16930
- if (this.active.size >= this.config.maxConcurrentRuns) return refuse("\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650");
17035
+ if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) return refuse("\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650");
16931
17036
  const launch = provider.buildLaunch({
16932
17037
  prompt: payload.prompt,
16933
17038
  sessionId: payload.session_id,
@@ -16976,6 +17081,7 @@ var RunSupervisor = class {
16976
17081
  timers: []
16977
17082
  };
16978
17083
  this.active.set(run.runId, run);
17084
+ this.preparing.delete(run.runId);
16979
17085
  this.unreported.add(run.runId);
16980
17086
  if (run.pid > 1) this.orphans?.add({ runId: run.runId, pid: run.pid, executable: launch.executable, startedAt: run.startedAt });
16981
17087
  run.tail.append(`\u25CF ${provider.id} \u542F\u52A8\u4E8E ${workdir}${payload.session_id ? `\uFF08\u7EE7\u7EED\u4F1A\u8BDD ${payload.session_id}\uFF09` : ""}
@@ -16995,7 +17101,7 @@ var RunSupervisor = class {
16995
17101
  run.tail.append(`${clean}
16996
17102
  `);
16997
17103
  });
16998
- child.once("close", (code, signal2) => void this.onExit(run, code, signal2, spawnError));
17104
+ child.once("close", (code, signal3) => void this.onExit(run, code, signal3, spawnError));
16999
17105
  if (provider.startInput) {
17000
17106
  child.once("spawn", () => {
17001
17107
  try {
@@ -17130,16 +17236,16 @@ var RunSupervisor = class {
17130
17236
  async prepareChatAttachments(payload) {
17131
17237
  if (!payload.attachments?.length) return [];
17132
17238
  if (!this.reporter.chatMedia) throw new Error("\u9644\u4EF6\u8F6C\u53D1\u672A\u914D\u7F6E");
17133
- const directory = join6(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
17134
- await mkdir4(directory, { recursive: true, mode: 448 });
17239
+ const directory = join7(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
17240
+ await mkdir5(directory, { recursive: true, mode: 448 });
17135
17241
  const files = [];
17136
17242
  for (const attachment of payload.attachments) {
17137
- const name = basename2(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
17138
- const path = join6(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
17243
+ const name = basename3(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
17244
+ const path = join7(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
17139
17245
  const existing = await stat2(path).catch(() => void 0);
17140
17246
  if (!existing?.isFile()) {
17141
17247
  const data = await this.reporter.chatMedia(payload.run_id, attachment.media_id);
17142
- await writeFile4(path, data, { mode: 384 });
17248
+ await writeFile5(path, data, { mode: 384 });
17143
17249
  }
17144
17250
  files.push({ path, name: attachment.name, kind: attachment.kind });
17145
17251
  }
@@ -17152,7 +17258,14 @@ var RunSupervisor = class {
17152
17258
  /** Stops a run: the whole process tree, then a `cancelled` report. */
17153
17259
  async cancel(runId) {
17154
17260
  const run = this.active.get(runId);
17155
- if (!run) return false;
17261
+ if (!run) {
17262
+ const preparing = this.preparing.get(runId);
17263
+ if (preparing) {
17264
+ preparing.controller.abort();
17265
+ return true;
17266
+ }
17267
+ return false;
17268
+ }
17156
17269
  run.cancelRequested = true;
17157
17270
  run.tail.append("\u25CF \u6536\u5230\u505C\u6B62\u6307\u4EE4\uFF0C\u6B63\u5728\u7ED3\u675F\u8FDB\u7A0B\u6811\n");
17158
17271
  this.logger.info("run-cancel", { runId, pid: run.pid });
@@ -17175,6 +17288,7 @@ var RunSupervisor = class {
17175
17288
  return { ...read, status: this.active.has(target.runId) ? "running" : "finished", run_id: target.runId };
17176
17289
  }
17177
17290
  async disposeAll() {
17291
+ for (const entry of this.preparing.values()) entry.controller.abort();
17178
17292
  await Promise.all([...this.active.values()].map((run) => {
17179
17293
  run.cancelRequested = true;
17180
17294
  return killProcessTree(run.pid, 1e3);
@@ -17200,17 +17314,22 @@ var RunSupervisor = class {
17200
17314
  if (!input) throw new Error("\u6CA1\u6709\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55");
17201
17315
  let candidate;
17202
17316
  try {
17203
- candidate = await realpath(isAbsolute2(input) ? input : resolve3(input));
17317
+ candidate = await realpath2(isAbsolute3(input) ? input : resolve3(input));
17204
17318
  } catch {
17205
17319
  throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${input}`);
17206
17320
  }
17207
17321
  for (const root of this.config.allowedRoots) {
17208
- const allowed = await realpath(root).catch(() => resolve3(root));
17209
- const segment = relative(allowed, candidate);
17210
- if (segment === "" || !segment.startsWith("..") && !isAbsolute2(segment)) return candidate;
17322
+ const allowed = await realpath2(root).catch(() => resolve3(root));
17323
+ const segment = relative2(allowed, candidate);
17324
+ if (segment === "" || !segment.startsWith("..") && !isAbsolute3(segment)) return candidate;
17211
17325
  }
17212
17326
  throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5728\u5141\u8BB8\u8303\u56F4\u5185\uFF1A${input}`);
17213
17327
  }
17328
+ async prepareTaskFiles(payload, signal2 = AbortSignal.timeout(15 * 6e4), workdir) {
17329
+ if (!payload.workdir) throw new Error("\u8BF7\u5148\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
17330
+ if (!this.reporter.taskFile) throw new Error("\u4EFB\u52A1\u9644\u4EF6\u4E0B\u8F7D\u672A\u914D\u7F6E");
17331
+ return prepareTaskAttachments(payload, workdir ?? await this.validateWorkdir(payload.workdir), this.reporter.taskFile, AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)]));
17332
+ }
17214
17333
  };
17215
17334
  function errorMessage(error51) {
17216
17335
  return error51 instanceof Error ? error51.message : String(error51);
@@ -17232,6 +17351,10 @@ function sanitizeChatEvent(event) {
17232
17351
  }
17233
17352
 
17234
17353
  // src/transport/gateway-client.ts
17354
+ import { createHash as createHash3 } from "node:crypto";
17355
+ import { createWriteStream } from "node:fs";
17356
+ import { Readable, Transform } from "node:stream";
17357
+ import { pipeline } from "node:stream/promises";
17235
17358
  var GatewayError = class extends Error {
17236
17359
  constructor(message, status) {
17237
17360
  super(message);
@@ -17313,19 +17436,46 @@ var GatewayClient = class {
17313
17436
  runStarted(runId, body) {
17314
17437
  return this.request("POST", `/runs/${encodeURIComponent(runId)}/started`, body, 15e3);
17315
17438
  }
17439
+ async taskFile(runId, fileId, destination, size, sha256, signal2) {
17440
+ const response = await fetch(`${this.config.serverUrl}/api/agent/runs/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}`, {
17441
+ headers: { authorization: `Bearer ${this.config.token}` },
17442
+ redirect: "error",
17443
+ signal: AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)])
17444
+ });
17445
+ if (!response.ok || !response.body) throw new GatewayError("\u65E0\u6CD5\u8BFB\u53D6\u4EFB\u52A1\u9644\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u662F\u5426\u5DF2\u5220\u9664\u53CA\u8BBE\u5907\u51ED\u636E", response.status);
17446
+ const declared = response.headers.get("content-length");
17447
+ if (declared !== null && Number(declared) !== size) {
17448
+ await response.body.cancel();
17449
+ throw new Error("\u9644\u4EF6\u5927\u5C0F\u4E0E\u4FDD\u5B58\u7684\u8BB0\u5F55\u4E0D\u4E00\u81F4");
17450
+ }
17451
+ const hash2 = createHash3("sha256");
17452
+ let received = 0;
17453
+ const verifier = new Transform({ transform(chunk, _encoding, callback) {
17454
+ received += chunk.length;
17455
+ if (received > size) {
17456
+ callback(new Error("\u9644\u4EF6\u4E0B\u8F7D\u8D85\u51FA\u8BB0\u5F55\u5927\u5C0F"));
17457
+ return;
17458
+ }
17459
+ hash2.update(chunk);
17460
+ callback(null, chunk);
17461
+ } });
17462
+ const target = createWriteStream(destination, { flags: "wx", mode: 384 });
17463
+ await pipeline(Readable.fromWeb(response.body), verifier, target, { signal: signal2 });
17464
+ if (received !== size || hash2.digest("hex") !== sha256) throw new Error("\u9644\u4EF6\u4E0B\u8F7D\u4E0D\u5B8C\u6574\u6216\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
17465
+ }
17316
17466
  runFinished(runId, body) {
17317
17467
  return this.request("POST", `/runs/${encodeURIComponent(runId)}/finished`, body, 15e3);
17318
17468
  }
17319
17469
  };
17320
17470
 
17321
17471
  // src/workspace/directory-browser.ts
17322
- import { readdir, realpath as realpath2, stat as stat3 } from "node:fs/promises";
17472
+ import { readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
17323
17473
  import { homedir as homedir5 } from "node:os";
17324
17474
  import { dirname as dirname6, parse as parse3, resolve as resolve4 } from "node:path";
17325
17475
  var MAX_ENTRIES = 1e3;
17326
17476
  async function listDirectory(requested, allowedRoots) {
17327
17477
  const raw = requested.trim().replace(/^~(?=$|[\\/])/, homedir5()) || homedir5();
17328
- const directory = await realpath2(resolve4(raw)).catch(() => {
17478
+ const directory = await realpath3(resolve4(raw)).catch(() => {
17329
17479
  throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${requested}`);
17330
17480
  });
17331
17481
  if (!(await stat3(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
@@ -17357,12 +17507,13 @@ async function listDirectory(requested, allowedRoots) {
17357
17507
 
17358
17508
  // src/workspace/workspace-manager.ts
17359
17509
  import { spawn as spawn8 } from "node:child_process";
17360
- import { chmod as chmod3, mkdir as mkdir5, mkdtemp, opendir, realpath as realpath3, rm as rm2, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
17510
+ import { chmod as chmod3, mkdir as mkdir6, mkdtemp, opendir, realpath as realpath4, rm as rm3, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
17361
17511
  import { homedir as homedir6, tmpdir } from "node:os";
17362
- import { isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve5 } from "node:path";
17512
+ import { isAbsolute as isAbsolute4, join as join8, relative as relative3, resolve as resolve5 } from "node:path";
17363
17513
  var MAX_NODES = 2e3;
17364
17514
  var MAX_DIRECTORY_ENTRIES = 12e3;
17365
17515
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
17516
+ ".echo",
17366
17517
  ".git",
17367
17518
  "node_modules",
17368
17519
  ".next",
@@ -17408,12 +17559,12 @@ var WorkspaceManager = class {
17408
17559
  validateRootPath(value) {
17409
17560
  const raw = value.trim().replace(/^~(?=$|[\\/])/, homedir6());
17410
17561
  const root = resolve5(raw);
17411
- if (!isAbsolute3(raw)) throw new Error("\u6839\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
17412
- if (relative2(root, resolve5(root, "..")) === "") throw new Error("\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u4F5C\u4E3A\u76EE\u5F55\u6839");
17562
+ if (!isAbsolute4(raw)) throw new Error("\u6839\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
17563
+ if (relative3(root, resolve5(root, "..")) === "") throw new Error("\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u4F5C\u4E3A\u76EE\u5F55\u6839");
17413
17564
  return root;
17414
17565
  }
17415
17566
  async syncTree(rootPath, request) {
17416
- await mkdir5(rootPath, { recursive: true });
17567
+ await mkdir6(rootPath, { recursive: true });
17417
17568
  await addAllowedRoot(this.config, rootPath);
17418
17569
  const results = [];
17419
17570
  const requestedPaths = /* @__PURE__ */ new Set();
@@ -17437,7 +17588,7 @@ var WorkspaceManager = class {
17437
17588
  });
17438
17589
  continue;
17439
17590
  }
17440
- await mkdir5(target, { recursive: true });
17591
+ await mkdir6(target, { recursive: true });
17441
17592
  results.push({
17442
17593
  relative_path: relativePath,
17443
17594
  status: "created",
@@ -17486,7 +17637,7 @@ var WorkspaceManager = class {
17486
17637
  }
17487
17638
  if (!metadata) {
17488
17639
  if (!gitUrl) throw new Error("A missing repository needs a Git remote URL");
17489
- await mkdir5(resolve5(target, ".."), { recursive: true });
17640
+ await mkdir6(resolve5(target, ".."), { recursive: true });
17490
17641
  const cloneArgs = branch ? ["clone", "--branch", branch, "--single-branch", gitUrl, target] : ["clone", gitUrl, target];
17491
17642
  await this.runGit(cloneArgs, rootPath, timeoutSeconds, false, gitEnvironment);
17492
17643
  created = true;
@@ -17580,13 +17731,13 @@ var WorkspaceManager = class {
17580
17731
  };
17581
17732
  }
17582
17733
  async scanTree(rootPath, maxDepth, scanRoots) {
17583
- const resolvedRoot = await realpath3(rootPath);
17734
+ const resolvedRoot = await realpath4(rootPath);
17584
17735
  const rootMetadata = await stat4(resolvedRoot);
17585
17736
  if (!rootMetadata.isDirectory()) throw new Error("The workspace root is not a directory");
17586
17737
  const nodes = [];
17587
17738
  let entriesRead = 0;
17588
17739
  const appendNode = async (target) => {
17589
- const relativePath = relative2(resolvedRoot, target).replaceAll("\\", "/");
17740
+ const relativePath = relative3(resolvedRoot, target).replaceAll("\\", "/");
17590
17741
  if (!relativePath || !this.isSafeRelativePath(relativePath)) return void 0;
17591
17742
  const git = await this.readGitMetadata(target);
17592
17743
  if (git) {
@@ -17670,8 +17821,8 @@ var WorkspaceManager = class {
17670
17821
  if (result.exitCode !== 0 || !topLevel) return false;
17671
17822
  try {
17672
17823
  const [resolvedDirectory, resolvedTopLevel] = await Promise.all([
17673
- realpath3(directory),
17674
- realpath3(topLevel)
17824
+ realpath4(directory),
17825
+ realpath4(topLevel)
17675
17826
  ]);
17676
17827
  return resolvedDirectory === resolvedTopLevel;
17677
17828
  } catch {
@@ -17691,8 +17842,8 @@ var WorkspaceManager = class {
17691
17842
  }
17692
17843
  resolveWithinRoot(rootPath, relativePath) {
17693
17844
  const target = resolve5(rootPath, relativePath);
17694
- const relativeToRoot = relative2(rootPath, target);
17695
- if (relativeToRoot.startsWith("..") || isAbsolute3(relativeToRoot)) {
17845
+ const relativeToRoot = relative3(rootPath, target);
17846
+ if (relativeToRoot.startsWith("..") || isAbsolute4(relativeToRoot)) {
17696
17847
  throw new Error("A workspace path must remain under the configured workspace root");
17697
17848
  }
17698
17849
  return target;
@@ -17733,13 +17884,13 @@ var WorkspaceManager = class {
17733
17884
  }
17734
17885
  async createGitAuthEnvironment(auth) {
17735
17886
  if (!auth) return void 0;
17736
- const directory = await mkdtemp(join7(tmpdir(), "toc-ocra-git-auth-"));
17887
+ const directory = await mkdtemp(join8(tmpdir(), "toc-ocra-git-auth-"));
17737
17888
  const isWindows = process.platform === "win32";
17738
- const scriptPath = join7(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17889
+ const scriptPath = join8(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17739
17890
  let script;
17740
17891
  if (isWindows) {
17741
- const helperPath = join7(directory, "askpass.cjs");
17742
- await writeFile5(
17892
+ const helperPath = join8(directory, "askpass.cjs");
17893
+ await writeFile6(
17743
17894
  helperPath,
17744
17895
  'const prompt = process.argv.slice(2).join(" ");\nprocess.stdout.write(/username/i.test(prompt) ? process.env.ECHO_GIT_USERNAME ?? "" : process.env.ECHO_GIT_PASSWORD ?? "");\n',
17745
17896
  { mode: 448 }
@@ -17748,7 +17899,7 @@ var WorkspaceManager = class {
17748
17899
  } else {
17749
17900
  script = '#!/bin/sh\ncase "$1" in *Username*|*username*) printf "%s\\n" "$ECHO_GIT_USERNAME" ;; *) printf "%s\\n" "$ECHO_GIT_PASSWORD" ;; esac\n';
17750
17901
  }
17751
- await writeFile5(scriptPath, script, { mode: 448 });
17902
+ await writeFile6(scriptPath, script, { mode: 448 });
17752
17903
  if (!isWindows) await chmod3(scriptPath, 448);
17753
17904
  return {
17754
17905
  env: {
@@ -17759,7 +17910,7 @@ var WorkspaceManager = class {
17759
17910
  ECHO_GIT_USERNAME: auth.username,
17760
17911
  ECHO_GIT_PASSWORD: auth.password
17761
17912
  },
17762
- cleanup: () => rm2(directory, { recursive: true, force: true })
17913
+ cleanup: () => rm3(directory, { recursive: true, force: true })
17763
17914
  };
17764
17915
  }
17765
17916
  message(error51) {
@@ -17784,6 +17935,7 @@ var RunStartPayloadSchema = external_exports.object({
17784
17935
  prompt: external_exports.string().max(512 * 1024),
17785
17936
  chat_session_id: external_exports.string().max(64).optional(),
17786
17937
  attachments: external_exports.array(external_exports.object({ media_id: external_exports.string().min(1).max(64), name: external_exports.string().max(200), kind: external_exports.enum(["image", "file"]) })).max(16).optional(),
17938
+ task_attachments: external_exports.array(external_exports.object({ file_id: external_exports.uuid(), name: external_exports.string().min(1).max(255), mime: external_exports.string().max(255), size: external_exports.number().int().positive(), sha256: external_exports.string().regex(/^[a-f0-9]{64}$/), prompt_token: external_exports.string().max(100) })).max(16).nullish(),
17787
17939
  session_id: external_exports.string().max(200).optional().default(""),
17788
17940
  model: external_exports.string().max(100).optional().default(""),
17789
17941
  reasoning_effort: external_exports.string().max(40).optional().default(""),
@@ -17854,13 +18006,14 @@ var AgentRuntime = class {
17854
18006
  await this.retry(() => this.client.chatEvents(runId, events), 5);
17855
18007
  },
17856
18008
  chatMedia: (runId, mediaId) => this.client.chatMedia(runId, mediaId),
18009
+ taskFile: (runId, fileId, destination, size, sha256, signal2) => this.client.taskFile(runId, fileId, destination, size, sha256, signal2),
17857
18010
  finished: async (runId, report) => {
17858
18011
  this.pendingReports.set(runId, report);
17859
18012
  await this.flushReport(runId);
17860
18013
  }
17861
18014
  };
17862
18015
  this.workspaces = new WorkspaceManager(config2, this.logger);
17863
- this.orphans = new OrphanRegistry(join8(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
18016
+ this.orphans = new OrphanRegistry(join9(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
17864
18017
  this.runs = new RunSupervisor(config2, this.logger, reporter, this.orphans);
17865
18018
  }
17866
18019
  config;
@@ -17873,7 +18026,7 @@ var AgentRuntime = class {
17873
18026
  workspaces;
17874
18027
  stopController = new AbortController();
17875
18028
  startedAt = (/* @__PURE__ */ new Date()).toISOString();
17876
- chatRunnerId = randomUUID2();
18029
+ chatRunnerId = randomUUID3();
17877
18030
  chatReady = false;
17878
18031
  chatLeaseInitialized = false;
17879
18032
  chatError;
@@ -18014,6 +18167,7 @@ var AgentRuntime = class {
18014
18167
  return {
18015
18168
  providers: snapshot.providers.map((p) => ({ id: p.id, installed: p.installed, runnable: p.available, version: p.version ?? "", path: p.resolvedPath ?? "" })),
18016
18169
  cli_chat: { version: 1, providers: CHAT_PROVIDERS.filter((id) => snapshot.detectedIds.includes(id)) },
18170
+ task_attachments: { version: 1 },
18017
18171
  detected_ids: snapshot.detectedIds,
18018
18172
  refreshed_at: snapshot.refreshedAt
18019
18173
  };
@@ -18067,6 +18221,11 @@ var AgentRuntime = class {
18067
18221
  }
18068
18222
  async execute(command) {
18069
18223
  switch (command.type) {
18224
+ case "run.files.prepare": {
18225
+ const payload = RunStartPayloadSchema.parse(command.payload);
18226
+ const prompt = await this.runs.prepareTaskFiles(payload);
18227
+ return { success: true, result: { prompt } };
18228
+ }
18070
18229
  case "chat.run.start":
18071
18230
  case "run.start": {
18072
18231
  const payload = RunStartPayloadSchema.parse(command.payload);
@@ -18401,7 +18560,7 @@ function preferSystemNode() {
18401
18560
  const names = process.platform === "win32" ? ["node.exe"] : ["node"];
18402
18561
  for (const directory of (process.env.PATH ?? "").split(delimiter5)) {
18403
18562
  for (const name of names) {
18404
- const candidate = directory && join9(directory, name);
18563
+ const candidate = directory && join10(directory, name);
18405
18564
  if (candidate && existsSync6(candidate)) {
18406
18565
  process.env.TOC_OCRA_NODE_PATH = candidate;
18407
18566
  return;
@@ -4,6 +4,7 @@ import type { ChatContentEvent } from '../providers/provider.js';
4
4
  import { type TerminalTailRead } from '../session/terminal-tail-buffer.js';
5
5
  import type { RunFinishedReport, RunStartPayload, RunStartedReport } from '../types/protocol.js';
6
6
  import { OrphanRegistry } from './orphan-registry.js';
7
+ import { type TaskFileDownload } from './task-attachments.js';
7
8
  /**
8
9
  * Supervises headless CLI turns. One process per turn:
9
10
  *
@@ -17,6 +18,7 @@ import { OrphanRegistry } from './orphan-registry.js';
17
18
  */
18
19
  export declare const COMPLETION_EXIT_GRACE_MS = 15000;
19
20
  export type RunReporter = {
21
+ taskFile?: TaskFileDownload;
20
22
  chatEvents?(runId: string, events: Array<ChatContentEvent & {
21
23
  sequence: number;
22
24
  }>): Promise<void>;
@@ -38,6 +40,7 @@ export declare class RunSupervisor {
38
40
  /** Run ids whose finish report has not been acknowledged yet. */
39
41
  private readonly unreported;
40
42
  private readonly chatRunIds;
43
+ private readonly preparing;
41
44
  constructor(config: AgentConfig, logger: DiagnosticLogger, reporter: RunReporter, orphans?: OrphanRegistry | undefined);
42
45
  get activeCount(): number;
43
46
  /** Runs to list in the heartbeat: still running, or finished but not yet reported. */
@@ -56,6 +59,7 @@ export declare class RunSupervisor {
56
59
  * directory, unknown CLI) are reported as a failed run, never thrown.
57
60
  */
58
61
  start(payload: RunStartPayload): Promise<void>;
62
+ private startPrepared;
59
63
  private onStdout;
60
64
  private onExit;
61
65
  private queueChatEvent;
@@ -72,4 +76,5 @@ export declare class RunSupervisor {
72
76
  private remember;
73
77
  private defaultChatWorkdir;
74
78
  validateWorkdir(input: string | undefined): Promise<string>;
79
+ prepareTaskFiles(payload: RunStartPayload, signal?: AbortSignal, workdir?: string): Promise<string>;
75
80
  }