@rallycry/conveyor-agent 11.0.31 → 11.0.32

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,445 @@ 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 isBackgroundTaskNotificationRecord(raw) {
2116
+ if (!isRecord3(raw) || raw.type !== "user") return false;
2117
+ if (raw.isSidechain === true || raw.isMeta === true) return false;
2118
+ const text = userRecordText(raw);
2119
+ return text !== void 0 && text.trimStart().startsWith(TASK_NOTIFICATION_PREFIX);
2120
+ }
2121
+ function mapSystem2(record) {
2122
+ if (record.subtype === "init") {
2123
+ const event = {
2124
+ kind: "init",
2125
+ model: stringField2(record, "model") ?? ""
2126
+ };
2127
+ const sessionId = stringField2(record, "session_id", "sessionId");
2128
+ if (sessionId !== void 0) event.claudeSessionId = sessionId;
2129
+ return [event];
2130
+ }
2131
+ if (record.subtype === "turn_duration") {
2132
+ const event = { kind: "turn_end" };
2133
+ const durationMs = record.durationMs;
2134
+ if (typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0) {
2135
+ event.durationMs = Math.round(durationMs);
2136
+ }
2137
+ return [event];
2138
+ }
2139
+ return [];
2140
+ }
2141
+ function mapAssistant2(record) {
2142
+ const message = record.message;
2143
+ if (!isRecord3(message)) return [];
2144
+ const content = isUnknownArray2(message.content) ? message.content : [];
2145
+ const model = stringField2(message, "model");
2146
+ const events = [];
2147
+ for (const raw of content) {
2148
+ if (!isRecord3(raw)) continue;
2149
+ if (raw.type === "text") {
2150
+ const text = stringField2(raw, "text");
2151
+ if (text && text.length > 0) {
2152
+ const event = {
2153
+ kind: "assistant_text",
2154
+ text: truncate2(text, TEXT_MAX)
2155
+ };
2156
+ if (model && model !== "<synthetic>") event.model = truncate2(model, 200);
2157
+ events.push(event);
2158
+ }
2159
+ } else if (raw.type === "tool_use") {
2160
+ const name = stringField2(raw, "name");
2161
+ if (name) {
2162
+ const input = "input" in raw ? raw.input : void 0;
2163
+ const event = {
2164
+ kind: "tool_use",
2165
+ name: truncate2(name, 200),
2166
+ input: compactToolInput(name, input)
2167
+ };
2168
+ const id = stringField2(raw, "id");
2169
+ if (id !== void 0) event.id = id;
2170
+ events.push(event);
2171
+ }
2172
+ }
2173
+ }
2174
+ return events;
2175
+ }
2176
+ function toolResultText(block) {
2177
+ const content = block.content;
2178
+ if (typeof content === "string") return content;
2179
+ if (isUnknownArray2(content)) {
2180
+ return content.filter((b) => isRecord3(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
2181
+ }
2182
+ return "";
2183
+ }
2184
+ function mapToolResults(content) {
2185
+ const events = [];
2186
+ for (const raw of content) {
2187
+ if (!isRecord3(raw) || raw.type !== "tool_result") continue;
2188
+ const event = {
2189
+ kind: "tool_result",
2190
+ output: truncate2(toolResultText(raw), TOOL_OUTPUT_MAX),
2191
+ isError: raw.is_error === true
2192
+ };
2193
+ const toolUseId = stringField2(raw, "tool_use_id");
2194
+ if (toolUseId !== void 0) event.toolUseId = toolUseId;
2195
+ events.push(event);
2196
+ }
2197
+ return events;
2198
+ }
2199
+ function mapUser(record) {
2200
+ const message = record.message;
2201
+ if (!isRecord3(message)) return [];
2202
+ const content = message.content;
2203
+ if (isUnknownArray2(content) && content.some((b) => isRecord3(b) && b.type === "tool_result")) {
2204
+ return mapToolResults(content);
2205
+ }
2206
+ const text = userRecordText(record);
2207
+ if (text === void 0) return [];
2208
+ const trimmed = text.trim();
2209
+ if (trimmed.length === 0 || isNonConversationText(trimmed)) return [];
2210
+ return [{ kind: "user_text", text: truncate2(trimmed, TEXT_MAX) }];
2211
+ }
2212
+ function mapRecord(raw) {
2213
+ switch (raw.type) {
2214
+ case "system":
2215
+ return mapSystem2(raw);
2216
+ case "assistant":
2217
+ return mapAssistant2(raw);
2218
+ case "user":
2219
+ return mapUser(raw);
2220
+ case "result":
2221
+ return [{ kind: "turn_end" }];
2222
+ default:
2223
+ return [];
2224
+ }
2225
+ }
2226
+ function isChatRecord(raw) {
2227
+ return isRecord3(raw) && raw.isSidechain !== true && raw.isMeta !== true;
2228
+ }
2229
+ function mapChatRecords(raw) {
2230
+ if (!isChatRecord(raw)) return [];
2231
+ const events = mapRecord(raw);
2232
+ const at = stringField2(raw, "timestamp");
2233
+ if (at !== void 0 && at.length <= 40) {
2234
+ for (const event of events) event.at = at;
2235
+ }
2236
+ return events;
2237
+ }
2238
+ function createChatRecordMapper(rootDir) {
2239
+ const turn = new ChatTurnAccumulator();
2240
+ return {
2241
+ map(raw) {
2242
+ const events = mapChatRecords(raw);
2243
+ if (!isChatRecord(raw)) return events;
2244
+ if (raw.type === "assistant" && isRecord3(raw.message)) {
2245
+ turn.addAssistantMessage(raw.message);
2246
+ } else if (raw.type === "user") {
2247
+ if ("toolUseResult" in raw) turn.addToolUseResult(raw.toolUseResult, rootDir?.());
2248
+ if (events.some((e) => e.kind === "user_text")) turn.reset();
2249
+ }
2250
+ for (const event of events) {
2251
+ if (event.kind === "turn_end") Object.assign(event, turn.drain());
2252
+ }
2253
+ return events;
2254
+ }
2255
+ };
2256
+ }
2257
+
1920
2258
  // src/harness/pty/output-coalescer.ts
1921
2259
  var DEFAULT_FLUSH_MS = 40;
1922
2260
  var DEFAULT_MAX_BUFFER_CHARS = 48 * 1024;
@@ -1972,8 +2310,8 @@ var PtyOutputCoalescer = class {
1972
2310
  // src/harness/pty/tool-server.ts
1973
2311
  import { createServer as createServer2 } from "http";
1974
2312
  import { z as z2 } from "zod";
1975
- import { writeFile as writeFile4 } from "fs/promises";
1976
- import { join as join4 } from "path";
2313
+ import { writeFile as writeFile5 } from "fs/promises";
2314
+ import { join as join5 } from "path";
1977
2315
  import { randomBytes } from "crypto";
1978
2316
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1979
2317
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -2164,21 +2502,21 @@ async function startToolServers(mcpServers, tempDir) {
2164
2502
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
2165
2503
  }
2166
2504
  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");
2505
+ const mcpConfigPath = join5(tempDir, "mcp-config.json");
2506
+ await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
2169
2507
  return { servers, mcpConfigPath, entries: config };
2170
2508
  }
2171
2509
 
2172
2510
  // src/harness/pty/credentials.ts
2173
- import { chmod as chmod2, mkdir as mkdir2, readFile, rm, writeFile as writeFile5 } from "fs/promises";
2511
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile6 } from "fs/promises";
2174
2512
  import { homedir as homedir2 } from "os";
2175
- import { join as join6 } from "path";
2513
+ import { join as join7 } from "path";
2176
2514
 
2177
2515
  // src/harness/pty/credentials-marker.ts
2178
2516
  import { createHash } from "crypto";
2179
- import { join as join5 } from "path";
2517
+ import { join as join6 } from "path";
2180
2518
  function conveyorCredentialsMarkerPath() {
2181
- return join5(claudeConfigHome(), ".conveyor-credentials.json");
2519
+ return join6(claudeConfigHome(), ".conveyor-credentials.json");
2182
2520
  }
2183
2521
  function tokenFingerprint(accessToken) {
2184
2522
  return createHash("sha256").update(accessToken).digest("hex");
@@ -2374,14 +2712,14 @@ function planCredentialsWrite(input) {
2374
2712
 
2375
2713
  // src/harness/pty/credentials.ts
2376
2714
  function claudeCredentialsPath() {
2377
- return join6(claudeConfigHome(), ".credentials.json");
2715
+ return join7(claudeConfigHome(), ".credentials.json");
2378
2716
  }
2379
2717
  function isConveyorCloudEnv(env = process.env) {
2380
2718
  return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
2381
2719
  }
2382
2720
  async function readRaw(path) {
2383
2721
  try {
2384
- return await readFile(path, "utf8");
2722
+ return await readFile2(path, "utf8");
2385
2723
  } catch {
2386
2724
  return null;
2387
2725
  }
@@ -2410,11 +2748,11 @@ async function readCredentialsIdentity(env = process.env) {
2410
2748
  );
2411
2749
  }
2412
2750
  var READ_BACK_DELAYS_MS = [250, 500, 1e3, 2e3];
2413
- var defaultSleep = (ms) => new Promise((resolve) => {
2751
+ var defaultSleep2 = (ms) => new Promise((resolve) => {
2414
2752
  setTimeout(resolve, ms);
2415
2753
  });
2416
2754
  async function writeWithReadBackRetry(io, contents, delaysMs = READ_BACK_DELAYS_MS) {
2417
- const sleep2 = io.sleep ?? defaultSleep;
2755
+ const sleep2 = io.sleep ?? defaultSleep2;
2418
2756
  for (let attempt = 0; ; attempt++) {
2419
2757
  await io.write(contents);
2420
2758
  if (await io.read() === contents) return true;
@@ -2424,7 +2762,7 @@ async function writeWithReadBackRetry(io, contents, delaysMs = READ_BACK_DELAYS_
2424
2762
  }
2425
2763
  function fsWriteIo(path, mode) {
2426
2764
  return {
2427
- write: (contents) => writeFile5(path, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
2765
+ write: (contents) => writeFile6(path, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
2428
2766
  read: () => readRaw(path)
2429
2767
  };
2430
2768
  }
@@ -2504,7 +2842,7 @@ async function sanitizeApprovedApiKeys(oauthToken) {
2504
2842
  }
2505
2843
  function claudeJsonPath() {
2506
2844
  const configDir = process.env.CLAUDE_CONFIG_DIR;
2507
- return configDir ? join6(configDir, ".claude.json") : join6(homedir2(), ".claude.json");
2845
+ return configDir ? join7(configDir, ".claude.json") : join7(homedir2(), ".claude.json");
2508
2846
  }
2509
2847
  function asRecord(value) {
2510
2848
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
@@ -2597,7 +2935,7 @@ function planClaudeJsonSeed(existingRaw, trustCwd, oauthIdentity) {
2597
2935
  return changed ? JSON.stringify(config) : null;
2598
2936
  }
2599
2937
  function conveyorOauthMarkerPath() {
2600
- return join6(claudeConfigHome(), "conveyor-oauth-account.json");
2938
+ return join7(claudeConfigHome(), "conveyor-oauth-account.json");
2601
2939
  }
2602
2940
  function parseOauthIdentity(raw) {
2603
2941
  if (!raw || raw.trim() === "") return null;
@@ -2696,6 +3034,7 @@ var ClaudeTuiAdapter = class {
2696
3034
  ...resume ? { resume } : {},
2697
3035
  ...options.sessionId ? { sessionId: options.sessionId } : {},
2698
3036
  model: options.model,
3037
+ ...options.effort ? { effort: options.effort } : {},
2699
3038
  permissionMode: options.permissionMode,
2700
3039
  settingsPath: input.settingsPath ?? "",
2701
3040
  ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
@@ -2719,6 +3058,13 @@ var ClaudeTuiAdapter = class {
2719
3058
  encodePromptBytes(text) {
2720
3059
  return buildPromptBytes(text);
2721
3060
  }
3061
+ /** `/model` and `/effort` apply inline (verified on claude 2.1.280). */
3062
+ modelSwitchCommands(switchTo) {
3063
+ return [
3064
+ ...switchTo.model ? [`/model ${switchTo.model}`] : [],
3065
+ ...switchTo.effort ? [`/effort ${switchTo.effort}`] : []
3066
+ ];
3067
+ }
2722
3068
  buildExitErrors(exitCode, rawOutput) {
2723
3069
  return buildExitErrors(exitCode, rawOutput, this.resolveBinary());
2724
3070
  }
@@ -2799,6 +3145,19 @@ var PtySession = class {
2799
3145
  rows = 40;
2800
3146
  unsubInput = null;
2801
3147
  unsubResize = null;
3148
+ unsubModelEffort = null;
3149
+ turnsBegun = 0;
3150
+ // Live model/effort switch from the composer pill: typed as the CLI's own
3151
+ // slash commands between turns. Closures read `this` lazily (class fields
3152
+ // initialize before parameter properties).
3153
+ modelSwitch = new ModelSwitchQueue({
3154
+ canWriteNow: () => !this._toreDown && this.pty !== null && this.activeQueue === null && !this.questionnairePending,
3155
+ write: (bytes) => this.writeStdin(bytes),
3156
+ encode: (text) => this.adapter.encodePromptBytes(text),
3157
+ commands: (switchTo) => this.adapter.modelSwitchCommands?.(switchTo) ?? null,
3158
+ applied: (switchTo) => this.onModelSwitched(switchTo, true),
3159
+ deferred: (switchTo) => this.onModelSwitched(switchTo, false)
3160
+ });
2802
3161
  // In-process HTTP MCP servers exposing the harness tools to the spawned CLI,
2803
3162
  // plus the path to the `--mcp-config` that points at them.
2804
3163
  toolServers = [];
@@ -2829,6 +3188,11 @@ var PtySession = class {
2829
3188
  // synthetic id so the card flips to answered.
2830
3189
  pendingSyntheticQuestionIds = [];
2831
3190
  questionResultRemap = /* @__PURE__ */ new Map();
3191
+ // Chat projection with a per-turn accumulator (usage, model, edited files
3192
+ // ride the turn_end event), so it must live as long as the process.
3193
+ // Edited-file paths are made relative to the spawn cwd (the repo root), read
3194
+ // lazily because class fields initialize before parameter properties.
3195
+ chatMapper = createChatRecordMapper(() => this.options.cwd);
2832
3196
  // An AskUserQuestion questionnaire is parked on screen (PreToolUse hook
2833
3197
  // fired, no PostToolUse envelope yet). While set the parked process must
2834
3198
  // not be reused: feeding follow-up prompt bytes into the live dialog would
@@ -2916,6 +3280,7 @@ var PtySession = class {
2916
3280
  get spawnFingerprint() {
2917
3281
  return this.adapter.spawnFingerprint({
2918
3282
  model: this.options.model,
3283
+ ...this.options.effort ? { effort: this.options.effort } : {},
2919
3284
  permissionMode: this.options.permissionMode,
2920
3285
  ...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
2921
3286
  cwd: this.options.cwd
@@ -2978,6 +3343,7 @@ var PtySession = class {
2978
3343
  * the prompt into the live pty. The caller must have verified canReuse().
2979
3344
  */
2980
3345
  async beginTurn(prompt, options) {
3346
+ this.turnsBegun += 1;
2981
3347
  this.prewarmed = false;
2982
3348
  this.turnPrompt = prompt;
2983
3349
  this.turn = turnOptionsFrom(options);
@@ -3057,6 +3423,31 @@ var PtySession = class {
3057
3423
  this.turn.abortController.signal.removeEventListener("abort", this.abortHandler);
3058
3424
  this.abortHandler = null;
3059
3425
  }
3426
+ void this.modelSwitch.flush();
3427
+ }
3428
+ /** Resolves once an in-flight model/effort switch finished typing — the
3429
+ * harness awaits it before deciding whether this parked process is reusable. */
3430
+ settleModelSwitch() {
3431
+ return this.modelSwitch.settle();
3432
+ }
3433
+ /**
3434
+ * Record a model/effort switch and tell the chat. A live switch also updates
3435
+ * the spawn options so the recorded fingerprint matches the next turn's
3436
+ * (which carries the same override) — the parked process is reused, not
3437
+ * respawned.
3438
+ */
3439
+ onModelSwitched(switchTo, live) {
3440
+ if (live) {
3441
+ if (switchTo.model) this.options.model = switchTo.model;
3442
+ if (switchTo.effort) this.options.effort = switchTo.effort;
3443
+ }
3444
+ const label = [switchTo.model, switchTo.effort && `effort ${switchTo.effort}`].filter(Boolean).join(" \xB7 ");
3445
+ this.sendChatEvent({
3446
+ kind: "user_text",
3447
+ text: live ? `Switched to ${label}` : `Saved ${label} \u2014 applies on next wake`,
3448
+ source: "model_switch",
3449
+ at: (/* @__PURE__ */ new Date()).toISOString()
3450
+ });
3060
3451
  }
3061
3452
  async collectCodexPrompt() {
3062
3453
  if (this.adapter.id === "codex" && typeof this.turnPrompt !== "string") {
@@ -3106,7 +3497,7 @@ var PtySession = class {
3106
3497
  const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
3107
3498
  await this.spawn(settingsPath, socketPath);
3108
3499
  } else {
3109
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-pty-"));
3500
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-pty-"));
3110
3501
  await this.spawn();
3111
3502
  this.pushEvent({
3112
3503
  type: "system",
@@ -3128,6 +3519,7 @@ var PtySession = class {
3128
3519
  if (this.bridge) {
3129
3520
  if (feedPrompt) this.attachInput();
3130
3521
  this.unsubResize = this.bridge.onResize((cols, rows) => this.resizePty(cols, rows));
3522
+ this.unsubModelEffort = this.bridge.onModelEffort?.((switchTo) => this.modelSwitch.request(switchTo)) ?? null;
3131
3523
  }
3132
3524
  if (feedPrompt) await this.feedPrompt();
3133
3525
  } catch (err) {
@@ -3151,7 +3543,7 @@ var PtySession = class {
3151
3543
  * moment it is known, which is how the runner learns the resume target.
3152
3544
  */
3153
3545
  async startCodexEventSources() {
3154
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-codex-"));
3546
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-codex-"));
3155
3547
  const { servers, entries } = await startToolServers(
3156
3548
  this.options.mcpServers ?? {},
3157
3549
  this.tempDir
@@ -3175,7 +3567,8 @@ var PtySession = class {
3175
3567
  claudeSessionId: id
3176
3568
  });
3177
3569
  },
3178
- (event) => this.sendChatEvent(event)
3570
+ // No per-record transcript timestamp on this path; stamp emit time.
3571
+ (event) => this.sendChatEvent({ ...event, at: event.at ?? (/* @__PURE__ */ new Date()).toISOString() })
3179
3572
  );
3180
3573
  this.codexSource = source;
3181
3574
  this.tailer = new JsonlTailer(
@@ -3188,7 +3581,7 @@ var PtySession = class {
3188
3581
  return { ...extras, mcpEntries: entries };
3189
3582
  }
3190
3583
  async startOpenCodeEventSources() {
3191
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-pty-"));
3584
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-pty-"));
3192
3585
  const { servers, entries } = await startToolServers(
3193
3586
  this.options.mcpServers ?? {},
3194
3587
  this.tempDir
@@ -3198,8 +3591,8 @@ var PtySession = class {
3198
3591
  let instructionsPath;
3199
3592
  const systemPrompt = this.options.appendSystemPrompt;
3200
3593
  if (systemPrompt && systemPrompt.trim() !== "") {
3201
- instructionsPath = join7(this.tempDir, "conveyor-instructions.md");
3202
- await writeFile6(instructionsPath, systemPrompt, "utf8");
3594
+ instructionsPath = join8(this.tempDir, "conveyor-instructions.md");
3595
+ await writeFile7(instructionsPath, systemPrompt, "utf8");
3203
3596
  }
3204
3597
  this.opencodeSource = new OpenCodeEventSource(
3205
3598
  (event) => this.handleTranscriptEvent(event),
@@ -3217,7 +3610,8 @@ var PtySession = class {
3217
3610
  claudeSessionId: id
3218
3611
  });
3219
3612
  },
3220
- (event) => this.sendChatEvent(event)
3613
+ // No per-record transcript timestamp on this path; stamp emit time.
3614
+ (event) => this.sendChatEvent({ ...event, at: event.at ?? (/* @__PURE__ */ new Date()).toISOString() })
3221
3615
  );
3222
3616
  this.tailer = new JsonlTailer(
3223
3617
  eventsSinkPath,
@@ -3241,8 +3635,8 @@ var PtySession = class {
3241
3635
  * paths spawn() must wire into the child's argv/env.
3242
3636
  */
3243
3637
  async startStructuredEventSources(sessionId) {
3244
- this.tempDir = await mkdtemp(join7(sessionTempBase(), "conveyor-pty-"));
3245
- const socketPath = join7(this.tempDir, "hook.sock");
3638
+ this.tempDir = await mkdtemp(join8(sessionTempBase(), "conveyor-pty-"));
3639
+ const socketPath = join8(this.tempDir, "hook.sock");
3246
3640
  this.socket = new HookSocketServer(
3247
3641
  socketPath,
3248
3642
  (progress) => this.handleProgress(progress),
@@ -3283,7 +3677,7 @@ var PtySession = class {
3283
3677
  * live-rendered card is the one that flips to answered.
3284
3678
  */
3285
3679
  relayChatRecord(raw) {
3286
- for (const event of mapChatRecords(raw)) {
3680
+ for (const event of this.chatMapper.map(raw)) {
3287
3681
  if (event.kind === "tool_use" && event.name === "AskUserQuestion") {
3288
3682
  const syntheticId = this.pendingSyntheticQuestionIds.shift();
3289
3683
  if (syntheticId) {
@@ -3415,6 +3809,8 @@ var PtySession = class {
3415
3809
  this.unsubInput = null;
3416
3810
  this.unsubResize?.();
3417
3811
  this.unsubResize = null;
3812
+ this.unsubModelEffort?.();
3813
+ this.unsubModelEffort = null;
3418
3814
  if (this.abortHandler) {
3419
3815
  this.turn.abortController?.signal.removeEventListener("abort", this.abortHandler);
3420
3816
  this.abortHandler = null;
@@ -3866,7 +4262,7 @@ var PtySession = class {
3866
4262
  // src/harness/pty/config-home-health.ts
3867
4263
  import { lstat, mkdir as mkdir4, symlink, unlink as unlink2 } from "fs/promises";
3868
4264
  import { homedir as homedir3 } from "os";
3869
- import { join as join8 } from "path";
4265
+ import { join as join9 } from "path";
3870
4266
  var MOUNT_DISCONNECT_CODES = /* @__PURE__ */ new Set(["ENOTCONN", "EIO", "ESTALE", "ENXIO"]);
3871
4267
  var MOUNT_DISCONNECT_MESSAGES = [
3872
4268
  "socket is not connected",
@@ -3882,10 +4278,10 @@ function isMountDisconnectError(err) {
3882
4278
  return MOUNT_DISCONNECT_MESSAGES.some((needle) => lower.includes(needle));
3883
4279
  }
3884
4280
  function podLocalConfigHome() {
3885
- return join8(homedir3(), ".claude-local");
4281
+ return join9(homedir3(), ".claude-local");
3886
4282
  }
3887
4283
  function sharedConfigHomePath() {
3888
- return join8(homedir3(), ".claude");
4284
+ return join9(homedir3(), ".claude");
3889
4285
  }
3890
4286
  function isConfigHomeFallbackActive() {
3891
4287
  return claudeConfigHome() === podLocalConfigHome();
@@ -3916,7 +4312,7 @@ async function repointSharedConfigHomeSymlink(fallback, log) {
3916
4312
  }
3917
4313
  async function isConfigHomeMountDead(cwd) {
3918
4314
  try {
3919
- await mkdir4(join8(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
4315
+ await mkdir4(join9(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
3920
4316
  return false;
3921
4317
  } catch (err) {
3922
4318
  return isMountDisconnectError(err);
@@ -3925,7 +4321,7 @@ async function isConfigHomeMountDead(cwd) {
3925
4321
  async function ensureUsableClaudeConfigHome(cwd, log) {
3926
4322
  const configHome = claudeConfigHome();
3927
4323
  try {
3928
- await mkdir4(join8(configHome, "projects", projectSlug(cwd)), { recursive: true });
4324
+ await mkdir4(join9(configHome, "projects", projectSlug(cwd)), { recursive: true });
3929
4325
  return { configHome, fellBack: false };
3930
4326
  } catch (err) {
3931
4327
  if (!isMountDisconnectError(err)) throw err;
@@ -3939,7 +4335,7 @@ async function ensureUsableClaudeConfigHome(cwd, log) {
3939
4335
  }
3940
4336
  );
3941
4337
  process.env.CLAUDE_CONFIG_DIR = fallback;
3942
- await mkdir4(join8(fallback, "projects", projectSlug(cwd)), { recursive: true });
4338
+ await mkdir4(join9(fallback, "projects", projectSlug(cwd)), { recursive: true });
3943
4339
  await repointSharedConfigHomeSymlink(fallback, log);
3944
4340
  return { configHome: fallback, fellBack: true };
3945
4341
  }
@@ -3984,6 +4380,7 @@ var PtyHarness = class _PtyHarness {
3984
4380
  fingerprintOf(options) {
3985
4381
  return this.adapter.spawnFingerprint({
3986
4382
  model: options.model,
4383
+ ...options.effort ? { effort: options.effort } : {},
3987
4384
  permissionMode: options.permissionMode,
3988
4385
  ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
3989
4386
  cwd: options.cwd
@@ -4047,6 +4444,7 @@ var PtyHarness = class _PtyHarness {
4047
4444
  });
4048
4445
  }
4049
4446
  const fingerprint = this.fingerprintOf(options);
4447
+ await this.parked?.settleModelSwitch();
4050
4448
  let session;
4051
4449
  if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(options)) {
4052
4450
  session = this.parked;
@@ -4372,7 +4770,7 @@ import { spawn } from "child_process";
4372
4770
 
4373
4771
  // src/harness/pty/adapters/types.ts
4374
4772
  import { accessSync, constants, statSync } from "fs";
4375
- import { join as join9 } from "path";
4773
+ import { join as join10 } from "path";
4376
4774
  var TuiUnavailableError = class extends Error {
4377
4775
  constructor(tui, message) {
4378
4776
  super(message);
@@ -4396,7 +4794,7 @@ function findOnPath(binary, env = process.env) {
4396
4794
  }
4397
4795
  for (const dir of (env.PATH ?? "").split(":")) {
4398
4796
  if (!dir) continue;
4399
- const candidate = join9(dir, binary);
4797
+ const candidate = join10(dir, binary);
4400
4798
  if (isExecutable(candidate)) return candidate;
4401
4799
  }
4402
4800
  return null;
@@ -4404,18 +4802,18 @@ function findOnPath(binary, env = process.env) {
4404
4802
 
4405
4803
  // src/harness/pty/adapters/opencode-auth.ts
4406
4804
  import { promises as fs } from "fs";
4407
- import { dirname as dirname2, join as join10 } from "path";
4805
+ import { dirname as dirname2, join as join11 } from "path";
4408
4806
  import { homedir as homedir4 } from "os";
4409
4807
  var logger = createServiceLogger("opencode-auth");
4410
4808
  var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
4411
4809
  var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
4412
4810
  function opencodeAuthPath(env) {
4413
- const dataHome = env.XDG_DATA_HOME ?? join10(env.HOME ?? homedir4(), ".local", "share");
4414
- return join10(dataHome, "opencode", "auth.json");
4811
+ const dataHome = env.XDG_DATA_HOME ?? join11(env.HOME ?? homedir4(), ".local", "share");
4812
+ return join11(dataHome, "opencode", "auth.json");
4415
4813
  }
4416
4814
  function opencodeConfigPath(env) {
4417
- const configHome = env.XDG_CONFIG_HOME ?? join10(env.HOME ?? homedir4(), ".config");
4418
- return join10(configHome, "opencode", "opencode.json");
4815
+ const configHome = env.XDG_CONFIG_HOME ?? join11(env.HOME ?? homedir4(), ".config");
4816
+ return join11(configHome, "opencode", "opencode.json");
4419
4817
  }
4420
4818
  function parseOauthSeed(b64) {
4421
4819
  if (!b64) return null;
@@ -4608,8 +5006,8 @@ function buildOpenCodeConfigContent(input) {
4608
5006
  }
4609
5007
 
4610
5008
  // 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";
5009
+ import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile8 } from "fs/promises";
5010
+ import { join as join12 } from "path";
4613
5011
  var MAX_STDERR_TAIL = 4e3;
4614
5012
  var OpenCodeHeadlessHarness = class {
4615
5013
  /** NDJSON from `--format json` is a trusted structured source. */
@@ -4631,7 +5029,7 @@ var OpenCodeHeadlessHarness = class {
4631
5029
  const prompt = await collectPrompt(opts.prompt);
4632
5030
  const binary = resolveOpenCodeBinary(process.env);
4633
5031
  await prepareOpenCodeCredentials(process.env);
4634
- this.tempDir = await mkdtemp2(join11(sessionTempBase(), "opencode-headless-"));
5032
+ this.tempDir = await mkdtemp2(join12(sessionTempBase(), "opencode-headless-"));
4635
5033
  const { servers, entries } = await startToolServers(
4636
5034
  opts.options.mcpServers ?? {},
4637
5035
  this.tempDir
@@ -4713,8 +5111,8 @@ var OpenCodeHeadlessHarness = class {
4713
5111
  */
4714
5112
  async writeSystemPrompt(text) {
4715
5113
  if (!text || text.trim() === "") return null;
4716
- const path = join11(this.tempDir, "conveyor-instructions.md");
4717
- await writeFile7(path, text, "utf8");
5114
+ const path = join12(this.tempDir, "conveyor-instructions.md");
5115
+ await writeFile8(path, text, "utf8");
4718
5116
  return path;
4719
5117
  }
4720
5118
  /**
@@ -4854,25 +5252,25 @@ ${tail}`);
4854
5252
 
4855
5253
  // src/harness/pty/adapters/codex.ts
4856
5254
  import { realpathSync, writeFileSync as writeFileSync2 } from "fs";
4857
- import { basename, dirname as dirname3, join as join14 } from "path";
5255
+ import { basename, dirname as dirname3, join as join15 } from "path";
4858
5256
 
4859
5257
  // src/harness/codex/credentials.ts
4860
5258
  import { createHash as createHash2 } from "crypto";
4861
- import { mkdir as mkdir5, readFile as readFile2, writeFile as writeFile8 } from "fs/promises";
5259
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile9 } from "fs/promises";
4862
5260
  import { homedir as homedir5, tmpdir } from "os";
4863
- import { join as join12 } from "path";
5261
+ import { join as join13 } from "path";
4864
5262
  function codexHome(env = process.env) {
4865
5263
  const key = env.CONVEYOR_CODING_AGENT_KEY_ID ?? "local";
4866
5264
  const scope = createHash2("sha256").update(key).digest("hex").slice(0, 24);
4867
- return env.CONVEYOR_CODEX_HOME ?? join12(env.HOME ?? homedir5(), ".conveyor-codex", scope);
5265
+ return env.CONVEYOR_CODEX_HOME ?? join13(env.HOME ?? homedir5(), ".conveyor-codex", scope);
4868
5266
  }
4869
5267
  function codexSqliteHome(env = process.env) {
4870
5268
  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);
5269
+ return env.CONVEYOR_CODEX_SQLITE_HOME ?? join13(tmpdir(), "conveyor-codex-state", scope);
4872
5270
  }
4873
5271
  async function readAuth(path) {
4874
5272
  try {
4875
- return JSON.parse(await readFile2(path, "utf8"));
5273
+ return JSON.parse(await readFile3(path, "utf8"));
4876
5274
  } catch {
4877
5275
  return {};
4878
5276
  }
@@ -4881,7 +5279,7 @@ async function prepareCodexCredentials(env) {
4881
5279
  const home = codexHome(env);
4882
5280
  await mkdir5(home, { recursive: true, mode: 448 });
4883
5281
  await mkdir5(codexSqliteHome(env), { recursive: true, mode: 448 });
4884
- const path = join12(home, "auth.json");
5282
+ const path = join13(home, "auth.json");
4885
5283
  const seed = env.CONVEYOR_CODEX_OAUTH;
4886
5284
  if (seed) {
4887
5285
  const tokens = JSON.parse(Buffer.from(seed, "base64").toString("utf8"));
@@ -4892,10 +5290,10 @@ async function prepareCodexCredentials(env) {
4892
5290
  }
4893
5291
  const existing = await readAuth(path);
4894
5292
  const stored = existing.tokens;
4895
- const seededAt = await readAuth(join12(home, "conveyor-auth-seed.json"));
5293
+ const seededAt = await readAuth(join13(home, "conveyor-auth-seed.json"));
4896
5294
  if (stored?.refresh_token && stored.account_id === tokens.accountId && typeof seededAt.expires === "number" && seededAt.expires >= tokens.expires)
4897
5295
  return;
4898
- await writeFile8(
5296
+ await writeFile9(
4899
5297
  path,
4900
5298
  JSON.stringify({
4901
5299
  auth_mode: "chatgpt",
@@ -4910,13 +5308,13 @@ async function prepareCodexCredentials(env) {
4910
5308
  }),
4911
5309
  { mode: 384 }
4912
5310
  );
4913
- await writeFile8(
4914
- join12(home, "conveyor-auth-seed.json"),
5311
+ await writeFile9(
5312
+ join13(home, "conveyor-auth-seed.json"),
4915
5313
  JSON.stringify({ expires: tokens.expires }),
4916
5314
  { mode: 384 }
4917
5315
  );
4918
5316
  } else if (env.CONVEYOR_AGENT_KEY || env.OPENAI_API_KEY) {
4919
- await writeFile8(
5317
+ await writeFile9(
4920
5318
  path,
4921
5319
  JSON.stringify({ OPENAI_API_KEY: env.CONVEYOR_AGENT_KEY ?? env.OPENAI_API_KEY }),
4922
5320
  { mode: 384 }
@@ -4927,10 +5325,10 @@ async function prepareCodexCredentials(env) {
4927
5325
  // src/harness/codex/lineage.ts
4928
5326
  import { createHash as createHash3 } from "crypto";
4929
5327
  import { readFileSync, mkdirSync, writeFileSync, renameSync } from "fs";
4930
- import { join as join13 } from "path";
5328
+ import { join as join14 } from "path";
4931
5329
  function lineagePath(id, cwd, env = process.env) {
4932
5330
  const key = createHash3("sha256").update(`${cwd}\0${id}`).digest("hex");
4933
- return join13(codexHome(env), `conveyor-lineage-${key}.json`);
5331
+ return join14(codexHome(env), `conveyor-lineage-${key}.json`);
4934
5332
  }
4935
5333
  function readCodexLineage(id, cwd) {
4936
5334
  try {
@@ -5035,7 +5433,7 @@ var CodexTuiAdapter = class {
5035
5433
  if (effort) config.model_reasoning_effort = effort;
5036
5434
  if (input.pluginPath && input.eventsSinkPath) {
5037
5435
  const profile = basename(dirname3(input.pluginPath));
5038
- const profilePath = join14(env.CODEX_HOME, `${profile}.config.toml`);
5436
+ const profilePath = join15(env.CODEX_HOME, `${profile}.config.toml`);
5039
5437
  writeFileSync2(
5040
5438
  profilePath,
5041
5439
  `hooks = ${tomlValue(codexHookConfig(input.pluginPath, input.eventsSinkPath))}
@@ -5407,6 +5805,18 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
5407
5805
  };
5408
5806
  }
5409
5807
 
5808
+ // src/execution/model-override.ts
5809
+ function getModelOverride(env = process.env) {
5810
+ return env.CONVEYOR_MODEL_OVERRIDE || void 0;
5811
+ }
5812
+ function getEffortOverride(env = process.env) {
5813
+ return env.CONVEYOR_EFFORT_OVERRIDE || void 0;
5814
+ }
5815
+ function composerOverrides(runMode, env = process.env) {
5816
+ if (runMode.configMode === "code-review" || runMode.agentMode === "review") return {};
5817
+ return { model: getModelOverride(env), effort: getEffortOverride(env) };
5818
+ }
5819
+
5410
5820
  // src/execution/query-executor.ts
5411
5821
  import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
5412
5822
  import {
@@ -6375,10 +6785,10 @@ function buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode) {
6375
6785
 
6376
6786
  // src/execution/system-prompt.ts
6377
6787
  import { readFileSync as readFileSync2 } from "fs";
6378
- import { join as join15 } from "path";
6788
+ import { join as join16 } from "path";
6379
6789
  function repoHasScript(workspaceDir, script) {
6380
6790
  try {
6381
- const pkg = JSON.parse(readFileSync2(join15(workspaceDir, "package.json"), "utf8"));
6791
+ const pkg = JSON.parse(readFileSync2(join16(workspaceDir, "package.json"), "utf8"));
6382
6792
  return typeof pkg.scripts?.[script] === "string";
6383
6793
  } catch {
6384
6794
  return false;
@@ -9655,7 +10065,7 @@ function buildMutationTools(connection, config) {
9655
10065
  }
9656
10066
 
9657
10067
  // src/tools/attachment-tools.ts
9658
- import { basename as basename2, extname, isAbsolute, join as join16 } from "path";
10068
+ import { basename as basename2, extname, isAbsolute, join as join17 } from "path";
9659
10069
  var MIME_BY_EXT = {
9660
10070
  ".png": "image/png",
9661
10071
  ".jpg": "image/jpeg",
@@ -9722,7 +10132,7 @@ function buildUploadAttachmentTool(connection, config) {
9722
10132
  return defineContractTool(uploadAttachmentContract, async ({ path, title, comment, tags }) => {
9723
10133
  const caption = title ?? comment;
9724
10134
  try {
9725
- const filePath = isAbsolute(path) ? path : join16(config.workspaceDir, path);
10135
+ const filePath = isAbsolute(path) ? path : join17(config.workspaceDir, path);
9726
10136
  const mimeType = inferMimeType(filePath);
9727
10137
  const info = await statWorkspacePath(filePath);
9728
10138
  if (!info.isFile) {
@@ -10353,8 +10763,8 @@ function buildDiscoveryTools(connection) {
10353
10763
  import { z as z9 } from "zod";
10354
10764
 
10355
10765
  // 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";
10766
+ import { readFile as readFile4 } from "fs/promises";
10767
+ import { isAbsolute as isAbsolute2, join as join18, normalize } from "path";
10358
10768
  var PROBLEM_TEXT = {
10359
10769
  not_found: "does not exist in the repo",
10360
10770
  expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
@@ -10369,7 +10779,7 @@ async function verifyLocator(link, absolutePath, fileSize) {
10369
10779
  if (fileSize !== void 0 && fileSize > MAX_LOCATOR_FILE_BYTES) return null;
10370
10780
  let content;
10371
10781
  try {
10372
- content = await readFile3(absolutePath, "utf8");
10782
+ content = await readFile4(absolutePath, "utf8");
10373
10783
  } catch {
10374
10784
  return null;
10375
10785
  }
@@ -10399,7 +10809,7 @@ async function verifyContextPaths(links, workspaceDir) {
10399
10809
  problems.push({ type: link.type, path: link.path, reason: shape });
10400
10810
  continue;
10401
10811
  }
10402
- const absolutePath = join17(workspaceDir, toRelativePath(link.path));
10812
+ const absolutePath = join18(workspaceDir, toRelativePath(link.path));
10403
10813
  const stat = await statWorkspacePath(absolutePath);
10404
10814
  const wantsDirectory = expectsDirectory(link.type);
10405
10815
  if (!stat.exists) {
@@ -12748,6 +13158,7 @@ function buildDisallowedTools(settings, mode, hasExitedPlanMode) {
12748
13158
  function buildQueryOptions(host, context) {
12749
13159
  const settings = context.agentSettings ?? host.config.agentSettings ?? {};
12750
13160
  const mode = host.agentMode;
13161
+ const overrides = composerOverrides({ configMode: host.config.mode, agentMode: mode });
12751
13162
  const isReadOnly = isReadOnlyMode(mode, host.hasExitedPlanMode);
12752
13163
  const needsCanUseTool = isReadOnly;
12753
13164
  const systemPromptText = buildSystemPrompt(
@@ -12768,7 +13179,8 @@ function buildQueryOptions(host, context) {
12768
13179
  );
12769
13180
  const settingSources = settings.settingSources ?? ["user", "project"];
12770
13181
  return {
12771
- model: context.model || host.config.model,
13182
+ // The user's explicit composer choice outranks the task's own model.
13183
+ model: overrides.model || context.model || host.config.model,
12772
13184
  systemPrompt: {
12773
13185
  type: "preset",
12774
13186
  preset: "claude_code",
@@ -12802,7 +13214,7 @@ function buildQueryOptions(host, context) {
12802
13214
  sandbox: context.useSandbox ? { enabled: true } : { enabled: false },
12803
13215
  hooks: buildHooks(host),
12804
13216
  maxTurns: settings.maxTurns,
12805
- effort: settings.effort,
13217
+ effort: overrides.effort || settings.effort,
12806
13218
  thinking: settings.thinking,
12807
13219
  betas: settings.betas,
12808
13220
  codex: settings.codex,
@@ -13489,7 +13901,8 @@ function buildPtyBridge(connection, onBackgroundTaskDone) {
13489
13901
  notifyPromptDeliveryFailure: (detail) => connection.postChatMessage(detail),
13490
13902
  requestPodRecycle: (reason) => connection.requestWorkspaceRecycle(reason),
13491
13903
  onInput: (handler) => connection.onPtyInput(handler),
13492
- onResize: (handler) => connection.onPtyResize(handler)
13904
+ onResize: (handler) => connection.onPtyResize(handler),
13905
+ onModelEffort: (handler) => connection.onPtyModelEffort(handler)
13493
13906
  };
13494
13907
  }
13495
13908
  var QueryBridge = class {
@@ -16014,6 +16427,8 @@ export {
16014
16427
  PtyHarness,
16015
16428
  resolveTuiKindFromEnv,
16016
16429
  resolveTuiAdapter,
16430
+ getModelOverride,
16431
+ getEffortOverride,
16017
16432
  buildProjectTools,
16018
16433
  resolvePlaywrightMcpServer,
16019
16434
  resolveSessionStart,