@staix/agent-hub 0.16.0 → 0.17.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 (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/adapters.md +18 -5
  4. package/docs/agent-notes/daemon.md +4 -3
  5. package/docs/agent-notes/tasks.md +2 -2
  6. package/docs/agent-notes/tests.md +5 -3
  7. package/docs/dogfood.md +6 -3
  8. package/docs/operations.md +45 -18
  9. package/docs/quickstart.md +6 -2
  10. package/docs/roles.md +1 -0
  11. package/docs/sdlc.md +11 -11
  12. package/docs/security.md +15 -5
  13. package/docs/smoke.md +51 -3
  14. package/docs/specs/2026-09-19-agent-hub-design.md +40 -4
  15. package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
  16. package/docs/verification/2026-10-25-0.17.0.md +74 -0
  17. package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
  18. package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
  19. package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
  20. package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
  21. package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
  22. package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -0
  23. package/docs/verified.json +20 -17
  24. package/package.json +1 -1
  25. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  26. package/plugins/agent-hub/server.js +1022 -160
  27. package/src/adapters/codex-review-isolation.ts +10 -1
  28. package/src/adapters/compat.ts +7 -2
  29. package/src/adapters/conformance/enrollment.ts +18 -4
  30. package/src/adapters/conformance/factories.ts +4 -1
  31. package/src/adapters/conformance/kit.ts +11 -0
  32. package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
  33. package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
  34. package/src/adapters/definitions/native-factories.ts +8 -0
  35. package/src/adapters/definitions/native-recovery.ts +8 -0
  36. package/src/adapters/definitions/qwen.ts +7 -4
  37. package/src/adapters/qwen-dual.ts +578 -0
  38. package/src/adapters/registry-types.ts +5 -1
  39. package/src/adapters/registry.ts +9 -1
  40. package/src/adapters/reviewer-mcp.ts +12 -6
  41. package/src/adapters/reviewer-print.ts +100 -17
  42. package/src/cli/upgrade.ts +4 -3
  43. package/src/hub/daemon.ts +134 -22
  44. package/src/hub/memory-plan-guard.ts +8 -33
  45. package/src/hub/tasks.ts +25 -12
  46. package/src/memory/brief.ts +45 -5
@@ -310,7 +310,7 @@ var init_compat = __esm(() => {
310
310
  },
311
311
  "kimi-review": {
312
312
  label: "Kimi reviewer (kimi-code)",
313
- verified: ["2.2.0", "2.3.0"],
313
+ verified: ["2.2.0", "2.3.0", "2.3.1"],
314
314
  gate: "isolation",
315
315
  probe: ["kimi", "--version"],
316
316
  parse: (out) => plain(out.trim()),
@@ -350,6 +350,15 @@ var init_compat = __esm(() => {
350
350
  config: "qwen_cmd",
351
351
  parse: (out) => plain(out.trim()),
352
352
  trustNote: "Native ACP identity and mode ids have dated receipts; 0.26.0 permission callbacks were checked without inference. Another version may change those contracts."
353
+ },
354
+ "qwen-dual": {
355
+ label: "Qwen Code dual-output TUI",
356
+ verified: ["0.26.1"],
357
+ gate: "contract",
358
+ probe: ["qwen", "--version"],
359
+ config: "qwen_cmd",
360
+ parse: (out) => plain(out.trim()),
361
+ trustNote: "The dual-output file contract (submit and confirmation_response input, user/assistant/stream_event/control events, session_start identity) was verified on 0.26.1 only. A hub deny ends the turn without any Qwen event, and no external stop of a running turn exists; another version may change either."
353
362
  }
354
363
  };
355
364
  MAX_BYTES = 64 * 1024;
@@ -792,6 +801,17 @@ function parseProcessTable(text) {
792
801
  return m && !m[4].startsWith("Z") ? [{ pid: Number(m[1]), ppid: Number(m[2]), pgid: Number(m[3]), started: m[5], command: m[6] }] : [];
793
802
  });
794
803
  }
804
+ function processTable() {
805
+ for (let attempt = 0;attempt < 2; attempt++) {
806
+ try {
807
+ const r = Bun.spawnSync(PS, { ...PS_OPTIONS, stdout: "pipe", stderr: "pipe", timeout: 1e4 });
808
+ const rows = r.exitCode === 0 ? own(r.stdout.toString()) : undefined;
809
+ if (rows)
810
+ return rows;
811
+ } catch {}
812
+ }
813
+ return;
814
+ }
795
815
  async function readProcessTable() {
796
816
  for (let attempt = 0;attempt < 2; attempt++) {
797
817
  try {
@@ -1332,7 +1352,15 @@ var DEFAULT_WATCHDOG_MS = 300000;
1332
1352
 
1333
1353
  // src/adapters/reviewer-print.ts
1334
1354
  import { randomUUID as randomUUID2 } from "crypto";
1335
- var ReviewerUnavailableError, reviewTarget = (env) => {
1355
+ function reviewerNoVerdictLine(peer, diagnostic, safeRefusal, withheld = false) {
1356
+ const prefix = `[${peer}] review of task #${diagnostic.task}: no verdict exit=${diagnostic.exitCode ?? "unknown"}; `;
1357
+ if (withheld)
1358
+ return prefix + "diagnostic withheld";
1359
+ const detail = diagnostic.lastTool ? `last hub tool ${diagnostic.lastTool}; ${diagnostic.reviewCalled ? "hub_review called" : "no hub_review call"}` : "no hub_review call";
1360
+ const line = safeRefusal === undefined ? undefined : Array.from(sanitize(safeRefusal).replace(/\s+/g, " ")).filter((char) => !/\p{C}/u.test(char)).slice(0, 240).join("");
1361
+ return prefix + detail + (diagnostic.refusal === undefined && diagnostic.hostRefusal === undefined ? "" : `; refusal: ${line ?? "withheld"}`);
1362
+ }
1363
+ var ReviewerUnavailableError, ReviewerAdmissionError, REVIEWER_FALLBACK_UNCONFIRMED = "reviewer fallback unconfirmed: ", reviewTarget = (env) => {
1336
1364
  const task = reviewTask(env);
1337
1365
  if (task === undefined || env.private)
1338
1366
  return;
@@ -1356,6 +1384,13 @@ var init_reviewer_print = __esm(() => {
1356
1384
  this.detail = detail;
1357
1385
  }
1358
1386
  };
1387
+ ReviewerAdmissionError = class ReviewerAdmissionError extends Error {
1388
+ reason;
1389
+ constructor(reason) {
1390
+ super("review admission changed");
1391
+ this.reason = reason;
1392
+ }
1393
+ };
1359
1394
  ReviewerPrintPeer = class ReviewerPrintPeer extends BasePeer {
1360
1395
  opts;
1361
1396
  hubNative = true;
@@ -1374,6 +1409,29 @@ var init_reviewer_print = __esm(() => {
1374
1409
  reviewing;
1375
1410
  reviewSeq = 0;
1376
1411
  runningBinding;
1412
+ activeTrace;
1413
+ recordToolTrace(task, trace) {
1414
+ if (trace.tool !== "hub_review" && trace.tool !== "hub_task_show")
1415
+ return;
1416
+ const active = this.activeTrace, binding = this.runningBinding;
1417
+ if (!active || active.task !== task || active.generation !== this.generation || this.reviewing !== task || binding?.task !== task || this.opts.bindingCurrent?.(task, binding.binding) === false)
1418
+ return;
1419
+ if (typeof trace.refusal === "string") {
1420
+ if (active.value.hostRefusal === undefined)
1421
+ active.value.hostRefusal = trace.refusal;
1422
+ else if (active.value.hostRefusal !== trace.refusal)
1423
+ active.value.refusalUncertain = true;
1424
+ }
1425
+ this.observeTool(active.value, trace);
1426
+ }
1427
+ observeTool(value, trace) {
1428
+ if (trace.tool !== "hub_review" && trace.tool !== "hub_task_show")
1429
+ return;
1430
+ value.lastTool = trace.tool;
1431
+ value.refusal = typeof trace.refusal === "string" ? trace.refusal : undefined;
1432
+ if (trace.tool === "hub_review")
1433
+ value.reviewCalled = true;
1434
+ }
1377
1435
  reviewBinding(task) {
1378
1436
  return this.reviewing === task && this.runningBinding?.task === task ? this.runningBinding.binding : undefined;
1379
1437
  }
@@ -1510,6 +1568,17 @@ var init_reviewer_print = __esm(() => {
1510
1568
  } catch (error) {
1511
1569
  if (generation !== this.generation)
1512
1570
  return;
1571
+ if (error instanceof ReviewerAdmissionError) {
1572
+ this.opts.log?.(`[${this.id}] review of task #${task} skipped: ${error.reason === "stale_binding" ? "stale binding" : "review policy changed"}`);
1573
+ if (error.reason === "policy_changed") {
1574
+ missing.push(task);
1575
+ const target = batch.targets[index];
1576
+ if (target)
1577
+ reroute.push({ ...target, cause: "no_verdict" });
1578
+ }
1579
+ batch.next = index + 1;
1580
+ continue;
1581
+ }
1513
1582
  if (error instanceof PrintChildCleanupError) {
1514
1583
  this.opts.log?.(`[${this.id}] review of task #${task}: ${error.message}`);
1515
1584
  this.settle("needs_review", `review process group still running: ${error.message}`);
@@ -1534,6 +1603,15 @@ var init_reviewer_print = __esm(() => {
1534
1603
  if (!recorded) {
1535
1604
  missing.push(task);
1536
1605
  const target = batch.targets[index];
1606
+ const diagnostic = { ...target, task, exitCode: result.code, ...result.trace };
1607
+ try {
1608
+ if (this.opts.onNoVerdict)
1609
+ this.opts.onNoVerdict(diagnostic, () => generation === this.generation);
1610
+ else
1611
+ this.opts.log?.(reviewerNoVerdictLine(this.id, diagnostic));
1612
+ } catch {
1613
+ this.opts.log?.(reviewerNoVerdictLine(this.id, { task, exitCode: result.code, reviewCalled: false }, undefined, true));
1614
+ }
1537
1615
  if (target)
1538
1616
  reroute.push({ ...target, cause: result.quota ? "quota" : "no_verdict" });
1539
1617
  if (result.quota) {
@@ -1543,14 +1621,18 @@ var init_reviewer_print = __esm(() => {
1543
1621
  }
1544
1622
  }
1545
1623
  batch.next = index + 1;
1546
- if (result.code !== 0)
1624
+ if (recorded && result.code !== 0)
1547
1625
  this.opts.log?.(`[${this.id}] review of task #${task} exited ${result.code}`);
1548
1626
  }
1549
1627
  if (generation !== this.generation)
1550
1628
  return;
1551
1629
  this.clearWatchdog();
1552
- this.settle(failure?.state ?? (missing.length ? "needs_review" : "completed"), failure?.reason ?? (missing.length ? `no verdict recorded for task ${missing.map((id) => `#${id}`).join(", ")}` : undefined));
1553
- await this.reportFailures(reroute, generation);
1630
+ const reoffered = await this.reportFailures(reroute, generation);
1631
+ if (generation !== this.generation)
1632
+ return;
1633
+ const unresolved = missing.length > 0 && !reoffered;
1634
+ const normalNoVerdict = !failure && reroute.length > 0 && reroute.every((value) => value.cause === "no_verdict");
1635
+ this.settle(failure?.state ?? (unresolved ? "needs_review" : "completed"), failure?.reason ?? (unresolved ? `${normalNoVerdict ? REVIEWER_FALLBACK_UNCONFIRMED : ""}no verdict recorded for task ${missing.map((id) => `#${id}`).join(", ")}` : undefined));
1554
1636
  if (generation === this.generation) {
1555
1637
  this.batch = undefined;
1556
1638
  this.setState("idle");
@@ -1558,19 +1640,23 @@ var init_reviewer_print = __esm(() => {
1558
1640
  }
1559
1641
  async reportFailures(failures, generation) {
1560
1642
  const seen = new Set;
1643
+ let reoffered = true;
1561
1644
  for (const failed of failures) {
1562
1645
  if (generation !== this.generation)
1563
- return;
1646
+ return false;
1564
1647
  const identity = JSON.stringify([failed.task, failed.head, failed.flowRevision]);
1565
1648
  if (seen.has(identity))
1566
1649
  continue;
1567
1650
  seen.add(identity);
1568
1651
  try {
1569
- await this.opts.onUnavailable?.(failed, () => generation === this.generation);
1652
+ if (await this.opts.onUnavailable?.(failed, () => generation === this.generation) !== true)
1653
+ reoffered = false;
1570
1654
  } catch {
1655
+ reoffered = false;
1571
1656
  this.opts.log?.(`[${this.id}] reviewer fallback could not be recorded; inspect the task`);
1572
1657
  }
1573
1658
  }
1659
+ return reoffered;
1574
1660
  }
1575
1661
  prepare(sessionId, input, task, mcpConfig) {
1576
1662
  const abort = new AbortController;
@@ -1607,9 +1693,16 @@ var init_reviewer_print = __esm(() => {
1607
1693
  }).catch(() => {});
1608
1694
  return done;
1609
1695
  }
1610
- async review(env, task, generation) {
1696
+ assertAdmission(env, task, binding) {
1697
+ if (this.opts.bindingCurrent?.(task, binding) === false)
1698
+ throw new ReviewerAdmissionError("stale_binding");
1611
1699
  if (this.opts.admit?.(env) === false)
1612
- throw new Error("review admission changed");
1700
+ throw new ReviewerAdmissionError(this.opts.bindingCurrent?.(task, binding) === false ? "stale_binding" : "policy_changed");
1701
+ }
1702
+ async review(env, task, generation) {
1703
+ const revision = Number(env.refs?.reviewRevision);
1704
+ const binding = Object.freeze({ head: env.refs?.commit, flowRevision: Number.isSafeInteger(revision) && revision > 0 ? revision : undefined });
1705
+ this.assertAdmission(env, task, binding);
1613
1706
  let facts;
1614
1707
  try {
1615
1708
  facts = this.opts.facts?.(env);
@@ -1620,8 +1713,8 @@ var init_reviewer_print = __esm(() => {
1620
1713
 
1621
1714
  ${facts}` : ""}`;
1622
1715
  let launch;
1623
- const revision = Number(env.refs?.reviewRevision);
1624
- const binding = Object.freeze({ head: env.refs?.commit, flowRevision: Number.isSafeInteger(revision) && revision > 0 ? revision : undefined });
1716
+ const trace = { reviewCalled: false };
1717
+ this.activeTrace = { task, generation, value: trace };
1625
1718
  const mcp = this.opts.backend.taskMcp ? this.opts.taskMcp?.(task, () => generation === this.generation && this.reviewToken(task) !== undefined && this.opts.bindingCurrent?.(task, binding) !== false) : undefined;
1626
1719
  let mcpClosed = false;
1627
1720
  const closeMcp = () => {
@@ -1638,8 +1731,11 @@ ${facts}` : ""}`;
1638
1731
  const release = () => releasing ??= Promise.resolve().then(() => launch?.cleanup?.()).then(() => {});
1639
1732
  try {
1640
1733
  launch = await this.prepare(randomUUID2(), input, task, mcp?.config);
1641
- if (generation !== this.generation || this.opts.admit?.(env) === false || launch.current?.() === false)
1642
- throw new ReviewerUnavailableError("reviewer isolation or admission changed before child spawn", launch.current?.() === false ? "binary" : "unknown");
1734
+ if (generation !== this.generation)
1735
+ throw new ReviewerAdmissionError("stale_binding");
1736
+ this.assertAdmission(env, task, binding);
1737
+ if (launch.current?.() === false)
1738
+ throw new ReviewerUnavailableError("reviewer isolation changed before child spawn", "binary");
1643
1739
  this.receipt = launch.isolation;
1644
1740
  let quota = false;
1645
1741
  const child = new OwnedPrintChild({
@@ -1666,6 +1762,11 @@ ${facts}` : ""}`;
1666
1762
  }, 100);
1667
1763
  },
1668
1764
  onFrame: (event) => {
1765
+ try {
1766
+ const observed = this.opts.backend.toolTrace?.(event);
1767
+ if (observed && generation === this.generation)
1768
+ this.observeTool(trace, observed);
1769
+ } catch {}
1669
1770
  const usage = this.opts.backend.usage?.(event);
1670
1771
  if (usage)
1671
1772
  this.opts.onUsage?.(usage, task);
@@ -1687,8 +1788,10 @@ ${facts}` : ""}`;
1687
1788
  const code = await child.done;
1688
1789
  if (this.child === child)
1689
1790
  this.child = undefined;
1690
- return { code, quota };
1791
+ return { code, quota, trace };
1691
1792
  } finally {
1793
+ if (this.activeTrace?.value === trace)
1794
+ this.activeTrace = undefined;
1692
1795
  closeMcp();
1693
1796
  if (policyTimer)
1694
1797
  clearInterval(policyTimer);
@@ -1988,7 +2091,15 @@ function ownServer(text) {
1988
2091
  const env = hub.env === undefined ? {} : object2(hub.env);
1989
2092
  if (Object.values(env).some((value) => typeof value !== "string"))
1990
2093
  throw new Error("invalid hub environment");
1991
- return { command: hub.command, args: hub.args ?? [], env, enabled: true, required: true, enabled_tools: TOOLS };
2094
+ return {
2095
+ command: hub.command,
2096
+ args: hub.args ?? [],
2097
+ env,
2098
+ enabled: true,
2099
+ required: true,
2100
+ enabled_tools: TOOLS,
2101
+ tools: Object.fromEntries(TOOLS.map((name) => [name, { approval_mode: "approve" }]))
2102
+ };
1992
2103
  }
1993
2104
  async function prepare(ctx) {
1994
2105
  let root;
@@ -2234,6 +2345,16 @@ var init_codex_review_isolation = __esm(() => {
2234
2345
  codexReviewerBackend = {
2235
2346
  id: "codex",
2236
2347
  prepare,
2348
+ toolTrace(event) {
2349
+ const row = event;
2350
+ const item = row?.item;
2351
+ if (row?.type !== "item.completed" || item?.type !== "mcp_tool_call" || item.server !== "agent-hub" || item.tool !== "hub_review" && item.tool !== "hub_task_show")
2352
+ return;
2353
+ const failure = typeof item.error === "string" ? item.error : item.error?.message;
2354
+ const refused = item.result?.isError === true && Array.isArray(item.result.content) ? item.result.content.filter((value) => value?.type === "text" && typeof value.text === "string").map((value) => value.text).join(`
2355
+ `) : undefined;
2356
+ return { tool: item.tool, ...typeof failure === "string" ? { refusal: failure } : typeof refused === "string" ? { refusal: refused } : {} };
2357
+ },
2237
2358
  usage(event) {
2238
2359
  const row = event;
2239
2360
  return row?.type === "turn.completed" && row.usage ? { input_tokens: row.usage.input_tokens, output_tokens: row.usage.output_tokens, cache_read_input_tokens: row.usage.cached_input_tokens } : undefined;
@@ -4670,12 +4791,696 @@ var init_jcode = __esm(() => {
4670
4791
  };
4671
4792
  });
4672
4793
 
4673
- // src/pi/launch.ts
4794
+ // src/pi/process-signature.ts
4795
+ var init_process_signature = __esm(() => {
4796
+ init_platform();
4797
+ });
4798
+
4799
+ // src/adapters/qwen-dual.ts
4800
+ import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync4, lstatSync as lstatSync7, mkdirSync as mkdirSync5, openSync as openSync4, readSync as readSync3, realpathSync as realpathSync2, renameSync as renameSync3, writeFileSync as writeFileSync5, writeSync } from "fs";
4674
4801
  import { join as join8 } from "path";
4802
+ function qwenDualNamesStateDir(input, stateDir) {
4803
+ const needles = new Set([".agenthub/state", ".agenthub\\state", "qwen-dual", "input.jsonl", stateDir]);
4804
+ try {
4805
+ needles.add(realpathSync2(stateDir));
4806
+ } catch {}
4807
+ const seen = (value, depth) => {
4808
+ if (depth > 8)
4809
+ return true;
4810
+ if (typeof value === "string") {
4811
+ const flat = value.replaceAll("\\", "/");
4812
+ return [...needles].some((n) => n && (value.includes(n) || flat.includes(n.replaceAll("\\", "/"))));
4813
+ }
4814
+ if (Array.isArray(value))
4815
+ return value.some((item) => seen(item, depth + 1));
4816
+ if (value && typeof value === "object")
4817
+ return Object.entries(value).some(([key, item]) => seen(key, depth + 1) || seen(item, depth + 1));
4818
+ return false;
4819
+ };
4820
+ return seen(input, 0);
4821
+ }
4822
+ var SUBMIT_CAP, READ_CAP, ANSWERED_CAP = 256, QWEN_STALLED_NOTE = "stalled: Qwen has said nothing for a while; the hub cannot stop a Qwen turn, press Esc in the Qwen terminal", QWEN_DETACHED_NOTE = "detached: the hub cannot read Qwen's event file; Qwen keeps working in its terminal and the hub reattaches when the file is readable again", DIR = "qwen-dual", QwenDualPeer;
4823
+ var init_qwen_dual = __esm(() => {
4824
+ init_envelope();
4825
+ init_child_process();
4826
+ init_process_signature();
4827
+ init_compat();
4828
+ init_qwen_native();
4829
+ SUBMIT_CAP = 256 * 1024;
4830
+ READ_CAP = 1024 * 1024;
4831
+ QwenDualPeer = class QwenDualPeer extends BasePeer {
4832
+ opts;
4833
+ permissionModeState = "unmanaged";
4834
+ dir;
4835
+ inputFile;
4836
+ eventFile;
4837
+ stateFile;
4838
+ mcpFile;
4839
+ signature;
4840
+ poller;
4841
+ monitor;
4842
+ state_ = { v: 1, offset: 0, generation: 0 };
4843
+ resume = false;
4844
+ _launch;
4845
+ stopping = true;
4846
+ requestedStopReason;
4847
+ detached = false;
4848
+ sessionSeen = false;
4849
+ primed = false;
4850
+ turn;
4851
+ owner;
4852
+ lastAssistant;
4853
+ pending = new Map;
4854
+ answered = new Set;
4855
+ stalled_ = false;
4856
+ exited = false;
4857
+ pollFailures = 0;
4858
+ discarding = false;
4859
+ constructor(id, opts) {
4860
+ super(id, opts.watchdogMs);
4861
+ this.opts = opts;
4862
+ this.dir = join8(opts.stateDir, DIR);
4863
+ this.inputFile = join8(this.dir, "input.jsonl");
4864
+ this.eventFile = join8(this.dir, "events.jsonl");
4865
+ this.stateFile = join8(this.dir, "state.json");
4866
+ this.mcpFile = join8(this.dir, "mcp.json");
4867
+ this.signature = opts.signature ?? processSignature;
4868
+ }
4869
+ get launch() {
4870
+ return this._launch;
4871
+ }
4872
+ get adopted() {
4873
+ return this.adopting;
4874
+ }
4875
+ adopting = false;
4876
+ get stalled() {
4877
+ return this.stalled_;
4878
+ }
4879
+ get statusNotice() {
4880
+ if (this.detached)
4881
+ return { code: "qwen-detached", message: QWEN_DETACHED_NOTE };
4882
+ return this.stalled_ ? { code: "qwen-stalled", message: QWEN_STALLED_NOTE } : undefined;
4883
+ }
4884
+ get isDetached() {
4885
+ return this.detached;
4886
+ }
4887
+ holdsTerminal() {
4888
+ if (this.stopping || this.exited)
4889
+ return false;
4890
+ if (this.state_.ownerPid !== undefined)
4891
+ return this.ownerAlive();
4892
+ return this.sessionSeen;
4893
+ }
4894
+ get sessionId() {
4895
+ return this.state_.sessionId;
4896
+ }
4897
+ get offset() {
4898
+ return this.state_.offset;
4899
+ }
4900
+ recoveryMetadata() {
4901
+ return { ...this.state_.sessionId ? { sessionId: this.state_.sessionId } : {}, launch: { mode: "tui" } };
4902
+ }
4903
+ ensureFile(path, create) {
4904
+ const found = lstatSync7(path, { throwIfNoEntry: false });
4905
+ if (!found) {
4906
+ if (!create)
4907
+ throw new Error("Qwen dual-output file is missing");
4908
+ writeFileSync5(path, "", { mode: 384, flag: "wx" });
4909
+ return;
4910
+ }
4911
+ if (!found.isFile() || found.isSymbolicLink())
4912
+ throw new Error("Qwen dual-output path is not a plain file");
4913
+ if (typeof process.getuid === "function" && found.uid !== process.getuid())
4914
+ throw new Error("Qwen dual-output file is not owned by the person running the hub");
4915
+ if ((found.mode & 63) !== 0)
4916
+ throw new Error("Qwen dual-output file is readable or writable by others");
4917
+ }
4918
+ loadState() {
4919
+ let fd;
4920
+ try {
4921
+ fd = openSync4(this.stateFile, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
4922
+ const info = fstatSync4(fd);
4923
+ if (!info.isFile() || info.size > 4096)
4924
+ return;
4925
+ const buffer = Buffer.alloc(info.size);
4926
+ readSync3(fd, buffer, 0, info.size, 0);
4927
+ const raw = JSON.parse(buffer.toString("utf8"));
4928
+ if (raw.v !== 1 || !Number.isSafeInteger(raw.offset) || raw.offset < 0 || !Number.isSafeInteger(raw.generation))
4929
+ return;
4930
+ this.state_ = {
4931
+ v: 1,
4932
+ offset: raw.offset,
4933
+ generation: raw.generation,
4934
+ ...qwenSessionId(raw.sessionId) ? { sessionId: raw.sessionId } : {},
4935
+ ...Number.isSafeInteger(raw.ownerPid) && typeof raw.ownerSignature === "string" ? { ownerPid: raw.ownerPid, ownerSignature: raw.ownerSignature } : {}
4936
+ };
4937
+ } catch {} finally {
4938
+ if (fd !== undefined)
4939
+ try {
4940
+ closeSync4(fd);
4941
+ } catch {}
4942
+ }
4943
+ }
4944
+ saveState() {
4945
+ const temp = `${this.stateFile}.${process.pid}.tmp`;
4946
+ try {
4947
+ writeFileSync5(temp, `${JSON.stringify(this.state_)}
4948
+ `, { mode: 384 });
4949
+ renameSync3(temp, this.stateFile);
4950
+ } catch {
4951
+ this.opts.log?.(`[${this.id}] could not record its Qwen read position`);
4952
+ }
4953
+ }
4954
+ ownerAlive() {
4955
+ const { ownerPid, ownerSignature } = this.state_;
4956
+ if (ownerPid === undefined || ownerSignature === undefined)
4957
+ return false;
4958
+ return this.signature(ownerPid) === ownerSignature;
4959
+ }
4960
+ async start() {
4961
+ mkdirSync5(this.dir, { recursive: true, mode: 448 });
4962
+ const dir = lstatSync7(this.dir);
4963
+ if (!dir.isDirectory() || dir.isSymbolicLink() || typeof process.getuid === "function" && dir.uid !== process.getuid() || (dir.mode & 63) !== 0)
4964
+ throw new Error("Qwen dual-output directory is not private to the person running the hub");
4965
+ this.loadState();
4966
+ for (const path of [this.inputFile, this.eventFile])
4967
+ this.ensureFile(path, true);
4968
+ this.stopping = false;
4969
+ this.exited = false;
4970
+ if (this.ownerAlive()) {
4971
+ this.adopting = true;
4972
+ const probe = openSync4(this.eventFile, constants3.O_RDONLY | constants3.O_NOFOLLOW);
4973
+ let size = 0;
4974
+ try {
4975
+ size = fstatSync4(probe).size;
4976
+ } finally {
4977
+ closeSync4(probe);
4978
+ }
4979
+ if (size < this.state_.offset) {
4980
+ this.state_.offset = 0;
4981
+ this.state_.generation++;
4982
+ }
4983
+ this.sessionSeen = this.state_.sessionId !== undefined;
4984
+ this.primed = true;
4985
+ this.saveState();
4986
+ } else {
4987
+ this.adopting = false;
4988
+ this.resume = this.opts.sessionId !== undefined || this.state_.sessionId !== undefined;
4989
+ const resumeId = this.opts.sessionId ?? this.state_.sessionId;
4990
+ const previous = lstatSync7(this.eventFile);
4991
+ if (previous.size > 0) {
4992
+ try {
4993
+ renameSync3(this.eventFile, join8(this.dir, "events.prev.jsonl"));
4994
+ } catch {}
4995
+ this.ensureFile(this.eventFile, true);
4996
+ }
4997
+ closeSync4(openSync4(this.inputFile, constants3.O_WRONLY | constants3.O_NOFOLLOW | constants3.O_TRUNC));
4998
+ this.state_ = { v: 1, offset: 0, generation: this.state_.generation + 1, ...resumeId ? { sessionId: resumeId } : {} };
4999
+ this.sessionSeen = false;
5000
+ this.saveState();
5001
+ this.writeLaunch(resumeId);
5002
+ }
5003
+ this.poller = setInterval(() => this.poll(), this.opts.pollMs ?? 100);
5004
+ this.poller.unref?.();
5005
+ if (!this.monitor) {
5006
+ this.monitor = setInterval(() => this.checkOwner(), this.opts.ownerMs ?? 1000);
5007
+ this.monitor.unref?.();
5008
+ }
5009
+ this.poll();
5010
+ if (this.adopting && !this.detached && this.sessionSeen)
5011
+ this.setState("idle");
5012
+ }
5013
+ writeLaunch(resumeId) {
5014
+ const [bin, ...rest] = this.opts.cmd.filter((word) => word !== "--acp");
5015
+ if (!bin)
5016
+ throw new Error("qwen_cmd must name an executable");
5017
+ const servers = Object.fromEntries(this.opts.mcpServers.map((server) => [server.name, { command: server.command, args: server.args, env: server.env }]));
5018
+ writeFileSync5(this.mcpFile, JSON.stringify({ mcpServers: servers }), { mode: 384 });
5019
+ const args = [
5020
+ ...rest,
5021
+ "--input-file",
5022
+ this.inputFile,
5023
+ "--json-file",
5024
+ this.eventFile,
5025
+ "--mcp-config",
5026
+ this.mcpFile,
5027
+ "--approval-mode",
5028
+ "default",
5029
+ ...this.opts.model ? ["--model", this.opts.model] : [],
5030
+ ...resumeId ? ["-r", resumeId] : []
5031
+ ];
5032
+ this._launch = { cmd: bin, args, env: peerChildEnv(this.id, {}) };
5033
+ }
5034
+ discoverOwner() {
5035
+ if (this.stopping || this.state_.ownerPid !== undefined)
5036
+ return;
5037
+ const rows = (this.opts.table ?? processTable)();
5038
+ if (!rows)
5039
+ return;
5040
+ const flag = `--input-file ${this.inputFile}`;
5041
+ const matches = rows.filter((row) => row.pid !== process.pid && row.command.includes(flag));
5042
+ const pids = new Set(matches.map((row) => row.pid));
5043
+ const roots = matches.filter((row) => !pids.has(row.ppid));
5044
+ if (roots.length !== 1)
5045
+ return;
5046
+ const pid = roots[0].pid, signature = this.signature(pid);
5047
+ if (!signature)
5048
+ return;
5049
+ this.state_.ownerPid = pid;
5050
+ this.state_.ownerSignature = signature;
5051
+ this.saveState();
5052
+ }
5053
+ checkOwner() {
5054
+ if (this.stopping || this.exited)
5055
+ return;
5056
+ if (this.state_.ownerPid === undefined) {
5057
+ if (this.sessionSeen)
5058
+ this.discoverOwner();
5059
+ return;
5060
+ }
5061
+ if (this.ownerAlive())
5062
+ return;
5063
+ if (this.state === "offline" && !this.sessionSeen)
5064
+ return;
5065
+ this.exited = true;
5066
+ this.opts.log?.(`[${this.id}] the Qwen TUI exited`);
5067
+ this.failTurn("the Qwen terminal ended before the turn settled; inspect it before retrying");
5068
+ this.abortApprovals();
5069
+ delete this.state_.ownerPid;
5070
+ delete this.state_.ownerSignature;
5071
+ this.saveState();
5072
+ this.setState("offline");
5073
+ this.opts.onExit?.();
5074
+ }
5075
+ appendInput(record) {
5076
+ const line = `${JSON.stringify(record)}
5077
+ `;
5078
+ if (Buffer.byteLength(line) > SUBMIT_CAP)
5079
+ throw new Error("Qwen submit exceeds its size bound");
5080
+ const fd = openSync4(this.inputFile, constants3.O_WRONLY | constants3.O_APPEND | constants3.O_NOFOLLOW);
5081
+ try {
5082
+ const info = fstatSync4(fd);
5083
+ if (!info.isFile() || (info.mode & 63) !== 0 || typeof process.getuid === "function" && info.uid !== process.getuid())
5084
+ throw new Error("Qwen input file is not private");
5085
+ writeSync(fd, line);
5086
+ } finally {
5087
+ closeSync4(fd);
5088
+ }
5089
+ }
5090
+ async deliver(envs, deliveryId) {
5091
+ if (this.state !== "idle" || this.stopping || this.detached) {
5092
+ if (deliveryId)
5093
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is ${this.state}` });
5094
+ throw new Error(`${this.id} is ${this.state}`);
5095
+ }
5096
+ let text = this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}
5097
+
5098
+ ${renderDigest(envs, false)}`;
5099
+ if (/^\s*[\/!@?]/.test(text))
5100
+ text = `[agent-hub]
5101
+ ${text}`;
5102
+ const turn = { envs, ...deliveryId ? { deliveryId } : {}, text, started: false, accepted: false };
5103
+ this.turn = turn;
5104
+ this.owner = undefined;
5105
+ this.lastAssistant = undefined;
5106
+ this.setState("busy");
5107
+ try {
5108
+ this.appendInput({ type: "submit", text });
5109
+ } catch (error) {
5110
+ this.turn = undefined;
5111
+ if (deliveryId)
5112
+ this.delivery({ id: deliveryId, state: "failed_safe", reason: "Qwen input file could not be written" });
5113
+ this.setState("idle");
5114
+ throw error instanceof Error ? error : new Error("Qwen input file could not be written");
5115
+ }
5116
+ this.primed = true;
5117
+ }
5118
+ onWatchdog() {
5119
+ if (this.state !== "busy" || this.stalled_)
5120
+ return;
5121
+ if (this.pending.size > 0) {
5122
+ this.touch();
5123
+ return;
5124
+ }
5125
+ this.stalled_ = true;
5126
+ this.opts.log?.(`[${this.id}] turn stalled; the hub cannot stop it (press Esc in the Qwen terminal)`);
5127
+ this.opts.onStalled?.(true);
5128
+ try {
5129
+ this.opts.onNotice?.(`${this.id}: ${QWEN_STALLED_NOTE}`);
5130
+ } catch {}
5131
+ }
5132
+ alive() {
5133
+ if (this.stalled_) {
5134
+ this.stalled_ = false;
5135
+ this.opts.onStalled?.(false);
5136
+ }
5137
+ if (this.state === "busy")
5138
+ this.touch();
5139
+ }
5140
+ detach(reason) {
5141
+ if (this.detached)
5142
+ return;
5143
+ this.detached = true;
5144
+ this.opts.log?.(`[${this.id}] event file ${reason}: detached`);
5145
+ this.failTurn("the Qwen event file became unreadable before the turn settled; inspect the Qwen terminal before retrying");
5146
+ this.abortApprovals();
5147
+ if (this.state !== "offline")
5148
+ this.setState("offline");
5149
+ }
5150
+ poll() {
5151
+ if (this.stopping)
5152
+ return;
5153
+ let fd;
5154
+ try {
5155
+ fd = openSync4(this.eventFile, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
5156
+ const info = fstatSync4(fd);
5157
+ if (!info.isFile())
5158
+ throw new Error("not a regular file");
5159
+ if (this.detached) {
5160
+ this.detached = false;
5161
+ this.pollFailures = 0;
5162
+ this.opts.log?.(`[${this.id}] event file readable again: reattached`);
5163
+ if (this.sessionSeen && this.state === "offline" && !this.exited)
5164
+ this.setState("idle");
5165
+ }
5166
+ let offset = this.state_.offset;
5167
+ if (info.size < offset) {
5168
+ offset = 0;
5169
+ this.discarding = false;
5170
+ this.state_.generation++;
5171
+ this.opts.log?.(`[${this.id}] event file was replaced; reading from its start`);
5172
+ }
5173
+ while (offset < info.size) {
5174
+ const want = Math.min(info.size - offset, READ_CAP), buffer = Buffer.alloc(want);
5175
+ const got = readSync3(fd, buffer, 0, want, offset);
5176
+ if (got <= 0)
5177
+ break;
5178
+ if (this.discarding) {
5179
+ const newline = buffer.subarray(0, got).indexOf(10);
5180
+ offset += newline < 0 ? got : newline + 1;
5181
+ this.state_.offset = offset;
5182
+ if (newline >= 0)
5183
+ this.discarding = false;
5184
+ continue;
5185
+ }
5186
+ const end = buffer.subarray(0, got).lastIndexOf(10) + 1;
5187
+ if (end === 0) {
5188
+ if (got < READ_CAP)
5189
+ break;
5190
+ this.opts.log?.(`[${this.id}] dropped an event line over ${READ_CAP} bytes`);
5191
+ this.discarding = true;
5192
+ offset += got;
5193
+ this.state_.offset = offset;
5194
+ continue;
5195
+ }
5196
+ offset += end;
5197
+ this.state_.offset = offset;
5198
+ for (const line of buffer.subarray(0, end).toString("utf8").split(`
5199
+ `))
5200
+ if (line.trim())
5201
+ this.parse(line);
5202
+ if (this.stopping)
5203
+ break;
5204
+ }
5205
+ this.saveState();
5206
+ } catch (error) {
5207
+ this.pollFailures++;
5208
+ if (!this.detached)
5209
+ this.detach(`could not be read (${error.code ?? "error"})`);
5210
+ } finally {
5211
+ if (fd !== undefined)
5212
+ try {
5213
+ closeSync4(fd);
5214
+ } catch {}
5215
+ }
5216
+ }
5217
+ parse(line) {
5218
+ let event;
5219
+ try {
5220
+ event = JSON.parse(line);
5221
+ } catch {
5222
+ this.opts.log?.(`[${this.id}] skipped an unreadable event line`);
5223
+ return;
5224
+ }
5225
+ if (!event || typeof event !== "object" || typeof event.type !== "string")
5226
+ return;
5227
+ try {
5228
+ this.handle(event);
5229
+ } catch (error) {
5230
+ this.opts.log?.(`[${this.id}] event handling failed: ${sanitize(error.message)}`);
5231
+ }
5232
+ }
5233
+ handle(event) {
5234
+ this.alive();
5235
+ switch (event.type) {
5236
+ case "system":
5237
+ return this.onSystem(event);
5238
+ case "user":
5239
+ return this.onUser(event);
5240
+ case "assistant":
5241
+ return this.onAssistant(event);
5242
+ case "stream_event":
5243
+ if (event.event?.type === "message_stop")
5244
+ this.onMessageStop();
5245
+ return;
5246
+ case "result":
5247
+ return this.onResult(event);
5248
+ case "control_request":
5249
+ return this.onControlRequest(event);
5250
+ case "control_response":
5251
+ return this.onControlResponse(event);
5252
+ default:
5253
+ return;
5254
+ }
5255
+ }
5256
+ onSystem(event) {
5257
+ if (event.subtype !== "session_start")
5258
+ return;
5259
+ const data = event.data ?? {};
5260
+ const id = event.session_id ?? data.session_id;
5261
+ if (!qwenSessionId(id))
5262
+ return;
5263
+ const verdict = assessVersion("qwen-dual", data.version);
5264
+ const supported = data.supported_events;
5265
+ if (!verdict.ok || data.protocol_version !== 2 || !Array.isArray(supported) || !["user", "assistant", "stream_event", "control_request", "control_response"].every((name) => supported.includes(name))) {
5266
+ this.opts.log?.(`[${this.id}] the Qwen TUI's dual-output contract is not verified; not attached`);
5267
+ try {
5268
+ this.opts.onNotice?.(`${this.id}: this Qwen's dual-output contract is not verified (${verdict.ok ? "protocol or event set" : verdict.reason}); the hub did not attach it`);
5269
+ } catch {}
5270
+ return;
5271
+ }
5272
+ this.state_.sessionId = id;
5273
+ this.sessionSeen = true;
5274
+ this.saveState();
5275
+ this.discoverOwner();
5276
+ if (!this.stopping && !this.detached && this.state === "offline")
5277
+ this.setState("idle");
5278
+ }
5279
+ promptText(event) {
5280
+ const content = event.message?.content;
5281
+ if (typeof content === "string")
5282
+ return content;
5283
+ if (!Array.isArray(content))
5284
+ return;
5285
+ const blocks = content.filter((block) => block?.type === "text" && typeof block.text === "string");
5286
+ return blocks.length ? blocks.map((block) => block.text).join("") : undefined;
5287
+ }
5288
+ onUser(event) {
5289
+ const text = this.promptText(event);
5290
+ if (text === undefined)
5291
+ return;
5292
+ const turn = this.turn;
5293
+ if (turn && !turn.started && (text.trim() === turn.text.trim() || text.trim().startsWith(turn.text.trim().slice(0, 200)))) {
5294
+ turn.started = true;
5295
+ this.owner = "hub";
5296
+ this.lastAssistant = undefined;
5297
+ if (turn.deliveryId && !turn.accepted) {
5298
+ turn.accepted = true;
5299
+ this.delivery({ id: turn.deliveryId, state: "accepted" });
5300
+ }
5301
+ return;
5302
+ }
5303
+ if (turn?.started)
5304
+ this.failTurn("the Qwen turn ended without a result (it was likely stopped in the terminal); inspect it before retrying", true);
5305
+ this.owner = "personal";
5306
+ this.lastAssistant = undefined;
5307
+ if (this.state === "idle")
5308
+ this.setState("busy");
5309
+ }
5310
+ onAssistant(event) {
5311
+ const content = event.message?.content;
5312
+ const blocks = Array.isArray(content) ? content : [];
5313
+ const text = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
5314
+ const toolUse = event.message?.stop_reason === "tool_use" || blocks.some((block) => block?.type === "tool_use");
5315
+ this.lastAssistant = { text, toolUse };
5316
+ }
5317
+ onMessageStop() {
5318
+ const last = this.lastAssistant;
5319
+ this.lastAssistant = undefined;
5320
+ if (!last || last.toolUse)
5321
+ return;
5322
+ this.endTurn(last.text);
5323
+ }
5324
+ onResult(event) {
5325
+ const text = typeof event.result === "string" ? event.result : this.lastAssistant?.text ?? "";
5326
+ this.endTurn(text);
5327
+ }
5328
+ endTurn(text) {
5329
+ const turn = this.turn, owner = this.owner;
5330
+ this.owner = undefined;
5331
+ this.lastAssistant = undefined;
5332
+ this.abortApprovals();
5333
+ if (owner === "hub" && turn?.started) {
5334
+ this.turn = undefined;
5335
+ const body = text.trim();
5336
+ if (body)
5337
+ this.onMessage?.(body, { inReplyTo: replyParent(turn.envs), to: replyAudience(turn.envs) });
5338
+ if (turn.deliveryId)
5339
+ this.delivery({ id: turn.deliveryId, state: "completed" });
5340
+ this.stalled_ = false;
5341
+ this.opts.onStalled?.(false);
5342
+ this.setState("idle");
5343
+ return;
5344
+ }
5345
+ if (owner === "personal") {
5346
+ this.stalled_ = false;
5347
+ this.opts.onStalled?.(false);
5348
+ if (!this.turn)
5349
+ this.setState("idle");
5350
+ }
5351
+ }
5352
+ failTurn(reason, notIdle = false, known = false) {
5353
+ const turn = this.turn;
5354
+ if (!turn)
5355
+ return;
5356
+ this.turn = undefined;
5357
+ this.owner = undefined;
5358
+ if (turn.deliveryId) {
5359
+ const receipt = { id: turn.deliveryId, state: known ? "completed" : "needs_review", reason };
5360
+ this.delivery(receipt);
5361
+ }
5362
+ try {
5363
+ this.opts.onNotice?.(`${this.id}: ${reason}`);
5364
+ } catch {}
5365
+ if (this.stalled_) {
5366
+ this.stalled_ = false;
5367
+ this.opts.onStalled?.(false);
5368
+ }
5369
+ if (!notIdle && this.state === "busy")
5370
+ this.setState("idle");
5371
+ }
5372
+ abortApprovals() {
5373
+ for (const { controller } of this.pending.values())
5374
+ controller.abort();
5375
+ this.pending.clear();
5376
+ }
5377
+ onControlRequest(event) {
5378
+ const { request_id: requestId, request } = event;
5379
+ if (typeof requestId !== "string" || !request || request.subtype !== "can_use_tool")
5380
+ return;
5381
+ const turn = this.turn;
5382
+ if (!turn?.started || this.owner !== "hub" || this.stopping || this.state !== "busy")
5383
+ return;
5384
+ const name = typeof request.tool_name === "string" ? request.tool_name : "";
5385
+ const input = request.input ?? {};
5386
+ const hubTool = name !== "" && this.opts.autoApprove?.(name) === true;
5387
+ if (!hubTool && qwenDualNamesStateDir({ input, path: request.blocked_path }, this.opts.guardDir ?? this.opts.stateDir)) {
5388
+ this.opts.log?.(`[${this.id}] approval refused: the call names the hub state dir`);
5389
+ return this.answer(requestId, false, turn, "a Qwen write or command that names the hub state dir was refused: the input file is a prompt channel");
5390
+ }
5391
+ let gate;
5392
+ try {
5393
+ gate = this.state_.sessionId ? this.opts.actionGate?.({ names: [name], input }, this.state_.sessionId) : undefined;
5394
+ } catch {
5395
+ gate = { title: "Action approval: the always-ask check failed; command withheld", denyOnly: true };
5396
+ }
5397
+ if (hubTool && !gate) {
5398
+ this.opts.log?.(`permission auto-approved for ${this.id}: ${name}`);
5399
+ return this.answer(requestId, true, turn);
5400
+ }
5401
+ const full = JSON.stringify(input), cut = full.length > 600;
5402
+ const title = gate ? gate.title : `${sanitize(name || "tool")}: ${cut ? `${full.slice(0, 600)} [cut, ${full.length} chars]` : full}`;
5403
+ const options = [...gate?.denyOnly ? [] : [{ optionId: "allow", name: "Allow", kind: "allow_once" }], { optionId: "deny", name: "Deny", kind: "reject_once" }];
5404
+ const controller = new AbortController;
5405
+ this.pending.set(requestId, { controller, turn });
5406
+ const tool = /^[\w.:@-]{1,80}$/.test(name) ? name : undefined;
5407
+ (async () => {
5408
+ let picked;
5409
+ try {
5410
+ picked = await this.opts.onPermission?.({ peer: this.id, title, options, ...tool ? { tool } : {}, ...gate ? { classed: true } : {} }, controller.signal);
5411
+ } catch {
5412
+ picked = undefined;
5413
+ }
5414
+ const still = this.pending.get(requestId);
5415
+ if (!still || still.controller !== controller || controller.signal.aborted || this.turn !== turn || this.stopping)
5416
+ return;
5417
+ this.pending.delete(requestId);
5418
+ this.answer(requestId, picked === "allow" && options.some((option) => option.optionId === "allow"), turn);
5419
+ })();
5420
+ }
5421
+ answer(requestId, allowed, turn, why) {
5422
+ try {
5423
+ this.appendInput({ type: "confirmation_response", request_id: requestId, allowed });
5424
+ } catch {
5425
+ this.opts.log?.(`[${this.id}] could not write an approval answer`);
5426
+ try {
5427
+ this.opts.onNotice?.(`${this.id}: the hub could not write its approval answer (the input file is unwritable); answer the prompt in the Qwen terminal`);
5428
+ } catch {}
5429
+ return;
5430
+ }
5431
+ this.answered.add(requestId);
5432
+ while (this.answered.size > ANSWERED_CAP)
5433
+ this.answered.delete(this.answered.values().next().value);
5434
+ if (allowed || this.turn !== turn)
5435
+ return;
5436
+ this.owner = undefined;
5437
+ this.abortApprovals();
5438
+ this.failTurn(why ?? "a hub deny cancelled the Qwen turn (Qwen emits nothing for it); the turn ended without an answer", false, true);
5439
+ }
5440
+ onControlResponse(event) {
5441
+ const response = event.response;
5442
+ const id = response?.request_id;
5443
+ if (typeof id !== "string")
5444
+ return;
5445
+ if (response.subtype === "error") {
5446
+ this.opts.log?.(`[${this.id}] Qwen refused an approval answer (stale or unknown request); not retried`);
5447
+ return;
5448
+ }
5449
+ const waiting = this.pending.get(id);
5450
+ if (waiting && !this.answered.has(id)) {
5451
+ this.pending.delete(id);
5452
+ waiting.controller.abort();
5453
+ this.opts.log?.(`[${this.id}] an approval was answered in the Qwen terminal; the hub's was withdrawn`);
5454
+ }
5455
+ }
5456
+ async stop(reason) {
5457
+ if (reason !== undefined)
5458
+ this.requestedStopReason = reason;
5459
+ this.stopping = true;
5460
+ if (this.poller)
5461
+ clearInterval(this.poller);
5462
+ this.poller = undefined;
5463
+ if (this.monitor)
5464
+ clearInterval(this.monitor);
5465
+ this.monitor = undefined;
5466
+ this.abortApprovals();
5467
+ const turn = this.turn;
5468
+ this.turn = undefined;
5469
+ this.owner = undefined;
5470
+ if (turn?.deliveryId)
5471
+ this.delivery({ id: turn.deliveryId, state: "needs_review", reason: reason ?? "Qwen session stopped before settlement" });
5472
+ this.saveState();
5473
+ this.setState("offline");
5474
+ }
5475
+ };
5476
+ });
5477
+
5478
+ // src/pi/launch.ts
5479
+ import { join as join9 } from "path";
4675
5480
  function buildPiLaunch(opts, inherited, extension, bridgePort, bridgeToken) {
4676
5481
  const adapter = ADAPTERS.find((adapter) => adapter.start.daemonPort === "pi-rpc");
4677
- const env = { ...peerChildEnv(opts.instance ?? adapter.kind, inherited), ...opts.hubStateDir !== undefined ? { AGENTHUB_STATE_DIR: opts.hubStateDir } : {}, PI_CODING_AGENT_DIR: join8(opts.stateDir, "pi"), AGENTHUB_PI_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`, AGENTHUB_PI_BRIDGE_TOKEN: bridgeToken, AGENTHUB_PI_OWNER_TOKEN: bridgeToken, AGENTHUB_PI_RELAY_URL: opts.relay.url, AGENTHUB_PI_RELAY_TOKEN: opts.relay.token, AGENTHUB_PI_MODELS: JSON.stringify(opts.relay.models), AGENTHUB_PI_TOOLS: JSON.stringify(opts.tools), AGENTHUB_PI_MAX_STEPS: String(opts.maxSteps ?? 30) };
4678
- const args = [...opts.mode === "headless" ? ["--mode", "rpc"] : [], "--provider", "agent-hub-local", "--model", opts.model ?? modelFor(opts.backend, opts.relay.models), "--models", opts.relay.models.map((model) => `agent-hub-local/${model.id}`).join(","), "--no-builtin-tools", "--no-skills", "--no-prompt-templates", "--no-extensions", "--extension", extension, ...opts.preamble ? ["--append-system-prompt", opts.preamble] : [], "--session-dir", join8(opts.stateDir, "pi-sessions")];
5482
+ const env = { ...peerChildEnv(opts.instance ?? adapter.kind, inherited), ...opts.hubStateDir !== undefined ? { AGENTHUB_STATE_DIR: opts.hubStateDir } : {}, PI_CODING_AGENT_DIR: join9(opts.stateDir, "pi"), AGENTHUB_PI_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`, AGENTHUB_PI_BRIDGE_TOKEN: bridgeToken, AGENTHUB_PI_OWNER_TOKEN: bridgeToken, AGENTHUB_PI_RELAY_URL: opts.relay.url, AGENTHUB_PI_RELAY_TOKEN: opts.relay.token, AGENTHUB_PI_MODELS: JSON.stringify(opts.relay.models), AGENTHUB_PI_TOOLS: JSON.stringify(opts.tools), AGENTHUB_PI_MAX_STEPS: String(opts.maxSteps ?? 30) };
5483
+ const args = [...opts.mode === "headless" ? ["--mode", "rpc"] : [], "--provider", "agent-hub-local", "--model", opts.model ?? modelFor(opts.backend, opts.relay.models), "--models", opts.relay.models.map((model) => `agent-hub-local/${model.id}`).join(","), "--no-builtin-tools", "--no-skills", "--no-prompt-templates", "--no-extensions", "--extension", extension, ...opts.preamble ? ["--append-system-prompt", opts.preamble] : [], "--session-dir", join9(opts.stateDir, "pi-sessions")];
4679
5484
  if (opts.sessionFile)
4680
5485
  args.push("--session", opts.sessionFile);
4681
5486
  else if (opts.sessionId)
@@ -4699,21 +5504,16 @@ var init_launch = __esm(() => {
4699
5504
  init_child_process();
4700
5505
  });
4701
5506
 
4702
- // src/pi/process-signature.ts
4703
- var init_process_signature = __esm(() => {
4704
- init_platform();
4705
- });
4706
-
4707
5507
  // src/hub/recovery-store.ts
4708
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
5508
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
4709
5509
  import { randomUUID as randomUUID3 } from "crypto";
4710
5510
  function atomicPrivateJSON(file, value) {
4711
5511
  const temporary = `${file}.${randomUUID3()}.tmp`;
4712
5512
  try {
4713
- writeFileSync5(temporary, `${JSON.stringify(value, null, 2)}
5513
+ writeFileSync6(temporary, `${JSON.stringify(value, null, 2)}
4714
5514
  `, { mode: 384, flag: "wx" });
4715
5515
  chmodSync(temporary, 384);
4716
- renameSync3(temporary, file);
5516
+ renameSync4(temporary, file);
4717
5517
  } finally {
4718
5518
  rmSync4(temporary, { force: true });
4719
5519
  }
@@ -4724,8 +5524,8 @@ var init_recovery_store = __esm(() => {
4724
5524
  });
4725
5525
 
4726
5526
  // src/pi/session.ts
4727
- import { readFileSync as readFileSync6, openSync as openSync4, closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync6 } from "fs";
4728
- import { join as join9, relative as relative2, resolve as resolve7, dirname as dirname6 } from "path";
5527
+ import { readFileSync as readFileSync6, openSync as openSync5, closeSync as closeSync5, fsyncSync, mkdirSync as mkdirSync7 } from "fs";
5528
+ import { join as join10, relative as relative2, resolve as resolve7, dirname as dirname6 } from "path";
4729
5529
  function sessionReadFailure(error) {
4730
5530
  if (error.code === "ENOENT")
4731
5531
  throw new PiSessionUnresumable("missing", error.message, { cause: error });
@@ -4735,7 +5535,7 @@ function validatePiSession(stateDir, cwd, sessionFile, sessionId) {
4735
5535
  let file, sessions;
4736
5536
  try {
4737
5537
  file = realPath(sessionFile);
4738
- sessions = realPath(join9(stateDir, "pi-sessions"));
5538
+ sessions = realPath(join10(stateDir, "pi-sessions"));
4739
5539
  } catch (error) {
4740
5540
  return sessionReadFailure(error);
4741
5541
  }
@@ -4793,11 +5593,11 @@ var init_permission_mode = __esm(() => {
4793
5593
  });
4794
5594
 
4795
5595
  // src/hub/path-policy.ts
4796
- import { accessSync, constants as constants3, lstatSync as lstatSync7, opendirSync, readlinkSync, statSync as statSync2 } from "fs";
4797
- import { dirname as dirname7, isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve8, sep as sep2 } from "path";
5596
+ import { accessSync, constants as constants4, lstatSync as lstatSync8, opendirSync, readlinkSync, statSync as statSync2 } from "fs";
5597
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve8, sep as sep2 } from "path";
4798
5598
  function existing(path) {
4799
5599
  try {
4800
- lstatSync7(path);
5600
+ lstatSync8(path);
4801
5601
  return true;
4802
5602
  } catch (error) {
4803
5603
  if (error.code === "ENOENT")
@@ -4813,7 +5613,7 @@ function policyRealPath(path) {
4813
5613
  throw new Error("path cannot be resolved");
4814
5614
  ancestor = parent;
4815
5615
  }
4816
- return join10(realPath(ancestor), relative3(ancestor, path));
5616
+ return join11(realPath(ancestor), relative3(ancestor, path));
4817
5617
  }
4818
5618
  function policyTargets(cwd, paths) {
4819
5619
  const root = realPath(cwd);
@@ -4841,14 +5641,14 @@ function directAliasTarget(path) {
4841
5641
  return;
4842
5642
  ancestor = parent;
4843
5643
  }
4844
- if (!lstatSync7(ancestor).isSymbolicLink())
5644
+ if (!lstatSync8(ancestor).isSymbolicLink())
4845
5645
  return;
4846
5646
  target = resolve8(dirname7(ancestor), readlinkSync(ancestor), relative3(ancestor, target));
4847
5647
  }
4848
5648
  return;
4849
5649
  }
4850
5650
  function aliasScope(dir, lexical, actual, includeParents, budget) {
4851
- accessSync(dir, constants3.R_OK);
5651
+ accessSync(dir, constants4.R_OK);
4852
5652
  const handle = opendirSync(dir);
4853
5653
  let matched = false;
4854
5654
  try {
@@ -4860,7 +5660,7 @@ function aliasScope(dir, lexical, actual, includeParents, budget) {
4860
5660
  budget.entries++;
4861
5661
  if (!entry.isSymbolicLink())
4862
5662
  continue;
4863
- const target = directAliasTarget(join10(dir, entry.name));
5663
+ const target = directAliasTarget(join11(dir, entry.name));
4864
5664
  if (target !== undefined && (beneath(lexical, target) || beneath(actual, target) || includeParents && (beneath(target, lexical) || beneath(target, actual))))
4865
5665
  matched = true;
4866
5666
  }
@@ -4956,7 +5756,7 @@ function globScope(root, lexical, actual, path, includeParents, budget, roots, a
4956
5756
  if (++budget.entries > 4096)
4957
5757
  throw new Error("policy glob discovery unavailable");
4958
5758
  const next = advance(pattern, states, entry.name);
4959
- if (next.size && visit(join10(dir, entry.name), next, depth + 1, chain))
5759
+ if (next.size && visit(join11(dir, entry.name), next, depth + 1, chain))
4960
5760
  return true;
4961
5761
  }
4962
5762
  } finally {
@@ -5054,7 +5854,7 @@ var init_deny = __esm(() => {
5054
5854
  import { spawn as spawn8, spawnSync as spawnSync3 } from "child_process";
5055
5855
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync4, readdirSync, rmSync as rmSync5 } from "fs";
5056
5856
  import { homedir as homedir4, tmpdir as tmpdir4 } from "os";
5057
- import { dirname as dirname9, join as join11, resolve as resolve9 } from "path";
5857
+ import { dirname as dirname9, join as join12, resolve as resolve9 } from "path";
5058
5858
  function developerDir() {
5059
5859
  const out = spawnSync3("xcode-select", ["-p"], { encoding: "utf8" });
5060
5860
  let dir = out.status === 0 ? out.stdout.trim() : "";
@@ -5075,7 +5875,7 @@ function moduleGitDirs(root) {
5075
5875
  const out = memoryGitSync(["-C", root, "rev-parse", "--git-common-dir"], { encoding: "utf8" });
5076
5876
  if (out.status !== 0)
5077
5877
  return;
5078
- const modulesRoot = join11(resolve9(root, out.stdout.trim()), "modules");
5878
+ const modulesRoot = join12(resolve9(root, out.stdout.trim()), "modules");
5079
5879
  const dirs = [];
5080
5880
  const walk = (dir, depth) => {
5081
5881
  if (depth > 8)
@@ -5089,14 +5889,14 @@ function moduleGitDirs(root) {
5089
5889
  const names = new Set(entries.map((e) => e.name));
5090
5890
  if (names.has("HEAD") && names.has("config")) {
5091
5891
  dirs.push(dir);
5092
- const nested = join11(dir, "modules");
5892
+ const nested = join12(dir, "modules");
5093
5893
  if (names.has("modules"))
5094
5894
  walk(nested, depth + 1);
5095
5895
  return;
5096
5896
  }
5097
5897
  for (const e of entries)
5098
5898
  if (e.isDirectory())
5099
- walk(join11(dir, e.name), depth + 1);
5899
+ walk(join12(dir, e.name), depth + 1);
5100
5900
  };
5101
5901
  walk(modulesRoot, 0);
5102
5902
  const entries = new Set([modulesRoot]);
@@ -5107,13 +5907,13 @@ function moduleGitDirs(root) {
5107
5907
  }
5108
5908
  function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5109
5909
  const home = homedir4();
5110
- const inHome = (p) => p.startsWith("~/") ? join11(home, p.slice(2)) : p;
5910
+ const inHome = (p) => p.startsWith("~/") ? join12(home, p.slice(2)) : p;
5111
5911
  const root = realPath(cwd);
5112
5912
  const gitDirs = externalGitDirs(root);
5113
5913
  const moduleGit = moduleGitDirs(root);
5114
5914
  const creds = [".ssh", ".aws", ".gnupg", ".config/gh", ".config/gcloud", ".kube", ".docker", ".netrc", ".npmrc", ".omniroute", ".claude", ".codex", ".kimi-code", "Library/Keychains"];
5115
5915
  const dev = developerDir();
5116
- const readable = [root, ...HOME_READABLE.map((p) => join11(home, p)), ...readAllow.map(inHome), ...gitDirs, ...dev ? [dev] : []];
5916
+ const readable = [root, ...HOME_READABLE.map((p) => join12(home, p)), ...readAllow.map(inHome), ...gitDirs, ...dev ? [dev] : []];
5117
5917
  const subpaths = (paths) => paths.map((p) => `(subpath ${q(p)})`).join(" ");
5118
5918
  const globals = (names) => names.map((n) => `(global-name ${q(n)})`).join(" ");
5119
5919
  const proxy = typeof network === "object" ? `(allow network-outbound (remote ip ${q(`localhost:${network.proxyPort}`)}))` : undefined;
@@ -5131,14 +5931,14 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5131
5931
  '(allow file-ioctl (regex #"^/dev/"))',
5132
5932
  ...proxy ? [proxy] : network ? ["(allow network*)"] : []
5133
5933
  ];
5134
- const places = `${creds.map((c) => `(subpath ${q(join11(home, c))})`).join(" ")} ${denyRegexes(root, deny, false).join(" ")}`;
5934
+ const places = `${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")} ${denyRegexes(root, deny, false).join(" ")}`;
5135
5935
  return [
5136
5936
  "(version 1)",
5137
5937
  ...start,
5138
5938
  "(deny file-write*)",
5139
5939
  ...readOnly.length ? ['(allow file-write* (literal "/dev/null"))'] : [`(allow file-write* (subpath ${q(root)}) (regex #"^/dev/") ${gitDirs.map((d) => `(subpath ${q(d)})`).join(" ")})`],
5140
5940
  `(deny file-write* ${hubWriteRegexes(root, gitDirs, moduleGit).join(" ")})`,
5141
- `(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join11(home, c))})`).join(" ")})`,
5941
+ `(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")})`,
5142
5942
  `(deny file-read* file-write* ${denyRegexes(root, deny).join(" ")})`,
5143
5943
  ...network ? [`(allow file-read* ${CA_BUNDLES.map((p) => `(literal ${q(p)})`).join(" ")} (regex ${q("/certifi/cacert\\.pem$")}))`, `(deny file-read* file-write* ${places})`] : []
5144
5944
  ].join(`
@@ -5152,7 +5952,7 @@ function sandboxedExec(argv, opts) {
5152
5952
  if (opts.signal?.aborted)
5153
5953
  return Promise.resolve({ code: null, output: "error: command cancelled before execution" });
5154
5954
  return new Promise((resolve) => {
5155
- const own = realPath(mkdtempSync4(join11(tmpdir4(), "ahub-cmd-")));
5955
+ const own = realPath(mkdtempSync4(join12(tmpdir4(), "ahub-cmd-")));
5156
5956
  const sandbox = `${opts.profile}
5157
5957
  (allow file-read* ${opts.readOnlyData ? "" : "file-write* "}process-exec (subpath ${q(own)}))`;
5158
5958
  const env = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: homedir4(), LANG: process.env.LANG ?? "en_US.UTF-8", TERM: "dumb", ...opts.env, TMPDIR: `${own}/` };
@@ -5227,8 +6027,8 @@ var init_sandbox = __esm(() => {
5227
6027
  });
5228
6028
 
5229
6029
  // src/local/tools.ts
5230
- import { lstatSync as lstatSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
5231
- import { dirname as dirname10, join as join12, relative as relative4, resolve as resolve10 } from "path";
6030
+ import { lstatSync as lstatSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
6031
+ import { dirname as dirname10, join as join13, relative as relative4, resolve as resolve10 } from "path";
5232
6032
  function guardPath(ctx, path, mode) {
5233
6033
  const root = realPath(ctx.cwd);
5234
6034
  const abs = resolve10(root, path);
@@ -5241,7 +6041,7 @@ function guardPath(ctx, path, mode) {
5241
6041
  } catch {
5242
6042
  throw new Error(`${path} goes through a dangling symlink`);
5243
6043
  }
5244
- const real = join12(realExisting, relative4(existing, abs));
6044
+ const real = join13(realExisting, relative4(existing, abs));
5245
6045
  const rel = relative4(root, real);
5246
6046
  if (rel.startsWith("..") || resolve10(root, rel) !== real)
5247
6047
  throw new Error(`${path} is outside the project directory`);
@@ -5249,7 +6049,7 @@ function guardPath(ctx, path, mode) {
5249
6049
  throw new Error(`${path} is on the secrets denylist`);
5250
6050
  if (mode === "write" && isHubWritePath(rel))
5251
6051
  throw new Error(`${path} is not writable by the local worker`);
5252
- if (mode === "write" && ctx.readOnly?.length && (lstatSync8(real, { throwIfNoEntry: false })?.nlink ?? 1) > 1)
6052
+ if (mode === "write" && ctx.readOnly?.length && (lstatSync9(real, { throwIfNoEntry: false })?.nlink ?? 1) > 1)
5253
6053
  throw new Error("hardlinked write target cannot be verified with data.readonly");
5254
6054
  if (mode === "write" && matchesPolicy(root, path, ctx.readOnly ?? []))
5255
6055
  throw new Error("target is protected by data.readonly");
@@ -5320,8 +6120,8 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5320
6120
  file = guardPath(ctx, String(a.path), "write");
5321
6121
  if (file !== approvedTarget)
5322
6122
  return "error: path target changed during approval; inspect before retrying";
5323
- mkdirSync7(dirname10(file), { recursive: true });
5324
- writeFileSync6(file, content);
6123
+ mkdirSync8(dirname10(file), { recursive: true });
6124
+ writeFileSync7(file, content);
5325
6125
  return `wrote ${a.path}`;
5326
6126
  }
5327
6127
  case "edit": {
@@ -5345,7 +6145,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5345
6145
  count = text.split(String(a.old)).length - 1;
5346
6146
  if (count !== 1)
5347
6147
  return `error: file changed during approval; \`old\` must match exactly once, it matched ${count} times`;
5348
- writeFileSync6(file, text.replace(String(a.old), () => String(a.new ?? "")));
6148
+ writeFileSync7(file, text.replace(String(a.old), () => String(a.new ?? "")));
5349
6149
  return `edited ${a.path}`;
5350
6150
  }
5351
6151
  case "bash": {
@@ -5395,7 +6195,7 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
5395
6195
  }
5396
6196
  var APPROVAL_EXPIRED_TEXT = "error: approval expired: no person answered; do not retry this call; hand the task off or stop", ApprovalWaitStop, approvalError = (picked, denied) => picked === true ? undefined : picked === "expired" ? APPROVAL_EXPIRED_TEXT : picked === "aborted" ? "error: approval withdrawn because the turn ended; no operation was executed" : denied, lexists = (p) => {
5397
6197
  try {
5398
- lstatSync8(p);
6198
+ lstatSync9(p);
5399
6199
  return true;
5400
6200
  } catch {
5401
6201
  return false;
@@ -5449,8 +6249,8 @@ var PI_CEILING_KIND = "tool-step-ceiling", PI_CEILING_UNIT = "tool-step";
5449
6249
 
5450
6250
  // src/adapters/pi.ts
5451
6251
  import { randomUUID as randomUUID4 } from "crypto";
5452
- import { existsSync as existsSync7, mkdirSync as mkdirSync8 } from "fs";
5453
- import { join as join13, resolve as resolve11 } from "path";
6252
+ import { existsSync as existsSync7, mkdirSync as mkdirSync9 } from "fs";
6253
+ import { join as join14, resolve as resolve11 } from "path";
5454
6254
  import { spawn as spawn9 } from "child_process";
5455
6255
  function ownerStillAlive(pid, signature) {
5456
6256
  const current = processSignature(pid);
@@ -5816,9 +6616,9 @@ var init_pi = __esm(() => {
5816
6616
  this.approvalAnswerEpoch = undefined;
5817
6617
  this.approvalStopReason = "";
5818
6618
  this.approvalAbortSent = false;
5819
- mkdirSync8(this.opts.stateDir, { recursive: true });
5820
- const sessions = join13(this.opts.stateDir, "pi-sessions");
5821
- mkdirSync8(sessions, { recursive: true, mode: 448 });
6619
+ mkdirSync9(this.opts.stateDir, { recursive: true });
6620
+ const sessions = join14(this.opts.stateDir, "pi-sessions");
6621
+ mkdirSync9(sessions, { recursive: true, mode: 448 });
5822
6622
  if (this.opts.sessionFile) {
5823
6623
  try {
5824
6624
  validatePiSession(this.opts.stateDir, this.opts.cwd, this.opts.sessionFile, this.opts.sessionId);
@@ -5875,7 +6675,7 @@ var init_pi = __esm(() => {
5875
6675
  return new Response("not found", { status: 404 });
5876
6676
  } });
5877
6677
  this.server = bridge;
5878
- const extension = resolve11(join13(import.meta.dir, "../pi/extension.ts"));
6678
+ const extension = resolve11(join14(import.meta.dir, "../pi/extension.ts"));
5879
6679
  const inherited = {};
5880
6680
  for (const key of ["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"])
5881
6681
  if (process.env[key])
@@ -8804,8 +9604,8 @@ var init_shipped_roles = __esm(() => {
8804
9604
 
8805
9605
  // src/hub/roles.ts
8806
9606
  import { createHash as createHash5 } from "crypto";
8807
- import { constants as constants4, closeSync as closeSync5, fstatSync as fstatSync4, lstatSync as lstatSync9, openSync as openSync5, readSync as readSync3, readdirSync as readdirSync2 } from "fs";
8808
- import { join as join14 } from "path";
9607
+ import { constants as constants5, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, openSync as openSync6, readSync as readSync4, readdirSync as readdirSync2 } from "fs";
9608
+ import { join as join15 } from "path";
8809
9609
  function parseRole(text, stem, layer) {
8810
9610
  if (Buffer.byteLength(text) > 8192 || text.includes("\r") || !ID.test(stem))
8811
9611
  throw new Error("invalid role size, encoding or file id");
@@ -8852,24 +9652,24 @@ function parseRole(text, stem, layer) {
8852
9652
  return { id: stem, name: header.name, group: header.group, summary: header.summary, source: header.source, base: list("base", BASES2), classes: list("classes", CLASSES2), body, review, layer, hash: createHash5("sha256").update(text).digest("hex") };
8853
9653
  }
8854
9654
  function projectRole(file) {
8855
- const fd = openSync5(file, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
9655
+ const fd = openSync6(file, constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK);
8856
9656
  try {
8857
- const before = fstatSync4(fd);
9657
+ const before = fstatSync5(fd);
8858
9658
  if (!before.isFile() || before.nlink !== 1 || before.size > 8192)
8859
9659
  throw new Error("role must be a bounded regular file");
8860
9660
  const bytes = Buffer.alloc(8193);
8861
9661
  let length = 0, n = 0;
8862
- while ((n = readSync3(fd, bytes, length, bytes.length - length, null)) > 0) {
9662
+ while ((n = readSync4(fd, bytes, length, bytes.length - length, null)) > 0) {
8863
9663
  length += n;
8864
9664
  if (length > 8192)
8865
9665
  throw new Error("role exceeds file cap");
8866
9666
  }
8867
- const after = fstatSync4(fd);
9667
+ const after = fstatSync5(fd);
8868
9668
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || length !== after.size)
8869
9669
  throw new Error("role changed during read");
8870
9670
  return bytes.subarray(0, length).toString("utf8");
8871
9671
  } finally {
8872
- closeSync5(fd);
9672
+ closeSync6(fd);
8873
9673
  }
8874
9674
  }
8875
9675
  function loadRoleCatalog(cwd) {
@@ -8884,9 +9684,9 @@ function loadRoleCatalog(cwd) {
8884
9684
  if (!cwd)
8885
9685
  return { roles, errors };
8886
9686
  try {
8887
- const config = join14(cwd, ".agenthub"), dir = join14(config, "roles");
9687
+ const config = join15(cwd, ".agenthub"), dir = join15(config, "roles");
8888
9688
  for (const path of [config, dir]) {
8889
- const stat = lstatSync9(path);
9689
+ const stat = lstatSync10(path);
8890
9690
  if (!stat.isDirectory() || stat.isSymbolicLink())
8891
9691
  throw new Error("unsafe role directory");
8892
9692
  }
@@ -8896,7 +9696,7 @@ function loadRoleCatalog(cwd) {
8896
9696
  for (const file of files) {
8897
9697
  const id = file.slice(0, -3);
8898
9698
  try {
8899
- roles.set(id, parseRole(projectRole(join14(dir, file)), id, "project"));
9699
+ roles.set(id, parseRole(projectRole(join15(dir, file)), id, "project"));
8900
9700
  } catch {
8901
9701
  errors.push({ id: ID.test(id) ? id : "invalid-file-id", reason: "invalid or unsafe project role" });
8902
9702
  }
@@ -9289,7 +10089,7 @@ var init_config = __esm(() => {
9289
10089
  });
9290
10090
 
9291
10091
  // src/hub/routing.ts
9292
- import { join as join15, resolve as resolve12 } from "path";
10092
+ import { join as join16, resolve as resolve12 } from "path";
9293
10093
  function effectiveLocalRoute(defaultRoute, classRoute, classFixedModel) {
9294
10094
  return classRoute ?? (classFixedModel === undefined ? defaultRoute : undefined);
9295
10095
  }
@@ -9300,7 +10100,7 @@ var init_routing = __esm(() => {
9300
10100
  init_spec_read();
9301
10101
  init_config();
9302
10102
  init_stage();
9303
- TEMPLATE = join15(import.meta.dir, "..", "..", "templates", "routing.toml");
10103
+ TEMPLATE = join16(import.meta.dir, "..", "..", "templates", "routing.toml");
9304
10104
  cache = new Map;
9305
10105
  });
9306
10106
 
@@ -9851,7 +10651,7 @@ var init_launch2 = __esm(() => {
9851
10651
 
9852
10652
  // src/adapters/claude-task-sessions.ts
9853
10653
  import { randomUUID as randomUUID7 } from "crypto";
9854
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync8 } from "fs";
10654
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync8 } from "fs";
9855
10655
  import { dirname as dirname11 } from "path";
9856
10656
  function readState(file) {
9857
10657
  let text;
@@ -9889,7 +10689,7 @@ class ClaudeTaskSessions {
9889
10689
  }
9890
10690
  write(sessions) {
9891
10691
  const next = { version: 1, sessions };
9892
- mkdirSync9(dirname11(this.file), { recursive: true, mode: 448 });
10692
+ mkdirSync10(dirname11(this.file), { recursive: true, mode: 448 });
9893
10693
  atomicPrivateJSON(this.file, next);
9894
10694
  this.state = next;
9895
10695
  }
@@ -9934,9 +10734,9 @@ var init_claude_task_sessions = __esm(() => {
9934
10734
 
9935
10735
  // src/adapters/claude-implementer.ts
9936
10736
  import { randomUUID as randomUUID8 } from "crypto";
9937
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
10737
+ import { lstatSync as lstatSync11, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9938
10738
  import { tmpdir as tmpdir5 } from "os";
9939
- import { join as join16 } from "path";
10739
+ import { join as join17 } from "path";
9940
10740
  function claudeImplementerArgs(turn, mcp, settings, prompt) {
9941
10741
  return [
9942
10742
  "-p",
@@ -9968,10 +10768,10 @@ function claudeImplementerArgs(turn, mcp, settings, prompt) {
9968
10768
  async function proveClaudeImplementer(peer, command) {
9969
10769
  const binary = Bun.which(command[0], { PATH: process.env.PATH }) ?? command[0];
9970
10770
  const fingerprint = () => {
9971
- const file = realPath(binary), stat = lstatSync10(file);
10771
+ const file = realPath(binary), stat = lstatSync11(file);
9972
10772
  return [file, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
9973
10773
  };
9974
- const binding = fingerprint(), home = mkdtempSync5(join16(tmpdir5(), "ahub-claude-proof-"));
10774
+ const binding = fingerprint(), home = mkdtempSync5(join17(tmpdir5(), "ahub-claude-proof-"));
9975
10775
  let version = "", timedOut = false;
9976
10776
  const child = new OwnedPrintChild({
9977
10777
  peer,
@@ -9980,7 +10780,7 @@ async function proveClaudeImplementer(peer, command) {
9980
10780
  cwd: home,
9981
10781
  input: "",
9982
10782
  envMode: "replace",
9983
- env: { PATH: process.env.PATH, HOME: home, CLAUDE_CONFIG_DIR: join16(home, ".claude") },
10783
+ env: { PATH: process.env.PATH, HOME: home, CLAUDE_CONFIG_DIR: join17(home, ".claude") },
9984
10784
  maxFrameBytes: 4096,
9985
10785
  maxOutputBytes: 4096,
9986
10786
  onLine: (line) => {
@@ -10051,7 +10851,7 @@ var init_claude_implementer = __esm(() => {
10051
10851
  if (options.mode !== undefined && options.mode !== "ask" && options.mode !== "ask-when-needed")
10052
10852
  throw new Error("Claude implementer refuses unsupported permission mode");
10053
10853
  this.mode = options.mode ?? "ask";
10054
- this.sessions = new ClaudeTaskSessions(join16(options.stateDir, claudeStateFile("claude-task-sessions", id)));
10854
+ this.sessions = new ClaudeTaskSessions(join17(options.stateDir, claudeStateFile("claude-task-sessions", id)));
10055
10855
  }
10056
10856
  get permissionModeState() {
10057
10857
  return this.mode;
@@ -10197,9 +10997,9 @@ var init_claude_implementer = __esm(() => {
10197
10997
  this.options.notify?.(`${this.id}: task #${group.task} session fresh; recorded transcript is missing`);
10198
10998
  }
10199
10999
  }
10200
- const launchId = randomUUID8(), directory = mkdtempSync5(join16(this.options.stateDir, "claude-child-")), settings = join16(directory, "settings.json");
11000
+ const launchId = randomUUID8(), directory = mkdtempSync5(join17(this.options.stateDir, "claude-child-")), settings = join17(directory, "settings.json");
10201
11001
  this.cleanup = () => rmSync6(directory, { recursive: true, force: true });
10202
- writeFileSync7(settings, sessionSettings(undefined, { script: this.options.factsHook, stateDir: this.options.stateDir, purpose: "permission" }), { mode: 384, flag: "wx" });
11002
+ writeFileSync8(settings, sessionSettings(undefined, { script: this.options.factsHook, stateDir: this.options.stateDir, purpose: "permission" }), { mode: 384, flag: "wx" });
10203
11003
  const env = {
10204
11004
  AGENTHUB_MODE: "tools",
10205
11005
  AGENTHUB_PEER_ID: this.id,
@@ -10333,6 +11133,14 @@ function createJcode(ctx) {
10333
11133
  init_jcode();
10334
11134
  return new JcodePeer(ctx.daemon.jcode);
10335
11135
  }
11136
+ function createQwenDual(ctx) {
11137
+ if (!("qwenDual" in ctx.daemon))
11138
+ throw new Error("Qwen TUI requires its dual-output transport options");
11139
+ if (ctx.permission === "read-only")
11140
+ throw new Error("Qwen has no verified read-only permission mapping");
11141
+ init_qwen_dual();
11142
+ return new QwenDualPeer(ctx.instance, ctx.daemon.qwenDual);
11143
+ }
10336
11144
  function createPi(ctx) {
10337
11145
  if (!("pi" in ctx.daemon))
10338
11146
  throw new Error("Pi requires its Pi transport options");
@@ -10384,6 +11192,7 @@ var init_native_factories = __esm(() => {
10384
11192
  DAEMON_PORTS = {
10385
11193
  "codex-appserver": (options) => ({ codex: options }),
10386
11194
  acp: (options) => ({ acp: options }),
11195
+ "qwen-dual": (options) => ({ qwenDual: options }),
10387
11196
  "owned-acp": (options) => ({ jcode: options }),
10388
11197
  "pi-rpc": (options) => ({ pi: options }),
10389
11198
  "local-worker": (options) => ({ local: options }),
@@ -10455,9 +11264,9 @@ var init_reviewer_variants = __esm(() => {
10455
11264
  });
10456
11265
 
10457
11266
  // src/adapters/agy/hooks.ts
10458
- import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync5, lstatSync as lstatSync11, openSync as openSync6, readSync as readSync4 } from "fs";
11267
+ import { closeSync as closeSync7, constants as constants6, fstatSync as fstatSync6, lstatSync as lstatSync12, openSync as openSync7, readSync as readSync5 } from "fs";
10459
11268
  import { homedir as homedir5 } from "os";
10460
- import { isAbsolute as isAbsolute6, join as join17, relative as relative5 } from "path";
11269
+ import { isAbsolute as isAbsolute6, join as join18, relative as relative5 } from "path";
10461
11270
  import { createHash as createHash6 } from "crypto";
10462
11271
  function decodeAgyHook(event, value) {
10463
11272
  if (!EVENTS2.includes(event) || !record3(value) || typeof value.conversationId !== "string" || !UUID2.test(value.conversationId) || typeof value.transcriptPath !== "string" || !isAbsolute6(value.transcriptPath) || value.transcriptPath.length > 4096 || !Array.isArray(value.workspacePaths) || !value.workspacePaths.length || value.workspacePaths.length > 32 || !value.workspacePaths.every((path) => typeof path === "string" && path.length <= 4096 && isAbsolute6(path)))
@@ -10495,19 +11304,19 @@ function readAgyTranscript(session, transcript) {
10495
11304
  if (!UUID2.test(session))
10496
11305
  return empty;
10497
11306
  const home = process.env.HOME ?? homedir5();
10498
- const candidates = ["antigravity-cli", "antigravity", "antigravity-ide"].map((surface) => join17(home, ".gemini", surface, "brain", session, ".system_generated/logs/transcript_full.jsonl"));
11307
+ const candidates = ["antigravity-cli", "antigravity", "antigravity-ide"].map((surface) => join18(home, ".gemini", surface, "brain", session, ".system_generated/logs/transcript_full.jsonl"));
10499
11308
  if (!candidates.includes(transcript))
10500
11309
  return empty;
10501
11310
  let fd;
10502
11311
  try {
10503
11312
  let cursor = home;
10504
11313
  for (const name of relative5(home, transcript).split("/")) {
10505
- cursor = join17(cursor, name);
10506
- if (lstatSync11(cursor).isSymbolicLink())
11314
+ cursor = join18(cursor, name);
11315
+ if (lstatSync12(cursor).isSymbolicLink())
10507
11316
  return empty;
10508
11317
  }
10509
- fd = openSync6(transcript, constants5.O_RDONLY | constants5.O_NOFOLLOW);
10510
- const stat = fstatSync5(fd);
11318
+ fd = openSync7(transcript, constants6.O_RDONLY | constants6.O_NOFOLLOW);
11319
+ const stat = fstatSync6(fd);
10511
11320
  if (!stat.isFile() || stat.nlink !== 1)
10512
11321
  return empty;
10513
11322
  const cap = 4 * 1024 * 1024;
@@ -10516,7 +11325,7 @@ function readAgyTranscript(session, transcript) {
10516
11325
  const buffer = Buffer.alloc(stat.size - from);
10517
11326
  let bytes = 0;
10518
11327
  while (bytes < buffer.length) {
10519
- const count = readSync4(fd, buffer, bytes, buffer.length - bytes, from + bytes);
11328
+ const count = readSync5(fd, buffer, bytes, buffer.length - bytes, from + bytes);
10520
11329
  if (!count)
10521
11330
  break;
10522
11331
  bytes += count;
@@ -10549,7 +11358,7 @@ function readAgyTranscript(session, transcript) {
10549
11358
  return empty;
10550
11359
  } finally {
10551
11360
  if (fd !== undefined)
10552
- closeSync6(fd);
11361
+ closeSync7(fd);
10553
11362
  }
10554
11363
  }
10555
11364
  var EVENTS2, UUID2, record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value), AGY_FACTS_HOOK;
@@ -10628,7 +11437,7 @@ function builtinCliEntries(definitions) {
10628
11437
  }
10629
11438
  // src/adapters/definitions/native-cli-entry-claude.ts
10630
11439
  import { readFileSync as readFileSync9 } from "fs";
10631
- import { join as join18 } from "path";
11440
+ import { join as join19 } from "path";
10632
11441
  import { randomUUID as randomUUID9 } from "crypto";
10633
11442
  async function runClaudeEntry(ctx) {
10634
11443
  const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, fail } = ctx;
@@ -10679,17 +11488,17 @@ async function runClaudeEntry(ctx) {
10679
11488
  }
10680
11489
  }
10681
11490
  let original;
10682
- for (const file of [join18(executionCwd, ".claude", "settings.local.json"), join18(executionCwd, ".claude", "settings.json"), join18(process.env.HOME ?? "", ".claude", "settings.json")]) {
11491
+ for (const file of [join19(executionCwd, ".claude", "settings.local.json"), join19(executionCwd, ".claude", "settings.json"), join19(process.env.HOME ?? "", ".claude", "settings.json")]) {
10683
11492
  try {
10684
11493
  original ??= JSON.parse(readFileSync9(file, "utf8")).statusLine;
10685
11494
  } catch {}
10686
11495
  }
10687
- const facts = claudeObservationHooks(projectConfig(), { script: join18(cliDir, "facts-hook.ts"), stateDir });
11496
+ const facts = claudeObservationHooks(projectConfig(), { script: join19(cliDir, "facts-hook.ts"), stateDir });
10688
11497
  let previousLaunch;
10689
11498
  try {
10690
- previousLaunch = JSON.parse(readFileSync9(join18(stateDir, claudeStateFile("claude-launch", peer)), "utf8"));
11499
+ previousLaunch = JSON.parse(readFileSync9(join19(stateDir, claudeStateFile("claude-launch", peer)), "utf8"));
10691
11500
  } catch {}
10692
- const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join18(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original } : {} }, ...facts ? { facts } : {} });
11501
+ const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join19(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original } : {} }, ...facts ? { facts } : {} });
10693
11502
  let result;
10694
11503
  try {
10695
11504
  let launchRecord;
@@ -11174,7 +11983,7 @@ var init_native_cli_entry_jcode = () => {};
11174
11983
 
11175
11984
  // src/adapters/definitions/native-cli-preview-claude.ts
11176
11985
  import { readFileSync as readFileSync10 } from "fs";
11177
- import { join as join19 } from "path";
11986
+ import { join as join20 } from "path";
11178
11987
  function previewClaude(ctx) {
11179
11988
  const { args, cwd, stateDir, cliDir, unattended, config } = ctx;
11180
11989
  const { buildLaunch, claudeObservationHooks } = ctx.helpers;
@@ -11182,13 +11991,13 @@ function previewClaude(ctx) {
11182
11991
  let envNames = state.envNames;
11183
11992
  let launch;
11184
11993
  let original;
11185
- for (const file of [join19(cwd, ".claude/settings.local.json"), join19(cwd, ".claude/settings.json"), join19(process.env.HOME ?? "", ".claude/settings.json")]) {
11994
+ for (const file of [join20(cwd, ".claude/settings.local.json"), join20(cwd, ".claude/settings.json"), join20(process.env.HOME ?? "", ".claude/settings.json")]) {
11186
11995
  try {
11187
11996
  original ??= JSON.parse(readFileSync10(file, "utf8")).statusLine;
11188
11997
  } catch {}
11189
11998
  }
11190
- const facts = claudeObservationHooks(config, { script: join19(cliDir, "facts-hook.ts"), stateDir });
11191
- launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script: join19(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {} }, ...facts ? { facts } : {} });
11999
+ const facts = claudeObservationHooks(config, { script: join20(cliDir, "facts-hook.ts"), stateDir });
12000
+ launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script: join20(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {} }, ...facts ? { facts } : {} });
11192
12001
  const safeArgs = args.map((arg, i) => {
11193
12002
  if (args[i - 1] === "--settings")
11194
12003
  return "{}";
@@ -11199,7 +12008,7 @@ function previewClaude(ctx) {
11199
12008
  return publicArg(arg);
11200
12009
  });
11201
12010
  const publicLaunch = buildLaunch("claude", safeArgs, { preview: true, unattended, statusLine: {
11202
- script: join19(cliDir, "statusline-tee.ts"),
12011
+ script: join20(cliDir, "statusline-tee.ts"),
11203
12012
  stateDir,
11204
12013
  ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {}
11205
12014
  }, facts });
@@ -11232,7 +12041,7 @@ function previewCodex(ctx) {
11232
12041
  var init_native_cli_preview_codex = () => {};
11233
12042
 
11234
12043
  // src/adapters/definitions/native-cli-preview-kimi.ts
11235
- import { join as join20 } from "path";
12044
+ import { join as join21 } from "path";
11236
12045
  function previewKimi(ctx) {
11237
12046
  const { args, cwd, stateDir, cliDir, config } = ctx;
11238
12047
  const { childEnv, startModeOf, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, kimiMcpPlan } = ctx.helpers;
@@ -11255,14 +12064,14 @@ function previewKimi(ctx) {
11255
12064
  launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map((arg) => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
11256
12065
  envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
11257
12066
  }
11258
- const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join20(cliDir, "../../plugins/agent-hub/server.js"), mode);
12067
+ const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join21(cliDir, "../../plugins/agent-hub/server.js"), mode);
11259
12068
  kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
11260
12069
  return finishPreview(ctx, launch, envNames, unresolved, kimiMetadata);
11261
12070
  }
11262
12071
  var init_native_cli_preview_kimi = () => {};
11263
12072
 
11264
12073
  // src/adapters/definitions/native-cli-preview-pi.ts
11265
- import { join as join21, resolve as resolve15 } from "path";
12074
+ import { join as join22, resolve as resolve15 } from "path";
11266
12075
  function previewPi(ctx) {
11267
12076
  const { args, stateDir, cliDir, config } = ctx;
11268
12077
  const { relayModelIds, buildPiLaunch, startModeOf } = ctx.helpers;
@@ -11297,7 +12106,7 @@ function previewPi(ctx) {
11297
12106
  tools: [],
11298
12107
  preamble: REDACTED,
11299
12108
  maxSteps: config.pi.max_steps
11300
- }, {}, resolve15(join21(cliDir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
12109
+ }, {}, resolve15(join22(cliDir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
11301
12110
  launch = pi;
11302
12111
  envNames = Object.keys(pi.env).concat(["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"].filter((key) => process.env[key] !== undefined));
11303
12112
  if (mode === "tui")
@@ -11382,8 +12191,8 @@ var init_native_config_policy = __esm(() => {
11382
12191
  });
11383
12192
 
11384
12193
  // src/adapters/definitions/native-policy-aux.ts
11385
- import { closeSync as closeSync7, constants as constants6, fstatSync as fstatSync6, openSync as openSync7, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
11386
- import { isAbsolute as isAbsolute7, join as join22, normalize } from "path";
12194
+ import { closeSync as closeSync8, constants as constants7, fstatSync as fstatSync7, openSync as openSync8, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
12195
+ import { isAbsolute as isAbsolute7, join as join23, normalize } from "path";
11387
12196
  function claudeCrashPlan(_meta) {
11388
12197
  return { how: "claude: the Claude Code plugin reconnects by itself while that session is still open" };
11389
12198
  }
@@ -11428,13 +12237,13 @@ function localCrashPlan(meta) {
11428
12237
  }
11429
12238
  function claudePlugins(root, cwd) {
11430
12239
  const enabled = new Map;
11431
- for (const file of [join22(root, "settings.json"), ...cwd ? [join22(cwd, ".claude", "settings.json"), join22(cwd, ".claude", "settings.local.json")] : []]) {
12240
+ for (const file of [join23(root, "settings.json"), ...cwd ? [join23(cwd, ".claude", "settings.json"), join23(cwd, ".claude", "settings.local.json")] : []]) {
11432
12241
  const map = json(file)?.enabledPlugins;
11433
12242
  if (map && typeof map === "object")
11434
12243
  for (const [key, on] of Object.entries(map))
11435
12244
  enabled.set(key, on === true);
11436
12245
  }
11437
- const installed = json(join22(root, "plugins", "installed_plugins.json"))?.plugins ?? {};
12246
+ const installed = json(join23(root, "plugins", "installed_plugins.json"))?.plugins ?? {};
11438
12247
  const out = [];
11439
12248
  for (const [key, on] of enabled) {
11440
12249
  if (!on)
@@ -11448,7 +12257,7 @@ function claudePlugins(root, cwd) {
11448
12257
  return out;
11449
12258
  }
11450
12259
  function codexPlugins(root) {
11451
- const config = text(join22(root, "config.toml"));
12260
+ const config = text(join23(root, "config.toml"));
11452
12261
  if (config === undefined)
11453
12262
  return [];
11454
12263
  const found = new Map, disabled = new Set;
@@ -11467,7 +12276,7 @@ function codexPlugins(root) {
11467
12276
  continue;
11468
12277
  let versions = [];
11469
12278
  try {
11470
- versions = readdirSync3(join22(root, "plugins", "cache", market, plugin));
12279
+ versions = readdirSync3(join23(root, "plugins", "cache", market, plugin));
11471
12280
  } catch {}
11472
12281
  out.push({ name, version: versions.length === 1 ? version2(versions[0]) : null });
11473
12282
  }
@@ -11487,8 +12296,8 @@ function absoluteContinuityPath(value) {
11487
12296
  var str3 = (value) => typeof value === "string" && value ? value : undefined, PLUGINS, CLAUDE_ONLY, text = (file) => {
11488
12297
  let fd;
11489
12298
  try {
11490
- fd = openSync7(file, constants6.O_RDONLY | constants6.O_NONBLOCK);
11491
- const st = fstatSync6(fd);
12299
+ fd = openSync8(file, constants7.O_RDONLY | constants7.O_NONBLOCK);
12300
+ const st = fstatSync7(fd);
11492
12301
  if (!st.isFile() || st.size > 1024 * 1024)
11493
12302
  return;
11494
12303
  return readFileSync11(fd, "utf8");
@@ -11496,7 +12305,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
11496
12305
  return;
11497
12306
  } finally {
11498
12307
  if (fd !== undefined)
11499
- closeSync7(fd);
12308
+ closeSync8(fd);
11500
12309
  }
11501
12310
  }, json = (file) => {
11502
12311
  try {
@@ -11505,7 +12314,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
11505
12314
  } catch {
11506
12315
  return;
11507
12316
  }
11508
- }, version2 = (v) => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null, claudePluginInventory = (ctx) => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() || join22(ctx.home, ".claude"), ctx.cwd), codexPluginInventory = (ctx) => codexPlugins(ctx.env.CODEX_HOME?.trim() || join22(ctx.home, ".codex")), codexToolSemantic = (name, _command) => name.toLowerCase() === "filechange" ? "edit" : undefined;
12317
+ }, version2 = (v) => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null, claudePluginInventory = (ctx) => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() || join23(ctx.home, ".claude"), ctx.cwd), codexPluginInventory = (ctx) => codexPlugins(ctx.env.CODEX_HOME?.trim() || join23(ctx.home, ".codex")), codexToolSemantic = (name, _command) => name.toLowerCase() === "filechange" ? "edit" : undefined;
11509
12318
  var init_native_policy_aux = __esm(() => {
11510
12319
  PLUGINS = new Map([["claude-mem", "claude-mem"], ["ponytail", "ponytail"], ["open-code-review", "open-code-review"]]);
11511
12320
  CLAUDE_ONLY = new Map([["codex@openai-codex", "codex-plugin"]]);
@@ -11548,9 +12357,9 @@ var init_native_cli_launch_common = __esm(() => {
11548
12357
  });
11549
12358
 
11550
12359
  // src/adapters/definitions/native-cli-claude-launch.ts
11551
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, renameSync as renameSync4, unlinkSync as unlinkSync3, readFileSync as readFileSync12 } from "fs";
12360
+ import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, renameSync as renameSync5, unlinkSync as unlinkSync3, readFileSync as readFileSync12 } from "fs";
11552
12361
  import { randomUUID as randomUUID10 } from "crypto";
11553
- import { resolve as resolve16, join as join23 } from "path";
12362
+ import { resolve as resolve16, join as join24 } from "path";
11554
12363
  function claudeChannel(userArgs) {
11555
12364
  const bundle = resolve16(import.meta.dir, "../../../plugins/agent-hub/server.js");
11556
12365
  for (let i = 0;i < userArgs.length; i++) {
@@ -11612,11 +12421,11 @@ function claudeNativeLaunch(ctx) {
11612
12421
  const stateDir = ctx.facts?.stateDir ?? ctx.statusLine?.stateDir;
11613
12422
  if (!stateDir)
11614
12423
  throw new Error("Claude session settings require the hub state directory");
11615
- const file = join23(stateDir, `claude-settings-${randomUUID10()}.json`);
12424
+ const file = join24(stateDir, `claude-settings-${randomUUID10()}.json`);
11616
12425
  try {
11617
- mkdirSync10(stateDir, { recursive: true, mode: 448 });
11618
- writeFileSync8(`${file}.tmp`, settingsArgs[1], { mode: 384, flag: "wx" });
11619
- renameSync4(`${file}.tmp`, file);
12426
+ mkdirSync11(stateDir, { recursive: true, mode: 448 });
12427
+ writeFileSync9(`${file}.tmp`, settingsArgs[1], { mode: 384, flag: "wx" });
12428
+ renameSync5(`${file}.tmp`, file);
11620
12429
  } catch {
11621
12430
  try {
11622
12431
  unlinkSync3(`${file}.tmp`);
@@ -11669,7 +12478,7 @@ var init_native_cli_codex_launch = __esm(() => {
11669
12478
  });
11670
12479
 
11671
12480
  // src/hub/events.ts
11672
- import { appendFileSync, closeSync as closeSync8, existsSync as existsSync8, fstatSync as fstatSync7, openSync as openSync8, readFileSync as readFileSync13, readSync as readSync5 } from "fs";
12481
+ import { appendFileSync, closeSync as closeSync9, existsSync as existsSync8, fstatSync as fstatSync8, openSync as openSync9, readFileSync as readFileSync13, readSync as readSync6 } from "fs";
11673
12482
  function readEvents(file, since = 0) {
11674
12483
  if (!existsSync8(file))
11675
12484
  return [];
@@ -11691,13 +12500,13 @@ var init_events = () => {};
11691
12500
  // src/cli/recovery-session-file.ts
11692
12501
  import { statSync as statSync4 } from "fs";
11693
12502
  import { homedir as homedir6 } from "os";
11694
- import { join as join24 } from "path";
12503
+ import { join as join25 } from "path";
11695
12504
  function checkedSessionPath(binding, kind) {
11696
12505
  if (kind === "rollout")
11697
- return join24(binding.launch.env.CODEX_HOME ?? join24(homedir6(), ".codex"), "sessions");
11698
- const config = binding.launch.env.CLAUDE_CONFIG_DIR ?? join24(homedir6(), ".claude");
12506
+ return join25(binding.launch.env.CODEX_HOME ?? join25(homedir6(), ".codex"), "sessions");
12507
+ const config = binding.launch.env.CLAUDE_CONFIG_DIR ?? join25(homedir6(), ".claude");
11699
12508
  const slug = binding.projectRoot.replace(/[^a-zA-Z0-9]/g, "-");
11700
- return join24(config, "projects", slug, `${binding.sessionId}.jsonl`);
12509
+ return join25(config, "projects", slug, `${binding.sessionId}.jsonl`);
11701
12510
  }
11702
12511
  function recordedFileState(file) {
11703
12512
  try {
@@ -11710,7 +12519,7 @@ var init_recovery_session_file = () => {};
11710
12519
 
11711
12520
  // src/adapters/definitions/native-recovery.ts
11712
12521
  import { readdirSync as readdirSync4 } from "fs";
11713
- import { basename as basename5, join as join25 } from "path";
12522
+ import { basename as basename5, join as join26 } from "path";
11714
12523
  var rolloutState = (binding) => {
11715
12524
  try {
11716
12525
  return readdirSync4(checkedSessionPath(binding, "rollout"), { recursive: true }).some((name) => String(name).endsWith(".jsonl") && basename5(String(name)).includes(binding.sessionId));
@@ -11734,7 +12543,7 @@ var init_native_recovery = __esm(() => {
11734
12543
  if (phase !== "plan")
11735
12544
  return false;
11736
12545
  let current, started = false, turned = false;
11737
- for (const e of readEvents(join25(stateDir, "events.jsonl"))) {
12546
+ for (const e of readEvents(join26(stateDir, "events.jsonl"))) {
11738
12547
  if (e.type === "native_thread" && e.peer === binding.peer) {
11739
12548
  current = e.thread;
11740
12549
  if (e.thread === binding.sessionId && e.fresh)
@@ -11798,7 +12607,12 @@ var init_native_recovery = __esm(() => {
11798
12607
  };
11799
12608
  QWEN_RECOVERY_SESSION = {
11800
12609
  legacyTargetSupport: { file: "src/hub/peer-catalog.ts", exportName: "OPTIONAL_PEERS", refusal: "target does not support Qwen; end Qwen and make a new plan, or choose a supporting target; runtimes unchanged" },
11801
- restoreArgs: (peer) => ({ ...startupArgs(peer), ...peer.sessionId ? { "session-id": peer.sessionId } : {} })
12610
+ restoreArgs: (peer) => ({ ...startupArgs(peer), ...peer.sessionId ? { "session-id": peer.sessionId } : {} }),
12611
+ recoveryRefusal: (peer, phase = "plan") => {
12612
+ if (peer.args?.mode !== "tui")
12613
+ return;
12614
+ return phase === "restore" ? "Qwen's own terminal cannot be restored as a headless worker; end it in that terminal and make a new recovery plan" : "qwen: its own terminal has no automatic restore; end it in that terminal, then make a new recovery plan (restart it with ahub qwen --mode tui, which resumes its session)";
12615
+ }
11802
12616
  };
11803
12617
  });
11804
12618
 
@@ -12271,9 +13085,15 @@ var init_builtin_metadata = __esm(() => {
12271
13085
  });
12272
13086
 
12273
13087
  // src/adapters/definitions/native-cli-entry-qwen.ts
13088
+ function verifiedLaunch(value) {
13089
+ const launch = value;
13090
+ if (!launch || typeof launch.cmd !== "string" || !launch.cmd || !Array.isArray(launch.args) || !launch.args.every((word) => typeof word === "string") || !launch.env || typeof launch.env !== "object" || Array.isArray(launch.env))
13091
+ return;
13092
+ return { cmd: launch.cmd, args: launch.args, env: launch.env };
13093
+ }
12274
13094
  async function runQwenEntry(ctx) {
12275
13095
  const peer = ctx.instance ?? "qwen";
12276
- const { args, cwd, stateDir, unattendedEnv, connect, fail, takeFlags } = ctx;
13096
+ const { args, cwd, stateDir, unattendedEnv, connect, fail, takeFlags, execWithEnv } = ctx;
12277
13097
  const { launcherPreview } = ctx.helpers;
12278
13098
  if (args.includes("--print-command") || args.includes("--dry-run")) {
12279
13099
  try {
@@ -12284,28 +13104,55 @@ async function runQwenEntry(ctx) {
12284
13104
  }
12285
13105
  const { one, rest } = takeFlags(args, ["--mode", "--model", "--session-id"], []);
12286
13106
  const adapter = ctx.definition;
13107
+ const usage = "usage: ahub qwen [--mode headless|tui] [--model <alias>] [--session-id <id>]";
12287
13108
  if (one["--session-id"] !== undefined && !adapter.session.crashIdentity?.({ sessionId: one["--session-id"] }).sessionId)
12288
13109
  fail("invalid Qwen session identity");
12289
- if (rest.length || one["--mode"] !== undefined && !adapter.start.forms.includes(one["--mode"]))
12290
- fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
13110
+ if (rest.length || one["--mode"] !== undefined && !["headless", "tui"].includes(one["--mode"]))
13111
+ fail(usage);
13112
+ const mode = one["--mode"] === "tui" ? "tui" : "headless";
13113
+ if (mode === "tui" && ctx.instance)
13114
+ fail("extra Qwen instances require headless mode");
13115
+ if (mode === "tui" && !(process.stdin.isTTY && process.stdout.isTTY))
13116
+ fail("Qwen's TUI needs a terminal: run ahub qwen --mode tui in one, or use --mode headless");
12291
13117
  const hub = await connect();
13118
+ let launch;
12292
13119
  try {
12293
13120
  const snapshot = (await hub.request({ t: "status" })).status;
12294
13121
  const warning = retainedLaunchWarning(snapshot, peer, ctx.definition);
12295
13122
  if (warning)
12296
13123
  console.error(warning);
12297
- const reply = await hub.request({ t: "start", peer, args: { mode: "headless", ...one["--model"] ? { model: one["--model"] } : {}, ...one["--session-id"] ? { sessionId: one["--session-id"] } : {} }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35000);
13124
+ const reply = await hub.request({ t: "start", peer, args: { mode, ...one["--model"] ? { model: one["--model"] } : {}, ...one["--session-id"] ? { sessionId: one["--session-id"] } : {} }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35000);
12298
13125
  if (!reply.ok)
12299
13126
  fail(reply.error);
12300
- console.log(reply.already ? `${peer} is already attached` : `${peer} attached (headless); use ahub say @${peer} and the console for approvals`);
13127
+ if (mode === "tui") {
13128
+ if (reply.already)
13129
+ return console.log("qwen is already attached (its TUI is running); use that terminal");
13130
+ if (reply.adopted)
13131
+ return console.log("qwen: the hub read on from its saved position; the Qwen terminal it was driving is still running");
13132
+ launch = reply.launch;
13133
+ } else
13134
+ console.log(reply.already ? `${peer} is already attached` : `${peer} attached (headless); use ahub say @${peer} and the console for approvals`);
12301
13135
  } finally {
12302
13136
  hub.close();
12303
13137
  }
13138
+ if (mode !== "tui")
13139
+ return;
13140
+ const verified = verifiedLaunch(launch);
13141
+ if (!verified)
13142
+ return fail("Qwen TUI launch metadata was not verified");
13143
+ const person = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("AGENTHUB_") || name === "AGENTHUB_HOME"));
13144
+ const env = { ...peerChildEnv(peer, person), ...verified.env };
13145
+ delete env.AGENTHUB_RECOVERY_OPERATION;
13146
+ delete env.AGENTHUB_UNATTENDED;
13147
+ console.error("Qwen runs in this terminal, driven by the hub through its dual-output files. Limits: a hub deny ends the turn without an answer, and a stalled turn is stopped only by pressing Esc here.");
13148
+ return execWithEnv(verified.cmd, verified.args, env);
12304
13149
  }
12305
- var init_native_cli_entry_qwen = () => {};
13150
+ var init_native_cli_entry_qwen = __esm(() => {
13151
+ init_child_process();
13152
+ });
12306
13153
 
12307
13154
  // src/adapters/definitions/native-cli-preview-qwen.ts
12308
- import { join as join26 } from "path";
13155
+ import { join as join27 } from "path";
12309
13156
  function previewQwen(ctx) {
12310
13157
  const { tool, args, cwd, stateDir, cliDir, config } = ctx;
12311
13158
  const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
@@ -12313,8 +13160,8 @@ function previewQwen(ctx) {
12313
13160
  let launch;
12314
13161
  const mode = value("--mode") ?? "headless", model = value("--model");
12315
13162
  const adapter = ctx.definition;
12316
- if (!adapter.start.forms.includes(mode))
12317
- throw new Error("Qwen TUI is not offered before verification");
13163
+ if (!["headless", "tui"].includes(mode))
13164
+ throw new Error("unknown Qwen mode");
12318
13165
  for (let i = 0;i < args.length; i += 2)
12319
13166
  if (!["--model", "--mode", "--session-id"].includes(args[i]) || !args[i + 1])
12320
13167
  throw new Error("unknown Qwen option");
@@ -12323,12 +13170,19 @@ function previewQwen(ctx) {
12323
13170
  throw new Error("invalid Qwen session identity");
12324
13171
  if (sessionId)
12325
13172
  unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
13173
+ if (mode === "tui") {
13174
+ const [bin = "qwen", ...prefix] = nativeCommand(config.qwen_cmd, ["qwen"]).filter((word) => word !== "--acp");
13175
+ launch = { cmd: bin, args: [...prefix, "--input-file", "<state dir>/qwen/qwen-dual/input.jsonl", "--json-file", "<state dir>/qwen/qwen-dual/events.jsonl", "--mcp-config", "<state dir>/qwen/qwen-dual/mcp.json", "--approval-mode", "default", ...model ? ["--model", REDACTED] : [], ...sessionId ? ["-r", REDACTED] : []] };
13176
+ unresolved.push({ field: "files", reason: "the daemon prepares the input and event files (0600) when the start is admitted; preview creates nothing" });
13177
+ unresolved.push({ field: "nativeIdentity", reason: `Qwen Code (${describe("qwen-dual")}) is checked before the start and again from its session_start event; preview starts nothing` });
13178
+ return finishPreview(ctx, launch, envNames, unresolved, undefined);
13179
+ }
12326
13180
  launch = adapter.start.launch({
12327
13181
  instance: tool,
12328
13182
  form: "headless",
12329
13183
  projectRoot: cwd,
12330
13184
  stateDir,
12331
- entrypoint: join26(cliDir, "main.js"),
13185
+ entrypoint: join27(cliDir, "main.js"),
12332
13186
  userArgs: [],
12333
13187
  unattended: false,
12334
13188
  env: {},
@@ -12353,6 +13207,7 @@ var init_qwen = __esm(() => {
12353
13207
  init_native_recovery();
12354
13208
  init_acp();
12355
13209
  init_qwen_native();
13210
+ init_native_factories();
12356
13211
  QWEN_DEFINITION = {
12357
13212
  api: 1,
12358
13213
  kind: "qwen",
@@ -12371,10 +13226,11 @@ var init_qwen = __esm(() => {
12371
13226
  conductorStartable: true,
12372
13227
  hubSwitches: [],
12373
13228
  ownedFlags: [],
12374
- preflight: (request) => request.form !== undefined && request.form !== "headless" ? "Qwen TUI is not offered before its native verification; use ahub qwen --mode headless" : undefined,
13229
+ preflight: (request) => request.form !== undefined && request.form !== "headless" && request.form !== "tui" ? "unknown Qwen mode; usage: ahub qwen [--mode headless|tui]" : undefined,
13230
+ createTui: createQwenDual,
12375
13231
  launch: (context) => {
12376
13232
  if (context.form !== "headless")
12377
- throw new Error("Qwen TUI is not offered before its native verification; use ahub qwen --mode headless");
13233
+ throw new Error("the Qwen TUI is launched from the files the daemon prepares; use ahub qwen --mode tui");
12378
13234
  const launch = buildQwenLaunch(context.nativeCommand ?? ["qwen", "--acp"], context.model);
12379
13235
  const argv = [launch.cmd, ...launch.args];
12380
13236
  return { ...launch, metadata: { packageEntrypoint: context.entrypoint, argv, command: argv.map(shellQuote3).join(" "), env: {} } };
@@ -12414,7 +13270,7 @@ var init_qwen = __esm(() => {
12414
13270
  turnEnd: "adapter",
12415
13271
  budget: { account: "qwen" },
12416
13272
  capabilities: { watch: true },
12417
- cli: { command: "qwen", entry: { lifecycleAdmission: true, execute: runQwenEntry }, preview: previewQwen, usage: [["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", `start optional Qwen Code ACP (${describe("qwen")}; roster.qwen, default off)`]], launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
13273
+ cli: { command: "qwen", entry: { lifecycleAdmission: true, execute: runQwenEntry }, preview: previewQwen, usage: [["ahub qwen [--mode headless|tui] [--model <alias>] [--session-id <id>] [--print-command]", `start optional Qwen Code ACP (${describe("qwen")}; roster.qwen, default off); --mode tui runs Qwen's own TUI in this terminal, driven through its dual-output files (${describe("qwen-dual")}); a hub deny ends the turn without an answer and a stalled turn is stopped with Esc in the Qwen terminal`]], launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
12418
13274
  display: QWEN_DISPLAY,
12419
13275
  hooks: { recallPlatform: "qwen" }
12420
13276
  };
@@ -12611,6 +13467,9 @@ function immutable(value, seen = new WeakSet) {
12611
13467
  immutable(member, seen);
12612
13468
  return Object.freeze(value);
12613
13469
  }
13470
+ function isFixedBuiltinDefinition(definition) {
13471
+ return definition !== undefined && fixedBuiltinDefinitions.has(definition);
13472
+ }
12614
13473
  function deriveAdapterLists(definitions) {
12615
13474
  const ordered = (field, eligible = (_definition) => true) => definitions.map((definition, index) => ({ definition, index })).filter(({ definition }) => eligible(definition)).sort((left, right) => {
12616
13475
  const a = left.definition.projection?.[field], b = right.definition.projection?.[field];
@@ -12780,7 +13639,7 @@ function assertDefinitionCoherence(definition) {
12780
13639
  fail("reviewer", "unavailable reviewer must remain metadata-only");
12781
13640
  }
12782
13641
  function assertNativeCapabilityScope(definition) {
12783
- if (fixedBuiltinDefinitions.has(definition))
13642
+ if (isFixedBuiltinDefinition(definition))
12784
13643
  return;
12785
13644
  const declared = (block, key) => block !== undefined && (key in block);
12786
13645
  const restricted = [
@@ -12788,7 +13647,10 @@ function assertNativeCapabilityScope(definition) {
12788
13647
  ["attach.admission", declared(definition.attach, "admission")],
12789
13648
  ["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
12790
13649
  ["session.crashIdentity", declared(definition.session, "crashIdentity")],
13650
+ ["session.nativeRevert", declared(definition.session, "nativeRevert")],
12791
13651
  ["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
13652
+ ["start.daemonPort qwen-dual", definition.start.daemonPort === "qwen-dual"],
13653
+ ["start.createTui", declared(definition.start, "createTui")],
12792
13654
  ["routing", definition.routing !== undefined],
12793
13655
  ["projection", definition.projection !== undefined],
12794
13656
  ["instances.nativeHome", declared(definition.instances, "nativeHome")],
@@ -20192,7 +21054,7 @@ init_project();
20192
21054
  // src/hub/instance-config.ts
20193
21055
  init_registry();
20194
21056
  import { readFileSync as readFileSync15 } from "fs";
20195
- import { join as join29 } from "path";
21057
+ import { join as join30 } from "path";
20196
21058
 
20197
21059
  // src/adapters/external-metadata.ts
20198
21060
  import { types as types3 } from "util";
@@ -20269,8 +21131,8 @@ init_registry();
20269
21131
  // src/hub/config-trust.ts
20270
21132
  init_child_process();
20271
21133
  init_git();
20272
- import { lstatSync as lstatSync12, statSync as statSync5 } from "fs";
20273
- import { dirname as dirname12, join as join27 } from "path";
21134
+ import { lstatSync as lstatSync13, statSync as statSync5 } from "fs";
21135
+ import { dirname as dirname12, join as join28 } from "path";
20274
21136
  var MACHINE_LOCAL = [
20275
21137
  "kimi_cmd",
20276
21138
  "permission_modes",
@@ -20302,7 +21164,7 @@ function configTracked(cwd, name) {
20302
21164
  return;
20303
21165
  let opened;
20304
21166
  try {
20305
- opened = statSync5(join27(cwd, ".agenthub", name));
21167
+ opened = statSync5(join28(cwd, ".agenthub", name));
20306
21168
  } catch {}
20307
21169
  return r.stdout.split("\x00").filter(Boolean).some((row) => {
20308
21170
  const path = row.slice(row.indexOf("\t") + 1);
@@ -20311,7 +21173,7 @@ function configTracked(cwd, name) {
20311
21173
  if (!opened)
20312
21174
  return path === `.agenthub/${name}`;
20313
21175
  try {
20314
- const s = statSync5(join27(cwd, path));
21176
+ const s = statSync5(join28(cwd, path));
20315
21177
  return !!opened && s.dev === opened.dev && s.ino === opened.ino;
20316
21178
  } catch {
20317
21179
  return false;
@@ -20368,7 +21230,7 @@ function readAdapterModuleDeclarations(cwd, layers) {
20368
21230
  // src/hub/control-client.ts
20369
21231
  init_project();
20370
21232
  import { readFileSync as readFileSync14 } from "fs";
20371
- import { join as join28, resolve as resolve17 } from "path";
21233
+ import { join as join29, resolve as resolve17 } from "path";
20372
21234
  function stateDirFor(cwd) {
20373
21235
  return projectContext(cwd).stateDir;
20374
21236
  }
@@ -20376,8 +21238,8 @@ var PROTOCOL = 16;
20376
21238
  var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL];
20377
21239
  function readControl(stateDir) {
20378
21240
  try {
20379
- const status = JSON.parse(readFileSync14(join28(stateDir, "status.json"), "utf8"));
20380
- const token = readFileSync14(join28(stateDir, "control-token"), "utf8").trim();
21241
+ const status = JSON.parse(readFileSync14(join29(stateDir, "status.json"), "utf8"));
21242
+ const token = readFileSync14(join29(stateDir, "control-token"), "utf8").trim();
20381
21243
  if (!Number.isInteger(status.controlPort) || status.controlPort < 1 || status.controlPort > 65535 || !token)
20382
21244
  return;
20383
21245
  const startModes = status.startModes && typeof status.startModes === "object" && !Array.isArray(status.startModes) ? Object.fromEntries(Object.entries(status.startModes).filter((entry) => entry[1] === "tui" || entry[1] === "headless")) : undefined;
@@ -20781,7 +21643,7 @@ init_sdlc_metrics();
20781
21643
  // package.json
20782
21644
  var package_default = {
20783
21645
  name: "@staix/agent-hub",
20784
- version: "0.16.0",
21646
+ version: "0.17.0",
20785
21647
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
20786
21648
  license: "MIT",
20787
21649
  type: "module",
@@ -20870,13 +21732,13 @@ init_workflow();
20870
21732
  init_deliverables();
20871
21733
 
20872
21734
  // src/hub/deliverable-files.ts
20873
- import { constants as constants7, lstatSync as lstatSync13 } from "fs";
21735
+ import { constants as constants8, lstatSync as lstatSync14 } from "fs";
20874
21736
  init_tools();
20875
21737
  init_permission_mode();
20876
21738
  init_project();
20877
21739
  init_deliverables();
20878
21740
  var CHUNK = 1024 * 1024;
20879
- var NOFOLLOW = constants7.O_NOFOLLOW ?? 0;
21741
+ var NOFOLLOW = constants8.O_NOFOLLOW ?? 0;
20880
21742
 
20881
21743
  // src/hub/tasks.ts
20882
21744
  init_board();
@@ -21108,7 +21970,7 @@ var INSTANCE_CONFIG_FILES = ["config.json", "config.local.json"];
21108
21970
  function readInstanceConfigLayers(cwd) {
21109
21971
  return INSTANCE_CONFIG_FILES.flatMap((name) => {
21110
21972
  try {
21111
- return [{ name, raw: JSON.parse(readFileSync15(join29(cwd, ".agenthub", name), "utf8")) }];
21973
+ return [{ name, raw: JSON.parse(readFileSync15(join30(cwd, ".agenthub", name), "utf8")) }];
21112
21974
  } catch (error) {
21113
21975
  if (error.code === "ENOENT")
21114
21976
  return [];
@@ -29462,7 +30324,7 @@ class StdioServerTransport {
29462
30324
 
29463
30325
  // src/adapters/claude-channel.ts
29464
30326
  import { readFileSync as readFileSync16 } from "fs";
29465
- import { join as join30 } from "path";
30327
+ import { join as join31 } from "path";
29466
30328
  init_hub_tools();
29467
30329
  init_envelope();
29468
30330
 
@@ -29504,7 +30366,7 @@ var channel = !toolsMode && process.env.AGENTHUB_CHANNEL === "1";
29504
30366
  function roles2() {
29505
30367
  const read = (name) => {
29506
30368
  try {
29507
- return JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8")).roles ?? {};
30369
+ return JSON.parse(readFileSync16(join31(projectRoot2, ".agenthub", name), "utf8")).roles ?? {};
29508
30370
  } catch {
29509
30371
  return {};
29510
30372
  }
@@ -29523,7 +30385,7 @@ function sessionProfile() {
29523
30385
  for (const name of ["config.json", "config.local.json"]) {
29524
30386
  let data;
29525
30387
  try {
29526
- data = JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8"));
30388
+ data = JSON.parse(readFileSync16(join31(projectRoot2, ".agenthub", name), "utf8"));
29527
30389
  } catch (error) {
29528
30390
  if (error.code === "ENOENT")
29529
30391
  continue;
@@ -29550,7 +30412,7 @@ function sessionRoles() {
29550
30412
  const read = (name) => {
29551
30413
  let raw;
29552
30414
  try {
29553
- raw = JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8")).catalog_roles;
30415
+ raw = JSON.parse(readFileSync16(join31(projectRoot2, ".agenthub", name), "utf8")).catalog_roles;
29554
30416
  } catch {
29555
30417
  return {};
29556
30418
  }
@@ -29580,7 +30442,7 @@ var HELD = `another session is attached to the hub as "${peerId}"; this one is s
29580
30442
  var INBOX_CAP = 200;
29581
30443
  function peerHeld() {
29582
30444
  try {
29583
- const status = JSON.parse(readFileSync16(join30(stateDir, "status.json"), "utf8"));
30445
+ const status = JSON.parse(readFileSync16(join31(stateDir, "status.json"), "utf8"));
29584
30446
  const peer = status.peers?.[peerId];
29585
30447
  if (channel && peer?.toolsOnly && !peer.claiming)
29586
30448
  return false;