toc-ocra 0.2.5 → 0.2.7

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.
@@ -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 join8 } from "node:path";
10
+ import { delimiter as delimiter5, join as join9 } from "node:path";
11
11
 
12
12
  // src/config.ts
13
13
  import { randomUUID } from "node:crypto";
@@ -15731,7 +15731,8 @@ var CliDetectionService = class {
15731
15731
  var cliDetection = new CliDetectionService();
15732
15732
 
15733
15733
  // src/runtime/agent-runtime.ts
15734
- import { join as join7 } from "node:path";
15734
+ import { randomUUID as randomUUID2 } from "node:crypto";
15735
+ import { join as join8 } from "node:path";
15735
15736
 
15736
15737
  // src/exec/script-runner.ts
15737
15738
  import { spawn as spawn3 } from "node:child_process";
@@ -15833,6 +15834,268 @@ function executeScript(command, workdir, timeoutSeconds) {
15833
15834
  });
15834
15835
  }
15835
15836
 
15837
+ // src/providers/chat/codex-app-server.ts
15838
+ function codexChatProvider(request, attachments) {
15839
+ let write;
15840
+ let threadId = request.sessionId;
15841
+ let hasStartedTurn = false;
15842
+ const send = (message) => write?.(`${JSON.stringify(message)}
15843
+ `);
15844
+ const options = {
15845
+ cwd: request.workdir,
15846
+ approvalPolicy: "never",
15847
+ sandbox: "danger-full-access",
15848
+ ...request.model ? { model: request.model } : {}
15849
+ };
15850
+ const threadRequest = () => send({
15851
+ id: 2,
15852
+ method: threadId ? "thread/resume" : "thread/start",
15853
+ params: { ...options, ...threadId ? { threadId } : { ephemeral: false } }
15854
+ });
15855
+ const startTurn = () => {
15856
+ if (hasStartedTurn) return;
15857
+ hasStartedTurn = true;
15858
+ const input = [];
15859
+ if (request.prompt) input.push({ type: "text", text: request.prompt });
15860
+ for (const attachment of attachments) {
15861
+ if (attachment.kind === "image") input.push({ type: "localImage", path: attachment.path });
15862
+ else input.push({ type: "text", text: `\u9644\u4EF6\u6587\u4EF6\uFF1A${attachment.name}
15863
+ \u672C\u673A\u8DEF\u5F84\uFF1A${attachment.path}` });
15864
+ }
15865
+ send({ id: 3, method: "turn/start", params: {
15866
+ threadId,
15867
+ input,
15868
+ cwd: request.workdir,
15869
+ approvalPolicy: "never",
15870
+ sandboxPolicy: { type: "dangerFullAccess" },
15871
+ ...request.model ? { model: request.model } : {},
15872
+ ...request.reasoningEffort ? { effort: request.reasoningEffort } : {}
15873
+ } });
15874
+ };
15875
+ const empty = () => ({ display: [] });
15876
+ return {
15877
+ id: "codex",
15878
+ buildLaunch() {
15879
+ return resolveExecutable("codex", ["app-server", "--listen", "stdio://"], providerEnvironment(), process.env.TOC_OCRA_CODEX_PATH);
15880
+ },
15881
+ startInput(writer) {
15882
+ write = writer;
15883
+ send({ id: 1, method: "initialize", params: {
15884
+ clientInfo: { name: "echo_cli_chat", title: "Echo CLI Chat", version: "1.0" },
15885
+ capabilities: { experimentalApi: true }
15886
+ } });
15887
+ },
15888
+ parseLine(line) {
15889
+ const message = tryJson(line);
15890
+ if (!message) return empty();
15891
+ if (message.id !== void 0 && !message.method) {
15892
+ const error51 = message.error;
15893
+ if (error51) return { display: [], turnError: str(error51.message) ?? "Codex \u4F1A\u8BDD\u8BF7\u6C42\u5931\u8D25", endProcess: true };
15894
+ if (message.id === 1) {
15895
+ send({ method: "initialized" });
15896
+ threadRequest();
15897
+ return empty();
15898
+ }
15899
+ if (message.id === 2) {
15900
+ const result = message.result;
15901
+ const thread = result?.thread;
15902
+ const id = str(thread?.id);
15903
+ if (!id) return { display: [], turnError: "Codex \u672A\u8FD4\u56DE\u539F\u751F\u4F1A\u8BDD ID", endProcess: true };
15904
+ threadId = id;
15905
+ startTurn();
15906
+ return { display: [`\u25CF \u4F1A\u8BDD ${id}`], sessionId: id };
15907
+ }
15908
+ return empty();
15909
+ }
15910
+ const params = message.params ?? {};
15911
+ if (params.threadId && params.threadId !== threadId) return empty();
15912
+ if (message.id !== void 0) {
15913
+ send({ id: message.id, error: { code: -32601, message: "\u5F53\u524D\u804A\u5929\u5C1A\u672A\u63A5\u5165\u4EA4\u4E92\u5F0F\u6743\u9650\u6216\u95EE\u7B54" } });
15914
+ return { display: [], turnError: "CLI \u6B63\u5728\u8BF7\u6C42\u4EA4\u4E92\u786E\u8BA4\uFF0C\u5F53\u524D\u804A\u5929\u5C1A\u672A\u63A5\u5165\u6B64\u64CD\u4F5C\u3002", endProcess: true };
15915
+ }
15916
+ const itemId = str(params.itemId) ?? "";
15917
+ switch (message.method) {
15918
+ case "item/agentMessage/delta":
15919
+ case "item/reasoning/textDelta":
15920
+ case "item/reasoning/summaryTextDelta": {
15921
+ const text = typeof params.delta === "string" ? params.delta : "";
15922
+ return { display: [], chatEvents: itemId && text ? [{ event: message.method === "item/agentMessage/delta" ? "text_delta" : "thought_delta", item_id: itemId, text }] : [] };
15923
+ }
15924
+ case "turn/plan/updated": {
15925
+ const plan = Array.isArray(params.plan) ? params.plan : [];
15926
+ return { display: [], chatEvents: [{ event: "item", item: {
15927
+ itemId: "plan",
15928
+ type: "plan",
15929
+ entries: plan.map((entry) => ({ content: String(entry.step ?? ""), status: String(entry.status ?? "pending") }))
15930
+ } }] };
15931
+ }
15932
+ case "item/started":
15933
+ case "item/completed": {
15934
+ const item = params.item ?? {};
15935
+ const id = str(item.id);
15936
+ if (!id || item.type === "userMessage") return empty();
15937
+ const completed = message.method === "item/completed";
15938
+ const status = completed ? item.status === "failed" ? "failed" : "completed" : "in_progress";
15939
+ let normalized;
15940
+ switch (item.type) {
15941
+ case "agentMessage":
15942
+ normalized = { itemId: id, type: "text", text: String(item.text ?? ""), status };
15943
+ break;
15944
+ case "reasoning": {
15945
+ const summary = Array.isArray(item.summary) ? item.summary.map(String).join("\n\n") : "";
15946
+ normalized = { itemId: id, type: "thought", text: summary, status };
15947
+ break;
15948
+ }
15949
+ case "plan":
15950
+ normalized = { itemId: id, type: "text", text: String(item.text ?? ""), status };
15951
+ break;
15952
+ case "commandExecution":
15953
+ normalized = { itemId: id, type: "tool_call", name: "shell", kind: "execute", title: String(item.command ?? "\u6267\u884C\u547D\u4EE4"), input: String(item.command ?? ""), output: clip(String(item.aggregatedOutput ?? ""), 64e3), text: clip(String(item.aggregatedOutput ?? ""), 64e3), status };
15954
+ break;
15955
+ case "fileChange":
15956
+ normalized = { itemId: id, type: "tool_call", name: "file_change", kind: "edit", title: "\u4FEE\u6539\u6587\u4EF6", input: clip(JSON.stringify(item.changes ?? []), 64e3), text: clip(JSON.stringify(item.changes ?? []), 64e3), status };
15957
+ break;
15958
+ case "mcpToolCall":
15959
+ normalized = { itemId: id, type: "tool_call", name: String(item.tool ?? "MCP"), kind: "call", title: String(item.tool ?? "\u5DE5\u5177\u8C03\u7528"), input: clip(JSON.stringify(item.arguments ?? {}), 64e3), ...completed ? { output: clip(JSON.stringify(item.result ?? item.error ?? {}), 64e3) } : {}, text: clip(JSON.stringify(item.result ?? item.arguments ?? {}), 64e3), status };
15960
+ break;
15961
+ default:
15962
+ normalized = { itemId: id, type: "tool_call", name: String(item.type ?? "tool"), title: String(item.type ?? "\u6267\u884C\u8FC7\u7A0B"), status };
15963
+ break;
15964
+ }
15965
+ if (completed && normalized.type === "thought" && !normalized.text) return empty();
15966
+ return {
15967
+ display: [],
15968
+ chatEvents: [{ event: "item", item: normalized }],
15969
+ ...completed && item.type === "agentMessage" ? { message: String(item.text ?? "") } : {}
15970
+ };
15971
+ }
15972
+ case "turn/completed": {
15973
+ const turn = params.turn ?? {};
15974
+ const error51 = turn.error ?? {};
15975
+ if (turn.status !== "completed") return { display: [], turnError: str(error51.message) ?? (turn.status === "interrupted" ? "\u6267\u884C\u5DF2\u4E2D\u65AD" : "Codex \u672C\u8F6E\u6267\u884C\u5931\u8D25"), endProcess: true };
15976
+ return { display: ["\u25CF \u672C\u8F6E\u5B8C\u6210"], turnCompleted: true, endProcess: true };
15977
+ }
15978
+ default:
15979
+ return empty();
15980
+ }
15981
+ }
15982
+ };
15983
+ }
15984
+
15985
+ // src/providers/chat/claude-stream-json.ts
15986
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
15987
+ var textContent = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.map((part) => str(record2(part).text) ?? "").filter(Boolean).join("\n") : JSON.stringify(value ?? "");
15988
+ function claudeChatProvider(attachments) {
15989
+ let messageID = "";
15990
+ let messageSequence = 0;
15991
+ let hasText = false;
15992
+ const blocks = /* @__PURE__ */ new Map();
15993
+ const tools = /* @__PURE__ */ new Map();
15994
+ const asItem = (block, status, output) => ({ event: "item", item: {
15995
+ itemId: block.id,
15996
+ type: block.type,
15997
+ status,
15998
+ ...block.type === "tool_call" ? { name: block.name, title: block.name, input: clip(block.partialInput || JSON.stringify(block.input), 64e3), ...output !== void 0 ? { output: clip(output, 64e3), text: clip(output, 64e3) } : {} } : { text: block.text }
15999
+ } });
16000
+ return {
16001
+ id: "claude",
16002
+ buildLaunch(request) {
16003
+ const files = attachments.map((file2) => `\u9644\u4EF6\u6587\u4EF6\uFF1A${file2.name}
16004
+ \u672C\u673A\u8DEF\u5F84\uFF1A${file2.path}`).join("\n\n");
16005
+ const launch = claudeProvider.buildLaunch({ ...request, prompt: [request.prompt, files].filter(Boolean).join("\n\n") });
16006
+ launch.args.push("--include-partial-messages");
16007
+ return launch;
16008
+ },
16009
+ parseLine(line) {
16010
+ const envelope = tryJson(line);
16011
+ if (!envelope) return { display: line.trim() ? [line] : [] };
16012
+ const sessionId = str(envelope.session_id);
16013
+ const base = sessionId ? { sessionId } : {};
16014
+ const events = [];
16015
+ if (envelope.parent_tool_use_id) return { ...base, display: [] };
16016
+ if (envelope.type === "stream_event") {
16017
+ const event = record2(envelope.event);
16018
+ const index = Number(event.index ?? 0);
16019
+ if (event.type === "message_start") {
16020
+ messageID = str(record2(event.message).id) ?? `message:${++messageSequence}`;
16021
+ blocks.clear();
16022
+ } else if (event.type === "content_block_start") {
16023
+ if (!messageID) messageID = `message:${++messageSequence}`;
16024
+ const part = record2(event.content_block);
16025
+ const type = part.type === "tool_use" ? "tool_call" : part.type === "thinking" ? "thought" : "text";
16026
+ if (!["tool_use", "thinking", "text"].includes(String(part.type))) return { ...base, display: [] };
16027
+ const block = { id: type === "tool_call" ? str(part.id) ?? `${messageID}:${index}` : `${messageID}:${index}`, type, name: str(part.name) ?? "tool", text: String(part.text ?? part.thinking ?? ""), input: part.input ?? {}, partialInput: "" };
16028
+ blocks.set(index, block);
16029
+ if (type === "tool_call") tools.set(block.id, block);
16030
+ events.push(asItem(block, "in_progress"));
16031
+ } else if (event.type === "content_block_delta") {
16032
+ const block = blocks.get(index);
16033
+ const delta = record2(event.delta);
16034
+ if (!block) return { ...base, display: [] };
16035
+ if (delta.type === "text_delta" || delta.type === "thinking_delta") {
16036
+ const text = String(delta.text ?? delta.thinking ?? "");
16037
+ block.text += text;
16038
+ if (delta.type === "text_delta") hasText ||= !!text;
16039
+ events.push({ event: delta.type === "text_delta" ? "text_delta" : "thought_delta", item_id: block.id, text });
16040
+ } else if (delta.type === "input_json_delta") {
16041
+ block.partialInput += String(delta.partial_json ?? "");
16042
+ if (block.partialInput.length > 64e3) block.partialInput = block.partialInput.slice(0, 64e3);
16043
+ events.push(asItem(block, "in_progress"));
16044
+ }
16045
+ } else if (event.type === "content_block_stop") {
16046
+ const block = blocks.get(index);
16047
+ if (block && block.type !== "tool_call") events.push(asItem(block, "completed"));
16048
+ }
16049
+ } else if (envelope.type === "assistant") {
16050
+ const message = record2(envelope.message);
16051
+ const id = str(message.id) || messageID || `message:${++messageSequence}`;
16052
+ const parts = Array.isArray(message.content) ? message.content : [];
16053
+ parts.forEach((value, index) => {
16054
+ const part = record2(value);
16055
+ if (!["text", "thinking", "tool_use"].includes(String(part.type))) return;
16056
+ const type = part.type === "tool_use" ? "tool_call" : part.type === "thinking" ? "thought" : "text";
16057
+ const block = { id: type === "tool_call" ? str(part.id) ?? `${id}:${index}` : `${id}:${index}`, type, name: str(part.name) ?? "tool", text: String(part.text ?? part.thinking ?? ""), input: part.input ?? {}, partialInput: "" };
16058
+ if (type === "tool_call") tools.set(block.id, block);
16059
+ if (type === "text") hasText ||= !!block.text;
16060
+ events.push(asItem(block, type === "tool_call" ? "in_progress" : "completed"));
16061
+ });
16062
+ } else if (envelope.type === "user") {
16063
+ const message = record2(envelope.message);
16064
+ for (const value of Array.isArray(message.content) ? message.content : []) {
16065
+ const part = record2(value);
16066
+ if (part.type !== "tool_result") continue;
16067
+ const id = str(part.tool_use_id);
16068
+ if (!id) continue;
16069
+ const block = tools.get(id) ?? { id, type: "tool_call", name: "tool", text: "", input: {}, partialInput: "" };
16070
+ events.push(asItem(block, part.is_error === true ? "failed" : "completed", textContent(part.content)));
16071
+ }
16072
+ } else if (envelope.type === "result") {
16073
+ const text = str(envelope.result) ?? "";
16074
+ if (envelope.is_error === true) {
16075
+ const errors = Array.isArray(envelope.errors) ? envelope.errors.map(String).join("\n") : "";
16076
+ return { ...base, display: [], turnError: text || errors || "Claude \u672C\u8F6E\u6267\u884C\u5931\u8D25", endProcess: true };
16077
+ }
16078
+ if (!hasText && text) events.push({ event: "item", item: { itemId: "answer", type: "text", text, status: "completed" } });
16079
+ return { ...base, display: ["\u25CF \u672C\u8F6E\u5B8C\u6210"], message: text, turnCompleted: true, chatEvents: events, endProcess: true };
16080
+ }
16081
+ return { ...base, display: [], chatEvents: events };
16082
+ }
16083
+ };
16084
+ }
16085
+
16086
+ // src/providers/chat/index.ts
16087
+ var CHAT_PROVIDERS = ["codex", "claude"];
16088
+ function resolveChatProvider(type, request, attachments) {
16089
+ switch (type) {
16090
+ case "codex":
16091
+ return codexChatProvider(request, attachments);
16092
+ case "claude":
16093
+ return claudeChatProvider(attachments);
16094
+ default:
16095
+ throw new Error(`\u6B64 CLI \u7684\u5B9E\u65F6\u804A\u5929\u9A71\u52A8\u5C1A\u672A\u542F\u7528\uFF1A${type}`);
16096
+ }
16097
+ }
16098
+
15836
16099
  // src/providers/model-catalog-collector.ts
15837
16100
  import { createHash } from "node:crypto";
15838
16101
  import { stat } from "node:fs/promises";
@@ -16404,8 +16667,8 @@ var OrphanRegistry = class {
16404
16667
  file;
16405
16668
  records = /* @__PURE__ */ new Map();
16406
16669
  writing = Promise.resolve();
16407
- add(record2) {
16408
- this.records.set(record2.runId, record2);
16670
+ add(record3) {
16671
+ this.records.set(record3.runId, record3);
16409
16672
  this.persist();
16410
16673
  }
16411
16674
  remove(runId) {
@@ -16429,12 +16692,12 @@ var OrphanRegistry = class {
16429
16692
  return [];
16430
16693
  }
16431
16694
  const reaped = [];
16432
- for (const record2 of previous) {
16433
- if (!record2 || typeof record2.pid !== "number" || record2.pid <= 1) continue;
16434
- const command = await commandOf(record2.pid);
16435
- if (command && command.includes(basename(record2.executable))) {
16436
- await killProcessTree(record2.pid);
16437
- reaped.push(record2);
16695
+ for (const record3 of previous) {
16696
+ if (!record3 || typeof record3.pid !== "number" || record3.pid <= 1) continue;
16697
+ const command = await commandOf(record3.pid);
16698
+ if (command && command.includes(basename(record3.executable))) {
16699
+ await killProcessTree(record3.pid);
16700
+ reaped.push(record3);
16438
16701
  }
16439
16702
  }
16440
16703
  this.records.clear();
@@ -16454,8 +16717,8 @@ function commandOf(pid) {
16454
16717
 
16455
16718
  // src/runs/run-supervisor.ts
16456
16719
  import { spawn as spawn7 } from "node:child_process";
16457
- import { realpath } from "node:fs/promises";
16458
- import { isAbsolute as isAbsolute2, relative, resolve as resolve3 } from "node:path";
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";
16459
16722
  import { createInterface as createInterface2 } from "node:readline";
16460
16723
 
16461
16724
  // src/session/terminal-line-sanitizer.ts
@@ -16594,6 +16857,7 @@ var RunSupervisor = class {
16594
16857
  recent = /* @__PURE__ */ new Map();
16595
16858
  /** Run ids whose finish report has not been acknowledged yet. */
16596
16859
  unreported = /* @__PURE__ */ new Set();
16860
+ chatRunIds = /* @__PURE__ */ new Set();
16597
16861
  get activeCount() {
16598
16862
  return this.active.size;
16599
16863
  }
@@ -16601,8 +16865,15 @@ var RunSupervisor = class {
16601
16865
  activeRunIds() {
16602
16866
  return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.unreported])];
16603
16867
  }
16868
+ activeChatRunIds() {
16869
+ return this.activeRunIds().filter((id) => this.chatRunIds.has(id));
16870
+ }
16871
+ activeTaskRunIds() {
16872
+ return this.activeRunIds().filter((id) => !this.chatRunIds.has(id));
16873
+ }
16604
16874
  markReported(runId) {
16605
16875
  this.unreported.delete(runId);
16876
+ this.chatRunIds.delete(runId);
16606
16877
  }
16607
16878
  recentRuns() {
16608
16879
  const running = [...this.active.values()].map((run) => ({ runId: run.runId, workstationId: run.workstationId, status: "running", at: run.startedAt }));
@@ -16618,9 +16889,10 @@ var RunSupervisor = class {
16618
16889
  this.logger.info("run-duplicate-ignored", { runId: payload.run_id });
16619
16890
  return;
16620
16891
  }
16892
+ if (payload.chat_session_id) this.chatRunIds.add(payload.run_id);
16621
16893
  const refuse = async (error51) => {
16622
16894
  this.logger.warn("run-refused", { runId: payload.run_id, error: error51 });
16623
- this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed" });
16895
+ this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed", chat: !!payload.chat_session_id });
16624
16896
  await this.report(payload.run_id, { status: "failed", error: error51, duration_ms: 0 });
16625
16897
  };
16626
16898
  if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) {
@@ -16633,10 +16905,29 @@ var RunSupervisor = class {
16633
16905
  let workdir;
16634
16906
  try {
16635
16907
  provider = resolveHeadlessProvider(payload.agent_type);
16636
- workdir = await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
16908
+ workdir = payload.chat_session_id && !payload.workdir ? await this.defaultChatWorkdir() : await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
16637
16909
  } catch (error51) {
16638
16910
  return refuse(errorMessage(error51));
16639
16911
  }
16912
+ const request = {
16913
+ prompt: payload.prompt,
16914
+ sessionId: payload.session_id,
16915
+ model: payload.model,
16916
+ reasoningEffort: payload.reasoning_effort,
16917
+ workdir
16918
+ };
16919
+ if (payload.chat_session_id) {
16920
+ try {
16921
+ if (!this.reporter.chatEvents) throw new Error("\u5B9E\u65F6\u804A\u5929\u8F6C\u53D1\u672A\u914D\u7F6E");
16922
+ const attachments = await this.prepareChatAttachments(payload);
16923
+ provider = resolveChatProvider(payload.agent_type, request, attachments);
16924
+ } catch (error51) {
16925
+ return refuse(errorMessage(error51));
16926
+ }
16927
+ }
16928
+ if (!payload.prompt && !payload.attachments?.length) return refuse("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
16929
+ 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");
16640
16931
  const launch = provider.buildLaunch({
16641
16932
  prompt: payload.prompt,
16642
16933
  sessionId: payload.session_id,
@@ -16652,12 +16943,20 @@ var RunSupervisor = class {
16652
16943
  detached: process.platform !== "win32",
16653
16944
  shell: false,
16654
16945
  windowsHide: true,
16655
- stdio: ["ignore", "pipe", "pipe"]
16946
+ stdio: [provider.startInput ? "pipe" : "ignore", "pipe", "pipe"]
16656
16947
  });
16657
16948
  } catch (error51) {
16658
16949
  return refuse(`\u65E0\u6CD5\u542F\u52A8 ${provider.id}\uFF1A${errorMessage(error51)}`);
16659
16950
  }
16660
16951
  const run = {
16952
+ chat: Boolean(payload.chat_session_id),
16953
+ chatReady: false,
16954
+ eventSequence: 0,
16955
+ pendingEvents: [],
16956
+ eventTail: Promise.resolve(),
16957
+ pendingBytes: 0,
16958
+ queuedBytes: 0,
16959
+ forwardingFailed: false,
16661
16960
  runId: payload.run_id,
16662
16961
  workstationId: payload.workstation_id,
16663
16962
  agentType: provider.id,
@@ -16683,6 +16982,7 @@ var RunSupervisor = class {
16683
16982
  `);
16684
16983
  this.logger.info("run-started", { runId: run.runId, agentType: provider.id, pid: run.pid, resume: Boolean(payload.session_id) });
16685
16984
  let spawnError = "";
16985
+ child.stdin?.on("error", () => void 0);
16686
16986
  child.once("error", (error51) => {
16687
16987
  spawnError = error51.message;
16688
16988
  });
@@ -16696,6 +16996,24 @@ var RunSupervisor = class {
16696
16996
  `);
16697
16997
  });
16698
16998
  child.once("close", (code, signal2) => void this.onExit(run, code, signal2, spawnError));
16999
+ if (provider.startInput) {
17000
+ child.once("spawn", () => {
17001
+ try {
17002
+ provider.startInput?.((message) => {
17003
+ child.stdin?.write(message);
17004
+ });
17005
+ } catch (error51) {
17006
+ run.turnError = errorMessage(error51);
17007
+ void killProcessTree(run.pid);
17008
+ }
17009
+ });
17010
+ run.timers.push(setTimeout(() => {
17011
+ if (!run.chatReady && this.active.has(run.runId)) {
17012
+ run.turnError = "CLI \u5B9E\u65F6\u4F1A\u8BDD\u521D\u59CB\u5316\u8D85\u65F6";
17013
+ void killProcessTree(run.pid);
17014
+ }
17015
+ }, 6e4));
17016
+ }
16699
17017
  const maxMs = this.config.maxRunSeconds * 1e3;
16700
17018
  run.timers.push(setTimeout(() => {
16701
17019
  run.timedOut = true;
@@ -16710,6 +17028,10 @@ var RunSupervisor = class {
16710
17028
  onStdout(run, rawLine) {
16711
17029
  const line = sanitizeTerminalLine(rawLine);
16712
17030
  const parsed = run.provider.parseLine(line);
17031
+ if (run.chat && !run.forwardingFailed) {
17032
+ for (const event of parsed.chatEvents ?? []) this.queueChatEvent(run, event);
17033
+ if (parsed.sessionId) run.chatReady = true;
17034
+ }
16713
17035
  for (const row of parsed.display) run.tail.append(`${row}
16714
17036
  `);
16715
17037
  if (parsed.message !== void 0) run.finalMessage = parsed.message;
@@ -16718,6 +17040,14 @@ var RunSupervisor = class {
16718
17040
  run.sessionId = parsed.sessionId;
16719
17041
  void this.reporter.started(run.runId, { session_id: parsed.sessionId }).catch(() => void 0);
16720
17042
  }
17043
+ if (parsed.endProcess) {
17044
+ run.child.stdin?.end();
17045
+ run.timers.push(setTimeout(() => {
17046
+ if (!this.active.has(run.runId)) return;
17047
+ run.killedAfterCompletion = Boolean(parsed.turnCompleted);
17048
+ void killProcessTree(run.pid);
17049
+ }, 1e3));
17050
+ }
16721
17051
  if ((parsed.turnCompleted || parsed.turnError) && !run.turnCompleted) {
16722
17052
  run.turnCompleted = true;
16723
17053
  run.timers.push(setTimeout(() => {
@@ -16732,6 +17062,9 @@ var RunSupervisor = class {
16732
17062
  for (const timer of run.timers) clearTimeout(timer);
16733
17063
  this.active.delete(run.runId);
16734
17064
  this.orphans?.remove(run.runId);
17065
+ if (run.eventTimer) clearTimeout(run.eventTimer);
17066
+ this.flushChatEvents(run);
17067
+ await run.eventTail;
16735
17068
  const durationMs = Date.now() - run.startedAt;
16736
17069
  let report;
16737
17070
  if (run.cancelRequested) {
@@ -16753,10 +17086,66 @@ var RunSupervisor = class {
16753
17086
  if (run.finalMessage) report.final_message = run.finalMessage;
16754
17087
  run.tail.append(`\u25CF \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF1A${report.status}${code !== null ? `\uFF08exit ${code}\uFF09` : ""}
16755
17088
  `);
16756
- this.remember({ runId: run.runId, workstationId: run.workstationId, tail: run.tail, finishedAt: Date.now(), status: report.status });
17089
+ this.remember({ runId: run.runId, workstationId: run.workstationId, tail: run.tail, finishedAt: Date.now(), status: report.status, chat: !!run.chat });
16757
17090
  this.logger.info("run-finished", { runId: run.runId, status: report.status, code, durationMs });
16758
17091
  await this.report(run.runId, report);
16759
17092
  }
17093
+ queueChatEvent(run, event) {
17094
+ const bytes = Buffer.byteLength(JSON.stringify(event));
17095
+ if (run.queuedBytes + bytes > 8 * 1024 * 1024) {
17096
+ run.forwardingFailed = true;
17097
+ run.turnError = "\u5B9E\u65F6\u8F93\u51FA\u8F6C\u53D1\u79EF\u538B\uFF0C\u8BF7\u68C0\u67E5\u8BBE\u5907\u7F51\u7EDC";
17098
+ void killProcessTree(run.pid);
17099
+ return;
17100
+ }
17101
+ run.pendingEvents.push({ ...event, sequence: ++run.eventSequence });
17102
+ run.pendingBytes += bytes;
17103
+ run.queuedBytes += bytes;
17104
+ if (run.pendingBytes > 512 * 1024 || run.pendingEvents.length >= 64) this.flushChatEvents(run);
17105
+ else if (!run.eventTimer) run.eventTimer = setTimeout(() => {
17106
+ delete run.eventTimer;
17107
+ this.flushChatEvents(run);
17108
+ }, 60);
17109
+ }
17110
+ flushChatEvents(run) {
17111
+ if (!run.pendingEvents.length) return;
17112
+ if (run.eventTimer) {
17113
+ clearTimeout(run.eventTimer);
17114
+ delete run.eventTimer;
17115
+ }
17116
+ const events = run.pendingEvents.splice(0);
17117
+ const bytes = run.pendingBytes;
17118
+ run.pendingBytes = 0;
17119
+ run.eventTail = run.eventTail.then(async () => {
17120
+ if (run.forwardingFailed) return;
17121
+ await this.reporter.chatEvents?.(run.runId, events);
17122
+ }).catch((error51) => {
17123
+ run.forwardingFailed = true;
17124
+ run.turnError = `\u5B9E\u65F6\u8F93\u51FA\u8F6C\u53D1\u5931\u8D25\uFF1A${errorMessage(error51)}`;
17125
+ this.logger.warn("chat-events-failed", { runId: run.runId, error: errorMessage(error51) });
17126
+ if (this.active.has(run.runId)) void killProcessTree(run.pid);
17127
+ }).finally(() => {
17128
+ run.queuedBytes -= bytes;
17129
+ });
17130
+ }
17131
+ async prepareChatAttachments(payload) {
17132
+ if (!payload.attachments?.length) return [];
17133
+ if (!this.reporter.chatMedia) throw new Error("\u9644\u4EF6\u8F6C\u53D1\u672A\u914D\u7F6E");
17134
+ const directory = join6(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
17135
+ await mkdir4(directory, { recursive: true, mode: 448 });
17136
+ const files = [];
17137
+ for (const attachment of payload.attachments) {
17138
+ const name = basename2(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
17139
+ const path = join6(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
17140
+ const existing = await stat2(path).catch(() => void 0);
17141
+ if (!existing?.isFile()) {
17142
+ const data = await this.reporter.chatMedia(payload.run_id, attachment.media_id);
17143
+ await writeFile4(path, data, { mode: 384 });
17144
+ }
17145
+ files.push({ path, name: attachment.name, kind: attachment.kind });
17146
+ }
17147
+ return files;
17148
+ }
16760
17149
  async report(runId, report) {
16761
17150
  this.unreported.add(runId);
16762
17151
  await this.reporter.finished(runId, report);
@@ -16799,6 +17188,15 @@ var RunSupervisor = class {
16799
17188
  if (item.finishedAt < cutoff || this.recent.size > MAX_RECENT_RUNS) this.recent.delete(id);
16800
17189
  }
16801
17190
  }
17191
+ async defaultChatWorkdir() {
17192
+ for (const root of this.config.allowedRoots) {
17193
+ try {
17194
+ return await this.validateWorkdir(root);
17195
+ } catch {
17196
+ }
17197
+ }
17198
+ throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u804A\u5929\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u8BF7\u914D\u7F6E\u4ECD\u5B58\u5728\u7684\u5141\u8BB8\u76EE\u5F55");
17199
+ }
16802
17200
  async validateWorkdir(input) {
16803
17201
  if (!input) throw new Error("\u6CA1\u6709\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55");
16804
17202
  let candidate;
@@ -16873,9 +17271,31 @@ var GatewayClient = class {
16873
17271
  const data = await this.request("GET", `/commands?wait=${waitSeconds}`, void 0, (waitSeconds + 15) * 1e3, signal2);
16874
17272
  return data.items ?? [];
16875
17273
  }
17274
+ chatHeartbeat(runnerId, providers, activeRuns) {
17275
+ return this.request("POST", "/chat-heartbeat", { runner_id: runnerId, providers, active_runs: activeRuns }, 15e3);
17276
+ }
17277
+ async pollChat(waitSeconds, runnerId, signal2) {
17278
+ const data = await this.request("GET", `/chat-commands?wait=${waitSeconds}&runner_id=${encodeURIComponent(runnerId)}`, void 0, (waitSeconds + 15) * 1e3, signal2);
17279
+ return data.items ?? [];
17280
+ }
16876
17281
  commandResult(commandId, body) {
16877
17282
  return this.request("POST", `/commands/${encodeURIComponent(commandId)}/result`, body, 15e3);
16878
17283
  }
17284
+ chatEvents(runId, events) {
17285
+ return this.request("POST", `/runs/${encodeURIComponent(runId)}/chat-events`, { events }, 15e3);
17286
+ }
17287
+ async chatMedia(runId, mediaId) {
17288
+ const response = await fetch(`${this.config.serverUrl}/api/agent/runs/${encodeURIComponent(runId)}/chat-media/${encodeURIComponent(mediaId)}`, {
17289
+ headers: { authorization: `Bearer ${this.config.token}` },
17290
+ signal: AbortSignal.timeout(3e4)
17291
+ });
17292
+ if (!response.ok) throw new GatewayError("\u65E0\u6CD5\u8BFB\u53D6\u804A\u5929\u9644\u4EF6", response.status);
17293
+ const declared = Number(response.headers.get("content-length") ?? 0);
17294
+ if (declared > 10 * 1024 * 1024) throw new GatewayError("\u804A\u5929\u9644\u4EF6\u8D85\u8FC7 10 MB", 413);
17295
+ const bytes = new Uint8Array(await response.arrayBuffer());
17296
+ if (bytes.length > 10 * 1024 * 1024) throw new GatewayError("\u804A\u5929\u9644\u4EF6\u8D85\u8FC7 10 MB", 413);
17297
+ return bytes;
17298
+ }
16879
17299
  runStarted(runId, body) {
16880
17300
  return this.request("POST", `/runs/${encodeURIComponent(runId)}/started`, body, 15e3);
16881
17301
  }
@@ -16885,7 +17305,7 @@ var GatewayClient = class {
16885
17305
  };
16886
17306
 
16887
17307
  // src/workspace/directory-browser.ts
16888
- import { readdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
17308
+ import { readdir, realpath as realpath2, stat as stat3 } from "node:fs/promises";
16889
17309
  import { homedir as homedir5 } from "node:os";
16890
17310
  import { dirname as dirname6, parse as parse3, resolve as resolve4 } from "node:path";
16891
17311
  var MAX_ENTRIES = 1e3;
@@ -16894,7 +17314,7 @@ async function listDirectory(requested, allowedRoots) {
16894
17314
  const directory = await realpath2(resolve4(raw)).catch(() => {
16895
17315
  throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${requested}`);
16896
17316
  });
16897
- if (!(await stat2(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
17317
+ if (!(await stat3(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
16898
17318
  const entries = await readdir(directory, { withFileTypes: true }).catch((error51) => {
16899
17319
  throw new Error(error51.code === "EACCES" || error51.code === "EPERM" ? "\u6CA1\u6709\u6743\u9650\u8BFB\u53D6\u8FD9\u4E2A\u76EE\u5F55" : error51.message);
16900
17320
  });
@@ -16923,9 +17343,9 @@ async function listDirectory(requested, allowedRoots) {
16923
17343
 
16924
17344
  // src/workspace/workspace-manager.ts
16925
17345
  import { spawn as spawn8 } from "node:child_process";
16926
- import { chmod as chmod3, mkdir as mkdir4, mkdtemp, opendir, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
17346
+ 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";
16927
17347
  import { homedir as homedir6, tmpdir } from "node:os";
16928
- import { isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve5 } from "node:path";
17348
+ import { isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve5 } from "node:path";
16929
17349
  var MAX_NODES = 2e3;
16930
17350
  var MAX_DIRECTORY_ENTRIES = 12e3;
16931
17351
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
@@ -16979,7 +17399,7 @@ var WorkspaceManager = class {
16979
17399
  return root;
16980
17400
  }
16981
17401
  async syncTree(rootPath, request) {
16982
- await mkdir4(rootPath, { recursive: true });
17402
+ await mkdir5(rootPath, { recursive: true });
16983
17403
  await addAllowedRoot(this.config, rootPath);
16984
17404
  const results = [];
16985
17405
  const requestedPaths = /* @__PURE__ */ new Set();
@@ -16989,7 +17409,7 @@ var WorkspaceManager = class {
16989
17409
  requestedPaths.add(relativePath);
16990
17410
  const target = this.resolveWithinRoot(rootPath, relativePath);
16991
17411
  try {
16992
- const metadata = await stat3(target).catch(() => void 0);
17412
+ const metadata = await stat4(target).catch(() => void 0);
16993
17413
  if (metadata) {
16994
17414
  if (!metadata.isDirectory()) {
16995
17415
  throw new Error("The existing workspace path is not a directory");
@@ -17003,7 +17423,7 @@ var WorkspaceManager = class {
17003
17423
  });
17004
17424
  continue;
17005
17425
  }
17006
- await mkdir4(target, { recursive: true });
17426
+ await mkdir5(target, { recursive: true });
17007
17427
  results.push({
17008
17428
  relative_path: relativePath,
17009
17429
  status: "created",
@@ -17047,12 +17467,12 @@ var WorkspaceManager = class {
17047
17467
  let created = false;
17048
17468
  let metadata;
17049
17469
  try {
17050
- metadata = await stat3(target);
17470
+ metadata = await stat4(target);
17051
17471
  } catch {
17052
17472
  }
17053
17473
  if (!metadata) {
17054
17474
  if (!gitUrl) throw new Error("A missing repository needs a Git remote URL");
17055
- await mkdir4(resolve5(target, ".."), { recursive: true });
17475
+ await mkdir5(resolve5(target, ".."), { recursive: true });
17056
17476
  const cloneArgs = branch ? ["clone", "--branch", branch, "--single-branch", gitUrl, target] : ["clone", gitUrl, target];
17057
17477
  await this.runGit(cloneArgs, rootPath, timeoutSeconds, false, gitEnvironment);
17058
17478
  created = true;
@@ -17147,7 +17567,7 @@ var WorkspaceManager = class {
17147
17567
  }
17148
17568
  async scanTree(rootPath, maxDepth, scanRoots) {
17149
17569
  const resolvedRoot = await realpath3(rootPath);
17150
- const rootMetadata = await stat3(resolvedRoot);
17570
+ const rootMetadata = await stat4(resolvedRoot);
17151
17571
  if (!rootMetadata.isDirectory()) throw new Error("The workspace root is not a directory");
17152
17572
  const nodes = [];
17153
17573
  let entriesRead = 0;
@@ -17202,7 +17622,7 @@ var WorkspaceManager = class {
17202
17622
  const target = this.resolveWithinRoot(resolvedRoot, relativePath);
17203
17623
  let metadata;
17204
17624
  try {
17205
- metadata = await stat3(target);
17625
+ metadata = await stat4(target);
17206
17626
  } catch (error51) {
17207
17627
  this.logger.warn("workspace-scan-root-missing", {
17208
17628
  rootPath: target,
@@ -17299,13 +17719,13 @@ var WorkspaceManager = class {
17299
17719
  }
17300
17720
  async createGitAuthEnvironment(auth) {
17301
17721
  if (!auth) return void 0;
17302
- const directory = await mkdtemp(join6(tmpdir(), "toc-ocra-git-auth-"));
17722
+ const directory = await mkdtemp(join7(tmpdir(), "toc-ocra-git-auth-"));
17303
17723
  const isWindows = process.platform === "win32";
17304
- const scriptPath = join6(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17724
+ const scriptPath = join7(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17305
17725
  let script;
17306
17726
  if (isWindows) {
17307
- const helperPath = join6(directory, "askpass.cjs");
17308
- await writeFile4(
17727
+ const helperPath = join7(directory, "askpass.cjs");
17728
+ await writeFile5(
17309
17729
  helperPath,
17310
17730
  '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',
17311
17731
  { mode: 448 }
@@ -17314,7 +17734,7 @@ var WorkspaceManager = class {
17314
17734
  } else {
17315
17735
  script = '#!/bin/sh\ncase "$1" in *Username*|*username*) printf "%s\\n" "$ECHO_GIT_USERNAME" ;; *) printf "%s\\n" "$ECHO_GIT_PASSWORD" ;; esac\n';
17316
17736
  }
17317
- await writeFile4(scriptPath, script, { mode: 448 });
17737
+ await writeFile5(scriptPath, script, { mode: 448 });
17318
17738
  if (!isWindows) await chmod3(scriptPath, 448);
17319
17739
  return {
17320
17740
  env: {
@@ -17347,7 +17767,9 @@ var RunStartPayloadSchema = external_exports.object({
17347
17767
  agent_type: external_exports.string().min(1).max(40),
17348
17768
  task_chain_id: external_exports.string().optional().default(""),
17349
17769
  message_id: external_exports.string().optional().default(""),
17350
- prompt: external_exports.string().min(1).max(512 * 1024),
17770
+ prompt: external_exports.string().max(512 * 1024),
17771
+ chat_session_id: external_exports.string().max(64).optional(),
17772
+ 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(),
17351
17773
  session_id: external_exports.string().max(200).optional().default(""),
17352
17774
  model: external_exports.string().max(100).optional().default(""),
17353
17775
  reasoning_effort: external_exports.string().max(40).optional().default(""),
@@ -17402,9 +17824,10 @@ var sleep2 = (ms, signal2) => new Promise((resolve7) => {
17402
17824
  }, { once: true });
17403
17825
  });
17404
17826
  var AgentRuntime = class {
17405
- constructor(config2, version2, logger) {
17827
+ constructor(config2, version2, logger, chatOnly = false) {
17406
17828
  this.config = config2;
17407
17829
  this.version = version2;
17830
+ this.chatOnly = chatOnly;
17408
17831
  this.ownsLogger = !logger;
17409
17832
  this.logger = logger ?? createDiagnosticLogger(config2.logFile, { level: config2.logLevel, maxAgeMs: 5 * 60 * 1e3 });
17410
17833
  this.client = new GatewayClient(config2);
@@ -17413,17 +17836,22 @@ var AgentRuntime = class {
17413
17836
  started: async (runId, report) => {
17414
17837
  await this.retry(() => this.client.runStarted(runId, report), 3);
17415
17838
  },
17839
+ chatEvents: async (runId, events) => {
17840
+ await this.retry(() => this.client.chatEvents(runId, events), 5);
17841
+ },
17842
+ chatMedia: (runId, mediaId) => this.client.chatMedia(runId, mediaId),
17416
17843
  finished: async (runId, report) => {
17417
17844
  this.pendingReports.set(runId, report);
17418
17845
  await this.flushReport(runId);
17419
17846
  }
17420
17847
  };
17421
17848
  this.workspaces = new WorkspaceManager(config2, this.logger);
17422
- this.orphans = new OrphanRegistry(join7(config2.dataDir, "active-runs.json"));
17849
+ this.orphans = new OrphanRegistry(join8(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
17423
17850
  this.runs = new RunSupervisor(config2, this.logger, reporter, this.orphans);
17424
17851
  }
17425
17852
  config;
17426
17853
  version;
17854
+ chatOnly;
17427
17855
  logger;
17428
17856
  client;
17429
17857
  runs;
@@ -17431,6 +17859,10 @@ var AgentRuntime = class {
17431
17859
  workspaces;
17432
17860
  stopController = new AbortController();
17433
17861
  startedAt = (/* @__PURE__ */ new Date()).toISOString();
17862
+ chatRunnerId = randomUUID2();
17863
+ chatReady = false;
17864
+ chatLeaseInitialized = false;
17865
+ chatError;
17434
17866
  detection;
17435
17867
  policy = DEFAULT_POLICY;
17436
17868
  heartbeatInterval = 30;
@@ -17450,13 +17882,14 @@ var AgentRuntime = class {
17450
17882
  /** Hash of the catalogs the server has stored (from its heartbeat reply). */
17451
17883
  serverCatalogHash;
17452
17884
  async start() {
17453
- const reaped = await this.orphans.reapPrevious();
17885
+ const reaped = this.chatOnly ? [] : await this.orphans.reapPrevious();
17454
17886
  if (reaped.length > 0) this.logger.warn("orphan-runs-stopped", { runIds: reaped.map((r) => r.runId) });
17455
17887
  this.detection = await cliDetection.detect();
17456
17888
  await this.refreshCatalogs();
17457
17889
  await this.heartbeat();
17458
17890
  void this.heartbeatLoop();
17459
- void this.pollLoop();
17891
+ if (!this.chatOnly) void this.pollLoop();
17892
+ void this.pollLoop(true);
17460
17893
  this.logger.info("agent-started", { serverUrl: this.config.serverUrl, deviceId: this.config.deviceId });
17461
17894
  process.stdout.write(`toc-ocra ${this.version} \u5DF2\u542F\u52A8\uFF1A${this.config.serverUrl}\uFF08\u8BBE\u5907 ${this.deviceName}\uFF09
17462
17895
  `);
@@ -17503,12 +17936,12 @@ var AgentRuntime = class {
17503
17936
  for (const runId of [...this.pendingReports.keys()]) await this.flushReport(runId, 1);
17504
17937
  const report = this.catalogReport;
17505
17938
  const sendCatalogs = report !== void 0 && report.hash !== this.serverCatalogHash;
17506
- try {
17939
+ if (!this.chatOnly) try {
17507
17940
  const response = await this.client.heartbeat({
17508
17941
  agent_version: this.version,
17509
17942
  hostname: this.config.hostname,
17510
17943
  capabilities: this.capabilitySummary(),
17511
- active_runs: this.runs.activeRunIds(),
17944
+ active_runs: this.runs.activeTaskRunIds(),
17512
17945
  ...sendCatalogs ? { model_catalogs: report } : {}
17513
17946
  });
17514
17947
  this.serverCatalogHash = response.model_catalog_hash ?? (sendCatalogs ? report.hash : this.serverCatalogHash);
@@ -17522,6 +17955,27 @@ var AgentRuntime = class {
17522
17955
  } catch (error51) {
17523
17956
  this.noteFailure("heartbeat", error51);
17524
17957
  }
17958
+ try {
17959
+ await this.client.chatHeartbeat(this.chatRunnerId, CHAT_PROVIDERS.filter((id) => this.detection?.detectedIds.includes(id)), this.runs.activeChatRunIds());
17960
+ if (!this.chatLeaseInitialized && this.chatOnly) {
17961
+ const reaped = await this.orphans.reapPrevious();
17962
+ if (reaped.length) this.logger.warn("chat-orphans-stopped", { runIds: reaped.map((run) => run.runId) });
17963
+ }
17964
+ this.chatLeaseInitialized = true;
17965
+ if (!this.chatReady) this.logger.info("chat-channel-ready");
17966
+ this.chatReady = true;
17967
+ this.chatError = void 0;
17968
+ if (this.chatOnly) {
17969
+ this.lastHeartbeatOk = true;
17970
+ this.heartbeatInterval = 20;
17971
+ }
17972
+ } catch (error51) {
17973
+ this.chatReady = false;
17974
+ const reason = error51 instanceof Error ? error51.message : String(error51);
17975
+ if (this.chatError !== reason) this.logger.warn("chat-channel-unavailable", { error: reason });
17976
+ this.chatError = reason;
17977
+ if (this.chatOnly) this.noteFailure("chat-heartbeat", error51);
17978
+ }
17525
17979
  void this.refreshCatalogs();
17526
17980
  }
17527
17981
  /** Re-reads catalogs whose sources changed; concurrent calls share one run. */
@@ -17545,6 +17999,7 @@ var AgentRuntime = class {
17545
17999
  if (!snapshot) return { providers: [] };
17546
18000
  return {
17547
18001
  providers: snapshot.providers.map((p) => ({ id: p.id, installed: p.installed, runnable: p.available, version: p.version ?? "", path: p.resolvedPath ?? "" })),
18002
+ cli_chat: { version: 1, providers: CHAT_PROVIDERS.filter((id) => snapshot.detectedIds.includes(id)) },
17548
18003
  detected_ids: snapshot.detectedIds,
17549
18004
  refreshed_at: snapshot.refreshedAt
17550
18005
  };
@@ -17561,16 +18016,23 @@ var AgentRuntime = class {
17561
18016
  this.lastError = { reason, at: (/* @__PURE__ */ new Date()).toISOString() };
17562
18017
  }
17563
18018
  // ---- Commands ----
17564
- async pollLoop() {
18019
+ async pollLoop(chat = false) {
17565
18020
  let backoff = 1e3;
17566
18021
  while (!this.stopController.signal.aborted) {
17567
18022
  try {
17568
- const commands = await this.client.poll(this.pollWait, this.stopController.signal);
18023
+ if (chat && !this.chatReady) {
18024
+ await sleep2(1e3, this.stopController.signal);
18025
+ continue;
18026
+ }
18027
+ const commands = chat ? await this.client.pollChat(this.pollWait, this.chatRunnerId, this.stopController.signal) : await this.client.poll(this.pollWait, this.stopController.signal);
17569
18028
  backoff = 1e3;
17570
18029
  for (const command of commands) void this.handle(command);
17571
18030
  } catch (error51) {
17572
18031
  if (this.stopController.signal.aborted) return;
17573
- this.noteFailure("poll", error51);
18032
+ if (chat) {
18033
+ this.chatReady = false;
18034
+ this.logger.warn("chat-poll-failed", { error: error51 instanceof Error ? error51.message : String(error51) });
18035
+ } else this.noteFailure("poll", error51);
17574
18036
  const unauthorized = error51 instanceof GatewayError && error51.unauthorized;
17575
18037
  await sleep2(unauthorized ? 6e4 : backoff, this.stopController.signal);
17576
18038
  backoff = Math.min(backoff * 2, 3e4);
@@ -17591,11 +18053,13 @@ var AgentRuntime = class {
17591
18053
  }
17592
18054
  async execute(command) {
17593
18055
  switch (command.type) {
18056
+ case "chat.run.start":
17594
18057
  case "run.start": {
17595
18058
  const payload = RunStartPayloadSchema.parse(command.payload);
17596
18059
  await this.runs.start(payload);
17597
18060
  return { success: true, result: { accepted: true } };
17598
18061
  }
18062
+ case "chat.run.cancel":
17599
18063
  case "run.cancel": {
17600
18064
  const payload = RunCancelPayloadSchema.parse(command.payload);
17601
18065
  const cancelled = await this.runs.cancel(payload.run_id);
@@ -17923,7 +18387,7 @@ function preferSystemNode() {
17923
18387
  const names = process.platform === "win32" ? ["node.exe"] : ["node"];
17924
18388
  for (const directory of (process.env.PATH ?? "").split(delimiter5)) {
17925
18389
  for (const name of names) {
17926
- const candidate = directory && join8(directory, name);
18390
+ const candidate = directory && join9(directory, name);
17927
18391
  if (candidate && existsSync6(candidate)) {
17928
18392
  process.env.TOC_OCRA_NODE_PATH = candidate;
17929
18393
  return;