@rallycry/conveyor-agent 11.0.31 → 11.0.33

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.
@@ -6,6 +6,7 @@ import {
6
6
  import {
7
7
  MAX_BETWEEN_TURN_BUFFER,
8
8
  MAX_DIAGNOSTIC_OUTPUT,
9
+ SUBMIT_SETTLE_MS,
9
10
  buildExitErrors,
10
11
  buildPromptBytes,
11
12
  buildSpawnArgs,
@@ -29,7 +30,7 @@ import {
29
30
  spawnOptionsFingerprint,
30
31
  transcriptSize,
31
32
  turnOptionsFrom
32
- } from "./chunk-WCJ6HRVU.js";
33
+ } from "./chunk-EKTPDG3G.js";
33
34
  import {
34
35
  AGENT_STATUS_REASON_USER_QUESTION,
35
36
  AgentConnection,
@@ -51,6 +52,7 @@ import {
51
52
  MAX_MANUAL_TEST_TITLE_LENGTH,
52
53
  PM_CHAT_HISTORY_LIMIT,
53
54
  PRE_BUILD_TASK_STATUSES,
55
+ PTY_CHAT_TURN_FILES_MAX,
54
56
  PTY_STREAM_PORT_ATTEMPTS,
55
57
  PTY_STREAM_PORT_BASE,
56
58
  PortDiscovery,
@@ -95,7 +97,7 @@ import {
95
97
  statWorkspacePath,
96
98
  updateRemoteToken,
97
99
  verifyGitCredential
98
- } from "./chunk-3MEPGN2A.js";
100
+ } from "./chunk-IHNXVFIB.js";
99
101
  import {
100
102
  registerBootMilestoneSocketFallback,
101
103
  reportBootMilestone
@@ -399,8 +401,8 @@ var ClaudeCodeHarness = class {
399
401
 
400
402
  // src/harness/pty/session.ts
401
403
  import { randomUUID } from "crypto";
402
- import { mkdtemp, mkdir as mkdir3, rm as rm2, writeFile as writeFile6 } from "fs/promises";
403
- import { join as join7, dirname } from "path";
404
+ import { mkdtemp, mkdir as mkdir3, rm as rm2, writeFile as writeFile7 } from "fs/promises";
405
+ import { join as join8, dirname } from "path";
404
406
 
405
407
  // src/harness/opencode/plugin.ts
406
408
  import { writeFile } from "fs/promises";
@@ -590,6 +592,8 @@ var OpenCodeEventSource = class {
590
592
  /** Session ids opencode reported with a `parentID` — subagent sessions. */
591
593
  childSessions = /* @__PURE__ */ new Set();
592
594
  lastLivenessAt = Number.NEGATIVE_INFINITY;
595
+ /** When the bus turn opened — the chat `turn_end.durationMs` source. */
596
+ turnStartedAt = 0;
593
597
  /** The opencode-assigned session id, once any event has carried it. */
594
598
  get sessionId() {
595
599
  return this.latchedSessionId;
@@ -695,6 +699,7 @@ var OpenCodeEventSource = class {
695
699
  }
696
700
  beginBusTurn() {
697
701
  this.active = true;
702
+ this.turnStartedAt = Date.now();
698
703
  this.lastLivenessAt = Number.NEGATIVE_INFINITY;
699
704
  this.usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
700
705
  this.assistantText = "";
@@ -709,7 +714,17 @@ var OpenCodeEventSource = class {
709
714
  this.emit(
710
715
  error ? buildResultEvent(1, this.usage, "", "", error) : buildResultEvent(0, this.usage, this.assistantText.trim(), "")
711
716
  );
712
- this.emitChat?.({ kind: "turn_end" });
717
+ this.emitChat?.({
718
+ kind: "turn_end",
719
+ durationMs: Math.max(0, Date.now() - this.turnStartedAt),
720
+ // opencode reports no cache split on the bus.
721
+ usage: {
722
+ input: Math.max(0, Math.floor(this.usage.inputTokens)),
723
+ output: Math.max(0, Math.floor(this.usage.outputTokens)),
724
+ cacheRead: 0,
725
+ cacheWrite: 0
726
+ }
727
+ });
713
728
  }
714
729
  relayAssistantPart(part) {
715
730
  if (part.type === "text") {
@@ -980,11 +995,11 @@ function mapTranscriptRecord(raw) {
980
995
  // src/harness/pty/jsonl-tailer.ts
981
996
  var POLL_INTERVAL_MS = 25;
982
997
  var JsonlTailer = class {
983
- constructor(path, onEvent, onRawRecord, mapRecord = mapTranscriptRecord) {
998
+ constructor(path, onEvent, onRawRecord, mapRecord2 = mapTranscriptRecord) {
984
999
  this.path = path;
985
1000
  this.onEvent = onEvent;
986
1001
  this.onRawRecord = onRawRecord;
987
- this.mapRecord = mapRecord;
1002
+ this.mapRecord = mapRecord2;
988
1003
  }
989
1004
  path;
990
1005
  onEvent;
@@ -1093,6 +1108,12 @@ var CodexEventSource = class {
1093
1108
  transcript = null;
1094
1109
  /** Last reported reading per rate-limit window, keyed by `five_hour` / `seven_day`. */
1095
1110
  windows = /* @__PURE__ */ new Map();
1111
+ /** Per-turn chat metadata for the `turn_end` event (see `turnEnd`). */
1112
+ turnStartedAt = null;
1113
+ turnModel;
1114
+ /** Latest cumulative `total_token_usage`, and its value when the turn began. */
1115
+ latestTotal;
1116
+ turnBaseline;
1096
1117
  handleRecord(raw) {
1097
1118
  if (!raw || typeof raw !== "object") return;
1098
1119
  const e = raw;
@@ -1114,6 +1135,8 @@ var CodexEventSource = class {
1114
1135
  if (e.hook_event_name === "UserPromptSubmit") {
1115
1136
  this.turnOpen = true;
1116
1137
  this.lastLivenessAt = Date.now();
1138
+ this.turnStartedAt = this.lastLivenessAt;
1139
+ this.turnBaseline = this.latestTotal;
1117
1140
  this.emit({ type: "tool_progress", tool_name: "Codex", elapsed_time_seconds: 0 });
1118
1141
  if (typeof e.prompt === "string")
1119
1142
  this.chat({ kind: "user_text", text: e.prompt.slice(0, 16e3) });
@@ -1128,7 +1151,7 @@ var CodexEventSource = class {
1128
1151
  } else if (e.hook_event_name === "Interrupt") {
1129
1152
  this.turnOpen = false;
1130
1153
  this.emit({ type: "result", subtype: "error", errors: ["Codex turn interrupted"] });
1131
- this.chat({ kind: "turn_end" });
1154
+ this.chat(this.turnEnd());
1132
1155
  }
1133
1156
  }
1134
1157
  async close() {
@@ -1161,6 +1184,7 @@ var CodexEventSource = class {
1161
1184
  if (!record.payload || typeof record.payload !== "object") return;
1162
1185
  const event = record.payload;
1163
1186
  if (event.type !== "task_complete") this.emitLiveness();
1187
+ this.trackTurnMetadata(record.type, event);
1164
1188
  if (record.type !== "event_msg") return;
1165
1189
  if (event.type === "token_count") {
1166
1190
  this.handleTokenCount(event);
@@ -1178,7 +1202,63 @@ var CodexEventSource = class {
1178
1202
  subtype: "error",
1179
1203
  errors: [redact(String(error.message ?? "Codex turn failed")).output]
1180
1204
  });
1181
- this.chat({ kind: "turn_end" });
1205
+ this.chat(this.turnEnd());
1206
+ }
1207
+ /** Model (`turn_context`) and usage (`token_count`) for the chat turn_end. */
1208
+ trackTurnMetadata(recordType, event) {
1209
+ if (recordType === "turn_context" && typeof event.model === "string") {
1210
+ this.turnModel = event.model.slice(0, 200);
1211
+ } else if (recordType === "event_msg" && event.type === "token_count") {
1212
+ this.addTurnUsage(event);
1213
+ }
1214
+ }
1215
+ /**
1216
+ * `token_count.info.total_token_usage` is cumulative for the session, so the
1217
+ * turn's usage is its growth since the prompt — robust to repeated
1218
+ * `token_count` events (rate-limit refreshes resend the same info). Codex
1219
+ * counts cached input inside `input_tokens`; it is split out to match the
1220
+ * Claude shape.
1221
+ */
1222
+ addTurnUsage(event) {
1223
+ const info = event.info;
1224
+ if (!info || typeof info !== "object") return;
1225
+ const total = info.total_token_usage;
1226
+ if (!total || typeof total !== "object") return;
1227
+ const count = (key) => {
1228
+ const value = total[key];
1229
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1230
+ };
1231
+ const cached = count("cached_input_tokens");
1232
+ this.latestTotal = {
1233
+ input: Math.max(0, count("input_tokens") - cached),
1234
+ output: count("output_tokens"),
1235
+ cacheRead: cached,
1236
+ cacheWrite: 0
1237
+ };
1238
+ }
1239
+ /** Usage since the turn's prompt, or undefined when no reading arrived. */
1240
+ turnUsage() {
1241
+ const latest = this.latestTotal;
1242
+ if (!latest || latest === this.turnBaseline) return void 0;
1243
+ const base = this.turnBaseline ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
1244
+ return {
1245
+ input: Math.max(0, latest.input - base.input),
1246
+ output: Math.max(0, latest.output - base.output),
1247
+ cacheRead: Math.max(0, latest.cacheRead - base.cacheRead),
1248
+ cacheWrite: 0
1249
+ };
1250
+ }
1251
+ /** The `turn_end` chat event with whatever turn metadata is known; resets it. */
1252
+ turnEnd() {
1253
+ const event = { kind: "turn_end" };
1254
+ if (this.turnStartedAt !== null)
1255
+ event.durationMs = Math.max(0, Date.now() - this.turnStartedAt);
1256
+ if (this.turnModel !== void 0) event.model = this.turnModel;
1257
+ const usage = this.turnUsage();
1258
+ if (usage !== void 0) event.usage = usage;
1259
+ this.turnStartedAt = null;
1260
+ this.turnBaseline = this.latestTotal;
1261
+ return event;
1182
1262
  }
1183
1263
  /**
1184
1264
  * `token_count` carries the account's rate-limit gauges on every turn:
@@ -1266,10 +1346,12 @@ var CodexEventSource = class {
1266
1346
  type: "assistant",
1267
1347
  message: { role: "assistant", content: [{ type: "text", text }] }
1268
1348
  });
1269
- this.chat({ kind: "assistant_text", text: text.slice(0, 16e3) });
1349
+ const reply = { kind: "assistant_text", text: text.slice(0, 16e3) };
1350
+ if (this.turnModel !== void 0) reply.model = this.turnModel;
1351
+ this.chat(reply);
1270
1352
  }
1271
1353
  this.emit({ type: "result", subtype: "success", result: text, total_cost_usd: 0 });
1272
- this.chat({ kind: "turn_end" });
1354
+ this.chat(this.turnEnd());
1273
1355
  }
1274
1356
  };
1275
1357
 
@@ -1467,192 +1549,9 @@ var HookSocketServer = class {
1467
1549
  }
1468
1550
  };
1469
1551
 
1470
- // src/harness/pty/limit-banner.ts
1471
- var BANNER_RE = /you'?(?:ve| have) (?:hit|reached) your ([\w-]+ )?(?:usage )?limit/i;
1472
- var RESET_RE = /resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i;
1473
- function nextUtcOccurrenceSeconds(hour12, minute, meridiem, nowMs) {
1474
- let hour = hour12 % 12;
1475
- if (meridiem.toLowerCase() === "pm") hour += 12;
1476
- const candidate = new Date(nowMs);
1477
- candidate.setUTCHours(hour, minute, 0, 0);
1478
- if (candidate.getTime() <= nowMs) candidate.setUTCDate(candidate.getUTCDate() + 1);
1479
- return Math.floor(candidate.getTime() / 1e3);
1480
- }
1481
- function matchUsageLimitBanner(text, now = Date.now()) {
1482
- const banner = BANNER_RE.exec(text);
1483
- if (!banner) return null;
1484
- const qualifier = banner[1]?.trim().toLowerCase();
1485
- const rateLimitType = qualifier === "weekly" ? "seven_day" : "five_hour";
1486
- const reset = RESET_RE.exec(text);
1487
- return {
1488
- rateLimitType,
1489
- ...reset ? {
1490
- resetsAtEpochSeconds: nextUtcOccurrenceSeconds(
1491
- Number(reset[1]),
1492
- reset[2] ? Number(reset[2]) : 0,
1493
- reset[3],
1494
- now
1495
- )
1496
- } : {}
1497
- };
1498
- }
1499
-
1500
- // src/harness/pty/chat-record-mapper.ts
1501
- var TEXT_MAX = 16e3;
1502
- var TOOL_INPUT_MAX = 1900;
1503
- var TOOL_OUTPUT_MAX = 1900;
1504
- var QUESTION_INPUT_MAX = 16e3;
1505
- function isRecord2(value) {
1506
- return typeof value === "object" && value !== null;
1507
- }
1508
- function isUnknownArray2(value) {
1509
- return Array.isArray(value);
1510
- }
1511
- function stringField2(record, ...keys) {
1512
- for (const key of keys) {
1513
- const value = record[key];
1514
- if (typeof value === "string") return value;
1515
- }
1516
- return void 0;
1517
- }
1518
- function truncate2(text, max) {
1519
- return text.length > max ? `${text.slice(0, max)}\u2026` : text;
1520
- }
1521
- function compactQuestionsJson(questions) {
1522
- const serialize = (qs) => JSON.stringify({ questions: qs });
1523
- const withDescriptions = (max) => questions.map((q) => ({
1524
- ...q,
1525
- options: q.options.map((o) => ({ ...o, description: truncate2(o.description, max) }))
1526
- }));
1527
- const full = serialize(questions);
1528
- if (full.length <= QUESTION_INPUT_MAX) return full;
1529
- for (const max of [2e3, 600, 80, 0]) {
1530
- const shortened = serialize(withDescriptions(max));
1531
- if (shortened.length <= QUESTION_INPUT_MAX) return shortened;
1532
- }
1533
- return serialize(withDescriptions(0)).slice(0, QUESTION_INPUT_MAX);
1534
- }
1535
- function compactToolInput(name, input) {
1536
- if (name === "AskUserQuestion" && isRecord2(input)) {
1537
- const questions = parseUserQuestions(input);
1538
- if (questions.length > 0) return compactQuestionsJson(questions);
1539
- }
1540
- return JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX);
1541
- }
1542
- var TASK_NOTIFICATION_PREFIX = "<task-notification>";
1543
- function isNonConversationText(text) {
1544
- const trimmed = text.trimStart();
1545
- return trimmed.startsWith("<command-name>") || trimmed.startsWith("<local-command-") || trimmed.startsWith(TASK_NOTIFICATION_PREFIX);
1546
- }
1547
- function userRecordText(record) {
1548
- const message = record.message;
1549
- if (!isRecord2(message)) return void 0;
1550
- const content = message.content;
1551
- if (typeof content === "string") return content;
1552
- if (!isUnknownArray2(content)) return void 0;
1553
- if (content.some((b) => isRecord2(b) && b.type === "tool_result")) return void 0;
1554
- return content.filter((b) => isRecord2(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
1555
- }
1556
- function isBackgroundTaskNotificationRecord(raw) {
1557
- if (!isRecord2(raw) || raw.type !== "user") return false;
1558
- if (raw.isSidechain === true || raw.isMeta === true) return false;
1559
- const text = userRecordText(raw);
1560
- return text !== void 0 && text.trimStart().startsWith(TASK_NOTIFICATION_PREFIX);
1561
- }
1562
- function mapSystem2(record) {
1563
- if (record.subtype === "init") {
1564
- const event = {
1565
- kind: "init",
1566
- model: stringField2(record, "model") ?? ""
1567
- };
1568
- const sessionId = stringField2(record, "session_id", "sessionId");
1569
- if (sessionId !== void 0) event.claudeSessionId = sessionId;
1570
- return [event];
1571
- }
1572
- if (record.subtype === "turn_duration") return [{ kind: "turn_end" }];
1573
- return [];
1574
- }
1575
- function mapAssistant2(record) {
1576
- const message = record.message;
1577
- if (!isRecord2(message)) return [];
1578
- const content = isUnknownArray2(message.content) ? message.content : [];
1579
- const events = [];
1580
- for (const raw of content) {
1581
- if (!isRecord2(raw)) continue;
1582
- if (raw.type === "text") {
1583
- const text = stringField2(raw, "text");
1584
- if (text && text.length > 0) {
1585
- events.push({ kind: "assistant_text", text: truncate2(text, TEXT_MAX) });
1586
- }
1587
- } else if (raw.type === "tool_use") {
1588
- const name = stringField2(raw, "name");
1589
- if (name) {
1590
- const input = "input" in raw ? raw.input : void 0;
1591
- const event = {
1592
- kind: "tool_use",
1593
- name: truncate2(name, 200),
1594
- input: compactToolInput(name, input)
1595
- };
1596
- const id = stringField2(raw, "id");
1597
- if (id !== void 0) event.id = id;
1598
- events.push(event);
1599
- }
1600
- }
1601
- }
1602
- return events;
1603
- }
1604
- function toolResultText(block) {
1605
- const content = block.content;
1606
- if (typeof content === "string") return content;
1607
- if (isUnknownArray2(content)) {
1608
- return content.filter((b) => isRecord2(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
1609
- }
1610
- return "";
1611
- }
1612
- function mapToolResults(content) {
1613
- const events = [];
1614
- for (const raw of content) {
1615
- if (!isRecord2(raw) || raw.type !== "tool_result") continue;
1616
- const event = {
1617
- kind: "tool_result",
1618
- output: truncate2(toolResultText(raw), TOOL_OUTPUT_MAX),
1619
- isError: raw.is_error === true
1620
- };
1621
- const toolUseId = stringField2(raw, "tool_use_id");
1622
- if (toolUseId !== void 0) event.toolUseId = toolUseId;
1623
- events.push(event);
1624
- }
1625
- return events;
1626
- }
1627
- function mapUser(record) {
1628
- const message = record.message;
1629
- if (!isRecord2(message)) return [];
1630
- const content = message.content;
1631
- if (isUnknownArray2(content) && content.some((b) => isRecord2(b) && b.type === "tool_result")) {
1632
- return mapToolResults(content);
1633
- }
1634
- const text = userRecordText(record);
1635
- if (text === void 0) return [];
1636
- const trimmed = text.trim();
1637
- if (trimmed.length === 0 || isNonConversationText(trimmed)) return [];
1638
- return [{ kind: "user_text", text: truncate2(trimmed, TEXT_MAX) }];
1639
- }
1640
- function mapChatRecords(raw) {
1641
- if (!isRecord2(raw)) return [];
1642
- if (raw.isSidechain === true || raw.isMeta === true) return [];
1643
- switch (raw.type) {
1644
- case "system":
1645
- return mapSystem2(raw);
1646
- case "assistant":
1647
- return mapAssistant2(raw);
1648
- case "user":
1649
- return mapUser(raw);
1650
- case "result":
1651
- return [{ kind: "turn_end" }];
1652
- default:
1653
- return [];
1654
- }
1655
- }
1552
+ // src/harness/pty/model-switch.ts
1553
+ import { readFile, writeFile as writeFile4 } from "fs/promises";
1554
+ import { join as join4 } from "path";
1656
1555
 
1657
1556
  // src/harness/pty/settings.ts
1658
1557
  import { mkdir, writeFile as writeFile3, chmod } from "fs/promises";
@@ -1917,6 +1816,453 @@ async function writeHookSettings(dir, opts = {}) {
1917
1816
  return { settingsPath, helperPath };
1918
1817
  }
1919
1818
 
1819
+ // src/harness/pty/model-switch.ts
1820
+ var SWITCH_COMMAND_GAP_MS = 800;
1821
+ var RESTORE_RECHECK_MS = 3e3;
1822
+ var PERSISTED_KEYS = ["model", "modelSettings"];
1823
+ var defaultSleep = (ms) => new Promise((resolve) => {
1824
+ setTimeout(resolve, ms);
1825
+ });
1826
+ async function readSettings(path) {
1827
+ let text;
1828
+ try {
1829
+ text = await readFile(path, "utf8");
1830
+ } catch {
1831
+ return { state: "missing" };
1832
+ }
1833
+ try {
1834
+ const parsed = JSON.parse(text);
1835
+ return parsed && typeof parsed === "object" ? { state: "ok", data: parsed } : { state: "invalid" };
1836
+ } catch {
1837
+ return { state: "invalid" };
1838
+ }
1839
+ }
1840
+ async function restorePersistedModelKeys(path, before) {
1841
+ if (before.state === "invalid") return;
1842
+ const current = await readSettings(path);
1843
+ if (current.state !== "ok") return;
1844
+ const prior = before.state === "ok" ? before.data : {};
1845
+ let changed = false;
1846
+ for (const key of PERSISTED_KEYS) {
1847
+ if (JSON.stringify(current.data[key]) === JSON.stringify(prior[key])) continue;
1848
+ if (prior[key] === void 0) delete current.data[key];
1849
+ else current.data[key] = prior[key];
1850
+ changed = true;
1851
+ }
1852
+ if (changed) await writeFile4(path, `${JSON.stringify(current.data, null, 2)}
1853
+ `, "utf8");
1854
+ }
1855
+ var ModelSwitchQueue = class {
1856
+ constructor(host) {
1857
+ this.host = host;
1858
+ }
1859
+ host;
1860
+ pending = null;
1861
+ flushing = null;
1862
+ /** A switch request from the relay. Applied now when idle, else queued. */
1863
+ request(switchTo) {
1864
+ this.pending = { ...this.pending, ...switchTo };
1865
+ if (!this.host.commands(this.pending)) {
1866
+ const deferred = this.pending;
1867
+ this.pending = null;
1868
+ this.host.deferred(deferred);
1869
+ return;
1870
+ }
1871
+ if (this.host.canWriteNow()) void this.flush();
1872
+ }
1873
+ /** True while switch commands are being typed. */
1874
+ get busy() {
1875
+ return this.flushing !== null;
1876
+ }
1877
+ /** Resolves once any in-flight switch finished typing (and was recorded). */
1878
+ async settle() {
1879
+ while (this.flushing) await this.flushing;
1880
+ }
1881
+ /** Turn boundary: write any queued switch. Safe to call when nothing is queued. */
1882
+ flush() {
1883
+ if (this.flushing) return this.flushing;
1884
+ if (!this.pending || !this.host.canWriteNow()) return Promise.resolve();
1885
+ const switchTo = this.pending;
1886
+ this.pending = null;
1887
+ this.flushing = this.write(switchTo).finally(() => {
1888
+ this.flushing = null;
1889
+ if (this.pending) void this.flush();
1890
+ });
1891
+ return this.flushing;
1892
+ }
1893
+ async write(switchTo) {
1894
+ const commands = this.host.commands(switchTo) ?? [];
1895
+ const sleep2 = this.host.sleep ?? defaultSleep;
1896
+ const path = this.host.settingsPath ?? join4(claudeConfigHome(), "settings.json");
1897
+ const before = await readSettings(path);
1898
+ for (const command of commands) {
1899
+ this.host.write(this.host.encode(command));
1900
+ await sleep2(SUBMIT_SETTLE_MS);
1901
+ this.host.write("\r");
1902
+ await sleep2(SWITCH_COMMAND_GAP_MS);
1903
+ }
1904
+ await restorePersistedModelKeys(path, before);
1905
+ this.host.applied(switchTo);
1906
+ void sleep2(RESTORE_RECHECK_MS).then(() => restorePersistedModelKeys(path, before)).catch(() => void 0);
1907
+ }
1908
+ };
1909
+
1910
+ // src/harness/pty/limit-banner.ts
1911
+ var BANNER_RE = /you'?(?:ve| have) (?:hit|reached) your ([\w-]+ )?(?:usage )?limit/i;
1912
+ var RESET_RE = /resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i;
1913
+ function nextUtcOccurrenceSeconds(hour12, minute, meridiem, nowMs) {
1914
+ let hour = hour12 % 12;
1915
+ if (meridiem.toLowerCase() === "pm") hour += 12;
1916
+ const candidate = new Date(nowMs);
1917
+ candidate.setUTCHours(hour, minute, 0, 0);
1918
+ if (candidate.getTime() <= nowMs) candidate.setUTCDate(candidate.getUTCDate() + 1);
1919
+ return Math.floor(candidate.getTime() / 1e3);
1920
+ }
1921
+ function matchUsageLimitBanner(text, now = Date.now()) {
1922
+ const banner = BANNER_RE.exec(text);
1923
+ if (!banner) return null;
1924
+ const qualifier = banner[1]?.trim().toLowerCase();
1925
+ const rateLimitType = qualifier === "weekly" ? "seven_day" : "five_hour";
1926
+ const reset = RESET_RE.exec(text);
1927
+ return {
1928
+ rateLimitType,
1929
+ ...reset ? {
1930
+ resetsAtEpochSeconds: nextUtcOccurrenceSeconds(
1931
+ Number(reset[1]),
1932
+ reset[2] ? Number(reset[2]) : 0,
1933
+ reset[3],
1934
+ now
1935
+ )
1936
+ } : {}
1937
+ };
1938
+ }
1939
+
1940
+ // src/harness/pty/chat-turn-accumulator.ts
1941
+ function isRecord2(value) {
1942
+ return typeof value === "object" && value !== null;
1943
+ }
1944
+ function tokenCount(usage, key) {
1945
+ const value = usage[key];
1946
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1947
+ }
1948
+ function countPatchLines(patch) {
1949
+ let added = 0;
1950
+ let removed = 0;
1951
+ for (const hunk of patch) {
1952
+ if (!isRecord2(hunk) || !Array.isArray(hunk.lines)) continue;
1953
+ for (const line of hunk.lines) {
1954
+ if (typeof line !== "string") continue;
1955
+ if (line.startsWith("+")) added += 1;
1956
+ else if (line.startsWith("-")) removed += 1;
1957
+ }
1958
+ }
1959
+ return { added, removed };
1960
+ }
1961
+ function countContentLines(content) {
1962
+ if (content.length === 0) return 0;
1963
+ const lines = content.split("\n");
1964
+ return content.endsWith("\n") ? lines.length - 1 : lines.length;
1965
+ }
1966
+ function editStatsFromToolResult(result) {
1967
+ if (!isRecord2(result)) return void 0;
1968
+ const filePath = result.filePath;
1969
+ if (typeof filePath !== "string" || filePath.length === 0) return void 0;
1970
+ const patch = result.structuredPatch;
1971
+ if (Array.isArray(patch) && patch.length > 0) {
1972
+ return { filePath, ...countPatchLines(patch) };
1973
+ }
1974
+ if (result.type === "create" && typeof result.content === "string") {
1975
+ return { filePath, added: countContentLines(result.content), removed: 0 };
1976
+ }
1977
+ if (Array.isArray(patch)) return { filePath, added: 0, removed: 0 };
1978
+ return void 0;
1979
+ }
1980
+ function relativePath(filePath, root) {
1981
+ const prefix = root?.replace(/\/+$/, "");
1982
+ if (prefix && filePath.startsWith(`${prefix}/`)) return filePath.slice(prefix.length + 1);
1983
+ return filePath;
1984
+ }
1985
+ function maxUsage(a, b) {
1986
+ return {
1987
+ input: Math.max(a.input, b.input),
1988
+ output: Math.max(a.output, b.output),
1989
+ cacheRead: Math.max(a.cacheRead, b.cacheRead),
1990
+ cacheWrite: Math.max(a.cacheWrite, b.cacheWrite)
1991
+ };
1992
+ }
1993
+ function sumUsage(readings) {
1994
+ const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
1995
+ for (const r of readings) {
1996
+ total.input += r.input;
1997
+ total.output += r.output;
1998
+ total.cacheRead += r.cacheRead;
1999
+ total.cacheWrite += r.cacheWrite;
2000
+ }
2001
+ return total;
2002
+ }
2003
+ var ChatTurnAccumulator = class {
2004
+ model;
2005
+ // The CLI writes one transcript record per content block and repeats the
2006
+ // message's usage on each (possibly as a growing streaming snapshot), so
2007
+ // usage is keyed by message id and the largest reading per id wins.
2008
+ usageByMessage = /* @__PURE__ */ new Map();
2009
+ anonymousMessages = 0;
2010
+ files = /* @__PURE__ */ new Map();
2011
+ /** Fold one assistant `message` into the turn. */
2012
+ addAssistantMessage(message) {
2013
+ if (typeof message.model === "string" && message.model.length > 0) {
2014
+ if (message.model !== "<synthetic>") this.model = message.model;
2015
+ }
2016
+ const usage = message.usage;
2017
+ if (!isRecord2(usage)) return;
2018
+ const reading = {
2019
+ input: tokenCount(usage, "input_tokens"),
2020
+ output: tokenCount(usage, "output_tokens"),
2021
+ cacheRead: tokenCount(usage, "cache_read_input_tokens"),
2022
+ cacheWrite: tokenCount(usage, "cache_creation_input_tokens")
2023
+ };
2024
+ const id = typeof message.id === "string" ? message.id : `anon-${this.anonymousMessages++}`;
2025
+ const previous = this.usageByMessage.get(id);
2026
+ this.usageByMessage.set(id, previous ? maxUsage(previous, reading) : reading);
2027
+ }
2028
+ /** Fold one user record's record-level `toolUseResult` into the file stats. */
2029
+ addToolUseResult(result, root) {
2030
+ const stats = editStatsFromToolResult(result);
2031
+ if (!stats) return;
2032
+ const path = relativePath(stats.filePath, root).slice(0, 500);
2033
+ const existing = this.files.get(path);
2034
+ if (existing) {
2035
+ existing.added += stats.added;
2036
+ existing.removed += stats.removed;
2037
+ return;
2038
+ }
2039
+ if (this.files.size >= PTY_CHAT_TURN_FILES_MAX) return;
2040
+ this.files.set(path, { path, added: stats.added, removed: stats.removed });
2041
+ }
2042
+ /** Return the turn's aggregates and reset for the next turn. */
2043
+ drain() {
2044
+ const result = {};
2045
+ if (this.model !== void 0) result.model = this.model.slice(0, 200);
2046
+ if (this.usageByMessage.size > 0) result.usage = sumUsage(this.usageByMessage.values());
2047
+ if (this.files.size > 0) result.files = [...this.files.values()];
2048
+ this.reset();
2049
+ return result;
2050
+ }
2051
+ reset() {
2052
+ this.model = void 0;
2053
+ this.usageByMessage.clear();
2054
+ this.anonymousMessages = 0;
2055
+ this.files.clear();
2056
+ }
2057
+ };
2058
+
2059
+ // src/harness/pty/chat-record-mapper.ts
2060
+ var TEXT_MAX = 16e3;
2061
+ var TOOL_INPUT_MAX = 1900;
2062
+ var TOOL_OUTPUT_MAX = 1900;
2063
+ var QUESTION_INPUT_MAX = 16e3;
2064
+ function isRecord3(value) {
2065
+ return typeof value === "object" && value !== null;
2066
+ }
2067
+ function isUnknownArray2(value) {
2068
+ return Array.isArray(value);
2069
+ }
2070
+ function stringField2(record, ...keys) {
2071
+ for (const key of keys) {
2072
+ const value = record[key];
2073
+ if (typeof value === "string") return value;
2074
+ }
2075
+ return void 0;
2076
+ }
2077
+ function truncate2(text, max) {
2078
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
2079
+ }
2080
+ function compactQuestionsJson(questions) {
2081
+ const serialize = (qs) => JSON.stringify({ questions: qs });
2082
+ const withDescriptions = (max) => questions.map((q) => ({
2083
+ ...q,
2084
+ options: q.options.map((o) => ({ ...o, description: truncate2(o.description, max) }))
2085
+ }));
2086
+ const full = serialize(questions);
2087
+ if (full.length <= QUESTION_INPUT_MAX) return full;
2088
+ for (const max of [2e3, 600, 80, 0]) {
2089
+ const shortened = serialize(withDescriptions(max));
2090
+ if (shortened.length <= QUESTION_INPUT_MAX) return shortened;
2091
+ }
2092
+ return serialize(withDescriptions(0)).slice(0, QUESTION_INPUT_MAX);
2093
+ }
2094
+ function compactToolInput(name, input) {
2095
+ if (name === "AskUserQuestion" && isRecord3(input)) {
2096
+ const questions = parseUserQuestions(input);
2097
+ if (questions.length > 0) return compactQuestionsJson(questions);
2098
+ }
2099
+ return JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX);
2100
+ }
2101
+ var TASK_NOTIFICATION_PREFIX = "<task-notification>";
2102
+ function isNonConversationText(text) {
2103
+ const trimmed = text.trimStart();
2104
+ return trimmed.startsWith("<command-name>") || trimmed.startsWith("<local-command-") || trimmed.startsWith(TASK_NOTIFICATION_PREFIX);
2105
+ }
2106
+ function userRecordText(record) {
2107
+ const message = record.message;
2108
+ if (!isRecord3(message)) return void 0;
2109
+ const content = message.content;
2110
+ if (typeof content === "string") return content;
2111
+ if (!isUnknownArray2(content)) return void 0;
2112
+ if (content.some((b) => isRecord3(b) && b.type === "tool_result")) return void 0;
2113
+ return content.filter((b) => isRecord3(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
2114
+ }
2115
+ function backgroundTaskCompletion(raw) {
2116
+ if (!isRecord3(raw) || raw.isSidechain === true || raw.isMeta === true) return null;
2117
+ const text = notificationText(raw);
2118
+ if (text === void 0 || !text.trimStart().startsWith(TASK_NOTIFICATION_PREFIX)) return null;
2119
+ const taskId = /<task-id>([^<]+)<\/task-id>/.exec(text)?.[1]?.trim();
2120
+ return { taskId: taskId || null };
2121
+ }
2122
+ function notificationText(record) {
2123
+ if (record.type === "user") return userRecordText(record);
2124
+ if (record.type !== "attachment") return void 0;
2125
+ const attachment = record.attachment;
2126
+ if (!isRecord3(attachment) || attachment.commandMode !== "task-notification") return void 0;
2127
+ return typeof attachment.prompt === "string" ? attachment.prompt : void 0;
2128
+ }
2129
+ function mapSystem2(record) {
2130
+ if (record.subtype === "init") {
2131
+ const event = {
2132
+ kind: "init",
2133
+ model: stringField2(record, "model") ?? ""
2134
+ };
2135
+ const sessionId = stringField2(record, "session_id", "sessionId");
2136
+ if (sessionId !== void 0) event.claudeSessionId = sessionId;
2137
+ return [event];
2138
+ }
2139
+ if (record.subtype === "turn_duration") {
2140
+ const event = { kind: "turn_end" };
2141
+ const durationMs = record.durationMs;
2142
+ if (typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0) {
2143
+ event.durationMs = Math.round(durationMs);
2144
+ }
2145
+ return [event];
2146
+ }
2147
+ return [];
2148
+ }
2149
+ function mapAssistant2(record) {
2150
+ const message = record.message;
2151
+ if (!isRecord3(message)) return [];
2152
+ const content = isUnknownArray2(message.content) ? message.content : [];
2153
+ const model = stringField2(message, "model");
2154
+ const events = [];
2155
+ for (const raw of content) {
2156
+ if (!isRecord3(raw)) continue;
2157
+ if (raw.type === "text") {
2158
+ const text = stringField2(raw, "text");
2159
+ if (text && text.length > 0) {
2160
+ const event = {
2161
+ kind: "assistant_text",
2162
+ text: truncate2(text, TEXT_MAX)
2163
+ };
2164
+ if (model && model !== "<synthetic>") event.model = truncate2(model, 200);
2165
+ events.push(event);
2166
+ }
2167
+ } else if (raw.type === "tool_use") {
2168
+ const name = stringField2(raw, "name");
2169
+ if (name) {
2170
+ const input = "input" in raw ? raw.input : void 0;
2171
+ const event = {
2172
+ kind: "tool_use",
2173
+ name: truncate2(name, 200),
2174
+ input: compactToolInput(name, input)
2175
+ };
2176
+ const id = stringField2(raw, "id");
2177
+ if (id !== void 0) event.id = id;
2178
+ events.push(event);
2179
+ }
2180
+ }
2181
+ }
2182
+ return events;
2183
+ }
2184
+ function toolResultText(block) {
2185
+ const content = block.content;
2186
+ if (typeof content === "string") return content;
2187
+ if (isUnknownArray2(content)) {
2188
+ return content.filter((b) => isRecord3(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
2189
+ }
2190
+ return "";
2191
+ }
2192
+ function mapToolResults(content) {
2193
+ const events = [];
2194
+ for (const raw of content) {
2195
+ if (!isRecord3(raw) || raw.type !== "tool_result") continue;
2196
+ const event = {
2197
+ kind: "tool_result",
2198
+ output: truncate2(toolResultText(raw), TOOL_OUTPUT_MAX),
2199
+ isError: raw.is_error === true
2200
+ };
2201
+ const toolUseId = stringField2(raw, "tool_use_id");
2202
+ if (toolUseId !== void 0) event.toolUseId = toolUseId;
2203
+ events.push(event);
2204
+ }
2205
+ return events;
2206
+ }
2207
+ function mapUser(record) {
2208
+ const message = record.message;
2209
+ if (!isRecord3(message)) return [];
2210
+ const content = message.content;
2211
+ if (isUnknownArray2(content) && content.some((b) => isRecord3(b) && b.type === "tool_result")) {
2212
+ return mapToolResults(content);
2213
+ }
2214
+ const text = userRecordText(record);
2215
+ if (text === void 0) return [];
2216
+ const trimmed = text.trim();
2217
+ if (trimmed.length === 0 || isNonConversationText(trimmed)) return [];
2218
+ return [{ kind: "user_text", text: truncate2(trimmed, TEXT_MAX) }];
2219
+ }
2220
+ function mapRecord(raw) {
2221
+ switch (raw.type) {
2222
+ case "system":
2223
+ return mapSystem2(raw);
2224
+ case "assistant":
2225
+ return mapAssistant2(raw);
2226
+ case "user":
2227
+ return mapUser(raw);
2228
+ case "result":
2229
+ return [{ kind: "turn_end" }];
2230
+ default:
2231
+ return [];
2232
+ }
2233
+ }
2234
+ function isChatRecord(raw) {
2235
+ return isRecord3(raw) && raw.isSidechain !== true && raw.isMeta !== true;
2236
+ }
2237
+ function mapChatRecords(raw) {
2238
+ if (!isChatRecord(raw)) return [];
2239
+ const events = mapRecord(raw);
2240
+ const at = stringField2(raw, "timestamp");
2241
+ if (at !== void 0 && at.length <= 40) {
2242
+ for (const event of events) event.at = at;
2243
+ }
2244
+ return events;
2245
+ }
2246
+ function createChatRecordMapper(rootDir) {
2247
+ const turn = new ChatTurnAccumulator();
2248
+ return {
2249
+ map(raw) {
2250
+ const events = mapChatRecords(raw);
2251
+ if (!isChatRecord(raw)) return events;
2252
+ if (raw.type === "assistant" && isRecord3(raw.message)) {
2253
+ turn.addAssistantMessage(raw.message);
2254
+ } else if (raw.type === "user") {
2255
+ if ("toolUseResult" in raw) turn.addToolUseResult(raw.toolUseResult, rootDir?.());
2256
+ if (events.some((e) => e.kind === "user_text")) turn.reset();
2257
+ }
2258
+ for (const event of events) {
2259
+ if (event.kind === "turn_end") Object.assign(event, turn.drain());
2260
+ }
2261
+ return events;
2262
+ }
2263
+ };
2264
+ }
2265
+
1920
2266
  // src/harness/pty/output-coalescer.ts
1921
2267
  var DEFAULT_FLUSH_MS = 40;
1922
2268
  var DEFAULT_MAX_BUFFER_CHARS = 48 * 1024;
@@ -1972,8 +2318,8 @@ var PtyOutputCoalescer = class {
1972
2318
  // src/harness/pty/tool-server.ts
1973
2319
  import { createServer as createServer2 } from "http";
1974
2320
  import { z as z2 } from "zod";
1975
- import { writeFile as writeFile4 } from "fs/promises";
1976
- import { join as join4 } from "path";
2321
+ import { writeFile as writeFile5 } from "fs/promises";
2322
+ import { join as join5 } from "path";
1977
2323
  import { randomBytes } from "crypto";
1978
2324
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1979
2325
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -2164,21 +2510,21 @@ async function startToolServers(mcpServers, tempDir) {
2164
2510
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
2165
2511
  }
2166
2512
  if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null, entries: config };
2167
- const mcpConfigPath = join4(tempDir, "mcp-config.json");
2168
- await writeFile4(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
2513
+ const mcpConfigPath = join5(tempDir, "mcp-config.json");
2514
+ await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
2169
2515
  return { servers, mcpConfigPath, entries: config };
2170
2516
  }
2171
2517
 
2172
2518
  // src/harness/pty/credentials.ts
2173
- import { chmod as chmod2, mkdir as mkdir2, readFile, rm, writeFile as writeFile5 } from "fs/promises";
2519
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile6 } from "fs/promises";
2174
2520
  import { homedir as homedir2 } from "os";
2175
- import { join as join6 } from "path";
2521
+ import { join as join7 } from "path";
2176
2522
 
2177
2523
  // src/harness/pty/credentials-marker.ts
2178
2524
  import { createHash } from "crypto";
2179
- import { join as join5 } from "path";
2525
+ import { join as join6 } from "path";
2180
2526
  function conveyorCredentialsMarkerPath() {
2181
- return join5(claudeConfigHome(), ".conveyor-credentials.json");
2527
+ return join6(claudeConfigHome(), ".conveyor-credentials.json");
2182
2528
  }
2183
2529
  function tokenFingerprint(accessToken) {
2184
2530
  return createHash("sha256").update(accessToken).digest("hex");
@@ -2374,14 +2720,14 @@ function planCredentialsWrite(input) {
2374
2720
 
2375
2721
  // src/harness/pty/credentials.ts
2376
2722
  function claudeCredentialsPath() {
2377
- return join6(claudeConfigHome(), ".credentials.json");
2723
+ return join7(claudeConfigHome(), ".credentials.json");
2378
2724
  }
2379
2725
  function isConveyorCloudEnv(env = process.env) {
2380
2726
  return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
2381
2727
  }
2382
2728
  async function readRaw(path) {
2383
2729
  try {
2384
- return await readFile(path, "utf8");
2730
+ return await readFile2(path, "utf8");
2385
2731
  } catch {
2386
2732
  return null;
2387
2733
  }
@@ -2410,11 +2756,11 @@ async function readCredentialsIdentity(env = process.env) {
2410
2756
  );
2411
2757
  }
2412
2758
  var READ_BACK_DELAYS_MS = [250, 500, 1e3, 2e3];
2413
- var defaultSleep = (ms) => new Promise((resolve) => {
2759
+ var defaultSleep2 = (ms) => new Promise((resolve) => {
2414
2760
  setTimeout(resolve, ms);
2415
2761
  });
2416
2762
  async function writeWithReadBackRetry(io, contents, delaysMs = READ_BACK_DELAYS_MS) {
2417
- const sleep2 = io.sleep ?? defaultSleep;
2763
+ const sleep2 = io.sleep ?? defaultSleep2;
2418
2764
  for (let attempt = 0; ; attempt++) {
2419
2765
  await io.write(contents);
2420
2766
  if (await io.read() === contents) return true;
@@ -2424,7 +2770,7 @@ async function writeWithReadBackRetry(io, contents, delaysMs = READ_BACK_DELAYS_
2424
2770
  }
2425
2771
  function fsWriteIo(path, mode) {
2426
2772
  return {
2427
- write: (contents) => writeFile5(path, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
2773
+ write: (contents) => writeFile6(path, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
2428
2774
  read: () => readRaw(path)
2429
2775
  };
2430
2776
  }
@@ -2504,7 +2850,7 @@ async function sanitizeApprovedApiKeys(oauthToken) {
2504
2850
  }
2505
2851
  function claudeJsonPath() {
2506
2852
  const configDir = process.env.CLAUDE_CONFIG_DIR;
2507
- return configDir ? join6(configDir, ".claude.json") : join6(homedir2(), ".claude.json");
2853
+ return configDir ? join7(configDir, ".claude.json") : join7(homedir2(), ".claude.json");
2508
2854
  }
2509
2855
  function asRecord(value) {
2510
2856
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
@@ -2597,7 +2943,7 @@ function planClaudeJsonSeed(existingRaw, trustCwd, oauthIdentity) {
2597
2943
  return changed ? JSON.stringify(config) : null;
2598
2944
  }
2599
2945
  function conveyorOauthMarkerPath() {
2600
- return join6(claudeConfigHome(), "conveyor-oauth-account.json");
2946
+ return join7(claudeConfigHome(), "conveyor-oauth-account.json");
2601
2947
  }
2602
2948
  function parseOauthIdentity(raw) {
2603
2949
  if (!raw || raw.trim() === "") return null;
@@ -2696,6 +3042,7 @@ var ClaudeTuiAdapter = class {
2696
3042
  ...resume ? { resume } : {},
2697
3043
  ...options.sessionId ? { sessionId: options.sessionId } : {},
2698
3044
  model: options.model,
3045
+ ...options.effort ? { effort: options.effort } : {},
2699
3046
  permissionMode: options.permissionMode,
2700
3047
  settingsPath: input.settingsPath ?? "",
2701
3048
  ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
@@ -2719,6 +3066,13 @@ var ClaudeTuiAdapter = class {
2719
3066
  encodePromptBytes(text) {
2720
3067
  return buildPromptBytes(text);
2721
3068
  }
3069
+ /** `/model` and `/effort` apply inline (verified on claude 2.1.280). */
3070
+ modelSwitchCommands(switchTo) {
3071
+ return [
3072
+ ...switchTo.model ? [`/model ${switchTo.model}`] : [],
3073
+ ...switchTo.effort ? [`/effort ${switchTo.effort}`] : []
3074
+ ];
3075
+ }
2722
3076
  buildExitErrors(exitCode, rawOutput) {
2723
3077
  return buildExitErrors(exitCode, rawOutput, this.resolveBinary());
2724
3078
  }
@@ -2799,6 +3153,19 @@ var PtySession = class {
2799
3153
  rows = 40;
2800
3154
  unsubInput = null;
2801
3155
  unsubResize = null;
3156
+ unsubModelEffort = null;
3157
+ turnsBegun = 0;
3158
+ // Live model/effort switch from the composer pill: typed as the CLI's own
3159
+ // slash commands between turns. Closures read `this` lazily (class fields
3160
+ // initialize before parameter properties).
3161
+ modelSwitch = new ModelSwitchQueue({
3162
+ canWriteNow: () => !this._toreDown && this.pty !== null && this.activeQueue === null && !this.questionnairePending,
3163
+ write: (bytes) => this.writeStdin(bytes),
3164
+ encode: (text) => this.adapter.encodePromptBytes(text),
3165
+ commands: (switchTo) => this.adapter.modelSwitchCommands?.(switchTo) ?? null,
3166
+ applied: (switchTo) => this.onModelSwitched(switchTo, true),
3167
+ deferred: (switchTo) => this.onModelSwitched(switchTo, false)
3168
+ });
2802
3169
  // In-process HTTP MCP servers exposing the harness tools to the spawned CLI,
2803
3170
  // plus the path to the `--mcp-config` that points at them.
2804
3171
  toolServers = [];
@@ -2829,6 +3196,11 @@ var PtySession = class {
2829
3196
  // synthetic id so the card flips to answered.
2830
3197
  pendingSyntheticQuestionIds = [];
2831
3198
  questionResultRemap = /* @__PURE__ */ new Map();
3199
+ // Chat projection with a per-turn accumulator (usage, model, edited files
3200
+ // ride the turn_end event), so it must live as long as the process.
3201
+ // Edited-file paths are made relative to the spawn cwd (the repo root), read
3202
+ // lazily because class fields initialize before parameter properties.
3203
+ chatMapper = createChatRecordMapper(() => this.options.cwd);
2832
3204
  // An AskUserQuestion questionnaire is parked on screen (PreToolUse hook
2833
3205
  // fired, no PostToolUse envelope yet). While set the parked process must
2834
3206
  // not be reused: feeding follow-up prompt bytes into the live dialog would
@@ -2916,6 +3288,7 @@ var PtySession = class {
2916
3288
  get spawnFingerprint() {
2917
3289
  return this.adapter.spawnFingerprint({
2918
3290
  model: this.options.model,
3291
+ ...this.options.effort ? { effort: this.options.effort } : {},
2919
3292
  permissionMode: this.options.permissionMode,
2920
3293
  ...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
2921
3294
  cwd: this.options.cwd
@@ -2978,6 +3351,7 @@ var PtySession = class {
2978
3351
  * the prompt into the live pty. The caller must have verified canReuse().
2979
3352
  */
2980
3353
  async beginTurn(prompt, options) {
3354
+ this.turnsBegun += 1;
2981
3355
  this.prewarmed = false;
2982
3356
  this.turnPrompt = prompt;
2983
3357
  this.turn = turnOptionsFrom(options);
@@ -3057,6 +3431,31 @@ var PtySession = class {
3057
3431
  this.turn.abortController.signal.removeEventListener("abort", this.abortHandler);
3058
3432
  this.abortHandler = null;
3059
3433
  }
3434
+ void this.modelSwitch.flush();
3435
+ }
3436
+ /** Resolves once an in-flight model/effort switch finished typing — the
3437
+ * harness awaits it before deciding whether this parked process is reusable. */
3438
+ settleModelSwitch() {
3439
+ return this.modelSwitch.settle();
3440
+ }
3441
+ /**
3442
+ * Record a model/effort switch and tell the chat. A live switch also updates
3443
+ * the spawn options so the recorded fingerprint matches the next turn's
3444
+ * (which carries the same override) — the parked process is reused, not
3445
+ * respawned.
3446
+ */
3447
+ onModelSwitched(switchTo, live) {
3448
+ if (live) {
3449
+ if (switchTo.model) this.options.model = switchTo.model;
3450
+ if (switchTo.effort) this.options.effort = switchTo.effort;
3451
+ }
3452
+ const label = [switchTo.model, switchTo.effort && `effort ${switchTo.effort}`].filter(Boolean).join(" \xB7 ");
3453
+ this.sendChatEvent({
3454
+ kind: "user_text",
3455
+ text: live ? `Switched to ${label}` : `Saved ${label} \u2014 applies on next wake`,
3456
+ source: "model_switch",
3457
+ at: (/* @__PURE__ */ new Date()).toISOString()
3458
+ });
3060
3459
  }
3061
3460
  async collectCodexPrompt() {
3062
3461
  if (this.adapter.id === "codex" && typeof this.turnPrompt !== "string") {
@@ -3106,7 +3505,7 @@ var PtySession = class {
3106
3505
  const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
3107
3506
  await this.spawn(settingsPath, socketPath);
3108
3507
  } else {
3109
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-pty-"));
3508
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-pty-"));
3110
3509
  await this.spawn();
3111
3510
  this.pushEvent({
3112
3511
  type: "system",
@@ -3128,6 +3527,7 @@ var PtySession = class {
3128
3527
  if (this.bridge) {
3129
3528
  if (feedPrompt) this.attachInput();
3130
3529
  this.unsubResize = this.bridge.onResize((cols, rows) => this.resizePty(cols, rows));
3530
+ this.unsubModelEffort = this.bridge.onModelEffort?.((switchTo) => this.modelSwitch.request(switchTo)) ?? null;
3131
3531
  }
3132
3532
  if (feedPrompt) await this.feedPrompt();
3133
3533
  } catch (err) {
@@ -3151,7 +3551,7 @@ var PtySession = class {
3151
3551
  * moment it is known, which is how the runner learns the resume target.
3152
3552
  */
3153
3553
  async startCodexEventSources() {
3154
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-codex-"));
3554
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-codex-"));
3155
3555
  const { servers, entries } = await startToolServers(
3156
3556
  this.options.mcpServers ?? {},
3157
3557
  this.tempDir
@@ -3175,7 +3575,8 @@ var PtySession = class {
3175
3575
  claudeSessionId: id
3176
3576
  });
3177
3577
  },
3178
- (event) => this.sendChatEvent(event)
3578
+ // No per-record transcript timestamp on this path; stamp emit time.
3579
+ (event) => this.sendChatEvent({ ...event, at: event.at ?? (/* @__PURE__ */ new Date()).toISOString() })
3179
3580
  );
3180
3581
  this.codexSource = source;
3181
3582
  this.tailer = new JsonlTailer(
@@ -3188,7 +3589,7 @@ var PtySession = class {
3188
3589
  return { ...extras, mcpEntries: entries };
3189
3590
  }
3190
3591
  async startOpenCodeEventSources() {
3191
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-pty-"));
3592
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-pty-"));
3192
3593
  const { servers, entries } = await startToolServers(
3193
3594
  this.options.mcpServers ?? {},
3194
3595
  this.tempDir
@@ -3198,8 +3599,8 @@ var PtySession = class {
3198
3599
  let instructionsPath;
3199
3600
  const systemPrompt = this.options.appendSystemPrompt;
3200
3601
  if (systemPrompt && systemPrompt.trim() !== "") {
3201
- instructionsPath = join7(this.tempDir, "conveyor-instructions.md");
3202
- await writeFile6(instructionsPath, systemPrompt, "utf8");
3602
+ instructionsPath = join8(this.tempDir, "conveyor-instructions.md");
3603
+ await writeFile7(instructionsPath, systemPrompt, "utf8");
3203
3604
  }
3204
3605
  this.opencodeSource = new OpenCodeEventSource(
3205
3606
  (event) => this.handleTranscriptEvent(event),
@@ -3217,7 +3618,8 @@ var PtySession = class {
3217
3618
  claudeSessionId: id
3218
3619
  });
3219
3620
  },
3220
- (event) => this.sendChatEvent(event)
3621
+ // No per-record transcript timestamp on this path; stamp emit time.
3622
+ (event) => this.sendChatEvent({ ...event, at: event.at ?? (/* @__PURE__ */ new Date()).toISOString() })
3221
3623
  );
3222
3624
  this.tailer = new JsonlTailer(
3223
3625
  eventsSinkPath,
@@ -3241,8 +3643,8 @@ var PtySession = class {
3241
3643
  * paths spawn() must wire into the child's argv/env.
3242
3644
  */
3243
3645
  async startStructuredEventSources(sessionId) {
3244
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-pty-"));
3245
- const socketPath = join7(this.tempDir, "hook.sock");
3646
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-pty-"));
3647
+ const socketPath = join8(this.tempDir, "hook.sock");
3246
3648
  this.socket = new HookSocketServer(
3247
3649
  socketPath,
3248
3650
  (progress) => this.handleProgress(progress),
@@ -3272,7 +3674,8 @@ var PtySession = class {
3272
3674
  * backgrounded gate/subagent just finished) and the chat-proxy projection.
3273
3675
  */
3274
3676
  handleRawRecord(raw) {
3275
- if (isBackgroundTaskNotificationRecord(raw)) this.bridge?.notifyBackgroundTaskDone?.();
3677
+ const completion = backgroundTaskCompletion(raw);
3678
+ if (completion) this.bridge?.notifyBackgroundTaskDone?.(completion.taskId);
3276
3679
  if (typeof this.bridge?.sendChatEvent === "function") this.relayChatRecord(raw);
3277
3680
  }
3278
3681
  /**
@@ -3283,7 +3686,7 @@ var PtySession = class {
3283
3686
  * live-rendered card is the one that flips to answered.
3284
3687
  */
3285
3688
  relayChatRecord(raw) {
3286
- for (const event of mapChatRecords(raw)) {
3689
+ for (const event of this.chatMapper.map(raw)) {
3287
3690
  if (event.kind === "tool_use" && event.name === "AskUserQuestion") {
3288
3691
  const syntheticId = this.pendingSyntheticQuestionIds.shift();
3289
3692
  if (syntheticId) {
@@ -3415,6 +3818,8 @@ var PtySession = class {
3415
3818
  this.unsubInput = null;
3416
3819
  this.unsubResize?.();
3417
3820
  this.unsubResize = null;
3821
+ this.unsubModelEffort?.();
3822
+ this.unsubModelEffort = null;
3418
3823
  if (this.abortHandler) {
3419
3824
  this.turn.abortController?.signal.removeEventListener("abort", this.abortHandler);
3420
3825
  this.abortHandler = null;
@@ -3866,7 +4271,7 @@ var PtySession = class {
3866
4271
  // src/harness/pty/config-home-health.ts
3867
4272
  import { lstat, mkdir as mkdir4, symlink, unlink as unlink2 } from "fs/promises";
3868
4273
  import { homedir as homedir3 } from "os";
3869
- import { join as join8 } from "path";
4274
+ import { join as join9 } from "path";
3870
4275
  var MOUNT_DISCONNECT_CODES = /* @__PURE__ */ new Set(["ENOTCONN", "EIO", "ESTALE", "ENXIO"]);
3871
4276
  var MOUNT_DISCONNECT_MESSAGES = [
3872
4277
  "socket is not connected",
@@ -3882,10 +4287,10 @@ function isMountDisconnectError(err) {
3882
4287
  return MOUNT_DISCONNECT_MESSAGES.some((needle) => lower.includes(needle));
3883
4288
  }
3884
4289
  function podLocalConfigHome() {
3885
- return join8(homedir3(), ".claude-local");
4290
+ return join9(homedir3(), ".claude-local");
3886
4291
  }
3887
4292
  function sharedConfigHomePath() {
3888
- return join8(homedir3(), ".claude");
4293
+ return join9(homedir3(), ".claude");
3889
4294
  }
3890
4295
  function isConfigHomeFallbackActive() {
3891
4296
  return claudeConfigHome() === podLocalConfigHome();
@@ -3916,7 +4321,7 @@ async function repointSharedConfigHomeSymlink(fallback, log) {
3916
4321
  }
3917
4322
  async function isConfigHomeMountDead(cwd) {
3918
4323
  try {
3919
- await mkdir4(join8(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
4324
+ await mkdir4(join9(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
3920
4325
  return false;
3921
4326
  } catch (err) {
3922
4327
  return isMountDisconnectError(err);
@@ -3925,7 +4330,7 @@ async function isConfigHomeMountDead(cwd) {
3925
4330
  async function ensureUsableClaudeConfigHome(cwd, log) {
3926
4331
  const configHome = claudeConfigHome();
3927
4332
  try {
3928
- await mkdir4(join8(configHome, "projects", projectSlug(cwd)), { recursive: true });
4333
+ await mkdir4(join9(configHome, "projects", projectSlug(cwd)), { recursive: true });
3929
4334
  return { configHome, fellBack: false };
3930
4335
  } catch (err) {
3931
4336
  if (!isMountDisconnectError(err)) throw err;
@@ -3939,7 +4344,7 @@ async function ensureUsableClaudeConfigHome(cwd, log) {
3939
4344
  }
3940
4345
  );
3941
4346
  process.env.CLAUDE_CONFIG_DIR = fallback;
3942
- await mkdir4(join8(fallback, "projects", projectSlug(cwd)), { recursive: true });
4347
+ await mkdir4(join9(fallback, "projects", projectSlug(cwd)), { recursive: true });
3943
4348
  await repointSharedConfigHomeSymlink(fallback, log);
3944
4349
  return { configHome: fallback, fellBack: true };
3945
4350
  }
@@ -3984,6 +4389,7 @@ var PtyHarness = class _PtyHarness {
3984
4389
  fingerprintOf(options) {
3985
4390
  return this.adapter.spawnFingerprint({
3986
4391
  model: options.model,
4392
+ ...options.effort ? { effort: options.effort } : {},
3987
4393
  permissionMode: options.permissionMode,
3988
4394
  ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
3989
4395
  cwd: options.cwd
@@ -4047,6 +4453,7 @@ var PtyHarness = class _PtyHarness {
4047
4453
  });
4048
4454
  }
4049
4455
  const fingerprint = this.fingerprintOf(options);
4456
+ await this.parked?.settleModelSwitch();
4050
4457
  let session;
4051
4458
  if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(options)) {
4052
4459
  session = this.parked;
@@ -4372,7 +4779,7 @@ import { spawn } from "child_process";
4372
4779
 
4373
4780
  // src/harness/pty/adapters/types.ts
4374
4781
  import { accessSync, constants, statSync } from "fs";
4375
- import { join as join9 } from "path";
4782
+ import { join as join10 } from "path";
4376
4783
  var TuiUnavailableError = class extends Error {
4377
4784
  constructor(tui, message) {
4378
4785
  super(message);
@@ -4396,7 +4803,7 @@ function findOnPath(binary, env = process.env) {
4396
4803
  }
4397
4804
  for (const dir of (env.PATH ?? "").split(":")) {
4398
4805
  if (!dir) continue;
4399
- const candidate = join9(dir, binary);
4806
+ const candidate = join10(dir, binary);
4400
4807
  if (isExecutable(candidate)) return candidate;
4401
4808
  }
4402
4809
  return null;
@@ -4404,18 +4811,18 @@ function findOnPath(binary, env = process.env) {
4404
4811
 
4405
4812
  // src/harness/pty/adapters/opencode-auth.ts
4406
4813
  import { promises as fs } from "fs";
4407
- import { dirname as dirname2, join as join10 } from "path";
4814
+ import { dirname as dirname2, join as join11 } from "path";
4408
4815
  import { homedir as homedir4 } from "os";
4409
4816
  var logger = createServiceLogger("opencode-auth");
4410
4817
  var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
4411
4818
  var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
4412
4819
  function opencodeAuthPath(env) {
4413
- const dataHome = env.XDG_DATA_HOME ?? join10(env.HOME ?? homedir4(), ".local", "share");
4414
- return join10(dataHome, "opencode", "auth.json");
4820
+ const dataHome = env.XDG_DATA_HOME ?? join11(env.HOME ?? homedir4(), ".local", "share");
4821
+ return join11(dataHome, "opencode", "auth.json");
4415
4822
  }
4416
4823
  function opencodeConfigPath(env) {
4417
- const configHome = env.XDG_CONFIG_HOME ?? join10(env.HOME ?? homedir4(), ".config");
4418
- return join10(configHome, "opencode", "opencode.json");
4824
+ const configHome = env.XDG_CONFIG_HOME ?? join11(env.HOME ?? homedir4(), ".config");
4825
+ return join11(configHome, "opencode", "opencode.json");
4419
4826
  }
4420
4827
  function parseOauthSeed(b64) {
4421
4828
  if (!b64) return null;
@@ -4608,8 +5015,8 @@ function buildOpenCodeConfigContent(input) {
4608
5015
  }
4609
5016
 
4610
5017
  // src/harness/opencode/index.ts
4611
- import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile7 } from "fs/promises";
4612
- import { join as join11 } from "path";
5018
+ import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile8 } from "fs/promises";
5019
+ import { join as join12 } from "path";
4613
5020
  var MAX_STDERR_TAIL = 4e3;
4614
5021
  var OpenCodeHeadlessHarness = class {
4615
5022
  /** NDJSON from `--format json` is a trusted structured source. */
@@ -4631,7 +5038,7 @@ var OpenCodeHeadlessHarness = class {
4631
5038
  const prompt = await collectPrompt(opts.prompt);
4632
5039
  const binary = resolveOpenCodeBinary(process.env);
4633
5040
  await prepareOpenCodeCredentials(process.env);
4634
- this.tempDir = await mkdtemp2(join11(sessionTempBase(), "opencode-headless-"));
5041
+ this.tempDir = await mkdtemp2(join12(sessionTempBase(), "opencode-headless-"));
4635
5042
  const { servers, entries } = await startToolServers(
4636
5043
  opts.options.mcpServers ?? {},
4637
5044
  this.tempDir
@@ -4713,8 +5120,8 @@ var OpenCodeHeadlessHarness = class {
4713
5120
  */
4714
5121
  async writeSystemPrompt(text) {
4715
5122
  if (!text || text.trim() === "") return null;
4716
- const path = join11(this.tempDir, "conveyor-instructions.md");
4717
- await writeFile7(path, text, "utf8");
5123
+ const path = join12(this.tempDir, "conveyor-instructions.md");
5124
+ await writeFile8(path, text, "utf8");
4718
5125
  return path;
4719
5126
  }
4720
5127
  /**
@@ -4854,25 +5261,25 @@ ${tail}`);
4854
5261
 
4855
5262
  // src/harness/pty/adapters/codex.ts
4856
5263
  import { realpathSync, writeFileSync as writeFileSync2 } from "fs";
4857
- import { basename, dirname as dirname3, join as join14 } from "path";
5264
+ import { basename, dirname as dirname3, join as join15 } from "path";
4858
5265
 
4859
5266
  // src/harness/codex/credentials.ts
4860
5267
  import { createHash as createHash2 } from "crypto";
4861
- import { mkdir as mkdir5, readFile as readFile2, writeFile as writeFile8 } from "fs/promises";
5268
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile9 } from "fs/promises";
4862
5269
  import { homedir as homedir5, tmpdir } from "os";
4863
- import { join as join12 } from "path";
5270
+ import { join as join13 } from "path";
4864
5271
  function codexHome(env = process.env) {
4865
5272
  const key = env.CONVEYOR_CODING_AGENT_KEY_ID ?? "local";
4866
5273
  const scope = createHash2("sha256").update(key).digest("hex").slice(0, 24);
4867
- return env.CONVEYOR_CODEX_HOME ?? join12(env.HOME ?? homedir5(), ".conveyor-codex", scope);
5274
+ return env.CONVEYOR_CODEX_HOME ?? join13(env.HOME ?? homedir5(), ".conveyor-codex", scope);
4868
5275
  }
4869
5276
  function codexSqliteHome(env = process.env) {
4870
5277
  const scope = createHash2("sha256").update(codexHome(env)).digest("hex").slice(0, 24);
4871
- return env.CONVEYOR_CODEX_SQLITE_HOME ?? join12(tmpdir(), "conveyor-codex-state", scope);
5278
+ return env.CONVEYOR_CODEX_SQLITE_HOME ?? join13(tmpdir(), "conveyor-codex-state", scope);
4872
5279
  }
4873
5280
  async function readAuth(path) {
4874
5281
  try {
4875
- return JSON.parse(await readFile2(path, "utf8"));
5282
+ return JSON.parse(await readFile3(path, "utf8"));
4876
5283
  } catch {
4877
5284
  return {};
4878
5285
  }
@@ -4881,7 +5288,7 @@ async function prepareCodexCredentials(env) {
4881
5288
  const home = codexHome(env);
4882
5289
  await mkdir5(home, { recursive: true, mode: 448 });
4883
5290
  await mkdir5(codexSqliteHome(env), { recursive: true, mode: 448 });
4884
- const path = join12(home, "auth.json");
5291
+ const path = join13(home, "auth.json");
4885
5292
  const seed = env.CONVEYOR_CODEX_OAUTH;
4886
5293
  if (seed) {
4887
5294
  const tokens = JSON.parse(Buffer.from(seed, "base64").toString("utf8"));
@@ -4892,10 +5299,10 @@ async function prepareCodexCredentials(env) {
4892
5299
  }
4893
5300
  const existing = await readAuth(path);
4894
5301
  const stored = existing.tokens;
4895
- const seededAt = await readAuth(join12(home, "conveyor-auth-seed.json"));
5302
+ const seededAt = await readAuth(join13(home, "conveyor-auth-seed.json"));
4896
5303
  if (stored?.refresh_token && stored.account_id === tokens.accountId && typeof seededAt.expires === "number" && seededAt.expires >= tokens.expires)
4897
5304
  return;
4898
- await writeFile8(
5305
+ await writeFile9(
4899
5306
  path,
4900
5307
  JSON.stringify({
4901
5308
  auth_mode: "chatgpt",
@@ -4910,13 +5317,13 @@ async function prepareCodexCredentials(env) {
4910
5317
  }),
4911
5318
  { mode: 384 }
4912
5319
  );
4913
- await writeFile8(
4914
- join12(home, "conveyor-auth-seed.json"),
5320
+ await writeFile9(
5321
+ join13(home, "conveyor-auth-seed.json"),
4915
5322
  JSON.stringify({ expires: tokens.expires }),
4916
5323
  { mode: 384 }
4917
5324
  );
4918
5325
  } else if (env.CONVEYOR_AGENT_KEY || env.OPENAI_API_KEY) {
4919
- await writeFile8(
5326
+ await writeFile9(
4920
5327
  path,
4921
5328
  JSON.stringify({ OPENAI_API_KEY: env.CONVEYOR_AGENT_KEY ?? env.OPENAI_API_KEY }),
4922
5329
  { mode: 384 }
@@ -4927,10 +5334,10 @@ async function prepareCodexCredentials(env) {
4927
5334
  // src/harness/codex/lineage.ts
4928
5335
  import { createHash as createHash3 } from "crypto";
4929
5336
  import { readFileSync, mkdirSync, writeFileSync, renameSync } from "fs";
4930
- import { join as join13 } from "path";
5337
+ import { join as join14 } from "path";
4931
5338
  function lineagePath(id, cwd, env = process.env) {
4932
5339
  const key = createHash3("sha256").update(`${cwd}\0${id}`).digest("hex");
4933
- return join13(codexHome(env), `conveyor-lineage-${key}.json`);
5340
+ return join14(codexHome(env), `conveyor-lineage-${key}.json`);
4934
5341
  }
4935
5342
  function readCodexLineage(id, cwd) {
4936
5343
  try {
@@ -5035,7 +5442,7 @@ var CodexTuiAdapter = class {
5035
5442
  if (effort) config.model_reasoning_effort = effort;
5036
5443
  if (input.pluginPath && input.eventsSinkPath) {
5037
5444
  const profile = basename(dirname3(input.pluginPath));
5038
- const profilePath = join14(env.CODEX_HOME, `${profile}.config.toml`);
5445
+ const profilePath = join15(env.CODEX_HOME, `${profile}.config.toml`);
5039
5446
  writeFileSync2(
5040
5447
  profilePath,
5041
5448
  `hooks = ${tomlValue(codexHookConfig(input.pluginPath, input.eventsSinkPath))}
@@ -5407,6 +5814,18 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
5407
5814
  };
5408
5815
  }
5409
5816
 
5817
+ // src/execution/model-override.ts
5818
+ function getModelOverride(env = process.env) {
5819
+ return env.CONVEYOR_MODEL_OVERRIDE || void 0;
5820
+ }
5821
+ function getEffortOverride(env = process.env) {
5822
+ return env.CONVEYOR_EFFORT_OVERRIDE || void 0;
5823
+ }
5824
+ function composerOverrides(runMode, env = process.env) {
5825
+ if (runMode.configMode === "code-review" || runMode.agentMode === "review") return {};
5826
+ return { model: getModelOverride(env), effort: getEffortOverride(env) };
5827
+ }
5828
+
5410
5829
  // src/execution/query-executor.ts
5411
5830
  import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
5412
5831
  import {
@@ -6375,10 +6794,10 @@ function buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode) {
6375
6794
 
6376
6795
  // src/execution/system-prompt.ts
6377
6796
  import { readFileSync as readFileSync2 } from "fs";
6378
- import { join as join15 } from "path";
6797
+ import { join as join16 } from "path";
6379
6798
  function repoHasScript(workspaceDir, script) {
6380
6799
  try {
6381
- const pkg = JSON.parse(readFileSync2(join15(workspaceDir, "package.json"), "utf8"));
6800
+ const pkg = JSON.parse(readFileSync2(join16(workspaceDir, "package.json"), "utf8"));
6382
6801
  return typeof pkg.scripts?.[script] === "string";
6383
6802
  } catch {
6384
6803
  return false;
@@ -9655,7 +10074,7 @@ function buildMutationTools(connection, config) {
9655
10074
  }
9656
10075
 
9657
10076
  // src/tools/attachment-tools.ts
9658
- import { basename as basename2, extname, isAbsolute, join as join16 } from "path";
10077
+ import { basename as basename2, extname, isAbsolute, join as join17 } from "path";
9659
10078
  var MIME_BY_EXT = {
9660
10079
  ".png": "image/png",
9661
10080
  ".jpg": "image/jpeg",
@@ -9722,7 +10141,7 @@ function buildUploadAttachmentTool(connection, config) {
9722
10141
  return defineContractTool(uploadAttachmentContract, async ({ path, title, comment, tags }) => {
9723
10142
  const caption = title ?? comment;
9724
10143
  try {
9725
- const filePath = isAbsolute(path) ? path : join16(config.workspaceDir, path);
10144
+ const filePath = isAbsolute(path) ? path : join17(config.workspaceDir, path);
9726
10145
  const mimeType = inferMimeType(filePath);
9727
10146
  const info = await statWorkspacePath(filePath);
9728
10147
  if (!info.isFile) {
@@ -10353,8 +10772,8 @@ function buildDiscoveryTools(connection) {
10353
10772
  import { z as z9 } from "zod";
10354
10773
 
10355
10774
  // src/execution/context-path-verifier.ts
10356
- import { readFile as readFile3 } from "fs/promises";
10357
- import { isAbsolute as isAbsolute2, join as join17, normalize } from "path";
10775
+ import { readFile as readFile4 } from "fs/promises";
10776
+ import { isAbsolute as isAbsolute2, join as join18, normalize } from "path";
10358
10777
  var PROBLEM_TEXT = {
10359
10778
  not_found: "does not exist in the repo",
10360
10779
  expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
@@ -10369,7 +10788,7 @@ async function verifyLocator(link, absolutePath, fileSize) {
10369
10788
  if (fileSize !== void 0 && fileSize > MAX_LOCATOR_FILE_BYTES) return null;
10370
10789
  let content;
10371
10790
  try {
10372
- content = await readFile3(absolutePath, "utf8");
10791
+ content = await readFile4(absolutePath, "utf8");
10373
10792
  } catch {
10374
10793
  return null;
10375
10794
  }
@@ -10399,7 +10818,7 @@ async function verifyContextPaths(links, workspaceDir) {
10399
10818
  problems.push({ type: link.type, path: link.path, reason: shape });
10400
10819
  continue;
10401
10820
  }
10402
- const absolutePath = join17(workspaceDir, toRelativePath(link.path));
10821
+ const absolutePath = join18(workspaceDir, toRelativePath(link.path));
10403
10822
  const stat = await statWorkspacePath(absolutePath);
10404
10823
  const wantsDirectory = expectsDirectory(link.type);
10405
10824
  if (!stat.exists) {
@@ -12748,6 +13167,7 @@ function buildDisallowedTools(settings, mode, hasExitedPlanMode) {
12748
13167
  function buildQueryOptions(host, context) {
12749
13168
  const settings = context.agentSettings ?? host.config.agentSettings ?? {};
12750
13169
  const mode = host.agentMode;
13170
+ const overrides = composerOverrides({ configMode: host.config.mode, agentMode: mode });
12751
13171
  const isReadOnly = isReadOnlyMode(mode, host.hasExitedPlanMode);
12752
13172
  const needsCanUseTool = isReadOnly;
12753
13173
  const systemPromptText = buildSystemPrompt(
@@ -12768,7 +13188,8 @@ function buildQueryOptions(host, context) {
12768
13188
  );
12769
13189
  const settingSources = settings.settingSources ?? ["user", "project"];
12770
13190
  return {
12771
- model: context.model || host.config.model,
13191
+ // The user's explicit composer choice outranks the task's own model.
13192
+ model: overrides.model || context.model || host.config.model,
12772
13193
  systemPrompt: {
12773
13194
  type: "preset",
12774
13195
  preset: "claude_code",
@@ -12802,7 +13223,7 @@ function buildQueryOptions(host, context) {
12802
13223
  sandbox: context.useSandbox ? { enabled: true } : { enabled: false },
12803
13224
  hooks: buildHooks(host),
12804
13225
  maxTurns: settings.maxTurns,
12805
- effort: settings.effort,
13226
+ effort: overrides.effort || settings.effort,
12806
13227
  thinking: settings.thinking,
12807
13228
  betas: settings.betas,
12808
13229
  codex: settings.codex,
@@ -13489,7 +13910,8 @@ function buildPtyBridge(connection, onBackgroundTaskDone) {
13489
13910
  notifyPromptDeliveryFailure: (detail) => connection.postChatMessage(detail),
13490
13911
  requestPodRecycle: (reason) => connection.requestWorkspaceRecycle(reason),
13491
13912
  onInput: (handler) => connection.onPtyInput(handler),
13492
- onResize: (handler) => connection.onPtyResize(handler)
13913
+ onResize: (handler) => connection.onPtyResize(handler),
13914
+ onModelEffort: (handler) => connection.onPtyModelEffort(handler)
13493
13915
  };
13494
13916
  }
13495
13917
  var QueryBridge = class {
@@ -13503,7 +13925,7 @@ var QueryBridge = class {
13503
13925
  let bridge;
13504
13926
  if (harnessKind === "pty") {
13505
13927
  this.directStream = wrapBridgeWithDirectStream(
13506
- buildPtyBridge(connection, () => this.onBackgroundTaskDone?.()),
13928
+ buildPtyBridge(connection, (taskId) => this.onBackgroundTaskDone?.(taskId)),
13507
13929
  connection
13508
13930
  );
13509
13931
  bridge = this.directStream.bridge;
@@ -14228,6 +14650,8 @@ function isBackgroundLaunch(tool2, input) {
14228
14650
  var BackgroundWorkTracker = class {
14229
14651
  /** Absolute expiry (epoch ms) of each outstanding launch, oldest first. */
14230
14652
  deadlines = [];
14653
+ /** CLI task ids already counted as complete. */
14654
+ completedTaskIds = /* @__PURE__ */ new Set();
14231
14655
  maxMs;
14232
14656
  onChange;
14233
14657
  log;
@@ -14256,7 +14680,11 @@ var BackgroundWorkTracker = class {
14256
14680
  * runner never saw, a re-injected record) must be a no-op, never a negative
14257
14681
  * balance that would keep the pod awake after the real work finished.
14258
14682
  */
14259
- noteCompletion(now = Date.now()) {
14683
+ noteCompletion(now = Date.now(), taskId = null) {
14684
+ if (taskId !== null) {
14685
+ if (this.completedTaskIds.has(taskId)) return;
14686
+ this.completedTaskIds.add(taskId);
14687
+ }
14260
14688
  this.prune(now);
14261
14689
  if (this.deadlines.length === 0) return;
14262
14690
  this.deadlines.shift();
@@ -14276,6 +14704,7 @@ var BackgroundWorkTracker = class {
14276
14704
  }
14277
14705
  /** Drop everything (shutdown / session teardown). */
14278
14706
  clear() {
14707
+ this.completedTaskIds.clear();
14279
14708
  if (this.deadlines.length === 0) return;
14280
14709
  this.deadlines.length = 0;
14281
14710
  this.onChange?.();
@@ -15644,7 +16073,7 @@ var SessionRunner = class _SessionRunner {
15644
16073
  onUserQuestion: (signal) => this.handleUserQuestionSignal(signal)
15645
16074
  });
15646
16075
  bridge.isParentTask = this.fullContext?.isParentTask ?? false;
15647
- bridge.onBackgroundTaskDone = () => this.backgroundWork.noteCompletion();
16076
+ bridge.onBackgroundTaskDone = (taskId) => this.backgroundWork.noteCompletion(Date.now(), taskId ?? null);
15648
16077
  bridge.onSoftStop = () => {
15649
16078
  process.stderr.write("[conveyor-agent] Soft stop requested (discovery ExitPlanMode)\n");
15650
16079
  this.softStop();
@@ -16014,6 +16443,8 @@ export {
16014
16443
  PtyHarness,
16015
16444
  resolveTuiKindFromEnv,
16016
16445
  resolveTuiAdapter,
16446
+ getModelOverride,
16447
+ getEffortOverride,
16017
16448
  buildProjectTools,
16018
16449
  resolvePlaywrightMcpServer,
16019
16450
  resolveSessionStart,