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