@khorsheed/dsh-local-agent-codex 0.2.0-rc.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -135,10 +135,12 @@ async function listCodexSessions(homeDir) {
135
135
  * `input_tokens` is the TOTAL input including cache hits (OpenAI-style
136
136
  * accounting, confirmed against `total_tokens` in the rollout token_count),
137
137
  * and `cached_input_tokens` is the cache-read subset — so the uncached
138
- * bucket subtracts the cached portion to avoid double counting. There is no
139
- * cache-write concept, so that bucket is omitted (the shared four-bucket
140
- * contract: uncached input, output, cache read, cache write — codex fills
141
- * the first three).
138
+ * bucket subtracts the cached portion to avoid double counting.
139
+ * `cached_input_tokens` rides every payload (zero included), so
140
+ * `cacheReadTokens` is written whenever the field is present — a reader can
141
+ * tell "no hits" from "not reported". Codex also reports cache creation
142
+ * (`cache_write_input_tokens`), mapped to `cacheWriteTokens` (the shared
143
+ * four-bucket contract: uncached input, output, cache read, cache write).
142
144
  * @param usage - the raw codex usage object (`turn.completed`'s `usage` or a
143
145
  * rollout `token_count` `info.last_token_usage`/`total_token_usage`).
144
146
  * @returns the shared usage record.
@@ -147,12 +149,14 @@ function usageFromCodex(usage) {
147
149
  const raw = usage;
148
150
  const input = Number(raw.input_tokens);
149
151
  const cached = Number(raw.cached_input_tokens);
152
+ const cacheWrite = Number(raw.cache_write_input_tokens);
150
153
  const output = Number(raw.output_tokens);
151
154
  const usageRecord = {
152
- inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
155
+ inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0)) : Number.isFinite(input) ? input : 0,
153
156
  outputTokens: Number.isFinite(output) ? output : 0
154
157
  };
155
- if (Number.isFinite(cached) && cached > 0) usageRecord.cacheReadTokens = cached;
158
+ if (Number.isFinite(cached)) usageRecord.cacheReadTokens = cached;
159
+ if (Number.isFinite(cacheWrite) && cacheWrite > 0) usageRecord.cacheWriteTokens = cacheWrite;
156
160
  return usageRecord;
157
161
  }
158
162
  /**
@@ -740,11 +744,6 @@ const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
740
744
  const RECLAIM_EOF_GRACE_MS = 1e3;
741
745
  /** Default minimum interval between one streaming item's snapshot messages. */
742
746
  const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
743
- /**
744
- * The app-server channel could not come up (spawn failure or handshake
745
- * timeout/mismatch). The provider catches exactly this and falls back to the
746
- * exec one-shot; any other error is a real round failure.
747
- */
748
747
  var LiveChannelUnavailableError = class extends Error {
749
748
  constructor(message) {
750
749
  super(message);
@@ -826,11 +825,14 @@ function codexAppServerItemToLine(item) {
826
825
  case "dynamicToolCall": {
827
826
  const tool = typeof item["tool"] === "string" ? item.tool : "dynamic";
828
827
  const args = item["arguments"] === void 0 || item["arguments"] === null ? void 0 : typeof item["arguments"] === "string" ? item["arguments"] : JSON.stringify(item["arguments"]);
828
+ const content = item["contentItems"];
829
+ const result = Array.isArray(content) ? content.filter((block) => typeof block === "object" && block !== null).map((block) => typeof block["text"] === "string" ? block["text"] : void 0).filter((text) => text !== void 0 && text !== "").join("\n") : void 0;
829
830
  return {
830
831
  kind: "tool",
831
832
  id: itemId ?? `codex-live-${randomUUID()}`,
832
833
  name: tool,
833
- ...args === void 0 || args === "" ? {} : { args }
834
+ ...args === void 0 || args === "" ? {} : { args },
835
+ ...result === void 0 || result === "" ? {} : { result }
834
836
  };
835
837
  }
836
838
  case "collabAgentToolCall": {
@@ -861,12 +863,14 @@ function codexAppServerItemToLine(item) {
861
863
  function usageFromAppServer(breakdown) {
862
864
  const input = Number(breakdown["inputTokens"]);
863
865
  const cached = Number(breakdown["cachedInputTokens"]);
866
+ const cacheWrite = Number(breakdown["cacheWriteInputTokens"]);
864
867
  const output = Number(breakdown["outputTokens"]);
865
868
  const usage = {
866
- inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
869
+ inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0)) : Number.isFinite(input) ? input : 0,
867
870
  outputTokens: Number.isFinite(output) ? output : 0
868
871
  };
869
- if (Number.isFinite(cached) && cached > 0) usage.cacheReadTokens = cached;
872
+ if (Number.isFinite(cached)) usage.cacheReadTokens = cached;
873
+ if (Number.isFinite(cacheWrite) && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
870
874
  return usage;
871
875
  }
872
876
  /**
@@ -1428,27 +1432,31 @@ var CodexLiveDriver = class {
1428
1432
  const lines = [];
1429
1433
  let mirrored = 0;
1430
1434
  let usage;
1431
- /** Whether a folded line already carries the round's usage (no backfill event exists). */
1432
- let usageCarried = false;
1435
+ /** The cumulative turn total the wire's `tokenUsage.total` names, for the settled report. */
1436
+ let totalUsage;
1433
1437
  /**
1434
- * The step ledger: `reservedSteps` holds every step a stream reservation
1435
- * consumed (in increasing order — permanently, whether the stream later
1436
- * completes or stays an orphan snapshot), and a sequential line folds at
1437
- * its index shifted past every reservation before it. A tool completed
1438
- * mid-stream therefore folds ABOVE the stream's step and the projection
1439
- * renders the round's chronological order.
1438
+ * Per-call usage by transcript line: each `thread/tokenUsage/updated`
1439
+ * notification closes one API call (its items have all completed), and
1440
+ * the call's LAST line carries its `tokenUsage.last`. The map is
1441
+ * authoritative per line, independent of the flush's withUsage flag.
1440
1442
  */
1441
- const reservedSteps = [];
1443
+ const usageByLine = /* @__PURE__ */ new Map();
1444
+ /** Whether a folded line already carries usage (per-call, or the round's fallback — no backfill event exists). */
1445
+ let usageCarried = false;
1442
1446
  /**
1443
- * The streaming items seen this round by item id:
1444
- * deltas accumulate into throttled snapshot assistant/messages appended
1445
- * at the item's reserved (turn, step) — the host folds repeated settles
1446
- * at one coordinate into one live-updating chat node, which is the only
1447
- * streaming channel left after 0.1.5 retired the durable per-chunk event.
1448
- * The item's completion folds at the same step and finalizes it; an entry
1449
- * whose completion never arrives is force-finalized at settle.
1447
+ * The streaming items seen this round by item id: deltas accumulate into
1448
+ * throttled snapshots at the item's step once it may open in the log
1449
+ * (previewed live before that) — the host folds repeated settles at one
1450
+ * coordinate into one live-updating chat node. The item's completion
1451
+ * folds at the same step and finalizes it; an entry whose completion
1452
+ * never arrives is force-finalized at settle. A tool that completes
1453
+ * mid-stream folds after the stream, keeping the round's chronology.
1450
1454
  */
1451
1455
  const streams = /* @__PURE__ */ new Map();
1456
+ /** Provisional stream keys (live flush and preview identity), never log steps. */
1457
+ let streamKeys = 0;
1458
+ /** Line indexes already written (a line may fold ahead of an earlier one it chronologically precedes). */
1459
+ const folded = /* @__PURE__ */ new Set();
1452
1460
  /** The item deltas currently accumulate into (items stream sequentially). */
1453
1461
  let activeStream;
1454
1462
  /** Items/completions that arrived before the turn id was known. */
@@ -1492,6 +1500,27 @@ var CodexLiveDriver = class {
1492
1500
  */
1493
1501
  const streamPublisher = localAgent?.liveStreams === void 0 ? void 0 : new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, (error) => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
1494
1502
  const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
1503
+ let lastStep = 0;
1504
+ let openStep;
1505
+ const openStreamStep = (stream) => {
1506
+ lastStep += 1;
1507
+ childSession.append("step/start", {
1508
+ turn,
1509
+ step: lastStep
1510
+ });
1511
+ stream.durable = lastStep;
1512
+ stream.opened = true;
1513
+ openStep = lastStep;
1514
+ };
1515
+ const closeOpenStep = () => {
1516
+ if (openStep === void 0) return;
1517
+ childSession.append("step/end", {
1518
+ turn,
1519
+ step: openStep
1520
+ });
1521
+ openStep = void 0;
1522
+ };
1523
+ const canOpenStream = (stream) => openStep === void 0 && mirrored >= stream.after && streams.keys().next().value === stream.itemId;
1495
1524
  const pendingStreamArrival = /* @__PURE__ */ new Map();
1496
1525
  const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
1497
1526
  if (stream.text.trim() === "") return;
@@ -1504,30 +1533,29 @@ var CodexLiveDriver = class {
1504
1533
  liveFlush.cancel(stream.step);
1505
1534
  const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
1506
1535
  pendingStreamArrival.delete(stream.step);
1507
- if (streamPublisher !== void 0 && !final) {
1508
- if (!stream.opened) {
1509
- childSession.append("step/start", {
1510
- turn,
1511
- step: stream.step
1536
+ if (!stream.opened) {
1537
+ if (canOpenStream(stream)) openStreamStep(stream);
1538
+ else if (!final) {
1539
+ streamPublisher?.preview(stream.step, stream.kind, stream.text, {
1540
+ itemId: stream.itemId,
1541
+ receivedAt
1512
1542
  });
1513
- stream.opened = true;
1543
+ return;
1544
+ } else {
1545
+ closeOpenStep();
1546
+ openStreamStep(stream);
1514
1547
  }
1515
- streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
1548
+ }
1549
+ if (streamPublisher !== void 0 && !final) {
1550
+ streamPublisher.update(stream.durable, stream.kind, stream.text, { receivedAt });
1516
1551
  stream.lastSnapshotAt = now;
1517
1552
  stream.lastSnapshotLen = stream.text.length;
1518
1553
  return;
1519
1554
  }
1520
- if (!stream.opened) {
1521
- childSession.append("step/start", {
1522
- turn,
1523
- step: stream.step
1524
- });
1525
- stream.opened = true;
1526
- }
1527
- const attachUsage = withUsage && !usageCarried && usage !== void 0;
1555
+ const attachUsage = withUsage && !usageCarried && usageByLine.size === 0 && usage !== void 0;
1528
1556
  childSession.append("assistant/message", {
1529
1557
  turn,
1530
- step: stream.step,
1558
+ step: stream.durable,
1531
1559
  message: codexAssistantEvent([stream.kind === "think" ? {
1532
1560
  type: "reasoning",
1533
1561
  text: stream.text
@@ -1536,7 +1564,7 @@ var CodexLiveDriver = class {
1536
1564
  text: stream.text
1537
1565
  }]),
1538
1566
  stream: [],
1539
- ...withUsage && !usageCarried && usage !== void 0 ? { usage } : {},
1567
+ ...withUsage && !usageCarried && usageByLine.size === 0 && usage !== void 0 ? { usage } : {},
1540
1568
  ...interrupted ? { interrupted: true } : {}
1541
1569
  }, { surfaceOp: "append" });
1542
1570
  if (attachUsage) usageCarried = true;
@@ -1544,11 +1572,19 @@ var CodexLiveDriver = class {
1544
1572
  stream.lastSnapshotLen = stream.text.length;
1545
1573
  persist();
1546
1574
  };
1575
+ /** Close a stream's live presentation once its final text is in the log. */
1576
+ const finishStream = (stream) => {
1577
+ liveFlush.cancel(stream.step);
1578
+ streams.delete(stream.itemId);
1579
+ if (activeStream === stream.itemId) activeStream = void 0;
1580
+ streamPublisher?.finish(stream.step);
1581
+ if (stream.durable !== void 0) streamPublisher?.finish(stream.durable);
1582
+ };
1547
1583
  /**
1548
- * Reserve the step one new streaming item will occupy — past every
1549
- * completed line (folded or held back) and every earlier reservation. A
1550
- * different item's deltas force one final snapshot of the current stream
1551
- * (its completion still folds at its own reserved step when it lands).
1584
+ * Track one new streaming item: its provisional key (live flush and
1585
+ * preview identity) and how many lines had completed when it began. A
1586
+ * different item's deltas force one freshness snapshot of the current
1587
+ * stream.
1552
1588
  */
1553
1589
  const reserveStream = (itemId, kind) => {
1554
1590
  if (activeStream !== void 0 && activeStream !== itemId) {
@@ -1558,12 +1594,12 @@ var CodexLiveDriver = class {
1558
1594
  }
1559
1595
  let stream = streams.get(itemId);
1560
1596
  if (stream === void 0) {
1561
- const step = lines.length + reservedSteps.length + 1;
1562
- reservedSteps.push(step);
1597
+ streamKeys += 1;
1563
1598
  stream = {
1564
1599
  itemId,
1565
1600
  kind,
1566
- step,
1601
+ step: streamKeys,
1602
+ after: lines.length,
1567
1603
  text: "",
1568
1604
  lastSnapshotAt: 0,
1569
1605
  lastSnapshotLen: 0,
@@ -1573,60 +1609,78 @@ var CodexLiveDriver = class {
1573
1609
  }
1574
1610
  activeStream = itemId;
1575
1611
  };
1576
- /** The sequential fold step for `lines[index]`: its position shifted past every reservation before it. */
1577
- const foldStep = (index) => {
1578
- let step = index + 1;
1579
- for (const reserved of reservedSteps) if (reserved <= step) step += 1;
1580
- else break;
1581
- return step;
1612
+ /** The stream a completed line finalizes, if it was streamed. */
1613
+ const streamOfLine = (line) => line === void 0 || line.kind === "tool" ? void 0 : streams.get(line.itemId ?? "");
1614
+ /** Write lines[index] at the next step(s). */
1615
+ const foldLine = (index, lineUsage) => {
1616
+ const line = lines[index];
1617
+ const stream = streamOfLine(line);
1618
+ if (stream !== void 0) {
1619
+ if (!stream.opened) {
1620
+ closeOpenStep();
1621
+ openStreamStep(stream);
1622
+ }
1623
+ const blocks = stream.kind === "think" ? [{
1624
+ type: "reasoning",
1625
+ text: line.kind === "tool" ? "" : line.text
1626
+ }] : [{
1627
+ type: "text",
1628
+ text: line.kind === "tool" ? "" : line.text
1629
+ }];
1630
+ childSession.append("assistant/message", {
1631
+ turn,
1632
+ step: stream.durable,
1633
+ message: codexAssistantEvent(blocks),
1634
+ stream: [],
1635
+ ...lineUsage === void 0 ? {} : { usage: lineUsage }
1636
+ }, { surfaceOp: "append" });
1637
+ finishStream(stream);
1638
+ closeOpenStep();
1639
+ } else {
1640
+ closeOpenStep();
1641
+ lastStep += 1;
1642
+ appendCodexTranscriptLine(childSession, turn, lastStep, line, lineUsage);
1643
+ }
1644
+ folded.add(index);
1645
+ localAgent?.reportRunProgress(childSession.id, {
1646
+ kind: "delta",
1647
+ text: codexLineText(line)
1648
+ });
1582
1649
  };
1583
- /** Mirror folded lines [mirrored, upto); the last line is held back until completion. */
1650
+ /**
1651
+ * Mirror the completed lines before `upto` in chronological order; the
1652
+ * last line is held back until completion. A line waits behind every
1653
+ * stream that began before it completed: that stream's line folds first,
1654
+ * or — while it is still streaming — the mirror stops until it lands.
1655
+ */
1584
1656
  const mirrorUpTo = (upto, withUsage) => {
1585
1657
  let usageIndex = -1;
1586
- if (withUsage) {
1658
+ if (withUsage && usageByLine.size === 0) {
1587
1659
  for (let index = 0; index < lines.length; index += 1) if (lines[index]?.kind !== "tool") usageIndex = index;
1588
1660
  }
1589
- for (let index = mirrored; index < upto; index += 1) {
1590
- const line = lines[index];
1591
- if (line === void 0) continue;
1661
+ const usageFor = (index) => {
1662
+ const own = usageByLine.get(index);
1663
+ if (own !== void 0) {
1664
+ usageCarried = true;
1665
+ return own;
1666
+ }
1592
1667
  const lineUsage = withUsage && index === usageIndex ? usage : void 0;
1593
1668
  if (lineUsage !== void 0) usageCarried = true;
1594
- const itemId = line.kind === "tool" ? void 0 : line.itemId;
1595
- const stream = itemId === void 0 ? void 0 : streams.get(itemId);
1596
- if (itemId !== void 0 && stream !== void 0) {
1597
- liveFlush.cancel(stream.step);
1598
- streams.delete(itemId);
1599
- if (activeStream === itemId) activeStream = void 0;
1600
- if (!stream.opened) childSession.append("step/start", {
1601
- turn,
1602
- step: stream.step
1603
- });
1604
- const blocks = stream.kind === "think" ? [{
1605
- type: "reasoning",
1606
- text: line.kind === "tool" ? "" : line.text
1607
- }] : [{
1608
- type: "text",
1609
- text: line.kind === "tool" ? "" : line.text
1610
- }];
1611
- childSession.append("assistant/message", {
1612
- turn,
1613
- step: stream.step,
1614
- message: codexAssistantEvent(blocks),
1615
- stream: [],
1616
- ...lineUsage === void 0 ? {} : { usage: lineUsage }
1617
- }, { surfaceOp: "append" });
1618
- streamPublisher?.finish(stream.step);
1619
- childSession.append("step/end", {
1620
- turn,
1621
- step: stream.step
1622
- });
1623
- } else appendCodexTranscriptLine(childSession, turn, foldStep(index), line, lineUsage);
1624
- mirrored = index + 1;
1625
- localAgent?.reportRunProgress(childSession.id, {
1626
- kind: "delta",
1627
- text: codexLineText(line)
1628
- });
1669
+ return lineUsage;
1670
+ };
1671
+ outer: for (let index = mirrored; index < upto; index += 1) {
1672
+ if (folded.has(index)) continue;
1673
+ const own = streamOfLine(lines[index]);
1674
+ const all = [...streams.values()];
1675
+ const before = own === void 0 ? all.filter((stream) => stream.after <= index) : all.slice(0, all.indexOf(own));
1676
+ for (const earlier of before) {
1677
+ const at = lines.findIndex((line, position) => position > index && position < upto && !folded.has(position) && streamOfLine(line) === earlier);
1678
+ if (at < 0) break outer;
1679
+ foldLine(at, usageFor(at));
1680
+ }
1681
+ foldLine(index, usageFor(index));
1629
1682
  }
1683
+ while (folded.has(mirrored)) mirrored += 1;
1630
1684
  if (upto > 0) persist();
1631
1685
  };
1632
1686
  let resolveCompletion;
@@ -1635,6 +1689,11 @@ var CodexLiveDriver = class {
1635
1689
  });
1636
1690
  const openTurn = () => {
1637
1691
  childSession.append("turn/start", { turn });
1692
+ if (spec.configuration?.model !== void 0) appendCodexRequestHeader(childSession, {
1693
+ provider: "codex-local",
1694
+ model: spec.configuration.model,
1695
+ ...spec.configuration.effort === void 0 ? {} : { reasoningEffort: spec.configuration.effort }
1696
+ });
1638
1697
  childSession.append("user/message", createUserMessage({
1639
1698
  content: [{
1640
1699
  type: "text",
@@ -1685,17 +1744,29 @@ var CodexLiveDriver = class {
1685
1744
  return;
1686
1745
  }
1687
1746
  if (method === "thread/tokenUsage/updated") {
1688
- const last = params["tokenUsage"]?.["last"];
1689
- if (last !== void 0) usage = usageFromAppServer(last);
1747
+ const tokenUsage = params["tokenUsage"];
1748
+ const last = tokenUsage?.["last"];
1749
+ const total = tokenUsage?.["total"];
1750
+ if (total !== void 0 && (Number.isFinite(Number(total["inputTokens"])) || Number.isFinite(Number(total["outputTokens"])))) totalUsage = usageFromAppServer(total);
1751
+ if (last !== void 0) {
1752
+ usage = usageFromAppServer(last);
1753
+ if (lines.length > 0) {
1754
+ usageByLine.set(lines.length - 1, usage);
1755
+ mirrorUpTo(lines.length - 1, true);
1756
+ }
1757
+ }
1690
1758
  return;
1691
1759
  }
1692
1760
  if (method === "turn/completed") {
1693
1761
  const turnData = params["turn"];
1694
- mirrorUpTo(lines.length, true);
1695
- resolveCompletion({
1696
- status: String(turnData["status"]),
1697
- ...turnData["error"] === null || turnData["error"] === void 0 ? {} : { error: turnData["error"] }
1698
- });
1762
+ try {
1763
+ mirrorUpTo(lines.length, true);
1764
+ } finally {
1765
+ resolveCompletion({
1766
+ status: String(turnData["status"]),
1767
+ ...turnData["error"] === null || turnData["error"] === void 0 ? {} : { error: turnData["error"] }
1768
+ });
1769
+ }
1699
1770
  }
1700
1771
  };
1701
1772
  /** Route one wire notification: round-scoped, turn-id tagged, early ones buffered. */
@@ -1795,15 +1866,19 @@ var CodexLiveDriver = class {
1795
1866
  liveFlush.dispose();
1796
1867
  if (turnOpened) {
1797
1868
  if (mirrored < lines.length) mirrorUpTo(lines.length, true);
1869
+ while (mirrored < lines.length && streams.size > 0) {
1870
+ const oldest = streams.values().next().value;
1871
+ appendStreamSnapshot(oldest, true, settled.stopReason !== "completed", false, true);
1872
+ finishStream(oldest);
1873
+ closeOpenStep();
1874
+ mirrorUpTo(lines.length, true);
1875
+ }
1798
1876
  if (streams.size > 0) {
1799
1877
  const remaining = [...streams.values()];
1800
1878
  for (const [position, stream] of remaining.entries()) {
1801
1879
  appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
1802
- streamPublisher?.finish(stream.step);
1803
- if (stream.opened) childSession.append("step/end", {
1804
- turn,
1805
- step: stream.step
1806
- });
1880
+ finishStream(stream);
1881
+ closeOpenStep();
1807
1882
  }
1808
1883
  streams.clear();
1809
1884
  activeStream = void 0;
@@ -1854,7 +1929,7 @@ var CodexLiveDriver = class {
1854
1929
  windowStart: startedAtMs,
1855
1930
  cwd: spec.cwd
1856
1931
  });
1857
- const settledUsage = usage ?? facts.usage;
1932
+ const settledUsage = totalUsage ?? usage ?? facts.usage;
1858
1933
  const round = {
1859
1934
  ...facts.model === void 0 ? {} : { observedModel: facts.model },
1860
1935
  ...facts.cliVersion === void 0 ? {} : { cliVersion: facts.cliVersion },
@@ -2046,23 +2121,6 @@ async function listCodexConfigModels(homeDir) {
2046
2121
  }
2047
2122
  return found;
2048
2123
  }
2049
- //#endregion
2050
- //#region lib/types/codex-cli-provider.js
2051
- /**
2052
- * One-shot Codex CLI subagent lifecycle: spawn `codex exec "<task>"` through
2053
- * the subprocess seam under the harness's scoped home, capture its printed
2054
- * response as the run output, and dispose to whole-tree quiescence. Mirrors
2055
- * the kimi-cli provider: every accepted run is a fresh process and a fresh
2056
- * codex session; there is no continuation across runs.
2057
- *
2058
- * The provider name `codex-local` avoids colliding with the official
2059
- * `subagent-codex` package's provider name `codex` — a composition that
2060
- * mounts both would fail loud with DUPLICATE_PROVIDER. The tool row in this
2061
- * bundle's patch takes the official model-facing name `subagent_codex`
2062
- * instead: the official preset row ships disabled, and the patch disables
2063
- * it too (a deliberate user re-enable conflicts loud, by design).
2064
- * @module @khorsheed/dsh-local-agent-codex/codex-cli-provider
2065
- */
2066
2124
  /** Quote one TOML basic string for the `-c` config override. */
2067
2125
  function tomlString(value) {
2068
2126
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
@@ -2086,7 +2144,7 @@ function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
2086
2144
  });
2087
2145
  const serverName = `dsh-member-${token.slice(0, 8)}`;
2088
2146
  const bridge = registry.memberBridgeCommand();
2089
- const configOverride = `mcp_servers.${serverName}={command=${tomlString(bridge.command)},args=[${bridge.args.map(tomlString).join(",")}],env={${MEMBER_BRIDGE_SOCKET_ENV}=${tomlString(registry.memberBridgeSocketPath())},${MEMBER_BRIDGE_TOKEN_ENV}=${tomlString(token)}},default_tools_approval_mode=${tomlString("approve")}}`;
2147
+ const configOverride = `mcp_servers.${serverName}={command=${tomlString(bridge.command)},args=[${bridge.args.map(tomlString).join(",")}],env={${MEMBER_BRIDGE_SOCKET_ENV}=${tomlString(registry.memberBridgeSocketPath())},${MEMBER_BRIDGE_TOKEN_ENV}=${tomlString(token)}},default_tools_approval_mode=${tomlString("approve")},tool_timeout_sec=1800}`;
2090
2148
  let released = false;
2091
2149
  return {
2092
2150
  token,
@@ -2557,6 +2615,23 @@ function foldCodexStreamLine(state, raw) {
2557
2615
  result: last.result === void 0 ? output : `${last.result}\n${output}`
2558
2616
  };
2559
2617
  }
2618
+ } else if (item.type === "custom_tool_call") {
2619
+ countToolCall(state, "custom_tool_call");
2620
+ state.lines.push({
2621
+ kind: "tool",
2622
+ id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
2623
+ name: item.name ?? "custom",
2624
+ ...typeof item.input === "string" && item.input !== "" ? { args: item.input } : {}
2625
+ });
2626
+ } else if (item.type === "custom_tool_call_output") {
2627
+ const output = typeof item.output === "string" ? item.output : Array.isArray(item.output) ? item.output.filter((block) => typeof block === "object" && block !== null && typeof block.text === "string").map((block) => block.text).join("\n") : void 0;
2628
+ if (output !== void 0 && output.trim() !== "") {
2629
+ const last = state.lines[state.lines.length - 1];
2630
+ if (last !== void 0 && last.kind === "tool") state.lines[state.lines.length - 1] = {
2631
+ ...last,
2632
+ result: last.result === void 0 ? output : `${last.result}\n${output}`
2633
+ };
2634
+ }
2560
2635
  }
2561
2636
  }
2562
2637
  /**
@@ -2591,7 +2666,8 @@ var CodexStreamParser = class {
2591
2666
  * not put one on the wire; the settle path then reads the rollout's
2592
2667
  * turn_context). Item types covered: `reasoning` (thinking), `agent_message`
2593
2668
  * (reply text), `command_execution` (shell command with aggregated output),
2594
- * `web_search_call`, and `function_call_output`. The last `agent_message`
2669
+ * `web_search_call`, `custom_tool_call`/`custom_tool_call_output` (deferred
2670
+ * and dynamic tools), and `function_call_output`. The last `agent_message`
2595
2671
  * wins as the run output (the final answer), and `turn.completed` carries the
2596
2672
  * turn's usage. Malformed lines are skipped.
2597
2673
  * @param stream - the collected stdout NDJSON text.
@@ -2683,6 +2759,11 @@ function startCodexCliRun(request, spec) {
2683
2759
  });
2684
2760
  const startedAtMs = Date.now();
2685
2761
  spec.childSession?.append("turn/start", { turn });
2762
+ if (spec.model !== void 0 && spec.childSession !== void 0) appendCodexRequestHeader(spec.childSession, {
2763
+ provider: "codex-local",
2764
+ model: spec.model,
2765
+ ...spec.effort === void 0 ? {} : { reasoningEffort: spec.effort }
2766
+ });
2686
2767
  const liveMirror = spec.childSession !== void 0 && spec.ctx !== void 0 ? createCodexLiveMirror(spec, task, turn) : void 0;
2687
2768
  let output = "";
2688
2769
  child.stdout?.on("data", (chunk) => {
@@ -2770,7 +2851,16 @@ function startCodexCliRun(request, spec) {
2770
2851
  onAbort
2771
2852
  }).then((settled) => {
2772
2853
  if (settled.stopReason === "completed") spec.onAuthSuccess?.();
2854
+ return settled;
2855
+ });
2856
+ result.then(() => child.done).then(async () => {
2857
+ if (spec.onAuthFailure !== void 0) {
2858
+ const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
2859
+ if (CODEX_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
2860
+ }
2861
+ await mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
2773
2862
  if (spec.childSession !== void 0) {
2863
+ const settled = await result;
2774
2864
  if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
2775
2865
  turn,
2776
2866
  reason: { kind: "completed" }
@@ -2792,15 +2882,10 @@ function startCodexCliRun(request, spec) {
2792
2882
  }
2793
2883
  }
2794
2884
  });
2885
+ if (spec.ctx !== void 0) await persistChildSession(spec.ctx, spec.childSession).catch((error) => {
2886
+ spec.ctx?.logger.warn(`subagent-codex: turn/end persistence failed: ${thrown(error).message}`);
2887
+ });
2795
2888
  }
2796
- return settled;
2797
- });
2798
- result.then(() => child.done).then(() => {
2799
- if (spec.onAuthFailure !== void 0) {
2800
- const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
2801
- if (CODEX_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
2802
- }
2803
- return mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
2804
2889
  }, () => {});
2805
2890
  return Promise.resolve(subprocessRunHandle({
2806
2891
  id: SessionId(randomUUID()),
@@ -2811,6 +2896,22 @@ function startCodexCliRun(request, spec) {
2811
2896
  teardown: disposeProcess
2812
2897
  }));
2813
2898
  }
2899
+ /**
2900
+ * Write the round's request/header when the config differs from the child
2901
+ * log's last one (the host logs changed snapshots, not per-call duplicates):
2902
+ * the first write is `initial`, a differing one `change`, an identical one
2903
+ * skipped. Context Lens reads the child log's config from these headers.
2904
+ */
2905
+ function appendCodexRequestHeader(childSession, config) {
2906
+ const serialized = JSON.stringify(config);
2907
+ let last;
2908
+ for (const event of childSession.snapshotEvents()) if (event.type === "request/header") last = JSON.stringify(event.data.header?.config);
2909
+ if (last === serialized) return;
2910
+ childSession.append("request/header", {
2911
+ header: { config },
2912
+ reason: last === void 0 ? "initial" : "change"
2913
+ });
2914
+ }
2814
2915
  /** One assistant-role message event, attributed to the codex route. */
2815
2916
  function codexAssistantEvent(blocks) {
2816
2917
  return createAssistantMessage({
@@ -2827,8 +2928,17 @@ function codexAssistantEvent(blocks) {
2827
2928
  * assembler requires: without a boundary the step never enters the location
2828
2929
  * index, the line's assistant message resolves to a turn-level location, and
2829
2930
  * the real-time view renders nothing (only a full rebuild, which mints step
2830
- * drafts from the explicit coordinates, recovers it). Tool cards escape that
2831
- * fate only because their definition does not consult the step location.
2931
+ * drafts from the explicit coordinates, recovers it).
2932
+ *
2933
+ * A tool line folds the NATIVE lifecycle inside its one step: an assistant
2934
+ * message advertising the tool-call block, the `tool/call`, then its
2935
+ * `tool/result` — the released-log reader pairs call and result against the
2936
+ * advertisement and resolves every advertised call before the step closes.
2937
+ * Every fold sees a COMPLETED item (the exec live mirror holds the volatile
2938
+ * last line; the live driver folds at item/completed), so a result the
2939
+ * stream has not recorded by fold time will never arrive — the call folds
2940
+ * with the synthesized interrupted result instead of leaving the step
2941
+ * unresolvable.
2832
2942
  * @returns whether the line folded (false when the options skipped it).
2833
2943
  */
2834
2944
  function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
@@ -2837,23 +2947,36 @@ function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
2837
2947
  step
2838
2948
  });
2839
2949
  if (line.kind === "tool") {
2950
+ const args = line.args ?? "";
2951
+ childSession.append("assistant/message", {
2952
+ turn,
2953
+ step,
2954
+ message: codexAssistantEvent([{
2955
+ type: "tool-call",
2956
+ id: line.id,
2957
+ name: line.name,
2958
+ arguments: args
2959
+ }]),
2960
+ stream: [],
2961
+ ...usage === void 0 ? {} : { usage }
2962
+ }, { surfaceOp: "append" });
2840
2963
  const call = childSession.append("tool/call", {
2841
2964
  turn,
2842
2965
  step,
2843
2966
  callId: line.id,
2844
2967
  name: line.name,
2845
- arguments: line.args ?? ""
2968
+ arguments: args
2846
2969
  });
2847
- if (line.result !== void 0) childSession.append("tool/result", {
2970
+ childSession.append("tool/result", {
2848
2971
  turn,
2849
2972
  step,
2850
2973
  message: createToolResultMessage({
2851
2974
  callId: line.id,
2852
2975
  content: [{
2853
2976
  type: "text",
2854
- text: line.result
2977
+ text: line.result ?? "(工具调用被中断,未能记录返回结果)"
2855
2978
  }],
2856
- isError: false
2979
+ isError: line.result === void 0
2857
2980
  })
2858
2981
  }, {
2859
2982
  surfaceOp: "append",
@@ -3224,6 +3347,47 @@ function dedupeChoices(layers) {
3224
3347
  }
3225
3348
  return choices;
3226
3349
  }
3350
+ /**
3351
+ * Scope the served directory to the surface asking for it (the same helper
3352
+ * lives in each provider's broker — fold into the family core when the
3353
+ * directory faces next evolve):
3354
+ *
3355
+ * - memberless (the card's add-model dialog, the invite picker): the
3356
+ * endpoint's served catalog only — the configuration/history extras the
3357
+ * broker's info extension adds for STATUS display must never become add
3358
+ * candidates;
3359
+ * - a member whose harness has a curated list: the list IS the vocabulary,
3360
+ * in the list's own order (the same rule `choices` already follows) — a
3361
+ * curated value the endpoint no longer serves stays as a bare row (the
3362
+ * card shows it the same way), and the member's current effective model is
3363
+ * kept visible as a bare entry when it sits outside the list, so a pre-list
3364
+ * binding never renders blank;
3365
+ * - a member without a curated list: the legacy extended vocabulary.
3366
+ */
3367
+ function scopeServedDirectory(directory, opts) {
3368
+ if (opts.member && opts.curated.length === 0) return directory;
3369
+ const served = directory.entries.filter((entry) => entry.source === "native");
3370
+ if (!opts.member) return {
3371
+ ...directory,
3372
+ entries: served
3373
+ };
3374
+ const byValue = new Map(served.flatMap((entry) => entry.resolvedModel === void 0 ? [[entry.value, entry]] : [[entry.value, entry], [entry.resolvedModel, entry]]));
3375
+ const entries = opts.curated.map((value) => byValue.get(value) ?? {
3376
+ value,
3377
+ label: value,
3378
+ source: "configuration"
3379
+ });
3380
+ const current = opts.current?.trim();
3381
+ if (current !== void 0 && current !== "" && !entries.some((entry) => entry.value === current || entry.resolvedModel === current)) entries.push({
3382
+ value: current,
3383
+ label: current,
3384
+ source: "configuration"
3385
+ });
3386
+ return {
3387
+ ...directory,
3388
+ entries
3389
+ };
3390
+ }
3227
3391
  var CodexModelBroker = class {
3228
3392
  deps;
3229
3393
  constructor(deps) {
@@ -3275,14 +3439,19 @@ var CodexModelBroker = class {
3275
3439
  async modelDirectory(childSessionId, refresh = false) {
3276
3440
  const home = this.deps.homeDir(childSessionId);
3277
3441
  if (refresh) await this.deps.refreshDirectory?.(home, this.deps.cwd?.(childSessionId));
3278
- return (await this.modelInfo(childSessionId, childSessionId === void 0 ? void 0 : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model)).directory ?? {
3442
+ const info = await this.modelInfo(childSessionId, childSessionId === void 0 ? void 0 : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model);
3443
+ return scopeServedDirectory(info.directory ?? {
3279
3444
  entries: [],
3280
3445
  complete: false,
3281
3446
  customInput: true,
3282
3447
  status: "unsupported",
3283
3448
  refreshing: false,
3284
3449
  revision: 0
3285
- };
3450
+ }, {
3451
+ member: childSessionId !== void 0,
3452
+ curated: this.deps.modelList?.(home) ?? [],
3453
+ current: info.effective
3454
+ });
3286
3455
  }
3287
3456
  async *followModelDirectory(childSessionId, signal) {
3288
3457
  if (this.deps.followDirectory === void 0) {
@@ -3733,7 +3902,7 @@ const inject = [
3733
3902
  * behavior byte-identical.
3734
3903
  */
3735
3904
  const SETTINGS_FIELDS = {
3736
- live: z.boolean().default(false),
3905
+ live: z.boolean().default(true),
3737
3906
  liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
3738
3907
  model: z.string(),
3739
3908
  recentModels: z.array(z.string()).default([])
@@ -3785,7 +3954,7 @@ function apply(ctx, config) {
3785
3954
  const model = vol(config.model);
3786
3955
  const recentModels = vol(config.recentModels);
3787
3956
  return {
3788
- live: vol(config.live) ?? false,
3957
+ live: vol(config.live) ?? true,
3789
3958
  liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
3790
3959
  ...model === void 0 ? {} : { model },
3791
3960
  ...recentModels === void 0 ? {} : { recentModels }
@@ -3802,6 +3971,7 @@ function apply(ctx, config) {
3802
3971
  }
3803
3972
  const face = settingsFace(ctx, {
3804
3973
  namespace: CODEX_SETTINGS_NAMESPACE,
3974
+ harness: "codex",
3805
3975
  legacySchema: CODEX_SETTINGS_SCHEMA,
3806
3976
  legacyBase,
3807
3977
  read: readSettings