@turmind/halo 1.4.6 → 1.5.0-alpha

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.
Files changed (76) hide show
  1. package/README.md +0 -1
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/160.c52d3d6cf7bc8286.js +1 -0
  4. package/admin-out/_next/static/chunks/220.e250bbbf65d24094.js +1 -0
  5. package/admin-out/_next/static/chunks/362.a0c58d3bf9b1300b.js +1 -0
  6. package/admin-out/_next/static/chunks/449.c4b881b9ebdc556e.js +1 -0
  7. package/admin-out/_next/static/chunks/613.fb09e55e0a61d3fe.js +1 -0
  8. package/admin-out/_next/static/chunks/73.05abce37d312e880.js +1 -0
  9. package/admin-out/_next/static/chunks/803-7128009ee0e1c10e.js +1 -0
  10. package/admin-out/_next/static/chunks/839.a2729fd33013d763.js +1 -0
  11. package/admin-out/_next/static/chunks/868.bbb91afd48c9eb58.js +1 -0
  12. package/admin-out/_next/static/chunks/892.a35118e09d8d8efd.js +1 -0
  13. package/admin-out/_next/static/chunks/989-7420ee650083df43.js +1 -0
  14. package/admin-out/_next/static/chunks/app/page-c33392db67853747.js +1 -0
  15. package/admin-out/_next/static/chunks/webpack-1a264efd965db7ab.js +1 -0
  16. package/admin-out/_next/static/css/7997e44c08e34b2b.css +3 -0
  17. package/admin-out/index.html +1 -1
  18. package/admin-out/index.txt +5 -5
  19. package/bundled-docs/design/agent.md +8 -7
  20. package/bundled-docs/design/architecture.md +9 -5
  21. package/bundled-docs/design/canvas-extensions.md +181 -0
  22. package/bundled-docs/design/cron.md +1 -1
  23. package/bundled-docs/design/feishu.md +3 -1
  24. package/bundled-docs/design/observability.md +2 -0
  25. package/bundled-docs/design/prompt-system.md +6 -10
  26. package/bundled-docs/design/relay.md +19 -15
  27. package/bundled-docs/design/session.md +15 -12
  28. package/bundled-docs/design/slack.md +2 -2
  29. package/bundled-docs/design/storage.md +4 -2
  30. package/bundled-docs/design/telegram.md +4 -2
  31. package/bundled-docs/design/wechat.md +7 -5
  32. package/bundled-docs/design/wecom.md +2 -2
  33. package/bundled-docs/design/ws.md +25 -18
  34. package/bundled-docs/dev/add-model-provider.md +21 -18
  35. package/bundled-docs/dev/api.md +25 -24
  36. package/bundled-docs/dev/env.md +2 -2
  37. package/bundled-docs/dev/tools.md +17 -82
  38. package/bundled-docs/guide/channels/wechat.md +1 -1
  39. package/bundled-docs/guide/cli.md +0 -1
  40. package/bundled-docs/guide/delegation-and-access.md +1 -2
  41. package/bundled-docs/guide/workspace.md +11 -0
  42. package/bundled-docs/requirements/chat.md +5 -4
  43. package/bundled-docs/requirements/command.md +1 -12
  44. package/bundled-docs/requirements/editor.md +9 -0
  45. package/bundled-docs/requirements/settings.md +9 -0
  46. package/dist/index.js +1898 -1037
  47. package/package.json +2 -2
  48. package/templates/agents/deep-executor/agent.yaml +0 -3
  49. package/templates/agents/default/AGENT.md +0 -17
  50. package/templates/agents/default/agent.yaml +0 -1
  51. package/templates/agents/executor/agent.yaml +1 -1
  52. package/templates/models/aws-bedrock-claude-invoke.yaml +31 -0
  53. package/templates/models/aws-bedrock-mantle.yaml +42 -7
  54. package/templates/models/aws-bedrock-openai.yaml +53 -17
  55. package/templates/prompts/root/DELEGATION.md +58 -0
  56. package/templates/skills/extension/SKILL.md +70 -0
  57. package/templates/skills/extension/templates/ext.sh +112 -0
  58. package/templates/skills/halo/SKILL.md +3 -0
  59. package/templates/skills/send-file/SKILL.md +1 -1
  60. package/admin-out/_next/static/chunks/220.1ed70c874686122f.js +0 -1
  61. package/admin-out/_next/static/chunks/23-092ac3901411582e.js +0 -1
  62. package/admin-out/_next/static/chunks/362.5970ef15ec3d7b15.js +0 -1
  63. package/admin-out/_next/static/chunks/449.a403f7ad18054e3f.js +0 -1
  64. package/admin-out/_next/static/chunks/613.f314fc12e05a566c.js +0 -1
  65. package/admin-out/_next/static/chunks/73.a4617a551a8e176c.js +0 -1
  66. package/admin-out/_next/static/chunks/767.16a0b33b6f5e7965.js +0 -1
  67. package/admin-out/_next/static/chunks/839.e381287756453ffe.js +0 -1
  68. package/admin-out/_next/static/chunks/868.cc7fd84c9f2fd711.js +0 -1
  69. package/admin-out/_next/static/chunks/892.92b2ed455683b49a.js +0 -1
  70. package/admin-out/_next/static/chunks/989-6a0ec35b8ae2cb39.js +0 -1
  71. package/admin-out/_next/static/chunks/app/page-bf9c0b21ae431c92.js +0 -1
  72. package/admin-out/_next/static/chunks/webpack-ca926228f8cba1ff.js +0 -1
  73. package/admin-out/_next/static/css/c0ea31cd787f7aa7.css +0 -3
  74. package/bundled-docs/design/goal-mode.md +0 -144
  75. /package/admin-out/_next/static/{qYSILjqBjfgTggO0T8E7T → Wn72NPclwda8BiGd0ChhY}/_buildManifest.js +0 -0
  76. /package/admin-out/_next/static/{qYSILjqBjfgTggO0T8E7T → Wn72NPclwda8BiGd0ChhY}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1048,6 +1048,9 @@ var init_config = __esm({
1048
1048
  ENV_PATTERN2 = /<<([A-Z_][A-Z0-9_]*)>>/g;
1049
1049
  _wsSettingsCache = /* @__PURE__ */ new Map();
1050
1050
  config = {
1051
+ // Goal entry points are retired, not the runtime. Internal/global-only, no UI:
1052
+ // set general.goal_mode_enabled in ~/.halo/secrets/settings.yaml, then restart.
1053
+ goalModeEnabled: settingsBool("general.goal_mode_enabled", false),
1051
1054
  server: {
1052
1055
  port: systemInt("HALO_PORT", "server.port", 9527),
1053
1056
  /** scrypt hash stored in `secrets/config.yaml`, set by `halo setup`.
@@ -1142,10 +1145,12 @@ var init_config = __esm({
1142
1145
  timeout: {
1143
1146
  shellExec: systemInt("HALO_SHELL_TIMEOUT", "timeout.shell_exec", 12e4),
1144
1147
  webFetch: systemInt("HALO_WEB_FETCH_TIMEOUT", "timeout.web_fetch", 1e4),
1145
- // Per model-call wall-clock cap. A bare fetch has no default timeout, so a
1146
- // half-open connection (server accepted but never sends bytes / sends no
1147
- // RST) hangs the await indefinitely. Bounds each callModel; on expiry the
1148
- // request aborts and the agent loop's retry takes over.
1148
+ // Per model-call cap. A bare fetch has no default timeout, so a half-open
1149
+ // connection (server accepted but never sends bytes / sends no RST) hangs
1150
+ // the await indefinitely. Bounds each callModel; on expiry the request
1151
+ // aborts and the agent loop's retry takes over. Streaming providers reset
1152
+ // the timer on every chunk, so for them it is an idle timeout — a long but
1153
+ // live stream is never cut off.
1149
1154
  modelRequest: systemInt("HALO_MODEL_TIMEOUT", "timeout.model_request", 30 * 6e4),
1150
1155
  sessionGrace: systemInt("HALO_SESSION_GRACE", "timeout.session_grace", 5 * 6e4),
1151
1156
  terminalGrace: systemInt("HALO_TERMINAL_GRACE", "timeout.terminal_grace", 5 * 6e4)
@@ -1402,7 +1407,7 @@ function classifyKind(msg, errName, httpStatus) {
1402
1407
  if (errName === "ThrottlingException" || httpStatus === 429 || msg.includes("throttl") || msg.includes("rate limit") || msg.includes("ThrottlingException") || msg.includes("ServiceUnavailableException") || msg.includes("API error 429")) {
1403
1408
  return "throttle";
1404
1409
  }
1405
- if (errName === "InternalServerException" || errName === "ModelTimeoutException" || errName === "ServiceUnavailableException" || httpStatus === 500 || httpStatus === 502 || httpStatus === 503 || httpStatus === 504 || httpStatus === 529 || httpStatus === 408) {
1410
+ if (errName === "InternalServerException" || errName === "ModelTimeoutException" || errName === "ModelStreamErrorException" || errName === "ServiceUnavailableException" || httpStatus === 500 || httpStatus === 502 || httpStatus === 503 || httpStatus === 504 || httpStatus === 529 || httpStatus === 408) {
1406
1411
  return "server_error";
1407
1412
  }
1408
1413
  if (msg === "fetch failed" || msg === "Model request timed out" || msg.includes("http2 request did not get a response") || msg.includes("UND_ERR_HEADERS_TIMEOUT") || msg.includes("HeadersTimeoutError") || msg.includes("socket hang up") || msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("EAI_AGAIN")) {
@@ -1616,7 +1621,7 @@ function toolResultImages(content) {
1616
1621
  }
1617
1622
  return images.length > 0 ? [{ type: "text", text: "[Images from the tool results above]" }, ...images] : [];
1618
1623
  }
1619
- var TOOL_ERROR_MARKER, TOOL_WARN_MARKER, MODEL_TIMEOUT_ERROR, AgentLoop;
1624
+ var TOOL_ERROR_MARKER, TOOL_WARN_MARKER, MODEL_TIMEOUT_ERROR, INTERRUPTED_REPLY_MARKER, AgentLoop;
1620
1625
  var init_agent_loop = __esm({
1621
1626
  "../server/dist/agents/agent-loop.js"() {
1622
1627
  "use strict";
@@ -1624,6 +1629,7 @@ var init_agent_loop = __esm({
1624
1629
  TOOL_ERROR_MARKER = "__TOOL_ERROR__";
1625
1630
  TOOL_WARN_MARKER = "__TOOL_WARN__";
1626
1631
  MODEL_TIMEOUT_ERROR = "Model request timed out";
1632
+ INTERRUPTED_REPLY_MARKER = "\n\n[reply interrupted by the user here \u2014 the text above is what was shown before the cut]";
1627
1633
  AgentLoop = class {
1628
1634
  tools;
1629
1635
  messages = [];
@@ -1658,8 +1664,42 @@ var init_agent_loop = __esm({
1658
1664
  timer.unref?.();
1659
1665
  const callSignal = options?.cancelSignal ? AbortSignal.any([options.cancelSignal, timeoutController.signal]) : timeoutController.signal;
1660
1666
  let result;
1667
+ let streamedText = "";
1661
1668
  try {
1662
- result = await this.callModel(callSignal);
1669
+ const deltas = [];
1670
+ let wake = null;
1671
+ let settled = false;
1672
+ const notify = () => {
1673
+ if (wake) {
1674
+ wake();
1675
+ wake = null;
1676
+ }
1677
+ };
1678
+ const call = this.callModel(callSignal, (d) => {
1679
+ deltas.push(d);
1680
+ timer.refresh();
1681
+ notify();
1682
+ });
1683
+ call.then(() => {
1684
+ settled = true;
1685
+ notify();
1686
+ }, () => {
1687
+ settled = true;
1688
+ notify();
1689
+ });
1690
+ while (!settled) {
1691
+ if (deltas.length === 0)
1692
+ await new Promise((r) => {
1693
+ wake = r;
1694
+ });
1695
+ while (deltas.length > 0) {
1696
+ const d = deltas.shift();
1697
+ if (d.type === "text_delta")
1698
+ streamedText += d.text;
1699
+ yield { type: d.type, text: d.text };
1700
+ }
1701
+ }
1702
+ result = await call;
1663
1703
  } catch (err) {
1664
1704
  if (timeoutController.signal.aborted && !options?.cancelSignal?.aborted) {
1665
1705
  throw new Error(MODEL_TIMEOUT_ERROR);
@@ -1667,9 +1707,12 @@ var init_agent_loop = __esm({
1667
1707
  throw err;
1668
1708
  } finally {
1669
1709
  clearTimeout(timer);
1710
+ if (result === void 0 && options?.cancelSignal?.aborted && streamedText.length > 0) {
1711
+ this.messages.push({ role: "assistant", content: [{ type: "text", text: streamedText + INTERRUPTED_REPLY_MARKER }] });
1712
+ }
1670
1713
  }
1671
1714
  if (result.stopReason === "refusal") {
1672
- yield { type: "usage", usage: result.usage, durationMs: result.durationMs };
1715
+ yield { type: "usage", usage: result.usage, durationMs: result.durationMs, ttftMs: result.ttftMs };
1673
1716
  yield { type: "stop", stopReason: "refusal", stopDetails: result.stopDetails };
1674
1717
  return;
1675
1718
  }
@@ -1688,7 +1731,8 @@ var init_agent_loop = __esm({
1688
1731
  yield {
1689
1732
  type: "usage",
1690
1733
  usage: result.usage,
1691
- durationMs: result.durationMs
1734
+ durationMs: result.durationMs,
1735
+ ttftMs: result.ttftMs
1692
1736
  };
1693
1737
  if (result.stopReason !== "tool_use") {
1694
1738
  yield { type: "stop", stopReason: result.stopReason };
@@ -2457,6 +2501,7 @@ function ensureHaloHome(haloHome2) {
2457
2501
  const globalDir2 = path8.join(haloHome2, "global");
2458
2502
  const secretsDir = path8.join(haloHome2, "secrets");
2459
2503
  fs7.mkdirSync(globalDir2, { recursive: true });
2504
+ fs7.mkdirSync(path8.join(globalDir2, "extensions"), { recursive: true });
2460
2505
  fs7.mkdirSync(secretsDir, { recursive: true, mode: 448 });
2461
2506
  try {
2462
2507
  fs7.chmodSync(secretsDir, 448);
@@ -2573,7 +2618,7 @@ var init_init = __esm({
2573
2618
  path8.resolve(__dirname, "..", "bundled-docs"),
2574
2619
  path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
2575
2620
  ];
2576
- TEMPLATE_VERSION = 67;
2621
+ TEMPLATE_VERSION = 74;
2577
2622
  VERSION_FILE = ".template-version";
2578
2623
  SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
2579
2624
  BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
@@ -2617,7 +2662,12 @@ var init_init = __esm({
2617
2662
  // root-prompt injection. Loaded on demand via `activate_skill('halo')`
2618
2663
  // when the user asks anything about Halo itself (config dirs, MD scopes,
2619
2664
  // doc lookup table). user-invocable: false — no `/halo` command.
2620
- "halo"
2665
+ "halo",
2666
+ // Canvas preview extensions: `/extension install|list|remove` manages
2667
+ // `~/.halo/global/extensions/<id>/` via templates/ext.sh (download from
2668
+ // halo-hub, unpack, pre-check, atomic replace). The server's root-dir
2669
+ // watcher picks the change up — no endpoint call from the skill.
2670
+ "extension"
2621
2671
  ]);
2622
2672
  BUNDLED_DOCS = [
2623
2673
  "guide/getting-started.md",
@@ -2773,7 +2823,7 @@ function buildSessionTools(sm, sessionId) {
2773
2823
  type: "object",
2774
2824
  properties: {
2775
2825
  agent_id: { type: "string", description: "The agent ID \u2014 must be one of the agents listed in your system prompt. Do not invent ids." },
2776
- message: { type: "string", description: "Task description / initial message for the agent" },
2826
+ message: { type: "string", description: "The brief \u2014 all the worker will know. State: the goal and what done looks like (where the deliverable lives, which checks pass); decisions the user already made in this conversation (choices, rejected options, constraints \u2014 quote them when wording matters); what you already know (paths, line numbers, snippets, the pattern to copy \u2014 findings, not pointers); boundaries (files not to touch, whether commit / push / build / deploy is allowed, in-flight work to leave alone); the exact verification commands. One task per session." },
2777
2827
  system_prompt_context: { type: "string", description: "Optional background material prepended to the opening message BEFORE your task text \u2014 despite the name, it is NOT injected into the system prompt. Put here what frames the whole task: relevant file paths, prior findings, user-stated constraints, role framing. Keep one-shot task instructions in `message` itself so it stays a clean task statement. Note the cost: this text lives in the child's conversation history and is re-sent on every one of its model calls until compaction \u2014 include what the child genuinely needs throughout, not bulk material it could read from files instead." },
2778
2828
  working_dir: { type: "string", description: "Optional focus directory for the sub-agent (workspace-relative or absolute; must be inside the workspace). The platform bakes the directory-scoped INSTRUCTIONS.md found along the path from the workspace root down to this directory into the sub-agent's system prompt (present every turn), and tags its prompt with this focus. Does NOT change where tools run (shell/file tools still operate from the project root). Omit for project-root scope." },
2779
2829
  title: { type: "string", description: "Optional session title shown in the admin sidebar. When omitted, auto-generated from the task message." }
@@ -3019,7 +3069,7 @@ ${skillInfo}` : ""
3019
3069
  function buildContinueTaskTool(sm, sessionId) {
3020
3070
  return {
3021
3071
  name: "continue_task",
3022
- description: "Call this when your turn was started by an interruption (a user or parent message arrived while you were working) and the task you were doing is NOT finished yet. After your current reply ends, the session automatically resumes the interrupted task. Do NOT call it if the task is done, if the user told you to stop, or in a normal (uninterrupted) turn \u2014 it does nothing there. The flag lasts one turn: if you get interrupted again before resuming, call it again.",
3072
+ description: "Call this when your turn was started by an interruption (a user or parent message arrived while you were working) and the task you were doing is NOT finished yet. After your current reply ends, the session automatically resumes the interrupted task. Do NOT call it if the task is done, if the user told you to stop, or in a normal (uninterrupted) turn \u2014 it does nothing there. The flag lasts one turn: if you get interrupted again before resuming, call it again. Write your reply to the interrupting message FIRST, then call this.",
3023
3073
  inputSchema: { type: "object", properties: {} },
3024
3074
  callback: async () => {
3025
3075
  switch (sm.requestSelfKick(sessionId)) {
@@ -4003,6 +4053,7 @@ var init_i18n = __esm({
4003
4053
  "agent.delete_usage": { zh: "\u7528\u6CD5\uFF1A/agent delete <\u540D\u79F0\u6216\u7F16\u53F7>", en: "Usage: /agent delete <name or number>" },
4004
4054
  "agent.delete_done": { zh: '\u{1F5D1} \u5DF2\u5220\u9664 agent "{name}"\uFF08{scope}\uFF09', en: '\u{1F5D1} Deleted agent "{name}" ({scope})' },
4005
4055
  // ── /goal verbs (goal mode) ──
4056
+ "goal.disabled": { zh: "Goal \u6A21\u5F0F\u5DF2\u4E0B\u7EBF\u3002", en: "Goal mode is disabled." },
4006
4057
  "verb.goal.create": { zh: "\u5728\u5F53\u524D\u4F1A\u8BDD\u4E0A\u542F\u52A8 goal \u610F\u56FE\u5BF9\u8BDD", en: "Start goal intake on the current session" },
4007
4058
  "verb.goal.status": { zh: "\u67E5\u770B\u5F53\u524D goal \u7684\u8F6E\u6B21 / \u4E0A\u9650 / \u72B6\u6001", en: "Show the current goal's round / caps / state" },
4008
4059
  "verb.goal.pause": { zh: "\u6682\u505C goal\uFF08\u505C\u6B62 worker \u548C goal \u4F1A\u8BDD\uFF09", en: "Pause the goal (stops worker + goal session)" },
@@ -4052,8 +4103,23 @@ var init_registry = __esm({
4052
4103
  registerDescriptor(desc4) {
4053
4104
  this.descriptors.set(desc4.name, desc4);
4054
4105
  }
4055
- listDescriptors() {
4056
- return Array.from(this.descriptors.values()).filter((d) => !d.hidden);
4106
+ listDescriptors(opts) {
4107
+ return Array.from(this.descriptors.values()).filter((d) => opts?.includeHidden || !d.hidden);
4108
+ }
4109
+ /** Startup integrity checks include hidden commands: visibility is not routing. */
4110
+ assertDispatchCommands(commands) {
4111
+ const builtins = this.listDescriptors({ includeHidden: true }).filter((d) => d.source === "builtin");
4112
+ const declared = new Set(builtins.map((d) => d.slashName));
4113
+ const dispatched = new Set(commands);
4114
+ const declaredServer = new Set(builtins.filter((d) => d.type === "server").map((d) => d.slashName));
4115
+ const missingDispatch = [...declaredServer].filter((n) => !dispatched.has(n));
4116
+ const orphanDispatch = [...dispatched].filter((n) => !declared.has(n));
4117
+ if (missingDispatch.length > 0) {
4118
+ throw new Error(`[Server] Command descriptors without a dispatch case: ${missingDispatch.join(", ")}. Either add a case in channels/shared/commands.ts dispatchCommand or change the descriptor type to 'client'.`);
4119
+ }
4120
+ if (orphanDispatch.length > 0) {
4121
+ throw new Error(`[Server] Dispatch cases without a descriptor: ${orphanDispatch.join(", ")}. Add a registerDescriptor entry in commands/index.ts or remove the dispatch case.`);
4122
+ }
4057
4123
  }
4058
4124
  };
4059
4125
  }
@@ -4068,6 +4134,7 @@ var init_commands = __esm({
4068
4134
  "../server/dist/commands/index.js"() {
4069
4135
  "use strict";
4070
4136
  init_registry();
4137
+ init_config();
4071
4138
  commandRegistry = new CommandRegistry();
4072
4139
  commandRegistry.registerDescriptor({ name: "help", slashName: "/help", description: "Show available commands", type: "server", source: "builtin" });
4073
4140
  commandRegistry.registerDescriptor({ name: "session", slashName: "/session", description: "Manage sessions", type: "server", argHint: "<verb>", source: "builtin", verbs: [
@@ -4096,7 +4163,7 @@ var init_commands = __esm({
4096
4163
  { name: "create", desc: "Create a new agent" },
4097
4164
  { name: "update", desc: "Modify an existing agent" }
4098
4165
  ] });
4099
- commandRegistry.registerDescriptor({ name: "goal", slashName: "/goal", description: "Run a goal loop (judge + dispatch until done)", type: "server", argHint: "<verb>", source: "builtin", verbs: [
4166
+ commandRegistry.registerDescriptor({ name: "goal", slashName: "/goal", description: "Run a goal loop (judge + dispatch until done)", type: "server", argHint: "<verb>", source: "builtin", hidden: !config.goalModeEnabled, verbs: [
4100
4167
  { name: "create", builtin: true, desc: "Start goal intake on the current session" },
4101
4168
  { name: "status", builtin: true, desc: "Show the current goal's round / caps / state" },
4102
4169
  { name: "pause", builtin: true, desc: "Pause the goal (stops worker + goal session)" },
@@ -4173,7 +4240,7 @@ async function getCommandSkillInfo(slashCommand, workspaceRoot) {
4173
4240
  }
4174
4241
  async function scanSkillDescriptors(workspaceRoot) {
4175
4242
  const entries = await scanSkillEntries(workspaceRoot);
4176
- const builtinSlashes = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
4243
+ const builtinSlashes = new Set(commandRegistry.listDescriptors({ includeHidden: true }).filter((d) => d.source === "builtin").map((d) => d.slashName));
4177
4244
  const claimed = new Set(builtinSlashes);
4178
4245
  const listed = [];
4179
4246
  for (const entry of entries) {
@@ -4409,6 +4476,9 @@ function wsEvoArchivedApplyZip(workspacePath, applyId) {
4409
4476
  function wsEvoHistoryDir(workspacePath, applyId) {
4410
4477
  return path15.join(wsEvoDir(workspacePath), "history", `apply-${applyId}`);
4411
4478
  }
4479
+ function globalExtensionsDir() {
4480
+ return path15.join(globalDir(), "extensions");
4481
+ }
4412
4482
  function globalLogsDir() {
4413
4483
  return path15.join(globalDir(), "logs");
4414
4484
  }
@@ -5132,7 +5202,7 @@ async function dispatchCommand(ctx, command, arg, opts) {
5132
5202
  // Goal mode (docs/plans/loop-mode.md): all verbs are builtin (no backing
5133
5203
  // skill) — create/status/pause/resume/clear in SUBCOMMAND_ROUTES.
5134
5204
  case "/goal":
5135
- return routeObjectOrSkill(ctx, command, arg);
5205
+ return config.goalModeEnabled ? routeObjectOrSkill(ctx, command, arg) : { text: t("goal.disabled", ctx.lang) };
5136
5206
  default:
5137
5207
  return routeObjectOrSkill(ctx, command, arg);
5138
5208
  }
@@ -5453,6 +5523,7 @@ var init_commands2 = __esm({
5453
5523
  init_i18n();
5454
5524
  init_skill_command();
5455
5525
  init_commands();
5526
+ init_config();
5456
5527
  init_enqueue();
5457
5528
  init_goal_mode();
5458
5529
  ALIASES_FILE = path17.join(os.homedir(), ".halo", "global", "aliases.yaml");
@@ -5571,7 +5642,7 @@ ${report}`;
5571
5642
  const body = report.length > cap2 ? report.slice(0, cap2) + `
5572
5643
 
5573
5644
  [Report truncated: ${report.length} chars total. Use relay_read("${host.workspaceRoot}", "${session.id}") for the full text.]` : report;
5574
- const header = `[Relay report \xB7 workspace ${host.workspaceRoot} \xB7 session ${session.id}]`;
5645
+ const header = `[Relay report \xB7 workspace ${host.workspaceRoot} \xB7 session ${session.id} \xB7 status: ${session.turnError ? "aborted" : "completed"}]`;
5575
5646
  const text6 = `${header}
5576
5647
 
5577
5648
  ${body}`;
@@ -5603,7 +5674,7 @@ async function deliverRelayInterim(host, sessionId, body) {
5603
5674
  console.error(`[Relay] caller workspace ${to.workspace} unreachable: ${err instanceof Error ? err.message : String(err)}`);
5604
5675
  return;
5605
5676
  }
5606
- const header = `[Relay interim report \xB7 workspace ${host.workspaceRoot} \xB7 session ${sessionId}] The session answered your latest message (below) and is now resuming the task that message interrupted \u2014 its final [Relay report] follows when that is done.`;
5677
+ const header = `[Relay interim report \xB7 workspace ${host.workspaceRoot} \xB7 session ${sessionId} \xB7 status: still running] This is an interim reply \u2014 the session is still working; its final [Relay report] follows when done. Do not treat this as the result.`;
5607
5678
  const text6 = `${header}
5608
5679
 
5609
5680
  ${body}`;
@@ -5670,7 +5741,7 @@ function buildRelayTools(host, callerSessionId) {
5670
5741
  async function dispatch6(params, hard) {
5671
5742
  const registry2 = getRelayRegistry();
5672
5743
  if (!registry2)
5673
- return jsonErr2("relay is unavailable in this runtime (server only)");
5744
+ return jsonErr2(RELAY_UNAVAILABLE);
5674
5745
  const resolved = resolveTarget(registry2, params.workspace);
5675
5746
  if (typeof resolved === "string")
5676
5747
  return resolved;
@@ -5705,7 +5776,7 @@ ${params.message}`;
5705
5776
  try {
5706
5777
  const registry2 = getRelayRegistry();
5707
5778
  if (!registry2)
5708
- return jsonErr2("relay is unavailable in this runtime (server only)");
5779
+ return jsonErr2(RELAY_UNAVAILABLE);
5709
5780
  const resolved = resolveTarget(registry2, params.workspace);
5710
5781
  if (typeof resolved === "string")
5711
5782
  return resolved;
@@ -5732,7 +5803,7 @@ ${params.message}`;
5732
5803
  try {
5733
5804
  const registry2 = getRelayRegistry();
5734
5805
  if (!registry2)
5735
- return jsonErr2("relay is unavailable in this runtime (server only)");
5806
+ return jsonErr2(RELAY_UNAVAILABLE);
5736
5807
  const resolved = resolveTarget(registry2, params.workspace);
5737
5808
  if (typeof resolved === "string")
5738
5809
  return resolved;
@@ -5757,7 +5828,7 @@ ${params.message}`;
5757
5828
  try {
5758
5829
  const registry2 = getRelayRegistry();
5759
5830
  if (!registry2)
5760
- return jsonErr2("relay is unavailable in this runtime (server only)");
5831
+ return jsonErr2(RELAY_UNAVAILABLE);
5761
5832
  const resolved = resolveTarget(registry2, params.workspace ?? host.workspaceRoot);
5762
5833
  if (typeof resolved === "string")
5763
5834
  return resolved;
@@ -5779,7 +5850,7 @@ ${params.message}`;
5779
5850
  };
5780
5851
  return [relaySend, relayInterrupt, relayStop, relayRead, relayList];
5781
5852
  }
5782
- var RELAY_CHANNEL_PREFIX, _registry, WORKSPACE_SESSION_PROPS;
5853
+ var RELAY_CHANNEL_PREFIX, _registry, RELAY_UNAVAILABLE, WORKSPACE_SESSION_PROPS;
5783
5854
  var init_relay = __esm({
5784
5855
  "../server/dist/agents/relay.js"() {
5785
5856
  "use strict";
@@ -5788,6 +5859,7 @@ var init_relay = __esm({
5788
5859
  init_commands2();
5789
5860
  RELAY_CHANNEL_PREFIX = "[channel: relay | from: ";
5790
5861
  _registry = null;
5862
+ RELAY_UNAVAILABLE = "relay is not available in the CLI / TUI \u2014 it only runs inside `halo server`. This is permanent for this runtime, not a temporary outage: do not retry. Tell the user to send this request from the admin UI or an IM / Web channel connected to the server, or to open the target workspace directly with `halo tui -w <workspace path>`.";
5791
5863
  WORKSPACE_SESSION_PROPS = {
5792
5864
  workspace: { type: "string", description: "Absolute path of the target workspace on this server." },
5793
5865
  session_id: { type: "string", description: "Session id inside that workspace." }
@@ -5982,27 +6054,27 @@ var init_workspace = __esm({
5982
6054
  constructor(projectRoot) {
5983
6055
  this.projectRoot = projectRoot;
5984
6056
  }
5985
- async readFile(path70) {
5986
- const fullPath = this.validatePath(path70);
6057
+ async readFile(path74) {
6058
+ const fullPath = this.validatePath(path74);
5987
6059
  return readFile(fullPath, "utf-8");
5988
6060
  }
5989
- async writeFile(path70, content) {
5990
- const fullPath = this.validatePath(path70);
6061
+ async writeFile(path74, content) {
6062
+ const fullPath = this.validatePath(path74);
5991
6063
  const dir = resolve(fullPath, "..");
5992
6064
  await mkdir(dir, { recursive: true });
5993
6065
  await writeFile(fullPath, content, "utf-8");
5994
6066
  }
5995
- validatePath(path70) {
5996
- const fullPath = isAbsolute(path70) ? resolve(path70) : resolve(this.projectRoot, path70);
6067
+ validatePath(path74) {
6068
+ const fullPath = isAbsolute(path74) ? resolve(path74) : resolve(this.projectRoot, path74);
5997
6069
  const root = resolve(this.projectRoot);
5998
6070
  if (fullPath !== root && !fullPath.startsWith(root + sep)) {
5999
- throw new WorkspaceError(`Path "${path70}" is outside the workspace directory "${this.projectRoot}"`);
6071
+ throw new WorkspaceError(`Path "${path74}" is outside the workspace directory "${this.projectRoot}"`);
6000
6072
  }
6001
6073
  try {
6002
6074
  const real = realpathSync(fullPath);
6003
6075
  const realRoot = realpathSync(root);
6004
6076
  if (real !== realRoot && !real.startsWith(realRoot + sep)) {
6005
- throw new WorkspaceError(`Path "${path70}" is outside the workspace directory "${this.projectRoot}"`);
6077
+ throw new WorkspaceError(`Path "${path74}" is outside the workspace directory "${this.projectRoot}"`);
6006
6078
  }
6007
6079
  } catch (err) {
6008
6080
  if (err instanceof WorkspaceError)
@@ -6192,12 +6264,12 @@ var init_git_manager = __esm({
6192
6264
  const code = parts[0];
6193
6265
  const status = code[0];
6194
6266
  const isRenameOrCopy = status === "R" || status === "C";
6195
- const path70 = isRenameOrCopy ? parts[2] : parts[1];
6196
- if (!path70 || seen.has(path70))
6267
+ const path74 = isRenameOrCopy ? parts[2] : parts[1];
6268
+ if (!path74 || seen.has(path74))
6197
6269
  continue;
6198
- seen.add(path70);
6270
+ seen.add(path74);
6199
6271
  files.push({
6200
- path: path70,
6272
+ path: path74,
6201
6273
  status,
6202
6274
  from: isRenameOrCopy ? parts[1] : void 0
6203
6275
  });
@@ -6345,9 +6417,9 @@ var init_git_manager = __esm({
6345
6417
  * empty for an added file or the root commit) and `modified` is the version
6346
6418
  * at `commit` (empty for a deletion). The working-tree/staged path above is
6347
6419
  * left entirely untouched (commit === undefined preserves existing behavior). */
6348
- async getFileDiff(path70, staged, from, commit) {
6420
+ async getFileDiff(path74, staged, from, commit) {
6349
6421
  if (commit) {
6350
- const parentPath = from || path70;
6422
+ const parentPath = from || path74;
6351
6423
  let original2 = "";
6352
6424
  try {
6353
6425
  original2 = await this.git.show([`${commit}^:${parentPath}`]);
@@ -6356,13 +6428,13 @@ var init_git_manager = __esm({
6356
6428
  }
6357
6429
  let modified2 = "";
6358
6430
  try {
6359
- modified2 = await this.git.show([`${commit}:${path70}`]);
6431
+ modified2 = await this.git.show([`${commit}:${path74}`]);
6360
6432
  } catch {
6361
6433
  modified2 = "";
6362
6434
  }
6363
6435
  return { original: original2, modified: modified2 };
6364
6436
  }
6365
- const headPath = from || path70;
6437
+ const headPath = from || path74;
6366
6438
  let original = "";
6367
6439
  try {
6368
6440
  original = await this.git.show([`HEAD:${headPath}`]);
@@ -6372,13 +6444,13 @@ var init_git_manager = __esm({
6372
6444
  let modified = "";
6373
6445
  if (staged) {
6374
6446
  try {
6375
- modified = await this.git.show([`:${path70}`]);
6447
+ modified = await this.git.show([`:${path74}`]);
6376
6448
  } catch {
6377
6449
  modified = "";
6378
6450
  }
6379
6451
  } else {
6380
6452
  try {
6381
- modified = await this.workspace.readFile(path70);
6453
+ modified = await this.workspace.readFile(path74);
6382
6454
  } catch {
6383
6455
  modified = "";
6384
6456
  }
@@ -6736,10 +6808,24 @@ function applyEvent(state, event) {
6736
6808
  initSubSessionLog(state, taskId, event.agentId ?? "", agentName, "");
6737
6809
  }
6738
6810
  switch (event.type) {
6811
+ // Streaming chunks land in the turn blocks as they arrive; the whole
6812
+ // `thinking` / `stream` that follows is stamped `streamed` and skipped so
6813
+ // the text isn't appended twice. Deltas don't trigger a save — the usage /
6814
+ // tool events that follow do.
6815
+ case "thinking_delta":
6816
+ appendThinking(getTarget(state, taskId), event.text ?? "");
6817
+ break;
6818
+ case "stream_delta":
6819
+ appendStream(getTarget(state, taskId), event.text ?? "", agentName);
6820
+ break;
6739
6821
  case "thinking":
6822
+ if (event.streamed)
6823
+ break;
6740
6824
  appendThinking(getTarget(state, taskId), event.text ?? "");
6741
6825
  break;
6742
6826
  case "stream":
6827
+ if (event.streamed)
6828
+ break;
6743
6829
  appendStream(getTarget(state, taskId), event.text ?? "", agentName);
6744
6830
  break;
6745
6831
  case "agent_start":
@@ -6826,9 +6912,8 @@ function applyEvent(state, event) {
6826
6912
  if (taskId) {
6827
6913
  const sub = state.subSessionLogs.get(taskId);
6828
6914
  if (sub) {
6829
- const turnId = sub.currentTurnId;
6830
- flushCompletedAssistantMessage(sub);
6831
- sub.messageLog.push(buildUsageMsg(event, agentName, taskId, turnId));
6915
+ sub.messageLog.push(buildUsageMsg(event, agentName, taskId, sub.currentTurnId));
6916
+ sub.currentTurnId = randomUUID();
6832
6917
  result.subSessionSave = taskId;
6833
6918
  }
6834
6919
  }
@@ -7147,15 +7232,15 @@ var init_session_ui_store = __esm({
7147
7232
  persistUIState(rootId, state, archive) {
7148
7233
  try {
7149
7234
  const projectPath = this.uiStateProjectPaths.get(rootId) ?? this.host.workspaceRoot;
7150
- const snapshot = createSaveSnapshot(state);
7151
- if (snapshot.length === 0)
7235
+ const snapshot2 = createSaveSnapshot(state);
7236
+ if (snapshot2.length === 0)
7152
7237
  return;
7153
7238
  const inMem = this.host.getSession(rootId);
7154
7239
  const row = inMem ? null : this.db.select().from(agentSessions).where(eq6(agentSessions.id, rootId)).get();
7155
7240
  this.host.persistSessionFile({
7156
7241
  sessionId: rootId,
7157
7242
  projectPath,
7158
- messages: snapshot,
7243
+ messages: snapshot2,
7159
7244
  contextTokens: state.contextTokens,
7160
7245
  outputTokens: state.outputTokens,
7161
7246
  agentId: inMem?.agentId ?? row?.agentId,
@@ -7676,8 +7761,236 @@ var init_session_query_store = __esm({
7676
7761
  }
7677
7762
  });
7678
7763
 
7764
+ // ../server/dist/agents/sse.js
7765
+ async function* readSseJson(body) {
7766
+ const reader = body.getReader();
7767
+ const decoder = new TextDecoder();
7768
+ let buffer = "";
7769
+ try {
7770
+ while (true) {
7771
+ const { done, value } = await reader.read();
7772
+ if (done)
7773
+ break;
7774
+ buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
7775
+ let sep2;
7776
+ while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
7777
+ const payload2 = frameData(buffer.slice(0, sep2));
7778
+ buffer = buffer.slice(sep2 + 2);
7779
+ if (payload2 !== void 0)
7780
+ yield JSON.parse(payload2);
7781
+ }
7782
+ }
7783
+ const payload = frameData((buffer + decoder.decode()).replace(/\r\n/g, "\n"));
7784
+ if (payload !== void 0)
7785
+ yield JSON.parse(payload);
7786
+ } finally {
7787
+ reader.releaseLock();
7788
+ }
7789
+ }
7790
+ function frameData(frame) {
7791
+ const data = [];
7792
+ for (const line of frame.split("\n")) {
7793
+ if (!line.startsWith("data:"))
7794
+ continue;
7795
+ data.push(line.slice(line[5] === " " ? 6 : 5));
7796
+ }
7797
+ if (data.length === 0)
7798
+ return void 0;
7799
+ const payload = data.join("\n");
7800
+ return payload === "[DONE]" ? void 0 : payload;
7801
+ }
7802
+ var init_sse = __esm({
7803
+ "../server/dist/agents/sse.js"() {
7804
+ "use strict";
7805
+ }
7806
+ });
7807
+
7808
+ // ../server/dist/agents/anthropic-stream.js
7809
+ async function fetchAnthropicStream(opts) {
7810
+ const { tag } = opts;
7811
+ const startTime = Date.now();
7812
+ const res = await fetch(opts.url, {
7813
+ method: "POST",
7814
+ signal: opts.signal,
7815
+ headers: { "content-type": "application/json", ...opts.headers },
7816
+ body: JSON.stringify({ ...opts.body, stream: true })
7817
+ });
7818
+ if (!res.ok) {
7819
+ const raw = await res.text();
7820
+ let err;
7821
+ try {
7822
+ err = JSON.parse(raw);
7823
+ } catch {
7824
+ throw new Error(`[${tag}] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
7825
+ }
7826
+ throw new Error(`[${tag}] ${res.status} ${err.error?.type ?? "?"}: ${err.error?.message ?? raw.slice(0, 200)}`);
7827
+ }
7828
+ if (!res.body)
7829
+ throw new Error(`[${tag}] empty response body`);
7830
+ const acc = new AnthropicStreamAccumulator(startTime, opts.onDelta);
7831
+ for await (const ev of readSseJson(res.body)) {
7832
+ if (ev.type === "error") {
7833
+ throw new Error(`[${tag}] ${SSE_ERROR_STATUS[ev.error?.type ?? ""] ?? "?"} ${ev.error?.type ?? "?"}: ${ev.error?.message ?? ""}`);
7834
+ }
7835
+ acc.push(ev);
7836
+ }
7837
+ if (opts.signal?.aborted)
7838
+ throw new DOMException("Model call aborted", "AbortError");
7839
+ return acc.finish();
7840
+ }
7841
+ var AnthropicStreamAccumulator, SSE_ERROR_STATUS;
7842
+ var init_anthropic_stream = __esm({
7843
+ "../server/dist/agents/anthropic-stream.js"() {
7844
+ "use strict";
7845
+ init_sse();
7846
+ AnthropicStreamAccumulator = class {
7847
+ startTime;
7848
+ onDelta;
7849
+ blocks = /* @__PURE__ */ new Map();
7850
+ /** Block indices in arrival order — `assistantBlocks` must keep wire order. */
7851
+ order = [];
7852
+ stopReason;
7853
+ stopDetails;
7854
+ inputTokens = 0;
7855
+ outputTokens = 0;
7856
+ cacheReadTokens = 0;
7857
+ cacheWriteTokens = 0;
7858
+ invocationMetrics;
7859
+ firstDeltaAt;
7860
+ constructor(startTime, onDelta) {
7861
+ this.startTime = startTime;
7862
+ this.onDelta = onDelta;
7863
+ }
7864
+ push(event) {
7865
+ switch (event.type) {
7866
+ case "message_start": {
7867
+ const u = event.message?.usage;
7868
+ this.inputTokens = u?.input_tokens ?? 0;
7869
+ this.cacheReadTokens = u?.cache_read_input_tokens ?? 0;
7870
+ this.cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
7871
+ break;
7872
+ }
7873
+ case "content_block_start": {
7874
+ const cb = event.content_block;
7875
+ const index = event.index ?? this.order.length;
7876
+ if (!cb)
7877
+ break;
7878
+ if (cb.type === "text")
7879
+ this.open(index, { type: "text", text: "" });
7880
+ else if (cb.type === "thinking")
7881
+ this.open(index, { type: "thinking", thinking: "" });
7882
+ else if (cb.type === "tool_use")
7883
+ this.open(index, { type: "tool_use", id: cb.id ?? "", name: cb.name ?? "", partialJson: "" });
7884
+ break;
7885
+ }
7886
+ case "content_block_delta": {
7887
+ const d = event.delta;
7888
+ if (!d)
7889
+ break;
7890
+ if (this.firstDeltaAt === void 0)
7891
+ this.firstDeltaAt = Date.now();
7892
+ const block = this.blocks.get(event.index ?? -1);
7893
+ if (d.type === "text_delta" && block?.type === "text") {
7894
+ block.text += d.text ?? "";
7895
+ if (d.text)
7896
+ this.onDelta?.({ type: "text_delta", text: d.text });
7897
+ } else if (d.type === "thinking_delta" && block?.type === "thinking") {
7898
+ block.thinking += d.thinking ?? "";
7899
+ if (d.thinking)
7900
+ this.onDelta?.({ type: "thinking_delta", text: d.thinking });
7901
+ } else if (d.type === "input_json_delta" && block?.type === "tool_use") {
7902
+ block.partialJson += d.partial_json ?? "";
7903
+ }
7904
+ break;
7905
+ }
7906
+ case "message_delta": {
7907
+ if (event.delta?.stop_reason)
7908
+ this.stopReason = event.delta.stop_reason;
7909
+ if (event.delta?.stop_details) {
7910
+ this.stopDetails = {
7911
+ category: event.delta.stop_details.category ?? null,
7912
+ explanation: event.delta.stop_details.explanation ?? null
7913
+ };
7914
+ }
7915
+ const u = event.usage;
7916
+ if (typeof u?.output_tokens === "number")
7917
+ this.outputTokens = u.output_tokens;
7918
+ if (typeof u?.input_tokens === "number")
7919
+ this.inputTokens = u.input_tokens;
7920
+ if (typeof u?.cache_read_input_tokens === "number")
7921
+ this.cacheReadTokens = u.cache_read_input_tokens;
7922
+ if (typeof u?.cache_creation_input_tokens === "number")
7923
+ this.cacheWriteTokens = u.cache_creation_input_tokens;
7924
+ break;
7925
+ }
7926
+ case "message_stop":
7927
+ this.invocationMetrics = event["amazon-bedrock-invocationMetrics"];
7928
+ break;
7929
+ }
7930
+ }
7931
+ /** Build the loop-facing result. Call once, after the stream has ended. */
7932
+ finish() {
7933
+ let text6 = "";
7934
+ let thinking = "";
7935
+ const toolCalls = [];
7936
+ const assistantBlocks = [];
7937
+ for (const index of this.order) {
7938
+ const block = this.blocks.get(index);
7939
+ if (block.type === "text") {
7940
+ if (!block.text)
7941
+ continue;
7942
+ text6 += block.text;
7943
+ assistantBlocks.push({ type: "text", text: block.text });
7944
+ } else if (block.type === "thinking") {
7945
+ thinking += block.thinking;
7946
+ } else {
7947
+ const input = JSON.parse(block.partialJson || "{}");
7948
+ toolCalls.push({ id: block.id, name: block.name, input });
7949
+ assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input });
7950
+ }
7951
+ }
7952
+ const inputTokens = this.inputTokens || (this.invocationMetrics?.inputTokenCount ?? 0);
7953
+ const outputTokens = this.outputTokens || (this.invocationMetrics?.outputTokenCount ?? 0);
7954
+ return {
7955
+ assistantBlocks,
7956
+ stopReason: this.stopReason ?? "end_turn",
7957
+ ...this.stopDetails ? { stopDetails: this.stopDetails } : {},
7958
+ text: text6,
7959
+ thinking,
7960
+ toolCalls,
7961
+ usage: {
7962
+ inputTokens,
7963
+ outputTokens,
7964
+ totalTokens: inputTokens + outputTokens,
7965
+ ...this.cacheReadTokens ? { cacheReadInputTokens: this.cacheReadTokens } : {},
7966
+ ...this.cacheWriteTokens ? { cacheWriteInputTokens: this.cacheWriteTokens } : {}
7967
+ },
7968
+ durationMs: Date.now() - this.startTime,
7969
+ ...this.firstDeltaAt !== void 0 ? { ttftMs: this.firstDeltaAt - this.startTime } : {}
7970
+ };
7971
+ }
7972
+ open(index, block) {
7973
+ this.blocks.set(index, block);
7974
+ this.order.push(index);
7975
+ }
7976
+ };
7977
+ SSE_ERROR_STATUS = {
7978
+ invalid_request_error: 400,
7979
+ authentication_error: 401,
7980
+ billing_error: 402,
7981
+ permission_error: 403,
7982
+ not_found_error: 404,
7983
+ request_too_large: 413,
7984
+ rate_limit_error: 429,
7985
+ api_error: 500,
7986
+ timeout_error: 504,
7987
+ overloaded_error: 529
7988
+ };
7989
+ }
7990
+ });
7991
+
7679
7992
  // ../server/dist/agents/bedrock-agent.js
7680
- import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
7993
+ import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime";
7681
7994
  function effortToBudget(effort, maxTokens) {
7682
7995
  const table = {
7683
7996
  low: 2048,
@@ -7702,6 +8015,7 @@ var init_bedrock_agent = __esm({
7702
8015
  "use strict";
7703
8016
  init_config();
7704
8017
  init_agent_loop();
8018
+ init_anthropic_stream();
7705
8019
  BedrockAgent = class extends AgentLoop {
7706
8020
  client;
7707
8021
  config;
@@ -7712,10 +8026,10 @@ var init_bedrock_agent = __esm({
7712
8026
  const region = extractRegionFromEndpoint(config2.endpoint);
7713
8027
  this.client = new BedrockRuntimeClient({ region, endpoint: config2.endpoint, ...creds });
7714
8028
  }
7715
- async callModel(signal) {
8029
+ async callModel(signal, onDelta) {
7716
8030
  const body = this.buildRequestBody();
7717
8031
  const startTime = Date.now();
7718
- const command = new InvokeModelCommand({
8032
+ const command = new InvokeModelWithResponseStreamCommand({
7719
8033
  modelId: this.config.modelId,
7720
8034
  contentType: "application/json",
7721
8035
  accept: "application/json",
@@ -7724,45 +8038,15 @@ var init_bedrock_agent = __esm({
7724
8038
  const response = await this.client.send(command, {
7725
8039
  abortSignal: signal
7726
8040
  });
7727
- const durationMs = Date.now() - startTime;
7728
- const raw = new TextDecoder().decode(response.body);
7729
- const msg = JSON.parse(raw);
7730
- let text6 = "";
7731
- let thinking = "";
7732
- const toolCalls = [];
7733
- const assistantBlocks = [];
7734
- for (const block of msg.content) {
7735
- if (block.type === "text" && block.text) {
7736
- text6 += block.text;
7737
- assistantBlocks.push({ type: "text", text: block.text });
7738
- } else if (block.type === "thinking" && block.thinking) {
7739
- thinking += block.thinking;
7740
- } else if (block.type === "tool_use") {
7741
- toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
7742
- assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
7743
- }
8041
+ const acc = new AnthropicStreamAccumulator(startTime, onDelta);
8042
+ const decoder = new TextDecoder();
8043
+ for await (const chunk of response.body ?? []) {
8044
+ if (chunk.chunk?.bytes)
8045
+ acc.push(JSON.parse(decoder.decode(chunk.chunk.bytes)));
7744
8046
  }
7745
- const u = msg.usage;
7746
- const inputTokens = u?.input_tokens ?? 0;
7747
- const outputTokens = u?.output_tokens ?? 0;
7748
- const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
7749
- const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
7750
- return {
7751
- assistantBlocks,
7752
- stopReason: msg.stop_reason ?? "end_turn",
7753
- ...msg.stop_details ? { stopDetails: { category: msg.stop_details.category ?? null, explanation: msg.stop_details.explanation ?? null } } : {},
7754
- text: text6,
7755
- thinking,
7756
- toolCalls,
7757
- usage: {
7758
- inputTokens,
7759
- outputTokens,
7760
- totalTokens: inputTokens + outputTokens,
7761
- ...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
7762
- ...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
7763
- },
7764
- durationMs
7765
- };
8047
+ if (signal?.aborted)
8048
+ throw new DOMException("Model call aborted", "AbortError");
8049
+ return acc.finish();
7766
8050
  }
7767
8051
  /** Build Anthropic Messages API request body */
7768
8052
  buildRequestBody() {
@@ -7828,6 +8112,107 @@ var init_bedrock_agent = __esm({
7828
8112
  }
7829
8113
  });
7830
8114
 
8115
+ // ../server/dist/agents/openai-chat-stream.js
8116
+ async function fetchChatCompletionStream(opts) {
8117
+ const { tag } = opts;
8118
+ const startTime = Date.now();
8119
+ const res = await fetch(opts.url, {
8120
+ method: "POST",
8121
+ signal: opts.signal,
8122
+ headers: { "Content-Type": "application/json", ...opts.headers },
8123
+ body: JSON.stringify({ ...opts.body, stream: true, stream_options: { include_usage: true } })
8124
+ });
8125
+ if (!res.ok) {
8126
+ const errText2 = await res.text().catch(() => "");
8127
+ throw new Error(`[${tag}] API error ${res.status}: ${errText2}`);
8128
+ }
8129
+ if (!res.body)
8130
+ throw new Error(`[${tag}] empty response body`);
8131
+ const acc = new ChatCompletionAccumulator(startTime, opts.onDelta);
8132
+ for await (const chunk of readSseJson(res.body)) {
8133
+ if (chunk.error) {
8134
+ throw new Error(`[${tag}] API error in stream: ${JSON.stringify(chunk.error).slice(0, 300)}`);
8135
+ }
8136
+ acc.push(chunk);
8137
+ }
8138
+ if (opts.signal?.aborted)
8139
+ throw new DOMException("Model call aborted", "AbortError");
8140
+ return acc.finish();
8141
+ }
8142
+ var ChatCompletionAccumulator;
8143
+ var init_openai_chat_stream = __esm({
8144
+ "../server/dist/agents/openai-chat-stream.js"() {
8145
+ "use strict";
8146
+ init_sse();
8147
+ ChatCompletionAccumulator = class {
8148
+ startTime;
8149
+ onDelta;
8150
+ content = "";
8151
+ reasoning = "";
8152
+ toolCalls = [];
8153
+ finishReason;
8154
+ usage;
8155
+ firstDeltaAt;
8156
+ constructor(startTime, onDelta) {
8157
+ this.startTime = startTime;
8158
+ this.onDelta = onDelta;
8159
+ }
8160
+ push(chunk) {
8161
+ if (chunk.usage)
8162
+ this.usage = chunk.usage;
8163
+ const choice = chunk.choices?.[0];
8164
+ if (!choice || typeof choice.index === "number" && choice.index !== 0)
8165
+ return;
8166
+ if (choice.finish_reason)
8167
+ this.finishReason = choice.finish_reason;
8168
+ const d = choice.delta;
8169
+ if (!d)
8170
+ return;
8171
+ if (typeof d.content === "string" && d.content !== "") {
8172
+ this.markFirstDelta();
8173
+ this.content += d.content;
8174
+ this.onDelta?.({ type: "text_delta", text: d.content });
8175
+ }
8176
+ const reasoning = d.reasoning_content ?? d.reasoning;
8177
+ if (typeof reasoning === "string" && reasoning !== "") {
8178
+ this.markFirstDelta();
8179
+ this.reasoning += reasoning;
8180
+ this.onDelta?.({ type: "thinking_delta", text: reasoning });
8181
+ }
8182
+ for (const tc of d.tool_calls ?? []) {
8183
+ this.markFirstDelta();
8184
+ const slot = tc.index ?? (tc.id ? this.toolCalls.length : Math.max(this.toolCalls.length - 1, 0));
8185
+ let call = this.toolCalls[slot];
8186
+ if (!call) {
8187
+ call = { id: tc.id ?? "", type: "function", function: { name: tc.function?.name ?? "", arguments: "" } };
8188
+ this.toolCalls[slot] = call;
8189
+ }
8190
+ if (tc.function?.name && !call.function.name)
8191
+ call.function.name = tc.function.name;
8192
+ call.function.arguments += tc.function?.arguments ?? "";
8193
+ }
8194
+ }
8195
+ /** Build the agent-facing result. Call once, after the stream has ended. */
8196
+ finish() {
8197
+ return {
8198
+ message: {
8199
+ ...this.content ? { content: this.content } : {},
8200
+ ...this.reasoning ? { reasoning_content: this.reasoning } : {},
8201
+ ...this.toolCalls.length > 0 ? { tool_calls: this.toolCalls.filter(Boolean) } : {}
8202
+ },
8203
+ finishReason: this.finishReason,
8204
+ usage: this.usage,
8205
+ ...this.firstDeltaAt !== void 0 ? { ttftMs: this.firstDeltaAt - this.startTime } : {}
8206
+ };
8207
+ }
8208
+ markFirstDelta() {
8209
+ if (this.firstDeltaAt === void 0)
8210
+ this.firstDeltaAt = Date.now();
8211
+ }
8212
+ };
8213
+ }
8214
+ });
8215
+
7831
8216
  // ../server/dist/agents/deepseek-agent.js
7832
8217
  function safeParse(json) {
7833
8218
  try {
@@ -7842,13 +8227,14 @@ var init_deepseek_agent = __esm({
7842
8227
  "use strict";
7843
8228
  init_config();
7844
8229
  init_agent_loop();
8230
+ init_openai_chat_stream();
7845
8231
  DeepSeekAgent = class extends AgentLoop {
7846
8232
  config;
7847
8233
  constructor(config2) {
7848
8234
  super(config2.tools);
7849
8235
  this.config = config2;
7850
8236
  }
7851
- async callModel(signal) {
8237
+ async callModel(signal, onDelta) {
7852
8238
  const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
7853
8239
  const startTime = Date.now();
7854
8240
  const messages2 = this.buildMessages();
@@ -7856,7 +8242,6 @@ var init_deepseek_agent = __esm({
7856
8242
  const body = {
7857
8243
  model: this.config.modelId,
7858
8244
  messages: messages2,
7859
- stream: false,
7860
8245
  max_completion_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
7861
8246
  ...tools.length > 0 ? { tools } : {}
7862
8247
  };
@@ -7865,24 +8250,14 @@ var init_deepseek_agent = __esm({
7865
8250
  } else {
7866
8251
  body.thinking = { type: "enabled" };
7867
8252
  }
7868
- const response = await fetch(url, {
7869
- method: "POST",
7870
- headers: {
7871
- "Content-Type": "application/json",
7872
- "Authorization": `Bearer ${this.config.apiKey}`
7873
- },
7874
- body: JSON.stringify(body),
7875
- signal
8253
+ const { message: msg, finishReason, usage: rawUsage, ttftMs } = await fetchChatCompletionStream({
8254
+ url,
8255
+ headers: { "Authorization": `Bearer ${this.config.apiKey}` },
8256
+ body,
8257
+ signal,
8258
+ onDelta,
8259
+ tag: "DeepSeekAgent"
7876
8260
  });
7877
- if (!response.ok) {
7878
- const errText2 = await response.text().catch(() => "");
7879
- throw new Error(`[DeepSeekAgent] API error ${response.status}: ${errText2}`);
7880
- }
7881
- const data = await response.json();
7882
- const choices = data.choices;
7883
- const choice = choices?.[0];
7884
- const msg = choice?.message;
7885
- const finishReason = choice?.finish_reason;
7886
8261
  let text6 = "";
7887
8262
  let thinking = "";
7888
8263
  const toolCalls = [];
@@ -7914,7 +8289,7 @@ var init_deepseek_agent = __esm({
7914
8289
  assistantBlocks.push({ type: "text", text: text6 });
7915
8290
  }
7916
8291
  const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
7917
- const usage = data.usage;
8292
+ const usage = rawUsage;
7918
8293
  const inputTokens = usage?.prompt_tokens ?? 0;
7919
8294
  const outputTokens = usage?.completion_tokens ?? 0;
7920
8295
  const cacheHitTokens = usage?.prompt_cache_hit_tokens ?? 0;
@@ -7930,7 +8305,8 @@ var init_deepseek_agent = __esm({
7930
8305
  totalTokens: inputTokens - cacheHitTokens + outputTokens,
7931
8306
  ...cacheHitTokens ? { cacheReadInputTokens: cacheHitTokens } : {}
7932
8307
  },
7933
- durationMs: Date.now() - startTime
8308
+ durationMs: Date.now() - startTime,
8309
+ ttftMs
7934
8310
  };
7935
8311
  }
7936
8312
  buildMessages() {
@@ -8047,13 +8423,14 @@ var init_kimi_agent = __esm({
8047
8423
  "use strict";
8048
8424
  init_config();
8049
8425
  init_agent_loop();
8426
+ init_openai_chat_stream();
8050
8427
  KimiAgent = class extends AgentLoop {
8051
8428
  config;
8052
8429
  constructor(config2) {
8053
8430
  super(config2.tools);
8054
8431
  this.config = config2;
8055
8432
  }
8056
- async callModel(signal) {
8433
+ async callModel(signal, onDelta) {
8057
8434
  const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
8058
8435
  const startTime = Date.now();
8059
8436
  const messages2 = this.buildMessages();
@@ -8061,7 +8438,6 @@ var init_kimi_agent = __esm({
8061
8438
  const body = {
8062
8439
  model: this.config.modelId,
8063
8440
  messages: messages2,
8064
- stream: false,
8065
8441
  max_completion_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
8066
8442
  ...tools.length > 0 ? { tools } : {},
8067
8443
  prompt_cache_key: this.config.cacheKey ?? void 0
@@ -8075,24 +8451,14 @@ var init_kimi_agent = __esm({
8075
8451
  body.thinking = { type: "disabled" };
8076
8452
  }
8077
8453
  }
8078
- const response = await fetch(url, {
8079
- method: "POST",
8080
- headers: {
8081
- "Content-Type": "application/json",
8082
- "Authorization": `Bearer ${this.config.apiKey}`
8083
- },
8084
- body: JSON.stringify(body),
8085
- signal
8454
+ const { message: msg, finishReason, usage: rawUsage, ttftMs } = await fetchChatCompletionStream({
8455
+ url,
8456
+ headers: { "Authorization": `Bearer ${this.config.apiKey}` },
8457
+ body,
8458
+ signal,
8459
+ onDelta,
8460
+ tag: "KimiAgent"
8086
8461
  });
8087
- if (!response.ok) {
8088
- const errText2 = await response.text().catch(() => "");
8089
- throw new Error(`[KimiAgent] API error ${response.status}: ${errText2}`);
8090
- }
8091
- const data = await response.json();
8092
- const choices = data.choices;
8093
- const choice = choices?.[0];
8094
- const msg = choice?.message;
8095
- const finishReason = choice?.finish_reason;
8096
8462
  let text6 = "";
8097
8463
  let thinking = "";
8098
8464
  const toolCalls = [];
@@ -8124,7 +8490,7 @@ var init_kimi_agent = __esm({
8124
8490
  assistantBlocks.push({ type: "text", text: text6 });
8125
8491
  }
8126
8492
  const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
8127
- const usage = data.usage;
8493
+ const usage = rawUsage;
8128
8494
  const inputTokens = usage?.prompt_tokens ?? 0;
8129
8495
  const outputTokens = usage?.completion_tokens ?? 0;
8130
8496
  const cachedTokens = usage?.cached_tokens ?? 0;
@@ -8140,7 +8506,8 @@ var init_kimi_agent = __esm({
8140
8506
  totalTokens: inputTokens - cachedTokens + outputTokens,
8141
8507
  ...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
8142
8508
  },
8143
- durationMs: Date.now() - startTime
8509
+ durationMs: Date.now() - startTime,
8510
+ ttftMs
8144
8511
  };
8145
8512
  }
8146
8513
  buildMessages() {
@@ -8260,72 +8627,25 @@ var init_minimax_agent = __esm({
8260
8627
  "use strict";
8261
8628
  init_config();
8262
8629
  init_agent_loop();
8630
+ init_anthropic_stream();
8263
8631
  MiniMaxAgent = class extends AgentLoop {
8264
8632
  config;
8265
8633
  constructor(config2) {
8266
8634
  super(config2.tools);
8267
8635
  this.config = config2;
8268
8636
  }
8269
- async callModel(signal) {
8270
- const url = `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`;
8271
- const body = this.buildRequestBody();
8272
- const startTime = Date.now();
8273
- const res = await fetch(url, {
8274
- method: "POST",
8275
- signal,
8637
+ async callModel(signal, onDelta) {
8638
+ return fetchAnthropicStream({
8639
+ url: `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`,
8276
8640
  headers: {
8277
8641
  "x-api-key": this.config.apiKey,
8278
- "anthropic-version": "2023-06-01",
8279
- "content-type": "application/json"
8642
+ "anthropic-version": "2023-06-01"
8280
8643
  },
8281
- body: JSON.stringify(body)
8644
+ body: this.buildRequestBody(),
8645
+ signal,
8646
+ onDelta,
8647
+ tag: "minimax"
8282
8648
  });
8283
- const durationMs = Date.now() - startTime;
8284
- const raw = await res.text();
8285
- let msg;
8286
- try {
8287
- msg = JSON.parse(raw);
8288
- } catch {
8289
- throw new Error(`[minimax] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
8290
- }
8291
- if (!res.ok || msg.error) {
8292
- throw new Error(`[minimax] ${res.status} ${msg.error?.type ?? "?"}: ${msg.error?.message ?? raw.slice(0, 200)}`);
8293
- }
8294
- let text6 = "";
8295
- let thinking = "";
8296
- const toolCalls = [];
8297
- const assistantBlocks = [];
8298
- for (const block of msg.content ?? []) {
8299
- if (block.type === "text" && block.text) {
8300
- text6 += block.text;
8301
- assistantBlocks.push({ type: "text", text: block.text });
8302
- } else if (block.type === "thinking" && block.thinking) {
8303
- thinking += block.thinking;
8304
- } else if (block.type === "tool_use") {
8305
- toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
8306
- assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
8307
- }
8308
- }
8309
- const u = msg.usage;
8310
- const inputTokens = u?.input_tokens ?? 0;
8311
- const outputTokens = u?.output_tokens ?? 0;
8312
- const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
8313
- const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
8314
- return {
8315
- assistantBlocks,
8316
- stopReason: msg.stop_reason ?? "end_turn",
8317
- text: text6,
8318
- thinking,
8319
- toolCalls,
8320
- usage: {
8321
- inputTokens,
8322
- outputTokens,
8323
- totalTokens: inputTokens + outputTokens,
8324
- ...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
8325
- ...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
8326
- },
8327
- durationMs
8328
- };
8329
8649
  }
8330
8650
  buildRequestBody() {
8331
8651
  const caching = this.config.promptCaching;
@@ -8399,71 +8719,22 @@ var init_qwen_agent = __esm({
8399
8719
  "use strict";
8400
8720
  init_config();
8401
8721
  init_agent_loop();
8722
+ init_anthropic_stream();
8402
8723
  QwenAgent = class extends AgentLoop {
8403
8724
  config;
8404
8725
  constructor(config2) {
8405
8726
  super(config2.tools);
8406
8727
  this.config = config2;
8407
8728
  }
8408
- async callModel(signal) {
8409
- const url = `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`;
8410
- const body = this.buildRequestBody();
8411
- const startTime = Date.now();
8412
- const res = await fetch(url, {
8413
- method: "POST",
8729
+ async callModel(signal, onDelta) {
8730
+ return fetchAnthropicStream({
8731
+ url: `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`,
8732
+ headers: { "x-api-key": this.config.apiKey },
8733
+ body: this.buildRequestBody(),
8414
8734
  signal,
8415
- headers: {
8416
- "x-api-key": this.config.apiKey,
8417
- "content-type": "application/json"
8418
- },
8419
- body: JSON.stringify(body)
8735
+ onDelta,
8736
+ tag: "qwen"
8420
8737
  });
8421
- const durationMs = Date.now() - startTime;
8422
- const raw = await res.text();
8423
- let msg;
8424
- try {
8425
- msg = JSON.parse(raw);
8426
- } catch {
8427
- throw new Error(`[qwen] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
8428
- }
8429
- if (!res.ok || msg.error) {
8430
- throw new Error(`[qwen] ${res.status} ${msg.error?.type ?? "?"}: ${msg.error?.message ?? raw.slice(0, 200)}`);
8431
- }
8432
- let text6 = "";
8433
- let thinking = "";
8434
- const toolCalls = [];
8435
- const assistantBlocks = [];
8436
- for (const block of msg.content ?? []) {
8437
- if (block.type === "text" && block.text) {
8438
- text6 += block.text;
8439
- assistantBlocks.push({ type: "text", text: block.text });
8440
- } else if (block.type === "thinking" && block.thinking) {
8441
- thinking += block.thinking;
8442
- } else if (block.type === "tool_use") {
8443
- toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
8444
- assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
8445
- }
8446
- }
8447
- const u = msg.usage;
8448
- const inputTokens = u?.input_tokens ?? 0;
8449
- const outputTokens = u?.output_tokens ?? 0;
8450
- const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
8451
- const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
8452
- return {
8453
- assistantBlocks,
8454
- stopReason: msg.stop_reason ?? "end_turn",
8455
- text: text6,
8456
- thinking,
8457
- toolCalls,
8458
- usage: {
8459
- inputTokens,
8460
- outputTokens,
8461
- totalTokens: inputTokens + outputTokens,
8462
- ...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
8463
- ...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
8464
- },
8465
- durationMs
8466
- };
8467
8738
  }
8468
8739
  buildRequestBody() {
8469
8740
  const cacheControl = this.config.promptCaching ? { type: "ephemeral" } : null;
@@ -8527,13 +8798,14 @@ var init_hunyuan_agent = __esm({
8527
8798
  "use strict";
8528
8799
  init_config();
8529
8800
  init_agent_loop();
8801
+ init_openai_chat_stream();
8530
8802
  HunyuanAgent = class extends AgentLoop {
8531
8803
  config;
8532
8804
  constructor(config2) {
8533
8805
  super(config2.tools);
8534
8806
  this.config = config2;
8535
8807
  }
8536
- async callModel(signal) {
8808
+ async callModel(signal, onDelta) {
8537
8809
  const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
8538
8810
  const startTime = Date.now();
8539
8811
  const messages2 = this.buildMessages();
@@ -8541,31 +8813,20 @@ var init_hunyuan_agent = __esm({
8541
8813
  const body = {
8542
8814
  model: this.config.modelId,
8543
8815
  messages: messages2,
8544
- stream: false,
8545
8816
  max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
8546
8817
  ...tools.length > 0 ? { tools } : {}
8547
8818
  };
8548
8819
  if (this.config.thinking?.enabled && this.config.thinking.effort) {
8549
8820
  body.reasoning_effort = this.config.thinking.effort;
8550
8821
  }
8551
- const response = await fetch(url, {
8552
- method: "POST",
8553
- headers: {
8554
- "Content-Type": "application/json",
8555
- "Authorization": `Bearer ${this.config.apiKey}`
8556
- },
8557
- body: JSON.stringify(body),
8558
- signal
8822
+ const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
8823
+ url,
8824
+ headers: { "Authorization": `Bearer ${this.config.apiKey}` },
8825
+ body,
8826
+ signal,
8827
+ onDelta,
8828
+ tag: "HunyuanAgent"
8559
8829
  });
8560
- if (!response.ok) {
8561
- const errText2 = await response.text().catch(() => "");
8562
- throw new Error(`[HunyuanAgent] API error ${response.status}: ${errText2}`);
8563
- }
8564
- const data = await response.json();
8565
- const choices = data.choices;
8566
- const choice = choices?.[0];
8567
- const msg = choice?.message;
8568
- const finishReason = choice?.finish_reason;
8569
8830
  let text6 = "";
8570
8831
  let thinking = "";
8571
8832
  const toolCalls = [];
@@ -8597,7 +8858,6 @@ var init_hunyuan_agent = __esm({
8597
8858
  assistantBlocks.push({ type: "text", text: text6 });
8598
8859
  }
8599
8860
  const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
8600
- const usage = data.usage;
8601
8861
  const promptTokens = usage?.prompt_tokens ?? 0;
8602
8862
  const completionTokens = usage?.completion_tokens ?? 0;
8603
8863
  const promptDetails = usage?.prompt_tokens_details;
@@ -8614,7 +8874,8 @@ var init_hunyuan_agent = __esm({
8614
8874
  totalTokens: promptTokens - cachedTokens + completionTokens,
8615
8875
  ...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
8616
8876
  },
8617
- durationMs: Date.now() - startTime
8877
+ durationMs: Date.now() - startTime,
8878
+ ttftMs
8618
8879
  };
8619
8880
  }
8620
8881
  buildMessages() {
@@ -8708,13 +8969,14 @@ var init_doubao_agent = __esm({
8708
8969
  "use strict";
8709
8970
  init_config();
8710
8971
  init_agent_loop();
8972
+ init_openai_chat_stream();
8711
8973
  DoubaoAgent = class extends AgentLoop {
8712
8974
  config;
8713
8975
  constructor(config2) {
8714
8976
  super(config2.tools);
8715
8977
  this.config = config2;
8716
8978
  }
8717
- async callModel(signal) {
8979
+ async callModel(signal, onDelta) {
8718
8980
  const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
8719
8981
  const startTime = Date.now();
8720
8982
  const messages2 = this.buildMessages();
@@ -8722,31 +8984,20 @@ var init_doubao_agent = __esm({
8722
8984
  const body = {
8723
8985
  model: this.config.modelId,
8724
8986
  messages: messages2,
8725
- stream: false,
8726
8987
  max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
8727
8988
  ...tools.length > 0 ? { tools } : {}
8728
8989
  };
8729
8990
  if (this.config.thinking) {
8730
8991
  body.thinking = { type: this.config.thinking.enabled ? "enabled" : "disabled" };
8731
8992
  }
8732
- const response = await fetch(url, {
8733
- method: "POST",
8734
- headers: {
8735
- "Content-Type": "application/json",
8736
- "Authorization": `Bearer ${this.config.apiKey}`
8737
- },
8738
- body: JSON.stringify(body),
8739
- signal
8993
+ const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
8994
+ url,
8995
+ headers: { "Authorization": `Bearer ${this.config.apiKey}` },
8996
+ body,
8997
+ signal,
8998
+ onDelta,
8999
+ tag: "DoubaoAgent"
8740
9000
  });
8741
- if (!response.ok) {
8742
- const errText2 = await response.text().catch(() => "");
8743
- throw new Error(`[DoubaoAgent] API error ${response.status}: ${errText2}`);
8744
- }
8745
- const data = await response.json();
8746
- const choices = data.choices;
8747
- const choice = choices?.[0];
8748
- const msg = choice?.message;
8749
- const finishReason = choice?.finish_reason;
8750
9001
  let text6 = "";
8751
9002
  let thinking = "";
8752
9003
  const toolCalls = [];
@@ -8778,7 +9029,6 @@ var init_doubao_agent = __esm({
8778
9029
  assistantBlocks.push({ type: "text", text: text6 });
8779
9030
  }
8780
9031
  const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
8781
- const usage = data.usage;
8782
9032
  const promptTokens = usage?.prompt_tokens ?? 0;
8783
9033
  const completionTokens = usage?.completion_tokens ?? 0;
8784
9034
  const promptDetails = usage?.prompt_tokens_details;
@@ -8795,7 +9045,8 @@ var init_doubao_agent = __esm({
8795
9045
  totalTokens: promptTokens - cachedTokens + completionTokens,
8796
9046
  ...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
8797
9047
  },
8798
- durationMs: Date.now() - startTime
9048
+ durationMs: Date.now() - startTime,
9049
+ ttftMs
8799
9050
  };
8800
9051
  }
8801
9052
  buildMessages() {
@@ -8889,6 +9140,7 @@ var init_zhipu_agent = __esm({
8889
9140
  "use strict";
8890
9141
  init_config();
8891
9142
  init_agent_loop();
9143
+ init_openai_chat_stream();
8892
9144
  EFFORTS = /* @__PURE__ */ new Set(["low", "high", "max"]);
8893
9145
  ZhipuAgent = class extends AgentLoop {
8894
9146
  config;
@@ -8896,7 +9148,7 @@ var init_zhipu_agent = __esm({
8896
9148
  super(config2.tools);
8897
9149
  this.config = config2;
8898
9150
  }
8899
- async callModel(signal) {
9151
+ async callModel(signal, onDelta) {
8900
9152
  const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
8901
9153
  const startTime = Date.now();
8902
9154
  const messages2 = this.buildMessages();
@@ -8904,7 +9156,6 @@ var init_zhipu_agent = __esm({
8904
9156
  const body = {
8905
9157
  model: this.config.modelId,
8906
9158
  messages: messages2,
8907
- stream: false,
8908
9159
  max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
8909
9160
  ...tools.length > 0 ? { tools } : {}
8910
9161
  };
@@ -8920,24 +9171,14 @@ var init_zhipu_agent = __esm({
8920
9171
  } else {
8921
9172
  body.thinking = { type: "disabled" };
8922
9173
  }
8923
- const response = await fetch(url, {
8924
- method: "POST",
8925
- headers: {
8926
- "Content-Type": "application/json",
8927
- "Authorization": `Bearer ${this.config.apiKey}`
8928
- },
8929
- body: JSON.stringify(body),
8930
- signal
9174
+ const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
9175
+ url,
9176
+ headers: { "Authorization": `Bearer ${this.config.apiKey}` },
9177
+ body,
9178
+ signal,
9179
+ onDelta,
9180
+ tag: "ZhipuAgent"
8931
9181
  });
8932
- if (!response.ok) {
8933
- const errText2 = await response.text().catch(() => "");
8934
- throw new Error(`[ZhipuAgent] API error ${response.status}: ${errText2}`);
8935
- }
8936
- const data = await response.json();
8937
- const choices = data.choices;
8938
- const choice = choices?.[0];
8939
- const msg = choice?.message;
8940
- const finishReason = choice?.finish_reason;
8941
9182
  let text6 = "";
8942
9183
  let thinking = "";
8943
9184
  const toolCalls = [];
@@ -8969,7 +9210,6 @@ var init_zhipu_agent = __esm({
8969
9210
  assistantBlocks.push({ type: "text", text: text6 });
8970
9211
  }
8971
9212
  const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
8972
- const usage = data.usage;
8973
9213
  const promptTokens = usage?.prompt_tokens ?? 0;
8974
9214
  const outputTokens = usage?.completion_tokens ?? 0;
8975
9215
  const details = usage?.prompt_tokens_details;
@@ -8987,7 +9227,8 @@ var init_zhipu_agent = __esm({
8987
9227
  totalTokens: inputTokens + outputTokens,
8988
9228
  ...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
8989
9229
  },
8990
- durationMs: Date.now() - startTime
9230
+ durationMs: Date.now() - startTime,
9231
+ ttftMs
8991
9232
  };
8992
9233
  }
8993
9234
  buildMessages() {
@@ -9100,13 +9341,14 @@ var init_openai_agent = __esm({
9100
9341
  "use strict";
9101
9342
  init_config();
9102
9343
  init_agent_loop();
9344
+ init_openai_chat_stream();
9103
9345
  OpenAIAgent = class extends AgentLoop {
9104
9346
  config;
9105
9347
  constructor(config2) {
9106
9348
  super(config2.tools);
9107
9349
  this.config = config2;
9108
9350
  }
9109
- async callModel(signal) {
9351
+ async callModel(signal, onDelta) {
9110
9352
  const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
9111
9353
  const startTime = Date.now();
9112
9354
  const messages2 = this.buildMessages();
@@ -9114,37 +9356,26 @@ var init_openai_agent = __esm({
9114
9356
  const body = {
9115
9357
  model: this.config.modelId,
9116
9358
  messages: messages2,
9117
- stream: false,
9118
9359
  max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
9119
9360
  ...tools.length > 0 ? { tools } : {}
9120
9361
  };
9121
9362
  if (this.config.thinking?.enabled && this.config.thinking.effort) {
9122
9363
  body.reasoning_effort = this.config.thinking.effort;
9123
9364
  }
9124
- const response = await fetch(url, {
9125
- method: "POST",
9126
- headers: {
9127
- "Content-Type": "application/json",
9128
- "Authorization": `Bearer ${this.config.apiKey}`
9129
- },
9130
- body: JSON.stringify(body),
9131
- signal
9365
+ const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
9366
+ url,
9367
+ headers: { "Authorization": `Bearer ${this.config.apiKey}` },
9368
+ body,
9369
+ signal,
9370
+ onDelta,
9371
+ tag: "OpenAIAgent"
9132
9372
  });
9133
- if (!response.ok) {
9134
- const errText2 = await response.text().catch(() => "");
9135
- throw new Error(`[OpenAIAgent] API error ${response.status}: ${errText2}`);
9136
- }
9137
- const data = await response.json();
9138
- const choices = data.choices;
9139
- const choice = choices?.[0];
9140
- const msg = choice?.message;
9141
- const finishReason = choice?.finish_reason;
9142
9373
  let text6 = "";
9143
9374
  let thinking = "";
9144
9375
  const toolCalls = [];
9145
9376
  const assistantBlocks = [];
9146
9377
  if (msg) {
9147
- const reasoning = msg.reasoning_content ?? msg.reasoning;
9378
+ const reasoning = msg.reasoning_content;
9148
9379
  if (reasoning && typeof reasoning === "string") {
9149
9380
  thinking = reasoning;
9150
9381
  }
@@ -9171,7 +9402,6 @@ var init_openai_agent = __esm({
9171
9402
  assistantBlocks.push({ type: "text", text: text6 });
9172
9403
  }
9173
9404
  const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
9174
- const usage = data.usage;
9175
9405
  const promptTokens = usage?.prompt_tokens ?? 0;
9176
9406
  const completionTokens = usage?.completion_tokens ?? 0;
9177
9407
  const promptDetails = usage?.prompt_tokens_details;
@@ -9188,7 +9418,8 @@ var init_openai_agent = __esm({
9188
9418
  totalTokens: promptTokens - cachedTokens + completionTokens,
9189
9419
  ...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
9190
9420
  },
9191
- durationMs: Date.now() - startTime
9421
+ durationMs: Date.now() - startTime,
9422
+ ttftMs
9192
9423
  };
9193
9424
  }
9194
9425
  buildMessages() {
@@ -9282,6 +9513,46 @@ var init_openai_agent = __esm({
9282
9513
  import { SignatureV4 } from "@smithy/signature-v4";
9283
9514
  import { Sha256 } from "@aws-crypto/sha256-js";
9284
9515
  import { defaultProvider } from "@aws-sdk/credential-provider-node";
9516
+ async function readResponsesStream(res, onDelta, startTime, signal) {
9517
+ if (!res.body)
9518
+ throw new Error("[MantleAgent] empty response body");
9519
+ let final;
9520
+ let firstDeltaAt;
9521
+ for await (const ev of readSseJson(res.body)) {
9522
+ switch (ev.type) {
9523
+ case "response.output_text.delta":
9524
+ firstDeltaAt ??= Date.now();
9525
+ if (ev.delta)
9526
+ onDelta?.({ type: "text_delta", text: ev.delta });
9527
+ break;
9528
+ case "response.reasoning_summary_text.delta":
9529
+ firstDeltaAt ??= Date.now();
9530
+ if (ev.delta)
9531
+ onDelta?.({ type: "thinking_delta", text: ev.delta });
9532
+ break;
9533
+ case "response.function_call_arguments.delta":
9534
+ firstDeltaAt ??= Date.now();
9535
+ break;
9536
+ case "response.completed":
9537
+ case "response.incomplete":
9538
+ final = ev.response;
9539
+ break;
9540
+ // No HTTP status exists mid-stream (the 200 already went out), so
9541
+ // classifyModelError falls back to its keyword checks on these messages.
9542
+ case "response.failed": {
9543
+ const failed = ev.response;
9544
+ throw new Error(`[MantleAgent] API error in stream: ${JSON.stringify(failed?.error ?? ev.response).slice(0, 300)}`);
9545
+ }
9546
+ case "error":
9547
+ throw new Error(`[MantleAgent] API error in stream: ${ev.code ?? "?"}: ${ev.message ?? ""}`);
9548
+ }
9549
+ }
9550
+ if (signal?.aborted)
9551
+ throw new DOMException("Model call aborted", "AbortError");
9552
+ if (!final)
9553
+ throw new Error("MantleEmptyResponse: stream ended without response.completed; transient, retrying");
9554
+ return { data: final, ttftMs: firstDeltaAt !== void 0 ? firstDeltaAt - startTime : void 0 };
9555
+ }
9285
9556
  function extractRegionFromHost(hostname) {
9286
9557
  const m = hostname.match(/^bedrock-(?:mantle|runtime)\.([a-z0-9-]+)\./);
9287
9558
  return m?.[1] ?? "us-east-1";
@@ -9299,13 +9570,14 @@ var init_mantle_agent = __esm({
9299
9570
  "use strict";
9300
9571
  init_config();
9301
9572
  init_agent_loop();
9573
+ init_sse();
9302
9574
  MantleAgent = class extends AgentLoop {
9303
9575
  config;
9304
9576
  constructor(config2) {
9305
9577
  super(config2.tools);
9306
9578
  this.config = config2;
9307
9579
  }
9308
- async callModel(signal) {
9580
+ async callModel(signal, onDelta) {
9309
9581
  const url = this.config.endpoint.replace(/\/+$/, "") + "/responses";
9310
9582
  const startTime = Date.now();
9311
9583
  const input = this.buildInput();
@@ -9313,7 +9585,7 @@ var init_mantle_agent = __esm({
9313
9585
  const body = {
9314
9586
  model: this.config.modelId,
9315
9587
  input,
9316
- stream: false,
9588
+ stream: true,
9317
9589
  max_output_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
9318
9590
  text: { verbosity: this.config.verbosity ?? "low" },
9319
9591
  ...tools.length > 0 ? { tools } : {}
@@ -9334,7 +9606,7 @@ var init_mantle_agent = __esm({
9334
9606
  const errText2 = await response.text().catch(() => "");
9335
9607
  throw new Error(`[MantleAgent] API error ${response.status}: ${errText2}`);
9336
9608
  }
9337
- const data = await response.json();
9609
+ const { data, ttftMs } = await readResponsesStream(response, onDelta, startTime, signal);
9338
9610
  const output = data.output ?? [];
9339
9611
  let text6 = "";
9340
9612
  let thinking = "";
@@ -9396,7 +9668,8 @@ var init_mantle_agent = __esm({
9396
9668
  totalTokens: inputTokens - cachedTokens + outputTokens,
9397
9669
  ...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
9398
9670
  },
9399
- durationMs: Date.now() - startTime
9671
+ durationMs: Date.now() - startTime,
9672
+ ttftMs
9400
9673
  };
9401
9674
  }
9402
9675
  /** Build the Responses API `input` array from system prompt + message log. */
@@ -9523,73 +9796,25 @@ var init_anthropic_agent = __esm({
9523
9796
  "use strict";
9524
9797
  init_config();
9525
9798
  init_agent_loop();
9799
+ init_anthropic_stream();
9526
9800
  AnthropicAgent = class extends AgentLoop {
9527
9801
  config;
9528
9802
  constructor(config2) {
9529
9803
  super(config2.tools);
9530
9804
  this.config = config2;
9531
9805
  }
9532
- async callModel(signal) {
9533
- const url = `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`;
9534
- const body = this.buildRequestBody();
9535
- const startTime = Date.now();
9536
- const res = await fetch(url, {
9537
- method: "POST",
9538
- signal,
9806
+ async callModel(signal, onDelta) {
9807
+ return fetchAnthropicStream({
9808
+ url: `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`,
9539
9809
  headers: {
9540
9810
  "x-api-key": this.config.apiKey,
9541
- "anthropic-version": "2023-06-01",
9542
- "content-type": "application/json"
9811
+ "anthropic-version": "2023-06-01"
9543
9812
  },
9544
- body: JSON.stringify(body)
9813
+ body: this.buildRequestBody(),
9814
+ signal,
9815
+ onDelta,
9816
+ tag: "anthropic"
9545
9817
  });
9546
- const durationMs = Date.now() - startTime;
9547
- const raw = await res.text();
9548
- let msg;
9549
- try {
9550
- msg = JSON.parse(raw);
9551
- } catch {
9552
- throw new Error(`[anthropic] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
9553
- }
9554
- if (!res.ok || msg.error) {
9555
- throw new Error(`[anthropic] ${res.status} ${msg.error?.type ?? "?"}: ${msg.error?.message ?? raw.slice(0, 200)}`);
9556
- }
9557
- let text6 = "";
9558
- let thinking = "";
9559
- const toolCalls = [];
9560
- const assistantBlocks = [];
9561
- for (const block of msg.content ?? []) {
9562
- if (block.type === "text" && block.text) {
9563
- text6 += block.text;
9564
- assistantBlocks.push({ type: "text", text: block.text });
9565
- } else if (block.type === "thinking" && block.thinking) {
9566
- thinking += block.thinking;
9567
- } else if (block.type === "tool_use") {
9568
- toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
9569
- assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
9570
- }
9571
- }
9572
- const u = msg.usage;
9573
- const inputTokens = u?.input_tokens ?? 0;
9574
- const outputTokens = u?.output_tokens ?? 0;
9575
- const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
9576
- const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
9577
- return {
9578
- assistantBlocks,
9579
- stopReason: msg.stop_reason ?? "end_turn",
9580
- ...msg.stop_details ? { stopDetails: { category: msg.stop_details.category ?? null, explanation: msg.stop_details.explanation ?? null } } : {},
9581
- text: text6,
9582
- thinking,
9583
- toolCalls,
9584
- usage: {
9585
- inputTokens,
9586
- outputTokens,
9587
- totalTokens: inputTokens + outputTokens,
9588
- ...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
9589
- ...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
9590
- },
9591
- durationMs
9592
- };
9593
9818
  }
9594
9819
  buildRequestBody() {
9595
9820
  const caching = this.config.promptCaching;
@@ -11404,55 +11629,6 @@ var init_workspace_tools = __esm({
11404
11629
  }
11405
11630
  });
11406
11631
 
11407
- // ../server/dist/tools/draft-tool.js
11408
- function createDraftTool() {
11409
- let drafts = 0;
11410
- const tool = {
11411
- name: "draft",
11412
- description: "Submit a draft answer for self-review. Returns a checklist that critiques the draft so you can improve it before sending the final answer.",
11413
- inputSchema: {
11414
- type: "object",
11415
- properties: {
11416
- content: {
11417
- type: "string",
11418
- description: "The complete draft answer. Required and must be non-empty."
11419
- }
11420
- },
11421
- required: ["content"]
11422
- },
11423
- callback: (input) => {
11424
- const content = input?.content;
11425
- if (typeof content !== "string" || content.trim().length === 0) {
11426
- return `${TOOL_ERROR_MARKER}
11427
- Error: draft requires non-empty \`content\` \u2014 put your full draft answer there.`;
11428
- }
11429
- drafts++;
11430
- if (drafts > MAX_DRAFTS) {
11431
- return `You have drafted ${MAX_DRAFTS} times this turn \u2014 that is the limit. Write your final answer now, directly, without calling draft again.`;
11432
- }
11433
- return REVIEW_CHECKLIST;
11434
- }
11435
- };
11436
- return { tool, reset: () => {
11437
- drafts = 0;
11438
- } };
11439
- }
11440
- var MAX_DRAFTS, REVIEW_CHECKLIST;
11441
- var init_draft_tool = __esm({
11442
- "../server/dist/tools/draft-tool.js"() {
11443
- "use strict";
11444
- init_agent_loop();
11445
- MAX_DRAFTS = 3;
11446
- REVIEW_CHECKLIST = `You are now a hostile reviewer whose job is to find everything wrong with the draft above. Do not defend it \u2014 attack it.
11447
-
11448
- 1. Facts: list every factual claim in the draft, each tagged [verified-with-tool] / [from-memory] / [guess]. For anything not [verified-with-tool], verify it NOW with file_read / grep / search, or mark it "unconfirmed" in the answer. Do not assert from memory.
11449
- 2. Delivery: is it friendly? Does it answer the question simply and directly, or is it dancing around? If it uses a rhetorical question, is that question actually necessary?
11450
- 3. Gaps: is anything missing? If it can be looked up, look it up; if it can't, say "unknown" \u2014 don't paper over it.
11451
-
11452
- If the draft clears all three, write the final answer now. If not, fix it (gather more via tools, or call draft again to rewrite).`;
11453
- }
11454
- });
11455
-
11456
11632
  // ../server/dist/prompts/system-prompts.js
11457
11633
  import fs23 from "node:fs/promises";
11458
11634
  import path27 from "node:path";
@@ -11553,7 +11729,6 @@ var init_session_agent_builder = __esm({
11553
11729
  "use strict";
11554
11730
  init_model_runtime();
11555
11731
  init_workspace_tools();
11556
- init_draft_tool();
11557
11732
  init_system_prompts();
11558
11733
  init_md_loader();
11559
11734
  init_config();
@@ -11576,7 +11751,7 @@ var init_session_agent_builder = __esm({
11576
11751
  const yamlConfig = await loadAgentYaml(agentId, this.host.workspaceRoot);
11577
11752
  const isRoot = !parentId;
11578
11753
  const { modelId, endpoint, providerId } = this.validateAgentModelConfig(agentId, yamlConfig);
11579
- const { workspaceTools, sessionTools, allowedNamespaces, draftReset } = this.resolveBaseToolSet({
11754
+ const { workspaceTools, sessionTools, allowedNamespaces } = this.resolveBaseToolSet({
11580
11755
  agentId,
11581
11756
  sessionId,
11582
11757
  modelId,
@@ -11615,7 +11790,7 @@ var init_session_agent_builder = __esm({
11615
11790
  systemPrompts,
11616
11791
  allToolNames
11617
11792
  });
11618
- return { agent, yamlConfig, contextConfig, modelId, systemPrompt, thinkingEffort, meta, draftReset };
11793
+ return { agent, yamlConfig, contextConfig, modelId, systemPrompt, thinkingEffort, meta };
11619
11794
  }
11620
11795
  /** Throw with a clear message if agent.yaml is missing the model triple.
11621
11796
  * Includes the resolved yaml path and distinguishes "file unreadable"
@@ -11659,16 +11834,10 @@ var init_session_agent_builder = __esm({
11659
11834
  const delegates = canDelegate(yamlConfig);
11660
11835
  const sessionTools = delegates ? this.host.createSessionTools(sessionId) : agentId === GOAL_AGENT_ID ? this.host.createGoalTools(sessionId) : [];
11661
11836
  sessionTools.push(this.host.createContinueTaskTool(sessionId));
11662
- let draftReset = null;
11663
- if (nameSet.has("draft")) {
11664
- const { tool, reset } = createDraftTool();
11665
- sessionTools.push(tool);
11666
- draftReset = reset;
11667
- }
11668
11837
  if (nameSet.has("relay_send") && accessLevel === null) {
11669
11838
  sessionTools.push(...this.host.createRelayTools(sessionId));
11670
11839
  }
11671
- return { workspaceTools, sessionTools, allowedNamespaces, draftReset };
11840
+ return { workspaceTools, sessionTools, allowedNamespaces };
11672
11841
  }
11673
11842
  /**
11674
11843
  * Compose the agent's system prompt from the MD layer cake +
@@ -12141,7 +12310,7 @@ async function initObservability() {
12141
12310
  return true;
12142
12311
  }
12143
12312
  const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
12144
- providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.4.6");
12313
+ providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.0-alpha");
12145
12314
  console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
12146
12315
  return true;
12147
12316
  }
@@ -12182,6 +12351,14 @@ function instruments() {
12182
12351
  return {
12183
12352
  tokenUsage: meter.createHistogram("gen_ai.client.token.usage", { unit: "{token}", description: "Tokens per model call, split by gen_ai.token.type" }),
12184
12353
  operationDuration: meter.createHistogram("gen_ai.client.operation.duration", { unit: "s", description: "Model call latency" }),
12354
+ // semconv's only TTFT metric (named `server.` though it's measured client-side
12355
+ // here: first streamed delta, see ModelCallResult.ttftMs). Buckets are the
12356
+ // spec's recommended set — the SDK default is ms-scale, useless for seconds.
12357
+ timeToFirstToken: meter.createHistogram("gen_ai.server.time_to_first_token", {
12358
+ unit: "s",
12359
+ description: "Time to first streamed token per model call",
12360
+ advice: { explicitBucketBoundaries: [1e-3, 5e-3, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] }
12361
+ }),
12185
12362
  toolDuration: meter.createHistogram("halo.tool.duration", { unit: "s", description: "Tool execution latency" }),
12186
12363
  turnDuration: meter.createHistogram("halo.turn.duration", { unit: "s", description: "Agent turn latency (invoke_agent)" }),
12187
12364
  modelRetries: meter.createCounter("halo.model.retries", { description: "Model-call retries by kind" })
@@ -12310,7 +12487,8 @@ function recordModelCall(session, turn, event) {
12310
12487
  "gen_ai.usage.input_tokens": event.usage?.inputTokens ?? 0,
12311
12488
  "gen_ai.usage.output_tokens": event.usage?.outputTokens ?? 0,
12312
12489
  "gen_ai.response.finish_reasons": [turn.cycleToolCalls.length > 0 ? "tool_use" : "end_turn"],
12313
- "session.id": session.id
12490
+ "session.id": session.id,
12491
+ ...event.ttftMs != null ? { "halo.ttft_ms": event.ttftMs } : {}
12314
12492
  }
12315
12493
  }, turn.ctx);
12316
12494
  if (captureContent()) {
@@ -12334,6 +12512,8 @@ function recordModelCall(session, turn, event) {
12334
12512
  m.tokenUsage.record(event.usage?.inputTokens ?? 0, { ...modelAttr, "gen_ai.token.type": "input" });
12335
12513
  m.tokenUsage.record(event.usage?.outputTokens ?? 0, { ...modelAttr, "gen_ai.token.type": "output" });
12336
12514
  m.operationDuration.record(durationMs / 1e3, { ...modelAttr, "gen_ai.operation.name": "chat" });
12515
+ if (event.ttftMs != null)
12516
+ m.timeToFirstToken.record(event.ttftMs / 1e3, modelAttr);
12337
12517
  }
12338
12518
  function recordToolCall(session, turn, event) {
12339
12519
  const id = event.toolUseId ?? "";
@@ -12428,6 +12608,15 @@ function simpleHash(str) {
12428
12608
  function sleep(ms) {
12429
12609
  return new Promise((resolve2) => setTimeout(resolve2, ms));
12430
12610
  }
12611
+ function formatAge(ms) {
12612
+ const s = Math.max(0, Math.floor(ms / 1e3));
12613
+ if (s < 60)
12614
+ return `${s}s`;
12615
+ const m = Math.floor(s / 60);
12616
+ if (m < 60)
12617
+ return `${m}m`;
12618
+ return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
12619
+ }
12431
12620
  function abortReason(reason) {
12432
12621
  return new DOMException(reason, "AbortError");
12433
12622
  }
@@ -12484,8 +12673,8 @@ function stripImageMarkers(s) {
12484
12673
  }
12485
12674
  function generateSessionId() {
12486
12675
  const ts = Date.now().toString(36);
12487
- const rand = Math.random().toString(36).slice(2, 8);
12488
- return `sid_${ts}_${rand}`;
12676
+ const rand2 = Math.random().toString(36).slice(2, 8);
12677
+ return `sid_${ts}_${rand2}`;
12489
12678
  }
12490
12679
  var CONTINUE_TASK_KICK, SessionManager;
12491
12680
  var init_session_manager = __esm({
@@ -12760,7 +12949,7 @@ var init_session_manager = __esm({
12760
12949
  }
12761
12950
  // ── Session lifecycle ───────────────────────────────────────────────
12762
12951
  /** Create default AgentSession fields */
12763
- createAgentSession(id, parentId, agentId, agentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort = "off", workingDir = null, accessLevel = null, meta, imageOverride, draftReset = null) {
12952
+ createAgentSession(id, parentId, agentId, agentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort = "off", workingDir = null, accessLevel = null, meta, imageOverride) {
12764
12953
  return {
12765
12954
  id,
12766
12955
  parentId,
@@ -12768,9 +12957,9 @@ var init_session_manager = __esm({
12768
12957
  agentName,
12769
12958
  agent,
12770
12959
  description,
12771
- draftReset,
12772
12960
  output: "",
12773
12961
  lastActivityAt: null,
12962
+ streamedThisCall: false,
12774
12963
  finalOutput: "",
12775
12964
  turnError: null,
12776
12965
  promise: null,
@@ -12834,14 +13023,14 @@ var init_session_manager = __esm({
12834
13023
  throw new Error(`Session ${sessionId} is archived and cannot be resumed`);
12835
13024
  const restoredAccessLevel = meta.accessLevel === "readonly" ? "readonly" : meta.accessLevel === "workspace" ? "workspace" : null;
12836
13025
  const restoredWorkingDir = meta.workingDir ? path30.resolve(this.workspaceRoot, meta.workingDir) : void 0;
12837
- const { agent, yamlConfig: resumedYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta: agentMeta, draftReset } = await this.buildAgentInstance(meta.agentId, sessionId, meta.parentId, restoredWorkingDir, restoredAccessLevel);
13026
+ const { agent, yamlConfig: resumedYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta: agentMeta } = await this.buildAgentInstance(meta.agentId, sessionId, meta.parentId, restoredWorkingDir, restoredAccessLevel);
12838
13027
  const savedMessages = this.loadAgentState(sessionId, meta.agentId);
12839
13028
  if (savedMessages.length > 0) {
12840
13029
  agent.messages = savedMessages;
12841
13030
  }
12842
13031
  const resumedImageOverride = resumedYaml?.model?.image;
12843
13032
  const resumedAgentName = meta.agentName ?? resumedYaml?.name ?? meta.agentId;
12844
- const session = this.createAgentSession(sessionId, meta.parentId, meta.agentId, resumedAgentName, agent, meta.description ?? "", contextConfig, modelId, systemPrompt, thinkingEffort, restoredWorkingDir ?? null, restoredAccessLevel, agentMeta, resumedImageOverride, draftReset);
13033
+ const session = this.createAgentSession(sessionId, meta.parentId, meta.agentId, resumedAgentName, agent, meta.description ?? "", contextConfig, modelId, systemPrompt, thinkingEffort, restoredWorkingDir ?? null, restoredAccessLevel, agentMeta, resumedImageOverride);
12845
13034
  this.sessions.set(sessionId, session);
12846
13035
  const rootId = this.findRootSessionId(sessionId);
12847
13036
  const state = this.uiStore.ensureUIState(rootId);
@@ -12889,10 +13078,12 @@ var init_session_manager = __esm({
12889
13078
  * @param explicitId - Optional explicit session ID (for unifying frontend sessionId with agent sessionId)
12890
13079
  */
12891
13080
  async createSession(agentId, parentId, description, agentName, explicitId, workingDir, accessLevel = null, title) {
13081
+ if (agentId === GOAL_AGENT_ID && !config.goalModeEnabled)
13082
+ throw new Error("Goal mode is disabled.");
12892
13083
  const segment = generateSessionId();
12893
13084
  const sessionId = explicitId ?? (parentId ? `${parentId}>${segment}` : segment);
12894
13085
  const now = Date.now();
12895
- const { agent, yamlConfig: createdYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta, draftReset } = await this.buildAgentInstance(agentId, sessionId, parentId, workingDir ?? void 0, accessLevel);
13086
+ const { agent, yamlConfig: createdYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta } = await this.buildAgentInstance(agentId, sessionId, parentId, workingDir ?? void 0, accessLevel);
12896
13087
  const resolvedAgentName = agentName ?? createdYaml?.name ?? agentId;
12897
13088
  this.db.insert(agentSessions).values({
12898
13089
  id: sessionId,
@@ -12915,7 +13106,7 @@ var init_session_manager = __esm({
12915
13106
  }
12916
13107
  }
12917
13108
  const createdImageOverride = createdYaml?.model?.image;
12918
- const session = this.createAgentSession(sessionId, parentId, agentId, resolvedAgentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort, workingDir ?? null, accessLevel, meta, createdImageOverride, draftReset);
13109
+ const session = this.createAgentSession(sessionId, parentId, agentId, resolvedAgentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort, workingDir ?? null, accessLevel, meta, createdImageOverride);
12919
13110
  this.sessions.set(sessionId, session);
12920
13111
  this.emitEvent(sessionId, { type: "context", agentId, agentName: resolvedAgentName, systemPrompt, taskId: parentId ? sessionId : void 0 });
12921
13112
  console.debug(`[SessionManager] Created session ${sessionId} for agent "${agentId}" (parent: ${parentId ?? "none"}, workingDir: ${workingDir ?? "project root"})`);
@@ -13157,16 +13348,28 @@ var init_session_manager = __esm({
13157
13348
  const agentId = session.agentId;
13158
13349
  const taskId = session.parentId ? session.id : void 0;
13159
13350
  switch (event.type) {
13351
+ // Streaming chunks: UI-only. They never touch output / finalOutput —
13352
+ // the whole `text` event below still carries the reply for those.
13353
+ case "text_delta": {
13354
+ session.streamedThisCall = true;
13355
+ this.emitEvent(session.id, { type: "stream_delta", text: event.text, agentName, agentId, taskId });
13356
+ break;
13357
+ }
13358
+ case "thinking_delta": {
13359
+ session.streamedThisCall = true;
13360
+ this.emitEvent(session.id, { type: "thinking_delta", text: event.text, agentName, agentId, taskId });
13361
+ break;
13362
+ }
13160
13363
  case "text": {
13161
13364
  session.output += event.text ?? "";
13162
13365
  session.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
13163
13366
  if (event.final)
13164
13367
  session.finalOutput += event.text ?? "";
13165
- this.emitEvent(session.id, { type: "stream", text: event.text, final: event.final, agentName, agentId, taskId });
13368
+ this.emitEvent(session.id, { type: "stream", text: event.text, final: event.final, streamed: session.streamedThisCall, agentName, agentId, taskId });
13166
13369
  break;
13167
13370
  }
13168
13371
  case "thinking": {
13169
- this.emitEvent(session.id, { type: "thinking", text: event.text, agentName, agentId, taskId });
13372
+ this.emitEvent(session.id, { type: "thinking", text: event.text, streamed: session.streamedThisCall, agentName, agentId, taskId });
13170
13373
  break;
13171
13374
  }
13172
13375
  case "tool_call": {
@@ -13199,9 +13402,11 @@ var init_session_manager = __esm({
13199
13402
  agentId,
13200
13403
  taskId,
13201
13404
  e2eMs: event.durationMs,
13405
+ ttftMs: event.ttftMs,
13202
13406
  thinkingEffort: session.thinkingEffort
13203
13407
  });
13204
13408
  session.lastContextTokens = (u.totalTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheWriteInputTokens ?? 0);
13409
+ session.streamedThisCall = false;
13205
13410
  break;
13206
13411
  }
13207
13412
  case "stop": {
@@ -13245,7 +13450,6 @@ var init_session_manager = __esm({
13245
13450
  session.abortController = new AbortController();
13246
13451
  const signal = session.abortController.signal;
13247
13452
  resultText = "";
13248
- session.draftReset?.();
13249
13453
  const last = session.agent.messages[session.agent.messages.length - 1];
13250
13454
  const input = attempt === 0 || last?.role !== "user" ? message : [];
13251
13455
  try {
@@ -13279,6 +13483,7 @@ var init_session_manager = __esm({
13279
13483
  break;
13280
13484
  } catch (err) {
13281
13485
  session.abortController = null;
13486
+ session.streamedThisCall = false;
13282
13487
  const { kind, msg, errName, httpStatus } = classifyModelError(err);
13283
13488
  if (signal.aborted || errName === "AbortError") {
13284
13489
  if (session.interruptRequested) {
@@ -13410,6 +13615,9 @@ var init_session_manager = __esm({
13410
13615
  session.selfKick = false;
13411
13616
  result = await this.runAgentTurn(session, message);
13412
13617
  console.debug(`[SessionManager] runSession ${sessionId} first turn done \u2014 result: ${result.slice(0, 150)}`);
13618
+ if (session.messageQueue.length > 0 && session.finalOutput !== "" && !session.turnError) {
13619
+ this.sendOpeningInterim(session);
13620
+ }
13413
13621
  }
13414
13622
  if (session.messageQueue.length > 0) {
13415
13623
  await this.drainQueue(session);
@@ -13508,11 +13716,17 @@ ${result}`;
13508
13716
  * wrap-up (a fresh task) rather than being a leftover from the original batch.
13509
13717
  *
13510
13718
  * Mid-tier parents are intentionally excluded (parentId !== null): their
13511
- * tryReportToParent bubble-up already gates them on a fully-drained subtree. */
13719
+ * tryReportToParent bubble-up already gates them on a fully-drained subtree.
13720
+ *
13721
+ * Returns two renderings of the same rows: `llm` (appended to the model
13722
+ * input, ISO stamps for a clock-less model) and `ui` (the `system` event the
13723
+ * admin shows — the message carries its own `timestamp`, so the header has no
13724
+ * clock and per-child times are relative ages, not raw UTC ISO). */
13512
13725
  siblingStatusSuffix(session) {
13513
13726
  if (session.parentId !== null)
13514
- return "";
13515
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
13727
+ return { llm: "", ui: "" };
13728
+ const now = Date.now();
13729
+ const nowIso = new Date(now).toISOString();
13516
13730
  const stillRunning = this.db.select({
13517
13731
  agentName: agentSessions.agentName,
13518
13732
  description: agentSessions.description,
@@ -13520,16 +13734,19 @@ ${result}`;
13520
13734
  updatedAt: agentSessions.updatedAt
13521
13735
  }).from(agentSessions).where(and6(eq9(agentSessions.parentId, session.id), isNull4(agentSessions.stoppedAt), isNull4(agentSessions.archivedAt))).all();
13522
13736
  if (stillRunning.length === 0 && session.messageQueue.length === 0) {
13523
- return `
13737
+ const done = "All sub-agents you dispatched have completed. This is the final report \u2014 you may now consolidate and wrap up.";
13738
+ return { llm: `
13524
13739
 
13525
- [System @ ${nowIso}] All sub-agents you dispatched have completed. This is the final report \u2014 you may now consolidate and wrap up.`;
13740
+ [System @ ${nowIso}] ${done}`, ui: `[System] ${done}` };
13526
13741
  }
13527
- const list = stillRunning.map((c) => ` - ${c.agentName} (created ${new Date(c.createdAt).toISOString()}, last active ${new Date(c.updatedAt).toISOString()}): ${c.description.slice(0, 80)}`).join("\n");
13528
- return `
13529
-
13530
- [System @ ${nowIso}] Do NOT wrap up yet \u2014 ${stillRunning.length} sub-agent(s) still running, ${session.messageQueue.length} report(s) still queued.
13742
+ const head = `Do NOT wrap up yet \u2014 ${stillRunning.length} sub-agent(s) still running, ${session.messageQueue.length} report(s) still queued.
13531
13743
  Still running:
13532
- ${list}`;
13744
+ `;
13745
+ const llmList = stillRunning.map((c) => ` - ${c.agentName} (created ${new Date(c.createdAt).toISOString()}, last active ${new Date(c.updatedAt).toISOString()}): ${c.description.slice(0, 80)}`).join("\n");
13746
+ const uiList = stillRunning.map((c) => ` - ${c.agentName} (started ${formatAge(now - c.createdAt)} ago, last active ${formatAge(now - c.updatedAt)} ago): ${c.description.slice(0, 80)}`).join("\n");
13747
+ return { llm: `
13748
+
13749
+ [System @ ${nowIso}] ${head}${llmList}`, ui: `[System] ${head}${uiList}` };
13533
13750
  }
13534
13751
  /** Drain queued messages (user→agent AND agent→agent share this one queue)
13535
13752
  * after a session finishes a turn. The whole queue is folded into ONE turn:
@@ -13561,12 +13778,12 @@ ${list}`;
13561
13778
  const merged = batch.map((q) => q.sourceSessionId ? `(from: session ${q.sourceSessionId})
13562
13779
  ${q.text}` : q.text).join("\n\n");
13563
13780
  const images = batch.flatMap((q) => q.images ?? []);
13564
- const suffix = batch.some((q) => q.sourceSessionId !== void 0) ? this.siblingStatusSuffix(session) : "";
13565
- if (suffix && session.parentId === null) {
13566
- this.emitEvent(session.id, { type: "system", text: suffix.trim(), agentName: session.agentName });
13781
+ const suffix = batch.some((q) => q.sourceSessionId !== void 0) ? this.siblingStatusSuffix(session) : { llm: "", ui: "" };
13782
+ if (suffix.ui && session.parentId === null) {
13783
+ this.emitEvent(session.id, { type: "system", text: suffix.ui, agentName: session.agentName });
13567
13784
  }
13568
13785
  const kickNote = kickFlagReset ? "\n\n[System] The continue_task flag you set last turn was reset by this interrupt. If the interrupted task is still unfinished after you answer, call continue_task again." : "";
13569
- const input = this.buildInput(merged + suffix + kickNote, images.length > 0 ? images : void 0, session.supportsImage);
13786
+ const input = this.buildInput(merged + suffix.llm + kickNote, images.length > 0 ? images : void 0, session.supportsImage);
13570
13787
  try {
13571
13788
  await this.runAgentTurn(session, input);
13572
13789
  } catch (err) {
@@ -13574,9 +13791,12 @@ ${q.text}` : q.text).join("\n\n");
13574
13791
  console.error(`[SessionManager] Drain run error for ${session.id}: ${errMsg}`);
13575
13792
  throw err;
13576
13793
  }
13794
+ const nextTurnFollows = session.selfKick && !session.interruptRequested || session.messageQueue.length > 0;
13795
+ const isAnswer = session.selfKick || session.finalOutput !== "";
13796
+ if (nextTurnFollows && isAnswer)
13797
+ this.sendInterimReport(session, batch);
13577
13798
  if (session.messageQueue.length === 0) {
13578
13799
  if (session.selfKick && !session.interruptRequested) {
13579
- this.sendInterimReport(session, batch);
13580
13800
  session.messageQueue.push({ text: CONTINUE_TASK_KICK });
13581
13801
  const taskId = session.parentId !== null ? session.id : void 0;
13582
13802
  this.emitEvent(session.id, { type: "user", text: CONTINUE_TASK_KICK, agentName: "user", report: true, taskId });
@@ -13589,30 +13809,52 @@ ${q.text}` : q.text).join("\n\n");
13589
13809
  }
13590
13810
  }
13591
13811
  }
13592
- /** continue_task interim report. The answer turn that precedes a kick is
13593
- * otherwise lost to whoever asked: the end-of-run reports (tryReportToParent /
13594
- * deliverRelayReport) read only the LAST turn's output, and the kick turn
13595
- * resets it. So when the batch just answered came from the party this session
13596
- * owes a report — its parent (sub-agent) or a relay caller (root, relay-
13597
- * prefixed message) — forward the answer now. Neither path touches the final
13598
- * report's state: no stoppedAt / agent_done (parent's sibling-status still sees
13599
- * this child running), reply_to kept. Goal workers are deliberately excluded
13600
- * (G's messages carry neither mark): an interim would wake G mid-round, and a
13601
- * fresh query_session from G would interrupt W again. */
13812
+ /** Interim report for a drained turn. A turn followed by another turn (kick /
13813
+ * queued message) is otherwise lost to whoever asked: the end-of-run reports
13814
+ * (tryReportToParent / deliverRelayReport) read only the LAST turn's output,
13815
+ * and the next runAgentTurn resets it. So when the batch just answered came
13816
+ * from the party this session owes a report — its parent (sub-agent) or a
13817
+ * relay caller (root, relay-prefixed message) — forward the answer now.
13818
+ * Neither path touches the final report's state: no stoppedAt / agent_done
13819
+ * (parent's sibling-status still sees this child running), reply_to kept.
13820
+ * Goal workers are deliberately excluded (G's messages carry neither mark):
13821
+ * an interim would wake G mid-round, and a fresh query_session from G would
13822
+ * interrupt W again. */
13602
13823
  sendInterimReport(session, batch) {
13603
13824
  const fromParent = session.parentId !== null && batch.some((q) => q.sourceSessionId === session.parentId);
13604
13825
  const fromRelay = session.parentId === null && batch.some((q) => q.sourceSessionId === void 0 && q.text.includes(RELAY_CHANNEL_PREFIX));
13605
- if (!fromParent && !fromRelay || session.turnError)
13826
+ if (fromParent)
13827
+ this.deliverInterim(session, "parent");
13828
+ else if (fromRelay)
13829
+ this.deliverInterim(session, "relay");
13830
+ }
13831
+ /** Interim report for the OPENING turn (runSession's `hasFirst` turn): when it
13832
+ * ends naturally while the queue already holds a follow-up, the drain resets
13833
+ * output and the wrap-up is lost the same way. No batch to read the door from
13834
+ * here — the opening message is the dispatch itself — so the owed party is
13835
+ * whoever this session reports to at the end: its parent, or a relay caller
13836
+ * (reply_to row). Goal workers have neither, so they get nothing. */
13837
+ sendOpeningInterim(session) {
13838
+ if (session.parentId !== null)
13839
+ this.deliverInterim(session, "parent");
13840
+ else if (readReplyTo(this.db, session.id) !== null)
13841
+ this.deliverInterim(session, "relay");
13842
+ }
13843
+ /** Body = the WHOLE turn's text (`output`, not `finalOutput`): in a turn that
13844
+ * answered and then called continue_task the answer rides the tool_use
13845
+ * response, whose text is `final: false` and never reaches finalOutput. */
13846
+ deliverInterim(session, to) {
13847
+ if (session.turnError)
13606
13848
  return;
13607
- const answer = session.finalOutput || session.output;
13849
+ const answer = session.output;
13608
13850
  if (!answer)
13609
13851
  return;
13610
13852
  const cap2 = config.limits.autoReportMax;
13611
13853
  const body = answer.length > cap2 ? answer.slice(0, cap2) + `
13612
13854
 
13613
13855
  [Interim report truncated: ${answer.length} chars total.]` : answer;
13614
- if (fromParent) {
13615
- const text6 = `[Interim report: answered your message (below) and is now resuming its interrupted task \u2014 the final report follows when that is done. Not the final result.]
13856
+ if (to === "parent") {
13857
+ const text6 = `[Interim report \xB7 status: still running] Answered your message (below); the session is still working \u2014 its final report follows when done. Not the final result.
13616
13858
 
13617
13859
  ${body}`;
13618
13860
  this.querySession(session.parentId, session.id, text6).catch((err) => {
@@ -13881,12 +14123,11 @@ ${cleaned}` : cleaned;
13881
14123
  if (accessLevel !== void 0 && accessLevel !== session.accessLevel) {
13882
14124
  session.accessLevel = accessLevel;
13883
14125
  const savedMessages = session.agent.messages;
13884
- const { agent, modelId, systemPrompt, draftReset } = await this.buildAgentInstance(session.agentId, sessionId, session.parentId, session.workingDir ?? void 0, accessLevel);
14126
+ const { agent, modelId, systemPrompt } = await this.buildAgentInstance(session.agentId, sessionId, session.parentId, session.workingDir ?? void 0, accessLevel);
13885
14127
  session.agent = agent;
13886
14128
  session.agent.messages = savedMessages;
13887
14129
  session.currentModelId = modelId;
13888
14130
  session.systemPrompt = systemPrompt;
13889
- session.draftReset = draftReset;
13890
14131
  this.db.update(agentSessions).set({ accessLevel }).where(eq9(agentSessions.id, sessionId)).run();
13891
14132
  }
13892
14133
  const meta = this.db.select().from(agentSessions).where(eq9(agentSessions.id, sessionId)).get();
@@ -14361,7 +14602,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
14361
14602
  if (!uiState)
14362
14603
  return "not_found";
14363
14604
  const messageLog = uiState.messageLog;
14364
- let start = -1;
14605
+ let start2 = -1;
14365
14606
  let seen = -1;
14366
14607
  for (let i = 0; i < messageLog.length; i++) {
14367
14608
  if (messageLog[i].role !== "user")
@@ -14370,22 +14611,22 @@ ${result.summary}`, agentName: "user", report: true, taskId });
14370
14611
  continue;
14371
14612
  seen++;
14372
14613
  if (seen === userOrdinal) {
14373
- start = i;
14614
+ start2 = i;
14374
14615
  break;
14375
14616
  }
14376
14617
  }
14377
- if (start === -1)
14618
+ if (start2 === -1)
14378
14619
  return "no_exchange";
14379
- const target = stripImageMarkers(messageLog[start].content);
14620
+ const target = stripImageMarkers(messageLog[start2].content);
14380
14621
  let rank = 0;
14381
- for (let i = 0; i <= start; i++) {
14622
+ for (let i = 0; i <= start2; i++) {
14382
14623
  const m = messageLog[i];
14383
14624
  if (m.role === "user" && !m.taskId && stripImageMarkers(m.content) === target)
14384
14625
  rank++;
14385
14626
  }
14386
14627
  const newLog = messageLog.slice();
14387
- for (let i = start; i < newLog.length; i++) {
14388
- if (i > start && newLog[i].role === "user")
14628
+ for (let i = start2; i < newLog.length; i++) {
14629
+ if (i > start2 && newLog[i].role === "user")
14389
14630
  break;
14390
14631
  newLog[i] = { ...newLog[i], deleted: true };
14391
14632
  }
@@ -14417,28 +14658,28 @@ ${result.summary}`, agentName: "user", report: true, taskId });
14417
14658
  * Returns the new array, or null when no matching turn exists. */
14418
14659
  deleteRawTurn(raw, target, rank) {
14419
14660
  let seen = 0;
14420
- let start = -1;
14661
+ let start2 = -1;
14421
14662
  for (let i = 0; i < raw.length; i++) {
14422
14663
  if (!isRawTurnStart(raw[i]))
14423
14664
  continue;
14424
14665
  if (stripImageMarkers(stripTurnStamp(rawMessageText(raw[i]))) === target) {
14425
14666
  seen++;
14426
14667
  if (seen === rank) {
14427
- start = i;
14668
+ start2 = i;
14428
14669
  break;
14429
14670
  }
14430
14671
  }
14431
14672
  }
14432
- if (start === -1)
14673
+ if (start2 === -1)
14433
14674
  return null;
14434
14675
  let end = raw.length;
14435
- for (let i = start + 1; i < raw.length; i++) {
14676
+ for (let i = start2 + 1; i < raw.length; i++) {
14436
14677
  if (isRawTurnStart(raw[i])) {
14437
14678
  end = i;
14438
14679
  break;
14439
14680
  }
14440
14681
  }
14441
- return [...raw.slice(0, start), ...raw.slice(end)];
14682
+ return [...raw.slice(0, start2), ...raw.slice(end)];
14442
14683
  }
14443
14684
  // ── Phase 2: deleteSession (SQLite only) ──────────────────────────
14444
14685
  /**
@@ -14999,7 +15240,7 @@ async function createHarness(opts) {
14999
15240
  function interrupt() {
15000
15241
  state.sm.interruptSession(state.sessionId);
15001
15242
  }
15002
- async function stop() {
15243
+ async function stop2() {
15003
15244
  if (state.sm.isSessionRunning(state.sessionId)) {
15004
15245
  await state.sm.stopSession(state.sessionId);
15005
15246
  }
@@ -15076,7 +15317,7 @@ async function createHarness(opts) {
15076
15317
  offEvent,
15077
15318
  command,
15078
15319
  interrupt,
15079
- stop,
15320
+ stop: stop2,
15080
15321
  destroy,
15081
15322
  switchWorkspace,
15082
15323
  getSessionTree,
@@ -16449,8 +16690,8 @@ function applyPathPick(value, ref, pick) {
16449
16690
  const before = value.slice(0, markerStart);
16450
16691
  const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
16451
16692
  const needsQuote = pick.insertPath.includes(" ");
16452
- const path70 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
16453
- return `${before}@${kind} ${path70}`;
16693
+ const path74 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
16694
+ return `${before}@${kind} ${path74}`;
16454
16695
  }
16455
16696
  var init_path_suggest_engine = __esm({
16456
16697
  "src/tui/path-suggest-engine.ts"() {
@@ -17948,7 +18189,9 @@ function extractMediaMessage(text6) {
17948
18189
  const { text: stripped, mediaPaths } = extractMediaPaths(text6);
17949
18190
  return { text: stripped.replace(/\n{3,}/g, "\n\n").trim(), mediaPaths };
17950
18191
  }
17951
- function isMediaPathAllowed(filePath, workspacePath) {
18192
+ function isMediaPathAllowed(filePath, workspacePath, accessLevel) {
18193
+ if (accessLevel === "full")
18194
+ return true;
17952
18195
  const resolved = path40.resolve(filePath);
17953
18196
  const ws = path40.resolve(workspacePath);
17954
18197
  return resolved === ws || resolved.startsWith(ws + path40.sep) || isInTempDir(resolved);
@@ -18263,6 +18506,48 @@ function createFileRoutes() {
18263
18506
  return c.json({ error: errorMessage }, 500);
18264
18507
  }
18265
18508
  });
18509
+ app2.put("/files/raw", async (c) => {
18510
+ try {
18511
+ const filePath = c.req.query("path");
18512
+ const projectId = c.req.query("projectId");
18513
+ const expectMtimeRaw = c.req.query("expectMtime");
18514
+ if (!filePath || !projectId) {
18515
+ return c.json({ error: "path and projectId are required" }, 400);
18516
+ }
18517
+ const expectMtime = expectMtimeRaw === void 0 ? void 0 : Number(expectMtimeRaw);
18518
+ if (expectMtime !== void 0 && !Number.isFinite(expectMtime)) {
18519
+ return c.json({ error: "expectMtime must be a number" }, 400);
18520
+ }
18521
+ const projectPath = await resolveProjectPath(projectId);
18522
+ if (!projectPath) {
18523
+ return c.json({ error: "Project not found" }, 404);
18524
+ }
18525
+ if (!validatePath(filePath, projectPath)) {
18526
+ return c.json({ error: "Path traversal not allowed" }, 403);
18527
+ }
18528
+ const absolutePath = path42.resolve(projectPath, filePath);
18529
+ let before;
18530
+ try {
18531
+ before = await fs35.stat(absolutePath);
18532
+ } catch (err) {
18533
+ if (err.code === "ENOENT")
18534
+ return c.json({ error: "File not found" }, 404);
18535
+ throw err;
18536
+ }
18537
+ if (before.isDirectory())
18538
+ return c.json({ error: "Cannot write a directory" }, 400);
18539
+ if (expectMtime !== void 0 && Math.round(before.mtimeMs) !== Math.round(expectMtime)) {
18540
+ return c.json({ error: "conflict", mtime: before.mtimeMs, size: before.size }, 409);
18541
+ }
18542
+ await fs35.writeFile(absolutePath, Buffer.from(await c.req.arrayBuffer()));
18543
+ const stat = await fs35.stat(absolutePath);
18544
+ return c.json({ ok: true, path: filePath, mtime: stat.mtimeMs, size: stat.size });
18545
+ } catch (err) {
18546
+ const errorMessage = err instanceof Error ? err.message : String(err);
18547
+ console.log(`[Files] Error writing raw file: ${errorMessage}`);
18548
+ return c.json({ error: errorMessage }, 500);
18549
+ }
18550
+ });
18266
18551
  app2.post("/files/new", async (c) => {
18267
18552
  try {
18268
18553
  const body = await c.req.json();
@@ -18378,7 +18663,8 @@ function createFileRoutes() {
18378
18663
  wav: "audio/wav",
18379
18664
  flac: "audio/flac",
18380
18665
  aac: "audio/aac",
18381
- m4a: "audio/mp4"
18666
+ m4a: "audio/mp4",
18667
+ wasm: "application/wasm"
18382
18668
  };
18383
18669
  app2.get("/files/download", async (c) => {
18384
18670
  try {
@@ -18407,15 +18693,15 @@ function createFileRoutes() {
18407
18693
  const disposition = inline ? `inline; filename="${encodeURIComponent(fileName)}"` : `attachment; filename="${encodeURIComponent(fileName)}"`;
18408
18694
  const fileSize = stat.size;
18409
18695
  const rangeHeader = c.req.header("range");
18410
- let start = 0;
18696
+ let start2 = 0;
18411
18697
  let end = fileSize - 1;
18412
18698
  let status = 200;
18413
18699
  if (rangeHeader) {
18414
18700
  const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader);
18415
18701
  if (match) {
18416
- start = parseInt(match[1], 10);
18702
+ start2 = parseInt(match[1], 10);
18417
18703
  end = match[2] ? parseInt(match[2], 10) : fileSize - 1;
18418
- if (isNaN(start) || isNaN(end) || start > end || end >= fileSize) {
18704
+ if (isNaN(start2) || isNaN(end) || start2 > end || end >= fileSize) {
18419
18705
  return new Response(null, {
18420
18706
  status: 416,
18421
18707
  headers: { "Content-Range": `bytes */${fileSize}` }
@@ -18424,8 +18710,8 @@ function createFileRoutes() {
18424
18710
  status = 206;
18425
18711
  }
18426
18712
  }
18427
- const chunkSize = fileSize === 0 ? 0 : end - start + 1;
18428
- const nodeStream = fileSize === 0 ? Readable.from([]) : createReadStream(absolutePath, { start, end });
18713
+ const chunkSize = fileSize === 0 ? 0 : end - start2 + 1;
18714
+ const nodeStream = fileSize === 0 ? Readable.from([]) : createReadStream(absolutePath, { start: start2, end });
18429
18715
  const req = c.req.raw;
18430
18716
  if (req.signal) {
18431
18717
  req.signal.addEventListener("abort", () => {
@@ -18440,7 +18726,7 @@ function createFileRoutes() {
18440
18726
  "Accept-Ranges": "bytes"
18441
18727
  };
18442
18728
  if (status === 206) {
18443
- headers["Content-Range"] = `bytes ${start}-${end}/${fileSize}`;
18729
+ headers["Content-Range"] = `bytes ${start2}-${end}/${fileSize}`;
18444
18730
  }
18445
18731
  return new Response(webStream, { status, headers });
18446
18732
  } catch (err) {
@@ -19565,7 +19851,6 @@ function getAvailableTools() {
19565
19851
  if (_cachedTools)
19566
19852
  return _cachedTools;
19567
19853
  const tools = createWorkspaceTools("/tmp");
19568
- const { tool: draft } = createDraftTool();
19569
19854
  const relay = buildRelayTools({ workspaceRoot: "/tmp" }, "");
19570
19855
  const relayChip = {
19571
19856
  name: "relay_send",
@@ -19573,7 +19858,7 @@ function getAvailableTools() {
19573
19858
 
19574
19859
  ${relay[0].description}`
19575
19860
  };
19576
- _cachedTools = [...tools, draft].map((t2) => ({ name: t2.name, description: t2.description })).concat(relayChip);
19861
+ _cachedTools = tools.map((t2) => ({ name: t2.name, description: t2.description })).concat(relayChip);
19577
19862
  return _cachedTools;
19578
19863
  }
19579
19864
  async function writeFileAtomic2(filePath, content) {
@@ -19766,6 +20051,8 @@ function createAgentConfigRoutes() {
19766
20051
  const id = body.name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-|-$/g, "");
19767
20052
  if (!id)
19768
20053
  return c.json({ error: "Invalid agent name" }, 400);
20054
+ if (id === GOAL_AGENT_ID && !config.goalModeEnabled)
20055
+ return c.json({ error: "Goal mode is disabled." }, 403);
19769
20056
  const scope = body.scope ?? "global";
19770
20057
  let baseDir;
19771
20058
  if (scope === "workspace") {
@@ -20029,6 +20316,13 @@ ${body.description ?? ""}
20029
20316
  if (!isSafeIdSegment(sessionId))
20030
20317
  return c.json({ error: "Invalid session id" }, 400);
20031
20318
  const dir = getSessionsDir(agentId, source, projectId || void 0);
20319
+ if (agentId === GOAL_AGENT_ID && !config.goalModeEnabled) {
20320
+ try {
20321
+ await fs39.access(path47.join(dir, `${sessionId}.json`));
20322
+ } catch {
20323
+ return c.json({ error: "Goal mode is disabled." }, 403);
20324
+ }
20325
+ }
20032
20326
  await ensureDir2(dir);
20033
20327
  const now = (/* @__PURE__ */ new Date()).toISOString();
20034
20328
  const messages2 = body.messages ?? [];
@@ -20092,8 +20386,8 @@ var init_agent_configs = __esm({
20092
20386
  "../server/dist/routes/agent-configs.js"() {
20093
20387
  "use strict";
20094
20388
  init_workspace_tools();
20095
- init_draft_tool();
20096
20389
  init_relay();
20390
+ init_goal_mode();
20097
20391
  init_md_loader();
20098
20392
  init_config();
20099
20393
  init_db();
@@ -20445,8 +20739,8 @@ function detectOrphans(tree, declared) {
20445
20739
  if (!block || typeof block !== "object")
20446
20740
  continue;
20447
20741
  for (const k of Object.keys(block)) {
20448
- const path70 = `${namespaceId}.${kindWord}.${k}`;
20449
- if (!declaredMap.has(path70)) {
20742
+ const path74 = `${namespaceId}.${kindWord}.${k}`;
20743
+ if (!declaredMap.has(path74)) {
20450
20744
  out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
20451
20745
  }
20452
20746
  }
@@ -21532,7 +21826,11 @@ function rememberWechatContextToken(db, accountId, userId, token) {
21532
21826
  return;
21533
21827
  const tokens = existing.config.contextTokens ?? {};
21534
21828
  if (tokens[userId] !== token) {
21535
- patchConfig(db, accountId, { contextTokens: { ...tokens, [userId]: token } });
21829
+ const ats = existing.config.contextTokenAts ?? {};
21830
+ patchConfig(db, accountId, {
21831
+ contextTokens: { ...tokens, [userId]: token },
21832
+ contextTokenAts: { ...ats, [userId]: Date.now() }
21833
+ });
21536
21834
  }
21537
21835
  _wechatContextTokenCache.set(key, token);
21538
21836
  }
@@ -21754,14 +22052,14 @@ function createWebChannel(deps) {
21754
22052
  return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
21755
22053
  }
21756
22054
  function resolveWorkspace(account, override) {
21757
- let path70 = account.workspacePath;
22055
+ let path74 = account.workspacePath;
21758
22056
  if (override && override !== account.workspacePath) {
21759
22057
  if (account.accessLevel !== "full") {
21760
22058
  return { ok: false, error: "workspace override requires a full-access token" };
21761
22059
  }
21762
- path70 = override;
22060
+ path74 = override;
21763
22061
  }
21764
- const resolved = resolveAccountWorkspace({ ...account, workspacePath: path70 });
22062
+ const resolved = resolveAccountWorkspace({ ...account, workspacePath: path74 });
21765
22063
  if (!resolved)
21766
22064
  return { ok: false, error: "workspace not found" };
21767
22065
  return { ok: true, path: resolved };
@@ -22197,11 +22495,19 @@ var init_session_message = __esm({
22197
22495
  }
22198
22496
  });
22199
22497
 
22498
+ // ../core/dist/protocol/extension-frames.js
22499
+ var init_extension_frames = __esm({
22500
+ "../core/dist/protocol/extension-frames.js"() {
22501
+ "use strict";
22502
+ }
22503
+ });
22504
+
22200
22505
  // ../core/dist/protocol/index.js
22201
22506
  var init_protocol = __esm({
22202
22507
  "../core/dist/protocol/index.js"() {
22203
22508
  "use strict";
22204
22509
  init_session_message();
22510
+ init_extension_frames();
22205
22511
  }
22206
22512
  });
22207
22513
 
@@ -23197,7 +23503,7 @@ var init_file_watcher = __esm({
23197
23503
  ];
23198
23504
  IS_WIN32 = process.platform === "win32";
23199
23505
  IGNORED_SEGMENT_SET = new Set(IGNORED_SEGMENTS);
23200
- NATIVE_IGNORE = IS_WIN32 ? IGNORED_SEGMENTS : IGNORED_SEGMENTS.flatMap((seg) => [seg, `**/${seg}/**`]);
23506
+ NATIVE_IGNORE = IS_WIN32 ? IGNORED_SEGMENTS : [...IGNORED_SEGMENTS, `**/{${IGNORED_SEGMENTS.join(",")}}/**`];
23201
23507
  nativeOpChain = Promise.resolve();
23202
23508
  WorkspaceWatcher = class {
23203
23509
  subscription = null;
@@ -23231,8 +23537,8 @@ var init_file_watcher = __esm({
23231
23537
  const epoch = ++this.startEpoch;
23232
23538
  this.workspaceRoot = root;
23233
23539
  try {
23234
- const { default: watcher } = await import("@parcel/watcher");
23235
- const sub = await enqueueNativeOp(() => watcher.subscribe(root, (err, events) => {
23540
+ const { default: watcher2 } = await import("@parcel/watcher");
23541
+ const sub = await enqueueNativeOp(() => watcher2.subscribe(root, (err, events) => {
23236
23542
  if (err) {
23237
23543
  console.warn(`[FileWatcher] watcher error on ${root}: ${err.message}`);
23238
23544
  return;
@@ -23492,28 +23798,43 @@ function sendJson(ws, data) {
23492
23798
  function sendWsNotification(event, state, turnId, ctx) {
23493
23799
  const agentName = event.agentName ?? "default";
23494
23800
  const taskId = event.taskId;
23801
+ const sessionId = ctx.sessionId;
23495
23802
  switch (event.type) {
23803
+ // Streaming chunks ride the same frames as the whole-text events — the
23804
+ // admin appends chat:thinking / chat:stream by turnId either way. The whole
23805
+ // event that follows a streamed call is stamped `streamed` and dropped so
23806
+ // the client doesn't render the text twice.
23807
+ case "thinking_delta":
23808
+ sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, taskId, turnId, sessionId });
23809
+ break;
23810
+ case "stream_delta":
23811
+ sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, taskId, turnId, sessionId });
23812
+ break;
23496
23813
  case "thinking":
23497
- sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, taskId, turnId });
23814
+ if (event.streamed)
23815
+ break;
23816
+ sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, taskId, turnId, sessionId });
23498
23817
  break;
23499
23818
  case "stream":
23500
- sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, taskId, turnId });
23819
+ if (event.streamed)
23820
+ break;
23821
+ sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, taskId, turnId, sessionId });
23501
23822
  break;
23502
23823
  case "agent_start":
23503
- sendJson(ctx.ws, { type: "agent:start", agentName, task: event.text, taskId });
23824
+ sendJson(ctx.ws, { type: "agent:start", agentName, task: event.text, taskId, sessionId });
23504
23825
  break;
23505
23826
  case "agent_done":
23506
- sendJson(ctx.ws, { type: "agent:done", agentName, taskId });
23827
+ sendJson(ctx.ws, { type: "agent:done", agentName, taskId, sessionId });
23507
23828
  break;
23508
23829
  case "tool_call":
23509
- sendJson(ctx.ws, { type: "agent:tool_call", tool: event.toolName, toolUseId: event.toolUseId, input: event.toolInput, agentName, taskId, turnId });
23830
+ sendJson(ctx.ws, { type: "agent:tool_call", tool: event.toolName, toolUseId: event.toolUseId, input: event.toolInput, agentName, taskId, turnId, sessionId });
23510
23831
  break;
23511
23832
  case "tool_result":
23512
- sendJson(ctx.ws, { type: "agent:tool_result", result: event.toolResult, toolUseId: event.toolUseId, agentName, taskId, durationMs: event.durationMs });
23833
+ sendJson(ctx.ws, { type: "agent:tool_result", result: event.toolResult, toolUseId: event.toolUseId, agentName, taskId, durationMs: event.durationMs, sessionId });
23513
23834
  break;
23514
23835
  case "followup_start":
23515
23836
  case "queued_message":
23516
- sendJson(ctx.ws, { type: "chat:followup", agentName });
23837
+ sendJson(ctx.ws, { type: "chat:followup", agentName, sessionId });
23517
23838
  break;
23518
23839
  case "usage":
23519
23840
  if (!taskId) {
@@ -23523,28 +23844,29 @@ function sendWsNotification(event, state, turnId, ctx) {
23523
23844
  outputTokens: state.outputTokens,
23524
23845
  turnId,
23525
23846
  modelId: event.modelId,
23526
- usage: buildUsageData(event)
23847
+ usage: buildUsageData(event),
23848
+ sessionId
23527
23849
  });
23528
23850
  }
23529
23851
  break;
23530
23852
  case "complete":
23531
- sendJson(ctx.ws, { type: "chat:complete", sessionId: ctx.sessionId });
23853
+ sendJson(ctx.ws, { type: "chat:complete", sessionId });
23532
23854
  break;
23533
23855
  case "context":
23534
- sendJson(ctx.ws, { type: "agent:context", agentName, systemPrompt: event.systemPrompt, taskId });
23856
+ sendJson(ctx.ws, { type: "agent:context", agentName, systemPrompt: event.systemPrompt, taskId, sessionId });
23535
23857
  break;
23536
23858
  case "system":
23537
23859
  if (!taskId && /^Compacting context \(\d+K tokens\)…$/.test(event.text ?? "")) {
23538
23860
  sendJson(ctx.ws, { type: "compact:started" });
23539
23861
  }
23540
- sendJson(ctx.ws, { type: "chat:system", text: event.text ?? "", taskId, agentName });
23862
+ sendJson(ctx.ws, { type: "chat:system", text: event.text ?? "", taskId, agentName, sessionId });
23541
23863
  break;
23542
23864
  case "error":
23543
- sendJson(ctx.ws, { type: "error", error: event.error, agentName, taskId });
23865
+ sendJson(ctx.ws, { type: "error", error: event.error, agentName, taskId, sessionId });
23544
23866
  break;
23545
23867
  case "user":
23546
23868
  if (!taskId && !event.localEcho) {
23547
- sendJson(ctx.ws, { type: "chat:user", text: event.text ?? "" });
23869
+ sendJson(ctx.ws, { type: "chat:user", text: event.text ?? "", sessionId });
23548
23870
  }
23549
23871
  break;
23550
23872
  case "compacted":
@@ -23877,14 +24199,14 @@ function setupWebSocketHandler(deps) {
23877
24199
  if (!client.sessionManager.isUIStateDirty(client.sessionId))
23878
24200
  return;
23879
24201
  const projectPath = resolveProjectPath2(client.projectId);
23880
- const snapshot = createSaveSnapshot(state);
23881
- if (snapshot.length === 0)
24202
+ const snapshot2 = createSaveSnapshot(state);
24203
+ if (snapshot2.length === 0)
23882
24204
  return;
23883
24205
  const sessionInfo = client.sessionManager.getSessionById(client.sessionId);
23884
24206
  client.sessionManager.persistSessionFile({
23885
24207
  sessionId: client.sessionId,
23886
24208
  projectPath,
23887
- messages: snapshot,
24209
+ messages: snapshot2,
23888
24210
  contextTokens: state.contextTokens,
23889
24211
  outputTokens: state.outputTokens,
23890
24212
  agentId: sessionInfo?.agentId,
@@ -24367,17 +24689,18 @@ ${msg.message}` : markers.join("\n");
24367
24689
  if (state2 && running) {
24368
24690
  console.debug(`[WS] Session still running \u2014 synthesizing in-progress state`);
24369
24691
  const agentName = detachedSession?.agentName ?? (state2.streamingAgent || "default");
24370
- sendJson(ws, { type: "chat:followup", agentName, replay: true });
24692
+ const sessionId = client2.sessionId;
24693
+ sendJson(ws, { type: "chat:followup", agentName, replay: true, sessionId });
24371
24694
  for (const block of state2.turnContentBlocks) {
24372
24695
  if (block.type === "thinking") {
24373
- sendJson(ws, { type: "chat:thinking", text: block.text, agentName, turnId: block.turnId, replay: true });
24696
+ sendJson(ws, { type: "chat:thinking", text: block.text, agentName, turnId: block.turnId, replay: true, sessionId });
24374
24697
  } else if (block.type === "text") {
24375
- sendJson(ws, { type: "chat:stream", text: block.text, agentName, turnId: block.turnId, replay: true });
24698
+ sendJson(ws, { type: "chat:stream", text: block.text, agentName, turnId: block.turnId, replay: true, sessionId });
24376
24699
  } else {
24377
24700
  const tc = block.toolCall;
24378
- sendJson(ws, { type: "agent:tool_call", tool: tc.name, toolUseId: tc.toolUseId, input: tc.input, agentName, turnId: block.turnId, replay: true });
24701
+ sendJson(ws, { type: "agent:tool_call", tool: tc.name, toolUseId: tc.toolUseId, input: tc.input, agentName, turnId: block.turnId, replay: true, sessionId });
24379
24702
  if (tc.output) {
24380
- sendJson(ws, { type: "agent:tool_result", result: tc.output, toolUseId: tc.toolUseId, agentName, durationMs: tc.durationMs, replay: true });
24703
+ sendJson(ws, { type: "agent:tool_result", result: tc.output, toolUseId: tc.toolUseId, agentName, durationMs: tc.durationMs, replay: true, sessionId });
24381
24704
  }
24382
24705
  }
24383
24706
  }
@@ -24681,14 +25004,14 @@ function resolveHaloCli() {
24681
25004
  return process.platform === "win32" ? "halo.cmd" : "halo";
24682
25005
  }
24683
25006
  function listDescendants(rootPid) {
24684
- let snapshot;
25007
+ let snapshot2;
24685
25008
  try {
24686
- snapshot = execFileSync("ps", ["-e", "-o", "pid=,ppid="], { encoding: "utf-8" });
25009
+ snapshot2 = execFileSync("ps", ["-e", "-o", "pid=,ppid="], { encoding: "utf-8" });
24687
25010
  } catch {
24688
25011
  return [];
24689
25012
  }
24690
25013
  const childrenOf = /* @__PURE__ */ new Map();
24691
- for (const line of snapshot.split("\n")) {
25014
+ for (const line of snapshot2.split("\n")) {
24692
25015
  const parts = line.trim().split(/\s+/);
24693
25016
  if (parts.length !== 2)
24694
25017
  continue;
@@ -25574,88 +25897,810 @@ var init_cron = __esm({
25574
25897
  }
25575
25898
  });
25576
25899
 
25577
- // ../server/dist/evolution/ticker.js
25578
- import { eq as eq16, and as and12, or as or2, asc, isNull as isNull6, sql as sql2 } from "drizzle-orm";
25579
- function setEvoSpawner(fn) {
25580
- _spawner = fn;
25900
+ // ../server/dist/extensions/registry.js
25901
+ import fs53 from "node:fs";
25902
+ import path61 from "node:path";
25903
+ function isExtensionId(id) {
25904
+ return ID_RE.test(id);
25905
+ }
25906
+ function extensionsRoot() {
25907
+ return globalExtensionsDir();
25908
+ }
25909
+ function parseManifest(raw, dirName, entryExists) {
25910
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
25911
+ return { error: "manifest must be a JSON object" };
25912
+ const m = raw;
25913
+ if (typeof m.id !== "string" || !ID_RE.test(m.id))
25914
+ return { error: "id must match ^[a-z0-9][a-z0-9_-]{0,63}$" };
25915
+ if (m.id !== dirName)
25916
+ return { error: `id mismatch: manifest "${m.id}" vs directory "${dirName}"` };
25917
+ if (typeof m.name !== "string" || m.name.trim() === "" || m.name.length > 64)
25918
+ return { error: "name must be a non-empty string (\u2264 64 chars)" };
25919
+ if (typeof m.version !== "string" || !VERSION_RE.test(m.version))
25920
+ return { error: "version must be semver (x.y.z[-pre])" };
25921
+ if (m.description !== void 0 && (typeof m.description !== "string" || m.description.length > 200))
25922
+ return { error: "description must be a string (\u2264 200 chars)" };
25923
+ if (!Array.isArray(m.extensions) || m.extensions.length === 0)
25924
+ return { error: "extensions must be a non-empty array" };
25925
+ for (const e of m.extensions) {
25926
+ if (typeof e !== "string" || !FILE_EXT_RE.test(e))
25927
+ return { error: `extensions entry "${String(e)}" must look like ".glb" (lowercase, with dot)` };
25928
+ }
25929
+ if (typeof m.entry !== "string" || m.entry === "" || m.entry.startsWith("/") || m.entry.split(/[\\/]/).includes("..")) {
25930
+ return { error: "entry must be a relative path inside the extension directory" };
25931
+ }
25932
+ if (!entryExists(m.entry))
25933
+ return { error: `entry not found: ${m.entry}` };
25934
+ let priority = "default";
25935
+ if (m.priority !== void 0) {
25936
+ if (m.priority !== "default" && m.priority !== "option")
25937
+ return { error: 'priority must be "default" or "option"' };
25938
+ priority = m.priority;
25939
+ }
25940
+ const capabilities = [];
25941
+ if (m.capabilities !== void 0) {
25942
+ if (!Array.isArray(m.capabilities))
25943
+ return { error: "capabilities must be an array" };
25944
+ for (const cap2 of m.capabilities) {
25945
+ if (typeof cap2 !== "string" || !CAPABILITIES.has(cap2))
25946
+ return { error: `unknown capability: ${String(cap2)}` };
25947
+ if (!capabilities.includes(cap2))
25948
+ capabilities.push(cap2);
25949
+ }
25950
+ }
25951
+ if (m.homepage !== void 0 && (typeof m.homepage !== "string" || !/^https?:\/\//.test(m.homepage)))
25952
+ return { error: "homepage must be an http(s) URL" };
25953
+ if (m.license !== void 0 && typeof m.license !== "string")
25954
+ return { error: "license must be a string" };
25955
+ return {
25956
+ id: m.id,
25957
+ name: m.name,
25958
+ version: m.version,
25959
+ ...m.description !== void 0 ? { description: m.description } : {},
25960
+ extensions: m.extensions,
25961
+ entry: m.entry,
25962
+ priority,
25963
+ capabilities,
25964
+ ...m.homepage !== void 0 ? { homepage: m.homepage } : {},
25965
+ ...m.license !== void 0 ? { license: m.license } : {}
25966
+ };
25581
25967
  }
25582
- function startEvoTicker() {
25583
- if (_interval)
25584
- return;
25585
- _startupTimer2 = setTimeout(() => {
25586
- _startupTimer2 = null;
25587
- runTick().catch(logTickError);
25588
- }, 2e3);
25589
- _interval = setInterval(() => {
25590
- runTick().catch(logTickError);
25591
- }, TICK_INTERVAL_MS);
25592
- console.log(`[EvoTicker] started (interval: ${TICK_INTERVAL_MS / 1e3}s)`);
25968
+ function readExtensionDir(dir, id = path61.basename(dir)) {
25969
+ let raw;
25970
+ try {
25971
+ raw = JSON.parse(fs53.readFileSync(path61.join(dir, MANIFEST_FILE), "utf-8"));
25972
+ } catch (err) {
25973
+ const code = err.code;
25974
+ return { id, error: code === "ENOENT" ? `${MANIFEST_FILE} missing` : `${MANIFEST_FILE} unreadable: ${err instanceof Error ? err.message : String(err)}` };
25975
+ }
25976
+ const parsed = parseManifest(raw, id, (entry) => {
25977
+ try {
25978
+ return fs53.statSync(path61.join(dir, entry)).isFile();
25979
+ } catch {
25980
+ return false;
25981
+ }
25982
+ });
25983
+ if ("error" in parsed)
25984
+ return { id, error: parsed.error };
25985
+ let installedAt = 0;
25986
+ try {
25987
+ installedAt = Math.round(fs53.statSync(dir).mtimeMs);
25988
+ } catch {
25989
+ }
25990
+ return { ...parsed, installedAt };
25593
25991
  }
25594
- function stopEvoTicker() {
25595
- if (_startupTimer2) {
25596
- clearTimeout(_startupTimer2);
25597
- _startupTimer2 = null;
25992
+ function scanExtensions() {
25993
+ const root = extensionsRoot();
25994
+ const extensions = [];
25995
+ const errors = [];
25996
+ let entries = [];
25997
+ try {
25998
+ entries = fs53.readdirSync(root, { withFileTypes: true });
25999
+ } catch {
25598
26000
  }
25599
- if (_interval) {
25600
- clearInterval(_interval);
25601
- _interval = null;
25602
- console.log("[EvoTicker] stopped");
26001
+ for (const entry of entries) {
26002
+ if (!entry.isDirectory() || entry.name.startsWith("."))
26003
+ continue;
26004
+ if (!ID_RE.test(entry.name)) {
26005
+ errors.push({ id: entry.name, error: "directory name is not a valid extension id" });
26006
+ continue;
26007
+ }
26008
+ const result = readExtensionDir(path61.join(root, entry.name));
26009
+ if ("error" in result)
26010
+ errors.push(result);
26011
+ else
26012
+ extensions.push(result);
25603
26013
  }
26014
+ extensions.sort((a, b) => a.id.localeCompare(b.id));
26015
+ errors.sort((a, b) => a.id.localeCompare(b.id));
26016
+ snapshot = { extensions, errors };
26017
+ return snapshot;
25604
26018
  }
25605
- function logTickError(err) {
25606
- console.error(`[EvoTicker] tick failed: ${err instanceof Error ? err.message : String(err)}`);
26019
+ function getSnapshot() {
26020
+ return snapshot;
25607
26021
  }
25608
- async function runTick() {
25609
- markTimeouts();
25610
- startPendingRuns();
25611
- startPendingApplies();
25612
- broadcastChanges();
26022
+ function getExtension(id) {
26023
+ return snapshot.extensions.find((e) => e.id === id);
25613
26024
  }
25614
- function broadcastChanges() {
25615
- const db = getEvoDb();
25616
- const inFlightLimit = (config.evolution.maxConcurrentRun + config.evolution.maxConcurrentApply) * 3;
25617
- const runRows = db.select({ id: evolutionRuns.id, status: evolutionRuns.status }).from(evolutionRuns).where(or2(eq16(evolutionRuns.status, "pending"), eq16(evolutionRuns.status, "running"))).limit(inFlightLimit).all();
25618
- for (const r of runRows) {
25619
- if (_lastRunStatus.get(r.id) !== r.status) {
25620
- _lastRunStatus.set(r.id, r.status);
25621
- broadcast({ type: "evolution:run_changed", id: r.id, status: r.status });
25622
- }
25623
- }
25624
- const applyRows = db.select({ id: evolutionApplies.id, status: evolutionApplies.status }).from(evolutionApplies).where(or2(eq16(evolutionApplies.status, "pending"), eq16(evolutionApplies.status, "running"), eq16(evolutionApplies.status, "syncing"))).limit(inFlightLimit).all();
25625
- for (const a of applyRows) {
25626
- if (_lastApplyStatus.get(a.id) !== a.status) {
25627
- _lastApplyStatus.set(a.id, a.status);
25628
- broadcast({ type: "evolution:apply_changed", id: a.id, status: a.status });
25629
- }
26025
+ function snapshotKey(s) {
26026
+ return [
26027
+ ...s.extensions.map((e) => `${e.id}@${e.version}#${e.installedAt}`),
26028
+ ...s.errors.map((e) => `!${e.id}:${e.error}`)
26029
+ ].join("\n");
26030
+ }
26031
+ var MANIFEST_FILE, ID_RE, VERSION_RE, FILE_EXT_RE, CAPABILITIES, snapshot;
26032
+ var init_registry2 = __esm({
26033
+ "../server/dist/extensions/registry.js"() {
26034
+ "use strict";
26035
+ init_paths();
26036
+ MANIFEST_FILE = "halo-extension.json";
26037
+ ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
26038
+ VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
26039
+ FILE_EXT_RE = /^\.[a-z0-9]+$/;
26040
+ CAPABILITIES = /* @__PURE__ */ new Set(["save"]);
26041
+ snapshot = { extensions: [], errors: [] };
25630
26042
  }
25631
- const aliveRunIds = new Set(runRows.map((r) => r.id));
25632
- for (const id of [..._lastRunStatus.keys()]) {
25633
- if (aliveRunIds.has(id))
26043
+ });
26044
+
26045
+ // ../server/dist/extensions/install.js
26046
+ import fs54 from "node:fs";
26047
+ import path62 from "node:path";
26048
+ import { randomBytes as randomBytes2 } from "node:crypto";
26049
+ import JSZip2 from "jszip";
26050
+ function safeEntryPath(name) {
26051
+ const posix = name.replace(/\\/g, "/");
26052
+ if (posix.startsWith("/") || /^[A-Za-z]:/.test(posix))
26053
+ throw new ExtensionInstallError(`zip entry has an absolute path: ${name}`);
26054
+ const segments = posix.split("/").filter((s) => s !== "" && s !== ".");
26055
+ if (segments.includes(".."))
26056
+ throw new ExtensionInstallError(`zip entry escapes the archive root: ${name}`);
26057
+ if (segments.some((s) => s.includes("\0")))
26058
+ throw new ExtensionInstallError(`zip entry name contains NUL: ${name}`);
26059
+ return segments.join("/");
26060
+ }
26061
+ async function unpackZip(zipBuf, dest) {
26062
+ let zip;
26063
+ try {
26064
+ zip = await JSZip2.loadAsync(zipBuf);
26065
+ } catch (err) {
26066
+ throw new ExtensionInstallError(`not a zip file: ${err instanceof Error ? err.message : String(err)}`);
26067
+ }
26068
+ const files = Object.values(zip.files);
26069
+ if (files.length === 0)
26070
+ throw new ExtensionInstallError("zip is empty");
26071
+ for (const entry of files) {
26072
+ const rel = safeEntryPath(entry.name);
26073
+ if (rel === "")
26074
+ continue;
26075
+ const perm = entry.unixPermissions;
26076
+ if (typeof perm === "number" && (perm & S_IFMT) === S_IFLNK)
26077
+ throw new ExtensionInstallError(`zip entry is a symlink: ${entry.name}`);
26078
+ const abs = path62.join(dest, rel);
26079
+ if (entry.dir) {
26080
+ fs54.mkdirSync(abs, { recursive: true });
25634
26081
  continue;
25635
- const row = db.select({ status: evolutionRuns.status }).from(evolutionRuns).where(eq16(evolutionRuns.id, id)).get();
25636
- const finalStatus = row?.status;
25637
- if (finalStatus && _lastRunStatus.get(id) !== finalStatus) {
25638
- broadcast({ type: "evolution:run_changed", id, status: finalStatus });
25639
26082
  }
25640
- _lastRunStatus.delete(id);
26083
+ fs54.mkdirSync(path62.dirname(abs), { recursive: true });
26084
+ fs54.writeFileSync(abs, await entry.async("nodebuffer"));
25641
26085
  }
25642
- const aliveApplyIds = new Set(applyRows.map((r) => r.id));
25643
- for (const id of [..._lastApplyStatus.keys()]) {
25644
- if (aliveApplyIds.has(id))
25645
- continue;
25646
- const row = db.select({ status: evolutionApplies.status }).from(evolutionApplies).where(eq16(evolutionApplies.id, id)).get();
25647
- const finalStatus = row?.status;
25648
- if (finalStatus && _lastApplyStatus.get(id) !== finalStatus) {
25649
- broadcast({ type: "evolution:apply_changed", id, status: finalStatus });
26086
+ }
26087
+ function stripWrapperDir(dir) {
26088
+ if (fs54.existsSync(path62.join(dir, MANIFEST_FILE)))
26089
+ return;
26090
+ const children = fs54.readdirSync(dir, { withFileTypes: true });
26091
+ if (children.length !== 1 || !children[0].isDirectory())
26092
+ return;
26093
+ const inner = path62.join(dir, children[0].name);
26094
+ if (!fs54.existsSync(path62.join(inner, MANIFEST_FILE)))
26095
+ return;
26096
+ for (const name of fs54.readdirSync(inner)) {
26097
+ fs54.renameSync(path62.join(inner, name), path62.join(dir, name));
26098
+ }
26099
+ fs54.rmdirSync(inner);
26100
+ }
26101
+ function rand() {
26102
+ return randomBytes2(3).toString("hex");
26103
+ }
26104
+ async function installExtensionZip(zipBuf, expectId) {
26105
+ const root = extensionsRoot();
26106
+ fs54.mkdirSync(root, { recursive: true });
26107
+ const stage = path62.join(root, `.tmp-unpack-${rand()}`);
26108
+ fs54.mkdirSync(stage);
26109
+ try {
26110
+ await unpackZip(zipBuf, stage);
26111
+ stripWrapperDir(stage);
26112
+ let manifestId;
26113
+ try {
26114
+ const raw = JSON.parse(fs54.readFileSync(path62.join(stage, MANIFEST_FILE), "utf-8"));
26115
+ manifestId = typeof raw.id === "string" ? raw.id : "";
26116
+ } catch (err) {
26117
+ throw new ExtensionInstallError(`${MANIFEST_FILE} missing or unreadable at the zip root: ${err instanceof Error ? err.message : String(err)}`);
26118
+ }
26119
+ if (!isExtensionId(manifestId))
26120
+ throw new ExtensionInstallError(`manifest id "${manifestId}" is not a valid extension id`);
26121
+ if (expectId !== void 0 && manifestId !== expectId)
26122
+ throw new ExtensionInstallError(`manifest id "${manifestId}" does not match requested "${expectId}"`);
26123
+ const verdict = readExtensionDir(stage, manifestId);
26124
+ if ("error" in verdict)
26125
+ throw new ExtensionInstallError(`invalid extension: ${verdict.error}`);
26126
+ const target = path62.join(root, manifestId);
26127
+ const old = path62.join(root, `.old-${manifestId}-${rand()}`);
26128
+ const hadOld = fs54.existsSync(target);
26129
+ if (hadOld)
26130
+ fs54.renameSync(target, old);
26131
+ try {
26132
+ fs54.renameSync(stage, target);
26133
+ } catch (err) {
26134
+ if (hadOld)
26135
+ fs54.renameSync(old, target);
26136
+ throw err;
25650
26137
  }
25651
- _lastApplyStatus.delete(id);
26138
+ if (hadOld)
26139
+ fs54.rmSync(old, { recursive: true, force: true });
26140
+ const info = readExtensionDir(target);
26141
+ if ("error" in info)
26142
+ throw new ExtensionInstallError(`installed but failed re-validation: ${info.error}`);
26143
+ console.log(`[Extensions] installed ${info.id}@${info.version}${hadOld ? " (replaced previous)" : ""}`);
26144
+ return info;
26145
+ } finally {
26146
+ fs54.rmSync(stage, { recursive: true, force: true });
25652
26147
  }
25653
26148
  }
25654
- function markTimeouts() {
25655
- const db = getEvoDb();
25656
- const now = Date.now();
25657
- const runCutoff = now - config.evolution.runTimeoutMinutes * 6e4;
25658
- const applyCutoff = now - config.evolution.applyTimeoutMinutes * 6e4;
26149
+ function uninstallExtension(id) {
26150
+ if (!isExtensionId(id))
26151
+ throw new ExtensionInstallError(`"${id}" is not a valid extension id`);
26152
+ const dir = path62.join(extensionsRoot(), id);
26153
+ if (!fs54.existsSync(dir))
26154
+ return false;
26155
+ fs54.rmSync(dir, { recursive: true, force: true });
26156
+ console.log(`[Extensions] removed ${id}`);
26157
+ return true;
26158
+ }
26159
+ var ExtensionInstallError, S_IFMT, S_IFLNK;
26160
+ var init_install = __esm({
26161
+ "../server/dist/extensions/install.js"() {
26162
+ "use strict";
26163
+ init_registry2();
26164
+ ExtensionInstallError = class extends Error {
26165
+ };
26166
+ S_IFMT = 61440;
26167
+ S_IFLNK = 40960;
26168
+ }
26169
+ });
26170
+
26171
+ // ../server/dist/extensions/watcher.js
26172
+ import fs55 from "node:fs";
26173
+ function scheduleFlush() {
26174
+ if (flushTimer)
26175
+ return;
26176
+ flushTimer = setTimeout(() => {
26177
+ flushTimer = null;
26178
+ rescanAndBroadcast();
26179
+ }, 300);
26180
+ }
26181
+ function rescanAndBroadcast() {
26182
+ const snap = scanExtensions();
26183
+ const key = snapshotKey(snap);
26184
+ if (key === lastKey)
26185
+ return;
26186
+ lastKey = key;
26187
+ broadcast({ type: "extension:changed", ...snap });
26188
+ }
26189
+ function openWatcher(root) {
26190
+ try {
26191
+ watcher = fs55.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
26192
+ watcher.on("error", (err) => {
26193
+ console.warn(`[Extensions] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
26194
+ closeWatcher();
26195
+ restartTimer = setTimeout(() => {
26196
+ restartTimer = null;
26197
+ start();
26198
+ }, 1e3);
26199
+ scheduleFlush();
26200
+ });
26201
+ } catch (err) {
26202
+ console.warn(`[Extensions] failed to watch ${root}: ${err instanceof Error ? err.message : String(err)} \u2014 live extension updates disabled`);
26203
+ watcher = null;
26204
+ }
26205
+ }
26206
+ function closeWatcher() {
26207
+ if (watcher) {
26208
+ watcher.close();
26209
+ watcher = null;
26210
+ }
26211
+ }
26212
+ function start() {
26213
+ if (watcher)
26214
+ return;
26215
+ const root = extensionsRoot();
26216
+ try {
26217
+ fs55.mkdirSync(root, { recursive: true });
26218
+ } catch (err) {
26219
+ console.warn(`[Extensions] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
26220
+ }
26221
+ lastKey = snapshotKey(scanExtensions());
26222
+ openWatcher(root);
26223
+ const { extensions, errors } = getSnapshot();
26224
+ if (extensions.length > 0 || errors.length > 0) {
26225
+ console.log(`[Extensions] ${extensions.length} installed${errors.length ? `, ${errors.length} invalid` : ""}: ${[...extensions.map((e) => `${e.id}@${e.version}`), ...errors.map((e) => `${e.id}(!)`)].join(", ")}`);
26226
+ }
26227
+ }
26228
+ function stop() {
26229
+ if (flushTimer)
26230
+ clearTimeout(flushTimer);
26231
+ flushTimer = null;
26232
+ if (restartTimer)
26233
+ clearTimeout(restartTimer);
26234
+ restartTimer = null;
26235
+ closeWatcher();
26236
+ }
26237
+ var watcher, flushTimer, restartTimer, lastKey;
26238
+ var init_watcher = __esm({
26239
+ "../server/dist/extensions/watcher.js"() {
26240
+ "use strict";
26241
+ init_broadcast();
26242
+ init_registry2();
26243
+ watcher = null;
26244
+ flushTimer = null;
26245
+ restartTimer = null;
26246
+ lastKey = "";
26247
+ }
26248
+ });
26249
+
26250
+ // ../server/dist/middleware/auth.js
26251
+ import { Hono as Hono15 } from "hono";
26252
+ import { getCookie, setCookie } from "hono/cookie";
26253
+ import crypto3 from "node:crypto";
26254
+ function passwordHash() {
26255
+ return config.server.password ?? "";
26256
+ }
26257
+ function passwordEnvPlaintext() {
26258
+ return config.server.passwordEnvPlaintext;
26259
+ }
26260
+ function jwtSecret() {
26261
+ const secret = config.server.jwtSecret ?? "";
26262
+ if (!secret) {
26263
+ throw new Error("jwt_secret missing \u2014 run `halo setup` to initialize");
26264
+ }
26265
+ return Buffer.from(secret, "base64");
26266
+ }
26267
+ function base64url(data) {
26268
+ const buf = typeof data === "string" ? Buffer.from(data) : data;
26269
+ return buf.toString("base64url");
26270
+ }
26271
+ function jwtSign(payload) {
26272
+ const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
26273
+ const body = base64url(JSON.stringify(payload));
26274
+ const sig = crypto3.createHmac("sha256", jwtSecret()).update(`${header}.${body}`).digest();
26275
+ return `${header}.${body}.${base64url(sig)}`;
26276
+ }
26277
+ function jwtVerify(token) {
26278
+ const parts = token.split(".");
26279
+ if (parts.length !== 3)
26280
+ return null;
26281
+ const [header, body, sig] = parts;
26282
+ let secret;
26283
+ try {
26284
+ secret = jwtSecret();
26285
+ } catch {
26286
+ return null;
26287
+ }
26288
+ const expected = base64url(crypto3.createHmac("sha256", secret).update(`${header}.${body}`).digest());
26289
+ if (sig !== expected)
26290
+ return null;
26291
+ try {
26292
+ const payload = JSON.parse(Buffer.from(body, "base64url").toString());
26293
+ if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp)
26294
+ return null;
26295
+ return payload;
26296
+ } catch {
26297
+ return null;
26298
+ }
26299
+ }
26300
+ function createToken() {
26301
+ const now = Math.floor(Date.now() / 1e3);
26302
+ return jwtSign({ iat: now, exp: now + TOKEN_MAX_AGE });
26303
+ }
26304
+ function constantTimeEqualString(a, b) {
26305
+ const ab = Buffer.from(a, "utf-8");
26306
+ const bb = Buffer.from(b, "utf-8");
26307
+ const max = Math.max(ab.length, bb.length);
26308
+ const ap = Buffer.alloc(max);
26309
+ ab.copy(ap);
26310
+ const bp = Buffer.alloc(max);
26311
+ bb.copy(bp);
26312
+ const eq17 = crypto3.timingSafeEqual(ap, bp);
26313
+ return eq17 && ab.length === bb.length;
26314
+ }
26315
+ function validateToken(token) {
26316
+ if (!token)
26317
+ return null;
26318
+ const payload = jwtVerify(token);
26319
+ if (payload?.scope)
26320
+ return null;
26321
+ return payload;
26322
+ }
26323
+ function mintScopedToken(scope, maxAgeSec) {
26324
+ const now = Math.floor(Date.now() / 1e3);
26325
+ return jwtSign({ scope, iat: now, exp: now + maxAgeSec });
26326
+ }
26327
+ function verifyScopedToken(token, scope) {
26328
+ if (!token)
26329
+ return false;
26330
+ const payload = jwtVerify(token);
26331
+ return payload !== null && payload.scope === scope;
26332
+ }
26333
+ function shouldRefresh(payload) {
26334
+ const iat = payload.iat;
26335
+ if (!iat)
26336
+ return true;
26337
+ return Date.now() / 1e3 - iat > REFRESH_AFTER;
26338
+ }
26339
+ function isAuthDisabled() {
26340
+ return passwordHash().length === 0 && passwordEnvPlaintext() == null;
26341
+ }
26342
+ function isAuthenticated(cookie) {
26343
+ if (isAuthDisabled())
26344
+ return true;
26345
+ return validateToken(cookie) !== null;
26346
+ }
26347
+ function getTokenFromCookieHeader(cookieHeader) {
26348
+ if (!cookieHeader)
26349
+ return void 0;
26350
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
26351
+ return match?.[1];
26352
+ }
26353
+ function setTokenCookie(c, token) {
26354
+ setCookie(c, COOKIE_NAME, token, {
26355
+ path: "/",
26356
+ // WS upgrade requests carry cookies on the underlying HTTP handshake;
26357
+ // the server reads them via the Cookie header, never via JS — so the
26358
+ // cookie can stay httpOnly to avoid XSS exfiltration.
26359
+ httpOnly: true,
26360
+ maxAge: TOKEN_MAX_AGE,
26361
+ sameSite: "Lax"
26362
+ });
26363
+ }
26364
+ function createAuthRoutes() {
26365
+ const app2 = new Hono15();
26366
+ app2.post("/auth/login", async (c) => {
26367
+ const ip = getClientIp(c);
26368
+ if (isLockedOut2(ip)) {
26369
+ return c.json({ error: "Too many attempts, try again later" }, 429);
26370
+ }
26371
+ const body = await c.req.json().catch(() => ({ password: "" }));
26372
+ if (!body.password) {
26373
+ recordFailure2(ip);
26374
+ return c.json({ error: "Invalid password" }, 401);
26375
+ }
26376
+ const envPlain = passwordEnvPlaintext();
26377
+ const ok = envPlain != null ? constantTimeEqualString(body.password, envPlain) : await verifyPassword(body.password, passwordHash());
26378
+ if (!ok) {
26379
+ recordFailure2(ip);
26380
+ return c.json({ error: "Invalid password" }, 401);
26381
+ }
26382
+ clearFailures2(ip);
26383
+ const token = createToken();
26384
+ setTokenCookie(c, token);
26385
+ return c.json({ ok: true });
26386
+ });
26387
+ app2.get("/auth/check", (c) => {
26388
+ const token = getCookie(c, COOKIE_NAME);
26389
+ const payload = validateToken(token);
26390
+ if (payload) {
26391
+ if (shouldRefresh(payload)) {
26392
+ const newToken = createToken();
26393
+ setTokenCookie(c, newToken);
26394
+ }
26395
+ return c.json({ authenticated: true, badge: config.server.badge, goalModeEnabled: config.goalModeEnabled });
26396
+ }
26397
+ return c.json({ authenticated: false, badge: config.server.badge }, 401);
26398
+ });
26399
+ app2.post("/auth/logout", (c) => {
26400
+ setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
26401
+ return c.json({ ok: true });
26402
+ });
26403
+ app2.post("/auth/change-password", async (c) => {
26404
+ const body = await c.req.json().catch(() => ({}));
26405
+ const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
26406
+ const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
26407
+ if (passwordEnvPlaintext() != null) {
26408
+ return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
26409
+ }
26410
+ if (!await verifyPassword(oldPassword, passwordHash())) {
26411
+ return c.json({ error: "Incorrect current password" }, 401);
26412
+ }
26413
+ const weak = passwordStrengthError(newPassword);
26414
+ if (weak)
26415
+ return c.json({ error: weak }, 400);
26416
+ if (newPassword === oldPassword) {
26417
+ return c.json({ error: "New password must differ from the current password" }, 400);
26418
+ }
26419
+ updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
26420
+ return c.json({ ok: true });
26421
+ });
26422
+ return app2;
26423
+ }
26424
+ function passwordStrengthError(pw) {
26425
+ if (pw.length < 8)
26426
+ return "Password must be at least 8 characters";
26427
+ if (!/[A-Za-z]/.test(pw))
26428
+ return "Password must contain at least one letter";
26429
+ if (!/[0-9]/.test(pw))
26430
+ return "Password must contain at least one digit";
26431
+ return null;
26432
+ }
26433
+ function authMiddleware() {
26434
+ return async (c, next) => {
26435
+ const path74 = c.req.path;
26436
+ if (PUBLIC_PATHS.some((p) => path74 === p || path74.startsWith(p + "?"))) {
26437
+ return next();
26438
+ }
26439
+ if (EXTENSION_ASSET_PATH.test(path74)) {
26440
+ return next();
26441
+ }
26442
+ const token = getCookie(c, COOKIE_NAME);
26443
+ const payload = validateToken(token);
26444
+ if (!payload) {
26445
+ if (!path74.startsWith("/api/")) {
26446
+ return next();
26447
+ }
26448
+ return c.json({ error: "Unauthorized" }, 401);
26449
+ }
26450
+ if (shouldRefresh(payload)) {
26451
+ const newToken = createToken();
26452
+ setTokenCookie(c, newToken);
26453
+ }
26454
+ return next();
26455
+ };
26456
+ }
26457
+ var TOKEN_MAX_AGE, REFRESH_AFTER, COOKIE_NAME, ADMIN_LOGIN_BUCKET, isLockedOut2, recordFailure2, clearFailures2, PUBLIC_PATHS, EXTENSION_ASSET_PATH;
26458
+ var init_auth = __esm({
26459
+ "../server/dist/middleware/auth.js"() {
26460
+ "use strict";
26461
+ init_config();
26462
+ init_password_hash();
26463
+ init_setup_config();
26464
+ init_brute_force();
26465
+ TOKEN_MAX_AGE = config.auth.tokenMaxAge;
26466
+ REFRESH_AFTER = config.auth.refreshAfter;
26467
+ COOKIE_NAME = "halo_token";
26468
+ ADMIN_LOGIN_BUCKET = "admin-login";
26469
+ isLockedOut2 = (ip) => isLockedOut(ADMIN_LOGIN_BUCKET, ip);
26470
+ recordFailure2 = (ip) => recordFailure(ADMIN_LOGIN_BUCKET, ip);
26471
+ clearFailures2 = (ip) => clearFailures(ADMIN_LOGIN_BUCKET, ip);
26472
+ PUBLIC_PATHS = ["/api/auth/login", "/api/auth/check", "/api/auth/logout", "/api/health", "/api/web/chat", "/api/web/sessions", "/api/web/stop", "/api/web/history", "/api/web/subscribe", "/api/web/file", "/api/show/state", "/api/show/session", "/api/metrics"];
26473
+ EXTENSION_ASSET_PATH = /^\/api\/extensions\/[^/]+\/[^/]+\/[^/]+\/./;
26474
+ }
26475
+ });
26476
+
26477
+ // ../server/dist/routes/extensions.js
26478
+ import { Hono as Hono16 } from "hono";
26479
+ import fs56 from "node:fs";
26480
+ import path63 from "node:path";
26481
+ import { Readable as Readable2 } from "node:stream";
26482
+ function assetMime(fileName) {
26483
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
26484
+ return imageMimeFromExt(ext) ?? ASSET_MIME[ext] ?? "application/octet-stream";
26485
+ }
26486
+ function resolveAsset(id, version, rest) {
26487
+ if (!isExtensionId(id))
26488
+ return null;
26489
+ const info = getExtension(id);
26490
+ if (!info || info.version !== version)
26491
+ return null;
26492
+ const segments = rest.split("/");
26493
+ if (segments.some((s) => s === "" || s === "." || s === ".." || s.startsWith(".") || s.includes("\\")))
26494
+ return null;
26495
+ let dir;
26496
+ let real;
26497
+ try {
26498
+ dir = fs56.realpathSync(path63.join(extensionsRoot(), id));
26499
+ real = fs56.realpathSync(path63.join(dir, ...segments));
26500
+ } catch {
26501
+ return null;
26502
+ }
26503
+ if (real !== dir && !real.startsWith(dir + path63.sep))
26504
+ return null;
26505
+ try {
26506
+ if (!fs56.statSync(real).isFile())
26507
+ return null;
26508
+ } catch {
26509
+ return null;
26510
+ }
26511
+ return real;
26512
+ }
26513
+ function createExtensionRoutes() {
26514
+ const app2 = new Hono16();
26515
+ app2.get("/extensions", (c) => c.json(getSnapshot()));
26516
+ app2.get("/extensions/token", (c) => c.json({
26517
+ token: mintScopedToken(EXT_TOKEN_SCOPE, EXT_TOKEN_MAX_AGE),
26518
+ expiresAt: Date.now() + EXT_TOKEN_MAX_AGE * 1e3
26519
+ }));
26520
+ app2.post("/extensions/install", async (c) => {
26521
+ if (Number(c.req.header("content-length") ?? 0) > MAX_ZIP_BYTES)
26522
+ return c.json({ error: `zip exceeds ${MAX_ZIP_BYTES / 1024 / 1024} MB` }, 413);
26523
+ let file = null;
26524
+ try {
26525
+ const form = await c.req.formData();
26526
+ const f = form.get("file");
26527
+ file = f instanceof File ? f : null;
26528
+ } catch {
26529
+ return c.json({ error: "multipart form with a `file` field is required" }, 400);
26530
+ }
26531
+ if (!file)
26532
+ return c.json({ error: "`file` (zip) is required" }, 400);
26533
+ if (file.size > MAX_ZIP_BYTES)
26534
+ return c.json({ error: `zip exceeds ${MAX_ZIP_BYTES / 1024 / 1024} MB` }, 413);
26535
+ try {
26536
+ const info = await installExtensionZip(Buffer.from(await file.arrayBuffer()));
26537
+ rescanAndBroadcast();
26538
+ return c.json(info);
26539
+ } catch (err) {
26540
+ if (err instanceof ExtensionInstallError)
26541
+ return c.json({ error: err.message }, 400);
26542
+ const msg = err instanceof Error ? err.message : String(err);
26543
+ console.log(`[Extensions] install failed: ${msg}`);
26544
+ return c.json({ error: msg }, 500);
26545
+ }
26546
+ });
26547
+ app2.delete("/extensions/:id", (c) => {
26548
+ const id = c.req.param("id");
26549
+ if (!isExtensionId(id))
26550
+ return c.json({ error: "Invalid extension id" }, 400);
26551
+ try {
26552
+ if (!uninstallExtension(id))
26553
+ return c.json({ error: "Extension not installed" }, 404);
26554
+ rescanAndBroadcast();
26555
+ return c.json({ ok: true, id });
26556
+ } catch (err) {
26557
+ const msg = err instanceof Error ? err.message : String(err);
26558
+ console.log(`[Extensions] uninstall ${id} failed: ${msg}`);
26559
+ return c.json({ error: msg }, 500);
26560
+ }
26561
+ });
26562
+ app2.get("/extensions/:id/:version/:token/:rest{.+}", (c) => {
26563
+ if (!verifyScopedToken(c.req.param("token"), EXT_TOKEN_SCOPE))
26564
+ return c.json({ error: "Unauthorized" }, 401);
26565
+ const abs = resolveAsset(c.req.param("id"), c.req.param("version"), c.req.param("rest"));
26566
+ if (!abs)
26567
+ return c.json({ error: "Not found" }, 404);
26568
+ const stat = fs56.statSync(abs);
26569
+ const nodeStream = fs56.createReadStream(abs);
26570
+ c.req.raw.signal?.addEventListener("abort", () => nodeStream.destroy(), { once: true });
26571
+ return new Response(Readable2.toWeb(nodeStream), {
26572
+ headers: {
26573
+ "Content-Type": assetMime(abs),
26574
+ "Content-Length": String(stat.size),
26575
+ // The version segment changes on every upgrade, so the URL is a
26576
+ // content address as far as the browser is concerned.
26577
+ "Cache-Control": "public, max-age=31536000, immutable",
26578
+ // Module scripts / fetch / wasm / dynamic import are CORS-mode
26579
+ // requests; the iframe is same-origin today so this is a no-op, kept
26580
+ // so the assets keep loading if the host ever drops allow-same-origin.
26581
+ "Access-Control-Allow-Origin": "*",
26582
+ // The token is in the URL — keep it out of Referer headers.
26583
+ "Referrer-Policy": "no-referrer"
26584
+ }
26585
+ });
26586
+ });
26587
+ return app2;
26588
+ }
26589
+ var MAX_ZIP_BYTES, EXT_TOKEN_MAX_AGE, EXT_TOKEN_SCOPE, ASSET_MIME;
26590
+ var init_extensions = __esm({
26591
+ "../server/dist/routes/extensions.js"() {
26592
+ "use strict";
26593
+ init_dist();
26594
+ init_registry2();
26595
+ init_install();
26596
+ init_watcher();
26597
+ init_auth();
26598
+ MAX_ZIP_BYTES = 100 * 1024 * 1024;
26599
+ EXT_TOKEN_MAX_AGE = 24 * 60 * 60;
26600
+ EXT_TOKEN_SCOPE = "ext";
26601
+ ASSET_MIME = {
26602
+ html: "text/html; charset=utf-8",
26603
+ htm: "text/html; charset=utf-8",
26604
+ js: "text/javascript; charset=utf-8",
26605
+ mjs: "text/javascript; charset=utf-8",
26606
+ css: "text/css; charset=utf-8",
26607
+ json: "application/json; charset=utf-8",
26608
+ map: "application/json; charset=utf-8",
26609
+ wasm: "application/wasm",
26610
+ txt: "text/plain; charset=utf-8",
26611
+ md: "text/plain; charset=utf-8",
26612
+ woff: "font/woff",
26613
+ woff2: "font/woff2",
26614
+ ttf: "font/ttf",
26615
+ otf: "font/otf",
26616
+ hdr: "application/octet-stream",
26617
+ bin: "application/octet-stream"
26618
+ };
26619
+ }
26620
+ });
26621
+
26622
+ // ../server/dist/evolution/ticker.js
26623
+ import { eq as eq16, and as and12, or as or2, asc, isNull as isNull6, sql as sql2 } from "drizzle-orm";
26624
+ function setEvoSpawner(fn) {
26625
+ _spawner = fn;
26626
+ }
26627
+ function startEvoTicker() {
26628
+ if (_interval)
26629
+ return;
26630
+ _startupTimer2 = setTimeout(() => {
26631
+ _startupTimer2 = null;
26632
+ runTick().catch(logTickError);
26633
+ }, 2e3);
26634
+ _interval = setInterval(() => {
26635
+ runTick().catch(logTickError);
26636
+ }, TICK_INTERVAL_MS);
26637
+ console.log(`[EvoTicker] started (interval: ${TICK_INTERVAL_MS / 1e3}s)`);
26638
+ }
26639
+ function stopEvoTicker() {
26640
+ if (_startupTimer2) {
26641
+ clearTimeout(_startupTimer2);
26642
+ _startupTimer2 = null;
26643
+ }
26644
+ if (_interval) {
26645
+ clearInterval(_interval);
26646
+ _interval = null;
26647
+ console.log("[EvoTicker] stopped");
26648
+ }
26649
+ }
26650
+ function logTickError(err) {
26651
+ console.error(`[EvoTicker] tick failed: ${err instanceof Error ? err.message : String(err)}`);
26652
+ }
26653
+ async function runTick() {
26654
+ markTimeouts();
26655
+ startPendingRuns();
26656
+ startPendingApplies();
26657
+ broadcastChanges();
26658
+ }
26659
+ function broadcastChanges() {
26660
+ const db = getEvoDb();
26661
+ const inFlightLimit = (config.evolution.maxConcurrentRun + config.evolution.maxConcurrentApply) * 3;
26662
+ const runRows = db.select({ id: evolutionRuns.id, status: evolutionRuns.status }).from(evolutionRuns).where(or2(eq16(evolutionRuns.status, "pending"), eq16(evolutionRuns.status, "running"))).limit(inFlightLimit).all();
26663
+ for (const r of runRows) {
26664
+ if (_lastRunStatus.get(r.id) !== r.status) {
26665
+ _lastRunStatus.set(r.id, r.status);
26666
+ broadcast({ type: "evolution:run_changed", id: r.id, status: r.status });
26667
+ }
26668
+ }
26669
+ const applyRows = db.select({ id: evolutionApplies.id, status: evolutionApplies.status }).from(evolutionApplies).where(or2(eq16(evolutionApplies.status, "pending"), eq16(evolutionApplies.status, "running"), eq16(evolutionApplies.status, "syncing"))).limit(inFlightLimit).all();
26670
+ for (const a of applyRows) {
26671
+ if (_lastApplyStatus.get(a.id) !== a.status) {
26672
+ _lastApplyStatus.set(a.id, a.status);
26673
+ broadcast({ type: "evolution:apply_changed", id: a.id, status: a.status });
26674
+ }
26675
+ }
26676
+ const aliveRunIds = new Set(runRows.map((r) => r.id));
26677
+ for (const id of [..._lastRunStatus.keys()]) {
26678
+ if (aliveRunIds.has(id))
26679
+ continue;
26680
+ const row = db.select({ status: evolutionRuns.status }).from(evolutionRuns).where(eq16(evolutionRuns.id, id)).get();
26681
+ const finalStatus = row?.status;
26682
+ if (finalStatus && _lastRunStatus.get(id) !== finalStatus) {
26683
+ broadcast({ type: "evolution:run_changed", id, status: finalStatus });
26684
+ }
26685
+ _lastRunStatus.delete(id);
26686
+ }
26687
+ const aliveApplyIds = new Set(applyRows.map((r) => r.id));
26688
+ for (const id of [..._lastApplyStatus.keys()]) {
26689
+ if (aliveApplyIds.has(id))
26690
+ continue;
26691
+ const row = db.select({ status: evolutionApplies.status }).from(evolutionApplies).where(eq16(evolutionApplies.id, id)).get();
26692
+ const finalStatus = row?.status;
26693
+ if (finalStatus && _lastApplyStatus.get(id) !== finalStatus) {
26694
+ broadcast({ type: "evolution:apply_changed", id, status: finalStatus });
26695
+ }
26696
+ _lastApplyStatus.delete(id);
26697
+ }
26698
+ }
26699
+ function markTimeouts() {
26700
+ const db = getEvoDb();
26701
+ const now = Date.now();
26702
+ const runCutoff = now - config.evolution.runTimeoutMinutes * 6e4;
26703
+ const applyCutoff = now - config.evolution.applyTimeoutMinutes * 6e4;
25659
26704
  const maxAttempts = config.evolution.maxAttempts;
25660
26705
  const runRows = db.select({
25661
26706
  id: evolutionRuns.id,
@@ -25817,7 +26862,7 @@ var init_ticker = __esm({
25817
26862
  });
25818
26863
 
25819
26864
  // ../server/dist/evolution/spawn.js
25820
- import path61 from "node:path";
26865
+ import path64 from "node:path";
25821
26866
  import { spawn as spawn4 } from "node:child_process";
25822
26867
  import { fileURLToPath as fileURLToPath2 } from "node:url";
25823
26868
  var __dirname2, WRAPPER_PATH, realEvoSpawner;
@@ -25825,8 +26870,8 @@ var init_spawn = __esm({
25825
26870
  "../server/dist/evolution/spawn.js"() {
25826
26871
  "use strict";
25827
26872
  init_child_env();
25828
- __dirname2 = path61.dirname(fileURLToPath2(import.meta.url));
25829
- WRAPPER_PATH = path61.resolve(__dirname2, "evo-wrapper.js");
26873
+ __dirname2 = path64.dirname(fileURLToPath2(import.meta.url));
26874
+ WRAPPER_PATH = path64.resolve(__dirname2, "evo-wrapper.js");
25830
26875
  realEvoSpawner = (mode, id) => {
25831
26876
  const child = spawn4(process.execPath, [WRAPPER_PATH, `--mode=${mode}`, `--id=${id}`], {
25832
26877
  detached: true,
@@ -25868,7 +26913,7 @@ async function shutdownChannels() {
25868
26913
  _running.length = 0;
25869
26914
  }
25870
26915
  var _running;
25871
- var init_registry2 = __esm({
26916
+ var init_registry3 = __esm({
25872
26917
  "../server/dist/channels/registry.js"() {
25873
26918
  "use strict";
25874
26919
  _running = [];
@@ -26223,6 +27268,7 @@ var init_inbound = __esm({
26223
27268
  });
26224
27269
 
26225
27270
  // ../server/dist/channels/telegram/handler.js
27271
+ import path65 from "node:path";
26226
27272
  import { Bot, InputFile } from "grammy";
26227
27273
  function inferMediaKind(filePath) {
26228
27274
  const cls = classifyMedia(filePath);
@@ -26338,25 +27384,33 @@ function startTelegramChannel(deps) {
26338
27384
  const route = bridge.getRoute(sessionId);
26339
27385
  if (!route)
26340
27386
  return;
26341
- if (!isMediaPathAllowed(filePath, account.workspacePath)) {
26342
- console.warn(`[Telegram] sendMedia blocked: ${filePath} not under workspace`);
26343
- return;
26344
- }
26345
- const kind = inferMediaKind(filePath);
26346
- const file = new InputFile(filePath);
26347
- switch (kind) {
26348
- case "photo":
26349
- await bot.api.sendPhoto(route.chatId, file);
26350
- break;
26351
- case "video":
26352
- await bot.api.sendVideo(route.chatId, file);
26353
- break;
26354
- case "voice":
26355
- await bot.api.sendVoice(route.chatId, file);
26356
- break;
26357
- case "document":
26358
- await bot.api.sendDocument(route.chatId, file);
26359
- break;
27387
+ try {
27388
+ if (!isMediaPathAllowed(filePath, account.workspacePath, account.accessLevel)) {
27389
+ throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
27390
+ }
27391
+ const kind = inferMediaKind(filePath);
27392
+ const file = new InputFile(filePath);
27393
+ switch (kind) {
27394
+ case "photo":
27395
+ await bot.api.sendPhoto(route.chatId, file);
27396
+ break;
27397
+ case "video":
27398
+ await bot.api.sendVideo(route.chatId, file);
27399
+ break;
27400
+ case "voice":
27401
+ await bot.api.sendVoice(route.chatId, file);
27402
+ break;
27403
+ case "document":
27404
+ await bot.api.sendDocument(route.chatId, file);
27405
+ break;
27406
+ }
27407
+ } catch (err) {
27408
+ console.log(`[Telegram] sendMedia ${filePath} failed: ${err instanceof Error ? err.message : String(err)}`);
27409
+ await bot.api.sendMessage(route.chatId, t("handler.upload_failed", getLang(account), {
27410
+ name: path65.basename(filePath),
27411
+ error: err instanceof Error ? err.message : String(err)
27412
+ })).catch(() => {
27413
+ });
26360
27414
  }
26361
27415
  }
26362
27416
  })
@@ -26421,7 +27475,8 @@ async function runBot(args) {
26421
27475
  bot.command("start", async (ctx) => {
26422
27476
  await ctx.reply(t("handler.start_greeting", lang));
26423
27477
  });
26424
- for (const cmd of builtinCommandNames()) {
27478
+ for (const command of DISPATCH_COMMANDS) {
27479
+ const cmd = command.slice(1);
26425
27480
  bot.command(cmd, async (ctx) => {
26426
27481
  const cmdCtx = buildCmdCtx4(ctx.from?.id ?? 0, ctx.chat?.id);
26427
27482
  if (!cmdCtx) {
@@ -26635,9 +27690,9 @@ var init_handler3 = __esm({
26635
27690
  init_event_adapter();
26636
27691
  init_media_store();
26637
27692
  init_accounts();
27693
+ init_commands2();
26638
27694
  init_inbound();
26639
27695
  init_i18n();
26640
- init_commands();
26641
27696
  init_media();
26642
27697
  init_session_prefix();
26643
27698
  MAX_TG_DOWNLOAD_BYTES = 20 * 1024 * 1024;
@@ -26645,19 +27700,19 @@ var init_handler3 = __esm({
26645
27700
  });
26646
27701
 
26647
27702
  // ../server/dist/routes/telegram.js
26648
- import { Hono as Hono15 } from "hono";
27703
+ import { Hono as Hono17 } from "hono";
26649
27704
  import { Bot as Bot2 } from "grammy";
26650
- import fs53 from "node:fs";
27705
+ import fs57 from "node:fs";
26651
27706
  function createTelegramRoutes(deps) {
26652
27707
  const { db, channel } = deps;
26653
- const app2 = new Hono15();
27708
+ const app2 = new Hono17();
26654
27709
  app2.get("/telegram/accounts", (c) => {
26655
27710
  const accounts = listAccounts3(db).map((acc) => {
26656
27711
  return {
26657
27712
  accountId: acc.accountId,
26658
27713
  botUsername: acc.botUsername,
26659
27714
  workspacePath: acc.workspacePath,
26660
- workspaceMissing: !fs53.existsSync(acc.workspacePath),
27715
+ workspaceMissing: !fs57.existsSync(acc.workspacePath),
26661
27716
  label: acc.label,
26662
27717
  enabled: acc.enabled,
26663
27718
  accessLevel: acc.accessLevel,
@@ -26846,7 +27901,7 @@ var init_descriptor = __esm({
26846
27901
  });
26847
27902
 
26848
27903
  // ../server/dist/channels/wechat/api.js
26849
- import crypto3 from "node:crypto";
27904
+ import crypto4 from "node:crypto";
26850
27905
  function buildClientVersion(version) {
26851
27906
  const [ma, mi, pa] = version.split(".").map((p) => parseInt(p, 10));
26852
27907
  return (ma & 255) << 16 | (mi & 255) << 8 | pa & 255;
@@ -26855,7 +27910,7 @@ function buildBaseInfo() {
26855
27910
  return { channel_version: CHANNEL_VERSION };
26856
27911
  }
26857
27912
  function randomWechatUin() {
26858
- const n = crypto3.randomBytes(4).readUInt32BE(0);
27913
+ const n = crypto4.randomBytes(4).readUInt32BE(0);
26859
27914
  return Buffer.from(String(n), "utf-8").toString("base64");
26860
27915
  }
26861
27916
  function commonHeaders() {
@@ -26990,7 +28045,12 @@ async function sendMessage(params) {
26990
28045
  try {
26991
28046
  const parsed = JSON.parse(raw);
26992
28047
  if (parsed.ret !== void 0 && parsed.ret !== 0 || parsed.errcode !== void 0 && parsed.errcode !== 0) {
26993
- const hint = parsed.ret === -2 ? " (gateway rejected the message \u2014 payload too large (>16KB) or outbound sent without the context_token from the recipient's last inbound message)" : "";
28048
+ let hint = "";
28049
+ if (parsed.ret === -2) {
28050
+ const tokenAge = !params.body.msg?.context_token ? "context_token missing" : params.contextTokenAt ? `context_token age ${Math.round((Date.now() - params.contextTokenAt) / 6e4)}m` : "context_token age unknown";
28051
+ console.warn(`[WeChat:sendmessage] ret=-2 to=${params.body.msg?.to_user_id ?? ""} ${tokenAge}`);
28052
+ hint = ` (gateway rejected the message \u2014 payload too large (>16KB), no context_token, or context_token expired (only a new inbound message from the recipient refreshes it); ${tokenAge})`;
28053
+ }
26994
28054
  throw new Error(`[WeChat:sendmessage] gateway error ret=${parsed.ret} errcode=${parsed.errcode} ${parsed.errmsg ?? ""}${hint}`);
26995
28055
  }
26996
28056
  } catch (err) {
@@ -27259,14 +28319,18 @@ var init_event_adapter2 = __esm({
27259
28319
  try {
27260
28320
  await this.deps.sendText(text6);
27261
28321
  } catch (err) {
27262
- console.warn(`[WeChat] sendText failed: ${err instanceof Error ? err.message : String(err)}`);
28322
+ const msg = `sendText failed: ${err instanceof Error ? err.message : String(err)}`;
28323
+ console.warn(`[WeChat] ${msg}`);
28324
+ this.deps.onSendError?.(msg);
27263
28325
  }
27264
28326
  }
27265
28327
  for (const p of mediaPaths) {
27266
28328
  try {
27267
28329
  await this.deps.sendMedia(p);
27268
28330
  } catch (err) {
27269
- console.warn(`[WeChat] sendMedia ${p} failed: ${err instanceof Error ? err.message : String(err)}`);
28331
+ const msg = `sendMedia ${p} failed: ${err instanceof Error ? err.message : String(err)}`;
28332
+ console.warn(`[WeChat] ${msg}`);
28333
+ this.deps.onSendError?.(msg);
27270
28334
  }
27271
28335
  }
27272
28336
  }
@@ -27275,7 +28339,7 @@ var init_event_adapter2 = __esm({
27275
28339
  });
27276
28340
 
27277
28341
  // ../server/dist/channels/wechat/cdn.js
27278
- import { createDecipheriv, createCipheriv, randomBytes as randomBytes2, createHash as createHash2 } from "node:crypto";
28342
+ import { createDecipheriv, createCipheriv, randomBytes as randomBytes3, createHash as createHash2 } from "node:crypto";
27279
28343
  function decryptAesEcb(ciphertext, key) {
27280
28344
  const decipher = createDecipheriv("aes-128-ecb", key, null);
27281
28345
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
@@ -27322,8 +28386,8 @@ async function downloadPlain(params) {
27322
28386
  return fetchCdnBytes(url, params.label);
27323
28387
  }
27324
28388
  function prepareUpload(plaintext) {
27325
- const filekey = randomBytes2(16).toString("hex");
27326
- const aeskey = randomBytes2(16);
28389
+ const filekey = randomBytes3(16).toString("hex");
28390
+ const aeskey = randomBytes3(16);
27327
28391
  return {
27328
28392
  filekey,
27329
28393
  aeskey,
@@ -27381,8 +28445,8 @@ var init_cdn = __esm({
27381
28445
  });
27382
28446
 
27383
28447
  // ../server/dist/channels/wechat/send-media.js
27384
- import fs54 from "node:fs/promises";
27385
- import path62 from "node:path";
28448
+ import fs58 from "node:fs/promises";
28449
+ import path66 from "node:path";
27386
28450
  function inferKind(filePath) {
27387
28451
  const cls = classifyMedia(filePath);
27388
28452
  if (cls === "image")
@@ -27392,9 +28456,9 @@ function inferKind(filePath) {
27392
28456
  return "file";
27393
28457
  }
27394
28458
  async function sendMediaFile(params) {
27395
- const { baseUrl, token, toUserId, contextToken, filePath } = params;
28459
+ const { baseUrl, token, toUserId, contextToken, contextTokenAt, filePath } = params;
27396
28460
  const kind = params.kind ?? inferKind(filePath);
27397
- const plaintext = await fs54.readFile(filePath);
28461
+ const plaintext = await fs58.readFile(filePath);
27398
28462
  const prep = prepareUpload(plaintext);
27399
28463
  console.log(`[WeChat] sendMediaFile: path=${filePath} kind=${kind} rawsize=${prep.rawsize} filesize=${prep.filesize}`);
27400
28464
  const mediaType = kind === "image" ? UploadMediaType.IMAGE : kind === "video" ? UploadMediaType.VIDEO : UploadMediaType.FILE;
@@ -27432,7 +28496,7 @@ async function sendMediaFile(params) {
27432
28496
  aesKeyBase64,
27433
28497
  fileSizeCiphertext: prep.filesize,
27434
28498
  fileSize: prep.rawsize,
27435
- fileName: path62.basename(filePath)
28499
+ fileName: path66.basename(filePath)
27436
28500
  });
27437
28501
  const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
27438
28502
  const body = {
@@ -27446,7 +28510,7 @@ async function sendMediaFile(params) {
27446
28510
  context_token: contextToken
27447
28511
  }
27448
28512
  };
27449
- await sendMessage({ baseUrl, token, body });
28513
+ await sendMessage({ baseUrl, token, body, contextTokenAt });
27450
28514
  console.log(`[WeChat] sendMediaFile: sendMessage success clientId=${clientId}`);
27451
28515
  return { clientId };
27452
28516
  }
@@ -27595,14 +28659,16 @@ var init_login = __esm({
27595
28659
  });
27596
28660
 
27597
28661
  // ../server/dist/channels/wechat/handler.js
27598
- import fs55 from "node:fs";
27599
- import path63 from "node:path";
28662
+ import fs59 from "node:fs";
28663
+ import path67 from "node:path";
27600
28664
  import QRCode from "qrcode";
27601
- function wxRoute(fromUserId, contextToken) {
27602
- return (prev) => ({
27603
- fromUserId,
27604
- contextToken: contextToken ?? (prev?.fromUserId === fromUserId ? prev.contextToken : void 0)
27605
- });
28665
+ function wxRoute(fromUserId, contextToken, contextTokenAt) {
28666
+ return (prev) => {
28667
+ if (contextToken !== void 0)
28668
+ return { fromUserId, contextToken, contextTokenAt: contextTokenAt ?? Date.now() };
28669
+ const carry = prev?.fromUserId === fromUserId ? prev : void 0;
28670
+ return { fromUserId, contextToken: carry?.contextToken, contextTokenAt: carry?.contextTokenAt };
28671
+ };
27606
28672
  }
27607
28673
  function startWechatChannel(deps) {
27608
28674
  const { registry: registry2, db } = deps;
@@ -27630,24 +28696,29 @@ function startWechatChannel(deps) {
27630
28696
  const route = bridge.getRoute(sessionId);
27631
28697
  if (!route)
27632
28698
  return;
27633
- await sendToUser({ account, toUserId: route.fromUserId, text: chunk, contextToken: route.contextToken });
28699
+ await sendToUser({ account, toUserId: route.fromUserId, text: chunk, contextToken: route.contextToken, contextTokenAt: route.contextTokenAt });
27634
28700
  },
27635
28701
  sendMedia: async (filePath) => {
27636
28702
  const route = bridge.getRoute(sessionId);
27637
28703
  if (!route)
27638
28704
  return;
27639
- if (!isMediaPathAllowed(filePath, account.workspacePath)) {
27640
- console.warn(`[WeChat] sendMedia blocked: ${filePath} not under workspace`);
27641
- return;
28705
+ if (!isMediaPathAllowed(filePath, account.workspacePath, account.accessLevel)) {
28706
+ throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
27642
28707
  }
27643
28708
  await sendMediaFile({
27644
28709
  baseUrl: account.baseUrl,
27645
28710
  token: account.botToken,
27646
28711
  toUserId: route.fromUserId,
27647
28712
  contextToken: route.contextToken,
28713
+ contextTokenAt: route.contextTokenAt,
27648
28714
  filePath
27649
28715
  });
27650
- }
28716
+ },
28717
+ // A dropped reply used to be visible only in the server log. Record
28718
+ // it in the session so the user sees the turn ran but never landed.
28719
+ // appendNotification only writes the log row — it emits no event, so
28720
+ // nothing loops back into this responder as another send attempt.
28721
+ onSendError: (m) => registry2.getOrCreate(account.workspacePath).appendNotification(sessionId, `\u26A0\uFE0F WeChat delivery failed: ${m}`)
27651
28722
  })
27652
28723
  });
27653
28724
  const restartSelf = () => {
@@ -27690,7 +28761,9 @@ function startWechatChannel(deps) {
27690
28761
  }
27691
28762
  function restoreReplyRoutes(args) {
27692
28763
  const { registry: registry2, db, account, bridge } = args;
27693
- const tokens = getAccount(db, account.accountId)?.config.contextTokens ?? {};
28764
+ const config2 = getAccount(db, account.accountId)?.config ?? {};
28765
+ const tokens = config2.contextTokens ?? {};
28766
+ const ats = config2.contextTokenAts ?? {};
27694
28767
  const users = Object.keys(tokens);
27695
28768
  if (users.length === 0)
27696
28769
  return;
@@ -27703,7 +28776,7 @@ function restoreReplyRoutes(args) {
27703
28776
  workspacePath,
27704
28777
  bridge,
27705
28778
  sessionPrefix: buildWxSessionPrefix(userId),
27706
- route: wxRoute(userId, tokens[userId])
28779
+ route: wxRoute(userId, tokens[userId], ats[userId])
27707
28780
  });
27708
28781
  if (sid)
27709
28782
  console.log(`[WeChat] ${account.accountId} reply route restored for ${sid}`);
@@ -27887,6 +28960,7 @@ async function handleInbound(args) {
27887
28960
  account: storedAccount,
27888
28961
  toUserId: fromUserId,
27889
28962
  contextToken: msg.context_token,
28963
+ contextTokenAt: Date.now(),
27890
28964
  text: t("handler.workspace_missing", lang, { path: storedAccount.workspacePath })
27891
28965
  });
27892
28966
  return;
@@ -27935,7 +29009,7 @@ async function handleInbound(args) {
27935
29009
  lang,
27936
29010
  // If the session is currently compacting or mid-turn, send an immediate
27937
29011
  // hint so the WeChat user doesn't stare at a silent chat for 30+ seconds.
27938
- sendHint: (hint) => sendToUser({ account, toUserId: fromUserId, contextToken: msg.context_token, text: hint }),
29012
+ sendHint: (hint) => sendToUser({ account, toUserId: fromUserId, contextToken: msg.context_token, contextTokenAt: Date.now(), text: hint }),
27939
29013
  uiText: userText || "[\u4EC5\u56FE\u7247]",
27940
29014
  agentText: userText,
27941
29015
  userTag: fromUserId,
@@ -28009,7 +29083,7 @@ async function handleSlashCommand(args) {
28009
29083
  await reply(login.message);
28010
29084
  return true;
28011
29085
  }
28012
- const tmpPath = path63.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
29086
+ const tmpPath = path67.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
28013
29087
  await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
28014
29088
  await sendMediaFile({
28015
29089
  baseUrl: account.baseUrl,
@@ -28018,7 +29092,7 @@ async function handleSlashCommand(args) {
28018
29092
  contextToken,
28019
29093
  filePath: tmpPath
28020
29094
  });
28021
- await fs55.promises.unlink(tmpPath).catch(() => {
29095
+ await fs59.promises.unlink(tmpPath).catch(() => {
28022
29096
  });
28023
29097
  await reply(t("wechat.qr_sent", lang, { level, path: account.workspacePath }));
28024
29098
  waitLogin({ sessionKey: login.sessionKey }).then(async (result2) => {
@@ -28061,7 +29135,7 @@ async function sendToUser(params) {
28061
29135
  context_token: params.contextToken
28062
29136
  }
28063
29137
  };
28064
- await sendMessage({ baseUrl: params.account.baseUrl, token: params.account.botToken, body });
29138
+ await sendMessage({ baseUrl: params.account.baseUrl, token: params.account.botToken, body, contextTokenAt: params.contextTokenAt });
28065
29139
  }
28066
29140
  function sleep2(ms, signal) {
28067
29141
  return new Promise((resolve2, reject) => {
@@ -28095,20 +29169,20 @@ var init_handler4 = __esm({
28095
29169
  RETRY_DELAY_MS = 2e3;
28096
29170
  }
28097
29171
  });
28098
-
28099
- // ../server/dist/routes/wechat.js
28100
- import { Hono as Hono16 } from "hono";
28101
- import path64 from "node:path";
28102
- import fs56 from "node:fs";
29172
+
29173
+ // ../server/dist/routes/wechat.js
29174
+ import { Hono as Hono18 } from "hono";
29175
+ import path68 from "node:path";
29176
+ import fs60 from "node:fs";
28103
29177
  function createWechatRoutes(deps) {
28104
29178
  const { db, channel } = deps;
28105
- const app2 = new Hono16();
29179
+ const app2 = new Hono18();
28106
29180
  app2.get("/wechat/accounts", (c) => {
28107
29181
  const accounts = listAccounts4(db).map((acc) => {
28108
29182
  return {
28109
29183
  ...stripSecrets(acc),
28110
29184
  workspacePath: acc.workspacePath,
28111
- workspaceMissing: !fs56.existsSync(acc.workspacePath)
29185
+ workspaceMissing: !fs60.existsSync(acc.workspacePath)
28112
29186
  };
28113
29187
  });
28114
29188
  return c.json({ accounts });
@@ -28124,7 +29198,7 @@ function createWechatRoutes(deps) {
28124
29198
  return c.json({ error: "sessionKey required" }, 400);
28125
29199
  if (!body.workspacePath)
28126
29200
  return c.json({ error: "workspacePath required" }, 400);
28127
- if (!path64.isAbsolute(body.workspacePath))
29201
+ if (!path68.isAbsolute(body.workspacePath))
28128
29202
  return c.json({ error: "workspacePath must be absolute" }, 400);
28129
29203
  const levelError = accessLevelError(body.accessLevel, CHAT_ACCESS_LEVELS);
28130
29204
  if (levelError)
@@ -28231,7 +29305,11 @@ function readContextToken(accountId, userId) {
28231
29305
  const acct = getAccount(getChannelDb(), accountId);
28232
29306
  const tokens = acct?.config?.contextTokens;
28233
29307
  const v = tokens?.[userId];
28234
- return typeof v === "string" && v.length > 0 ? v : void 0;
29308
+ if (typeof v !== "string" || v.length === 0)
29309
+ return {};
29310
+ const ats = acct?.config?.contextTokenAts;
29311
+ const at = ats?.[userId];
29312
+ return { token: v, at: typeof at === "number" ? at : void 0 };
28235
29313
  }
28236
29314
  async function dispatch2(accountId, text6, explicitChatId, media) {
28237
29315
  const acct = getAccount4(getChannelDb(), accountId);
@@ -28244,12 +29322,12 @@ async function dispatch2(accountId, text6, explicitChatId, media) {
28244
29322
  throw new Error("no wechat target \u2014 bind the account first (the QR-login owner becomes the default cron recipient)");
28245
29323
  }
28246
29324
  const out = [];
28247
- const contextToken = readContextToken(accountId, chatId);
29325
+ const { token: contextToken, at: contextTokenAt } = readContextToken(accountId, chatId);
28248
29326
  if (text6) {
28249
29327
  const chunks = splitText(text6, WECHAT_TEXT_LIMIT);
28250
29328
  for (const [i, chunk] of chunks.entries()) {
28251
29329
  try {
28252
- await sendToUser({ account: acct, toUserId: chatId, text: chunk, contextToken });
29330
+ await sendToUser({ account: acct, toUserId: chatId, text: chunk, contextToken, contextTokenAt });
28253
29331
  } catch (err) {
28254
29332
  throw new Error(`chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`);
28255
29333
  }
@@ -28268,6 +29346,7 @@ async function dispatch2(accountId, text6, explicitChatId, media) {
28268
29346
  token: acct.botToken,
28269
29347
  toUserId: chatId,
28270
29348
  contextToken,
29349
+ contextTokenAt,
28271
29350
  filePath
28272
29351
  });
28273
29352
  out.push({ channelType: "wechat", accountId, chatId, ok: true });
@@ -28334,20 +29413,20 @@ var init_descriptor2 = __esm({
28334
29413
  });
28335
29414
 
28336
29415
  // ../server/dist/routes/web.js
28337
- import { Hono as Hono17 } from "hono";
29416
+ import { Hono as Hono19 } from "hono";
28338
29417
  import { streamSSE } from "hono/streaming";
28339
- import crypto4 from "node:crypto";
28340
- import fs57 from "node:fs";
29418
+ import crypto5 from "node:crypto";
29419
+ import fs61 from "node:fs";
28341
29420
  function createWebRoutes(deps) {
28342
29421
  const { db, channel } = deps;
28343
- const app2 = new Hono17();
29422
+ const app2 = new Hono19();
28344
29423
  app2.get("/web/accounts", (c) => {
28345
29424
  const accounts = listAccounts2(db).map((acc) => {
28346
29425
  return {
28347
29426
  accountId: acc.accountId,
28348
29427
  token: acc.token,
28349
29428
  workspacePath: acc.workspacePath,
28350
- workspaceMissing: !fs57.existsSync(acc.workspacePath),
29429
+ workspaceMissing: !fs61.existsSync(acc.workspacePath),
28351
29430
  label: acc.label,
28352
29431
  enabled: acc.enabled,
28353
29432
  accessLevel: acc.accessLevel,
@@ -28368,8 +29447,8 @@ function createWebRoutes(deps) {
28368
29447
  const wsError = validateWorkspaceBody(body.workspacePath);
28369
29448
  if (wsError)
28370
29449
  return c.json({ error: wsError }, 400);
28371
- const accountId = crypto4.randomUUID().slice(0, 8);
28372
- const token = crypto4.randomBytes(24).toString("base64url");
29450
+ const accountId = crypto5.randomUUID().slice(0, 8);
29451
+ const token = crypto5.randomBytes(24).toString("base64url");
28373
29452
  insertAccount2(db, {
28374
29453
  accountId,
28375
29454
  token,
@@ -28530,30 +29609,30 @@ function createWebRoutes(deps) {
28530
29609
  const filePath = c.req.query("path");
28531
29610
  if (!filePath)
28532
29611
  return c.json({ error: "path required" }, 400);
28533
- const path70 = await import("node:path");
28534
- const resolved = path70.default.resolve(account.workspacePath, filePath);
28535
- const root = path70.default.resolve(account.workspacePath);
28536
- if (resolved !== root && !resolved.startsWith(root + path70.default.sep)) {
29612
+ const path74 = await import("node:path");
29613
+ const resolved = path74.default.resolve(account.workspacePath, filePath);
29614
+ const root = path74.default.resolve(account.workspacePath);
29615
+ if (resolved !== root && !resolved.startsWith(root + path74.default.sep)) {
28537
29616
  return c.json({ error: "path traversal not allowed" }, 403);
28538
29617
  }
28539
29618
  let real;
28540
29619
  let realRoot;
28541
29620
  try {
28542
- real = fs57.realpathSync(resolved);
28543
- realRoot = fs57.realpathSync(root);
29621
+ real = fs61.realpathSync(resolved);
29622
+ realRoot = fs61.realpathSync(root);
28544
29623
  } catch {
28545
29624
  return c.json({ error: "file not found" }, 404);
28546
29625
  }
28547
- if (real !== realRoot && !real.startsWith(realRoot + path70.default.sep)) {
29626
+ if (real !== realRoot && !real.startsWith(realRoot + path74.default.sep)) {
28548
29627
  return c.json({ error: "path traversal not allowed" }, 403);
28549
29628
  }
28550
29629
  if (isHiddenWorkspacePath(real, realRoot)) {
28551
29630
  return c.json({ error: "path is not accessible" }, 403);
28552
29631
  }
28553
- const stat = fs57.statSync(real);
29632
+ const stat = fs61.statSync(real);
28554
29633
  if (stat.isDirectory())
28555
29634
  return c.json({ error: "cannot serve directory" }, 400);
28556
- const ext = path70.default.extname(real).toLowerCase();
29635
+ const ext = path74.default.extname(real).toLowerCase();
28557
29636
  const nonImageMimes = {
28558
29637
  ".mp4": "video/mp4",
28559
29638
  ".webm": "video/webm",
@@ -28561,8 +29640,8 @@ function createWebRoutes(deps) {
28561
29640
  ".txt": "text/plain"
28562
29641
  };
28563
29642
  const contentType = imageMimeFromExt(ext) || nonImageMimes[ext] || "application/octet-stream";
28564
- const fileName = path70.default.basename(resolved);
28565
- const data = fs57.readFileSync(real);
29643
+ const fileName = path74.default.basename(resolved);
29644
+ const data = fs61.readFileSync(real);
28566
29645
  c.header("content-type", contentType);
28567
29646
  c.header("content-disposition", `inline; filename="${fileName}"`);
28568
29647
  return c.body(data);
@@ -28893,10 +29972,10 @@ async function authTest(botToken) {
28893
29972
  return slackPost("auth.test", botToken, {});
28894
29973
  }
28895
29974
  async function uploadFile(args) {
28896
- const fs64 = await import("node:fs");
28897
- const path70 = await import("node:path");
28898
- const filename = args.filename ?? path70.basename(args.filePath);
28899
- const stat = fs64.statSync(args.filePath);
29975
+ const fs68 = await import("node:fs");
29976
+ const path74 = await import("node:path");
29977
+ const filename = args.filename ?? path74.basename(args.filePath);
29978
+ const stat = fs68.statSync(args.filePath);
28900
29979
  if (!stat.isFile())
28901
29980
  throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
28902
29981
  const length = stat.size;
@@ -28914,7 +29993,7 @@ async function uploadFile(args) {
28914
29993
  if (step1Parsed.ok === false || !step1Parsed.upload_url || !step1Parsed.file_id) {
28915
29994
  throw new Error(`[slack:files.getUploadURLExternal] ${step1Parsed.error ?? "unknown"}`);
28916
29995
  }
28917
- const buf = fs64.readFileSync(args.filePath);
29996
+ const buf = fs68.readFileSync(args.filePath);
28918
29997
  const step2 = await fetch(step1Parsed.upload_url, {
28919
29998
  method: "POST",
28920
29999
  body: buf
@@ -29059,7 +30138,7 @@ var init_api2 = __esm({
29059
30138
  });
29060
30139
 
29061
30140
  // ../server/dist/channels/slack/handler.js
29062
- import path65 from "node:path";
30141
+ import path69 from "node:path";
29063
30142
  import { WebSocket } from "ws";
29064
30143
  function slashToBang(text6, commandNames) {
29065
30144
  if (commandNames.length === 0)
@@ -29190,12 +30269,11 @@ function startSlackChannel(deps) {
29190
30269
  const route = bridge.getRoute(sessionId);
29191
30270
  if (!account || !route)
29192
30271
  return;
29193
- const resolved = path65.resolve(filePath);
29194
- if (!isMediaPathAllowed(resolved, account.workspacePath)) {
29195
- console.warn(`[Slack] sendMedia blocked: ${filePath} not under workspace`);
29196
- return;
29197
- }
30272
+ const resolved = path69.resolve(filePath);
29198
30273
  try {
30274
+ if (!isMediaPathAllowed(resolved, account.workspacePath, account.accessLevel)) {
30275
+ throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
30276
+ }
29199
30277
  await uploadFile({
29200
30278
  botToken: account.botToken,
29201
30279
  channel: route.channelId,
@@ -29209,7 +30287,7 @@ function startSlackChannel(deps) {
29209
30287
  channel: route.channelId,
29210
30288
  threadTs: route.replyTs,
29211
30289
  text: t("handler.upload_failed", getLang(account), {
29212
- name: path65.basename(filePath),
30290
+ name: path69.basename(filePath),
29213
30291
  error: err instanceof Error ? err.message : String(err)
29214
30292
  })
29215
30293
  }).catch(() => {
@@ -29471,11 +30549,11 @@ var init_handler5 = __esm({
29471
30549
  });
29472
30550
 
29473
30551
  // ../server/dist/routes/slack.js
29474
- import { Hono as Hono18 } from "hono";
29475
- import fs58 from "node:fs";
30552
+ import { Hono as Hono20 } from "hono";
30553
+ import fs62 from "node:fs";
29476
30554
  function createSlackRoutes(deps) {
29477
30555
  const { db, channel } = deps;
29478
- const app2 = new Hono18();
30556
+ const app2 = new Hono20();
29479
30557
  app2.get("/slack/accounts/:id/search", async (c) => {
29480
30558
  const id = c.req.param("id");
29481
30559
  const q = c.req.query("q") ?? "";
@@ -29495,7 +30573,7 @@ function createSlackRoutes(deps) {
29495
30573
  botUserId: a.botUserId,
29496
30574
  teamId: a.teamId,
29497
30575
  workspacePath: a.workspacePath,
29498
- workspaceMissing: !fs58.existsSync(a.workspacePath),
30576
+ workspaceMissing: !fs62.existsSync(a.workspacePath),
29499
30577
  label: a.label,
29500
30578
  enabled: a.enabled,
29501
30579
  accessLevel: a.accessLevel,
@@ -30014,10 +31092,10 @@ async function replyMessage(args) {
30014
31092
  return { message_id: parsed.data?.message_id ?? "" };
30015
31093
  }
30016
31094
  async function uploadImage(args) {
30017
- const fs64 = await import("node:fs");
30018
- const path70 = await import("node:path");
30019
- const buf = fs64.readFileSync(args.filePath);
30020
- const filename = path70.basename(args.filePath);
31095
+ const fs68 = await import("node:fs");
31096
+ const path74 = await import("node:path");
31097
+ const buf = fs68.readFileSync(args.filePath);
31098
+ const filename = path74.basename(args.filePath);
30021
31099
  const send = async (token2) => {
30022
31100
  const form = new FormData();
30023
31101
  form.append("image_type", "message");
@@ -30048,10 +31126,10 @@ async function uploadImage(args) {
30048
31126
  return { imageKey: parsed.data.image_key };
30049
31127
  }
30050
31128
  async function uploadFile2(args) {
30051
- const fs64 = await import("node:fs");
30052
- const path70 = await import("node:path");
30053
- const buf = fs64.readFileSync(args.filePath);
30054
- const filename = args.filename ?? path70.basename(args.filePath);
31129
+ const fs68 = await import("node:fs");
31130
+ const path74 = await import("node:path");
31131
+ const buf = fs68.readFileSync(args.filePath);
31132
+ const filename = args.filename ?? path74.basename(args.filePath);
30055
31133
  const send = async (token2) => {
30056
31134
  const form = new FormData();
30057
31135
  form.append("file_type", args.fileType);
@@ -30157,7 +31235,7 @@ var init_api3 = __esm({
30157
31235
  });
30158
31236
 
30159
31237
  // ../server/dist/channels/feishu/handler.js
30160
- import path66 from "node:path";
31238
+ import path70 from "node:path";
30161
31239
  import * as Lark from "@larksuiteoapi/node-sdk";
30162
31240
  function buildSessionPrefixForThread2(chatId, rootId) {
30163
31241
  return sessionPrefix("feishu", `${chatId}:${rootId}`);
@@ -30360,12 +31438,11 @@ function startFeishuChannel(deps) {
30360
31438
  const route = bridge.getRoute(sessionId);
30361
31439
  if (!account || !route)
30362
31440
  return;
30363
- const resolved = path66.resolve(filePath);
30364
- if (!isMediaPathAllowed(resolved, account.workspacePath)) {
30365
- console.warn(`[Feishu] sendMedia blocked: ${filePath} not under workspace`);
30366
- return;
30367
- }
31441
+ const resolved = path70.resolve(filePath);
30368
31442
  try {
31443
+ if (!isMediaPathAllowed(resolved, account.workspacePath, account.accessLevel)) {
31444
+ throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
31445
+ }
30369
31446
  await sendFeishuMedia({ account, ...route, filePath: resolved });
30370
31447
  } catch (err) {
30371
31448
  console.log(`[Feishu] sendMedia ${filePath} failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -30373,7 +31450,7 @@ function startFeishuChannel(deps) {
30373
31450
  account,
30374
31451
  ...route,
30375
31452
  text: t("handler.upload_failed", getLang(account), {
30376
- name: path66.basename(filePath),
31453
+ name: path70.basename(filePath),
30377
31454
  error: err instanceof Error ? err.message : String(err)
30378
31455
  })
30379
31456
  }).catch(() => {
@@ -30543,7 +31620,7 @@ async function replyToInbound(args) {
30543
31620
  async function sendFeishuMedia(args) {
30544
31621
  const { account, inboundMessageId, isP2P, chatId, filePath } = args;
30545
31622
  const cls = classifyMedia(filePath);
30546
- const ext = path66.extname(filePath).toLowerCase();
31623
+ const ext = path70.extname(filePath).toLowerCase();
30547
31624
  if (cls === "image") {
30548
31625
  const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
30549
31626
  await replyToInboundRaw({
@@ -30670,11 +31747,11 @@ var init_handler6 = __esm({
30670
31747
  });
30671
31748
 
30672
31749
  // ../server/dist/routes/feishu.js
30673
- import { Hono as Hono19 } from "hono";
30674
- import fs59 from "node:fs";
31750
+ import { Hono as Hono21 } from "hono";
31751
+ import fs63 from "node:fs";
30675
31752
  function createFeishuRoutes(deps) {
30676
31753
  const { db, channel } = deps;
30677
- const app2 = new Hono19();
31754
+ const app2 = new Hono21();
30678
31755
  app2.get("/feishu/accounts/:id/search", async (c) => {
30679
31756
  const id = c.req.param("id");
30680
31757
  const q = c.req.query("q") ?? "";
@@ -30695,7 +31772,7 @@ function createFeishuRoutes(deps) {
30695
31772
  botOpenId: a.botOpenId,
30696
31773
  hasEncryptKey: !!a.encryptKey,
30697
31774
  workspacePath: a.workspacePath,
30698
- workspaceMissing: !fs59.existsSync(a.workspacePath),
31775
+ workspaceMissing: !fs63.existsSync(a.workspacePath),
30699
31776
  label: a.label,
30700
31777
  enabled: a.enabled,
30701
31778
  accessLevel: a.accessLevel,
@@ -31068,8 +32145,8 @@ var init_event_adapter5 = __esm({
31068
32145
  });
31069
32146
 
31070
32147
  // ../server/dist/channels/wecom/handler.js
31071
- import path67 from "node:path";
31072
- import fs60 from "node:fs/promises";
32148
+ import path71 from "node:path";
32149
+ import fs64 from "node:fs/promises";
31073
32150
  import { WSClient as WSClient2, generateReqId } from "@wecom/aibot-node-sdk";
31074
32151
  function pickConversation(body) {
31075
32152
  const userid = body.from?.userid ?? "unknown";
@@ -31196,13 +32273,13 @@ async function replyText(wsClient, route, text6) {
31196
32273
  }
31197
32274
  async function sendWecomMedia(args) {
31198
32275
  const { wsClient, route, filePath } = args;
31199
- const ext = path67.extname(filePath).toLowerCase();
32276
+ const ext = path71.extname(filePath).toLowerCase();
31200
32277
  const cls = classifyMedia(filePath);
31201
32278
  const type = cls === "image" && [".png", ".jpg", ".jpeg", ".gif"].includes(ext) ? "image" : cls === "video" && ext === ".mp4" ? "video" : "file";
31202
- const buf = await fs60.readFile(filePath);
32279
+ const buf = await fs64.readFile(filePath);
31203
32280
  if (buf.length > MAX_WECOM_DOWNLOAD_BYTES)
31204
32281
  throw new Error("file exceeds 20MB");
31205
- const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path67.basename(filePath) });
32282
+ const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path71.basename(filePath) });
31206
32283
  await wsClient.replyMedia({ headers: { req_id: route.reqId } }, type, mediaId);
31207
32284
  }
31208
32285
  function startWecomChannel(deps) {
@@ -31226,17 +32303,16 @@ function startWecomChannel(deps) {
31226
32303
  const wsClient = states.get(accountId)?.wsClient;
31227
32304
  if (!account || !route || !wsClient)
31228
32305
  return;
31229
- const resolved = path67.resolve(filePath);
31230
- if (!isMediaPathAllowed(resolved, account.workspacePath)) {
31231
- console.warn(`[WeCom] sendMedia blocked: ${filePath} not under workspace`);
31232
- return;
31233
- }
32306
+ const resolved = path71.resolve(filePath);
31234
32307
  try {
32308
+ if (!isMediaPathAllowed(resolved, account.workspacePath, account.accessLevel)) {
32309
+ throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
32310
+ }
31235
32311
  await sendWecomMedia({ wsClient, route, filePath: resolved });
31236
32312
  } catch (err) {
31237
32313
  console.log(`[WeCom] sendMedia ${filePath} failed: ${err instanceof Error ? err.message : String(err)}`);
31238
32314
  await replyText(wsClient, route, t("handler.upload_failed", getLang(account), {
31239
- name: path67.basename(filePath),
32315
+ name: path71.basename(filePath),
31240
32316
  error: err instanceof Error ? err.message : String(err)
31241
32317
  })).catch(() => {
31242
32318
  });
@@ -31453,17 +32529,17 @@ var init_handler7 = __esm({
31453
32529
  });
31454
32530
 
31455
32531
  // ../server/dist/routes/wecom.js
31456
- import { Hono as Hono20 } from "hono";
31457
- import fs61 from "node:fs";
32532
+ import { Hono as Hono22 } from "hono";
32533
+ import fs65 from "node:fs";
31458
32534
  function createWecomRoutes(deps) {
31459
32535
  const { db, channel } = deps;
31460
- const app2 = new Hono20();
32536
+ const app2 = new Hono22();
31461
32537
  app2.get("/wecom/accounts", (c) => {
31462
32538
  const accounts = listAccounts7(db).map((a) => ({
31463
32539
  accountId: a.accountId,
31464
32540
  botId: a.botId,
31465
32541
  workspacePath: a.workspacePath,
31466
- workspaceMissing: !fs61.existsSync(a.workspacePath),
32542
+ workspaceMissing: !fs65.existsSync(a.workspacePath),
31467
32543
  label: a.label,
31468
32544
  enabled: a.enabled,
31469
32545
  accessLevel: a.accessLevel,
@@ -31657,235 +32733,25 @@ var init_descriptors = __esm({
31657
32733
  }
31658
32734
  });
31659
32735
 
31660
- // ../server/dist/middleware/auth.js
31661
- import { Hono as Hono21 } from "hono";
31662
- import { getCookie, setCookie } from "hono/cookie";
31663
- import crypto5 from "node:crypto";
31664
- function passwordHash() {
31665
- return config.server.password ?? "";
31666
- }
31667
- function passwordEnvPlaintext() {
31668
- return config.server.passwordEnvPlaintext;
31669
- }
31670
- function jwtSecret() {
31671
- const secret = config.server.jwtSecret ?? "";
31672
- if (!secret) {
31673
- throw new Error("jwt_secret missing \u2014 run `halo setup` to initialize");
31674
- }
31675
- return Buffer.from(secret, "base64");
31676
- }
31677
- function base64url(data) {
31678
- const buf = typeof data === "string" ? Buffer.from(data) : data;
31679
- return buf.toString("base64url");
31680
- }
31681
- function jwtSign(payload) {
31682
- const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
31683
- const body = base64url(JSON.stringify(payload));
31684
- const sig = crypto5.createHmac("sha256", jwtSecret()).update(`${header}.${body}`).digest();
31685
- return `${header}.${body}.${base64url(sig)}`;
31686
- }
31687
- function jwtVerify(token) {
31688
- const parts = token.split(".");
31689
- if (parts.length !== 3)
31690
- return null;
31691
- const [header, body, sig] = parts;
31692
- let secret;
31693
- try {
31694
- secret = jwtSecret();
31695
- } catch {
31696
- return null;
31697
- }
31698
- const expected = base64url(crypto5.createHmac("sha256", secret).update(`${header}.${body}`).digest());
31699
- if (sig !== expected)
31700
- return null;
31701
- try {
31702
- const payload = JSON.parse(Buffer.from(body, "base64url").toString());
31703
- if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp)
31704
- return null;
31705
- return payload;
31706
- } catch {
31707
- return null;
31708
- }
31709
- }
31710
- function createToken() {
31711
- const now = Math.floor(Date.now() / 1e3);
31712
- return jwtSign({ iat: now, exp: now + TOKEN_MAX_AGE });
31713
- }
31714
- function constantTimeEqualString(a, b) {
31715
- const ab = Buffer.from(a, "utf-8");
31716
- const bb = Buffer.from(b, "utf-8");
31717
- const max = Math.max(ab.length, bb.length);
31718
- const ap = Buffer.alloc(max);
31719
- ab.copy(ap);
31720
- const bp = Buffer.alloc(max);
31721
- bb.copy(bp);
31722
- const eq17 = crypto5.timingSafeEqual(ap, bp);
31723
- return eq17 && ab.length === bb.length;
31724
- }
31725
- function validateToken(token) {
31726
- if (!token)
31727
- return null;
31728
- return jwtVerify(token);
31729
- }
31730
- function shouldRefresh(payload) {
31731
- const iat = payload.iat;
31732
- if (!iat)
31733
- return true;
31734
- return Date.now() / 1e3 - iat > REFRESH_AFTER;
31735
- }
31736
- function isAuthDisabled() {
31737
- return passwordHash().length === 0 && passwordEnvPlaintext() == null;
31738
- }
31739
- function isAuthenticated(cookie) {
31740
- if (isAuthDisabled())
31741
- return true;
31742
- return validateToken(cookie) !== null;
31743
- }
31744
- function getTokenFromCookieHeader(cookieHeader) {
31745
- if (!cookieHeader)
31746
- return void 0;
31747
- const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
31748
- return match?.[1];
31749
- }
31750
- function setTokenCookie(c, token) {
31751
- setCookie(c, COOKIE_NAME, token, {
31752
- path: "/",
31753
- // WS upgrade requests carry cookies on the underlying HTTP handshake;
31754
- // the server reads them via the Cookie header, never via JS — so the
31755
- // cookie can stay httpOnly to avoid XSS exfiltration.
31756
- httpOnly: true,
31757
- maxAge: TOKEN_MAX_AGE,
31758
- sameSite: "Lax"
31759
- });
31760
- }
31761
- function createAuthRoutes() {
31762
- const app2 = new Hono21();
31763
- app2.post("/auth/login", async (c) => {
31764
- const ip = getClientIp(c);
31765
- if (isLockedOut2(ip)) {
31766
- return c.json({ error: "Too many attempts, try again later" }, 429);
31767
- }
31768
- const body = await c.req.json().catch(() => ({ password: "" }));
31769
- if (!body.password) {
31770
- recordFailure2(ip);
31771
- return c.json({ error: "Invalid password" }, 401);
31772
- }
31773
- const envPlain = passwordEnvPlaintext();
31774
- const ok = envPlain != null ? constantTimeEqualString(body.password, envPlain) : await verifyPassword(body.password, passwordHash());
31775
- if (!ok) {
31776
- recordFailure2(ip);
31777
- return c.json({ error: "Invalid password" }, 401);
31778
- }
31779
- clearFailures2(ip);
31780
- const token = createToken();
31781
- setTokenCookie(c, token);
31782
- return c.json({ ok: true });
31783
- });
31784
- app2.get("/auth/check", (c) => {
31785
- const token = getCookie(c, COOKIE_NAME);
31786
- const payload = validateToken(token);
31787
- if (payload) {
31788
- if (shouldRefresh(payload)) {
31789
- const newToken = createToken();
31790
- setTokenCookie(c, newToken);
31791
- }
31792
- return c.json({ authenticated: true, badge: config.server.badge });
31793
- }
31794
- return c.json({ authenticated: false, badge: config.server.badge }, 401);
31795
- });
31796
- app2.post("/auth/logout", (c) => {
31797
- setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
31798
- return c.json({ ok: true });
31799
- });
31800
- app2.post("/auth/change-password", async (c) => {
31801
- const body = await c.req.json().catch(() => ({}));
31802
- const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
31803
- const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
31804
- if (passwordEnvPlaintext() != null) {
31805
- return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
31806
- }
31807
- if (!await verifyPassword(oldPassword, passwordHash())) {
31808
- return c.json({ error: "Incorrect current password" }, 401);
31809
- }
31810
- const weak = passwordStrengthError(newPassword);
31811
- if (weak)
31812
- return c.json({ error: weak }, 400);
31813
- if (newPassword === oldPassword) {
31814
- return c.json({ error: "New password must differ from the current password" }, 400);
31815
- }
31816
- updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
31817
- return c.json({ ok: true });
31818
- });
31819
- return app2;
31820
- }
31821
- function passwordStrengthError(pw) {
31822
- if (pw.length < 8)
31823
- return "Password must be at least 8 characters";
31824
- if (!/[A-Za-z]/.test(pw))
31825
- return "Password must contain at least one letter";
31826
- if (!/[0-9]/.test(pw))
31827
- return "Password must contain at least one digit";
31828
- return null;
31829
- }
31830
- function authMiddleware() {
31831
- return async (c, next) => {
31832
- const path70 = c.req.path;
31833
- if (PUBLIC_PATHS.some((p) => path70 === p || path70.startsWith(p + "?"))) {
31834
- return next();
31835
- }
31836
- const token = getCookie(c, COOKIE_NAME);
31837
- const payload = validateToken(token);
31838
- if (!payload) {
31839
- if (!path70.startsWith("/api/")) {
31840
- return next();
31841
- }
31842
- return c.json({ error: "Unauthorized" }, 401);
31843
- }
31844
- if (shouldRefresh(payload)) {
31845
- const newToken = createToken();
31846
- setTokenCookie(c, newToken);
31847
- }
31848
- return next();
31849
- };
31850
- }
31851
- var TOKEN_MAX_AGE, REFRESH_AFTER, COOKIE_NAME, ADMIN_LOGIN_BUCKET, isLockedOut2, recordFailure2, clearFailures2, PUBLIC_PATHS;
31852
- var init_auth = __esm({
31853
- "../server/dist/middleware/auth.js"() {
31854
- "use strict";
31855
- init_config();
31856
- init_password_hash();
31857
- init_setup_config();
31858
- init_brute_force();
31859
- TOKEN_MAX_AGE = config.auth.tokenMaxAge;
31860
- REFRESH_AFTER = config.auth.refreshAfter;
31861
- COOKIE_NAME = "halo_token";
31862
- ADMIN_LOGIN_BUCKET = "admin-login";
31863
- isLockedOut2 = (ip) => isLockedOut(ADMIN_LOGIN_BUCKET, ip);
31864
- recordFailure2 = (ip) => recordFailure(ADMIN_LOGIN_BUCKET, ip);
31865
- clearFailures2 = (ip) => clearFailures(ADMIN_LOGIN_BUCKET, ip);
31866
- PUBLIC_PATHS = ["/api/auth/login", "/api/auth/check", "/api/auth/logout", "/api/health", "/api/web/chat", "/api/web/sessions", "/api/web/stop", "/api/web/history", "/api/web/subscribe", "/api/web/file", "/api/show/state", "/api/show/session", "/api/metrics"];
31867
- }
31868
- });
31869
-
31870
32736
  // ../server/dist/index.js
31871
32737
  var dist_exports = {};
31872
- import { Hono as Hono22 } from "hono";
32738
+ import { Hono as Hono23 } from "hono";
31873
32739
  import { cors } from "hono/cors";
31874
32740
  import { compress } from "hono/compress";
31875
32741
  import { serve } from "@hono/node-server";
31876
32742
  import { serveStatic } from "@hono/node-server/serve-static";
31877
32743
  import { WebSocketServer } from "ws";
31878
- import path68 from "node:path";
31879
- import fs62 from "node:fs";
32744
+ import path72 from "node:path";
32745
+ import fs66 from "node:fs";
31880
32746
  import { spawnSync as spawnSync3 } from "node:child_process";
31881
32747
  import { homedir as homedir24 } from "node:os";
31882
32748
  import { fileURLToPath as fileURLToPath3 } from "node:url";
31883
32749
  function findProjectRoot() {
31884
32750
  let dir = process.cwd();
31885
32751
  for (let i = 0; i < 32; i++) {
31886
- if (fs62.existsSync(path68.join(dir, "pnpm-workspace.yaml")))
32752
+ if (fs66.existsSync(path72.join(dir, "pnpm-workspace.yaml")))
31887
32753
  return dir;
31888
- const parent = path68.dirname(dir);
32754
+ const parent = path72.dirname(dir);
31889
32755
  if (parent === dir)
31890
32756
  break;
31891
32757
  dir = parent;
@@ -31894,15 +32760,15 @@ function findProjectRoot() {
31894
32760
  return process.cwd();
31895
32761
  }
31896
32762
  function acquireSingleInstanceLock(lockFile) {
31897
- fs62.mkdirSync(path68.dirname(lockFile), { recursive: true });
31898
- const fd = fs62.openSync(lockFile, fs62.constants.O_RDWR | fs62.constants.O_CREAT, 420);
32763
+ fs66.mkdirSync(path72.dirname(lockFile), { recursive: true });
32764
+ const fd = fs66.openSync(lockFile, fs66.constants.O_RDWR | fs66.constants.O_CREAT, 420);
31899
32765
  const result = spawnSync3("flock", ["-n", "3"], {
31900
32766
  stdio: ["ignore", "ignore", "ignore", fd]
31901
32767
  });
31902
32768
  if (result.error) {
31903
32769
  const content = readLockPid(fd);
31904
32770
  if (content > 0 && isProcessAlive(content)) {
31905
- fs62.closeSync(fd);
32771
+ fs66.closeSync(fd);
31906
32772
  console.error(`[Server] another halo server is already running (pid ${content}). kill it first.`);
31907
32773
  process.exit(1);
31908
32774
  }
@@ -31911,19 +32777,19 @@ function acquireSingleInstanceLock(lockFile) {
31911
32777
  if (content > 0 && !isProcessAlive(content)) {
31912
32778
  console.warn(`[Server] removing stale server.lock (pid ${content} not running)`);
31913
32779
  } else {
31914
- fs62.closeSync(fd);
32780
+ fs66.closeSync(fd);
31915
32781
  console.error(`[Server] another halo server is already running${content > 0 ? ` (pid ${content})` : ""}. kill it first.`);
31916
32782
  process.exit(1);
31917
32783
  }
31918
32784
  }
31919
- fs62.ftruncateSync(fd);
31920
- fs62.writeSync(fd, String(process.pid));
31921
- fs62.fsyncSync(fd);
32785
+ fs66.ftruncateSync(fd);
32786
+ fs66.writeSync(fd, String(process.pid));
32787
+ fs66.fsyncSync(fd);
31922
32788
  }
31923
32789
  function readLockPid(fd) {
31924
32790
  try {
31925
32791
  const buf = Buffer.alloc(32);
31926
- const n = fs62.readSync(fd, buf, 0, 32, 0);
32792
+ const n = fs66.readSync(fd, buf, 0, 32, 0);
31927
32793
  return parseInt(buf.subarray(0, n).toString("utf-8").trim(), 10) || 0;
31928
32794
  } catch {
31929
32795
  return 0;
@@ -31951,7 +32817,7 @@ function isProcessAlive(pid) {
31951
32817
  }
31952
32818
  }
31953
32819
  function readGitSha() {
31954
- if (!fs62.existsSync(path68.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs62.existsSync(path68.join(PROJECT_ROOT, ".git")))
32820
+ if (!fs66.existsSync(path72.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs66.existsSync(path72.join(PROJECT_ROOT, ".git")))
31955
32821
  return null;
31956
32822
  try {
31957
32823
  const rev = spawnSync3("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
@@ -31968,19 +32834,20 @@ function readGitSha() {
31968
32834
  }
31969
32835
  function resolveFrontendDir() {
31970
32836
  if (process.env.HALO_FRONTEND_DIR)
31971
- return path68.resolve(process.env.HALO_FRONTEND_DIR);
31972
- const bundled = path68.resolve(__dirname3, "..", "admin-out");
31973
- if (fs62.existsSync(path68.join(bundled, "index.html")))
32837
+ return path72.resolve(process.env.HALO_FRONTEND_DIR);
32838
+ const bundled = path72.resolve(__dirname3, "..", "admin-out");
32839
+ if (fs66.existsSync(path72.join(bundled, "index.html")))
31974
32840
  return bundled;
31975
- const sibling = path68.resolve(__dirname3, "..", "..", "admin", "out");
31976
- if (fs62.existsSync(path68.join(sibling, "index.html")))
32841
+ const sibling = path72.resolve(__dirname3, "..", "..", "admin", "out");
32842
+ if (fs66.existsSync(path72.join(sibling, "index.html")))
31977
32843
  return sibling;
31978
- return path68.resolve(PROJECT_ROOT, "packages", "admin", "out");
32844
+ return path72.resolve(PROJECT_ROOT, "packages", "admin", "out");
31979
32845
  }
31980
32846
  async function gracefulShutdown(signal) {
31981
32847
  console.log(`[Server] Received ${signal}, shutting down gracefully...`);
31982
32848
  stopEvoTicker();
31983
32849
  stopArchiveDaemon();
32850
+ stop();
31984
32851
  stopCronDaemon();
31985
32852
  await shutdownChannels();
31986
32853
  wss.clients.forEach((client) => {
@@ -31994,7 +32861,7 @@ async function gracefulShutdown(signal) {
31994
32861
  await shutdownObservability();
31995
32862
  process.exit(0);
31996
32863
  }
31997
- var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, GIT_SHA, AGENTCORE, app, allowlist, authRoutes, fileRoutes, dataPreviewRoutes, gitRoutes, agentConfigRoutes, skillRoutes, settingsRoutes, evolutionRoutes, cronRoutes, channelDb, registry, sessionRoutes, sessionArchiveRoutes, showRoutes, metricsRoutes, commandRoutes, FRONTEND_DIR, server, wss;
32864
+ var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, GIT_SHA, AGENTCORE, app, allowlist, authRoutes, fileRoutes, dataPreviewRoutes, gitRoutes, agentConfigRoutes, skillRoutes, settingsRoutes, evolutionRoutes, cronRoutes, extensionRoutes, channelDb, registry, sessionRoutes, sessionArchiveRoutes, showRoutes, metricsRoutes, commandRoutes, FRONTEND_DIR, server, wss;
31998
32865
  var init_dist2 = __esm({
31999
32866
  async "../server/dist/index.js"() {
32000
32867
  "use strict";
@@ -32023,11 +32890,13 @@ var init_dist2 = __esm({
32023
32890
  init_runs_db();
32024
32891
  init_runner();
32025
32892
  init_cron();
32893
+ init_extensions();
32894
+ init_watcher();
32026
32895
  init_evo_db();
32027
32896
  init_ticker();
32028
32897
  init_archive();
32029
32898
  init_spawn();
32030
- init_registry2();
32899
+ init_registry3();
32031
32900
  init_descriptors();
32032
32901
  init_auth();
32033
32902
  init_logger();
@@ -32036,13 +32905,13 @@ var init_dist2 = __esm({
32036
32905
  init_sandbox();
32037
32906
  init_init();
32038
32907
  init_git_ssh();
32039
- __dirname3 = path68.dirname(fileURLToPath3(import.meta.url));
32908
+ __dirname3 = path72.dirname(fileURLToPath3(import.meta.url));
32040
32909
  PORT = config.server.port;
32041
- HALO_VERSION = "1.4.6";
32910
+ HALO_VERSION = "1.5.0-alpha";
32042
32911
  PROJECT_ROOT = findProjectRoot();
32043
- HALO_HOME4 = path68.join(homedir24(), ".halo");
32912
+ HALO_HOME4 = path72.join(homedir24(), ".halo");
32044
32913
  GIT_SHA = readGitSha();
32045
- if (!fs62.existsSync(path68.join(HALO_HOME4, "global", ".template-version"))) {
32914
+ if (!fs66.existsSync(path72.join(HALO_HOME4, "global", ".template-version"))) {
32046
32915
  process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
32047
32916
  process.exit(1);
32048
32917
  }
@@ -32063,20 +32932,8 @@ var init_dist2 = __esm({
32063
32932
  process.exit(1);
32064
32933
  }
32065
32934
  if (!AGENTCORE)
32066
- acquireSingleInstanceLock(path68.join(HALO_HOME4, "global", "server.lock"));
32067
- {
32068
- const declared = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
32069
- const dispatched = new Set(DISPATCH_COMMANDS);
32070
- const declaredServer = new Set(commandRegistry.listDescriptors().filter((d) => d.type === "server" && d.source === "builtin").map((d) => d.slashName));
32071
- const missingDispatch = [...declaredServer].filter((n) => !dispatched.has(n));
32072
- const orphanDispatch = [...dispatched].filter((n) => !declared.has(n));
32073
- if (missingDispatch.length > 0) {
32074
- throw new Error(`[Server] Command descriptors without a dispatch case: ${missingDispatch.join(", ")}. Either add a case in channels/shared/commands.ts dispatchCommand or change the descriptor type to 'client'.`);
32075
- }
32076
- if (orphanDispatch.length > 0) {
32077
- throw new Error(`[Server] Dispatch cases without a descriptor: ${orphanDispatch.join(", ")}. Add a registerDescriptor entry in commands/index.ts or remove the dispatch case.`);
32078
- }
32079
- }
32935
+ acquireSingleInstanceLock(path72.join(HALO_HOME4, "global", "server.lock"));
32936
+ commandRegistry.assertDispatchCommands(DISPATCH_COMMANDS);
32080
32937
  await initObservability();
32081
32938
  initLogger();
32082
32939
  await initBwrapCheck();
@@ -32089,7 +32946,7 @@ var init_dist2 = __esm({
32089
32946
  ensureSshAgent();
32090
32947
  console.log(`[Server] Halo home: ${HALO_HOME4}`);
32091
32948
  console.log("[Server] Services initialized (ModelRuntime)");
32092
- app = new Hono22();
32949
+ app = new Hono23();
32093
32950
  allowlist = config.server.corsOrigins;
32094
32951
  app.use("/*", cors({
32095
32952
  origin: allowlist.length > 0 ? (origin) => origin && allowlist.includes(origin) ? origin : null : (origin) => origin ?? "*",
@@ -32136,11 +32993,15 @@ var init_dist2 = __esm({
32136
32993
  app.route("/api", evolutionRoutes);
32137
32994
  cronRoutes = createCronRoutes();
32138
32995
  app.route("/api", cronRoutes);
32139
- channelDb = createChannelDb(path68.join(HALO_HOME4, "secrets"));
32996
+ extensionRoutes = createExtensionRoutes();
32997
+ app.route("/api", extensionRoutes);
32998
+ if (!AGENTCORE)
32999
+ start();
33000
+ channelDb = createChannelDb(path72.join(HALO_HOME4, "secrets"));
32140
33001
  setChannelDb(channelDb);
32141
- setEvoDb(createEvoDb(path68.join(HALO_HOME4, "global")));
32142
- setCronDb(createCronDb(path68.join(HALO_HOME4, "global")));
32143
- setRunsDb(createRunsDb(path68.join(HALO_HOME4, "global")));
33002
+ setEvoDb(createEvoDb(path72.join(HALO_HOME4, "global")));
33003
+ setCronDb(createCronDb(path72.join(HALO_HOME4, "global")));
33004
+ setRunsDb(createRunsDb(path72.join(HALO_HOME4, "global")));
32144
33005
  setEvoSpawner(realEvoSpawner);
32145
33006
  if (!AGENTCORE) {
32146
33007
  startEvoTicker();
@@ -32150,7 +33011,7 @@ var init_dist2 = __esm({
32150
33011
  setRelayRegistry(registry);
32151
33012
  setCronSessionRegistry(registry);
32152
33013
  for (const ws of listRunningWorkspaces()) {
32153
- if (!fs62.existsSync(path68.join(ws, ".halo")))
33014
+ if (!fs66.existsSync(path72.join(ws, ".halo")))
32154
33015
  continue;
32155
33016
  if (!claimWorkspaceRuntime(ws))
32156
33017
  continue;
@@ -32179,11 +33040,11 @@ var init_dist2 = __esm({
32179
33040
  console.log(`[Server] AgentCore runtime mode \u2014 workspace: ${config.server.agentcoreWorkspace}`);
32180
33041
  }
32181
33042
  FRONTEND_DIR = resolveFrontendDir();
32182
- app.use("/*", serveStatic({ root: path68.relative(process.cwd(), FRONTEND_DIR) }));
33043
+ app.use("/*", serveStatic({ root: path72.relative(process.cwd(), FRONTEND_DIR) }));
32183
33044
  app.get("/*", (c) => {
32184
- const indexPath = path68.join(FRONTEND_DIR, "index.html");
33045
+ const indexPath = path72.join(FRONTEND_DIR, "index.html");
32185
33046
  try {
32186
- const html = fs62.readFileSync(indexPath, "utf-8");
33047
+ const html = fs66.readFileSync(indexPath, "utf-8");
32187
33048
  return c.html(html);
32188
33049
  } catch {
32189
33050
  return c.text("Frontend not built. Run: cd packages/admin && npx next build", 503);
@@ -32973,11 +33834,11 @@ process.emitWarning = (warning, ...args) => {
32973
33834
 
32974
33835
  // src/index.ts
32975
33836
  init_server_supervisor();
32976
- import path69 from "node:path";
32977
- import fs63 from "node:fs";
33837
+ import path73 from "node:path";
33838
+ import fs67 from "node:fs";
32978
33839
  import { homedir as homedir25 } from "node:os";
32979
33840
  import { parseArgs as parseArgs2 } from "node:util";
32980
- var VERSION = "1.4.6";
33841
+ var VERSION = "1.5.0-alpha";
32981
33842
  var HELP_TOP = `Usage: halo <command> [options]
32982
33843
 
32983
33844
  Commands:
@@ -33234,8 +34095,8 @@ async function cmdSetup(argv = []) {
33234
34095
  const { buildBindProviderPlan: buildBindProviderPlan2, KEEP_CURRENT: KEEP_CURRENT2, AWS_CHAIN_PROVIDER: AWS_CHAIN_PROVIDER2 } = await Promise.resolve().then(() => (init_setup_bind_provider(), setup_bind_provider_exports));
33235
34096
  const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
33236
34097
  const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
33237
- const haloHome2 = path69.join(homedir25(), ".halo");
33238
- const globalDir2 = path69.join(haloHome2, "global");
34098
+ const haloHome2 = path73.join(homedir25(), ".halo");
34099
+ const globalDir2 = path73.join(haloHome2, "global");
33239
34100
  ensureHaloHome2(haloHome2);
33240
34101
  process.stderr.write(`[setup] ${haloHome2}/ ready
33241
34102
  `);
@@ -33578,7 +34439,7 @@ async function cmdUpgrade() {
33578
34439
  `);
33579
34440
  process.exit(1);
33580
34441
  }
33581
- const broken = verifyNativeModules2(path69.join(rootDir, PKG));
34442
+ const broken = verifyNativeModules2(path73.join(rootDir, PKG));
33582
34443
  if (broken.length > 0) {
33583
34444
  process.stderr.write(
33584
34445
  `
@@ -33609,7 +34470,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
33609
34470
  async function cmdAgents(flags) {
33610
34471
  const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
33611
34472
  await initRuntime2();
33612
- const workspace = path69.resolve(flags.workspace ?? process.cwd());
34473
+ const workspace = path73.resolve(flags.workspace ?? process.cwd());
33613
34474
  const agents = await listAgents2(workspace);
33614
34475
  if (agents.length === 0) {
33615
34476
  process.stderr.write("No agents found.\n");
@@ -33625,7 +34486,7 @@ async function cmdAgents(flags) {
33625
34486
  async function cmdSessions(flags) {
33626
34487
  const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
33627
34488
  await initRuntime2();
33628
- const workspace = path69.resolve(flags.workspace ?? process.cwd());
34489
+ const workspace = path73.resolve(flags.workspace ?? process.cwd());
33629
34490
  const sessions = listSessions2(workspace);
33630
34491
  if (sessions.length === 0) {
33631
34492
  process.stderr.write("No sessions found.\n");
@@ -33738,11 +34599,11 @@ ${stdinContent}` : stdinContent;
33738
34599
  harness.destroy();
33739
34600
  }
33740
34601
  }
33741
- var SERVER_LOCK_PATH = path69.join(homedir25(), ".halo", "global", "server.lock");
33742
- var SERVER_LOG_PATH = path69.join(homedir25(), ".halo", "logs", "server.log");
34602
+ var SERVER_LOCK_PATH = path73.join(homedir25(), ".halo", "global", "server.lock");
34603
+ var SERVER_LOG_PATH = path73.join(homedir25(), ".halo", "logs", "server.log");
33743
34604
  function readServerPid() {
33744
34605
  try {
33745
- const txt = fs63.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
34606
+ const txt = fs67.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
33746
34607
  const n = parseInt(txt, 10);
33747
34608
  return Number.isFinite(n) && n > 0 ? n : null;
33748
34609
  } catch {
@@ -33829,8 +34690,8 @@ async function cmdServerStart(argv, _unused) {
33829
34690
  return;
33830
34691
  }
33831
34692
  const { spawn: spawn5 } = await import("node:child_process");
33832
- fs63.mkdirSync(path69.dirname(SERVER_LOG_PATH), { recursive: true });
33833
- const out = fs63.openSync(SERVER_LOG_PATH, "a");
34693
+ fs67.mkdirSync(path73.dirname(SERVER_LOG_PATH), { recursive: true });
34694
+ const out = fs67.openSync(SERVER_LOG_PATH, "a");
33834
34695
  const child = spawn5(process.argv[0], [process.argv[1], "server", "start"], {
33835
34696
  detached: true,
33836
34697
  stdio: ["ignore", out, out],
@@ -33931,7 +34792,7 @@ async function cmdServerLogs(argv) {
33931
34792
  lines: { type: "string", short: "n" }
33932
34793
  }
33933
34794
  });
33934
- if (!fs63.existsSync(SERVER_LOG_PATH)) {
34795
+ if (!fs67.existsSync(SERVER_LOG_PATH)) {
33935
34796
  process.stderr.write(`No log file at ${SERVER_LOG_PATH}
33936
34797
  `);
33937
34798
  return;
@@ -33949,7 +34810,7 @@ function exitMissingSetup() {
33949
34810
  process.exit(1);
33950
34811
  }
33951
34812
  function isHaloHomeReady() {
33952
- return fs63.existsSync(path69.join(homedir25(), ".halo", "global", ".template-version"));
34813
+ return fs67.existsSync(path73.join(homedir25(), ".halo", "global", ".template-version"));
33953
34814
  }
33954
34815
  async function main2() {
33955
34816
  const argv = process.argv.slice(2);