@staix/agent-hub 0.15.0 → 0.16.0

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 (80) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +12 -8
  3. package/docs/agent-notes/README.md +4 -0
  4. package/docs/agent-notes/adapters.md +6 -3
  5. package/docs/agent-notes/budget.md +4 -2
  6. package/docs/agent-notes/daemon.md +9 -3
  7. package/docs/agent-notes/local-worker.md +2 -2
  8. package/docs/agent-notes/tasks.md +13 -1
  9. package/docs/agent-notes/tests.md +3 -0
  10. package/docs/dogfood.md +13 -4
  11. package/docs/events.md +1 -1
  12. package/docs/memory.md +78 -10
  13. package/docs/operations.md +116 -30
  14. package/docs/quickstart.md +8 -4
  15. package/docs/security.md +80 -18
  16. package/docs/smoke.md +7 -3
  17. package/docs/specs/2026-09-19-agent-hub-design.md +84 -8
  18. package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
  19. package/docs/verification/2026-10-24-amem-runtime.md +69 -0
  20. package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
  21. package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
  22. package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
  23. package/docs/verification/2026-10-25-0.16.0.md +82 -0
  24. package/docs/verified.json +26 -17
  25. package/package.json +2 -2
  26. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  27. package/plugins/agent-hub/server.js +2085 -1702
  28. package/src/adapters/acp.ts +5 -2
  29. package/src/adapters/codex-appserver.ts +69 -1
  30. package/src/adapters/compat.ts +2 -2
  31. package/src/adapters/definitions/builtin-metadata.ts +3 -3
  32. package/src/adapters/local-worker.ts +5 -4
  33. package/src/adapters/registry-types.ts +1 -1
  34. package/src/cli/console-state.ts +20 -2
  35. package/src/cli/console-symbols.ts +3 -2
  36. package/src/cli/console-usage.ts +5 -2
  37. package/src/cli/console.ts +5 -1
  38. package/src/cli/help.ts +7 -5
  39. package/src/cli/identity.ts +3 -1
  40. package/src/cli/main.ts +44 -10
  41. package/src/cli/memory.ts +119 -7
  42. package/src/cli/output.ts +12 -4
  43. package/src/hub/action-class.ts +211 -17
  44. package/src/hub/action-enforce.ts +13 -5
  45. package/src/hub/action-policy.ts +19 -8
  46. package/src/hub/ask.ts +16 -4
  47. package/src/hub/board.ts +86 -31
  48. package/src/hub/budget-notes.ts +65 -0
  49. package/src/hub/budget.ts +42 -10
  50. package/src/hub/bus.ts +1 -1
  51. package/src/hub/code-facts.ts +56 -16
  52. package/src/hub/conductor.ts +5 -2
  53. package/src/hub/config-apply.ts +7 -0
  54. package/src/hub/control-client.ts +1 -1
  55. package/src/hub/daemon.ts +250 -49
  56. package/src/hub/envelope.ts +1 -1
  57. package/src/hub/events.ts +2 -2
  58. package/src/hub/hub-tools.ts +3 -3
  59. package/src/hub/kimi-usage.ts +12 -0
  60. package/src/hub/lifecycle.ts +10 -5
  61. package/src/hub/memory-code-head.ts +44 -0
  62. package/src/hub/memory-code-overlay.ts +119 -0
  63. package/src/hub/memory-plan-guard.ts +10 -1
  64. package/src/hub/memory-stale-anchors.ts +159 -0
  65. package/src/hub/memory-symbols.ts +74 -0
  66. package/src/hub/planning.ts +10 -4
  67. package/src/hub/read-tools.ts +21 -3
  68. package/src/hub/sdlc.ts +34 -0
  69. package/src/hub/settings.ts +17 -2
  70. package/src/hub/settled-roles.ts +15 -3
  71. package/src/hub/task-sweep.ts +25 -3
  72. package/src/hub/tasks.ts +353 -64
  73. package/src/hub/token-window.ts +15 -2
  74. package/src/hub/window-kind.ts +13 -0
  75. package/src/local/tools.ts +5 -5
  76. package/src/memory/brief.ts +22 -12
  77. package/src/memory/capture.ts +26 -40
  78. package/src/memory/client.ts +137 -65
  79. package/src/memory/recall.ts +13 -22
  80. package/src/ui/index.html +100 -18
@@ -47,7 +47,7 @@ var __esm = (fn, res, err) => () => {
47
47
  // node_modules/@staix/agent-mem/src/memory/platform.ts
48
48
  import { realpathSync } from "fs";
49
49
  import { basename, dirname, join, resolve, sep } from "path";
50
- import { spawnSync, execFileSync } from "child_process";
50
+ import { createRequire } from "module";
51
51
  import { createHash } from "crypto";
52
52
  function canonicalPath(path) {
53
53
  return canonical(path);
@@ -59,7 +59,7 @@ function realPath(path) {
59
59
  if (!resolve(path).includes("\\"))
60
60
  throw error;
61
61
  const argv = process.platform === "darwin" ? ["/bin/realpath", "--", path] : ["realpath", "-e", "--", path];
62
- const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8" });
62
+ const r = childProcess().spawnSync(argv[0], argv.slice(1), { encoding: "utf8" });
63
63
  if (r.status !== 0 || !r.stdout.endsWith(`
64
64
  `))
65
65
  throw error;
@@ -82,11 +82,11 @@ function memoryGitEnv(env = process.env, options = {}) {
82
82
  }
83
83
  function memoryGitSync(args, options) {
84
84
  const argv = memoryGitArgv(args);
85
- return spawnSync(argv[0], argv.slice(1), { ...options, env: memoryGitEnv(options.env ?? process.env) });
85
+ return childProcess().spawnSync(argv[0], argv.slice(1), { ...options, env: memoryGitEnv(options.env ?? process.env) });
86
86
  }
87
87
  function processSignature(pid) {
88
88
  try {
89
- const text = execFileSync("ps", ["-p", String(pid), "-o", "lstart=,comm="], { encoding: "utf8", env: { ...process.env, LC_ALL: "C", TZ: "UTC" } }).trim();
89
+ const text = childProcess().execFileSync("ps", ["-p", String(pid), "-o", "lstart=,comm="], { encoding: "utf8", env: { ...process.env, LC_ALL: "C", TZ: "UTC" } }).trim();
90
90
  return text ? createHash("sha256").update(text).digest("hex") : undefined;
91
91
  } catch {
92
92
  return;
@@ -110,7 +110,7 @@ function processLiveness(pid, signature, identity = processSignature) {
110
110
  return error.code === "ESRCH" ? "gone" : "unknown";
111
111
  }
112
112
  }
113
- var canonical = (path) => {
113
+ var require2, processes, childProcess = () => processes ??= require2("node:child_process"), canonical = (path) => {
114
114
  const absolute = resolve(path);
115
115
  try {
116
116
  return realpathSync(absolute);
@@ -120,6 +120,7 @@ var canonical = (path) => {
120
120
  }
121
121
  }, DENY_SEGMENTS, DENY_NAMES, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args];
122
122
  var init_platform = __esm(() => {
123
+ require2 = createRequire(import.meta.url);
123
124
  DENY_SEGMENTS = [".maru/secrets", ".agenthub/state", ".agenthub/archive"];
124
125
  DENY_NAMES = ["^\\.env", "\\.pem$", "\\.key$", "^id_rsa", "^id_ed25519", "^credentials(\\.|$)", "^auth\\.json$", "^\\.netrc$", "^\\.npmrc$", "^\\.pypirc$"];
125
126
  SAFE_CONFIG = ["core.fsmonitor=false", "core.hooksPath=/dev/null", "diff.external=", "core.pager=cat", "credential.helper=", "core.askpass="];
@@ -300,7 +301,7 @@ var init_compat = __esm(() => {
300
301
  },
301
302
  "codex-review": {
302
303
  label: "Codex reviewer (codex-cli)",
303
- verified: ["0.146.0", "0.163.0"],
304
+ verified: ["0.146.0", "0.163.0", "0.164.0"],
304
305
  gate: "isolation",
305
306
  probe: ["codex", "--version"],
306
307
  config: "codex_bin",
@@ -1020,7 +1021,7 @@ var init_child_process = __esm(() => {
1020
1021
  });
1021
1022
 
1022
1023
  // src/adapters/agy/entry.ts
1023
- import { spawn, spawnSync as spawnSync2 } from "child_process";
1024
+ import { spawn, spawnSync } from "child_process";
1024
1025
  import { resolve as resolve4 } from "path";
1025
1026
  function agyTuiSpawnOptions(cwd, env) {
1026
1027
  return { cwd, env, stdio: "inherit" };
@@ -1078,7 +1079,7 @@ async function runAgyEntry(ctx) {
1078
1079
  console.log(JSON.stringify({ command: "agy", args: argv.map(publicArg), mode: "tui", permission: "unmanaged", mcp: ".agents/plugins/agent-hub/", writes: false }));
1079
1080
  return;
1080
1081
  }
1081
- const version = spawnSync2("agy", ["--version"], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, env: peerChildEnv("agy") });
1082
+ const version = spawnSync("agy", ["--version"], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, env: peerChildEnv("agy") });
1082
1083
  const gate = agyVersionGate(version.status === 0 ? version.stdout : "");
1083
1084
  if (!gate.ok)
1084
1085
  fail(gate.message);
@@ -1802,7 +1803,7 @@ var init_claude_print = __esm(() => {
1802
1803
  import { createHash as createHash2 } from "crypto";
1803
1804
  import { spawn as spawn3 } from "child_process";
1804
1805
  import { existsSync as existsSync2, lstatSync as lstatSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
1805
- import { createRequire } from "module";
1806
+ import { createRequire as createRequire2 } from "module";
1806
1807
  import { homedir as homedir2, tmpdir } from "os";
1807
1808
  import { basename as basename2, delimiter, dirname as dirname4, isAbsolute as isAbsolute3, join as join5, relative, resolve as resolve5 } from "path";
1808
1809
  function codexCoreDenyGlobs(names = DENY_NAMES, segments = DENY_SEGMENTS) {
@@ -1934,7 +1935,7 @@ function resolveCommand(ctx, cwd) {
1934
1935
  if (!target || !["arm64", "x64"].includes(process.arch))
1935
1936
  throw new Error("unverified native platform");
1936
1937
  const suffix = `${process.platform}-${process.arch === "x64" ? "x64" : "arm64"}`;
1937
- const manifest = createRequire(binary).resolve(`@openai/codex-${suffix}/package.json`);
1938
+ const manifest = createRequire2(binary).resolve(`@openai/codex-${suffix}/package.json`);
1938
1939
  binary = realPath(join5(dirname4(manifest), "vendor", target, "bin", "codex"));
1939
1940
  }
1940
1941
  const files = [binary];
@@ -2534,1680 +2535,1776 @@ var nonnegative = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0,
2534
2535
  // src/hub/context-window.ts
2535
2536
  var init_context_window = () => {};
2536
2537
 
2537
- // src/adapters/codex-appserver.ts
2538
- import { spawn as spawn5 } from "child_process";
2539
- function parse(raw) {
2540
- try {
2541
- return JSON.parse(raw);
2542
- } catch {
2543
- return;
2538
+ // src/hub/activity.ts
2539
+ function activityText(value) {
2540
+ return typeof value === "string" ? value.slice(0, TEXT_CAP) : "";
2541
+ }
2542
+ function updateActivity(u) {
2543
+ if (u?.sessionUpdate === "agent_message_chunk" || u?.sessionUpdate === "agent_thought_chunk") {
2544
+ if (u.content?.type !== "text" || typeof u.content.text !== "string")
2545
+ return;
2546
+ return { kind: u.sessionUpdate === "agent_message_chunk" ? "message" : "thought", text: activityText(u.content.text) };
2547
+ }
2548
+ if (["tool_call", "tool_call_update"].includes(u?.sessionUpdate)) {
2549
+ const fields = [u.toolCallId, u.title, u.kind, u.status].filter((v) => typeof v === "string").map(activityText);
2550
+ for (const location of Array.isArray(u.locations) ? u.locations.slice(0, 12) : []) {
2551
+ if (typeof location?.path === "string")
2552
+ fields.push(activityText(location.path));
2553
+ }
2554
+ for (const c of Array.isArray(u.content) ? u.content.slice(0, 12) : []) {
2555
+ if (c?.type === "diff" && typeof c.path === "string") {
2556
+ const lines = (v) => typeof v === "string" && v ? v.split(`
2557
+ `).length : 0;
2558
+ fields.push(`+${lines(c.newText)} -${lines(c.oldText)} ${activityText(c.path)}`);
2559
+ } else if (c?.type === "content" && c.content?.type === "text") {
2560
+ fields.push(activityText(c.content.text).split(`
2561
+ `).slice(-8).join(`
2562
+ `));
2563
+ } else if (c?.type === "terminal")
2564
+ fields.push("terminal output not reported by this update");
2565
+ }
2566
+ return { kind: "tool", text: fields.join(" ").slice(0, TEXT_CAP) };
2544
2567
  }
2568
+ if (u?.sessionUpdate === "plan" && Array.isArray(u.entries))
2569
+ return { kind: "plan", text: u.entries.slice(0, 32).map((e) => [e?.status, e?.priority, e?.content].filter((v) => typeof v === "string").map(activityText).join(" ")).join(`
2570
+ `).slice(0, TEXT_CAP) };
2545
2571
  }
2546
- var UNIDENTIFIED_CANCELLATION_NOTICE = "Codex cancelled turn has no verified native id; reattach the native session before further hub delivery", counter = (value) => {
2547
- const n = Number(value);
2548
- return Number.isFinite(n) && n >= 0 ? n : 0;
2549
- }, TRACKED, CodexPeer;
2550
- var init_codex_appserver = __esm(() => {
2551
- init_envelope();
2552
- init_context_window();
2553
- init_child_process();
2554
- TRACKED = new Set(["thread/start", "thread/resume"]);
2555
- CodexPeer = class CodexPeer extends BasePeer {
2556
- opts;
2557
- permissionMode;
2558
- nativeApprovalPolicies = new Map;
2559
- overriddenApprovals = new Set;
2560
- restoringApprovals = new Map;
2561
- proc;
2562
- server;
2563
- link;
2564
- claimedTui;
2565
- threadId = "";
2566
- contextEpoch = 0;
2567
- get sessionGeneration() {
2568
- return this.contextEpoch;
2572
+ var TEXT_CAP = 4096;
2573
+
2574
+ // src/adapters/acp-quota.ts
2575
+ function acpQuotaRefusal(message) {
2576
+ if (!message.startsWith(KIMI_2_1_1_5H_REFUSAL_PREFIX))
2577
+ return;
2578
+ return { id: "5h", used: 1, windowMins: 300, source: "kimi ACP usage-limit refusal" };
2579
+ }
2580
+ var KIMI_2_1_1_5H_REFUSAL_PREFIX = "Authentication required: 403 You've reached your 5-hour usage limit. Your quota will reset when the current 5-hour window ends.";
2581
+
2582
+ // src/adapters/definitions/kimi-native.ts
2583
+ var KIMI_MODE_IDS;
2584
+ var init_kimi_native = __esm(() => {
2585
+ KIMI_MODE_IDS = { ask: "default", "ask-when-needed": "yolo", "never-ask": "auto" };
2586
+ });
2587
+
2588
+ // src/adapters/definitions/qwen-native.ts
2589
+ function qwenNativeIdentity(instance, production, init, kind) {
2590
+ const native = init;
2591
+ const identity = production === true && (kind ?? instance) === "qwen" && native?.agentInfo?.name === "qwen-code";
2592
+ const version = identity ? assessVersion("qwen", native?.agentInfo?.version) : undefined;
2593
+ if (production && !version?.ok)
2594
+ throw new QwenIdentityError(version && !version.ok ? version.reason : undefined);
2595
+ const verified = !!version?.ok;
2596
+ return verified;
2597
+ }
2598
+ function buildQwenLaunch(command, model) {
2599
+ const [cmd, ...args] = command;
2600
+ if (!cmd)
2601
+ throw new Error("qwen_cmd must name an executable");
2602
+ return { cmd, args: [...args, ...model ? ["--model", model] : []] };
2603
+ }
2604
+ var QWEN_MODE_IDS, QwenIdentityError, qwenSessionId = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
2605
+ var init_qwen_native = __esm(() => {
2606
+ init_compat();
2607
+ QWEN_MODE_IDS = { ask: "default", "ask-when-needed": "auto-edit", "never-ask": "yolo" };
2608
+ QwenIdentityError = class QwenIdentityError extends Error {
2609
+ constructor(reason) {
2610
+ super(reason ? `Qwen Code native identity is not verified: ${reason}` : "Qwen Code native identity is not verified; check the executable and its advertised ACP version");
2569
2611
  }
2570
- tokenTotal;
2571
- tokenKinds;
2572
- freshThread = false;
2573
- activeTurns = new Set;
2574
- deliveryEpoch = 0;
2575
- retiredTurns = new Set;
2576
- unidentifiedCancellation = false;
2577
- get statusNotice() {
2578
- return this.unidentifiedCancellation ? { code: "unidentified-cancellation", message: UNIDENTIFIED_CANCELLATION_NOTICE } : undefined;
2612
+ };
2613
+ });
2614
+
2615
+ // src/adapters/acp-owned-group.ts
2616
+ class AcpOwnedGroup {
2617
+ proc;
2618
+ leader;
2619
+ ports;
2620
+ confirmedGone = false;
2621
+ constructor(proc, leader, ports) {
2622
+ this.proc = proc;
2623
+ this.leader = leader;
2624
+ this.ports = ports;
2625
+ }
2626
+ static async capture(proc, ports = PORTS) {
2627
+ const pid = proc.pid;
2628
+ if (!Number.isSafeInteger(pid) || !pid || pid <= 1 || proc.exitCode !== null || proc.signalCode !== null)
2629
+ throw ownershipError();
2630
+ const rows = await AcpOwnedGroup.read(ports);
2631
+ const leader = rows.find((row) => row.pid === pid);
2632
+ if (!leader || leader.pgid !== pid || leader.ppid !== process.pid)
2633
+ throw ownershipError();
2634
+ const signature = AcpOwnedGroup.identity(ports, pid);
2635
+ if (!signature || proc.pid !== pid || proc.exitCode !== null || proc.signalCode !== null)
2636
+ throw ownershipError();
2637
+ return new AcpOwnedGroup(proc, { pid, ppid: leader.ppid, pgid: leader.pgid, started: leader.started }, ports);
2638
+ }
2639
+ static async confirmGone(proc, ports = PORTS) {
2640
+ const pid = proc.pid;
2641
+ if (pid === undefined)
2642
+ return true;
2643
+ if (!Number.isSafeInteger(pid) || pid <= 1 || !(typeof proc.exitCode === "number" || typeof proc.signalCode === "string"))
2644
+ return false;
2645
+ try {
2646
+ const rows = await AcpOwnedGroup.read(ports);
2647
+ return proc.pid === pid && (typeof proc.exitCode === "number" || typeof proc.signalCode === "string") && !rows.some((row) => row.pid === pid || row.pgid === pid);
2648
+ } catch {
2649
+ return false;
2579
2650
  }
2580
- unboundAccepted = false;
2581
- nextId = -1;
2582
- pending = new Map;
2583
- usageTimer;
2584
- injected;
2585
- answering = [];
2586
- lastAnswer = "";
2587
- deltas = new Map;
2588
- primed = false;
2589
- version;
2590
- steers = new Set;
2591
- turnDeliveries = new Map;
2592
- unboundDeliveries = new Set;
2593
- constructor(id, opts) {
2594
- super(id, opts.watchdogMs);
2595
- this.opts = opts;
2596
- this.permissionMode = opts.permissionMode ?? "ask";
2651
+ }
2652
+ async verify() {
2653
+ if (this.confirmedGone || !await this.inspect(false))
2654
+ throw ownershipError();
2655
+ }
2656
+ async stop({ killMs = 2000 } = {}) {
2657
+ if (this.confirmedGone)
2658
+ return;
2659
+ if (!Number.isFinite(killMs) || killMs <= 0 || killMs > 1e4)
2660
+ throw shutdownError();
2661
+ if (!await this.inspect(true)) {
2662
+ this.confirmedGone = true;
2663
+ return;
2597
2664
  }
2598
- recoveryMetadata() {
2599
- return {
2600
- launch: { kind: "codex", bin: this.opts.bin ?? "codex", cwd: this.opts.cwd, appPort: this.opts.appPort, proxyPort: this.opts.proxyPort },
2601
- ...this.threadId ? { threadId: this.threadId } : {}
2602
- };
2665
+ this.signal("SIGSTOP");
2666
+ if (!await this.inspect(true)) {
2667
+ this.confirmedGone = true;
2668
+ return;
2603
2669
  }
2604
- get thread() {
2605
- return this.threadId;
2670
+ this.signal("SIGKILL");
2671
+ const end = this.ports.now() + killMs;
2672
+ for (;; ) {
2673
+ const left = end - this.ports.now();
2674
+ if (left <= 0)
2675
+ throw shutdownError();
2676
+ const rows = await AcpOwnedGroup.read(this.ports, Math.min(READ_MS, left));
2677
+ const leader = rows.find((row) => row.pid === this.leader.pid);
2678
+ if (leader && (leader.started !== this.leader.started || leader.pgid !== this.leader.pgid))
2679
+ throw ownershipError();
2680
+ if (!rows.some((row) => row.pgid === this.leader.pgid) && !leader) {
2681
+ this.confirmedGone = true;
2682
+ return;
2683
+ }
2684
+ if (this.ports.now() >= end)
2685
+ throw shutdownError();
2686
+ await this.ports.sleep(Math.min(25, end - this.ports.now()));
2606
2687
  }
2607
- get proxyUrl() {
2608
- return `ws://127.0.0.1:${this.server?.port ?? this.opts.proxyPort}`;
2688
+ }
2689
+ async inspect(allowGone) {
2690
+ const rows = await AcpOwnedGroup.read(this.ports);
2691
+ const leader = rows.find((row) => row.pid === this.leader.pid);
2692
+ const members = rows.some((row) => row.pgid === this.leader.pgid);
2693
+ if (!leader && !members && allowGone)
2694
+ return false;
2695
+ if (!leader || !this.sameLeader(leader) || this.proc.pid !== this.leader.pid || this.proc.exitCode !== null || this.proc.signalCode !== null || !AcpOwnedGroup.identity(this.ports, this.leader.pid))
2696
+ throw ownershipError();
2697
+ return true;
2698
+ }
2699
+ sameLeader(row) {
2700
+ return row.pid === this.leader.pid && row.pgid === this.leader.pgid && row.ppid === this.leader.ppid && row.started === this.leader.started;
2701
+ }
2702
+ signal(signal) {
2703
+ try {
2704
+ this.ports.signal(-this.leader.pgid, signal);
2705
+ } catch {
2706
+ throw ownershipError();
2609
2707
  }
2610
- terminalOwner(launchId) {
2611
- const link = this.link;
2612
- return this.threadId && link && link === this.claimedTui && link.launchId === launchId && link.tui.readyState === WebSocket.OPEN && link.up.readyState === WebSocket.OPEN ? { generation: String(this.sessionGeneration) } : undefined;
2708
+ }
2709
+ static identity(ports, pid) {
2710
+ try {
2711
+ return ports.signature(pid);
2712
+ } catch {
2713
+ return;
2613
2714
  }
2614
- async start() {
2615
- const upstream = this.opts.upstreamUrl ?? await this.spawnAppServer();
2616
- try {
2617
- this.server = Bun.serve({
2618
- hostname: "127.0.0.1",
2619
- port: this.opts.proxyPort,
2620
- fetch: (req, server) => {
2621
- if (req.headers.has("origin"))
2622
- return new Response("forbidden", { status: 403 });
2623
- const launchId = /^Bearer (\S+)$/.exec(req.headers.get("authorization") ?? "")?.[1];
2624
- if (server.upgrade(req, { data: { ...launchId && launchId.length <= 128 ? { launchId } : {} } }))
2625
- return;
2626
- return new Response("agent-hub codex proxy");
2627
- },
2628
- websocket: {
2629
- open: (tui) => {
2630
- if (this.opts.admitAttach?.() === false) {
2631
- tui.close(4000, "codex is off roster; retry when a person adds it");
2632
- return;
2633
- }
2634
- if (this.claimedTui) {
2635
- tui.close(1013, "codex TUI already attached to this hub");
2636
- return;
2637
- }
2638
- this.attach(tui, upstream);
2639
- },
2640
- message: (tui, data) => {
2641
- if (this.opts.admitAttach?.() === false) {
2642
- tui.close(4000, "codex is off roster");
2643
- return;
2644
- }
2645
- if (tui.data?.up)
2646
- this.fromTui(tui.data, String(data));
2647
- },
2648
- close: (tui) => {
2649
- if (tui.data?.up)
2650
- this.detach(tui.data);
2651
- }
2652
- }
2653
- });
2654
- } catch (error) {
2655
- const proc = this.proc;
2656
- if (proc && await this.stopQuietly(proc) && this.proc === proc)
2657
- this.proc = undefined;
2658
- throw error;
2659
- }
2660
- }
2661
- stopQuietly(proc) {
2662
- return stopOwnedProcess(proc, { group: true }).then(() => true, (error) => {
2663
- this.opts.log?.(`[${this.id}] ${error.message}`);
2664
- return false;
2665
- });
2666
- }
2667
- async stop() {
2668
- this.server?.stop(true);
2669
- this.claimedTui?.tui.close(1001, "hub shutting down");
2670
- const proc = this.proc;
2671
- if (proc) {
2672
- await stopOwnedProcess(proc, { group: true });
2673
- if (this.proc === proc)
2674
- this.proc = undefined;
2675
- }
2676
- this.setState("offline");
2677
- }
2678
- deliver(envs, deliveryId) {
2679
- const link = this.link;
2680
- if (this.state !== "idle" || !link || link.up.readyState !== WebSocket.OPEN) {
2681
- if (deliveryId)
2682
- this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is not injectable` });
2683
- return Promise.reject(new Error(`${this.id} is not injectable`));
2684
- }
2685
- const text = this.render(envs);
2686
- const id = this.nextId--, epoch = this.deliveryEpoch, session = this.contextEpoch;
2687
- this.setState("busy");
2688
- return new Promise((resolve, reject) => {
2689
- let invalidated = false;
2690
- const retiredAtCancel = new Set;
2691
- const invalidate = (reason) => {
2692
- if (invalidated)
2693
- return;
2694
- invalidated = true;
2695
- for (const turn of [...this.activeTurns, ...this.turnDeliveries.keys()])
2696
- if (!turn.startsWith("unknown:"))
2697
- retiredAtCancel.add(turn);
2698
- envs = [];
2699
- if (deliveryId)
2700
- this.delivery({ id: deliveryId, state: "needs_review", reason });
2701
- reject(new Error(reason));
2702
- };
2703
- this.pending.set(id, {
2704
- deliveryId,
2705
- kind: "deliver",
2706
- invalidate,
2707
- resolve: (result) => {
2708
- const nativeTurn = typeof result?.turn?.id === "string" ? result.turn.id : undefined;
2709
- if (invalidated || epoch !== this.deliveryEpoch || session !== this.contextEpoch || link !== this.link) {
2710
- invalidate("Codex turn acceptance arrived after its delivery was invalidated");
2711
- if (nativeTurn && session === this.contextEpoch && link === this.link && (!retiredAtCancel.has(nativeTurn) || this.retiredTurns.has(nativeTurn)))
2712
- this.retireTurn(nativeTurn);
2713
- return;
2714
- }
2715
- this.primed = true;
2716
- this.injected = replyParent(envs);
2717
- this.answering = [...envs];
2718
- if (nativeTurn && !this.turnDeliveries.has(nativeTurn))
2719
- this.turnDeliveries.set(nativeTurn, new Set);
2720
- if (!nativeTurn && !this.activeTurns.size)
2721
- this.unboundAccepted = true;
2722
- if (deliveryId) {
2723
- if (nativeTurn)
2724
- this.addTurnDelivery(nativeTurn, deliveryId);
2725
- else
2726
- this.unboundDeliveries.add(deliveryId);
2727
- this.delivery({ id: deliveryId, state: "accepted" });
2728
- }
2729
- resolve();
2730
- },
2731
- reject: (e) => {
2732
- this.restoringApprovals.delete(id);
2733
- if (invalidated)
2734
- return;
2735
- if (epoch !== this.deliveryEpoch || session !== this.contextEpoch || link !== this.link) {
2736
- invalidate("Codex turn refusal arrived after its delivery was invalidated");
2737
- return;
2738
- }
2739
- if (!this.activeTurns.size)
2740
- this.setState("idle");
2741
- if (deliveryId)
2742
- this.delivery({ id: deliveryId, state: this.knownRejection(e) ? "failed_safe" : "needs_review", reason: e.message });
2743
- reject(e);
2744
- }
2745
- });
2746
- link.up.send(JSON.stringify({ method: "turn/start", id, params: this.applyPermissionPolicy({ threadId: this.threadId, input: [{ type: "text", text }] }, id) }));
2747
- });
2715
+ }
2716
+ static async read(ports, ms = READ_MS) {
2717
+ let timer;
2718
+ try {
2719
+ const rows = await Promise.race([
2720
+ Promise.resolve().then(() => ports.table()),
2721
+ new Promise((resolve) => {
2722
+ timer = setTimeout(() => resolve(undefined), ms);
2723
+ })
2724
+ ]);
2725
+ if (!rows)
2726
+ throw ownershipError();
2727
+ return rows;
2728
+ } catch {
2729
+ throw ownershipError();
2730
+ } finally {
2731
+ clearTimeout(timer);
2748
2732
  }
2749
- render(envs) {
2750
- return this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}
2733
+ }
2734
+ }
2735
+ var PORTS, READ_MS = 1000, ownershipError = () => new Error("Configured ACP process ownership could not be verified"), shutdownError = () => new Error("Configured ACP process group shutdown incomplete");
2736
+ var init_acp_owned_group = __esm(() => {
2737
+ init_platform();
2738
+ init_child_process();
2739
+ PORTS = {
2740
+ table: readProcessTable,
2741
+ signature: processSignature,
2742
+ signal: (target, signal) => {
2743
+ process.kill(target, signal);
2744
+ },
2745
+ now: () => performance.now(),
2746
+ sleep: (ms) => Bun.sleep(ms)
2747
+ };
2748
+ });
2751
2749
 
2752
- ${renderDigest(envs, false)}`;
2753
- }
2754
- steer(envs, deliveryId) {
2755
- const link = this.link;
2756
- const expectedTurnId = [...this.activeTurns].reverse().find((t) => !t.startsWith("unknown:"));
2757
- if (!link || link.up.readyState !== WebSocket.OPEN || !expectedTurnId) {
2758
- if (deliveryId)
2759
- this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} has no steerable turn` });
2760
- return Promise.reject(new Error(`${this.id} has no steerable turn`));
2761
- }
2762
- const id = this.nextId--;
2763
- const expectedEpoch = this.contextEpoch;
2764
- this.steers.add(id);
2765
- return new Promise((resolve, reject) => {
2766
- let timer;
2767
- const settle = () => {
2768
- clearTimeout(timer);
2769
- this.steers.delete(id);
2770
- };
2771
- const fail = (e) => {
2772
- settle();
2773
- if (deliveryId)
2774
- this.delivery({ id: deliveryId, state: this.knownRejection(e) ? "failed_safe" : "needs_review", reason: e.message });
2775
- reject(e);
2776
- };
2777
- this.pending.set(id, {
2778
- deliveryId,
2779
- kind: "steer",
2780
- expectedTurnId,
2781
- afterTurn: deliveryId ? () => {
2782
- if (timer !== undefined)
2783
- return;
2784
- timer = setTimeout(() => {
2785
- const pending = this.pending.get(id);
2786
- this.pending.delete(id);
2787
- pending?.reject(new Error("steer unanswered: RPC timeout"));
2788
- }, this.opts.steerTimeoutMs ?? 1e4);
2789
- timer.unref?.();
2790
- } : undefined,
2791
- resolve: (result) => {
2792
- if (deliveryId && (this.link !== link || this.contextEpoch !== expectedEpoch || !this.activeTurns.has(expectedTurnId) || typeof result?.turnId === "string" && result.turnId !== expectedTurnId)) {
2793
- fail(new Error("steer accepted after its original native turn ended or changed"));
2794
- return;
2795
- }
2796
- settle();
2797
- this.primed = true;
2798
- this.injected = replyParent(this.injected ? [this.injected, ...envs] : envs);
2799
- this.answering.push(...envs);
2800
- if (deliveryId) {
2801
- this.addTurnDelivery(expectedTurnId, deliveryId);
2802
- this.delivery({ id: deliveryId, state: "accepted" });
2803
- }
2804
- resolve();
2805
- },
2806
- reject: fail
2807
- });
2808
- const input = [{ type: "text", text: this.render(envs) }];
2809
- link.up.send(JSON.stringify({ method: "turn/steer", id, params: { threadId: this.threadId, expectedTurnId, input } }));
2810
- });
2811
- }
2812
- steerText(text) {
2813
- const link = this.link;
2814
- const expectedTurnId = [...this.activeTurns].reverse().find((t) => !t.startsWith("unknown:"));
2815
- if (!link || link.up.readyState !== WebSocket.OPEN || !expectedTurnId)
2816
- return Promise.resolve("refused");
2817
- const id = this.nextId--;
2818
- return new Promise((resolve) => {
2819
- const timer = setTimeout(() => this.pending.delete(id) && resolve("unanswered"), this.opts.steerTimeoutMs ?? 1e4);
2820
- timer.unref?.();
2821
- this.pending.set(id, { resolve: () => (clearTimeout(timer), resolve("accepted")), reject: () => (clearTimeout(timer), resolve("refused")) });
2822
- link.up.send(JSON.stringify({ method: "turn/steer", id, params: { threadId: this.threadId, expectedTurnId, input: [{ type: "text", text }] } }));
2823
- });
2824
- }
2825
- onWatchdog() {
2826
- this.deliveryEpoch++;
2827
- const known = new Set([...this.activeTurns, ...this.turnDeliveries.keys()].filter((id) => !id.startsWith("unknown:")));
2828
- const pendingStarts = [...this.pending.values()].filter((pending) => pending.kind === "deliver");
2829
- if ([...this.activeTurns].some((id) => id.startsWith("unknown:")) || (this.unboundDeliveries.size || this.unboundAccepted || pendingStarts.length) && !known.size) {
2830
- this.unidentifiedCancellation = true;
2831
- this.opts.log?.(UNIDENTIFIED_CANCELLATION_NOTICE);
2832
- }
2833
- for (const turnId of known)
2834
- this.retireTurn(turnId);
2835
- for (const pending of pendingStarts)
2836
- pending.invalidate?.("Codex turn watchdog timeout before native acceptance was confirmed");
2837
- this.activeTurns.clear();
2838
- this.unboundAccepted = false;
2839
- for (const ids of this.turnDeliveries.values())
2840
- for (const id of ids)
2841
- this.delivery({ id, state: "needs_review", reason: "Codex turn watchdog timeout" });
2842
- for (const id of this.unboundDeliveries)
2843
- this.delivery({ id, state: "needs_review", reason: "Codex turn watchdog timeout" });
2844
- this.turnDeliveries.clear();
2845
- this.unboundDeliveries.clear();
2846
- this.injected = undefined;
2847
- this.answering = [];
2848
- this.lastAnswer = "";
2849
- this.deltas.clear();
2850
- this.abandonSteers("turn went silent");
2851
- if (this.unidentifiedCancellation)
2852
- this.setState("offline");
2853
- else
2854
- super.onWatchdog();
2855
- }
2856
- retireTurn(turnId) {
2857
- this.retiredTurns.add(turnId);
2858
- if (this.link?.up.readyState === WebSocket.OPEN)
2859
- this.link.up.send(JSON.stringify({ method: "turn/interrupt", id: this.nextId--, params: { threadId: this.threadId, turnId } }));
2860
- }
2861
- abandonSteers(reason) {
2862
- for (const id of this.steers) {
2863
- const p = this.pending.get(id);
2864
- this.pending.delete(id);
2865
- p?.reject(new Error(`steer unanswered: ${reason}`));
2866
- }
2867
- }
2868
- endSteers(turnId) {
2869
- for (const id of this.steers) {
2870
- const pending = this.pending.get(id);
2871
- if (!pending || pending.expectedTurnId !== turnId)
2872
- continue;
2873
- if (pending.deliveryId)
2874
- pending.afterTurn?.();
2875
- else {
2876
- this.pending.delete(id);
2877
- pending.reject(new Error("steer unanswered: turn completed"));
2878
- }
2879
- }
2880
- }
2881
- addTurnDelivery(turnId, deliveryId) {
2882
- const ids = this.turnDeliveries.get(turnId) ?? new Set;
2883
- ids.add(deliveryId);
2884
- this.turnDeliveries.set(turnId, ids);
2885
- this.unboundDeliveries.delete(deliveryId);
2886
- }
2887
- knownRejection(error) {
2888
- return /turn in progress|no active turn|not injectable|rejected|usage limit/i.test(error.message);
2889
- }
2890
- async spawnAppServer() {
2891
- const port = this.opts.appPort;
2892
- const probe = () => fetch(`http://127.0.0.1:${port}/healthz`).then((r) => r.ok, () => false);
2893
- if (await probe()) {
2894
- throw new Error(`port ${port} already answers /healthz: an app-server the hub does not own is running (orphan from a crashed hub?)`);
2750
+ // src/adapters/acp.ts
2751
+ import { spawn as spawn5 } from "child_process";
2752
+ import { createInterface } from "readline";
2753
+ function normalizeACPUsage(value, source = "usage_update") {
2754
+ const unknown = { source, availability: "unsupported", shape: "none" };
2755
+ if (!value || typeof value !== "object" || Array.isArray(value))
2756
+ return unknown;
2757
+ const raw = value;
2758
+ const f = raw.usage && typeof raw.usage === "object" && !Array.isArray(raw.usage) ? raw.usage : raw;
2759
+ const valid = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
2760
+ for (const key of ["totalTokens", "total_tokens"]) {
2761
+ if (f[key] === undefined)
2762
+ continue;
2763
+ const shape = "total";
2764
+ return valid(f[key]) ? { source, availability: "known", shape, total: f[key] } : { source, availability: "invalid", shape };
2765
+ }
2766
+ for (const [input, output] of [["inputTokens", "outputTokens"], ["input_tokens", "output_tokens"]]) {
2767
+ if (f[input] === undefined && f[output] === undefined)
2768
+ continue;
2769
+ const a = f[input], b = f[output];
2770
+ return valid(a) && valid(b) && Number.isFinite(a + b) ? { source, availability: "known", shape: "input-output", total: a + b } : { source, availability: "invalid", shape: "input-output" };
2771
+ }
2772
+ if (f.used !== undefined) {
2773
+ if (!valid(f.used) || !valid(f.size) || !Number.isSafeInteger(f.used) || !Number.isSafeInteger(f.size) || f.size <= 0)
2774
+ return { source, availability: "invalid", shape: "context-used" };
2775
+ return { source, availability: "unsupported", shape: "context-used", contextUsed: f.used, contextCapacity: f.size };
2776
+ }
2777
+ return unknown;
2778
+ }
2779
+ function canonicalMcpToolName(announcedTitle, serverNames) {
2780
+ const m = announcedTitle?.match(/^([\w-]{1,80}) \((.+) MCP Server\)$/);
2781
+ return m && serverNames?.some((name) => name === m[2]) ? `mcp__${m[2]}__${m[1]}` : undefined;
2782
+ }
2783
+ function jsonObject(text) {
2784
+ try {
2785
+ const v = text === undefined ? undefined : JSON.parse(text);
2786
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
2787
+ } catch {
2788
+ return;
2789
+ }
2790
+ }
2791
+ var ACTION_BUDGET_OPTION = "agent-hub:action-budget", HANDSHAKE_MS = 30000, TOOL_INPUT_CAP = 64, TOOL_CEILING_MS, RpcTimeoutError, AcpPeer;
2792
+ var init_acp = __esm(() => {
2793
+ init_envelope();
2794
+ init_kimi_native();
2795
+ init_qwen_native();
2796
+ init_qwen_native();
2797
+ init_child_process();
2798
+ init_acp_owned_group();
2799
+ TOOL_CEILING_MS = 30 * 60000;
2800
+ RpcTimeoutError = class RpcTimeoutError extends Error {
2801
+ };
2802
+ AcpPeer = class AcpPeer extends BasePeer {
2803
+ opts;
2804
+ permissionMode;
2805
+ nativePolicy;
2806
+ modeUnknown = false;
2807
+ kimiAgent = false;
2808
+ qwenAgent = false;
2809
+ availableModes = new Set;
2810
+ proc;
2811
+ ownedGroup;
2812
+ sessionId = "";
2813
+ nextId = 1;
2814
+ pending = new Map;
2815
+ chunks = [];
2816
+ toolInputs = new Map;
2817
+ toolText = new Map;
2818
+ toolTitles = new Map;
2819
+ openTools = new Map;
2820
+ lastSignAt = 0;
2821
+ primed = false;
2822
+ turn = 0;
2823
+ stopping = false;
2824
+ activeDeliveryId;
2825
+ activeLegacy;
2826
+ deliveryAccepted = false;
2827
+ turnEnvs = [];
2828
+ steerIds = [];
2829
+ cancelWait;
2830
+ promptActivity = false;
2831
+ activityOpen = false;
2832
+ activityPrivate = false;
2833
+ activityStub = false;
2834
+ activityStarted = 0;
2835
+ activityPending;
2836
+ activityTimer;
2837
+ emitActivity(kind, text) {
2838
+ if (!this.opts.onActivity || !this.activityOpen)
2839
+ return;
2840
+ try {
2841
+ this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
2842
+ } catch {
2843
+ this.activityPrivate = true;
2895
2844
  }
2896
- let gone = "";
2897
- const env = peerChildEnv(this.id, { ...process.env, ...this.opts.env ?? {} });
2898
- this.proc = spawn5(this.opts.bin ?? "codex", ["app-server", "--listen", `ws://127.0.0.1:${port}`, ...this.opts.extraArgs ?? []], {
2899
- cwd: this.opts.cwd,
2900
- env,
2901
- stdio: ["ignore", "ignore", "pipe"],
2902
- detached: true
2903
- });
2904
- trackGroup(this.proc);
2905
- this.proc.stderr?.on("data", (d) => this.opts.log?.(`[${this.id}] ${String(d).trimEnd()}`));
2906
- this.proc.on("error", (e) => gone = `cannot run ${this.opts.bin ?? "codex"}: ${e.message}`);
2907
- this.proc.on("exit", (code) => {
2908
- gone ||= `codex app-server exited with code ${code}`;
2909
- this.opts.log?.(`[${this.id}] ${gone}`);
2910
- this.setState("offline");
2911
- });
2912
- for (let i = 0;i < 100 && !gone; i++) {
2913
- if (await probe())
2914
- return `ws://127.0.0.1:${port}`;
2915
- await Bun.sleep(100);
2845
+ if (this.activityPrivate) {
2846
+ if (this.activityStub)
2847
+ return;
2848
+ this.activityStub = true;
2849
+ kind = "private";
2850
+ text = "Private turn: activity withheld";
2916
2851
  }
2917
- if (gone)
2918
- throw new Error(gone);
2919
- const proc = this.proc;
2920
- if (proc && await this.stopQuietly(proc) && this.proc === proc)
2921
- this.proc = undefined;
2922
- throw new Error("codex app-server did not become healthy within 10 s");
2852
+ try {
2853
+ this.opts.onActivity({ kind, text: activityText(text), at: Date.now(), turn: this.turn });
2854
+ } catch {}
2923
2855
  }
2924
- attach(tui, upstream) {
2925
- const up = new WebSocket(upstream);
2926
- const link = { tui, up, backlog: [], tracked: new Map, ...tui.data.launchId ? { launchId: tui.data.launchId } : {} };
2927
- this.claimedTui = link;
2928
- tui.data = link;
2929
- up.onopen = () => {
2930
- for (const frame of link.backlog.splice(0))
2931
- up.send(frame);
2932
- };
2933
- up.onmessage = (ev) => this.fromAppServer(link, String(ev.data));
2934
- up.onclose = () => tui.close();
2856
+ flushActivity() {
2857
+ clearTimeout(this.activityTimer);
2858
+ this.activityTimer = undefined;
2859
+ const pending = this.activityPending;
2860
+ this.activityPending = undefined;
2861
+ if (pending)
2862
+ this.emitActivity(pending.kind, pending.text);
2935
2863
  }
2936
- detach(link) {
2937
- link.up.close();
2938
- if (this.claimedTui === link)
2939
- this.claimedTui = undefined;
2940
- if (this.link !== link)
2864
+ updateWatch(u) {
2865
+ if (!this.opts.onActivity)
2941
2866
  return;
2942
- clearInterval(this.usageTimer);
2943
- this.link = undefined;
2944
- this.restoringApprovals.clear();
2945
- this.threadId = "";
2946
- this.contextEpoch++;
2947
- this.deliveryEpoch++;
2948
- this.retiredTurns.clear();
2949
- this.unidentifiedCancellation = false;
2950
- this.activeTurns.clear();
2951
- this.unboundAccepted = false;
2952
- for (const p of this.pending.values()) {
2953
- if (p.kind === "deliver")
2954
- p.invalidate?.("Codex TUI detached");
2867
+ const item = updateActivity(u);
2868
+ if (!item)
2869
+ return;
2870
+ if (item.kind === "message" || item.kind === "thought") {
2871
+ if (this.activityPending?.kind !== item.kind || this.activityPending.text.length + item.text.length > 4096)
2872
+ this.flushActivity();
2873
+ if (this.activityPending)
2874
+ this.activityPending.text += item.text;
2955
2875
  else
2956
- p.reject(new Error(p.kind === "steer" ? "Codex TUI detached" : "codex TUI detached"));
2876
+ this.activityPending = { kind: item.kind, text: item.text };
2877
+ this.activityTimer ??= setTimeout(() => this.flushActivity(), 50);
2878
+ this.activityTimer.unref?.();
2879
+ } else {
2880
+ this.flushActivity();
2881
+ this.emitActivity(item.kind, item.text);
2957
2882
  }
2958
- this.pending.clear();
2959
- for (const ids of this.turnDeliveries.values())
2960
- for (const id of ids)
2961
- this.delivery({ id, state: "needs_review", reason: "Codex TUI detached" });
2962
- for (const id of this.unboundDeliveries)
2963
- this.delivery({ id, state: "needs_review", reason: "Codex TUI detached" });
2964
- this.turnDeliveries.clear();
2965
- this.unboundDeliveries.clear();
2966
- this.forgetTurn();
2967
- this.setState("offline");
2968
2883
  }
2969
- detachForRoster(mode = "ask") {
2970
- this.permissionMode = mode;
2971
- const link = this.claimedTui;
2972
- if (!link)
2973
- return;
2974
- link.tui.close(4000, "codex is off roster; retry when a person adds it");
2975
- this.detach(link);
2884
+ clearActivity() {
2885
+ clearTimeout(this.activityTimer);
2886
+ this.activityTimer = undefined;
2887
+ this.activityPending = undefined;
2976
2888
  }
2977
- forgetTurn() {
2978
- this.injected = undefined;
2979
- this.answering = [];
2980
- this.lastAnswer = "";
2889
+ endActivity(reason, discardPending = false) {
2890
+ if (discardPending)
2891
+ this.clearActivity();
2892
+ else
2893
+ this.flushActivity();
2894
+ this.emitActivity("turn_end", `${reason}; ${Date.now() - this.activityStarted} ms`);
2895
+ this.activityOpen = false;
2981
2896
  }
2982
- get proxyAttached() {
2983
- return !!this.link && this.link.up.readyState === WebSocket.OPEN;
2897
+ permissionWaits = new Set;
2898
+ setState(next) {
2899
+ if (next === "offline") {
2900
+ this.clearActivity();
2901
+ this.cancelPermissions();
2902
+ }
2903
+ super.setState(next);
2904
+ }
2905
+ cancelPermissions() {
2906
+ for (const wait of this.permissionWaits)
2907
+ wait.abort();
2908
+ this.permissionWaits.clear();
2909
+ }
2910
+ constructor(id, opts) {
2911
+ super(id, opts.watchdogMs);
2912
+ this.opts = opts;
2913
+ this.nativePolicy = opts.nativePolicy === undefined ? undefined : { ...opts.nativePolicy, ...opts.nativePolicy.modes === undefined ? {} : { modes: { ...opts.nativePolicy.modes } } };
2914
+ this.permissionMode = this.nativePolicy?.lockedReadOnly ? "ask" : opts.permissionMode ?? "ask";
2915
+ }
2916
+ recoveryMetadata() {
2917
+ return { launch: { kind: "acp", ...this.opts.launchModel ? { model: this.opts.launchModel } : {} }, ...this.sessionId ? { sessionId: this.sessionId } : {} };
2918
+ }
2919
+ async start() {
2920
+ if (this.opts.requireOwnedGroup && this.proc)
2921
+ throw new Error("Configured ACP still has a process; stop it before starting another");
2922
+ this.stopping = false;
2923
+ const [bin, ...args] = this.opts.cmd;
2924
+ const proc = spawn5(bin, args, { cwd: this.opts.cwd, env: peerChildEnv(this.id, { ...process.env, ...this.opts.env ?? {} }), stdio: ["pipe", "pipe", "pipe"], detached: true });
2925
+ this.proc = proc;
2926
+ trackGroup(proc);
2927
+ proc.on("error", (e) => {
2928
+ if (!this.opts.requireOwnedGroup || this.proc === proc)
2929
+ this.down(`spawn failed: ${e.message}`);
2930
+ });
2931
+ proc.on("exit", (code) => {
2932
+ if (!this.opts.requireOwnedGroup || this.proc === proc)
2933
+ this.down(`exited with code ${code}`);
2934
+ });
2935
+ proc.stdin.on("error", () => {});
2936
+ proc.stderr.on("data", (d) => this.opts.log?.(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP stderr output withheld" : `[${this.id}] ${String(d).trimEnd()}`));
2937
+ createInterface({ input: proc.stdout }).on("line", (line) => this.onLine(line));
2938
+ const handshake = async () => {
2939
+ const init = await this.request("initialize", {
2940
+ protocolVersion: 1,
2941
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
2942
+ });
2943
+ this.kimiAgent = this.nativePolicy === undefined && init?.agentInfo?.name === "Kimi Code CLI";
2944
+ const expected = this.opts.expectedAgent, version = init?.agentInfo?.version;
2945
+ if (expected && (init?.agentInfo?.name !== expected.name || !(typeof expected.version === "function" ? expected.version(version) : version === expected.version)))
2946
+ throw new Error("native ACP identity is not verified for this production adapter");
2947
+ const qwen = qwenNativeIdentity(this.id, this.opts.qwen025, init, this.opts.kind);
2948
+ this.qwenAgent = this.nativePolicy === undefined && qwen;
2949
+ const resume = this.opts.resumeSessionId;
2950
+ if (!resume)
2951
+ return this.request("session/new", { cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
2952
+ if (!init?.agentCapabilities?.loadSession)
2953
+ throw new Error(`${this.id} cannot load an earlier session (the agent offers no loadSession)`);
2954
+ const loaded = await this.request("session/load", { sessionId: resume, cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
2955
+ if (loaded?.sessionId !== undefined && loaded.sessionId !== resume || this.opts.requireLoadSessionEcho && loaded?.sessionId !== resume)
2956
+ throw new Error("native ACP load did not confirm the requested conversation; no restored session is claimed");
2957
+ return { ...loaded, sessionId: resume };
2958
+ };
2959
+ try {
2960
+ if (this.opts.requireOwnedGroup) {
2961
+ const proof = await AcpOwnedGroup.capture(proc);
2962
+ this.ownedGroup = { proc, proof };
2963
+ if (this.proc !== proc || this.stopping)
2964
+ throw new Error("Configured ACP process ownership could not be verified");
2965
+ }
2966
+ const timeout = new Promise((_, reject) => {
2967
+ setTimeout(() => reject(new Error(`${this.id} did not complete the ACP handshake within ${HANDSHAKE_MS / 1000} s`)), HANDSHAKE_MS).unref();
2968
+ });
2969
+ const session = await Promise.race([handshake(), timeout]);
2970
+ if (this.nativePolicy !== undefined && (typeof session?.sessionId !== "string" || !session.sessionId.trim() || Buffer.byteLength(session.sessionId) > 4096 || /[\p{Cc}\p{Cf}]/u.test(session.sessionId))) {
2971
+ throw new Error("Configured ACP did not provide a valid native session identity");
2972
+ }
2973
+ this.sessionId = session.sessionId;
2974
+ const modes = session.modes?.availableModes;
2975
+ this.availableModes = new Set(Array.isArray(modes) ? modes.flatMap((mode) => typeof mode?.id === "string" ? [mode.id] : []) : []);
2976
+ const resetResumed = this.opts.resumeSessionId && typeof session.modes?.currentModeId === "string" && session.modes.currentModeId !== KIMI_MODE_IDS.ask;
2977
+ if (this.nativePolicy?.lockedReadOnly)
2978
+ await this.applyNativeMode(this.nativePolicy.readOnly, "ask");
2979
+ else if (this.nativePolicy?.modes !== undefined || this.qwenAgent || this.kimiAgent && (this.permissionMode !== "ask" || resetResumed))
2980
+ await this.setPermissionMode(this.permissionMode);
2981
+ if (this.opts.requireOwnedGroup) {
2982
+ const owned = this.ownedGroup;
2983
+ if (!owned || owned.proc !== proc || this.proc !== proc || this.stopping)
2984
+ throw new Error("Configured ACP process ownership could not be verified");
2985
+ await owned.proof.verify();
2986
+ if (this.proc !== proc || this.stopping)
2987
+ throw new Error("Configured ACP process ownership could not be verified");
2988
+ }
2989
+ } catch (e) {
2990
+ this.setState("offline");
2991
+ if (this.proc === proc) {
2992
+ if (this.opts.requireOwnedGroup) {
2993
+ const owned = this.ownedGroup;
2994
+ if (owned?.proc === proc) {
2995
+ await owned.proof.stop().then(() => {
2996
+ if (this.proc === proc) {
2997
+ this.proc = undefined;
2998
+ this.ownedGroup = undefined;
2999
+ }
3000
+ }, () => this.opts.log?.("Configured ACP process group shutdown incomplete"));
3001
+ } else {
3002
+ proc.stdin.end();
3003
+ if (await AcpOwnedGroup.confirmGone(proc) && this.proc === proc) {
3004
+ this.proc = undefined;
3005
+ this.ownedGroup = undefined;
3006
+ } else
3007
+ this.opts.log?.("Configured ACP process ownership could not be verified; no process signals were sent");
3008
+ }
3009
+ } else
3010
+ await stopOwnedProcess(proc, { group: true }).then(() => {
3011
+ if (this.proc === proc)
3012
+ this.proc = undefined;
3013
+ }, (stop) => this.opts.log?.(`[${this.id}] ${stop.message}`));
3014
+ }
3015
+ throw e;
3016
+ }
3017
+ this.setState("idle");
2984
3018
  }
2985
3019
  getPermissionMode() {
2986
3020
  return this.permissionMode;
2987
3021
  }
2988
- get permissionModeState() {
2989
- return this.getPermissionMode();
3022
+ get actionGated() {
3023
+ return this.opts.actionGate !== undefined;
2990
3024
  }
2991
- setStartupPermissionMode(mode) {
2992
- if (this.link || this.state !== "offline" || this.overriddenApprovals.size)
2993
- throw new Error("codex startup mode reconciliation requires a fresh unattached proxy");
2994
- this.permissionMode = mode;
3025
+ get pendingOwnedProcess() {
3026
+ return this.opts.requireOwnedGroup === true && this.proc !== undefined;
2995
3027
  }
2996
- clearPermissionMode() {
2997
- const restorable = [...this.overriddenApprovals].every((thread) => this.nativeApprovalPolicies.has(thread));
2998
- this.permissionMode = "ask";
2999
- return restorable;
3028
+ get permissionModeState() {
3029
+ return this.nativePolicy?.lockedReadOnly || this.nativePolicy?.modes !== undefined || this.kimiAgent || this.qwenAgent ? this.modeUnknown ? "unknown" : this.permissionMode : "unmanaged";
3000
3030
  }
3001
3031
  async setPermissionMode(mode) {
3002
- if (!this.link || this.link.up.readyState !== WebSocket.OPEN)
3003
- throw new Error("codex permission mode unavailable: attach the TUI through ahub codex first");
3004
- if (mode === "ask" && [...this.overriddenApprovals].some((thread) => !this.nativeApprovalPolicies.has(thread)))
3005
- throw new Error("codex cannot restore ask: native approval policy unavailable; close the TUI, then run ahub permission codex ask before you restart the Codex session with ahub codex");
3006
- this.permissionMode = mode;
3007
- }
3008
- applyPermissionPolicy(params, id) {
3009
- const thread = typeof params.threadId === "string" ? params.threadId : this.threadId;
3010
- if (this.permissionMode !== "ask") {
3011
- this.overriddenApprovals.add(thread);
3012
- return { ...params, approvalPolicy: this.permissionMode === "never-ask" ? "never" : "on-request" };
3013
- }
3014
- if (this.overriddenApprovals.has(thread) && this.nativeApprovalPolicies.has(thread)) {
3015
- this.restoringApprovals.set(id, thread);
3016
- return { ...params, approvalPolicy: this.nativeApprovalPolicies.get(thread) };
3017
- }
3018
- return params;
3019
- }
3020
- settlePermissionRestore(id, failed) {
3021
- const thread = this.restoringApprovals.get(id);
3022
- this.restoringApprovals.delete(id);
3023
- if (thread !== undefined && !failed && this.permissionMode === "ask")
3024
- this.overriddenApprovals.delete(thread);
3032
+ if (this.nativePolicy?.lockedReadOnly)
3033
+ throw new Error(`${this.id} permission mode is locked read-only; runtime changes are refused`);
3034
+ const modes = this.nativePolicy !== undefined ? this.nativePolicy.modes : this.qwenAgent ? QWEN_MODE_IDS : this.kimiAgent ? KIMI_MODE_IDS : undefined;
3035
+ if (modes === undefined)
3036
+ throw new Error(`${this.id} permission mode ${mode} unmanaged: this ACP agent has no verified mode mapping; use Kimi Code CLI or add an agent-specific mapping`);
3037
+ await this.applyNativeMode(modes[mode], mode);
3025
3038
  }
3026
- fromTui(link, raw) {
3027
- const msg = parse(raw);
3028
- if (msg?.method === "turn/start" && msg.params && link === this.link) {
3029
- const thread = typeof msg.params.threadId === "string" ? msg.params.threadId : this.threadId;
3030
- const restoring = this.overriddenApprovals.has(thread) && this.nativeApprovalPolicies.has(thread);
3031
- if (msg.params.approvalPolicy != null && !restoring)
3032
- this.nativeApprovalPolicies.set(thread, msg.params.approvalPolicy);
3033
- if (this.permissionMode !== "ask" || this.overriddenApprovals.has(thread)) {
3034
- msg.params = this.applyPermissionPolicy(msg.params, msg.id);
3035
- raw = JSON.stringify(msg);
3039
+ async applyNativeMode(id, mode) {
3040
+ if (this.modeUnknown)
3041
+ throw new Error(`${this.id} permission mode unknown after an unanswered change; restart the peer before changing modes`);
3042
+ if (typeof id !== "string" || !id || !this.sessionId || !this.proc || !this.availableModes.has(id))
3043
+ throw new Error(`${this.id} permission mode ${mode} unavailable: session does not offer ${id}`);
3044
+ try {
3045
+ await this.request("session/set_mode", { sessionId: this.sessionId, modeId: id }, this.opts.permissionModeTimeoutMs ?? HANDSHAKE_MS);
3046
+ } catch (error) {
3047
+ if (error instanceof RpcTimeoutError) {
3048
+ this.modeUnknown = true;
3049
+ this.turn++;
3050
+ this.setState("offline");
3051
+ await this.stop().catch((stop) => this.opts.log?.(`[${this.id}] ${stop.message}`));
3052
+ throw new Error(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP native permission mode unknown; peer is offline, restart it before sending work" : `${this.id} permission mode ${mode} unknown: ${error.message}; peer is offline, restart it before sending work`);
3036
3053
  }
3054
+ throw new Error(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP native permission mode refused" : `${this.id} permission mode ${mode} refused: ${error.message}`);
3037
3055
  }
3038
- if (msg?.id !== undefined && TRACKED.has(msg.method))
3039
- link.tracked.set(msg.id, msg.method);
3040
- if (link.up.readyState === WebSocket.OPEN)
3041
- link.up.send(raw);
3042
- else
3043
- link.backlog.push(raw);
3056
+ if (this.cancelWait?.reported)
3057
+ await this.cancelWait.released;
3058
+ if (this.modeUnknown)
3059
+ throw new Error(`${this.id} permission mode unknown; restart the peer`);
3060
+ this.permissionMode = mode;
3044
3061
  }
3045
- fromAppServer(link, raw) {
3046
- const msg = parse(raw);
3047
- const nativeId = msg?.params?.turnId ?? msg?.params?.turn?.id ?? msg?.result?.turn?.id;
3048
- if (this.state === "busy" && !(typeof nativeId === "string" && this.retiredTurns.has(nativeId)))
3049
- this.touch();
3050
- if (!msg)
3051
- return void link.tui.send(raw);
3052
- if (msg.id !== undefined && !msg.method)
3053
- this.settlePermissionRestore(msg.id, !!msg.error);
3054
- if (typeof msg.id === "number" && msg.id < 0 && !msg.method) {
3055
- const p = this.pending.get(msg.id);
3056
- this.pending.delete(msg.id);
3057
- if (msg.error)
3058
- p?.reject(new Error(msg.error.message ?? "app-server rejected the request"));
3059
- else
3060
- p?.resolve(msg.result);
3062
+ async stop(reason) {
3063
+ if (this.activityOpen)
3064
+ this.endActivity("stopped", true);
3065
+ this.stopping = true;
3066
+ this.cancelPermissions();
3067
+ this.clearCancelWait();
3068
+ const legacy = this.activeLegacy;
3069
+ this.activeLegacy = undefined;
3070
+ if (legacy?.turn === this.turn)
3071
+ this.onFailed?.(legacy.envs);
3072
+ if (reason !== undefined)
3073
+ this.turn++;
3074
+ if (this.activeDeliveryId)
3075
+ this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason: reason ?? "ACP session stopped before settlement" });
3076
+ this.settleSteers("needs_review", reason ?? "ACP session stopped before settlement");
3077
+ this.activeDeliveryId = undefined;
3078
+ const proc = this.proc;
3079
+ if (!proc) {
3080
+ this.setState("offline");
3061
3081
  return;
3062
3082
  }
3063
- if (typeof msg.result?.userAgent === "string")
3064
- this.version = /^[^/\s]+\/(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(msg.result.userAgent)?.[1] ?? this.version;
3065
- const tracked = msg.id !== undefined && !msg.method ? link.tracked.get(msg.id) : undefined;
3066
- if (tracked && link.tracked.delete(msg.id)) {
3067
- if (typeof msg.result?.thread?.id === "string") {
3068
- const thread = msg.result.thread.id;
3069
- if (!this.overriddenApprovals.has(thread) && msg.result.approvalPolicy != null)
3070
- this.nativeApprovalPolicies.set(thread, msg.result.approvalPolicy);
3071
- this.adopt(link, msg.result.thread.id, tracked === "thread/start");
3072
- }
3073
- } else if (msg.method)
3074
- this.onNotification(link, msg.method, msg.params ?? {});
3075
- link.tui.send(raw);
3076
- }
3077
- adopt(link, threadId, fresh = false) {
3078
- if (typeof threadId !== "string" || !threadId || link !== this.claimedTui || link.tui.readyState !== WebSocket.OPEN || link.up.readyState !== WebSocket.OPEN)
3079
- return;
3080
- this.abandonSteers("thread changed");
3081
- for (const [id, pending] of this.pending)
3082
- if (pending.kind === "deliver") {
3083
- pending.invalidate?.("Codex native thread changed before acceptance was confirmed");
3084
- this.pending.delete(id);
3083
+ if (this.opts.requireOwnedGroup) {
3084
+ this.setState("offline");
3085
+ const owned = this.ownedGroup;
3086
+ if (!owned) {
3087
+ if (await AcpOwnedGroup.confirmGone(proc) && this.proc === proc) {
3088
+ this.proc = undefined;
3089
+ return;
3090
+ }
3091
+ throw new Error("Configured ACP process ownership could not be verified");
3085
3092
  }
3086
- for (const ids of this.turnDeliveries.values())
3087
- for (const id of ids)
3088
- this.delivery({ id, state: "needs_review", reason: "Codex native thread changed" });
3089
- for (const id of this.unboundDeliveries)
3090
- this.delivery({ id, state: "needs_review", reason: "Codex native thread changed" });
3091
- this.turnDeliveries.clear();
3092
- this.unboundDeliveries.clear();
3093
- this.tokenTotal = undefined;
3094
- this.tokenKinds = undefined;
3095
- this.freshThread = fresh;
3096
- this.link = link;
3097
- this.threadId = threadId;
3098
- this.contextEpoch++;
3099
- this.deliveryEpoch++;
3100
- this.retiredTurns.clear();
3101
- this.unidentifiedCancellation = false;
3102
- this.activeTurns.clear();
3103
- this.unboundAccepted = false;
3104
- this.forgetTurn();
3105
- this.opts.log?.(`[${this.id}] thread ${threadId}`);
3106
- this.opts.onThread?.(threadId, fresh);
3107
- this.setState("idle");
3108
- this.readUsage();
3109
- clearInterval(this.usageTimer);
3110
- this.usageTimer = setInterval(() => this.readUsage(), this.opts.usagePollMs ?? 600000);
3111
- this.usageTimer.unref?.();
3112
- }
3113
- revert(turnId) {
3114
- const link = this.link;
3115
- if (this.state !== "idle" || !link || link.up.readyState !== WebSocket.OPEN)
3116
- return Promise.reject(new Error(`${this.id} is not idle`));
3117
- const id = this.nextId--;
3118
- return new Promise((resolve, reject) => {
3119
- const timer = setTimeout(() => this.pending.delete(id) && reject(new Error("thread/revert got no answer")), 30000);
3120
- timer.unref?.();
3121
- this.pending.set(id, { resolve: () => (clearTimeout(timer), resolve()), reject: (e) => (clearTimeout(timer), reject(e)) });
3122
- link.up.send(JSON.stringify({ method: "thread/revert", id, params: { threadId: this.threadId, beforeTurnId: turnId } }));
3123
- });
3124
- }
3125
- readUsage() {
3126
- const link = this.link;
3127
- if (!this.opts.onUsage || !link || link.up.readyState !== WebSocket.OPEN)
3128
- return;
3129
- const id = this.nextId--;
3130
- this.pending.set(id, { resolve: (result) => this.opts.onUsage?.(result?.rateLimits, false), reject: () => {} });
3131
- link.up.send(JSON.stringify({ method: "account/rateLimits/read", id }));
3132
- setTimeout(() => this.pending.delete(id), 30000).unref?.();
3093
+ if (owned.proc !== proc)
3094
+ throw new Error("Configured ACP process ownership could not be verified");
3095
+ await owned.proof.stop();
3096
+ if (this.ownedGroup === owned)
3097
+ this.ownedGroup = undefined;
3098
+ } else
3099
+ await stopOwnedProcess(proc, { group: true });
3100
+ if (this.proc === proc)
3101
+ this.proc = undefined;
3102
+ this.setState("offline");
3133
3103
  }
3134
- onNotification(link, method, params) {
3135
- if (link === this.link && method === "account/rateLimits/updated")
3136
- return void this.opts.onUsage?.(params.rateLimits, false);
3137
- if (link === this.link && method === "error" && params.error?.codexErrorInfo === "usageLimitExceeded") {
3138
- this.opts.onUsage?.({ rateLimitReachedType: "usageLimitExceeded" }, true);
3139
- }
3140
- if (link !== this.link || params.threadId !== this.threadId)
3141
- return;
3142
- const nativeId = method === "turn/started" || method === "turn/completed" ? params.turn?.id : params.turnId;
3143
- if (typeof nativeId === "string" && this.retiredTurns.has(nativeId)) {
3144
- if (method === "turn/completed")
3145
- this.retiredTurns.delete(nativeId);
3146
- return;
3104
+ async deliver(envs, deliveryId) {
3105
+ if (this.state !== "idle" || this.stopping) {
3106
+ if (deliveryId)
3107
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is ${this.state}` });
3108
+ throw new Error(`${this.id} is ${this.state}`);
3147
3109
  }
3148
- if (this.unidentifiedCancellation)
3149
- return;
3150
- const unknownTurn = () => [...this.activeTurns].some((id) => id.startsWith("unknown:"));
3151
- if (method === "thread/tokenUsage/updated") {
3152
- const reader = this.opts.contextReader;
3153
- this.opts.onContext?.(reader ? reader.read(params.tokenUsage, this.threadId, Date.now()) : codexContext(params.tokenUsage, this.threadId, Date.now()));
3154
- const total = Number(params.tokenUsage?.total?.totalTokens);
3155
- if (!Number.isFinite(total))
3156
- return;
3157
- const previous = this.tokenKinds;
3158
- const kinds = {
3159
- input: counter(params.tokenUsage?.total?.inputTokens),
3160
- cachedInput: counter(params.tokenUsage?.total?.cachedInputTokens),
3161
- output: counter(params.tokenUsage?.total?.outputTokens),
3162
- reasoning: counter(params.tokenUsage?.total?.reasoningOutputTokens)
3163
- };
3164
- const added = this.tokenTotal === undefined ? this.freshThread ? total : 0 : Math.max(0, total - this.tokenTotal);
3165
- const base = previous ?? (this.freshThread && this.tokenTotal === undefined ? { input: 0, cachedInput: 0, output: 0, reasoning: 0 } : undefined);
3166
- this.tokenTotal = total;
3167
- this.tokenKinds = kinds;
3168
- if (added > 0)
3169
- this.opts.onTokens?.(added);
3170
- if (added > 0 && base)
3171
- this.opts.onTokenBreakdown?.({
3172
- input: Math.max(0, kinds.input - base.input),
3173
- cachedInput: Math.max(0, kinds.cachedInput - base.cachedInput),
3174
- output: Math.max(0, kinds.output - base.output),
3175
- reasoning: Math.max(0, kinds.reasoning - base.reasoning),
3176
- total: added
3177
- });
3178
- return;
3110
+ const turn = ++this.turn;
3111
+ this.activeDeliveryId = deliveryId;
3112
+ this.activeLegacy = deliveryId ? undefined : { turn, envs };
3113
+ this.turnEnvs = envs;
3114
+ this.steerIds = [];
3115
+ this.deliveryAccepted = false;
3116
+ this.promptActivity = false;
3117
+ this.chunks = [];
3118
+ this.openTools.clear();
3119
+ this.activityOpen = true;
3120
+ this.activityPrivate = envs.some((env) => env.private === true);
3121
+ try {
3122
+ this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
3123
+ } catch {
3124
+ this.activityPrivate = true;
3179
3125
  }
3180
- if (method === "turn/started") {
3181
- const nativeTurn = params.turn?.id ?? `unknown:${Date.now()}`;
3182
- this.unboundAccepted = false;
3183
- this.activeTurns.add(nativeTurn);
3184
- for (const id of [...this.unboundDeliveries])
3185
- this.addTurnDelivery(nativeTurn, id);
3186
- this.lastAnswer = "";
3187
- this.setState("busy");
3188
- if (params.turn?.id)
3189
- this.opts.onTurn?.(params.turn.id);
3190
- } else if (method === "item/agentMessage/delta") {
3191
- if (!this.activeTurns.size || typeof params.turnId === "string" && !this.activeTurns.has(params.turnId) && !unknownTurn())
3192
- return;
3193
- const buf = this.deltas.get(params.itemId) ?? [];
3194
- buf.push(params.delta);
3195
- this.deltas.set(params.itemId, buf);
3196
- } else if (method === "item/completed" && ["fileChange", "commandExecution", "mcpToolCall", "userMessage"].includes(params.item?.type)) {
3197
- if (typeof params.turnId === "string" && !this.activeTurns.has(params.turnId) && !unknownTurn())
3126
+ this.activityStub = false;
3127
+ this.activityStarted = Date.now();
3128
+ this.setState("busy");
3129
+ this.emitActivity("delivery", envs.map((env) => `${env.from} ${env.id}`).join(", "));
3130
+ const prompt = this.request("session/prompt", { sessionId: this.sessionId, prompt: [{ type: "text", text: this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}
3131
+
3132
+ ${renderDigest(envs, false)}` }] });
3133
+ const reported = () => {
3134
+ if (this.cancelWait?.turn === turn)
3135
+ this.cancelWait.reported = true;
3136
+ };
3137
+ prompt.then(reported, reported);
3138
+ prompt.then((result) => {
3139
+ if (turn !== this.turn)
3198
3140
  return;
3199
- try {
3200
- this.opts.onItem?.(params.item, typeof params.turnId === "string" && this.activeTurns.has(params.turnId) ? params.turnId : undefined);
3201
- } catch (error) {
3202
- this.opts.log?.(`[${this.id}] item handler failed: ${error.message}`);
3141
+ if (this.activeLegacy?.turn === turn)
3142
+ this.activeLegacy = undefined;
3143
+ this.endActivity(typeof result?.stopReason === "string" ? result.stopReason : "unknown", result?.stopReason === "cancelled");
3144
+ this.observeUsage(result, "prompt_result");
3145
+ this.primed = true;
3146
+ const body = this.chunks.join("").trim();
3147
+ const answering = this.turnEnvs.length ? this.turnEnvs : envs;
3148
+ if (body)
3149
+ this.onMessage?.(body, { inReplyTo: replyParent(answering), to: replyAudience(answering) });
3150
+ if (result?.stopReason !== "end_turn")
3151
+ this.reportTurnFailure(envs, `ACP prompt ended without normal completion (${String(result?.stopReason ?? "unknown")})`);
3152
+ if (deliveryId && this.activeDeliveryId === deliveryId) {
3153
+ this.acceptDelivery();
3154
+ this.delivery({ id: deliveryId, state: result?.stopReason === "end_turn" ? "completed" : "needs_review", ...result?.stopReason === "end_turn" ? {} : { reason: "ACP prompt ended without normal completion" } });
3203
3155
  }
3204
- } else if (method === "item/completed" && params.item?.type === "agentMessage") {
3205
- if (!this.activeTurns.size || typeof params.turnId === "string" && !this.activeTurns.has(params.turnId) && !unknownTurn())
3206
- return;
3207
- const item = params.item;
3208
- const text = item.text ?? item.content?.map((c) => c.text ?? "").join("") ?? this.deltas.get(item.id)?.join("") ?? "";
3209
- this.deltas.delete(item.id);
3210
- if (item.phase !== "commentary" && text.trim())
3211
- this.lastAnswer = text.trim();
3212
- } else if (method === "turn/completed") {
3213
- const nativeTurn = params.turn?.id;
3214
- if (!this.activeTurns.size || typeof nativeTurn === "string" && !this.activeTurns.has(nativeTurn) && ![...this.activeTurns].some((id) => id.startsWith("unknown:")))
3156
+ this.settleSteers(result?.stopReason === "end_turn" ? "completed" : "needs_review", "ACP prompt ended without normal completion");
3157
+ }).catch((e) => {
3158
+ if (turn !== this.turn) {
3159
+ if (this.cancelWait?.turn === turn)
3160
+ this.opts.log?.(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP cancelled prompt rejected; native details withheld" : `[${this.id}] cancelled prompt rejected: ${sanitize(e.message)}`);
3215
3161
  return;
3216
- this.activeTurns.delete(nativeTurn);
3217
- for (const id of this.activeTurns)
3218
- if (id.startsWith("unknown:"))
3219
- this.activeTurns.delete(id);
3220
- if (params.turn?.status === "failed") {
3221
- this.opts.log?.(`[${this.id}] turn failed: ${params.turn.error?.message ?? "unknown error"}`);
3222
3162
  }
3223
- this.endSteers(nativeTurn);
3224
- if (this.activeTurns.size)
3163
+ this.endActivity("failed", true);
3164
+ const reason = this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP prompt failed" : sanitize(e.message);
3165
+ this.settleSteers("needs_review", reason);
3166
+ this.opts.log?.(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP prompt failed; native details withheld" : `[${this.id}] prompt failed: ${reason}`);
3167
+ const quota = !this.stopping && this.kimiAgent ? acpQuotaRefusal(e.message) : undefined;
3168
+ if (quota) {
3169
+ let paused = false;
3170
+ try {
3171
+ paused = this.opts.onQuotaRefusal?.(quota) === true;
3172
+ } catch (error) {
3173
+ this.opts.log?.(`[${this.id}] quota pause failed: ${sanitize(error instanceof Error ? error.message : String(error))}`);
3174
+ }
3175
+ const safe = paused && !this.deliveryAccepted && !this.promptActivity;
3176
+ if (deliveryId && this.activeDeliveryId === deliveryId)
3177
+ this.delivery({ id: deliveryId, state: safe ? "failed_safe" : "needs_review", reason, ...safe ? { quota: true } : {} });
3178
+ else if (!deliveryId && this.activeLegacy?.turn === turn) {
3179
+ this.activeLegacy = undefined;
3180
+ this.onFailed?.(envs, safe ? { quota: true } : undefined);
3181
+ }
3225
3182
  return;
3226
- if (typeof nativeTurn === "string") {
3227
- for (const id of this.turnDeliveries.get(nativeTurn) ?? [])
3228
- this.delivery({ id, state: params.turn?.status === "failed" ? "needs_review" : "completed", ...params.turn?.status === "failed" ? { reason: params.turn?.error?.message ?? "Codex turn failed" } : {} });
3229
- this.turnDeliveries.delete(nativeTurn);
3230
3183
  }
3231
- const inReplyTo = this.injected;
3232
- const to = replyAudience(this.answering);
3233
- this.injected = undefined;
3234
- this.answering = [];
3235
- this.deltas.clear();
3236
- if (this.lastAnswer)
3237
- this.onMessage?.(this.lastAnswer, inReplyTo ? { inReplyTo, to } : {});
3238
- this.lastAnswer = "";
3239
- this.setState("idle");
3240
- }
3241
- }
3242
- };
3243
- });
3244
-
3245
- // src/hub/activity.ts
3246
- function activityText(value) {
3247
- return typeof value === "string" ? value.slice(0, TEXT_CAP) : "";
3248
- }
3249
- function updateActivity(u) {
3250
- if (u?.sessionUpdate === "agent_message_chunk" || u?.sessionUpdate === "agent_thought_chunk") {
3251
- if (u.content?.type !== "text" || typeof u.content.text !== "string")
3252
- return;
3253
- return { kind: u.sessionUpdate === "agent_message_chunk" ? "message" : "thought", text: activityText(u.content.text) };
3254
- }
3255
- if (["tool_call", "tool_call_update"].includes(u?.sessionUpdate)) {
3256
- const fields = [u.toolCallId, u.title, u.kind, u.status].filter((v) => typeof v === "string").map(activityText);
3257
- for (const location of Array.isArray(u.locations) ? u.locations.slice(0, 12) : []) {
3258
- if (typeof location?.path === "string")
3259
- fields.push(activityText(location.path));
3260
- }
3261
- for (const c of Array.isArray(u.content) ? u.content.slice(0, 12) : []) {
3262
- if (c?.type === "diff" && typeof c.path === "string") {
3263
- const lines = (v) => typeof v === "string" && v ? v.split(`
3264
- `).length : 0;
3265
- fields.push(`+${lines(c.newText)} -${lines(c.oldText)} ${activityText(c.path)}`);
3266
- } else if (c?.type === "content" && c.content?.type === "text") {
3267
- fields.push(activityText(c.content.text).split(`
3268
- `).slice(-8).join(`
3269
- `));
3270
- } else if (c?.type === "terminal")
3271
- fields.push("terminal output not reported by this update");
3272
- }
3273
- return { kind: "tool", text: fields.join(" ").slice(0, TEXT_CAP) };
3274
- }
3275
- if (u?.sessionUpdate === "plan" && Array.isArray(u.entries))
3276
- return { kind: "plan", text: u.entries.slice(0, 32).map((e) => [e?.status, e?.priority, e?.content].filter((v) => typeof v === "string").map(activityText).join(" ")).join(`
3277
- `).slice(0, TEXT_CAP) };
3278
- }
3279
- var TEXT_CAP = 4096;
3280
-
3281
- // src/adapters/acp-quota.ts
3282
- function acpQuotaRefusal(message) {
3283
- if (!message.startsWith(KIMI_2_1_1_5H_REFUSAL_PREFIX))
3284
- return;
3285
- return { id: "5h", used: 1, windowMins: 300, source: "kimi ACP usage-limit refusal" };
3286
- }
3287
- var KIMI_2_1_1_5H_REFUSAL_PREFIX = "Authentication required: 403 You've reached your 5-hour usage limit. Your quota will reset when the current 5-hour window ends.";
3288
-
3289
- // src/adapters/definitions/kimi-native.ts
3290
- var KIMI_MODE_IDS;
3291
- var init_kimi_native = __esm(() => {
3292
- KIMI_MODE_IDS = { ask: "default", "ask-when-needed": "yolo", "never-ask": "auto" };
3293
- });
3294
-
3295
- // src/adapters/definitions/qwen-native.ts
3296
- function qwenNativeIdentity(instance, production, init, kind) {
3297
- const native = init;
3298
- const identity = production === true && (kind ?? instance) === "qwen" && native?.agentInfo?.name === "qwen-code";
3299
- const version = identity ? assessVersion("qwen", native?.agentInfo?.version) : undefined;
3300
- if (production && !version?.ok)
3301
- throw new QwenIdentityError(version && !version.ok ? version.reason : undefined);
3302
- const verified = !!version?.ok;
3303
- return verified;
3304
- }
3305
- function buildQwenLaunch(command, model) {
3306
- const [cmd, ...args] = command;
3307
- if (!cmd)
3308
- throw new Error("qwen_cmd must name an executable");
3309
- return { cmd, args: [...args, ...model ? ["--model", model] : []] };
3310
- }
3311
- var QWEN_MODE_IDS, QwenIdentityError, qwenSessionId = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
3312
- var init_qwen_native = __esm(() => {
3313
- init_compat();
3314
- QWEN_MODE_IDS = { ask: "default", "ask-when-needed": "auto-edit", "never-ask": "yolo" };
3315
- QwenIdentityError = class QwenIdentityError extends Error {
3316
- constructor(reason) {
3317
- super(reason ? `Qwen Code native identity is not verified: ${reason}` : "Qwen Code native identity is not verified; check the executable and its advertised ACP version");
3318
- }
3319
- };
3320
- });
3321
-
3322
- // src/adapters/acp-owned-group.ts
3323
- class AcpOwnedGroup {
3324
- proc;
3325
- leader;
3326
- ports;
3327
- confirmedGone = false;
3328
- constructor(proc, leader, ports) {
3329
- this.proc = proc;
3330
- this.leader = leader;
3331
- this.ports = ports;
3332
- }
3333
- static async capture(proc, ports = PORTS) {
3334
- const pid = proc.pid;
3335
- if (!Number.isSafeInteger(pid) || !pid || pid <= 1 || proc.exitCode !== null || proc.signalCode !== null)
3336
- throw ownershipError();
3337
- const rows = await AcpOwnedGroup.read(ports);
3338
- const leader = rows.find((row) => row.pid === pid);
3339
- if (!leader || leader.pgid !== pid || leader.ppid !== process.pid)
3340
- throw ownershipError();
3341
- const signature = AcpOwnedGroup.identity(ports, pid);
3342
- if (!signature || proc.pid !== pid || proc.exitCode !== null || proc.signalCode !== null)
3343
- throw ownershipError();
3344
- return new AcpOwnedGroup(proc, { pid, ppid: leader.ppid, pgid: leader.pgid, started: leader.started }, ports);
3345
- }
3346
- static async confirmGone(proc, ports = PORTS) {
3347
- const pid = proc.pid;
3348
- if (pid === undefined)
3349
- return true;
3350
- if (!Number.isSafeInteger(pid) || pid <= 1 || !(typeof proc.exitCode === "number" || typeof proc.signalCode === "string"))
3351
- return false;
3352
- try {
3353
- const rows = await AcpOwnedGroup.read(ports);
3354
- return proc.pid === pid && (typeof proc.exitCode === "number" || typeof proc.signalCode === "string") && !rows.some((row) => row.pid === pid || row.pgid === pid);
3355
- } catch {
3356
- return false;
3357
- }
3358
- }
3359
- async verify() {
3360
- if (this.confirmedGone || !await this.inspect(false))
3361
- throw ownershipError();
3362
- }
3363
- async stop({ killMs = 2000 } = {}) {
3364
- if (this.confirmedGone)
3365
- return;
3366
- if (!Number.isFinite(killMs) || killMs <= 0 || killMs > 1e4)
3367
- throw shutdownError();
3368
- if (!await this.inspect(true)) {
3369
- this.confirmedGone = true;
3370
- return;
3184
+ this.reportTurnFailure(envs, reason);
3185
+ if (deliveryId && this.activeDeliveryId === deliveryId)
3186
+ this.delivery({ id: deliveryId, state: "needs_review", reason });
3187
+ else if (!deliveryId && this.activeLegacy?.turn === turn) {
3188
+ this.activeLegacy = undefined;
3189
+ this.onFailed?.(envs);
3190
+ }
3191
+ }).finally(() => {
3192
+ const waiting = this.cancelWait;
3193
+ if (waiting?.turn === turn) {
3194
+ this.clearCancelWait();
3195
+ if (!this.stopping && this.turn === waiting.successor && this.state === "busy")
3196
+ this.setState("idle");
3197
+ return;
3198
+ }
3199
+ if (turn === this.turn && !this.stopping && this.state === "busy")
3200
+ this.setState("idle");
3201
+ if (this.activeLegacy?.turn === turn)
3202
+ this.activeLegacy = undefined;
3203
+ if (turn === this.turn && this.activeDeliveryId === deliveryId)
3204
+ this.activeDeliveryId = undefined;
3205
+ });
3371
3206
  }
3372
- this.signal("SIGSTOP");
3373
- if (!await this.inspect(true)) {
3374
- this.confirmedGone = true;
3375
- return;
3207
+ reportTurnFailure(envs, reason) {
3208
+ try {
3209
+ const result = this.opts.onTurnFailure?.(envs, this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP prompt failed" : reason);
3210
+ if (result)
3211
+ result.catch(() => this.opts.log?.("ACP failure handoff could not be completed"));
3212
+ } catch {
3213
+ this.opts.log?.("ACP failure handoff could not be completed");
3214
+ }
3376
3215
  }
3377
- this.signal("SIGKILL");
3378
- const end = this.ports.now() + killMs;
3379
- for (;; ) {
3380
- const left = end - this.ports.now();
3381
- if (left <= 0)
3382
- throw shutdownError();
3383
- const rows = await AcpOwnedGroup.read(this.ports, Math.min(READ_MS, left));
3384
- const leader = rows.find((row) => row.pid === this.leader.pid);
3385
- if (leader && (leader.started !== this.leader.started || leader.pgid !== this.leader.pgid))
3386
- throw ownershipError();
3387
- if (!rows.some((row) => row.pgid === this.leader.pgid) && !leader) {
3388
- this.confirmedGone = true;
3216
+ onWatchdog() {
3217
+ if (this.state !== "busy")
3218
+ return;
3219
+ if (this.cancelWait) {
3220
+ this.clearWatchdog();
3389
3221
  return;
3390
3222
  }
3391
- if (this.ports.now() >= end)
3392
- throw shutdownError();
3393
- await this.ports.sleep(Math.min(25, end - this.ports.now()));
3394
- }
3395
- }
3396
- async inspect(allowGone) {
3397
- const rows = await AcpOwnedGroup.read(this.ports);
3398
- const leader = rows.find((row) => row.pid === this.leader.pid);
3399
- const members = rows.some((row) => row.pgid === this.leader.pgid);
3400
- if (!leader && !members && allowGone)
3401
- return false;
3402
- if (!leader || !this.sameLeader(leader) || this.proc.pid !== this.leader.pid || this.proc.exitCode !== null || this.proc.signalCode !== null || !AcpOwnedGroup.identity(this.ports, this.leader.pid))
3403
- throw ownershipError();
3404
- return true;
3405
- }
3406
- sameLeader(row) {
3407
- return row.pid === this.leader.pid && row.pgid === this.leader.pgid && row.ppid === this.leader.ppid && row.started === this.leader.started;
3408
- }
3409
- signal(signal) {
3410
- try {
3411
- this.ports.signal(-this.leader.pgid, signal);
3412
- } catch {
3413
- throw ownershipError();
3414
- }
3415
- }
3416
- static identity(ports, pid) {
3417
- try {
3418
- return ports.signature(pid);
3419
- } catch {
3420
- return;
3223
+ if (this.openTools.size) {
3224
+ const ceiling = Math.max(this.watchdogMs, this.opts.toolCeilingMs ?? TOOL_CEILING_MS);
3225
+ const remaining = ceiling - (Date.now() - this.lastSignAt);
3226
+ if (remaining > 0) {
3227
+ this.touch(Math.min(this.watchdogMs, remaining));
3228
+ return;
3229
+ }
3230
+ try {
3231
+ this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
3232
+ } catch {
3233
+ this.activityPrivate = true;
3234
+ }
3235
+ const tools = this.activityPrivate ? "a tool call" : [...new Set(this.openTools.values())].map((title) => JSON.stringify(activityText(title).replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").slice(0, 80))).join(", ");
3236
+ this.opts.log?.(`[${this.id}] open tool call (${tools}) silent past the ${Math.round(ceiling / 60000)} min tool ceiling; cancelling the turn`);
3237
+ }
3238
+ this.endActivity("watchdog cancelled", true);
3239
+ const cancelledTurn = this.turn;
3240
+ this.cancelPermissions();
3241
+ this.activeLegacy = undefined;
3242
+ const durable = !!this.activeDeliveryId;
3243
+ if (this.activeDeliveryId)
3244
+ this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason: "ACP turn watchdog timeout" });
3245
+ this.settleSteers("needs_review", "ACP turn watchdog timeout");
3246
+ this.activeDeliveryId = undefined;
3247
+ this.notify("session/cancel", { sessionId: this.sessionId });
3248
+ this.turn++;
3249
+ if (durable)
3250
+ this.setState("offline");
3251
+ else {
3252
+ this.clearWatchdog();
3253
+ const successor = this.turn;
3254
+ const timer = setTimeout(() => {
3255
+ const waiting = this.cancelWait;
3256
+ if (!waiting || waiting.turn !== cancelledTurn || waiting.successor !== successor)
3257
+ return;
3258
+ this.clearCancelWait();
3259
+ if (!this.stopping && this.turn === successor) {
3260
+ this.setState("offline");
3261
+ this.opts.log?.(`[${this.id}] cancelled prompt did not report within the cancellation deadline; peer offline, queued work retained; stop and restart the peer to resume delivery`);
3262
+ }
3263
+ }, this.opts.cancelWaitMs ?? HANDSHAKE_MS);
3264
+ timer.unref?.();
3265
+ let release;
3266
+ const released = new Promise((resolve) => {
3267
+ release = resolve;
3268
+ });
3269
+ this.cancelWait = { turn: cancelledTurn, successor, timer, reported: false, released, release };
3270
+ }
3421
3271
  }
3422
- }
3423
- static async read(ports, ms = READ_MS) {
3424
- let timer;
3425
- try {
3426
- const rows = await Promise.race([
3427
- Promise.resolve().then(() => ports.table()),
3428
- new Promise((resolve) => {
3429
- timer = setTimeout(() => resolve(undefined), ms);
3430
- })
3431
- ]);
3432
- if (!rows)
3433
- throw ownershipError();
3434
- return rows;
3435
- } catch {
3436
- throw ownershipError();
3437
- } finally {
3438
- clearTimeout(timer);
3272
+ clearCancelWait() {
3273
+ const waiting = this.cancelWait;
3274
+ this.cancelWait = undefined;
3275
+ if (waiting) {
3276
+ clearTimeout(waiting.timer);
3277
+ waiting.release();
3278
+ }
3439
3279
  }
3440
- }
3441
- }
3442
- var PORTS, READ_MS = 1000, ownershipError = () => new Error("Configured ACP process ownership could not be verified"), shutdownError = () => new Error("Configured ACP process group shutdown incomplete");
3443
- var init_acp_owned_group = __esm(() => {
3444
- init_platform();
3445
- init_child_process();
3446
- PORTS = {
3447
- table: readProcessTable,
3448
- signature: processSignature,
3449
- signal: (target, signal) => {
3450
- process.kill(target, signal);
3451
- },
3452
- now: () => performance.now(),
3453
- sleep: (ms) => Bun.sleep(ms)
3454
- };
3455
- });
3456
-
3457
- // src/adapters/acp.ts
3458
- import { spawn as spawn6 } from "child_process";
3459
- import { createInterface } from "readline";
3460
- function normalizeACPUsage(value, source = "usage_update") {
3461
- const unknown = { source, availability: "unsupported", shape: "none" };
3462
- if (!value || typeof value !== "object" || Array.isArray(value))
3463
- return unknown;
3464
- const raw = value;
3465
- const f = raw.usage && typeof raw.usage === "object" && !Array.isArray(raw.usage) ? raw.usage : raw;
3466
- const valid = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
3467
- for (const key of ["totalTokens", "total_tokens"]) {
3468
- if (f[key] === undefined)
3469
- continue;
3470
- const shape = "total";
3471
- return valid(f[key]) ? { source, availability: "known", shape, total: f[key] } : { source, availability: "invalid", shape };
3472
- }
3473
- for (const [input, output] of [["inputTokens", "outputTokens"], ["input_tokens", "output_tokens"]]) {
3474
- if (f[input] === undefined && f[output] === undefined)
3475
- continue;
3476
- const a = f[input], b = f[output];
3477
- return valid(a) && valid(b) && Number.isFinite(a + b) ? { source, availability: "known", shape: "input-output", total: a + b } : { source, availability: "invalid", shape: "input-output" };
3478
- }
3479
- if (f.used !== undefined) {
3480
- if (!valid(f.used) || !valid(f.size) || !Number.isSafeInteger(f.used) || !Number.isSafeInteger(f.size) || f.size <= 0)
3481
- return { source, availability: "invalid", shape: "context-used" };
3482
- return { source, availability: "unsupported", shape: "context-used", contextUsed: f.used, contextCapacity: f.size };
3483
- }
3484
- return unknown;
3485
- }
3486
- function canonicalMcpToolName(announcedTitle, serverNames) {
3487
- const m = announcedTitle?.match(/^([\w-]{1,80}) \((.+) MCP Server\)$/);
3488
- return m && serverNames?.some((name) => name === m[2]) ? `mcp__${m[2]}__${m[1]}` : undefined;
3489
- }
3490
- function jsonObject(text) {
3491
- try {
3492
- const v = text === undefined ? undefined : JSON.parse(text);
3493
- return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
3494
- } catch {
3495
- return;
3496
- }
3497
- }
3498
- var ACTION_BUDGET_OPTION = "agent-hub:action-budget", HANDSHAKE_MS = 30000, TOOL_INPUT_CAP = 64, TOOL_CEILING_MS, RpcTimeoutError, AcpPeer;
3499
- var init_acp = __esm(() => {
3500
- init_envelope();
3501
- init_kimi_native();
3502
- init_qwen_native();
3503
- init_qwen_native();
3504
- init_child_process();
3505
- init_acp_owned_group();
3506
- TOOL_CEILING_MS = 30 * 60000;
3507
- RpcTimeoutError = class RpcTimeoutError extends Error {
3508
- };
3509
- AcpPeer = class AcpPeer extends BasePeer {
3510
- opts;
3511
- permissionMode;
3512
- nativePolicy;
3513
- modeUnknown = false;
3514
- kimiAgent = false;
3515
- qwenAgent = false;
3516
- availableModes = new Set;
3517
- proc;
3518
- ownedGroup;
3519
- sessionId = "";
3520
- nextId = 1;
3521
- pending = new Map;
3522
- chunks = [];
3523
- toolInputs = new Map;
3524
- toolText = new Map;
3525
- toolTitles = new Map;
3526
- openTools = new Map;
3527
- lastSignAt = 0;
3528
- primed = false;
3529
- turn = 0;
3530
- stopping = false;
3531
- activeDeliveryId;
3532
- activeLegacy;
3533
- deliveryAccepted = false;
3534
- turnEnvs = [];
3535
- steerIds = [];
3536
- cancelWait;
3537
- promptActivity = false;
3538
- activityOpen = false;
3539
- activityPrivate = false;
3540
- activityStub = false;
3541
- activityStarted = 0;
3542
- activityPending;
3543
- activityTimer;
3544
- emitActivity(kind, text) {
3545
- if (!this.opts.onActivity || !this.activityOpen)
3280
+ observeUsage(value, source) {
3281
+ const observation = normalizeACPUsage(value, source);
3282
+ this.opts.onUsageDiagnostic?.(observation, this.sessionId);
3283
+ if (observation.availability === "known")
3284
+ this.opts.onTokens?.(observation.total, this.sessionId);
3285
+ }
3286
+ currentTurn() {
3287
+ return this.turn;
3288
+ }
3289
+ adoptIntoTurn(turn, envs, deliveryId) {
3290
+ if (turn !== this.turn || this.state !== "busy" || this.cancelWait || this.stopping)
3291
+ throw new Error(`${this.id} is ${this.state}, or its turn changed`);
3292
+ this.turnEnvs = [...this.turnEnvs, ...envs];
3293
+ this.activityPrivate ||= envs.some((env) => env.private === true);
3294
+ this.primed = true;
3295
+ if (deliveryId) {
3296
+ this.steerIds.push(deliveryId);
3297
+ this.delivery({ id: deliveryId, state: "accepted" });
3298
+ }
3299
+ }
3300
+ settleSteers(state, reason) {
3301
+ const ids = this.steerIds;
3302
+ this.steerIds = [];
3303
+ for (const id of ids)
3304
+ this.delivery({ id, state, ...state === "needs_review" ? { reason: reason ?? "steered turn did not complete normally" } : {} });
3305
+ }
3306
+ acceptDelivery() {
3307
+ if (!this.activeDeliveryId || this.deliveryAccepted)
3308
+ return;
3309
+ this.deliveryAccepted = true;
3310
+ this.delivery({ id: this.activeDeliveryId, state: "accepted" });
3311
+ }
3312
+ down(reason) {
3313
+ this.clearCancelWait();
3314
+ if (this.nativePolicy !== undefined || this.opts.requireOwnedGroup)
3315
+ reason = "Configured ACP transport stopped";
3316
+ this.opts.log?.(`[${this.id}] ${reason}`);
3317
+ for (const p of this.pending.values())
3318
+ p.reject(new Error(reason));
3319
+ this.pending.clear();
3320
+ if (this.activeDeliveryId)
3321
+ this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason });
3322
+ this.settleSteers("needs_review", reason);
3323
+ this.activeDeliveryId = undefined;
3324
+ this.setState("offline");
3325
+ }
3326
+ onLine(line) {
3327
+ if (this.stopping)
3546
3328
  return;
3329
+ let msg;
3547
3330
  try {
3548
- this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
3331
+ msg = JSON.parse(line);
3549
3332
  } catch {
3550
- this.activityPrivate = true;
3333
+ return;
3551
3334
  }
3552
- if (this.activityPrivate) {
3553
- if (this.activityStub)
3335
+ if (this.state === "busy" && !this.cancelWait) {
3336
+ this.lastSignAt = Date.now();
3337
+ this.touch();
3338
+ }
3339
+ if (msg.method === "session/update") {
3340
+ if (msg.params?.sessionId && msg.params.sessionId !== this.sessionId)
3554
3341
  return;
3555
- this.activityStub = true;
3556
- kind = "private";
3557
- text = "Private turn: activity withheld";
3342
+ const u = msg.params?.update;
3343
+ if (u?.sessionUpdate === "usage_update" && this.state !== "offline") {
3344
+ const observation = normalizeACPUsage(u, "usage_update");
3345
+ if (observation.shape === "context-used") {
3346
+ if (this.state === "busy" && !this.cancelWait || msg.params?.sessionId === this.sessionId)
3347
+ this.opts.onUsageDiagnostic?.(observation, this.sessionId);
3348
+ return;
3349
+ }
3350
+ }
3351
+ if (this.state !== "busy" || this.cancelWait)
3352
+ return;
3353
+ this.promptActivity = true;
3354
+ this.acceptDelivery();
3355
+ this.updateWatch(u);
3356
+ if (u?.sessionUpdate === "agent_message_chunk" && u.content?.type === "text")
3357
+ this.chunks.push(u.content.text);
3358
+ else if ((u?.sessionUpdate === "tool_call" || u?.sessionUpdate === "tool_call_update") && typeof u.toolCallId === "string") {
3359
+ if (u.sessionUpdate === "tool_call")
3360
+ this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId), this.toolTitles.delete(u.toolCallId);
3361
+ if (u.rawInput !== undefined)
3362
+ this.toolInputs.set(u.toolCallId, u.rawInput);
3363
+ if (u.sessionUpdate === "tool_call" && typeof u.title === "string")
3364
+ this.toolTitles.set(u.toolCallId, u.title);
3365
+ const text = Array.isArray(u.content) ? u.content.map((c) => c?.type === "content" && c.content?.type === "text" ? String(c.content.text) : "").join("") : "";
3366
+ if (text)
3367
+ this.toolText.set(u.toolCallId, text);
3368
+ if (u.status === "completed" || u.status === "failed")
3369
+ this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId), this.toolTitles.delete(u.toolCallId), this.openTools.delete(u.toolCallId);
3370
+ else if (u.sessionUpdate === "tool_call" || this.openTools.has(u.toolCallId))
3371
+ this.openTools.set(u.toolCallId, this.toolTitles.get(u.toolCallId) ?? "tool");
3372
+ while (this.openTools.size > TOOL_INPUT_CAP)
3373
+ this.openTools.delete(this.openTools.keys().next().value);
3374
+ for (const map of [this.toolInputs, this.toolText, this.toolTitles])
3375
+ while (map.size > TOOL_INPUT_CAP)
3376
+ map.delete(map.keys().next().value);
3377
+ } else if (u?.sessionUpdate === "usage_update")
3378
+ this.observeUsage(u, "usage_update");
3379
+ } else if (msg.method === "session/request_permission") {
3380
+ if (this.cancelWait || this.state === "offline") {
3381
+ this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "cancelled" } } });
3382
+ return;
3383
+ }
3384
+ if (this.state === "busy" && (!msg.params?.sessionId || msg.params.sessionId === this.sessionId))
3385
+ this.promptActivity = true;
3386
+ this.answerPermission(msg);
3387
+ } else if (msg.method && msg.id !== undefined) {
3388
+ this.send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "method not supported by agent-hub" } });
3389
+ } else if (msg.id !== undefined) {
3390
+ const p = this.pending.get(msg.id);
3391
+ this.pending.delete(msg.id);
3392
+ if (msg.error)
3393
+ p?.reject(new Error(msg.error.message ?? "rpc error"));
3394
+ else
3395
+ p?.resolve(msg.result);
3558
3396
  }
3559
- try {
3560
- this.opts.onActivity({ kind, text: activityText(text), at: Date.now(), turn: this.turn });
3561
- } catch {}
3562
- }
3563
- flushActivity() {
3564
- clearTimeout(this.activityTimer);
3565
- this.activityTimer = undefined;
3566
- const pending = this.activityPending;
3567
- this.activityPending = undefined;
3568
- if (pending)
3569
- this.emitActivity(pending.kind, pending.text);
3570
3397
  }
3571
- updateWatch(u) {
3572
- if (!this.opts.onActivity)
3573
- return;
3574
- const item = updateActivity(u);
3575
- if (!item)
3576
- return;
3577
- if (item.kind === "message" || item.kind === "thought") {
3578
- if (this.activityPending?.kind !== item.kind || this.activityPending.text.length + item.text.length > 4096)
3579
- this.flushActivity();
3580
- if (this.activityPending)
3581
- this.activityPending.text += item.text;
3398
+ async answerPermission(msg) {
3399
+ this.flushActivity();
3400
+ const call = msg.params?.toolCall ?? {};
3401
+ const kind = typeof call.kind === "string" ? call.kind : undefined;
3402
+ if (this.nativePolicy?.lockedReadOnly && !["read", "search", "think"].includes(kind ?? "")) {
3403
+ const rejected = (msg.params?.options ?? []).find((option) => option.kind === "reject_once");
3404
+ return this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: rejected ? { outcome: "selected", optionId: rejected.optionId } : { outcome: "cancelled" } } });
3405
+ }
3406
+ const once = (msg.params?.options ?? []).find((o) => o.kind === "allow_once");
3407
+ const id = typeof call.toolCallId === "string" ? call.toolCallId : undefined;
3408
+ const announced = id === undefined ? undefined : this.toolTitles.get(id);
3409
+ const canonical = canonicalMcpToolName(announced, this.opts.mcpServers?.map((s) => s.name));
3410
+ const identity = this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? undefined : [typeof call.title === "string" ? call.title : undefined, canonical].find((candidate) => candidate !== undefined && this.opts.autoApprove?.(candidate));
3411
+ const rawInput = call.rawInput ?? (id === undefined ? undefined : this.toolInputs.get(id) ?? jsonObject(this.toolText.get(id)));
3412
+ let gate;
3413
+ try {
3414
+ gate = rawInput === undefined || !this.sessionId ? undefined : this.opts.actionGate?.({ names: [call.title, announced, canonical].filter((name) => typeof name === "string"), ...kind ? { kind } : {}, input: rawInput }, this.sessionId);
3415
+ } catch {
3416
+ gate = { title: "Action approval: the always-ask check failed; command withheld", denyOnly: true };
3417
+ }
3418
+ if (once && identity && !gate) {
3419
+ this.opts.log?.(`permission auto-approved for ${this.id}: ${identity}`);
3420
+ this.emitActivity("ask", `${String(msg.id)}: ${activityText(announced ?? call.title)} (automatic)`);
3421
+ this.emitActivity("answered", `${String(msg.id)}: allow_once (automatic)`);
3422
+ return this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "selected", optionId: once.optionId } } });
3423
+ }
3424
+ const raw = rawInput;
3425
+ const full = raw === undefined ? "" : typeof raw === "string" ? raw : JSON.stringify(raw);
3426
+ const cut = full.length > 600;
3427
+ const input = raw === undefined ? "" : `: ${cut ? `${full.slice(0, 600)} [cut, ${full.length} chars]` : full}`;
3428
+ const named = typeof call.title === "string" && jsonObject(call.title) === undefined ? call.title : announced;
3429
+ const title = gate ? gate.title : raw === undefined ? `${named ?? "tool call"} (payload not reported by the agent)` : `${named ?? "tool call"}${input}`;
3430
+ this.emitActivity("ask", `${String(msg.id)}: ${activityText(title)}; answer in ahub console`);
3431
+ let options = (msg.params?.options ?? []).filter((o) => o.kind !== "allow_always" || !gate && !this.nativePolicy?.lockedReadOnly && raw !== undefined && !cut);
3432
+ if (gate?.denyOnly)
3433
+ options = options.filter((o) => o.kind === "reject_once" || o.kind === "reject_always");
3434
+ const budgetOption = !gate?.denyOnly && gate?.budget && once ? { optionId: ACTION_BUDGET_OPTION, name: `Allow ${gate.budget.label} for this session`, kind: "allow_once" } : undefined;
3435
+ if (budgetOption)
3436
+ options = [...options, budgetOption];
3437
+ const tool = typeof named === "string" && /^[\w.:@-]{1,80}$/.test(named) ? named : undefined;
3438
+ const turn = this.turn;
3439
+ const alive = setInterval(() => this.state === "busy" && turn === this.turn && this.touch(), Math.max(10, Math.min(30000, Math.floor(this.watchdogMs / 3))));
3440
+ const controller = new AbortController;
3441
+ this.permissionWaits.add(controller);
3442
+ let picked;
3443
+ try {
3444
+ const cancelled = new Promise((resolve) => controller.signal.addEventListener("abort", () => resolve(undefined), { once: true }));
3445
+ picked = await Promise.race([this.opts.onPermission?.({ peer: this.id, title, options, ...tool ? { tool } : {}, ...kind ? { kind } : {}, ...gate ? { classed: true } : {} }, controller.signal).catch(() => {
3446
+ return;
3447
+ }), cancelled]);
3448
+ } finally {
3449
+ clearInterval(alive);
3450
+ this.permissionWaits.delete(controller);
3451
+ }
3452
+ let valid = !controller.signal.aborted && this.state !== "offline" && turn === this.turn && !this.stopping && options.some((o) => o.optionId === picked);
3453
+ let answer = picked;
3454
+ if (valid && budgetOption && picked === budgetOption.optionId) {
3455
+ let granted = false;
3456
+ try {
3457
+ granted = gate.budget.approve();
3458
+ } catch {}
3459
+ if (granted && once)
3460
+ answer = once.optionId;
3582
3461
  else
3583
- this.activityPending = { kind: item.kind, text: item.text };
3584
- this.activityTimer ??= setTimeout(() => this.flushActivity(), 50);
3585
- this.activityTimer.unref?.();
3586
- } else {
3587
- this.flushActivity();
3588
- this.emitActivity(item.kind, item.text);
3462
+ valid = false;
3589
3463
  }
3464
+ if (turn === this.turn)
3465
+ this.emitActivity("answered", `${String(msg.id)}: ${valid ? options.find((o) => o.optionId === picked)?.kind : "cancelled"}`);
3466
+ this.send({
3467
+ jsonrpc: "2.0",
3468
+ id: msg.id,
3469
+ result: { outcome: valid ? { outcome: "selected", optionId: answer } : { outcome: "cancelled" } }
3470
+ });
3590
3471
  }
3591
- clearActivity() {
3592
- clearTimeout(this.activityTimer);
3593
- this.activityTimer = undefined;
3594
- this.activityPending = undefined;
3472
+ request(method, params, timeoutMs) {
3473
+ const id = this.nextId++;
3474
+ return new Promise((resolve, reject) => {
3475
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
3476
+ this.pending.delete(id);
3477
+ reject(new RpcTimeoutError(`${method} did not answer within ${timeoutMs / 1000} s`));
3478
+ }, timeoutMs);
3479
+ timer?.unref();
3480
+ this.pending.set(id, {
3481
+ resolve: (value) => {
3482
+ clearTimeout(timer);
3483
+ resolve(value);
3484
+ },
3485
+ reject: (error) => {
3486
+ clearTimeout(timer);
3487
+ reject(error);
3488
+ }
3489
+ });
3490
+ this.send({ jsonrpc: "2.0", id, method, params });
3491
+ });
3595
3492
  }
3596
- endActivity(reason, discardPending = false) {
3597
- if (discardPending)
3598
- this.clearActivity();
3599
- else
3600
- this.flushActivity();
3601
- this.emitActivity("turn_end", `${reason}; ${Date.now() - this.activityStarted} ms`);
3602
- this.activityOpen = false;
3493
+ notify(method, params) {
3494
+ this.send({ jsonrpc: "2.0", method, params });
3495
+ }
3496
+ send(msg) {
3497
+ if (this.proc?.stdin.writable)
3498
+ this.proc.stdin.write(`${JSON.stringify(msg)}
3499
+ `);
3500
+ }
3501
+ };
3502
+ });
3503
+
3504
+ // src/adapters/codex-appserver.ts
3505
+ import { spawn as spawn6 } from "child_process";
3506
+ function parse(raw) {
3507
+ try {
3508
+ return JSON.parse(raw);
3509
+ } catch {
3510
+ return;
3511
+ }
3512
+ }
3513
+ var UNIDENTIFIED_CANCELLATION_NOTICE = "Codex cancelled turn has no verified native id; reattach the native session before further hub delivery", counter = (value) => {
3514
+ const n = Number(value);
3515
+ return Number.isFinite(n) && n >= 0 ? n : 0;
3516
+ }, TRACKED, CodexPeer;
3517
+ var init_codex_appserver = __esm(() => {
3518
+ init_envelope();
3519
+ init_context_window();
3520
+ init_child_process();
3521
+ init_acp();
3522
+ TRACKED = new Set(["thread/start", "thread/resume"]);
3523
+ CodexPeer = class CodexPeer extends BasePeer {
3524
+ opts;
3525
+ permissionMode;
3526
+ actionWaits = new Map;
3527
+ get actionGated() {
3528
+ return this.opts.actionGate !== undefined;
3603
3529
  }
3604
- permissionWaits = new Set;
3605
- setState(next) {
3606
- if (next === "offline") {
3607
- this.clearActivity();
3608
- this.cancelPermissions();
3609
- }
3610
- super.setState(next);
3530
+ nativeApprovalPolicies = new Map;
3531
+ overriddenApprovals = new Set;
3532
+ restoringApprovals = new Map;
3533
+ proc;
3534
+ server;
3535
+ link;
3536
+ claimedTui;
3537
+ threadId = "";
3538
+ contextEpoch = 0;
3539
+ get sessionGeneration() {
3540
+ return this.contextEpoch;
3611
3541
  }
3612
- cancelPermissions() {
3613
- for (const wait of this.permissionWaits)
3614
- wait.abort();
3615
- this.permissionWaits.clear();
3542
+ tokenTotal;
3543
+ tokenKinds;
3544
+ freshThread = false;
3545
+ activeTurns = new Set;
3546
+ deliveryEpoch = 0;
3547
+ retiredTurns = new Set;
3548
+ unidentifiedCancellation = false;
3549
+ get statusNotice() {
3550
+ return this.unidentifiedCancellation ? { code: "unidentified-cancellation", message: UNIDENTIFIED_CANCELLATION_NOTICE } : undefined;
3616
3551
  }
3552
+ unboundAccepted = false;
3553
+ nextId = -1;
3554
+ pending = new Map;
3555
+ usageTimer;
3556
+ injected;
3557
+ answering = [];
3558
+ lastAnswer = "";
3559
+ deltas = new Map;
3560
+ primed = false;
3561
+ version;
3562
+ steers = new Set;
3563
+ turnDeliveries = new Map;
3564
+ unboundDeliveries = new Set;
3617
3565
  constructor(id, opts) {
3618
3566
  super(id, opts.watchdogMs);
3619
3567
  this.opts = opts;
3620
- this.nativePolicy = opts.nativePolicy === undefined ? undefined : { ...opts.nativePolicy, ...opts.nativePolicy.modes === undefined ? {} : { modes: { ...opts.nativePolicy.modes } } };
3621
- this.permissionMode = this.nativePolicy?.lockedReadOnly ? "ask" : opts.permissionMode ?? "ask";
3568
+ this.permissionMode = opts.permissionMode ?? "ask";
3622
3569
  }
3623
3570
  recoveryMetadata() {
3624
- return { launch: { kind: "acp", ...this.opts.launchModel ? { model: this.opts.launchModel } : {} }, ...this.sessionId ? { sessionId: this.sessionId } : {} };
3625
- }
3626
- async start() {
3627
- if (this.opts.requireOwnedGroup && this.proc)
3628
- throw new Error("Configured ACP still has a process; stop it before starting another");
3629
- this.stopping = false;
3630
- const [bin, ...args] = this.opts.cmd;
3631
- const proc = spawn6(bin, args, { cwd: this.opts.cwd, env: peerChildEnv(this.id, { ...process.env, ...this.opts.env ?? {} }), stdio: ["pipe", "pipe", "pipe"], detached: true });
3632
- this.proc = proc;
3633
- trackGroup(proc);
3634
- proc.on("error", (e) => {
3635
- if (!this.opts.requireOwnedGroup || this.proc === proc)
3636
- this.down(`spawn failed: ${e.message}`);
3637
- });
3638
- proc.on("exit", (code) => {
3639
- if (!this.opts.requireOwnedGroup || this.proc === proc)
3640
- this.down(`exited with code ${code}`);
3641
- });
3642
- proc.stdin.on("error", () => {});
3643
- proc.stderr.on("data", (d) => this.opts.log?.(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP stderr output withheld" : `[${this.id}] ${String(d).trimEnd()}`));
3644
- createInterface({ input: proc.stdout }).on("line", (line) => this.onLine(line));
3645
- const handshake = async () => {
3646
- const init = await this.request("initialize", {
3647
- protocolVersion: 1,
3648
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
3649
- });
3650
- this.kimiAgent = this.nativePolicy === undefined && init?.agentInfo?.name === "Kimi Code CLI";
3651
- const expected = this.opts.expectedAgent, version = init?.agentInfo?.version;
3652
- if (expected && (init?.agentInfo?.name !== expected.name || !(typeof expected.version === "function" ? expected.version(version) : version === expected.version)))
3653
- throw new Error("native ACP identity is not verified for this production adapter");
3654
- const qwen = qwenNativeIdentity(this.id, this.opts.qwen025, init, this.opts.kind);
3655
- this.qwenAgent = this.nativePolicy === undefined && qwen;
3656
- const resume = this.opts.resumeSessionId;
3657
- if (!resume)
3658
- return this.request("session/new", { cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
3659
- if (!init?.agentCapabilities?.loadSession)
3660
- throw new Error(`${this.id} cannot load an earlier session (the agent offers no loadSession)`);
3661
- const loaded = await this.request("session/load", { sessionId: resume, cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
3662
- if (loaded?.sessionId !== undefined && loaded.sessionId !== resume || this.opts.requireLoadSessionEcho && loaded?.sessionId !== resume)
3663
- throw new Error("native ACP load did not confirm the requested conversation; no restored session is claimed");
3664
- return { ...loaded, sessionId: resume };
3571
+ return {
3572
+ launch: { kind: "codex", bin: this.opts.bin ?? "codex", cwd: this.opts.cwd, appPort: this.opts.appPort, proxyPort: this.opts.proxyPort },
3573
+ ...this.threadId ? { threadId: this.threadId } : {}
3665
3574
  };
3666
- try {
3667
- if (this.opts.requireOwnedGroup) {
3668
- const proof = await AcpOwnedGroup.capture(proc);
3669
- this.ownedGroup = { proc, proof };
3670
- if (this.proc !== proc || this.stopping)
3671
- throw new Error("Configured ACP process ownership could not be verified");
3672
- }
3673
- const timeout = new Promise((_, reject) => {
3674
- setTimeout(() => reject(new Error(`${this.id} did not complete the ACP handshake within ${HANDSHAKE_MS / 1000} s`)), HANDSHAKE_MS).unref();
3675
- });
3676
- const session = await Promise.race([handshake(), timeout]);
3677
- if (this.nativePolicy !== undefined && (typeof session?.sessionId !== "string" || !session.sessionId.trim() || Buffer.byteLength(session.sessionId) > 4096 || /[\p{Cc}\p{Cf}]/u.test(session.sessionId))) {
3678
- throw new Error("Configured ACP did not provide a valid native session identity");
3679
- }
3680
- this.sessionId = session.sessionId;
3681
- const modes = session.modes?.availableModes;
3682
- this.availableModes = new Set(Array.isArray(modes) ? modes.flatMap((mode) => typeof mode?.id === "string" ? [mode.id] : []) : []);
3683
- const resetResumed = this.opts.resumeSessionId && typeof session.modes?.currentModeId === "string" && session.modes.currentModeId !== KIMI_MODE_IDS.ask;
3684
- if (this.nativePolicy?.lockedReadOnly)
3685
- await this.applyNativeMode(this.nativePolicy.readOnly, "ask");
3686
- else if (this.nativePolicy?.modes !== undefined || this.qwenAgent || this.kimiAgent && (this.permissionMode !== "ask" || resetResumed))
3687
- await this.setPermissionMode(this.permissionMode);
3688
- if (this.opts.requireOwnedGroup) {
3689
- const owned = this.ownedGroup;
3690
- if (!owned || owned.proc !== proc || this.proc !== proc || this.stopping)
3691
- throw new Error("Configured ACP process ownership could not be verified");
3692
- await owned.proof.verify();
3693
- if (this.proc !== proc || this.stopping)
3694
- throw new Error("Configured ACP process ownership could not be verified");
3695
- }
3696
- } catch (e) {
3697
- this.setState("offline");
3698
- if (this.proc === proc) {
3699
- if (this.opts.requireOwnedGroup) {
3700
- const owned = this.ownedGroup;
3701
- if (owned?.proc === proc) {
3702
- await owned.proof.stop().then(() => {
3703
- if (this.proc === proc) {
3704
- this.proc = undefined;
3705
- this.ownedGroup = undefined;
3706
- }
3707
- }, () => this.opts.log?.("Configured ACP process group shutdown incomplete"));
3708
- } else {
3709
- proc.stdin.end();
3710
- if (await AcpOwnedGroup.confirmGone(proc) && this.proc === proc) {
3711
- this.proc = undefined;
3712
- this.ownedGroup = undefined;
3713
- } else
3714
- this.opts.log?.("Configured ACP process ownership could not be verified; no process signals were sent");
3715
- }
3716
- } else
3717
- await stopOwnedProcess(proc, { group: true }).then(() => {
3718
- if (this.proc === proc)
3719
- this.proc = undefined;
3720
- }, (stop) => this.opts.log?.(`[${this.id}] ${stop.message}`));
3721
- }
3722
- throw e;
3723
- }
3724
- this.setState("idle");
3725
- }
3726
- getPermissionMode() {
3727
- return this.permissionMode;
3728
- }
3729
- get actionGated() {
3730
- return this.opts.actionGate !== undefined;
3731
3575
  }
3732
- get pendingOwnedProcess() {
3733
- return this.opts.requireOwnedGroup === true && this.proc !== undefined;
3576
+ get thread() {
3577
+ return this.threadId;
3734
3578
  }
3735
- get permissionModeState() {
3736
- return this.nativePolicy?.lockedReadOnly || this.nativePolicy?.modes !== undefined || this.kimiAgent || this.qwenAgent ? this.modeUnknown ? "unknown" : this.permissionMode : "unmanaged";
3579
+ get proxyUrl() {
3580
+ return `ws://127.0.0.1:${this.server?.port ?? this.opts.proxyPort}`;
3737
3581
  }
3738
- async setPermissionMode(mode) {
3739
- if (this.nativePolicy?.lockedReadOnly)
3740
- throw new Error(`${this.id} permission mode is locked read-only; runtime changes are refused`);
3741
- const modes = this.nativePolicy !== undefined ? this.nativePolicy.modes : this.qwenAgent ? QWEN_MODE_IDS : this.kimiAgent ? KIMI_MODE_IDS : undefined;
3742
- if (modes === undefined)
3743
- throw new Error(`${this.id} permission mode ${mode} unmanaged: this ACP agent has no verified mode mapping; use Kimi Code CLI or add an agent-specific mapping`);
3744
- await this.applyNativeMode(modes[mode], mode);
3582
+ terminalOwner(launchId) {
3583
+ const link = this.link;
3584
+ return this.threadId && link && link === this.claimedTui && link.launchId === launchId && link.tui.readyState === WebSocket.OPEN && link.up.readyState === WebSocket.OPEN ? { generation: String(this.sessionGeneration) } : undefined;
3745
3585
  }
3746
- async applyNativeMode(id, mode) {
3747
- if (this.modeUnknown)
3748
- throw new Error(`${this.id} permission mode unknown after an unanswered change; restart the peer before changing modes`);
3749
- if (typeof id !== "string" || !id || !this.sessionId || !this.proc || !this.availableModes.has(id))
3750
- throw new Error(`${this.id} permission mode ${mode} unavailable: session does not offer ${id}`);
3586
+ async start() {
3587
+ const upstream = this.opts.upstreamUrl ?? await this.spawnAppServer();
3751
3588
  try {
3752
- await this.request("session/set_mode", { sessionId: this.sessionId, modeId: id }, this.opts.permissionModeTimeoutMs ?? HANDSHAKE_MS);
3589
+ this.server = Bun.serve({
3590
+ hostname: "127.0.0.1",
3591
+ port: this.opts.proxyPort,
3592
+ fetch: (req, server) => {
3593
+ if (req.headers.has("origin"))
3594
+ return new Response("forbidden", { status: 403 });
3595
+ const launchId = /^Bearer (\S+)$/.exec(req.headers.get("authorization") ?? "")?.[1];
3596
+ if (server.upgrade(req, { data: { ...launchId && launchId.length <= 128 ? { launchId } : {} } }))
3597
+ return;
3598
+ return new Response("agent-hub codex proxy");
3599
+ },
3600
+ websocket: {
3601
+ open: (tui) => {
3602
+ if (this.opts.admitAttach?.() === false) {
3603
+ tui.close(4000, "codex is off roster; retry when a person adds it");
3604
+ return;
3605
+ }
3606
+ if (this.claimedTui) {
3607
+ tui.close(1013, "codex TUI already attached to this hub");
3608
+ return;
3609
+ }
3610
+ this.attach(tui, upstream);
3611
+ },
3612
+ message: (tui, data) => {
3613
+ if (this.opts.admitAttach?.() === false) {
3614
+ tui.close(4000, "codex is off roster");
3615
+ return;
3616
+ }
3617
+ if (tui.data?.up)
3618
+ this.fromTui(tui.data, String(data));
3619
+ },
3620
+ close: (tui) => {
3621
+ if (tui.data?.up)
3622
+ this.detach(tui.data);
3623
+ }
3624
+ }
3625
+ });
3753
3626
  } catch (error) {
3754
- if (error instanceof RpcTimeoutError) {
3755
- this.modeUnknown = true;
3756
- this.turn++;
3757
- this.setState("offline");
3758
- await this.stop().catch((stop) => this.opts.log?.(`[${this.id}] ${stop.message}`));
3759
- throw new Error(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP native permission mode unknown; peer is offline, restart it before sending work" : `${this.id} permission mode ${mode} unknown: ${error.message}; peer is offline, restart it before sending work`);
3760
- }
3761
- throw new Error(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP native permission mode refused" : `${this.id} permission mode ${mode} refused: ${error.message}`);
3762
- }
3763
- if (this.cancelWait?.reported)
3764
- await this.cancelWait.released;
3765
- if (this.modeUnknown)
3766
- throw new Error(`${this.id} permission mode unknown; restart the peer`);
3767
- this.permissionMode = mode;
3627
+ const proc = this.proc;
3628
+ if (proc && await this.stopQuietly(proc) && this.proc === proc)
3629
+ this.proc = undefined;
3630
+ throw error;
3631
+ }
3768
3632
  }
3769
- async stop(reason) {
3770
- if (this.activityOpen)
3771
- this.endActivity("stopped", true);
3772
- this.stopping = true;
3773
- this.cancelPermissions();
3774
- this.clearCancelWait();
3775
- const legacy = this.activeLegacy;
3776
- this.activeLegacy = undefined;
3777
- if (legacy?.turn === this.turn)
3778
- this.onFailed?.(legacy.envs);
3779
- if (reason !== undefined)
3780
- this.turn++;
3781
- if (this.activeDeliveryId)
3782
- this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason: reason ?? "ACP session stopped before settlement" });
3783
- this.settleSteers("needs_review", reason ?? "ACP session stopped before settlement");
3784
- this.activeDeliveryId = undefined;
3633
+ stopQuietly(proc) {
3634
+ return stopOwnedProcess(proc, { group: true }).then(() => true, (error) => {
3635
+ this.opts.log?.(`[${this.id}] ${error.message}`);
3636
+ return false;
3637
+ });
3638
+ }
3639
+ async stop() {
3640
+ this.abortActionWaits();
3641
+ this.server?.stop(true);
3642
+ this.claimedTui?.tui.close(1001, "hub shutting down");
3785
3643
  const proc = this.proc;
3786
- if (!proc) {
3787
- this.setState("offline");
3788
- return;
3789
- }
3790
- if (this.opts.requireOwnedGroup) {
3791
- this.setState("offline");
3792
- const owned = this.ownedGroup;
3793
- if (!owned) {
3794
- if (await AcpOwnedGroup.confirmGone(proc) && this.proc === proc) {
3795
- this.proc = undefined;
3796
- return;
3797
- }
3798
- throw new Error("Configured ACP process ownership could not be verified");
3799
- }
3800
- if (owned.proc !== proc)
3801
- throw new Error("Configured ACP process ownership could not be verified");
3802
- await owned.proof.stop();
3803
- if (this.ownedGroup === owned)
3804
- this.ownedGroup = undefined;
3805
- } else
3644
+ if (proc) {
3806
3645
  await stopOwnedProcess(proc, { group: true });
3807
- if (this.proc === proc)
3808
- this.proc = undefined;
3646
+ if (this.proc === proc)
3647
+ this.proc = undefined;
3648
+ }
3809
3649
  this.setState("offline");
3810
3650
  }
3811
- async deliver(envs, deliveryId) {
3812
- if (this.state !== "idle" || this.stopping) {
3651
+ deliver(envs, deliveryId) {
3652
+ const link = this.link;
3653
+ if (this.state !== "idle" || !link || link.up.readyState !== WebSocket.OPEN) {
3813
3654
  if (deliveryId)
3814
- this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is ${this.state}` });
3815
- throw new Error(`${this.id} is ${this.state}`);
3816
- }
3817
- const turn = ++this.turn;
3818
- this.activeDeliveryId = deliveryId;
3819
- this.activeLegacy = deliveryId ? undefined : { turn, envs };
3820
- this.turnEnvs = envs;
3821
- this.steerIds = [];
3822
- this.deliveryAccepted = false;
3823
- this.promptActivity = false;
3824
- this.chunks = [];
3825
- this.openTools.clear();
3826
- this.activityOpen = true;
3827
- this.activityPrivate = envs.some((env) => env.private === true);
3828
- try {
3829
- this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
3830
- } catch {
3831
- this.activityPrivate = true;
3655
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is not injectable` });
3656
+ return Promise.reject(new Error(`${this.id} is not injectable`));
3832
3657
  }
3833
- this.activityStub = false;
3834
- this.activityStarted = Date.now();
3658
+ const text = this.render(envs);
3659
+ const id = this.nextId--, epoch = this.deliveryEpoch, session = this.contextEpoch;
3835
3660
  this.setState("busy");
3836
- this.emitActivity("delivery", envs.map((env) => `${env.from} ${env.id}`).join(", "));
3837
- const prompt = this.request("session/prompt", { sessionId: this.sessionId, prompt: [{ type: "text", text: this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}
3838
-
3839
- ${renderDigest(envs, false)}` }] });
3840
- const reported = () => {
3841
- if (this.cancelWait?.turn === turn)
3842
- this.cancelWait.reported = true;
3843
- };
3844
- prompt.then(reported, reported);
3845
- prompt.then((result) => {
3846
- if (turn !== this.turn)
3847
- return;
3848
- if (this.activeLegacy?.turn === turn)
3849
- this.activeLegacy = undefined;
3850
- this.endActivity(typeof result?.stopReason === "string" ? result.stopReason : "unknown", result?.stopReason === "cancelled");
3851
- this.observeUsage(result, "prompt_result");
3852
- this.primed = true;
3853
- const body = this.chunks.join("").trim();
3854
- const answering = this.turnEnvs.length ? this.turnEnvs : envs;
3855
- if (body)
3856
- this.onMessage?.(body, { inReplyTo: replyParent(answering), to: replyAudience(answering) });
3857
- if (result?.stopReason !== "end_turn")
3858
- this.reportTurnFailure(envs, `ACP prompt ended without normal completion (${String(result?.stopReason ?? "unknown")})`);
3859
- if (deliveryId && this.activeDeliveryId === deliveryId) {
3860
- this.acceptDelivery();
3861
- this.delivery({ id: deliveryId, state: result?.stopReason === "end_turn" ? "completed" : "needs_review", ...result?.stopReason === "end_turn" ? {} : { reason: "ACP prompt ended without normal completion" } });
3862
- }
3863
- this.settleSteers(result?.stopReason === "end_turn" ? "completed" : "needs_review", "ACP prompt ended without normal completion");
3864
- }).catch((e) => {
3865
- if (turn !== this.turn) {
3866
- if (this.cancelWait?.turn === turn)
3867
- this.opts.log?.(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP cancelled prompt rejected; native details withheld" : `[${this.id}] cancelled prompt rejected: ${sanitize(e.message)}`);
3868
- return;
3869
- }
3870
- this.endActivity("failed", true);
3871
- const reason = this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP prompt failed" : sanitize(e.message);
3872
- this.settleSteers("needs_review", reason);
3873
- this.opts.log?.(this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP prompt failed; native details withheld" : `[${this.id}] prompt failed: ${reason}`);
3874
- const quota = !this.stopping && this.kimiAgent ? acpQuotaRefusal(e.message) : undefined;
3875
- if (quota) {
3876
- let paused = false;
3877
- try {
3878
- paused = this.opts.onQuotaRefusal?.(quota) === true;
3879
- } catch (error) {
3880
- this.opts.log?.(`[${this.id}] quota pause failed: ${sanitize(error instanceof Error ? error.message : String(error))}`);
3881
- }
3882
- const safe = paused && !this.deliveryAccepted && !this.promptActivity;
3883
- if (deliveryId && this.activeDeliveryId === deliveryId)
3884
- this.delivery({ id: deliveryId, state: safe ? "failed_safe" : "needs_review", reason, ...safe ? { quota: true } : {} });
3885
- else if (!deliveryId && this.activeLegacy?.turn === turn) {
3886
- this.activeLegacy = undefined;
3887
- this.onFailed?.(envs, safe ? { quota: true } : undefined);
3661
+ return new Promise((resolve, reject) => {
3662
+ let invalidated = false;
3663
+ const retiredAtCancel = new Set;
3664
+ const invalidate = (reason) => {
3665
+ if (invalidated)
3666
+ return;
3667
+ invalidated = true;
3668
+ for (const turn of [...this.activeTurns, ...this.turnDeliveries.keys()])
3669
+ if (!turn.startsWith("unknown:"))
3670
+ retiredAtCancel.add(turn);
3671
+ envs = [];
3672
+ if (deliveryId)
3673
+ this.delivery({ id: deliveryId, state: "needs_review", reason });
3674
+ reject(new Error(reason));
3675
+ };
3676
+ this.pending.set(id, {
3677
+ deliveryId,
3678
+ kind: "deliver",
3679
+ invalidate,
3680
+ resolve: (result) => {
3681
+ const nativeTurn = typeof result?.turn?.id === "string" ? result.turn.id : undefined;
3682
+ if (invalidated || epoch !== this.deliveryEpoch || session !== this.contextEpoch || link !== this.link) {
3683
+ invalidate("Codex turn acceptance arrived after its delivery was invalidated");
3684
+ if (nativeTurn && session === this.contextEpoch && link === this.link && (!retiredAtCancel.has(nativeTurn) || this.retiredTurns.has(nativeTurn)))
3685
+ this.retireTurn(nativeTurn);
3686
+ return;
3687
+ }
3688
+ this.primed = true;
3689
+ this.injected = replyParent(envs);
3690
+ this.answering = [...envs];
3691
+ if (nativeTurn && !this.turnDeliveries.has(nativeTurn))
3692
+ this.turnDeliveries.set(nativeTurn, new Set);
3693
+ if (!nativeTurn && !this.activeTurns.size)
3694
+ this.unboundAccepted = true;
3695
+ if (deliveryId) {
3696
+ if (nativeTurn)
3697
+ this.addTurnDelivery(nativeTurn, deliveryId);
3698
+ else
3699
+ this.unboundDeliveries.add(deliveryId);
3700
+ this.delivery({ id: deliveryId, state: "accepted" });
3701
+ }
3702
+ resolve();
3703
+ },
3704
+ reject: (e) => {
3705
+ this.restoringApprovals.delete(id);
3706
+ if (invalidated)
3707
+ return;
3708
+ if (epoch !== this.deliveryEpoch || session !== this.contextEpoch || link !== this.link) {
3709
+ invalidate("Codex turn refusal arrived after its delivery was invalidated");
3710
+ return;
3711
+ }
3712
+ if (!this.activeTurns.size)
3713
+ this.setState("idle");
3714
+ if (deliveryId)
3715
+ this.delivery({ id: deliveryId, state: this.knownRejection(e) ? "failed_safe" : "needs_review", reason: e.message });
3716
+ reject(e);
3888
3717
  }
3889
- return;
3890
- }
3891
- this.reportTurnFailure(envs, reason);
3892
- if (deliveryId && this.activeDeliveryId === deliveryId)
3893
- this.delivery({ id: deliveryId, state: "needs_review", reason });
3894
- else if (!deliveryId && this.activeLegacy?.turn === turn) {
3895
- this.activeLegacy = undefined;
3896
- this.onFailed?.(envs);
3897
- }
3898
- }).finally(() => {
3899
- const waiting = this.cancelWait;
3900
- if (waiting?.turn === turn) {
3901
- this.clearCancelWait();
3902
- if (!this.stopping && this.turn === waiting.successor && this.state === "busy")
3903
- this.setState("idle");
3904
- return;
3905
- }
3906
- if (turn === this.turn && !this.stopping && this.state === "busy")
3907
- this.setState("idle");
3908
- if (this.activeLegacy?.turn === turn)
3909
- this.activeLegacy = undefined;
3910
- if (turn === this.turn && this.activeDeliveryId === deliveryId)
3911
- this.activeDeliveryId = undefined;
3718
+ });
3719
+ link.up.send(JSON.stringify({ method: "turn/start", id, params: this.applyPermissionPolicy({ threadId: this.threadId, input: [{ type: "text", text }] }, id) }));
3720
+ });
3721
+ }
3722
+ render(envs) {
3723
+ return this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}
3724
+
3725
+ ${renderDigest(envs, false)}`;
3726
+ }
3727
+ steer(envs, deliveryId) {
3728
+ const link = this.link;
3729
+ const expectedTurnId = [...this.activeTurns].reverse().find((t) => !t.startsWith("unknown:"));
3730
+ if (!link || link.up.readyState !== WebSocket.OPEN || !expectedTurnId) {
3731
+ if (deliveryId)
3732
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} has no steerable turn` });
3733
+ return Promise.reject(new Error(`${this.id} has no steerable turn`));
3734
+ }
3735
+ const id = this.nextId--;
3736
+ const expectedEpoch = this.contextEpoch;
3737
+ this.steers.add(id);
3738
+ return new Promise((resolve, reject) => {
3739
+ let timer;
3740
+ const settle = () => {
3741
+ clearTimeout(timer);
3742
+ this.steers.delete(id);
3743
+ };
3744
+ const fail = (e) => {
3745
+ settle();
3746
+ if (deliveryId)
3747
+ this.delivery({ id: deliveryId, state: this.knownRejection(e) ? "failed_safe" : "needs_review", reason: e.message });
3748
+ reject(e);
3749
+ };
3750
+ this.pending.set(id, {
3751
+ deliveryId,
3752
+ kind: "steer",
3753
+ expectedTurnId,
3754
+ afterTurn: deliveryId ? () => {
3755
+ if (timer !== undefined)
3756
+ return;
3757
+ timer = setTimeout(() => {
3758
+ const pending = this.pending.get(id);
3759
+ this.pending.delete(id);
3760
+ pending?.reject(new Error("steer unanswered: RPC timeout"));
3761
+ }, this.opts.steerTimeoutMs ?? 1e4);
3762
+ timer.unref?.();
3763
+ } : undefined,
3764
+ resolve: (result) => {
3765
+ if (deliveryId && (this.link !== link || this.contextEpoch !== expectedEpoch || !this.activeTurns.has(expectedTurnId) || typeof result?.turnId === "string" && result.turnId !== expectedTurnId)) {
3766
+ fail(new Error("steer accepted after its original native turn ended or changed"));
3767
+ return;
3768
+ }
3769
+ settle();
3770
+ this.primed = true;
3771
+ this.injected = replyParent(this.injected ? [this.injected, ...envs] : envs);
3772
+ this.answering.push(...envs);
3773
+ if (deliveryId) {
3774
+ this.addTurnDelivery(expectedTurnId, deliveryId);
3775
+ this.delivery({ id: deliveryId, state: "accepted" });
3776
+ }
3777
+ resolve();
3778
+ },
3779
+ reject: fail
3780
+ });
3781
+ const input = [{ type: "text", text: this.render(envs) }];
3782
+ link.up.send(JSON.stringify({ method: "turn/steer", id, params: { threadId: this.threadId, expectedTurnId, input } }));
3912
3783
  });
3913
3784
  }
3914
- reportTurnFailure(envs, reason) {
3915
- try {
3916
- const result = this.opts.onTurnFailure?.(envs, this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? "Configured ACP prompt failed" : reason);
3917
- if (result)
3918
- result.catch(() => this.opts.log?.("ACP failure handoff could not be completed"));
3919
- } catch {
3920
- this.opts.log?.("ACP failure handoff could not be completed");
3921
- }
3785
+ steerText(text) {
3786
+ const link = this.link;
3787
+ const expectedTurnId = [...this.activeTurns].reverse().find((t) => !t.startsWith("unknown:"));
3788
+ if (!link || link.up.readyState !== WebSocket.OPEN || !expectedTurnId)
3789
+ return Promise.resolve("refused");
3790
+ const id = this.nextId--;
3791
+ return new Promise((resolve) => {
3792
+ const timer = setTimeout(() => this.pending.delete(id) && resolve("unanswered"), this.opts.steerTimeoutMs ?? 1e4);
3793
+ timer.unref?.();
3794
+ this.pending.set(id, { resolve: () => (clearTimeout(timer), resolve("accepted")), reject: () => (clearTimeout(timer), resolve("refused")) });
3795
+ link.up.send(JSON.stringify({ method: "turn/steer", id, params: { threadId: this.threadId, expectedTurnId, input: [{ type: "text", text }] } }));
3796
+ });
3922
3797
  }
3923
3798
  onWatchdog() {
3924
- if (this.state !== "busy")
3925
- return;
3926
- if (this.cancelWait) {
3927
- this.clearWatchdog();
3928
- return;
3799
+ this.abortActionWaits();
3800
+ this.deliveryEpoch++;
3801
+ const known = new Set([...this.activeTurns, ...this.turnDeliveries.keys()].filter((id) => !id.startsWith("unknown:")));
3802
+ const pendingStarts = [...this.pending.values()].filter((pending) => pending.kind === "deliver");
3803
+ if ([...this.activeTurns].some((id) => id.startsWith("unknown:")) || (this.unboundDeliveries.size || this.unboundAccepted || pendingStarts.length) && !known.size) {
3804
+ this.unidentifiedCancellation = true;
3805
+ this.opts.log?.(UNIDENTIFIED_CANCELLATION_NOTICE);
3929
3806
  }
3930
- if (this.openTools.size) {
3931
- const ceiling = Math.max(this.watchdogMs, this.opts.toolCeilingMs ?? TOOL_CEILING_MS);
3932
- const remaining = ceiling - (Date.now() - this.lastSignAt);
3933
- if (remaining > 0) {
3934
- this.touch(Math.min(this.watchdogMs, remaining));
3935
- return;
3936
- }
3937
- try {
3938
- this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
3939
- } catch {
3940
- this.activityPrivate = true;
3807
+ for (const turnId of known)
3808
+ this.retireTurn(turnId);
3809
+ for (const pending of pendingStarts)
3810
+ pending.invalidate?.("Codex turn watchdog timeout before native acceptance was confirmed");
3811
+ this.activeTurns.clear();
3812
+ this.unboundAccepted = false;
3813
+ for (const ids of this.turnDeliveries.values())
3814
+ for (const id of ids)
3815
+ this.delivery({ id, state: "needs_review", reason: "Codex turn watchdog timeout" });
3816
+ for (const id of this.unboundDeliveries)
3817
+ this.delivery({ id, state: "needs_review", reason: "Codex turn watchdog timeout" });
3818
+ this.turnDeliveries.clear();
3819
+ this.unboundDeliveries.clear();
3820
+ this.injected = undefined;
3821
+ this.answering = [];
3822
+ this.lastAnswer = "";
3823
+ this.deltas.clear();
3824
+ this.abandonSteers("turn went silent");
3825
+ if (this.unidentifiedCancellation)
3826
+ this.setState("offline");
3827
+ else
3828
+ super.onWatchdog();
3829
+ }
3830
+ retireTurn(turnId) {
3831
+ this.abortActionWaits(undefined, turnId);
3832
+ this.retiredTurns.add(turnId);
3833
+ if (this.link?.up.readyState === WebSocket.OPEN)
3834
+ this.link.up.send(JSON.stringify({ method: "turn/interrupt", id: this.nextId--, params: { threadId: this.threadId, turnId } }));
3835
+ }
3836
+ abandonSteers(reason) {
3837
+ for (const id of this.steers) {
3838
+ const p = this.pending.get(id);
3839
+ this.pending.delete(id);
3840
+ p?.reject(new Error(`steer unanswered: ${reason}`));
3841
+ }
3842
+ }
3843
+ endSteers(turnId) {
3844
+ for (const id of this.steers) {
3845
+ const pending = this.pending.get(id);
3846
+ if (!pending || pending.expectedTurnId !== turnId)
3847
+ continue;
3848
+ if (pending.deliveryId)
3849
+ pending.afterTurn?.();
3850
+ else {
3851
+ this.pending.delete(id);
3852
+ pending.reject(new Error("steer unanswered: turn completed"));
3941
3853
  }
3942
- const tools = this.activityPrivate ? "a tool call" : [...new Set(this.openTools.values())].map((title) => JSON.stringify(activityText(title).replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").slice(0, 80))).join(", ");
3943
- this.opts.log?.(`[${this.id}] open tool call (${tools}) silent past the ${Math.round(ceiling / 60000)} min tool ceiling; cancelling the turn`);
3944
3854
  }
3945
- this.endActivity("watchdog cancelled", true);
3946
- const cancelledTurn = this.turn;
3947
- this.cancelPermissions();
3948
- this.activeLegacy = undefined;
3949
- const durable = !!this.activeDeliveryId;
3950
- if (this.activeDeliveryId)
3951
- this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason: "ACP turn watchdog timeout" });
3952
- this.settleSteers("needs_review", "ACP turn watchdog timeout");
3953
- this.activeDeliveryId = undefined;
3954
- this.notify("session/cancel", { sessionId: this.sessionId });
3955
- this.turn++;
3956
- if (durable)
3855
+ }
3856
+ addTurnDelivery(turnId, deliveryId) {
3857
+ const ids = this.turnDeliveries.get(turnId) ?? new Set;
3858
+ ids.add(deliveryId);
3859
+ this.turnDeliveries.set(turnId, ids);
3860
+ this.unboundDeliveries.delete(deliveryId);
3861
+ }
3862
+ knownRejection(error) {
3863
+ return /turn in progress|no active turn|not injectable|rejected|usage limit/i.test(error.message);
3864
+ }
3865
+ async spawnAppServer() {
3866
+ const port = this.opts.appPort;
3867
+ const probe = () => fetch(`http://127.0.0.1:${port}/healthz`).then((r) => r.ok, () => false);
3868
+ if (await probe()) {
3869
+ throw new Error(`port ${port} already answers /healthz: an app-server the hub does not own is running (orphan from a crashed hub?)`);
3870
+ }
3871
+ let gone = "";
3872
+ const env = peerChildEnv(this.id, { ...process.env, ...this.opts.env ?? {} });
3873
+ this.proc = spawn6(this.opts.bin ?? "codex", ["app-server", "--listen", `ws://127.0.0.1:${port}`, ...this.opts.extraArgs ?? []], {
3874
+ cwd: this.opts.cwd,
3875
+ env,
3876
+ stdio: ["ignore", "ignore", "pipe"],
3877
+ detached: true
3878
+ });
3879
+ trackGroup(this.proc);
3880
+ this.proc.stderr?.on("data", (d) => this.opts.log?.(`[${this.id}] ${String(d).trimEnd()}`));
3881
+ this.proc.on("error", (e) => gone = `cannot run ${this.opts.bin ?? "codex"}: ${e.message}`);
3882
+ this.proc.on("exit", (code) => {
3883
+ gone ||= `codex app-server exited with code ${code}`;
3884
+ this.opts.log?.(`[${this.id}] ${gone}`);
3957
3885
  this.setState("offline");
3958
- else {
3959
- this.clearWatchdog();
3960
- const successor = this.turn;
3961
- const timer = setTimeout(() => {
3962
- const waiting = this.cancelWait;
3963
- if (!waiting || waiting.turn !== cancelledTurn || waiting.successor !== successor)
3964
- return;
3965
- this.clearCancelWait();
3966
- if (!this.stopping && this.turn === successor) {
3967
- this.setState("offline");
3968
- this.opts.log?.(`[${this.id}] cancelled prompt did not report within the cancellation deadline; peer offline, queued work retained; stop and restart the peer to resume delivery`);
3969
- }
3970
- }, this.opts.cancelWaitMs ?? HANDSHAKE_MS);
3971
- timer.unref?.();
3972
- let release;
3973
- const released = new Promise((resolve) => {
3974
- release = resolve;
3975
- });
3976
- this.cancelWait = { turn: cancelledTurn, successor, timer, reported: false, released, release };
3886
+ });
3887
+ for (let i = 0;i < 100 && !gone; i++) {
3888
+ if (await probe())
3889
+ return `ws://127.0.0.1:${port}`;
3890
+ await Bun.sleep(100);
3977
3891
  }
3892
+ if (gone)
3893
+ throw new Error(gone);
3894
+ const proc = this.proc;
3895
+ if (proc && await this.stopQuietly(proc) && this.proc === proc)
3896
+ this.proc = undefined;
3897
+ throw new Error("codex app-server did not become healthy within 10 s");
3978
3898
  }
3979
- clearCancelWait() {
3980
- const waiting = this.cancelWait;
3981
- this.cancelWait = undefined;
3982
- if (waiting) {
3983
- clearTimeout(waiting.timer);
3984
- waiting.release();
3899
+ attach(tui, upstream) {
3900
+ const up = new WebSocket(upstream);
3901
+ const link = { tui, up, backlog: [], tracked: new Map, approvalIds: new Set, ...tui.data.launchId ? { launchId: tui.data.launchId } : {} };
3902
+ this.claimedTui = link;
3903
+ tui.data = link;
3904
+ up.onopen = () => {
3905
+ for (const frame of link.backlog.splice(0))
3906
+ up.send(frame);
3907
+ };
3908
+ up.onmessage = (ev) => this.fromAppServer(link, String(ev.data));
3909
+ up.onclose = () => tui.close();
3910
+ }
3911
+ detach(link) {
3912
+ this.abortActionWaits(link);
3913
+ link.up.close();
3914
+ if (this.claimedTui === link)
3915
+ this.claimedTui = undefined;
3916
+ if (this.link !== link)
3917
+ return;
3918
+ clearInterval(this.usageTimer);
3919
+ this.link = undefined;
3920
+ this.restoringApprovals.clear();
3921
+ this.threadId = "";
3922
+ this.contextEpoch++;
3923
+ this.deliveryEpoch++;
3924
+ this.retiredTurns.clear();
3925
+ this.unidentifiedCancellation = false;
3926
+ this.activeTurns.clear();
3927
+ this.unboundAccepted = false;
3928
+ for (const p of this.pending.values()) {
3929
+ if (p.kind === "deliver")
3930
+ p.invalidate?.("Codex TUI detached");
3931
+ else
3932
+ p.reject(new Error(p.kind === "steer" ? "Codex TUI detached" : "codex TUI detached"));
3985
3933
  }
3934
+ this.pending.clear();
3935
+ for (const ids of this.turnDeliveries.values())
3936
+ for (const id of ids)
3937
+ this.delivery({ id, state: "needs_review", reason: "Codex TUI detached" });
3938
+ for (const id of this.unboundDeliveries)
3939
+ this.delivery({ id, state: "needs_review", reason: "Codex TUI detached" });
3940
+ this.turnDeliveries.clear();
3941
+ this.unboundDeliveries.clear();
3942
+ this.forgetTurn();
3943
+ this.setState("offline");
3986
3944
  }
3987
- observeUsage(value, source) {
3988
- const observation = normalizeACPUsage(value, source);
3989
- this.opts.onUsageDiagnostic?.(observation, this.sessionId);
3990
- if (observation.availability === "known")
3991
- this.opts.onTokens?.(observation.total, this.sessionId);
3945
+ detachForRoster(mode = "ask") {
3946
+ this.permissionMode = mode;
3947
+ const link = this.claimedTui;
3948
+ if (!link)
3949
+ return;
3950
+ link.tui.close(4000, "codex is off roster; retry when a person adds it");
3951
+ this.detach(link);
3952
+ }
3953
+ forgetTurn() {
3954
+ this.injected = undefined;
3955
+ this.answering = [];
3956
+ this.lastAnswer = "";
3957
+ }
3958
+ get proxyAttached() {
3959
+ return !!this.link && this.link.up.readyState === WebSocket.OPEN;
3960
+ }
3961
+ getPermissionMode() {
3962
+ return this.permissionMode;
3963
+ }
3964
+ get permissionModeState() {
3965
+ return this.getPermissionMode();
3992
3966
  }
3993
- currentTurn() {
3994
- return this.turn;
3967
+ setStartupPermissionMode(mode) {
3968
+ if (this.link || this.state !== "offline" || this.overriddenApprovals.size)
3969
+ throw new Error("codex startup mode reconciliation requires a fresh unattached proxy");
3970
+ this.permissionMode = mode;
3995
3971
  }
3996
- adoptIntoTurn(turn, envs, deliveryId) {
3997
- if (turn !== this.turn || this.state !== "busy" || this.cancelWait || this.stopping)
3998
- throw new Error(`${this.id} is ${this.state}, or its turn changed`);
3999
- this.turnEnvs = [...this.turnEnvs, ...envs];
4000
- this.activityPrivate ||= envs.some((env) => env.private === true);
4001
- this.primed = true;
4002
- if (deliveryId) {
4003
- this.steerIds.push(deliveryId);
4004
- this.delivery({ id: deliveryId, state: "accepted" });
4005
- }
3972
+ clearPermissionMode() {
3973
+ const restorable = [...this.overriddenApprovals].every((thread) => this.nativeApprovalPolicies.has(thread));
3974
+ this.permissionMode = "ask";
3975
+ return restorable;
4006
3976
  }
4007
- settleSteers(state, reason) {
4008
- const ids = this.steerIds;
4009
- this.steerIds = [];
4010
- for (const id of ids)
4011
- this.delivery({ id, state, ...state === "needs_review" ? { reason: reason ?? "steered turn did not complete normally" } : {} });
3977
+ async setPermissionMode(mode) {
3978
+ if (!this.link || this.link.up.readyState !== WebSocket.OPEN)
3979
+ throw new Error("codex permission mode unavailable: attach the TUI through ahub codex first");
3980
+ if (mode === "ask" && [...this.overriddenApprovals].some((thread) => !this.nativeApprovalPolicies.has(thread)))
3981
+ throw new Error("codex cannot restore ask: native approval policy unavailable; close the TUI, then run ahub permission codex ask before you restart the Codex session with ahub codex");
3982
+ this.permissionMode = mode;
4012
3983
  }
4013
- acceptDelivery() {
4014
- if (!this.activeDeliveryId || this.deliveryAccepted)
4015
- return;
4016
- this.deliveryAccepted = true;
4017
- this.delivery({ id: this.activeDeliveryId, state: "accepted" });
3984
+ applyPermissionPolicy(params, id) {
3985
+ const thread = typeof params.threadId === "string" ? params.threadId : this.threadId;
3986
+ if (this.permissionMode !== "ask") {
3987
+ this.overriddenApprovals.add(thread);
3988
+ return { ...params, approvalPolicy: this.permissionMode === "never-ask" ? "never" : "on-request" };
3989
+ }
3990
+ if (this.overriddenApprovals.has(thread) && this.nativeApprovalPolicies.has(thread)) {
3991
+ this.restoringApprovals.set(id, thread);
3992
+ return { ...params, approvalPolicy: this.nativeApprovalPolicies.get(thread) };
3993
+ }
3994
+ return params;
4018
3995
  }
4019
- down(reason) {
4020
- this.clearCancelWait();
4021
- if (this.nativePolicy !== undefined || this.opts.requireOwnedGroup)
4022
- reason = "Configured ACP transport stopped";
4023
- this.opts.log?.(`[${this.id}] ${reason}`);
4024
- for (const p of this.pending.values())
4025
- p.reject(new Error(reason));
4026
- this.pending.clear();
4027
- if (this.activeDeliveryId)
4028
- this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason });
4029
- this.settleSteers("needs_review", reason);
4030
- this.activeDeliveryId = undefined;
4031
- this.setState("offline");
3996
+ settlePermissionRestore(id, failed) {
3997
+ const thread = this.restoringApprovals.get(id);
3998
+ this.restoringApprovals.delete(id);
3999
+ if (thread !== undefined && !failed && this.permissionMode === "ask")
4000
+ this.overriddenApprovals.delete(thread);
4032
4001
  }
4033
- onLine(line) {
4034
- if (this.stopping)
4035
- return;
4036
- let msg;
4037
- try {
4038
- msg = JSON.parse(line);
4039
- } catch {
4002
+ fromTui(link, raw) {
4003
+ const msg = parse(raw);
4004
+ if (msg?.id !== undefined && !msg.method && link.approvalIds.has(msg.id))
4040
4005
  return;
4006
+ if (msg?.method === "turn/interrupt" && link === this.link)
4007
+ this.abortActionWaits(link, typeof msg.params?.turnId === "string" ? msg.params.turnId : undefined);
4008
+ if (msg?.method === "turn/start" && msg.params && link === this.link) {
4009
+ const thread = typeof msg.params.threadId === "string" ? msg.params.threadId : this.threadId;
4010
+ const restoring = this.overriddenApprovals.has(thread) && this.nativeApprovalPolicies.has(thread);
4011
+ if (msg.params.approvalPolicy != null && !restoring)
4012
+ this.nativeApprovalPolicies.set(thread, msg.params.approvalPolicy);
4013
+ if (this.permissionMode !== "ask" || this.overriddenApprovals.has(thread)) {
4014
+ msg.params = this.applyPermissionPolicy(msg.params, msg.id);
4015
+ raw = JSON.stringify(msg);
4016
+ }
4041
4017
  }
4042
- if (this.state === "busy" && !this.cancelWait) {
4043
- this.lastSignAt = Date.now();
4018
+ if (msg?.id !== undefined && TRACKED.has(msg.method))
4019
+ link.tracked.set(msg.id, msg.method);
4020
+ if (link.up.readyState === WebSocket.OPEN)
4021
+ link.up.send(raw);
4022
+ else
4023
+ link.backlog.push(raw);
4024
+ }
4025
+ fromAppServer(link, raw) {
4026
+ const msg = parse(raw);
4027
+ const nativeId = msg?.params?.turnId ?? msg?.params?.turn?.id ?? msg?.result?.turn?.id;
4028
+ if (this.state === "busy" && !(typeof nativeId === "string" && this.retiredTurns.has(nativeId)))
4044
4029
  this.touch();
4045
- }
4046
- if (msg.method === "session/update") {
4047
- if (msg.params?.sessionId && msg.params.sessionId !== this.sessionId)
4048
- return;
4049
- const u = msg.params?.update;
4050
- if (u?.sessionUpdate === "usage_update" && this.state !== "offline") {
4051
- const observation = normalizeACPUsage(u, "usage_update");
4052
- if (observation.shape === "context-used") {
4053
- if (this.state === "busy" && !this.cancelWait || msg.params?.sessionId === this.sessionId)
4054
- this.opts.onUsageDiagnostic?.(observation, this.sessionId);
4055
- return;
4056
- }
4057
- }
4058
- if (this.state !== "busy" || this.cancelWait)
4059
- return;
4060
- this.promptActivity = true;
4061
- this.acceptDelivery();
4062
- this.updateWatch(u);
4063
- if (u?.sessionUpdate === "agent_message_chunk" && u.content?.type === "text")
4064
- this.chunks.push(u.content.text);
4065
- else if ((u?.sessionUpdate === "tool_call" || u?.sessionUpdate === "tool_call_update") && typeof u.toolCallId === "string") {
4066
- if (u.sessionUpdate === "tool_call")
4067
- this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId), this.toolTitles.delete(u.toolCallId);
4068
- if (u.rawInput !== undefined)
4069
- this.toolInputs.set(u.toolCallId, u.rawInput);
4070
- if (u.sessionUpdate === "tool_call" && typeof u.title === "string")
4071
- this.toolTitles.set(u.toolCallId, u.title);
4072
- const text = Array.isArray(u.content) ? u.content.map((c) => c?.type === "content" && c.content?.type === "text" ? String(c.content.text) : "").join("") : "";
4073
- if (text)
4074
- this.toolText.set(u.toolCallId, text);
4075
- if (u.status === "completed" || u.status === "failed")
4076
- this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId), this.toolTitles.delete(u.toolCallId), this.openTools.delete(u.toolCallId);
4077
- else if (u.sessionUpdate === "tool_call" || this.openTools.has(u.toolCallId))
4078
- this.openTools.set(u.toolCallId, this.toolTitles.get(u.toolCallId) ?? "tool");
4079
- while (this.openTools.size > TOOL_INPUT_CAP)
4080
- this.openTools.delete(this.openTools.keys().next().value);
4081
- for (const map of [this.toolInputs, this.toolText, this.toolTitles])
4082
- while (map.size > TOOL_INPUT_CAP)
4083
- map.delete(map.keys().next().value);
4084
- } else if (u?.sessionUpdate === "usage_update")
4085
- this.observeUsage(u, "usage_update");
4086
- } else if (msg.method === "session/request_permission") {
4087
- if (this.cancelWait || this.state === "offline") {
4088
- this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "cancelled" } } });
4089
- return;
4090
- }
4091
- if (this.state === "busy" && (!msg.params?.sessionId || msg.params.sessionId === this.sessionId))
4092
- this.promptActivity = true;
4093
- this.answerPermission(msg);
4094
- } else if (msg.method && msg.id !== undefined) {
4095
- this.send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "method not supported by agent-hub" } });
4096
- } else if (msg.id !== undefined) {
4030
+ if (!msg)
4031
+ return void link.tui.send(raw);
4032
+ if (this.interceptActionApproval(link, msg))
4033
+ return;
4034
+ if (msg.id !== undefined && !msg.method)
4035
+ this.settlePermissionRestore(msg.id, !!msg.error);
4036
+ if (typeof msg.id === "number" && msg.id < 0 && !msg.method) {
4097
4037
  const p = this.pending.get(msg.id);
4098
4038
  this.pending.delete(msg.id);
4099
4039
  if (msg.error)
4100
- p?.reject(new Error(msg.error.message ?? "rpc error"));
4040
+ p?.reject(new Error(msg.error.message ?? "app-server rejected the request"));
4101
4041
  else
4102
4042
  p?.resolve(msg.result);
4043
+ return;
4103
4044
  }
4045
+ if (typeof msg.result?.userAgent === "string")
4046
+ this.version = /^[^/\s]+\/(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(msg.result.userAgent)?.[1] ?? this.version;
4047
+ const tracked = msg.id !== undefined && !msg.method ? link.tracked.get(msg.id) : undefined;
4048
+ if (tracked && link.tracked.delete(msg.id)) {
4049
+ if (typeof msg.result?.thread?.id === "string") {
4050
+ const thread = msg.result.thread.id;
4051
+ if (!this.overriddenApprovals.has(thread) && msg.result.approvalPolicy != null)
4052
+ this.nativeApprovalPolicies.set(thread, msg.result.approvalPolicy);
4053
+ this.adopt(link, msg.result.thread.id, tracked === "thread/start");
4054
+ }
4055
+ } else if (msg.method)
4056
+ this.onNotification(link, msg.method, msg.params ?? {});
4057
+ link.tui.send(raw);
4104
4058
  }
4105
- async answerPermission(msg) {
4106
- this.flushActivity();
4107
- const call = msg.params?.toolCall ?? {};
4108
- const kind = typeof call.kind === "string" ? call.kind : undefined;
4109
- if (this.nativePolicy?.lockedReadOnly && !["read", "search", "think"].includes(kind ?? "")) {
4110
- const rejected = (msg.params?.options ?? []).find((option) => option.kind === "reject_once");
4111
- return this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: rejected ? { outcome: "selected", optionId: rejected.optionId } : { outcome: "cancelled" } } });
4112
- }
4113
- const once = (msg.params?.options ?? []).find((o) => o.kind === "allow_once");
4114
- const id = typeof call.toolCallId === "string" ? call.toolCallId : undefined;
4115
- const announced = id === undefined ? undefined : this.toolTitles.get(id);
4116
- const canonical = canonicalMcpToolName(announced, this.opts.mcpServers?.map((s) => s.name));
4117
- const identity = this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? undefined : [typeof call.title === "string" ? call.title : undefined, canonical].find((candidate) => candidate !== undefined && this.opts.autoApprove?.(candidate));
4118
- const rawInput = call.rawInput ?? (id === undefined ? undefined : this.toolInputs.get(id) ?? jsonObject(this.toolText.get(id)));
4059
+ abortActionWaits(link, turn) {
4060
+ for (const [controller, wait] of this.actionWaits)
4061
+ if ((!link || wait.link === link) && (!turn || wait.turn === turn))
4062
+ controller.abort();
4063
+ }
4064
+ interceptActionApproval(link, msg) {
4065
+ if (!this.opts.actionGate || !(typeof msg.id === "string" || Number.isSafeInteger(msg.id)))
4066
+ return false;
4067
+ const legacy = msg.method === "execCommandApproval", params = msg.params;
4068
+ if (!legacy && msg.method !== "item/commandExecution/requestApproval")
4069
+ return false;
4070
+ if (!params || typeof params !== "object")
4071
+ return false;
4072
+ if (link.approvalIds.has(msg.id))
4073
+ return true;
4074
+ if (legacy ? !Array.isArray(params.command) || !params.command.length || !params.command.every((word) => typeof word === "string") : typeof params.command !== "string" || !params.command.length || params.kind != null && params.kind !== "command")
4075
+ return false;
4076
+ const thread = legacy ? params.conversationId : params.threadId;
4119
4077
  let gate;
4120
4078
  try {
4121
- gate = rawInput === undefined || !this.sessionId ? undefined : this.opts.actionGate?.({ names: [call.title, announced, canonical].filter((name) => typeof name === "string"), ...kind ? { kind } : {}, input: rawInput }, this.sessionId);
4079
+ gate = this.opts.actionGate({ names: ["shell"], kind: "execute", input: { command: params.command, cwd: params.cwd } }, typeof thread === "string" ? thread : "");
4122
4080
  } catch {
4123
- gate = { title: "Action approval: the always-ask check failed; approve only if you know what runs" };
4081
+ gate = { title: "Action approval: the always-ask check failed; command withheld", denyOnly: true };
4082
+ }
4083
+ if (!gate)
4084
+ return false;
4085
+ const verdict = gate;
4086
+ if (link.approvalIds.size >= 1024) {
4087
+ this.abortActionWaits(link);
4088
+ link.up.close();
4089
+ link.tui.close(1008, "Codex action approval id capacity reached; reattach");
4090
+ return true;
4091
+ }
4092
+ link.approvalIds.add(msg.id);
4093
+ const named = [...this.activeTurns].filter((turn) => !turn.startsWith("unknown:") && !this.retiredTurns.has(turn));
4094
+ const turn = legacy ? named.length === 1 ? named[0] : undefined : typeof params.turnId === "string" ? params.turnId : undefined;
4095
+ const epoch = this.contextEpoch, controller = new AbortController;
4096
+ const live = () => !controller.signal.aborted && link === this.link && epoch === this.contextEpoch && thread === this.threadId && typeof turn === "string" && this.activeTurns.has(turn) && !this.retiredTurns.has(turn) && !this.unidentifiedCancellation && this.state === "busy" && link.tui.readyState === WebSocket.OPEN && link.up.readyState === WebSocket.OPEN;
4097
+ const answer = (allow) => {
4098
+ if (link.up.readyState === WebSocket.OPEN)
4099
+ link.up.send(JSON.stringify({ id: msg.id, result: { decision: legacy ? allow ? "approved" : "abort" : allow ? "accept" : "decline" } }));
4100
+ };
4101
+ if (!live()) {
4102
+ answer(false);
4103
+ return true;
4104
+ }
4105
+ this.actionWaits.set(controller, { link, turn });
4106
+ const options = [{ optionId: "allow", name: "Allow once", kind: "allow_once" }, { optionId: "deny", name: "Deny", kind: "reject_once" }];
4107
+ if (verdict.denyOnly)
4108
+ options.splice(0, 1);
4109
+ if (!verdict.denyOnly && verdict.budget)
4110
+ options.push({ optionId: ACTION_BUDGET_OPTION, name: `Allow ${verdict.budget.label} for this session`, kind: "allow_once" });
4111
+ const alive = setInterval(() => {
4112
+ if (live())
4113
+ this.touch();
4114
+ }, Math.max(10, Math.min(30000, Math.floor(this.watchdogMs / 3))));
4115
+ (async () => {
4116
+ let picked;
4117
+ try {
4118
+ const cancelled = new Promise((resolve) => controller.signal.addEventListener("abort", () => resolve(undefined), { once: true }));
4119
+ picked = await Promise.race([this.opts.onPermission?.({ peer: this.id, title: verdict.title, tool: "shell", kind: "execute", classed: true, options }, controller.signal), cancelled]);
4120
+ } catch {} finally {
4121
+ clearInterval(alive);
4122
+ this.actionWaits.delete(controller);
4123
+ }
4124
+ let allow = !verdict.denyOnly && live() && picked === "allow";
4125
+ if (!verdict.denyOnly && live() && verdict.budget && picked === ACTION_BUDGET_OPTION) {
4126
+ try {
4127
+ allow = verdict.budget.approve() && live();
4128
+ } catch {
4129
+ allow = false;
4130
+ }
4131
+ }
4132
+ answer(allow);
4133
+ })();
4134
+ return true;
4135
+ }
4136
+ adopt(link, threadId, fresh = false) {
4137
+ if (typeof threadId !== "string" || !threadId || link !== this.claimedTui || link.tui.readyState !== WebSocket.OPEN || link.up.readyState !== WebSocket.OPEN)
4138
+ return;
4139
+ this.abortActionWaits();
4140
+ this.abandonSteers("thread changed");
4141
+ for (const [id, pending] of this.pending)
4142
+ if (pending.kind === "deliver") {
4143
+ pending.invalidate?.("Codex native thread changed before acceptance was confirmed");
4144
+ this.pending.delete(id);
4145
+ }
4146
+ for (const ids of this.turnDeliveries.values())
4147
+ for (const id of ids)
4148
+ this.delivery({ id, state: "needs_review", reason: "Codex native thread changed" });
4149
+ for (const id of this.unboundDeliveries)
4150
+ this.delivery({ id, state: "needs_review", reason: "Codex native thread changed" });
4151
+ this.turnDeliveries.clear();
4152
+ this.unboundDeliveries.clear();
4153
+ this.tokenTotal = undefined;
4154
+ this.tokenKinds = undefined;
4155
+ this.freshThread = fresh;
4156
+ this.link = link;
4157
+ this.threadId = threadId;
4158
+ this.contextEpoch++;
4159
+ this.deliveryEpoch++;
4160
+ this.retiredTurns.clear();
4161
+ this.unidentifiedCancellation = false;
4162
+ this.activeTurns.clear();
4163
+ this.unboundAccepted = false;
4164
+ this.forgetTurn();
4165
+ this.opts.log?.(`[${this.id}] thread ${threadId}`);
4166
+ this.opts.onThread?.(threadId, fresh);
4167
+ this.setState("idle");
4168
+ this.readUsage();
4169
+ clearInterval(this.usageTimer);
4170
+ this.usageTimer = setInterval(() => this.readUsage(), this.opts.usagePollMs ?? 600000);
4171
+ this.usageTimer.unref?.();
4172
+ }
4173
+ revert(turnId) {
4174
+ const link = this.link;
4175
+ if (this.state !== "idle" || !link || link.up.readyState !== WebSocket.OPEN)
4176
+ return Promise.reject(new Error(`${this.id} is not idle`));
4177
+ const id = this.nextId--;
4178
+ return new Promise((resolve, reject) => {
4179
+ const timer = setTimeout(() => this.pending.delete(id) && reject(new Error("thread/revert got no answer")), 30000);
4180
+ timer.unref?.();
4181
+ this.pending.set(id, { resolve: () => (clearTimeout(timer), resolve()), reject: (e) => (clearTimeout(timer), reject(e)) });
4182
+ link.up.send(JSON.stringify({ method: "thread/revert", id, params: { threadId: this.threadId, beforeTurnId: turnId } }));
4183
+ });
4184
+ }
4185
+ readUsage() {
4186
+ const link = this.link;
4187
+ if (!this.opts.onUsage || !link || link.up.readyState !== WebSocket.OPEN)
4188
+ return;
4189
+ const id = this.nextId--;
4190
+ this.pending.set(id, { resolve: (result) => this.opts.onUsage?.(result?.rateLimits, false), reject: () => {} });
4191
+ link.up.send(JSON.stringify({ method: "account/rateLimits/read", id }));
4192
+ setTimeout(() => this.pending.delete(id), 30000).unref?.();
4193
+ }
4194
+ onNotification(link, method, params) {
4195
+ if (link === this.link && method === "account/rateLimits/updated")
4196
+ return void this.opts.onUsage?.(params.rateLimits, false);
4197
+ if (link === this.link && method === "error" && params.error?.codexErrorInfo === "usageLimitExceeded") {
4198
+ this.opts.onUsage?.({ rateLimitReachedType: "usageLimitExceeded" }, true);
4124
4199
  }
4125
- if (once && identity && !gate) {
4126
- this.opts.log?.(`permission auto-approved for ${this.id}: ${identity}`);
4127
- this.emitActivity("ask", `${String(msg.id)}: ${activityText(announced ?? call.title)} (automatic)`);
4128
- this.emitActivity("answered", `${String(msg.id)}: allow_once (automatic)`);
4129
- return this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "selected", optionId: once.optionId } } });
4200
+ if (link !== this.link || params.threadId !== this.threadId)
4201
+ return;
4202
+ const nativeId = method === "turn/started" || method === "turn/completed" ? params.turn?.id : params.turnId;
4203
+ if (typeof nativeId === "string" && this.retiredTurns.has(nativeId)) {
4204
+ if (method === "turn/completed")
4205
+ this.retiredTurns.delete(nativeId);
4206
+ return;
4130
4207
  }
4131
- const raw = rawInput;
4132
- const full = raw === undefined ? "" : typeof raw === "string" ? raw : JSON.stringify(raw);
4133
- const cut = full.length > 600;
4134
- const input = raw === undefined ? "" : `: ${cut ? `${full.slice(0, 600)} [cut, ${full.length} chars]` : full}`;
4135
- const named = typeof call.title === "string" && jsonObject(call.title) === undefined ? call.title : announced;
4136
- const title = gate ? gate.title : raw === undefined ? `${named ?? "tool call"} (payload not reported by the agent)` : `${named ?? "tool call"}${input}`;
4137
- this.emitActivity("ask", `${String(msg.id)}: ${activityText(title)}; answer in ahub console`);
4138
- let options = (msg.params?.options ?? []).filter((o) => o.kind !== "allow_always" || !gate && !this.nativePolicy?.lockedReadOnly && raw !== undefined && !cut);
4139
- const budgetOption = gate?.budget && once ? { optionId: ACTION_BUDGET_OPTION, name: `Allow ${gate.budget.label} for this session`, kind: "allow_once" } : undefined;
4140
- if (budgetOption)
4141
- options = [...options, budgetOption];
4142
- const tool = typeof named === "string" && /^[\w.:@-]{1,80}$/.test(named) ? named : undefined;
4143
- const turn = this.turn;
4144
- const alive = setInterval(() => this.state === "busy" && turn === this.turn && this.touch(), Math.max(10, Math.min(30000, Math.floor(this.watchdogMs / 3))));
4145
- const controller = new AbortController;
4146
- this.permissionWaits.add(controller);
4147
- let picked;
4148
- try {
4149
- const cancelled = new Promise((resolve) => controller.signal.addEventListener("abort", () => resolve(undefined), { once: true }));
4150
- picked = await Promise.race([this.opts.onPermission?.({ peer: this.id, title, options, ...tool ? { tool } : {}, ...kind ? { kind } : {}, ...gate ? { classed: true } : {} }, controller.signal).catch(() => {
4208
+ if (this.unidentifiedCancellation)
4209
+ return;
4210
+ const unknownTurn = () => [...this.activeTurns].some((id) => id.startsWith("unknown:"));
4211
+ if (method === "thread/tokenUsage/updated") {
4212
+ const reader = this.opts.contextReader;
4213
+ this.opts.onContext?.(reader ? reader.read(params.tokenUsage, this.threadId, Date.now()) : codexContext(params.tokenUsage, this.threadId, Date.now()));
4214
+ const total = Number(params.tokenUsage?.total?.totalTokens);
4215
+ if (!Number.isFinite(total))
4151
4216
  return;
4152
- }), cancelled]);
4153
- } finally {
4154
- clearInterval(alive);
4155
- this.permissionWaits.delete(controller);
4217
+ const previous = this.tokenKinds;
4218
+ const kinds = {
4219
+ input: counter(params.tokenUsage?.total?.inputTokens),
4220
+ cachedInput: counter(params.tokenUsage?.total?.cachedInputTokens),
4221
+ output: counter(params.tokenUsage?.total?.outputTokens),
4222
+ reasoning: counter(params.tokenUsage?.total?.reasoningOutputTokens)
4223
+ };
4224
+ const added = this.tokenTotal === undefined ? this.freshThread ? total : 0 : Math.max(0, total - this.tokenTotal);
4225
+ const base = previous ?? (this.freshThread && this.tokenTotal === undefined ? { input: 0, cachedInput: 0, output: 0, reasoning: 0 } : undefined);
4226
+ this.tokenTotal = total;
4227
+ this.tokenKinds = kinds;
4228
+ if (added > 0)
4229
+ this.opts.onTokens?.(added);
4230
+ if (added > 0 && base)
4231
+ this.opts.onTokenBreakdown?.({
4232
+ input: Math.max(0, kinds.input - base.input),
4233
+ cachedInput: Math.max(0, kinds.cachedInput - base.cachedInput),
4234
+ output: Math.max(0, kinds.output - base.output),
4235
+ reasoning: Math.max(0, kinds.reasoning - base.reasoning),
4236
+ total: added
4237
+ });
4238
+ return;
4156
4239
  }
4157
- let valid = !controller.signal.aborted && this.state !== "offline" && turn === this.turn && !this.stopping && options.some((o) => o.optionId === picked);
4158
- let answer = picked;
4159
- if (valid && budgetOption && picked === budgetOption.optionId) {
4160
- let granted = false;
4240
+ if (method === "turn/started") {
4241
+ const nativeTurn = params.turn?.id ?? `unknown:${Date.now()}`;
4242
+ this.unboundAccepted = false;
4243
+ this.activeTurns.add(nativeTurn);
4244
+ for (const id of [...this.unboundDeliveries])
4245
+ this.addTurnDelivery(nativeTurn, id);
4246
+ this.lastAnswer = "";
4247
+ this.setState("busy");
4248
+ if (params.turn?.id)
4249
+ this.opts.onTurn?.(params.turn.id);
4250
+ } else if (method === "item/agentMessage/delta") {
4251
+ if (!this.activeTurns.size || typeof params.turnId === "string" && !this.activeTurns.has(params.turnId) && !unknownTurn())
4252
+ return;
4253
+ const buf = this.deltas.get(params.itemId) ?? [];
4254
+ buf.push(params.delta);
4255
+ this.deltas.set(params.itemId, buf);
4256
+ } else if (method === "item/completed" && ["fileChange", "commandExecution", "mcpToolCall", "userMessage"].includes(params.item?.type)) {
4257
+ if (typeof params.turnId === "string" && !this.activeTurns.has(params.turnId) && !unknownTurn())
4258
+ return;
4161
4259
  try {
4162
- granted = gate.budget.approve();
4163
- } catch {}
4164
- if (granted && once)
4165
- answer = once.optionId;
4166
- else
4167
- valid = false;
4260
+ this.opts.onItem?.(params.item, typeof params.turnId === "string" && this.activeTurns.has(params.turnId) ? params.turnId : undefined);
4261
+ } catch (error) {
4262
+ this.opts.log?.(`[${this.id}] item handler failed: ${error.message}`);
4263
+ }
4264
+ } else if (method === "item/completed" && params.item?.type === "agentMessage") {
4265
+ if (!this.activeTurns.size || typeof params.turnId === "string" && !this.activeTurns.has(params.turnId) && !unknownTurn())
4266
+ return;
4267
+ const item = params.item;
4268
+ const text = item.text ?? item.content?.map((c) => c.text ?? "").join("") ?? this.deltas.get(item.id)?.join("") ?? "";
4269
+ this.deltas.delete(item.id);
4270
+ if (item.phase !== "commentary" && text.trim())
4271
+ this.lastAnswer = text.trim();
4272
+ } else if (method === "turn/completed") {
4273
+ const nativeTurn = params.turn?.id;
4274
+ this.abortActionWaits(link, typeof nativeTurn === "string" ? nativeTurn : undefined);
4275
+ if (!this.activeTurns.size || typeof nativeTurn === "string" && !this.activeTurns.has(nativeTurn) && ![...this.activeTurns].some((id) => id.startsWith("unknown:")))
4276
+ return;
4277
+ this.activeTurns.delete(nativeTurn);
4278
+ for (const id of this.activeTurns)
4279
+ if (id.startsWith("unknown:"))
4280
+ this.activeTurns.delete(id);
4281
+ if (params.turn?.status === "failed") {
4282
+ this.opts.log?.(`[${this.id}] turn failed: ${params.turn.error?.message ?? "unknown error"}`);
4283
+ }
4284
+ this.endSteers(nativeTurn);
4285
+ if (this.activeTurns.size)
4286
+ return;
4287
+ if (typeof nativeTurn === "string") {
4288
+ for (const id of this.turnDeliveries.get(nativeTurn) ?? [])
4289
+ this.delivery({ id, state: params.turn?.status === "failed" ? "needs_review" : "completed", ...params.turn?.status === "failed" ? { reason: params.turn?.error?.message ?? "Codex turn failed" } : {} });
4290
+ this.turnDeliveries.delete(nativeTurn);
4291
+ }
4292
+ const inReplyTo = this.injected;
4293
+ const to = replyAudience(this.answering);
4294
+ this.injected = undefined;
4295
+ this.answering = [];
4296
+ this.deltas.clear();
4297
+ if (this.lastAnswer)
4298
+ this.onMessage?.(this.lastAnswer, inReplyTo ? { inReplyTo, to } : {});
4299
+ this.lastAnswer = "";
4300
+ this.setState("idle");
4168
4301
  }
4169
- if (turn === this.turn)
4170
- this.emitActivity("answered", `${String(msg.id)}: ${valid ? options.find((o) => o.optionId === picked)?.kind : "cancelled"}`);
4171
- this.send({
4172
- jsonrpc: "2.0",
4173
- id: msg.id,
4174
- result: { outcome: valid ? { outcome: "selected", optionId: answer } : { outcome: "cancelled" } }
4175
- });
4176
- }
4177
- request(method, params, timeoutMs) {
4178
- const id = this.nextId++;
4179
- return new Promise((resolve, reject) => {
4180
- const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
4181
- this.pending.delete(id);
4182
- reject(new RpcTimeoutError(`${method} did not answer within ${timeoutMs / 1000} s`));
4183
- }, timeoutMs);
4184
- timer?.unref();
4185
- this.pending.set(id, {
4186
- resolve: (value) => {
4187
- clearTimeout(timer);
4188
- resolve(value);
4189
- },
4190
- reject: (error) => {
4191
- clearTimeout(timer);
4192
- reject(error);
4193
- }
4194
- });
4195
- this.send({ jsonrpc: "2.0", id, method, params });
4196
- });
4197
- }
4198
- notify(method, params) {
4199
- this.send({ jsonrpc: "2.0", method, params });
4200
- }
4201
- send(msg) {
4202
- if (this.proc?.stdin.writable)
4203
- this.proc.stdin.write(`${JSON.stringify(msg)}
4204
- `);
4205
4302
  }
4206
4303
  };
4207
4304
  });
4208
4305
 
4209
4306
  // src/adapters/jcode.ts
4210
- import { spawn as spawn7, spawnSync as spawnSync3 } from "child_process";
4307
+ import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
4211
4308
  import { createConnection } from "net";
4212
4309
  import { closeSync as closeSync3, constants as constants2, existsSync as existsSync4, fstatSync as fstatSync3, lstatSync as lstatSync6, mkdtempSync as mkdtempSync3, openSync as openSync3, readSync as readSync2, rmSync as rmSync3 } from "fs";
4213
4310
  import { tmpdir as tmpdir3 } from "os";
@@ -4954,12 +5051,12 @@ var init_deny = __esm(() => {
4954
5051
  });
4955
5052
 
4956
5053
  // src/local/sandbox.ts
4957
- import { spawn as spawn8, spawnSync as spawnSync4 } from "child_process";
5054
+ import { spawn as spawn8, spawnSync as spawnSync3 } from "child_process";
4958
5055
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync4, readdirSync, rmSync as rmSync5 } from "fs";
4959
5056
  import { homedir as homedir4, tmpdir as tmpdir4 } from "os";
4960
5057
  import { dirname as dirname9, join as join11, resolve as resolve9 } from "path";
4961
5058
  function developerDir() {
4962
- const out = spawnSync4("xcode-select", ["-p"], { encoding: "utf8" });
5059
+ const out = spawnSync3("xcode-select", ["-p"], { encoding: "utf8" });
4963
5060
  let dir = out.status === 0 ? out.stdout.trim() : "";
4964
5061
  try {
4965
5062
  dir = dir && realPath(dir);
@@ -5085,7 +5182,7 @@ function sandboxedExec(argv, opts) {
5085
5182
  rmSync5(own, { recursive: true, force: true });
5086
5183
  } catch {
5087
5184
  try {
5088
- spawnSync4("chflags", ["-R", "nouchg", own]);
5185
+ spawnSync3("chflags", ["-R", "nouchg", own]);
5089
5186
  rmSync5(own, { recursive: true, force: true });
5090
5187
  } catch {}
5091
5188
  }
@@ -5215,7 +5312,7 @@ async function runTool(name, rawArgs, ctx) {
5215
5312
  const approvedTarget = file;
5216
5313
  const content = String(a.content ?? "");
5217
5314
  const refused = approvalError(await ctx.permit(`write ${a.path} (${content.length} chars):
5218
- ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path)), "error: the user did not approve this write");
5315
+ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a), "error: the user did not approve this write");
5219
5316
  if (refused)
5220
5317
  return refused;
5221
5318
  if (ctx.signal?.aborted)
@@ -5236,7 +5333,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path)), "er
5236
5333
  return `error: \`old\` must match exactly once, it matched ${count} times`;
5237
5334
  const refused = approvalError(await ctx.permit(`edit ${a.path}:
5238
5335
  - ${preview(String(a.old), 600)}
5239
- + ${preview(String(a.new ?? ""), 600)}`, "edit", ctx.signal, file, undefined, String(a.path)), "error: the user did not approve this edit");
5336
+ + ${preview(String(a.new ?? ""), 600)}`, "edit", ctx.signal, file, undefined, String(a.path), a), "error: the user did not approve this edit");
5240
5337
  if (refused)
5241
5338
  return refused;
5242
5339
  if (ctx.signal?.aborted)
@@ -5257,7 +5354,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path)), "er
5257
5354
  return "error: empty command";
5258
5355
  if (command.length > 4000)
5259
5356
  return "error: command longer than 4000 characters; put it in a script file with write, then run that";
5260
- const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal), "error: the user did not approve this command");
5357
+ const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal, undefined, undefined, undefined, a), "error: the user did not approve this command");
5261
5358
  if (refused)
5262
5359
  return refused;
5263
5360
  const res = await sandboxedExec(["/bin/bash", "-c", command], { cwd: ctx.cwd, profile: ctx.sandboxProfile, readOnlyData: !!ctx.readOnly?.length, timeoutMs: (Number(a.timeout_s) || 120) * 1000, ...ctx.sandboxEnv ? { env: ctx.sandboxEnv } : {}, ...ctx.signal ? { signal: ctx.signal } : {} });
@@ -5276,7 +5373,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path)), "er
5276
5373
  if (problem)
5277
5374
  return `error: git ${sub}: ${problem}`;
5278
5375
  if (GIT_WRITE.has(sub)) {
5279
- const refused = approvalError(await ctx.permit(`git ${args.join(" ")}`, "git", ctx.signal), "error: the user did not approve this git command");
5376
+ const refused = approvalError(await ctx.permit(`git ${args.join(" ")}`, "git", ctx.signal, undefined, undefined, undefined, a), "error: the user did not approve this git command");
5280
5377
  if (refused)
5281
5378
  return refused;
5282
5379
  }
@@ -8315,7 +8412,9 @@ var init_task_code_symbols = __esm(() => {
8315
8412
  });
8316
8413
 
8317
8414
  // src/hub/planning.ts
8318
- var init_planning = () => {};
8415
+ var init_planning = __esm(() => {
8416
+ init_sdlc();
8417
+ });
8319
8418
  // src/hub/deliverables.ts
8320
8419
  var DELIVERABLE_LIMITS;
8321
8420
  var init_deliverables = __esm(() => {
@@ -9106,7 +9205,7 @@ var init_hub_tools = __esm(() => {
9106
9205
  tool("hub_check_cancel", "Cancel your task-bound isolated check; only its owner or the person may cancel.", { run: str2 }, ["run"]),
9107
9206
  tool("hub_delivery_retry", "Request one redelivery of your own needs_review delivery. An earlier attempt may have effects: inspect those effects before acting. Refuses another peer's row or any lineage already explicitly retried; a repeated uncertain row needs the person. Never completes/discards a delivery or changes task state.", { delivery_id: { type: "string", minLength: 1, maxLength: 256 } }, ["delivery_id"]),
9108
9207
  tool("hub_backlog_list", "Read public backlog cards with planning revisions. Configured planners only; PII remains a stub.", {}),
9109
- tool("hub_backlog_plan", "Plan an unpinned backlog item outside frozen releases with the revision you read and a bounded reason. A person's rank is retained. Actual dates cannot be set.", { id, revision: { type: "integer", minimum: 0 }, reason: { type: "string", maxLength: 1000 }, fields: { type: "object", properties: { rank: { type: ["integer", "null"], minimum: 0 }, priority: { type: ["integer", "null"], minimum: 0, maximum: 3 }, release: { type: ["integer", "null"], minimum: 1 }, plannedStart: { type: ["integer", "null"], minimum: 1 }, due: { type: ["integer", "null"], minimum: 1 }, deps: { type: "array", maxItems: 64, uniqueItems: true, items: { type: "integer", minimum: 1 } }, reserved: { type: ["string", "null"], minLength: 1, maxLength: 32 } }, additionalProperties: false } }, ["id", "revision", "reason", "fields"]),
9208
+ tool("hub_backlog_plan", "Plan an unpinned backlog item outside frozen releases with the revision you read and a bounded reason. Omit fields or give {} for a note-only revision. fields.sdlc accepts only repo/issue identity on unstarted backlog work; relinking clears old spec acceptance. A person's rank is retained. Actual dates cannot be set.", { id, revision: { type: "integer", minimum: 0 }, reason: { type: "string", maxLength: 1000 }, fields: { type: "object", properties: { sdlc: { type: "object", minProperties: 1, properties: { repo: { type: ["string", "null"], maxLength: 400 }, issue: { type: ["integer", "null"], minimum: 1 } }, additionalProperties: false }, rank: { type: ["integer", "null"], minimum: 0 }, priority: { type: ["integer", "null"], minimum: 0, maximum: 3 }, release: { type: ["integer", "null"], minimum: 1 }, plannedStart: { type: ["integer", "null"], minimum: 1 }, due: { type: ["integer", "null"], minimum: 1 }, deps: { type: "array", maxItems: 64, uniqueItems: true, items: { type: "integer", minimum: 1 } }, reserved: { type: ["string", "null"], minLength: 1, maxLength: 32 } }, additionalProperties: false } }, ["id", "revision", "reason"]),
9110
9209
  tool("hub_backlog_pull", "Reserve a WIP slot and pull ready unpinned backlog work into To Do at its current revision. Preserves person order/reservations and frozen scope; pending unowned work can be undone with hub_backlog_undo.", { id, revision: { type: "integer", minimum: 0 }, reason: { type: "string", minLength: 1, maxLength: 1000 } }, ["id", "revision", "reason"]),
9111
9210
  tool("hub_backlog_split", "Split unstarted backlog work into two to eight staged parts. The original card retains integration/acceptance and depends on the children. Use after for existing prerequisite ids and afterParts for earlier zero-based part indexes. Latest unused unchanged children can be undone with hub_backlog_undo.", { id, revision: { type: "integer", minimum: 0 }, reason: { type: "string", minLength: 1, maxLength: 1000 }, parts: { type: "array", minItems: 2, maxItems: 8, items: { type: "object", properties: { title: { type: "string", minLength: 1, maxLength: 300 }, detail: { type: "string", maxLength: 8000 }, class: str2, refs, plan, after: { type: "array", maxItems: 64, uniqueItems: true, items: { type: "integer", minimum: 1 } }, afterParts: { type: "array", maxItems: 8, uniqueItems: true, items: { type: "integer", minimum: 0 } } }, required: ["title"], additionalProperties: false } } }, ["id", "revision", "reason", "parts"]),
9112
9211
  tool("hub_backlog_undo", "Undo your latest planning change at its current revision. Refuses intervening changes and another actor's edit.", { id, revision: { type: "integer", minimum: 1 } }, ["id", "revision"]),
@@ -9127,9 +9226,9 @@ var init_hub_tools = __esm(() => {
9127
9226
  tool("hub_checkpoint", "Answer a checkpoint request from the hub (quota or context window pressure): what you were doing, what is half done, what whoever continues must know. Write the same to .agenthub/checkpoint.md first if you can.", { summary: str2, request_id: { type: "string", description: "The request id supplied by a context checkpoint request; required for context checkpoints" } }, ["summary"]),
9128
9227
  tool("hub_memory_search", "Search daemon-scoped visible memory, fails first. Query <=200 UTF-8 bytes; default 8, max 20 lines; whole output <=4 KB. Incomplete excerpts are marked. No project argument.", { query: { type: "string", maxLength: 200, description: "At most 200 UTF-8 bytes, enforced by the daemon" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, ["query"]),
9129
9228
  tool("hub_memory_show", "Read up to 8 visible memory ids or unique prefixes (6..64 UTF-8 bytes each). Original ids redirect; unavailable ids share one refusal. Whole output <=8 KB.", { ids: { type: "array", minItems: 1, maxItems: 8, items: { type: "string", minLength: 6, maxLength: 64, description: "6 to 64 UTF-8 bytes" } } }, ["ids"]),
9130
- tool("hub_memory_graph", "Read screened memory neighbourhoods, fails first. Up to 8 starts (<=200 UTF-8 bytes each), 1 or 2 hops; default 60/max 120 nodes, max 240 edges, whole output <=6 KB. No project argument.", { starts: { type: "array", minItems: 1, maxItems: 8, items: { type: "string", minLength: 1, maxLength: 200, description: "At most 200 UTF-8 bytes" } }, hops: { type: "integer", enum: [1, 2] }, limit: { type: "integer", minimum: 1, maximum: 120 } }, ["starts"]),
9229
+ tool("hub_memory_graph", "Read screened memory neighbourhoods, fails first. Symbol starts also include optional current-head one-hop code neighbours as derived SHA-stamped edges; unknown coverage is explicit. Up to 8 starts (<=200 UTF-8 bytes each), 1 or 2 hops; default 60/max 120 nodes, max 240 edges, whole output <=6 KB. No project argument.", { starts: { type: "array", minItems: 1, maxItems: 8, items: { type: "string", minLength: 1, maxLength: 200, description: "At most 200 UTF-8 bytes" } }, hops: { type: "integer", enum: [1, 2] }, limit: { type: "integer", minimum: 1, maximum: 120 } }, ["starts"]),
9131
9230
  tool("hub_memory_correct", "Propose a correction to a visible memory entry. This creates a machine-local candidate only; a person accepts or rejects it with amem review. It never edits a note. id is an exact id or unique visible prefix; text and reason must each fit 4000 UTF-8 bytes.", { id: str2, text: str2, reason: str2 }, ["id", "text", "reason"]),
9132
- tool("hub_remember", "Save a decision, finding, contract or fail to the memory all agents share (the hub's memory and claude-mem); the other agents also get it with their next message. A fail is an approach you tried that does not work, and why: the most useful note, it stops the others spending their quota on it. Do not retry what a fail note rules out without new evidence. Conclusions worth recalling, not chatter. For kind fail, optional approach, symptom, cause, evidence and retry_when strings compose fixed labeled text; text may be omitted when structured fields are supplied. The complete text must fit 4000 UTF-8 bytes and is refused, never truncated, when oversized. A note on an open task is short-term until the task is approved, when its decisions, contracts and fails become long-term; horizon long keeps it now, scope shared lets the person's other projects find it.", { text: str2, approach: str2, symptom: str2, cause: str2, evidence: str2, retry_when: str2, title: str2, kind: { type: "string", enum: [...NOTE_KINDS] }, task: id, horizon: { type: "string", enum: ["short", "long"] }, scope: { type: "string", enum: ["project", "shared"] } })
9231
+ tool("hub_remember", "Save a decision, finding, contract or fail to the memory all agents share (the hub's amem curated store); the other agents also get it with their next message. A fail is an approach you tried that does not work, and why: the most useful note, it stops the others spending their quota on it. Do not retry what a fail note rules out without new evidence. Conclusions worth recalling, not chatter. For kind fail, optional approach, symptom, cause, evidence and retry_when strings compose fixed labeled text; text may be omitted when structured fields are supplied. The complete text must fit 4000 UTF-8 bytes and is refused, never truncated, when oversized. A note on an open task is short-term until the task is approved, when its decisions, contracts and fails become long-term; horizon long keeps it now, scope shared lets the person's other projects find it.", { text: str2, approach: str2, symptom: str2, cause: str2, evidence: str2, retry_when: str2, title: str2, kind: { type: "string", enum: [...NOTE_KINDS] }, task: id, horizon: { type: "string", enum: ["short", "long"] }, scope: { type: "string", enum: ["project", "shared"] } })
9133
9232
  ];
9134
9233
  PLANNER_REVIEW_TOOLS = [
9135
9234
  tool("hub_planner_groom", "Read a bounded planner grooming digest of dependencies, repository-scoped overlaps, WIP and blockers. No board change; unavailable evidence stays unknown.", {}),
@@ -9407,6 +9506,7 @@ var init_local_worker = __esm(() => {
9407
9506
  this.routePii = !!policy?.pii;
9408
9507
  const ctx = this.toolContext(envs, turnSignal, !!policy?.pii, reply);
9409
9508
  let usedTools = false;
9509
+ let captureInvocation = 0;
9410
9510
  for (let step = 0;step < maxSteps; step++) {
9411
9511
  this.elide(msgs);
9412
9512
  const res = await this.call(msgs, policy, envs);
@@ -9455,7 +9555,7 @@ var init_local_worker = __esm(() => {
9455
9555
  }
9456
9556
  this.observeTool(call, output, policy, `${this.sessionId}.${turn}.${step}`);
9457
9557
  if (name !== "hub_send" && !TASK_TOOL_NAMES.has(name) && !CONDUCTOR_TOOL_NAMES.has(name))
9458
- capture?.observe({ tool: call.function.name, args: call.function.arguments, output, id: call.id, paths: touchedPaths(call.function.name, safeParse(call.function.arguments)) });
9558
+ capture?.observe({ tool: call.function.name, args: call.function.arguments, output, id: call.id, sourceId: `${this.sessionId}.${turn}.${step}.${captureInvocation++}`, paths: touchedPaths(call.function.name, safeParse(call.function.arguments)) });
9459
9559
  }
9460
9560
  } finally {
9461
9561
  if (results.length)
@@ -9471,7 +9571,7 @@ var init_local_worker = __esm(() => {
9471
9571
  cwd: this.opts.cwd,
9472
9572
  deny: this.opts.tools.deny,
9473
9573
  readOnly: this.opts.tools.readOnly,
9474
- permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath) => {
9574
+ permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input) => {
9475
9575
  if (turnSignal.aborted)
9476
9576
  return "aborted";
9477
9577
  const generation = this.turn;
@@ -9500,7 +9600,7 @@ var init_local_worker = __esm(() => {
9500
9600
  };
9501
9601
  const onAbort = () => finish("aborted");
9502
9602
  turnSignal.addEventListener("abort", onAbort, { once: true });
9503
- this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath).then(finish, (error) => {
9603
+ this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input).then(finish, (error) => {
9504
9604
  turnSignal.removeEventListener("abort", onAbort);
9505
9605
  reject(error);
9506
9606
  });
@@ -10683,7 +10783,7 @@ var init_native_cli_entry_codex = () => {};
10683
10783
 
10684
10784
  // src/adapters/definitions/native-cli-entry-kimi.ts
10685
10785
  import { resolve as resolve13 } from "path";
10686
- import { spawnSync as spawnSync5 } from "child_process";
10786
+ import { spawnSync as spawnSync4 } from "child_process";
10687
10787
  async function runKimiEntry(ctx) {
10688
10788
  const peer = ctx.instance ?? "kimi";
10689
10789
  const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, connect, fail, takeFlags } = ctx;
@@ -10750,7 +10850,7 @@ async function runKimiEntry(ctx) {
10750
10850
  const written = configureKimiMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve13(cliDir, "../../plugins/agent-hub/server.js"), "tui");
10751
10851
  if (written.tracked)
10752
10852
  console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
10753
- result = spawnSync5(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
10853
+ result = spawnSync4(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
10754
10854
  } finally {
10755
10855
  if (admission.admissionId)
10756
10856
  await hub.request({ t: "kimi_launch_end", admissionId: admission.admissionId }).catch(() => {});
@@ -11959,7 +12059,7 @@ var init_builtin_metadata = __esm(() => {
11959
12059
  permission: { grants: { ...hubGrants } },
11960
12060
  session: { idField: "sessionId", currentId: sessionId("sessionId"), restoreOnStart: true, launchKind: "pi", launchRecord: true, ...PI_RECOVERY_SESSION, crashPlan: piCrashPlan, unrecordedTerminal: "tui", continuityBookmark: piRecoveryContinuity, continuity: { key: "pi", parse: parsePiContinuity } },
11961
12061
  turnEnd: "adapter",
11962
- budget: { account: "pi", instrumented: true },
12062
+ budget: { account: "pi", instrumented: true, noQuotaReason: "on-prem backends have no provider quota" },
11963
12063
  reporting: { tokensNotReported: true },
11964
12064
  configuration: { schema: piConfigSchema, routingSchema: piRoutingSchema, settings: PI_SETTINGS, restartReasons: PI_RESTART_REASONS, validateEffective: piEffectiveConfig },
11965
12065
  routing: { localAllowedRefusal: "local_allowed = false also excludes pi for this class", longContext: "backend-limit", preferredTier: true, handoffPriority: 0, trace: "backend", backendLabel: "pi" },
@@ -11982,7 +12082,7 @@ var init_builtin_metadata = __esm(() => {
11982
12082
  permission: { grants: { ...hubGrants } },
11983
12083
  session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: true, launchKind: "local", ...LOCAL_RECOVERY_SESSION, crashPlan: localCrashPlan },
11984
12084
  turnEnd: "adapter",
11985
- budget: { account: "local", quotaExempt: true, instrumented: true },
12085
+ budget: { account: "local", quotaExempt: true, instrumented: true, noQuotaReason: "on-prem gateway has no provider quota" },
11986
12086
  configuration: { schema: localConfigSchema, routingSchema: localRoutingSchema, restartReasons: LOCAL_RESTART_REASONS },
11987
12087
  routing: { localAllowedRefusal: "local_allowed = false for this class", longContext: "skip-owner", preferredTier: true, handoffPriority: 1, trace: "route" },
11988
12088
  cli: { command: "local", entry: { lifecycleAdmission: false, execute: runLocalEntry }, usage: [["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"]], launchConfigKeys: ["local.deny", "local.bash_network", "local.network_allow", "local.max_steps", "local.read_allow", "routing.local"], startWords: () => ["local"] },
@@ -12123,7 +12223,7 @@ var init_builtin_metadata = __esm(() => {
12123
12223
  permission: { modes: {}, grants: { ...hubGrants } },
12124
12224
  session: { idField: "sessionId", currentId: sessionId("sessionId"), launchKind: "pi", launchRecord: true },
12125
12225
  turnEnd: "adapter",
12126
- budget: { account: "pi" },
12226
+ budget: { account: "pi", noQuotaReason: "on-prem backends have no provider quota" },
12127
12227
  cli: { command: "pi", startWords: (form) => ["pi", "--mode", form] },
12128
12228
  display: palette("#c79936")
12129
12229
  },
@@ -12768,19 +12868,57 @@ var init_registry = __esm(() => {
12768
12868
  }));
12769
12869
  });
12770
12870
 
12871
+ // node_modules/@staix/agent-mem/src/memory/private-spans.ts
12872
+ var init_private_spans = () => {};
12873
+
12771
12874
  // node_modules/@staix/agent-mem/src/memory/screen.ts
12772
12875
  var init_screen = () => {};
12773
12876
 
12774
12877
  // node_modules/@staix/agent-mem/src/memory/mutation.ts
12775
12878
  var init_mutation = () => {};
12776
12879
 
12880
+ // node_modules/@staix/agent-mem/src/memory/project.ts
12881
+ var init_project2 = __esm(() => {
12882
+ init_platform();
12883
+ });
12884
+
12885
+ // node_modules/@staix/agent-mem/src/memory/policy.ts
12886
+ var MAX_BYTES3, MAX_STRING_BYTES, flags;
12887
+ var init_policy = __esm(() => {
12888
+ init_screen();
12889
+ init_platform();
12890
+ init_project2();
12891
+ MAX_BYTES3 = 256 * 1024;
12892
+ MAX_STRING_BYTES = 16 * 1024;
12893
+ flags = new Set(["private", "pii", "privateturn", "isprivate", "ispii"]);
12894
+ });
12895
+
12896
+ // node_modules/@staix/agent-mem/src/memory/config.ts
12897
+ var MAX_CONFIG_BYTES2;
12898
+ var init_config2 = __esm(() => {
12899
+ init_policy();
12900
+ MAX_CONFIG_BYTES2 = 64 * 1024;
12901
+ });
12902
+
12777
12903
  // node_modules/@staix/agent-mem/src/memory/home.ts
12778
12904
  var init_home = __esm(() => {
12779
12905
  init_platform();
12906
+ init_config2();
12780
12907
  });
12781
12908
 
12782
- // node_modules/@staix/agent-mem/src/memory/search-fts.ts
12783
- var init_search_fts = () => {};
12909
+ // node_modules/@staix/agent-mem/src/memory/machine.ts
12910
+ var init_machine = () => {};
12911
+
12912
+ // node_modules/@staix/agent-mem/src/memory/lifecycle-log.ts
12913
+ var MAX_LOG_BYTES;
12914
+ var init_lifecycle_log = __esm(() => {
12915
+ MAX_LOG_BYTES = 8 * 1024 * 1024;
12916
+ });
12917
+
12918
+ // node_modules/@staix/agent-mem/src/memory/aliases.ts
12919
+ var init_aliases = __esm(() => {
12920
+ init_lifecycle_log();
12921
+ });
12784
12922
 
12785
12923
  // node_modules/@staix/agent-mem/src/memory/generation.ts
12786
12924
  var init_generation = __esm(() => {
@@ -12791,16 +12929,41 @@ var init_generation = __esm(() => {
12791
12929
  // node_modules/@staix/agent-mem/src/memory/shard.ts
12792
12930
  var init_shard = __esm(() => {
12793
12931
  init_generation();
12932
+ init_aliases();
12933
+ init_aliases();
12934
+ });
12935
+
12936
+ // node_modules/@staix/agent-mem/src/memory/lifecycle.ts
12937
+ var init_lifecycle = __esm(() => {
12938
+ init_aliases();
12939
+ init_lifecycle_log();
12940
+ init_shard();
12941
+ });
12942
+
12943
+ // node_modules/@staix/agent-mem/src/memory/search-fts.ts
12944
+ var init_search_fts = () => {};
12945
+
12946
+ // node_modules/@staix/agent-mem/src/memory/capture-input.ts
12947
+ var init_capture_input = __esm(() => {
12948
+ init_screen();
12949
+ init_platform();
12950
+ init_private_spans();
12794
12951
  });
12795
12952
 
12796
12953
  // node_modules/@staix/agent-mem/src/memory/capture.ts
12954
+ var SPOOL_MAX_BYTES;
12797
12955
  var init_capture = __esm(() => {
12798
12956
  init_screen();
12799
- init_platform();
12957
+ init_capture_input();
12958
+ init_capture_input();
12800
12959
  init_home();
12801
12960
  init_shard();
12802
- init_store();
12961
+ init_lifecycle();
12962
+ init_machine();
12963
+ init_config2();
12964
+ init_policy();
12803
12965
  init_search_fts();
12966
+ SPOOL_MAX_BYTES = 256 * 1024;
12804
12967
  });
12805
12968
 
12806
12969
  // node_modules/@staix/agent-mem/src/memory/ops-contract.ts
@@ -13027,9 +13190,13 @@ var init_reader_lease = __esm(() => {
13027
13190
  var DEFAULT_TTL_MS, UNSETTLED_MS, DEFAULT_LEASE_MS, MIGRATION_BOOTSTRAP;
13028
13191
  var init_store = __esm(() => {
13029
13192
  init_platform();
13193
+ init_private_spans();
13030
13194
  init_screen();
13031
13195
  init_mutation();
13032
13196
  init_home();
13197
+ init_machine();
13198
+ init_lifecycle();
13199
+ init_machine();
13033
13200
  init_search_fts();
13034
13201
  init_shard();
13035
13202
  init_capture();
@@ -13039,10 +13206,97 @@ var init_store = __esm(() => {
13039
13206
  init_bridge();
13040
13207
  init_migrate();
13041
13208
  init_reader_lease();
13042
- DEFAULT_TTL_MS = 14 * 24 * 3600000;
13043
- UNSETTLED_MS = 180 * 24 * 3600000;
13044
- DEFAULT_LEASE_MS = 4 * 3600000;
13045
- MIGRATION_BOOTSTRAP = Symbol("migration index bootstrap");
13209
+ DEFAULT_TTL_MS = 14 * 24 * 3600000;
13210
+ UNSETTLED_MS = 180 * 24 * 3600000;
13211
+ DEFAULT_LEASE_MS = 4 * 3600000;
13212
+ MIGRATION_BOOTSTRAP = Symbol("migration index bootstrap");
13213
+ });
13214
+
13215
+ // node_modules/@staix/agent-mem/src/memory/work-lists.ts
13216
+ var forbidden;
13217
+ var init_work_lists = __esm(() => {
13218
+ init_generation();
13219
+ init_mutation();
13220
+ init_platform();
13221
+ init_screen();
13222
+ init_private_spans();
13223
+ init_shard();
13224
+ init_machine();
13225
+ forbidden = new Set(["__proto__", "constructor", "prototype"]);
13226
+ });
13227
+
13228
+ // node_modules/@staix/agent-mem/src/memory/generation-sync.ts
13229
+ var zero;
13230
+ var init_generation_sync = __esm(() => {
13231
+ init_bridge();
13232
+ init_generation();
13233
+ init_mutation();
13234
+ init_platform();
13235
+ init_prune();
13236
+ init_sync();
13237
+ zero = "0".repeat(40);
13238
+ });
13239
+
13240
+ // node_modules/@staix/agent-mem/src/memory/sync-screen.ts
13241
+ var MAX_BYTES4, GENERATED;
13242
+ var init_sync_screen = __esm(() => {
13243
+ init_config2();
13244
+ init_policy();
13245
+ MAX_BYTES4 = 8 * 1024 * 1024;
13246
+ GENERATED = new Set(["id", "schema", "op", "at", "machine", "actor", "target", "promotedFrom", "supersedes", "expires", "by", "tier", "scope", "kind", "source"]);
13247
+ });
13248
+
13249
+ // node_modules/@staix/agent-mem/src/memory/sync.ts
13250
+ var liveGroups;
13251
+ var init_sync = __esm(() => {
13252
+ init_platform();
13253
+ init_mutation();
13254
+ init_generation_sync();
13255
+ init_sync_screen();
13256
+ liveGroups = new Set;
13257
+ });
13258
+
13259
+ // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
13260
+ var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
13261
+ var init_candidate_pending = __esm(() => {
13262
+ CANDIDATE_AGE_MS = 30 * 86400000;
13263
+ CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
13264
+ CANDIDATE_DUE_MS = 7 * 86400000;
13265
+ });
13266
+
13267
+ // node_modules/@staix/agent-mem/src/memory/path-glob.ts
13268
+ var init_path_glob = __esm(() => {
13269
+ init_project2();
13270
+ });
13271
+
13272
+ // node_modules/@staix/agent-mem/src/memory/repo-scope.ts
13273
+ var REPO_DIR = ".amem", REPO_LOG = "log.jsonl", REPO_ATTRIBUTES_LINE, MAX_LINE_BYTES, MAX_FILE_BYTES, MAX_CONFIG_BYTES3;
13274
+ var init_repo_scope = __esm(() => {
13275
+ init_platform();
13276
+ init_policy();
13277
+ init_project2();
13278
+ init_path_glob();
13279
+ init_shard();
13280
+ REPO_ATTRIBUTES_LINE = `${REPO_DIR}/${REPO_LOG} merge=union`;
13281
+ MAX_LINE_BYTES = 16 * 1024;
13282
+ MAX_FILE_BYTES = 1024 * 1024;
13283
+ MAX_CONFIG_BYTES3 = 1024 * 1024;
13284
+ });
13285
+
13286
+ // node_modules/@staix/agent-mem/src/session-brief.ts
13287
+ var init_session_brief = __esm(() => {
13288
+ init_generation();
13289
+ init_home();
13290
+ init_platform();
13291
+ init_policy();
13292
+ init_config2();
13293
+ init_candidate_pending();
13294
+ init_project2();
13295
+ init_reader_lease();
13296
+ init_shard();
13297
+ init_lifecycle();
13298
+ init_repo_scope();
13299
+ init_work_lists();
13046
13300
  });
13047
13301
 
13048
13302
  // node_modules/ajv/dist/compile/codegen/code.js
@@ -20263,95 +20517,9 @@ init_registry();
20263
20517
  init_hub_tools();
20264
20518
  init_envelope();
20265
20519
 
20266
- // src/hub/tasks.ts
20267
- init_task_code_symbols();
20268
- init_test_integrity();
20269
- init_plan_conformance();
20270
-
20271
- // src/hub/sdlc-release.ts
20272
- init_child_process();
20273
- init_git();
20274
-
20275
- // src/hub/tasks.ts
20276
- init_spec_read();
20277
- init_sdlc_metrics();
20278
- // package.json
20279
- var package_default = {
20280
- name: "@staix/agent-hub",
20281
- version: "0.15.0",
20282
- description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
20283
- license: "MIT",
20284
- type: "module",
20285
- bin: {
20286
- ahub: "src/cli/main.js",
20287
- "agent-hub": "src/cli/main.js"
20288
- },
20289
- scripts: {
20290
- check: "scripts/check.sh",
20291
- build: "bun scripts/build.mjs",
20292
- test: "bun test"
20293
- },
20294
- engines: {
20295
- bun: ">=1.4.2"
20296
- },
20297
- dependencies: {
20298
- "@staix/agent-mem": "0.4.0"
20299
- },
20300
- devDependencies: {
20301
- "@modelcontextprotocol/sdk": "^1.27.1",
20302
- "@types/bun": "^1.3.0",
20303
- typescript: "^5.8.0"
20304
- },
20305
- files: [
20306
- ".claude-plugin",
20307
- "src",
20308
- "src/pet/window.jxa.js",
20309
- "src/pet/window.html",
20310
- "plugins",
20311
- "templates",
20312
- "scripts/check-docs.mjs",
20313
- "docs",
20314
- "README.md",
20315
- "LICENSE",
20316
- "CHANGELOG.md",
20317
- "THIRD_PARTY_NOTICES.md",
20318
- "LICENSES"
20319
- ],
20320
- repository: {
20321
- type: "git",
20322
- url: "git+https://github.com/STAIxBWLB/agent-hub.git"
20323
- },
20324
- homepage: "https://github.com/STAIxBWLB/agent-hub",
20325
- publishConfig: {
20326
- access: "public"
20327
- },
20328
- exports: {
20329
- "./adapter": "./src/adapters/public.ts",
20330
- "./conformance": "./src/adapters/conformance/index.ts"
20331
- }
20332
- };
20333
-
20334
- // src/version.ts
20335
- var VERSION = package_default.version;
20336
-
20337
- // src/hub/no-writes.ts
20338
- var OWNER_TOOL_EFFECTS = new Set(["hub_task_propose", "hub_task_decline", "hub_task_done", "hub_task_cancel", "hub_task_assign", "hub_task_escalate", "hub_review", "hub_checkpoint", "hub_remember", "hub_memory_correct", "hub_check_start", "hub_check_cancel", "hub_sdlc_record", "hub_plan_review", "hub_backlog_plan", "hub_backlog_pull", "hub_backlog_undo", "hub_backlog_split", "hub_backlog_return_untouched", "hub_release_new", "hub_release_configure", "hub_release_assign", "hub_release_cut"]);
20339
-
20340
- // src/hub/github-import.ts
20341
- init_sdlc();
20342
-
20343
- // src/hub/tasks.ts
20344
- init_planning();
20345
-
20346
- // src/hub/sdlc-queue.ts
20347
- init_test_integrity();
20348
- init_plan_conformance();
20349
- init_review_findings();
20350
- init_sdlc();
20351
-
20352
- // src/hub/tasks.ts
20353
- init_review_findings();
20354
- init_roles();
20520
+ // src/hub/memory-symbols.ts
20521
+ init_deny();
20522
+ init_permission_mode();
20355
20523
  init_path_policy();
20356
20524
 
20357
20525
  // node_modules/@staix/agent-mem/src/index.ts
@@ -20366,46 +20534,76 @@ init_shard();
20366
20534
  init_generation();
20367
20535
  init_capture();
20368
20536
  init_store();
20537
+ init_lifecycle();
20369
20538
  // node_modules/@staix/agent-mem/src/memory/capacity.ts
20370
20539
  init_home();
20371
- init_capture();
20540
+
20541
+ // node_modules/@staix/agent-mem/src/memory/metrics.ts
20372
20542
  var METRICS_MAX_BYTES = 64 * 1024;
20373
- // node_modules/@staix/agent-mem/src/memory/sync.ts
20374
- init_platform();
20375
- init_mutation();
20543
+ // node_modules/@staix/agent-mem/src/memory/capacity.ts
20544
+ init_lifecycle();
20545
+ init_capture();
20376
20546
 
20377
- // node_modules/@staix/agent-mem/src/memory/generation-sync.ts
20378
- init_bridge();
20379
- init_generation();
20380
- init_mutation();
20381
- init_platform();
20382
- init_prune();
20383
- var zero = "0".repeat(40);
20384
- // node_modules/@staix/agent-mem/src/memory/import.ts
20385
- init_platform();
20547
+ // node_modules/@staix/agent-mem/src/index.ts
20548
+ init_lifecycle();
20549
+
20550
+ // node_modules/@staix/agent-mem/src/memory/gc.ts
20551
+ init_capture();
20552
+ init_lifecycle();
20553
+ // node_modules/@staix/agent-mem/src/memory/curated-read.ts
20386
20554
  init_store();
20387
20555
  init_shard();
20388
20556
 
20389
- // node_modules/@staix/agent-mem/src/memory/work-lists.ts
20390
- init_generation();
20391
- init_mutation();
20557
+ // node_modules/@staix/agent-mem/src/memory/project-export.ts
20558
+ init_lifecycle();
20392
20559
  init_platform();
20560
+ init_policy();
20393
20561
  init_screen();
20394
20562
  init_shard();
20395
- init_store();
20396
- var forbidden = new Set(["__proto__", "constructor", "prototype"]);
20397
- // node_modules/@staix/agent-mem/src/memory/project.ts
20563
+ init_work_lists();
20564
+ // node_modules/@staix/agent-mem/src/memory/cross-project.ts
20565
+ init_search_fts();
20566
+ init_capture();
20567
+ init_lifecycle();
20568
+
20569
+ // node_modules/@staix/agent-mem/src/memory/vault.ts
20570
+ init_generation();
20571
+ init_policy();
20572
+ init_generation();
20573
+
20574
+ // node_modules/@staix/agent-mem/src/index.ts
20575
+ init_sync();
20576
+
20577
+ // node_modules/@staix/agent-mem/src/memory/import.ts
20398
20578
  init_platform();
20579
+ init_store();
20580
+ init_shard();
20581
+ init_work_lists();
20399
20582
 
20400
20583
  // node_modules/@staix/agent-mem/src/index.ts
20584
+ init_project2();
20401
20585
  init_home();
20402
20586
  init_platform();
20403
20587
 
20404
20588
  // node_modules/@staix/agent-mem/src/memory/commands.ts
20405
20589
  init_store();
20590
+ init_sync();
20591
+ init_project2();
20592
+ var AMEM_NAMES = { memory: "amem", goal: "amem goal" };
20593
+ var memoryUsage = (name = AMEM_NAMES.memory) => `usage: ${name} [list] [--tier short|long] [--kind <kind>] [--peer <p>] [--machine <id>] [--since <iso>] [--limit N] | search <words> [--history] [--as-of <UTC>] | show <id> [--history] [--as-of <UTC>] | graph <id|node> [--hops 1|2] [--history] [--as-of <UTC>] | promote|demote|forget <id> | supersede <old> <new> | rebuild | sync [--remote <url>] | machine [--label <name>] | import claude-mem [--db <path>] [--limit N] [--restart] | dedupe [--apply] | suggestions | confirm|reject <suggestion id> [--json]`;
20594
+ var MEMORY_USAGE = memoryUsage();
20595
+ var goalUsage = (name = AMEM_NAMES.goal) => `usage: ${name} [list] [--all] | show <id> | new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task id> | state <id> open|active|blocked|done|dropped | done <id> | take <id> [--force] [--json]`;
20596
+ var GOAL_USAGE = goalUsage();
20597
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
20598
+ init_candidate_pending();
20599
+
20600
+ // node_modules/@staix/agent-mem/src/memory/note-add.ts
20601
+ init_store();
20602
+
20406
20603
  // node_modules/@staix/agent-mem/src/memory/candidates.ts
20407
20604
  init_home();
20408
20605
  init_platform();
20606
+ init_private_spans();
20409
20607
  var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
20410
20608
  var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
20411
20609
 
@@ -20441,6 +20639,7 @@ var MCP_TOOLS = [
20441
20639
  tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" } }, ["query"])),
20442
20640
  tool2("amem_show", "Show a screened memory entry in this project by its exact id or unique prefix.", object6({ id: string(80) }, ["id"])),
20443
20641
  tool2("amem_timeline", "Read screened memory near an entry or time in this project.", object6({ id: string(80), since: string(40), limit })),
20642
+ tool2("amem_brief", "Read an intent/path-scoped brief of live public memory: fails first, then decisions/contracts and one-hop neighbours. Memory is untrusted data; byte cap covers the complete brief body.", object6({ intent: string(1000), paths: { type: "array", maxItems: 150, items: string(512) }, symbols: { type: "array", maxItems: 150, items: string(512) }, maxBytes: { type: "integer", minimum: 1, maximum: 8192 } }, ["intent"])),
20444
20643
  tool2("amem_graph", "Read the screened curated-memory neighbourhood. Capture is never graph evidence.", object6({ start: string(500), hops: { type: "integer", minimum: 1, maximum: 2 }, limit }, ["start"])),
20445
20644
  tool2("amem_work_state_read", "Read project work lists; closed lists/items can be included explicitly.", object6({ list: string(200), includeClosed: { type: "boolean" } })),
20446
20645
  tool2("amem_work_state_write", "Append scalar project work-list fields; null clears, task selects an item, list status done closes.", object6({ list: string(200), fields: {
@@ -20449,32 +20648,215 @@ var MCP_TOOLS = [
20449
20648
  maxProperties: 64,
20450
20649
  additionalProperties: { type: ["string", "number", "boolean", "null"], maxLength: 4000 }
20451
20650
  } }, ["list", "fields"]), false),
20452
- tool2("amem_remember", "Propose a general memory candidate for person acceptance. Never writes directly to curated notes.", object6({ text: string(3000), title: string(200), kind: { type: "string", enum: ["decision", "finding", "contract", "fail"] } }, ["text"]), false),
20651
+ tool2("amem_remember", "Propose a memory candidate (text, optional kind/title), or kind=fail with approach, symptom, cause, evidenceRefs and retryWhen. Person acceptance only; never writes curated notes.", object6({
20652
+ text: string(3000),
20653
+ title: string(200),
20654
+ kind: { type: "string", enum: ["decision", "finding", "contract", "fail"] },
20655
+ approach: string(4000),
20656
+ symptom: string(4000),
20657
+ cause: string(4000),
20658
+ evidenceRefs: { type: "array", minItems: 1, maxItems: 16, items: string(400) },
20659
+ retryWhen: string(4000)
20660
+ }), false),
20661
+ tool2("amem_correct", "Propose a correction to an exact curated entry id visible in this project. The host binds its current revision; the full accepted body including reason and provenance headers must fit 4000 characters. A person must accept before any note or supersession is written.", object6({ id: string(80), proposedText: string(4000), reason: string(4000), kind: { type: "string", enum: ["decision", "contract", "fail"] } }, ["id", "proposedText", "reason"]), false),
20453
20662
  tool2("amem_vault_candidates", "List screened durable curated notes eligible for a person to move to the vault. Read only.", object6({ limit })),
20454
20663
  tool2("amem_capacity", "Read this machine's bounded capture capacity proposal; applying it is person-only.", object6({}))
20455
20664
  ];
20665
+ // node_modules/@staix/agent-mem/src/memory/plan-guard.ts
20666
+ init_store();
20667
+ init_project2();
20668
+
20669
+ // node_modules/@staix/agent-mem/src/memory/intent-brief.ts
20670
+ init_policy();
20671
+
20672
+ // node_modules/@staix/agent-mem/src/memory/recall-staleness.ts
20673
+ init_platform();
20674
+ init_policy();
20675
+ init_project2();
20676
+
20456
20677
  // node_modules/@staix/agent-mem/src/mcp-memory.ts
20457
20678
  init_capture();
20458
20679
  init_generation();
20459
20680
  init_home();
20460
20681
  init_platform();
20682
+ init_policy();
20683
+ init_config2();
20684
+ init_project2();
20685
+ init_reader_lease();
20686
+ init_search_fts();
20687
+ init_shard();
20688
+ init_lifecycle();
20689
+ init_repo_scope();
20690
+ init_store();
20691
+ init_work_lists();
20461
20692
 
20462
- // node_modules/@staix/agent-mem/src/memory/policy.ts
20693
+ // node_modules/@staix/agent-mem/src/index.ts
20694
+ init_policy();
20695
+ init_work_lists();
20696
+
20697
+ // node_modules/@staix/agent-mem/src/hook-git.ts
20698
+ init_platform();
20699
+ init_platform();
20700
+
20701
+ // node_modules/@staix/agent-mem/src/hook-spool.ts
20702
+ init_capture_input();
20703
+ init_platform();
20704
+ init_policy();
20705
+ init_config2();
20706
+ init_policy();
20463
20707
  init_screen();
20708
+ init_lifecycle();
20709
+
20710
+ // node_modules/@staix/agent-mem/src/hook-contract.ts
20711
+ var HOOK_INPUT_BYTES = 64 * 1024;
20712
+
20713
+ // node_modules/@staix/agent-mem/src/hook-spool.ts
20714
+ var RECORD_BYTES = HOOK_INPUT_BYTES + 8192;
20715
+ var MAX_BYTES5 = 2 * 1024 * 1024;
20716
+ var DRAIN_BYTES = 256 * 1024;
20717
+ var CAPACITY_BYTES = 256 * 1024;
20718
+ var RETIRE_AFTER_MS = 7 * 24 * 3600000;
20719
+
20720
+ // node_modules/@staix/agent-mem/src/hook-project.ts
20721
+ init_platform();
20722
+ init_project2();
20464
20723
  init_platform();
20465
- var MAX_BYTES3 = 256 * 1024;
20466
- var MAX_STRING_BYTES = 16 * 1024;
20467
- var flags = new Set(["private", "pii", "privateturn", "isprivate", "ispii"]);
20468
20724
 
20469
- // node_modules/@staix/agent-mem/src/mcp-memory.ts
20470
- init_reader_lease();
20471
- init_search_fts();
20725
+ // node_modules/@staix/agent-mem/src/hook-native.ts
20726
+ var SUPERVISOR = [
20727
+ "marker=$1; seconds=$2; shift 2; leader=$$",
20728
+ 'owned() { identity=$(/bin/ps -ww -p "$leader" -o pgid=,command=) || return 1; case "$identity" in *"$marker"*) ;; *) return 1 ;; esac; set -f; set -- $identity; [ "$1" = "$leader" ]; }',
20729
+ 'reap() { if owned; then kill -KILL "-$leader"; fi; }',
20730
+ "[ -x /bin/sleep ] && [ -x /bin/ps ] && owned || exit 125",
20731
+ "trap reap EXIT HUP INT TERM",
20732
+ '( /bin/sleep "$seconds" & sleeper=$!; kill -0 "$sleeper" 2>/dev/null || { reap; exit 125; }; printf "\\n%s:ARMED\\n" "$marker"; wait "$sleeper"; reap ) & watchdog=$!',
20733
+ 'kill -0 "$watchdog" 2>/dev/null || { reap; exit 125; }',
20734
+ '"$@" </dev/null; code=$?; printf "\\n%s:%s\\n" "$marker" "$code"',
20735
+ "read -r ignored || :",
20736
+ 'wait "$watchdog"'
20737
+ ].join("; ");
20738
+
20739
+ // node_modules/@staix/agent-mem/src/hooks.ts
20740
+ init_aliases();
20741
+ init_home();
20742
+ init_platform();
20743
+ init_policy();
20744
+ init_config2();
20745
+ // node_modules/@staix/agent-mem/src/ingest.ts
20746
+ init_capture();
20747
+ init_home();
20748
+ init_platform();
20749
+ init_policy();
20750
+ init_config2();
20751
+ init_project2();
20472
20752
  init_shard();
20473
- init_store();
20753
+ var MAX_BYTES6 = 16 * 1024 * 1024;
20754
+ var DEFAULT_BYTES = 4 * 1024 * 1024;
20755
+ var MAX_CURSOR_BYTES = 128 * 1024;
20756
+ var utf8 = new TextDecoder("utf-8", { fatal: true });
20757
+ var MAX_RECORD = 256 * 1024;
20474
20758
 
20475
- // node_modules/@staix/agent-mem/src/memory/vault.ts
20759
+ // node_modules/@staix/agent-mem/src/index.ts
20760
+ init_session_brief();
20761
+
20762
+ // node_modules/@staix/agent-mem/src/memory/decay-review.ts
20476
20763
  init_generation();
20764
+ // node_modules/@staix/agent-mem/src/memory/goal-promotion.ts
20477
20765
  init_generation();
20766
+ init_lifecycle();
20767
+ init_platform();
20768
+ init_shard();
20769
+ // src/hub/tasks.ts
20770
+ init_task_code_symbols();
20771
+ init_test_integrity();
20772
+ init_plan_conformance();
20773
+
20774
+ // src/hub/sdlc-release.ts
20775
+ init_child_process();
20776
+ init_git();
20777
+
20778
+ // src/hub/tasks.ts
20779
+ init_spec_read();
20780
+ init_sdlc_metrics();
20781
+ // package.json
20782
+ var package_default = {
20783
+ name: "@staix/agent-hub",
20784
+ version: "0.16.0",
20785
+ description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
20786
+ license: "MIT",
20787
+ type: "module",
20788
+ bin: {
20789
+ ahub: "src/cli/main.js",
20790
+ "agent-hub": "src/cli/main.js"
20791
+ },
20792
+ scripts: {
20793
+ check: "scripts/check.sh",
20794
+ build: "bun scripts/build.mjs",
20795
+ test: "bun test"
20796
+ },
20797
+ engines: {
20798
+ bun: ">=1.4.2"
20799
+ },
20800
+ dependencies: {
20801
+ "@staix/agent-mem": "0.7.0"
20802
+ },
20803
+ devDependencies: {
20804
+ "@modelcontextprotocol/sdk": "^1.27.1",
20805
+ "@types/bun": "^1.3.0",
20806
+ typescript: "^5.8.0"
20807
+ },
20808
+ files: [
20809
+ ".claude-plugin",
20810
+ "src",
20811
+ "src/pet/window.jxa.js",
20812
+ "src/pet/window.html",
20813
+ "plugins",
20814
+ "templates",
20815
+ "scripts/check-docs.mjs",
20816
+ "docs",
20817
+ "README.md",
20818
+ "LICENSE",
20819
+ "CHANGELOG.md",
20820
+ "THIRD_PARTY_NOTICES.md",
20821
+ "LICENSES"
20822
+ ],
20823
+ repository: {
20824
+ type: "git",
20825
+ url: "git+https://github.com/STAIxBWLB/agent-hub.git"
20826
+ },
20827
+ homepage: "https://github.com/STAIxBWLB/agent-hub",
20828
+ publishConfig: {
20829
+ access: "public"
20830
+ },
20831
+ exports: {
20832
+ "./adapter": "./src/adapters/public.ts",
20833
+ "./conformance": "./src/adapters/conformance/index.ts"
20834
+ }
20835
+ };
20836
+
20837
+ // src/version.ts
20838
+ var VERSION = package_default.version;
20839
+
20840
+ // src/hub/no-writes.ts
20841
+ var OWNER_TOOL_EFFECTS = new Set(["hub_task_propose", "hub_task_decline", "hub_task_done", "hub_task_cancel", "hub_task_assign", "hub_task_escalate", "hub_review", "hub_checkpoint", "hub_remember", "hub_memory_correct", "hub_check_start", "hub_check_cancel", "hub_sdlc_record", "hub_plan_review", "hub_backlog_plan", "hub_backlog_pull", "hub_backlog_undo", "hub_backlog_split", "hub_backlog_return_untouched", "hub_release_new", "hub_release_configure", "hub_release_assign", "hub_release_cut"]);
20842
+
20843
+ // src/hub/github-import.ts
20844
+ init_sdlc();
20845
+
20846
+ // src/hub/tasks.ts
20847
+ init_planning();
20848
+
20849
+ // src/hub/sdlc-queue.ts
20850
+ init_test_integrity();
20851
+ init_plan_conformance();
20852
+ init_review_findings();
20853
+ init_sdlc();
20854
+
20855
+ // src/hub/tasks.ts
20856
+ init_review_findings();
20857
+ init_roles();
20858
+ init_path_policy();
20859
+
20478
20860
  // src/hub/memory-candidates.ts
20479
20861
  init_tool_refusal();
20480
20862
 
@@ -20528,11 +20910,12 @@ var CONTRADICTION_WINDOW_MS = 7 * 86400000;
20528
20910
  var QUIET_EVENTS = new Set(["answer", "reviewer changed", "idle sweep", "memory reference started"]);
20529
20911
  QUIET_EVENTS.add("ready held");
20530
20912
  QUIET_EVENTS.add("plan observed");
20913
+ QUIET_EVENTS.add("planner changed");
20531
20914
  var OWNERSHIP_EVENTS = new Set(["assigned", "escalated", "reassigned", "unassigned"]);
20915
+ var GROOM_FYI_WINDOW_MS = 10 * 60000;
20532
20916
 
20533
20917
  // src/hub/conductor.ts
20534
20918
  init_tool_refusal();
20535
-
20536
20919
  // src/hub/instances.ts
20537
20920
  import { types as types4 } from "util";
20538
20921
  init_project();