@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.
- package/dist/{boot-65PTSADA.js → boot-T5HQVF3W.js} +2 -2
- package/dist/{chunk-WCJ6HRVU.js → chunk-EKTPDG3G.js} +5 -6
- package/dist/{chunk-EITUTXXJ.js → chunk-F7XHSNU7.js} +1 -1
- package/dist/{chunk-3MEPGN2A.js → chunk-IHNXVFIB.js} +1195 -1137
- package/dist/{chunk-RFACW7A4.js → chunk-Q2E2TM3J.js} +681 -266
- package/dist/{chunk-C76KMDGU.js → chunk-XZDTLVWA.js} +1 -1
- package/dist/cli.js +19 -9
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -3
- package/dist/{serve-boot-WXNLUX77.js → serve-boot-VDR3RXRA.js} +2 -2
- package/dist/{server-E6IYHSMN.js → server-GJZKUVBE.js} +2 -2
- package/package.json +1 -1
|
@@ -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-
|
|
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-
|
|
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
|
|
403
|
-
import { join as
|
|
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?.({
|
|
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,
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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/
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
|
1976
|
-
import { join as
|
|
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 =
|
|
2168
|
-
await
|
|
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
|
|
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
|
|
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
|
|
2517
|
+
import { join as join6 } from "path";
|
|
2180
2518
|
function conveyorCredentialsMarkerPath() {
|
|
2181
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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) =>
|
|
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 ?
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
3202
|
-
await
|
|
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
|
-
|
|
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(
|
|
3245
|
-
const socketPath =
|
|
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
|
|
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
|
|
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
|
|
4281
|
+
return join9(homedir3(), ".claude-local");
|
|
3886
4282
|
}
|
|
3887
4283
|
function sharedConfigHomePath() {
|
|
3888
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 ??
|
|
4414
|
-
return
|
|
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 ??
|
|
4418
|
-
return
|
|
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
|
|
4612
|
-
import { join as
|
|
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(
|
|
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 =
|
|
4717
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
4914
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
6788
|
+
import { join as join16 } from "path";
|
|
6379
6789
|
function repoHasScript(workspaceDir, script) {
|
|
6380
6790
|
try {
|
|
6381
|
-
const pkg = JSON.parse(readFileSync2(
|
|
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
|
|
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 :
|
|
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
|
|
10357
|
-
import { isAbsolute as isAbsolute2, join as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|