@rallycry/conveyor-agent 10.13.50 → 10.13.51
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-I353EAK6.js → boot-BZE36LT6.js} +3 -3
- package/dist/{chunk-36VMMHYD.js → chunk-EQO7QFGN.js} +780 -171
- package/dist/chunk-EQO7QFGN.js.map +1 -0
- package/dist/{chunk-3Z4YQNJV.js → chunk-HVSPADKP.js} +2 -2
- package/dist/{chunk-XR6H326I.js → chunk-KZQBYSMN.js} +112 -1
- package/dist/chunk-KZQBYSMN.js.map +1 -0
- package/dist/cli.js +73 -241
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-NIOBJ46G.js → server-PPTELJWY.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-36VMMHYD.js.map +0 -1
- package/dist/chunk-XR6H326I.js.map +0 -1
- /package/dist/{boot-I353EAK6.js.map → boot-BZE36LT6.js.map} +0 -0
- /package/dist/{chunk-3Z4YQNJV.js.map → chunk-HVSPADKP.js.map} +0 -0
- /package/dist/{server-NIOBJ46G.js.map → server-PPTELJWY.js.map} +0 -0
|
@@ -26,21 +26,30 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
MAX_BETWEEN_TURN_BUFFER,
|
|
28
28
|
MAX_DIAGNOSTIC_OUTPUT,
|
|
29
|
+
buildExitErrors,
|
|
29
30
|
buildPromptBytes,
|
|
31
|
+
buildSpawnArgs,
|
|
32
|
+
cleanTerminalOutput,
|
|
30
33
|
inheritedEnv,
|
|
31
34
|
killPtyWithEscalation,
|
|
35
|
+
needsRawReadyGate,
|
|
32
36
|
parseUserQuestions,
|
|
33
37
|
renderPromptContentText,
|
|
38
|
+
resolveClaudeBinary,
|
|
34
39
|
resolvePlanDialogTiming,
|
|
35
40
|
resolvePtySpawn,
|
|
41
|
+
resolveRawTuiProbeTiming,
|
|
36
42
|
resolveSubmitNudgeTiming,
|
|
37
43
|
resolveSubmitRedeliveryMaxAttempts,
|
|
38
44
|
resolveSubmitSettleMs,
|
|
45
|
+
sawTerminalSetup,
|
|
46
|
+
sentinelEchoed,
|
|
39
47
|
sessionTempBase,
|
|
40
48
|
sleep,
|
|
49
|
+
spawnOptionsFingerprint,
|
|
41
50
|
transcriptSize,
|
|
42
51
|
turnOptionsFrom
|
|
43
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-KZQBYSMN.js";
|
|
44
53
|
|
|
45
54
|
// src/setup/bootstrap.ts
|
|
46
55
|
var BOOTSTRAP_TIMEOUT_MS = 3e4;
|
|
@@ -1728,7 +1737,11 @@ import { z as z6 } from "zod";
|
|
|
1728
1737
|
import { z as z7 } from "zod";
|
|
1729
1738
|
var EXTERNAL_AGENT_MESSAGE_SOURCE = "external_agent";
|
|
1730
1739
|
var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
|
|
1740
|
+
var DEFAULT_OPUS_MODEL = "claude-opus-5";
|
|
1741
|
+
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
|
|
1731
1742
|
var FABLE_MODEL = "claude-fable-5";
|
|
1743
|
+
var PREVIOUS_SONNET_MODEL = "claude-sonnet-4-6";
|
|
1744
|
+
var PREVIOUS_OPUS_MODEL = "claude-opus-4-8";
|
|
1732
1745
|
var PTY_STREAM_PORT_BASE = 7420;
|
|
1733
1746
|
var PTY_STREAM_PORT_ATTEMPTS = 8;
|
|
1734
1747
|
function encodePtyStreamFrame(frame) {
|
|
@@ -1800,6 +1813,14 @@ var checkpointPathSchema = z.string().transform(normalizeCheckpointPath).pipe(
|
|
|
1800
1813
|
var secretNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
1801
1814
|
var checkpointKeySchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
1802
1815
|
var checkpointDigestRefSchema = z.string().regex(/^[^\s@]+@sha256:[0-9a-f]{64}$/);
|
|
1816
|
+
var ACTIONS_PREBAKE_REGISTRY_PATTERN = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::(?:[1-9][0-9]{0,4}))?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
|
1817
|
+
var actionsPrebakeRegistrySchema = z.string().trim().min(1).regex(
|
|
1818
|
+
ACTIONS_PREBAKE_REGISTRY_PATTERN,
|
|
1819
|
+
"Actions prebake registry must be a lowercase host[:port] with an optional path prefix"
|
|
1820
|
+
).refine((value) => {
|
|
1821
|
+
const port = /:([0-9]+)(?:\/|$)/.exec(value)?.[1];
|
|
1822
|
+
return !port || Number(port) <= 65535;
|
|
1823
|
+
}, "Actions prebake registry port must be between 1 and 65535");
|
|
1803
1824
|
function uniqueSortedArray(item, minimum = 0) {
|
|
1804
1825
|
return z.array(item).min(minimum).superRefine((values, ctx) => {
|
|
1805
1826
|
if (new Set(values).size !== values.length) {
|
|
@@ -3174,6 +3195,30 @@ var AGENT_STATUS_REASON_USER_QUESTION = "user_question";
|
|
|
3174
3195
|
var TASK_CHAT_HISTORY_LIMIT = 20;
|
|
3175
3196
|
var PM_CHAT_HISTORY_LIMIT = 40;
|
|
3176
3197
|
var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT) + 10;
|
|
3198
|
+
function formatModelId(provider, model) {
|
|
3199
|
+
return `${provider}/${model}`;
|
|
3200
|
+
}
|
|
3201
|
+
function anthropicEntry(model, label, inputPerMillion, outputPerMillion, experimental = false) {
|
|
3202
|
+
return {
|
|
3203
|
+
provider: "anthropic",
|
|
3204
|
+
model,
|
|
3205
|
+
id: formatModelId("anthropic", model),
|
|
3206
|
+
label,
|
|
3207
|
+
format: "anthropic-messages",
|
|
3208
|
+
inputPrice: inputPerMillion / 1e6,
|
|
3209
|
+
outputPrice: outputPerMillion / 1e6,
|
|
3210
|
+
supportsTools: true,
|
|
3211
|
+
...experimental ? { experimental: true } : {}
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
var ANTHROPIC_CATALOG = [
|
|
3215
|
+
anthropicEntry(DEFAULT_OPUS_MODEL, "Opus 5 Latest", 15, 75),
|
|
3216
|
+
anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 4.8", 15, 75),
|
|
3217
|
+
anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5 Latest", 3, 15),
|
|
3218
|
+
anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet 4.6", 3, 15),
|
|
3219
|
+
anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 0.8, 4),
|
|
3220
|
+
anthropicEntry(FABLE_MODEL, "Fable 5 (experimental)", 10, 50, true)
|
|
3221
|
+
];
|
|
3177
3222
|
var HUMAN_PROSE_WRITING_STYLE = `## Writing style for humans
|
|
3178
3223
|
When you write prose a person will read \u2014 chat messages, plan updates, PR titles and bodies, PR review guides (the \`publish_review_guide\` overview and section explanations), review comments \u2014 follow these rules (based on ASD-STE100 Simplified Technical English):
|
|
3179
3224
|
- Use active voice. Say who does what ("The API rejects the request", not "the request is rejected").
|
|
@@ -3760,6 +3805,8 @@ function defineTool(name, description, schema, handler, options) {
|
|
|
3760
3805
|
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
3761
3806
|
import { z as z8 } from "zod";
|
|
3762
3807
|
var ClaudeCodeHarness = class {
|
|
3808
|
+
/** The SDK stream is itself the structured-event source. */
|
|
3809
|
+
emitsStructuredEvents = true;
|
|
3763
3810
|
async *executeQuery(opts) {
|
|
3764
3811
|
const sdkEvents = query({
|
|
3765
3812
|
prompt: opts.prompt,
|
|
@@ -4611,76 +4658,6 @@ async function writeHookSettings(dir, opts = {}) {
|
|
|
4611
4658
|
return { settingsPath, helperPath };
|
|
4612
4659
|
}
|
|
4613
4660
|
|
|
4614
|
-
// src/harness/pty/spawn-args.ts
|
|
4615
|
-
function resolveClaudeBinary() {
|
|
4616
|
-
return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
|
|
4617
|
-
}
|
|
4618
|
-
function buildSpawnArgs(input) {
|
|
4619
|
-
const args = [];
|
|
4620
|
-
if (input.resume) {
|
|
4621
|
-
args.push("--resume", input.resume);
|
|
4622
|
-
} else if (input.sessionId) {
|
|
4623
|
-
args.push("--session-id", input.sessionId);
|
|
4624
|
-
}
|
|
4625
|
-
args.push("--model", input.model);
|
|
4626
|
-
if (input.permissionMode === "bypassPermissions") {
|
|
4627
|
-
args.push("--dangerously-skip-permissions");
|
|
4628
|
-
} else {
|
|
4629
|
-
args.push("--permission-mode", "plan");
|
|
4630
|
-
}
|
|
4631
|
-
args.push("--settings", input.settingsPath);
|
|
4632
|
-
if (input.appendSystemPrompt) {
|
|
4633
|
-
args.push("--append-system-prompt", input.appendSystemPrompt);
|
|
4634
|
-
}
|
|
4635
|
-
if (input.mcpConfigPath) {
|
|
4636
|
-
args.push("--mcp-config", input.mcpConfigPath);
|
|
4637
|
-
if (input.strictMcpConfig) {
|
|
4638
|
-
args.push("--strict-mcp-config");
|
|
4639
|
-
}
|
|
4640
|
-
}
|
|
4641
|
-
return args;
|
|
4642
|
-
}
|
|
4643
|
-
function spawnOptionsFingerprint(input) {
|
|
4644
|
-
return JSON.stringify([
|
|
4645
|
-
input.model,
|
|
4646
|
-
input.permissionMode,
|
|
4647
|
-
input.appendSystemPrompt ?? "",
|
|
4648
|
-
input.cwd
|
|
4649
|
-
]);
|
|
4650
|
-
}
|
|
4651
|
-
var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
4652
|
-
function cleanTerminalOutput(raw, maxChars = 1200) {
|
|
4653
|
-
const noAnsi = raw.replace(ANSI_CSI, "");
|
|
4654
|
-
let out = "";
|
|
4655
|
-
for (const ch of noAnsi) {
|
|
4656
|
-
const code = ch.charCodeAt(0);
|
|
4657
|
-
if (ch === "\r" || ch === "\n") out += "\n";
|
|
4658
|
-
else if (ch === " ") out += ch;
|
|
4659
|
-
else if (code < 32 || code === 127) continue;
|
|
4660
|
-
else out += ch;
|
|
4661
|
-
}
|
|
4662
|
-
const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
4663
|
-
const text = lines.join("\n").trim();
|
|
4664
|
-
return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
|
|
4665
|
-
}
|
|
4666
|
-
function isMissingBinaryFailure(tail) {
|
|
4667
|
-
return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
|
|
4668
|
-
}
|
|
4669
|
-
function buildExitErrors(exitCode, rawOutput, binary) {
|
|
4670
|
-
const errors = [`claude exited (code ${exitCode}) without a result`];
|
|
4671
|
-
const tail = cleanTerminalOutput(rawOutput);
|
|
4672
|
-
if (isMissingBinaryFailure(tail)) {
|
|
4673
|
-
errors.push(
|
|
4674
|
-
`The \`${binary}\` CLI could not be started \u2014 it is not installed or not on PATH. Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) or set CONVEYOR_CLAUDE_BIN to its absolute path.`
|
|
4675
|
-
);
|
|
4676
|
-
}
|
|
4677
|
-
if (tail) {
|
|
4678
|
-
errors.push(`Last terminal output before exit:
|
|
4679
|
-
${tail}`);
|
|
4680
|
-
}
|
|
4681
|
-
return errors;
|
|
4682
|
-
}
|
|
4683
|
-
|
|
4684
4661
|
// src/execution/redactor.ts
|
|
4685
4662
|
var REDACTED = "<redacted>";
|
|
4686
4663
|
var BEARER_RE = /\b(Bearer\s+)[A-Za-z0-9_\-.]{20,}/g;
|
|
@@ -4986,10 +4963,10 @@ async function startToolServers(mcpServers, tempDir) {
|
|
|
4986
4963
|
servers.push(server);
|
|
4987
4964
|
config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
|
|
4988
4965
|
}
|
|
4989
|
-
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
4966
|
+
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null, entries: config };
|
|
4990
4967
|
const mcpConfigPath = join2(tempDir, "mcp-config.json");
|
|
4991
4968
|
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
4992
|
-
return { servers, mcpConfigPath };
|
|
4969
|
+
return { servers, mcpConfigPath, entries: config };
|
|
4993
4970
|
}
|
|
4994
4971
|
|
|
4995
4972
|
// src/harness/pty/credentials.ts
|
|
@@ -5425,6 +5402,20 @@ var PtySession = class {
|
|
|
5425
5402
|
recentOutput = "";
|
|
5426
5403
|
coalescer = null;
|
|
5427
5404
|
_toreDown = false;
|
|
5405
|
+
// Raw-TUI input detection (see awaitRawTuiInputLive). `probeWindow`, when
|
|
5406
|
+
// non-null, accumulates raw output so a probe can look for its own sentinel
|
|
5407
|
+
// coming back.
|
|
5408
|
+
spawnedAt = 0;
|
|
5409
|
+
// Set once the child writes a DEC private mode — it owns the tty from then on,
|
|
5410
|
+
// so the kernel no longer echoes our keystrokes and a sentinel coming back is
|
|
5411
|
+
// attributable to the app's own repaint.
|
|
5412
|
+
sawTerminalSetup = false;
|
|
5413
|
+
probeWindow = null;
|
|
5414
|
+
// Whether this process has already taken a prompt write. Only the FIRST one
|
|
5415
|
+
// waits on readiness: once the TUI is up, later writes (a multi-message
|
|
5416
|
+
// prompt, a follow-up turn) must go straight through — output is flowing
|
|
5417
|
+
// while the model works, so a quiet check would block until the turn ended.
|
|
5418
|
+
wroteToProcess = false;
|
|
5428
5419
|
exitListeners = [];
|
|
5429
5420
|
idleListeners = [];
|
|
5430
5421
|
abortHandler = null;
|
|
@@ -5921,8 +5912,8 @@ var PtySession = class {
|
|
|
5921
5912
|
// server doesn't load).
|
|
5922
5913
|
...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
|
|
5923
5914
|
});
|
|
5924
|
-
const
|
|
5925
|
-
const pty =
|
|
5915
|
+
const spawn2 = await resolvePtySpawn();
|
|
5916
|
+
const pty = spawn2(spec.file, spec.args, {
|
|
5926
5917
|
name: "xterm-color",
|
|
5927
5918
|
cols: this.cols,
|
|
5928
5919
|
rows: this.rows,
|
|
@@ -5940,15 +5931,101 @@ var PtySession = class {
|
|
|
5940
5931
|
}
|
|
5941
5932
|
pty.onData((data) => {
|
|
5942
5933
|
this.coalescer?.write(data);
|
|
5934
|
+
if (!this.sawTerminalSetup && sawTerminalSetup(data)) this.sawTerminalSetup = true;
|
|
5935
|
+
if (this.probeWindow !== null) this.probeWindow += data;
|
|
5943
5936
|
this.recentOutput = (this.recentOutput + data).slice(-MAX_DIAGNOSTIC_OUTPUT);
|
|
5944
5937
|
});
|
|
5945
5938
|
pty.onExit((event) => {
|
|
5946
5939
|
void this.finalizeOnExit(event.exitCode);
|
|
5947
5940
|
});
|
|
5941
|
+
this.spawnedAt = Date.now();
|
|
5942
|
+
this.sawTerminalSetup = false;
|
|
5943
|
+
this.probeWindow = null;
|
|
5944
|
+
this.wroteToProcess = false;
|
|
5948
5945
|
this.pty = pty;
|
|
5949
5946
|
}
|
|
5947
|
+
/** Open a fresh probe-observation window over raw output. */
|
|
5948
|
+
openProbeWindow() {
|
|
5949
|
+
this.probeWindow = "";
|
|
5950
|
+
}
|
|
5951
|
+
/**
|
|
5952
|
+
* Write `bytes`, then wait up to `ackMs` for the TUI to render `sentinel` back.
|
|
5953
|
+
* Positive proof that a live input box consumed the keystrokes.
|
|
5954
|
+
*/
|
|
5955
|
+
async writeAndAwaitEcho(bytes, sentinel, timing) {
|
|
5956
|
+
this.openProbeWindow();
|
|
5957
|
+
this.writeStdin(bytes);
|
|
5958
|
+
const deadline = Date.now() + timing.ackMs;
|
|
5959
|
+
while (Date.now() < deadline) {
|
|
5960
|
+
if (this._toreDown) return false;
|
|
5961
|
+
if (sentinelEchoed(this.probeWindow ?? "", sentinel)) return true;
|
|
5962
|
+
await sleep(timing.pollMs);
|
|
5963
|
+
}
|
|
5964
|
+
return false;
|
|
5965
|
+
}
|
|
5966
|
+
/**
|
|
5967
|
+
* Wait until a raw-relay TUI's input box is DEMONSTRABLY accepting keystrokes,
|
|
5968
|
+
* by typing a short sentinel and watching for the TUI to render it back, then
|
|
5969
|
+
* erasing it. Readiness is detected, never inferred from elapsed time.
|
|
5970
|
+
*
|
|
5971
|
+
* A structured-events TUI does not need this: `armSubmitNudge` re-presses
|
|
5972
|
+
* Enter until a transcript record proves the turn started. A raw adapter has
|
|
5973
|
+
* no such evidence, so the nudge is deliberately never armed for it — its
|
|
5974
|
+
* single paste + Enter is the only shot, and one fired during startup is
|
|
5975
|
+
* discarded with no trace, leaving the card parked on an empty input box.
|
|
5976
|
+
*
|
|
5977
|
+
* See resolveRawTuiProbeTiming for the measurements behind this, including the
|
|
5978
|
+
* signals that were tried and rejected (DECSET 2004, output-quiet windows, and
|
|
5979
|
+
* "any output after a keystroke" — all of which report ready too early).
|
|
5980
|
+
*/
|
|
5981
|
+
async awaitRawTuiInputLive() {
|
|
5982
|
+
if (!needsRawReadyGate(this.adapter.capabilities)) return;
|
|
5983
|
+
const timing = resolveRawTuiProbeTiming();
|
|
5984
|
+
const { sentinel } = timing;
|
|
5985
|
+
const start = this.spawnedAt || Date.now();
|
|
5986
|
+
const setupDeadline = start + timing.firstOutputMaxMs;
|
|
5987
|
+
while (!this._toreDown && Date.now() < setupDeadline) {
|
|
5988
|
+
if (this.sawTerminalSetup) break;
|
|
5989
|
+
await sleep(timing.pollMs);
|
|
5990
|
+
}
|
|
5991
|
+
if (this._toreDown) return;
|
|
5992
|
+
const deadline = start + timing.maxMs;
|
|
5993
|
+
let live = false;
|
|
5994
|
+
let probes = 0;
|
|
5995
|
+
while (!this._toreDown && Date.now() < deadline) {
|
|
5996
|
+
probes++;
|
|
5997
|
+
if (await this.writeAndAwaitEcho(sentinel, sentinel, timing)) {
|
|
5998
|
+
live = true;
|
|
5999
|
+
break;
|
|
6000
|
+
}
|
|
6001
|
+
await sleep(timing.retryMs);
|
|
6002
|
+
}
|
|
6003
|
+
if (this._toreDown) return;
|
|
6004
|
+
if (!live) {
|
|
6005
|
+
process.stderr.write(
|
|
6006
|
+
`[conveyor-agent] raw TUI never echoed the input probe after ${probes} attempt(s) in ${Date.now() - start}ms \u2014 pasting anyway
|
|
6007
|
+
`
|
|
6008
|
+
);
|
|
6009
|
+
this.probeWindow = null;
|
|
6010
|
+
return;
|
|
6011
|
+
}
|
|
6012
|
+
for (let round = 0; round < probes && !this._toreDown; round++) {
|
|
6013
|
+
const gone = !await this.writeAndAwaitEcho(
|
|
6014
|
+
"\x7F".repeat(sentinel.length),
|
|
6015
|
+
sentinel,
|
|
6016
|
+
timing
|
|
6017
|
+
);
|
|
6018
|
+
if (gone) break;
|
|
6019
|
+
}
|
|
6020
|
+
this.probeWindow = null;
|
|
6021
|
+
}
|
|
5950
6022
|
async deliverPrompt(text) {
|
|
5951
6023
|
if (text === "" && !this.adapter.capabilities.prefill) return;
|
|
6024
|
+
if (!this.wroteToProcess) {
|
|
6025
|
+
await this.awaitRawTuiInputLive();
|
|
6026
|
+
if (this._toreDown) return;
|
|
6027
|
+
this.wroteToProcess = true;
|
|
6028
|
+
}
|
|
5952
6029
|
this.writeStdin(this.adapter.encodePromptBytes(text));
|
|
5953
6030
|
if (this.turn.promptDelivery === "prefill") return;
|
|
5954
6031
|
this.deliveredTexts.push(text);
|
|
@@ -6300,6 +6377,11 @@ var PtyHarness = class _PtyHarness {
|
|
|
6300
6377
|
bridge;
|
|
6301
6378
|
adapter;
|
|
6302
6379
|
static log = createServiceLogger("pty-harness");
|
|
6380
|
+
/** Delegated to the adapter: Claude tails a transcript + hook socket, while a
|
|
6381
|
+
* raw-relay TUI (opencode) has no trusted event source at all. */
|
|
6382
|
+
get emitsStructuredEvents() {
|
|
6383
|
+
return this.adapter.capabilities.structuredEvents;
|
|
6384
|
+
}
|
|
6303
6385
|
/** Fingerprint of the spawn-time options a reused process cannot change. */
|
|
6304
6386
|
fingerprintOf(options) {
|
|
6305
6387
|
return this.adapter.spawnFingerprint({
|
|
@@ -6610,21 +6692,564 @@ var PtyHarness = class _PtyHarness {
|
|
|
6610
6692
|
}
|
|
6611
6693
|
};
|
|
6612
6694
|
|
|
6695
|
+
// src/harness/opencode/index.ts
|
|
6696
|
+
import { spawn } from "child_process";
|
|
6697
|
+
|
|
6698
|
+
// src/harness/pty/adapters/types.ts
|
|
6699
|
+
import { accessSync, constants, statSync } from "fs";
|
|
6700
|
+
import { join as join6 } from "path";
|
|
6701
|
+
var TuiUnavailableError = class extends Error {
|
|
6702
|
+
constructor(tui, message) {
|
|
6703
|
+
super(message);
|
|
6704
|
+
this.tui = tui;
|
|
6705
|
+
this.name = "TuiUnavailableError";
|
|
6706
|
+
}
|
|
6707
|
+
tui;
|
|
6708
|
+
};
|
|
6709
|
+
function isExecutable(path2) {
|
|
6710
|
+
try {
|
|
6711
|
+
if (!statSync(path2).isFile()) return false;
|
|
6712
|
+
accessSync(path2, constants.X_OK);
|
|
6713
|
+
return true;
|
|
6714
|
+
} catch {
|
|
6715
|
+
return false;
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
function findOnPath(binary, env = process.env) {
|
|
6719
|
+
if (binary.includes("/")) {
|
|
6720
|
+
return isExecutable(binary) ? binary : null;
|
|
6721
|
+
}
|
|
6722
|
+
for (const dir of (env.PATH ?? "").split(":")) {
|
|
6723
|
+
if (!dir) continue;
|
|
6724
|
+
const candidate = join6(dir, binary);
|
|
6725
|
+
if (isExecutable(candidate)) return candidate;
|
|
6726
|
+
}
|
|
6727
|
+
return null;
|
|
6728
|
+
}
|
|
6729
|
+
|
|
6730
|
+
// src/harness/pty/adapters/opencode-auth.ts
|
|
6731
|
+
import { promises as fs } from "fs";
|
|
6732
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
6733
|
+
import { homedir as homedir4 } from "os";
|
|
6734
|
+
var logger = createServiceLogger("opencode-auth");
|
|
6735
|
+
var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
|
|
6736
|
+
var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
|
|
6737
|
+
function opencodeAuthPath(env) {
|
|
6738
|
+
const dataHome = env.XDG_DATA_HOME ?? join7(env.HOME ?? homedir4(), ".local", "share");
|
|
6739
|
+
return join7(dataHome, "opencode", "auth.json");
|
|
6740
|
+
}
|
|
6741
|
+
function opencodeConfigPath(env) {
|
|
6742
|
+
const configHome = env.XDG_CONFIG_HOME ?? join7(env.HOME ?? homedir4(), ".config");
|
|
6743
|
+
return join7(configHome, "opencode", "opencode.json");
|
|
6744
|
+
}
|
|
6745
|
+
function parseOauthSeed(b64) {
|
|
6746
|
+
if (!b64) return null;
|
|
6747
|
+
try {
|
|
6748
|
+
const parsed = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
|
|
6749
|
+
if (typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number") {
|
|
6750
|
+
return { access: parsed.access, refresh: parsed.refresh, expires: parsed.expires };
|
|
6751
|
+
}
|
|
6752
|
+
return null;
|
|
6753
|
+
} catch {
|
|
6754
|
+
return null;
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
function shouldSeed(existingEntry, seed) {
|
|
6758
|
+
if (!existingEntry || typeof existingEntry !== "object") return true;
|
|
6759
|
+
const entry = existingEntry;
|
|
6760
|
+
if (entry.type !== "oauth") return true;
|
|
6761
|
+
if (typeof entry.access !== "string" || typeof entry.refresh !== "string") return true;
|
|
6762
|
+
if (typeof entry.expires !== "number") return true;
|
|
6763
|
+
return entry.expires < seed.expires;
|
|
6764
|
+
}
|
|
6765
|
+
async function readJsonFile(path2) {
|
|
6766
|
+
try {
|
|
6767
|
+
return JSON.parse(await fs.readFile(path2, "utf8"));
|
|
6768
|
+
} catch {
|
|
6769
|
+
return {};
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
async function writeJsonFile(path2, value) {
|
|
6773
|
+
await fs.mkdir(dirname2(path2), { recursive: true });
|
|
6774
|
+
await fs.writeFile(path2, `${JSON.stringify(value, null, 2)}
|
|
6775
|
+
`, { mode: 384 });
|
|
6776
|
+
}
|
|
6777
|
+
async function ensureAuthEntry(env, seed) {
|
|
6778
|
+
const path2 = opencodeAuthPath(env);
|
|
6779
|
+
const store = await readJsonFile(path2);
|
|
6780
|
+
if (!shouldSeed(store.openai, seed)) {
|
|
6781
|
+
logger.info("opencode oauth store is fresher than the seed; leaving it alone");
|
|
6782
|
+
return;
|
|
6783
|
+
}
|
|
6784
|
+
store.openai = {
|
|
6785
|
+
type: "oauth",
|
|
6786
|
+
access: seed.access,
|
|
6787
|
+
refresh: seed.refresh,
|
|
6788
|
+
expires: seed.expires
|
|
6789
|
+
};
|
|
6790
|
+
await writeJsonFile(path2, store);
|
|
6791
|
+
logger.info("seeded opencode oauth store entry");
|
|
6792
|
+
}
|
|
6793
|
+
async function ensurePluginConfig(env) {
|
|
6794
|
+
const path2 = opencodeConfigPath(env);
|
|
6795
|
+
const config = await readJsonFile(path2);
|
|
6796
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
6797
|
+
const isOurs = (p) => typeof p === "string" && (p === PLUGIN_PACKAGE || p.startsWith(`${PLUGIN_PACKAGE}@`));
|
|
6798
|
+
const hasExactPin = plugins.includes(OPENCODE_CODEX_PLUGIN);
|
|
6799
|
+
const hasStalePin = plugins.some((p) => isOurs(p) && p !== OPENCODE_CODEX_PLUGIN);
|
|
6800
|
+
if (hasExactPin && !hasStalePin) return;
|
|
6801
|
+
const kept = plugins.filter((p) => !isOurs(p));
|
|
6802
|
+
config.plugin = [...kept, OPENCODE_CODEX_PLUGIN];
|
|
6803
|
+
await writeJsonFile(path2, config);
|
|
6804
|
+
logger.info("ensured opencode codex-auth plugin in config");
|
|
6805
|
+
}
|
|
6806
|
+
async function seedOpenCodeOauth(env) {
|
|
6807
|
+
const seed = parseOauthSeed(env.CONVEYOR_OPENCODE_OAUTH);
|
|
6808
|
+
if (!seed) return;
|
|
6809
|
+
try {
|
|
6810
|
+
await ensureAuthEntry(env, seed);
|
|
6811
|
+
await ensurePluginConfig(env);
|
|
6812
|
+
} catch (err) {
|
|
6813
|
+
logger.warn(
|
|
6814
|
+
`failed to seed opencode oauth store: ${err instanceof Error ? err.message : String(err)}`
|
|
6815
|
+
);
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
|
|
6819
|
+
// src/harness/opencode/credentials.ts
|
|
6820
|
+
var PROVIDER_KEY_ENV = {
|
|
6821
|
+
openai: "OPENAI_API_KEY",
|
|
6822
|
+
anthropic: "ANTHROPIC_API_KEY"
|
|
6823
|
+
};
|
|
6824
|
+
var DEFAULT_OPENCODE_PROVIDER = "openai";
|
|
6825
|
+
function buildOpenCodeCredentialEnv(source) {
|
|
6826
|
+
const set = {};
|
|
6827
|
+
const oauthActive = Boolean(source.CONVEYOR_OPENCODE_OAUTH);
|
|
6828
|
+
const key = source.CONVEYOR_AGENT_KEY;
|
|
6829
|
+
const provider = source.CONVEYOR_AGENT_PROVIDER ?? DEFAULT_OPENCODE_PROVIDER;
|
|
6830
|
+
const keyEnvVar = PROVIDER_KEY_ENV[provider];
|
|
6831
|
+
if (key && keyEnvVar && !oauthActive) set[keyEnvVar] = key;
|
|
6832
|
+
return { set, unset: ["CONVEYOR_AGENT_KEY", "CONVEYOR_OPENCODE_OAUTH"] };
|
|
6833
|
+
}
|
|
6834
|
+
async function prepareOpenCodeCredentials(source = process.env) {
|
|
6835
|
+
await seedOpenCodeOauth(source);
|
|
6836
|
+
}
|
|
6837
|
+
function buildOpenCodeChildEnv(source, configContent) {
|
|
6838
|
+
const env = {};
|
|
6839
|
+
for (const [key, value] of Object.entries(source)) {
|
|
6840
|
+
if (typeof value === "string") env[key] = value;
|
|
6841
|
+
}
|
|
6842
|
+
const { set, unset } = buildOpenCodeCredentialEnv(source);
|
|
6843
|
+
for (const key of unset) delete env[key];
|
|
6844
|
+
Object.assign(env, set);
|
|
6845
|
+
if (configContent) env.OPENCODE_CONFIG_CONTENT = configContent;
|
|
6846
|
+
return env;
|
|
6847
|
+
}
|
|
6848
|
+
function resolveOpenCodeModel(source, optionsModel) {
|
|
6849
|
+
const model = source.CONVEYOR_AGENT_MODEL ?? optionsModel;
|
|
6850
|
+
if (!model) return void 0;
|
|
6851
|
+
if (model.includes("/")) return model;
|
|
6852
|
+
const provider = source.CONVEYOR_AGENT_PROVIDER ?? DEFAULT_OPENCODE_PROVIDER;
|
|
6853
|
+
return `${provider}/${model}`;
|
|
6854
|
+
}
|
|
6855
|
+
|
|
6856
|
+
// src/harness/opencode/spawn.ts
|
|
6857
|
+
function resolveOpenCodeBinary(env = process.env) {
|
|
6858
|
+
const override = env.CONVEYOR_OPENCODE_BIN;
|
|
6859
|
+
const found = override ? findOnPath(override, env) : findOnPath("opencode", env);
|
|
6860
|
+
if (!found) {
|
|
6861
|
+
throw new TuiUnavailableError(
|
|
6862
|
+
"opencode",
|
|
6863
|
+
"The opencode CLI is not available in this environment. It must be baked into the pod image (see Dockerfile.base) \u2014 re-run Build Image for this project, or set CONVEYOR_OPENCODE_BIN to its absolute path."
|
|
6864
|
+
);
|
|
6865
|
+
}
|
|
6866
|
+
return found;
|
|
6867
|
+
}
|
|
6868
|
+
var PROMPT_MAX_CHARS = 96e3;
|
|
6869
|
+
var MUTATING_TOOLS = ["edit", "bash", "external_directory"];
|
|
6870
|
+
function buildRunArgs(input) {
|
|
6871
|
+
const args = ["run", "--dir", input.cwd, "--format", "json", "--auto"];
|
|
6872
|
+
if (input.model) {
|
|
6873
|
+
args.push("--model", input.model);
|
|
6874
|
+
}
|
|
6875
|
+
if (input.resumeSessionId) args.push("--session", input.resumeSessionId);
|
|
6876
|
+
args.push(truncatePrompt(input.prompt));
|
|
6877
|
+
return args;
|
|
6878
|
+
}
|
|
6879
|
+
function truncatePrompt(prompt) {
|
|
6880
|
+
if (prompt.length <= PROMPT_MAX_CHARS) return prompt;
|
|
6881
|
+
return `${prompt.slice(0, PROMPT_MAX_CHARS)}
|
|
6882
|
+
|
|
6883
|
+
[prompt truncated at ${PROMPT_MAX_CHARS} characters]`;
|
|
6884
|
+
}
|
|
6885
|
+
function buildOpenCodeConfigContent(input) {
|
|
6886
|
+
const { entries, systemPromptPath, readOnly } = input;
|
|
6887
|
+
const mcp = {};
|
|
6888
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
6889
|
+
if (entry.type === "http") {
|
|
6890
|
+
mcp[name] = {
|
|
6891
|
+
type: "remote",
|
|
6892
|
+
url: entry.url,
|
|
6893
|
+
enabled: true,
|
|
6894
|
+
...Object.keys(entry.headers).length > 0 ? { headers: entry.headers } : {}
|
|
6895
|
+
};
|
|
6896
|
+
} else {
|
|
6897
|
+
mcp[name] = {
|
|
6898
|
+
type: "local",
|
|
6899
|
+
command: [entry.command, ...entry.args ?? []],
|
|
6900
|
+
enabled: true,
|
|
6901
|
+
...entry.env ? { environment: entry.env } : {}
|
|
6902
|
+
};
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
const config = {};
|
|
6906
|
+
if (Object.keys(mcp).length > 0) config.mcp = mcp;
|
|
6907
|
+
if (systemPromptPath) config.instructions = [systemPromptPath];
|
|
6908
|
+
if (readOnly) {
|
|
6909
|
+
config.permission = {
|
|
6910
|
+
"*": "allow",
|
|
6911
|
+
...Object.fromEntries(MUTATING_TOOLS.map((tool2) => [tool2, "deny"]))
|
|
6912
|
+
};
|
|
6913
|
+
}
|
|
6914
|
+
if (Object.keys(config).length === 0) return null;
|
|
6915
|
+
return JSON.stringify(config);
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6918
|
+
// src/harness/opencode/events.ts
|
|
6919
|
+
function parseOpenCodeLine(line) {
|
|
6920
|
+
const trimmed = line.trim();
|
|
6921
|
+
if (!trimmed.startsWith("{")) return null;
|
|
6922
|
+
try {
|
|
6923
|
+
return JSON.parse(trimmed);
|
|
6924
|
+
} catch {
|
|
6925
|
+
return null;
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
function numberAt(source, ...keys) {
|
|
6929
|
+
let cursor = source;
|
|
6930
|
+
for (const key of keys) {
|
|
6931
|
+
if (typeof cursor !== "object" || cursor === null) return 0;
|
|
6932
|
+
cursor = cursor[key];
|
|
6933
|
+
}
|
|
6934
|
+
return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
|
|
6935
|
+
}
|
|
6936
|
+
function accumulateUsage(event, into) {
|
|
6937
|
+
const part = event.part;
|
|
6938
|
+
if (!part) return;
|
|
6939
|
+
into.inputTokens += numberAt(part.tokens, "input");
|
|
6940
|
+
into.outputTokens += numberAt(part.tokens, "output");
|
|
6941
|
+
const cost = part.cost;
|
|
6942
|
+
if (typeof cost === "number" && Number.isFinite(cost)) into.totalCostUsd += cost;
|
|
6943
|
+
}
|
|
6944
|
+
function mapOpenCodeEvent(event) {
|
|
6945
|
+
const part = event.part;
|
|
6946
|
+
if (!part) return null;
|
|
6947
|
+
if (part.type === "text") return mapTextPart(part);
|
|
6948
|
+
if (part.type === "tool") return mapToolPart(part);
|
|
6949
|
+
return null;
|
|
6950
|
+
}
|
|
6951
|
+
function mapTextPart(part) {
|
|
6952
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
6953
|
+
if (text.trim() === "") return null;
|
|
6954
|
+
return {
|
|
6955
|
+
type: "assistant",
|
|
6956
|
+
message: { role: "assistant", content: [{ type: "text", text }] }
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
function mapToolPart(part) {
|
|
6960
|
+
const status = part.state?.status;
|
|
6961
|
+
if (status !== "completed" && status !== "error") return null;
|
|
6962
|
+
return {
|
|
6963
|
+
type: "assistant",
|
|
6964
|
+
message: {
|
|
6965
|
+
role: "assistant",
|
|
6966
|
+
content: [
|
|
6967
|
+
{
|
|
6968
|
+
type: "tool_use",
|
|
6969
|
+
name: typeof part.tool === "string" ? part.tool : "unknown",
|
|
6970
|
+
input: part.state?.input ?? {},
|
|
6971
|
+
...typeof part.id === "string" ? { id: part.id } : {}
|
|
6972
|
+
}
|
|
6973
|
+
]
|
|
6974
|
+
}
|
|
6975
|
+
};
|
|
6976
|
+
}
|
|
6977
|
+
function errorMessageOf(event) {
|
|
6978
|
+
if (event.type !== "error") return null;
|
|
6979
|
+
const error = event.error;
|
|
6980
|
+
const message = typeof error?.data?.message === "string" ? error.data.message : null;
|
|
6981
|
+
const name = typeof error?.name === "string" ? error.name : "error";
|
|
6982
|
+
const status = typeof error?.data?.statusCode === "number" ? ` (HTTP ${error.data.statusCode})` : "";
|
|
6983
|
+
return message ? `${name}${status}: ${message}` : `${name}${status}`;
|
|
6984
|
+
}
|
|
6985
|
+
function sessionIdOf(event) {
|
|
6986
|
+
return typeof event.sessionID === "string" && event.sessionID.length > 0 ? event.sessionID : null;
|
|
6987
|
+
}
|
|
6988
|
+
function buildResultEvent(exitCode, usage, assistantText, stderrTail, reportedError) {
|
|
6989
|
+
if (exitCode === 0) {
|
|
6990
|
+
return {
|
|
6991
|
+
type: "result",
|
|
6992
|
+
subtype: "success",
|
|
6993
|
+
// The runner substitutes "Task completed." for an empty summary, so a run
|
|
6994
|
+
// that only used tools still reads sensibly in chat.
|
|
6995
|
+
result: assistantText,
|
|
6996
|
+
total_cost_usd: usage.totalCostUsd
|
|
6997
|
+
};
|
|
6998
|
+
}
|
|
6999
|
+
const errors = reportedError ? [reportedError] : [`opencode run exited with code ${exitCode}`];
|
|
7000
|
+
if (stderrTail.trim()) errors.push(`stderr:
|
|
7001
|
+
${stderrTail.trim()}`);
|
|
7002
|
+
return { type: "result", subtype: "error", errors };
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
// src/harness/opencode/index.ts
|
|
7006
|
+
import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
7007
|
+
import { join as join8 } from "path";
|
|
7008
|
+
var MAX_STDERR_TAIL = 4e3;
|
|
7009
|
+
var OpenCodeHeadlessHarness = class {
|
|
7010
|
+
/** NDJSON from `--format json` is a trusted structured source. */
|
|
7011
|
+
emitsStructuredEvents = true;
|
|
7012
|
+
/** opencode's session id from the last run, for `--session` on the next turn. */
|
|
7013
|
+
lastSessionId = null;
|
|
7014
|
+
/** MCP tool servers started for the in-flight run. */
|
|
7015
|
+
toolServers = [];
|
|
7016
|
+
tempDir = "";
|
|
7017
|
+
/** The same in-process MCP handle the PTY path uses — tools are identical. */
|
|
7018
|
+
createMcpServer(config) {
|
|
7019
|
+
return new PtyMcpServer(config.name, config.tools);
|
|
7020
|
+
}
|
|
7021
|
+
/** opencode's own session id, exposed so the runner can persist lineage. */
|
|
7022
|
+
get sessionId() {
|
|
7023
|
+
return this.lastSessionId;
|
|
7024
|
+
}
|
|
7025
|
+
async *executeQuery(opts) {
|
|
7026
|
+
const prompt = await collectPrompt(opts.prompt);
|
|
7027
|
+
const binary = resolveOpenCodeBinary(process.env);
|
|
7028
|
+
await prepareOpenCodeCredentials(process.env);
|
|
7029
|
+
this.tempDir = await mkdtemp2(join8(sessionTempBase(), "opencode-headless-"));
|
|
7030
|
+
const { servers, entries } = await startToolServers(
|
|
7031
|
+
opts.options.mcpServers ?? {},
|
|
7032
|
+
this.tempDir
|
|
7033
|
+
);
|
|
7034
|
+
this.toolServers = servers;
|
|
7035
|
+
const args = buildRunArgs({
|
|
7036
|
+
prompt,
|
|
7037
|
+
cwd: opts.options.cwd,
|
|
7038
|
+
model: resolveOpenCodeModel(process.env, opts.options.model),
|
|
7039
|
+
// `resume` is Claude-transcript-derived, so it is always absent here (there
|
|
7040
|
+
// is no Claude session file for an opencode run). Falling back to the id
|
|
7041
|
+
// opencode itself reported is what makes a multi-turn card keep its
|
|
7042
|
+
// conversation instead of starting fresh on every message.
|
|
7043
|
+
resumeSessionId: opts.resume ?? this.lastSessionId
|
|
7044
|
+
});
|
|
7045
|
+
const readOnly = opts.options.allowDangerouslySkipPermissions === false;
|
|
7046
|
+
const systemPromptPath = await this.writeSystemPrompt(opts.options.appendSystemPrompt);
|
|
7047
|
+
const configContent = buildOpenCodeConfigContent({
|
|
7048
|
+
entries,
|
|
7049
|
+
systemPromptPath,
|
|
7050
|
+
readOnly
|
|
7051
|
+
});
|
|
7052
|
+
const queue = new AsyncEventQueue();
|
|
7053
|
+
const usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
|
|
7054
|
+
let assistantText = "";
|
|
7055
|
+
let stderrTail = "";
|
|
7056
|
+
let reportedError = null;
|
|
7057
|
+
const child = spawn(binary, args, {
|
|
7058
|
+
cwd: opts.options.cwd,
|
|
7059
|
+
// stdin IGNORED — an open pipe makes `opencode run` block forever.
|
|
7060
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7061
|
+
env: buildOpenCodeChildEnv(process.env, configContent)
|
|
7062
|
+
});
|
|
7063
|
+
const abort = opts.options.abortController;
|
|
7064
|
+
const onAbort = () => {
|
|
7065
|
+
child.kill("SIGTERM");
|
|
7066
|
+
};
|
|
7067
|
+
abort?.signal.addEventListener("abort", onAbort, { once: true });
|
|
7068
|
+
let carry = "";
|
|
7069
|
+
const ingest = (line) => {
|
|
7070
|
+
const { text, error } = this.ingestLine(line, usage, queue);
|
|
7071
|
+
if (text) assistantText += text;
|
|
7072
|
+
if (error) reportedError = error;
|
|
7073
|
+
};
|
|
7074
|
+
child.stdout.setEncoding("utf8");
|
|
7075
|
+
child.stdout.on("data", (chunk) => {
|
|
7076
|
+
carry += chunk;
|
|
7077
|
+
const lines = carry.split("\n");
|
|
7078
|
+
carry = lines.pop() ?? "";
|
|
7079
|
+
for (const line of lines) ingest(line);
|
|
7080
|
+
});
|
|
7081
|
+
child.stderr.setEncoding("utf8");
|
|
7082
|
+
child.stderr.on("data", (chunk) => {
|
|
7083
|
+
stderrTail = (stderrTail + chunk).slice(-MAX_STDERR_TAIL);
|
|
7084
|
+
});
|
|
7085
|
+
const exited = new Promise((resolve) => {
|
|
7086
|
+
child.once("error", () => resolve(-1));
|
|
7087
|
+
child.once("close", (code) => resolve(code ?? -1));
|
|
7088
|
+
});
|
|
7089
|
+
const finish = (async () => {
|
|
7090
|
+
const code = await exited;
|
|
7091
|
+
ingest(carry);
|
|
7092
|
+
queue.push(buildResultEvent(code, usage, assistantText.trim(), stderrTail, reportedError));
|
|
7093
|
+
queue.close();
|
|
7094
|
+
})();
|
|
7095
|
+
try {
|
|
7096
|
+
for await (const event of queue.drain()) yield event;
|
|
7097
|
+
await finish;
|
|
7098
|
+
} finally {
|
|
7099
|
+
abort?.signal.removeEventListener("abort", onAbort);
|
|
7100
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
7101
|
+
await this.cleanup();
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
/**
|
|
7105
|
+
* Persist the Conveyor system prompt (task context, plan, mode instructions)
|
|
7106
|
+
* so it can be referenced by the config's `instructions`. opencode has no
|
|
7107
|
+
* `--append-system-prompt`; without this the card runs with no context at all.
|
|
7108
|
+
*/
|
|
7109
|
+
async writeSystemPrompt(text) {
|
|
7110
|
+
if (!text || text.trim() === "") return null;
|
|
7111
|
+
const path2 = join8(this.tempDir, "conveyor-instructions.md");
|
|
7112
|
+
await writeFile4(path2, text, "utf8");
|
|
7113
|
+
return path2;
|
|
7114
|
+
}
|
|
7115
|
+
/**
|
|
7116
|
+
* Parse one NDJSON line and push whatever it maps to. Returns assistant text so
|
|
7117
|
+
* the caller can accumulate the turn's summary; the session id is latched here
|
|
7118
|
+
* because every event carries it and any one of them will do.
|
|
7119
|
+
*/
|
|
7120
|
+
ingestLine(line, usage, queue) {
|
|
7121
|
+
const none = { text: "", error: null };
|
|
7122
|
+
const parsed = parseOpenCodeLine(line);
|
|
7123
|
+
if (!parsed) return none;
|
|
7124
|
+
const sid = sessionIdOf(parsed);
|
|
7125
|
+
if (sid) this.lastSessionId = sid;
|
|
7126
|
+
accumulateUsage(parsed, usage);
|
|
7127
|
+
const error = errorMessageOf(parsed);
|
|
7128
|
+
if (error) return { text: "", error };
|
|
7129
|
+
const mapped = mapOpenCodeEvent(parsed);
|
|
7130
|
+
if (!mapped) return none;
|
|
7131
|
+
queue.push(mapped);
|
|
7132
|
+
if (mapped.type !== "assistant") return none;
|
|
7133
|
+
const block = mapped.message.content[0];
|
|
7134
|
+
return { text: block?.type === "text" && block.text ? block.text : "", error: null };
|
|
7135
|
+
}
|
|
7136
|
+
async dispose() {
|
|
7137
|
+
await this.cleanup();
|
|
7138
|
+
}
|
|
7139
|
+
async cleanup() {
|
|
7140
|
+
for (const server of this.toolServers) await server.close().catch(() => void 0);
|
|
7141
|
+
this.toolServers = [];
|
|
7142
|
+
if (this.tempDir) {
|
|
7143
|
+
await rm3(this.tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
7144
|
+
this.tempDir = "";
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
};
|
|
7148
|
+
async function collectPrompt(prompt) {
|
|
7149
|
+
if (typeof prompt === "string") return prompt;
|
|
7150
|
+
const parts = [];
|
|
7151
|
+
for await (const message of prompt) {
|
|
7152
|
+
const content = message?.message?.content;
|
|
7153
|
+
if (typeof content === "string") parts.push(content);
|
|
7154
|
+
else if (Array.isArray(content)) {
|
|
7155
|
+
for (const block of content) {
|
|
7156
|
+
const b = block;
|
|
7157
|
+
if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
return parts.join("\n\n");
|
|
7162
|
+
}
|
|
7163
|
+
|
|
6613
7164
|
// src/harness/index.ts
|
|
6614
|
-
function
|
|
6615
|
-
return kind === "
|
|
7165
|
+
function supportsImageBlocks(kind) {
|
|
7166
|
+
return kind === "sdk";
|
|
7167
|
+
}
|
|
7168
|
+
function createHarness(kind = "sdk", ptyBridge, adapter) {
|
|
7169
|
+
if (kind === "opencode") return new OpenCodeHeadlessHarness();
|
|
7170
|
+
if (kind !== "pty") return new ClaudeCodeHarness();
|
|
7171
|
+
return adapter ? new PtyHarness(ptyBridge, adapter) : new PtyHarness(ptyBridge);
|
|
7172
|
+
}
|
|
7173
|
+
|
|
7174
|
+
// src/harness/pty/adapters/opencode.ts
|
|
7175
|
+
var OpenCodeTuiAdapter = class {
|
|
7176
|
+
constructor(env = process.env) {
|
|
7177
|
+
this.env = env;
|
|
7178
|
+
}
|
|
7179
|
+
env;
|
|
7180
|
+
id = "opencode";
|
|
7181
|
+
capabilities = {
|
|
7182
|
+
resume: false,
|
|
7183
|
+
structuredEvents: false,
|
|
7184
|
+
prefill: false,
|
|
7185
|
+
passiveTurns: false
|
|
7186
|
+
};
|
|
7187
|
+
resolveBinary(env = this.env) {
|
|
7188
|
+
const override = env.CONVEYOR_OPENCODE_BIN;
|
|
7189
|
+
const found = override ? findOnPath(override, env) : findOnPath("opencode", env);
|
|
7190
|
+
if (!found) {
|
|
7191
|
+
throw new TuiUnavailableError(
|
|
7192
|
+
"opencode",
|
|
7193
|
+
"The opencode CLI is not available in this environment. It must be baked into the pod image (see Dockerfile.base) \u2014 re-run Build Image for this project, or set CONVEYOR_OPENCODE_BIN to its absolute path."
|
|
7194
|
+
);
|
|
7195
|
+
}
|
|
7196
|
+
return found;
|
|
7197
|
+
}
|
|
7198
|
+
buildSpawn(input) {
|
|
7199
|
+
const env = { ...inheritedEnv() };
|
|
7200
|
+
const credentials = buildOpenCodeCredentialEnv(this.env);
|
|
7201
|
+
for (const key of credentials.unset) delete env[key];
|
|
7202
|
+
Object.assign(env, credentials.set);
|
|
7203
|
+
const model = resolveOpenCodeModel(this.env, input.options.model);
|
|
7204
|
+
const args = [];
|
|
7205
|
+
if (model) args.push("--model", model);
|
|
7206
|
+
return { file: this.resolveBinary(), args, env };
|
|
7207
|
+
}
|
|
7208
|
+
async prepareEnvironment() {
|
|
7209
|
+
await prepareOpenCodeCredentials(this.env);
|
|
7210
|
+
}
|
|
7211
|
+
spawnFingerprint(input) {
|
|
7212
|
+
return JSON.stringify(["opencode", input.model, input.cwd]);
|
|
7213
|
+
}
|
|
7214
|
+
encodePromptBytes(text) {
|
|
7215
|
+
return buildPromptBytes(text);
|
|
7216
|
+
}
|
|
7217
|
+
buildExitErrors(exitCode, rawOutput) {
|
|
7218
|
+
const errors = [`opencode exited (code ${exitCode}) without a result`];
|
|
7219
|
+
const tail = cleanTerminalOutput(rawOutput);
|
|
7220
|
+
if (tail) errors.push(`Last terminal output before exit:
|
|
7221
|
+
${tail}`);
|
|
7222
|
+
return errors;
|
|
7223
|
+
}
|
|
7224
|
+
};
|
|
7225
|
+
|
|
7226
|
+
// src/harness/pty/adapters/index.ts
|
|
7227
|
+
function resolveTuiKindFromEnv(env) {
|
|
7228
|
+
const raw = env.CONVEYOR_TUI ?? "claude-code";
|
|
7229
|
+
if (TUI_KINDS.includes(raw)) return raw;
|
|
7230
|
+
throw new Error(`Unknown TUI "${raw}" in CONVEYOR_TUI (expected: ${TUI_KINDS.join(", ")})`);
|
|
7231
|
+
}
|
|
7232
|
+
function resolveTuiAdapter(kind = "claude-code") {
|
|
7233
|
+
switch (kind) {
|
|
7234
|
+
case "claude-code":
|
|
7235
|
+
return new ClaudeTuiAdapter();
|
|
7236
|
+
case "opencode":
|
|
7237
|
+
return new OpenCodeTuiAdapter();
|
|
7238
|
+
default:
|
|
7239
|
+
throw new Error(`Unknown TUI kind: ${kind}`);
|
|
7240
|
+
}
|
|
6616
7241
|
}
|
|
6617
7242
|
|
|
6618
7243
|
// src/harness/pty/stream-server.ts
|
|
6619
7244
|
import net from "net";
|
|
6620
|
-
var
|
|
7245
|
+
var logger2 = createServiceLogger("PtyStreamServer");
|
|
6621
7246
|
var RING_MAX_CHARS = 256 * 1024;
|
|
6622
7247
|
var PtyStreamServer = class {
|
|
6623
7248
|
constructor(options) {
|
|
6624
7249
|
this.options = options;
|
|
6625
7250
|
this.server = net.createServer((socket) => this.handleConnection(socket));
|
|
6626
7251
|
this.server.on("error", (err) => {
|
|
6627
|
-
|
|
7252
|
+
logger2.warn(`PTY stream server error: ${err.message}`);
|
|
6628
7253
|
});
|
|
6629
7254
|
}
|
|
6630
7255
|
options;
|
|
@@ -6665,7 +7290,7 @@ var PtyStreamServer = class {
|
|
|
6665
7290
|
return port;
|
|
6666
7291
|
}
|
|
6667
7292
|
}
|
|
6668
|
-
|
|
7293
|
+
logger2.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
|
|
6669
7294
|
return null;
|
|
6670
7295
|
}
|
|
6671
7296
|
tryListen(port) {
|
|
@@ -6786,7 +7411,7 @@ var PtyStreamServer = class {
|
|
|
6786
7411
|
};
|
|
6787
7412
|
|
|
6788
7413
|
// src/harness/pty/direct-stream.ts
|
|
6789
|
-
var
|
|
7414
|
+
var logger3 = createServiceLogger("PtyDirectStream");
|
|
6790
7415
|
var RELAY_COALESCE_MS = 2e3;
|
|
6791
7416
|
var RELAY_MAX_BUFFER_CHARS = 48 * 1024;
|
|
6792
7417
|
var DirectStreamController = class {
|
|
@@ -6835,7 +7460,7 @@ var DirectStreamController = class {
|
|
|
6835
7460
|
});
|
|
6836
7461
|
void created.listen().then((port) => this.onListening(created, port)).catch((err) => {
|
|
6837
7462
|
this.starting = false;
|
|
6838
|
-
|
|
7463
|
+
logger3.warn(
|
|
6839
7464
|
`PTY stream server failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
6840
7465
|
);
|
|
6841
7466
|
});
|
|
@@ -6848,7 +7473,7 @@ var DirectStreamController = class {
|
|
|
6848
7473
|
}
|
|
6849
7474
|
this.server = created;
|
|
6850
7475
|
this.reporter.reportPtyStream(port);
|
|
6851
|
-
|
|
7476
|
+
logger3.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
|
|
6852
7477
|
}
|
|
6853
7478
|
/** Push the min box across both transports to the pty. */
|
|
6854
7479
|
applyDims() {
|
|
@@ -10154,7 +10779,7 @@ function buildMutationTools(connection, config) {
|
|
|
10154
10779
|
}
|
|
10155
10780
|
|
|
10156
10781
|
// src/tools/attachment-tools.ts
|
|
10157
|
-
import { basename, extname, isAbsolute, join as
|
|
10782
|
+
import { basename, extname, isAbsolute, join as join9 } from "path";
|
|
10158
10783
|
var MIME_BY_EXT = {
|
|
10159
10784
|
".png": "image/png",
|
|
10160
10785
|
".jpg": "image/jpeg",
|
|
@@ -10205,7 +10830,7 @@ ${snippet}`;
|
|
|
10205
10830
|
function buildUploadAttachmentTool(connection, config) {
|
|
10206
10831
|
return defineContractTool(uploadAttachmentContract, async ({ path: path2, title, tags }) => {
|
|
10207
10832
|
try {
|
|
10208
|
-
const filePath = isAbsolute(path2) ? path2 :
|
|
10833
|
+
const filePath = isAbsolute(path2) ? path2 : join9(config.workspaceDir, path2);
|
|
10209
10834
|
const mimeType = inferMimeType(filePath);
|
|
10210
10835
|
const info = await statWorkspacePath(filePath);
|
|
10211
10836
|
if (!info.isFile) {
|
|
@@ -10717,7 +11342,7 @@ import { z as z16 } from "zod";
|
|
|
10717
11342
|
|
|
10718
11343
|
// src/execution/context-path-verifier.ts
|
|
10719
11344
|
import { readFile as readFile2 } from "fs/promises";
|
|
10720
|
-
import { isAbsolute as isAbsolute2, join as
|
|
11345
|
+
import { isAbsolute as isAbsolute2, join as join10, normalize } from "path";
|
|
10721
11346
|
var PROBLEM_TEXT = {
|
|
10722
11347
|
not_found: "does not exist in the repo",
|
|
10723
11348
|
expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
|
|
@@ -10762,7 +11387,7 @@ async function verifyContextPaths(links, workspaceDir) {
|
|
|
10762
11387
|
problems.push({ type: link.type, path: link.path, reason: shape });
|
|
10763
11388
|
continue;
|
|
10764
11389
|
}
|
|
10765
|
-
const absolutePath =
|
|
11390
|
+
const absolutePath = join10(workspaceDir, toRelativePath(link.path));
|
|
10766
11391
|
const stat = await statWorkspacePath(absolutePath);
|
|
10767
11392
|
const wantsDirectory = expectsDirectory(link.type);
|
|
10768
11393
|
if (!stat.exists) {
|
|
@@ -11435,38 +12060,6 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
|
|
|
11435
12060
|
});
|
|
11436
12061
|
}
|
|
11437
12062
|
|
|
11438
|
-
// src/harness/pty/adapters/types.ts
|
|
11439
|
-
import { accessSync, constants, statSync } from "fs";
|
|
11440
|
-
import { join as join8 } from "path";
|
|
11441
|
-
var TuiUnavailableError = class extends Error {
|
|
11442
|
-
constructor(tui, message) {
|
|
11443
|
-
super(message);
|
|
11444
|
-
this.tui = tui;
|
|
11445
|
-
this.name = "TuiUnavailableError";
|
|
11446
|
-
}
|
|
11447
|
-
tui;
|
|
11448
|
-
};
|
|
11449
|
-
function isExecutable(path2) {
|
|
11450
|
-
try {
|
|
11451
|
-
if (!statSync(path2).isFile()) return false;
|
|
11452
|
-
accessSync(path2, constants.X_OK);
|
|
11453
|
-
return true;
|
|
11454
|
-
} catch {
|
|
11455
|
-
return false;
|
|
11456
|
-
}
|
|
11457
|
-
}
|
|
11458
|
-
function findOnPath(binary, env = process.env) {
|
|
11459
|
-
if (binary.includes("/")) {
|
|
11460
|
-
return isExecutable(binary) ? binary : null;
|
|
11461
|
-
}
|
|
11462
|
-
for (const dir of (env.PATH ?? "").split(":")) {
|
|
11463
|
-
if (!dir) continue;
|
|
11464
|
-
const candidate = join8(dir, binary);
|
|
11465
|
-
if (isExecutable(candidate)) return candidate;
|
|
11466
|
-
}
|
|
11467
|
-
return null;
|
|
11468
|
-
}
|
|
11469
|
-
|
|
11470
12063
|
// src/execution/playwright-mcp.ts
|
|
11471
12064
|
var PLAYWRIGHT_MCP_BINARIES = ["playwright-mcp", "mcp-server-playwright"];
|
|
11472
12065
|
var PLAYWRIGHT_MCP_ARGS = ["--browser", "chromium", "--headless", "--no-sandbox", "--isolated"];
|
|
@@ -11479,7 +12072,7 @@ function resolvePlaywrightMcpServer(env = process.env) {
|
|
|
11479
12072
|
}
|
|
11480
12073
|
|
|
11481
12074
|
// src/execution/event-handlers.ts
|
|
11482
|
-
var
|
|
12075
|
+
var logger4 = createServiceLogger("event-handlers");
|
|
11483
12076
|
function safeVoid(promise, context) {
|
|
11484
12077
|
if (promise && typeof promise.catch === "function") {
|
|
11485
12078
|
promise.catch((err) => {
|
|
@@ -11634,7 +12227,7 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
|
|
|
11634
12227
|
}
|
|
11635
12228
|
function handleRateLimitEvent(event, host) {
|
|
11636
12229
|
const { rate_limit_info } = event;
|
|
11637
|
-
|
|
12230
|
+
logger4.info("Rate limit event received", { rate_limit_info });
|
|
11638
12231
|
const status = rate_limit_info.status;
|
|
11639
12232
|
const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
|
|
11640
12233
|
if (utilization !== void 0 && rate_limit_info.rateLimitType) {
|
|
@@ -12357,7 +12950,7 @@ function buildCanUseTool(host) {
|
|
|
12357
12950
|
}
|
|
12358
12951
|
|
|
12359
12952
|
// src/execution/query-executor.ts
|
|
12360
|
-
var
|
|
12953
|
+
var logger5 = createServiceLogger("QueryExecutor");
|
|
12361
12954
|
var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
|
|
12362
12955
|
var RETRY_DELAYS_MS2 = [6e4, 12e4, 18e4, 3e5];
|
|
12363
12956
|
function buildHooks(host) {
|
|
@@ -12449,7 +13042,7 @@ function repairTornSessionFile(path2) {
|
|
|
12449
13042
|
}
|
|
12450
13043
|
if (keepEnd === content.length) return false;
|
|
12451
13044
|
truncateSync(path2, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
|
|
12452
|
-
|
|
13045
|
+
logger5.warn("Repaired torn transcript before resume", {
|
|
12453
13046
|
path: path2,
|
|
12454
13047
|
trimmedBytes: content.length - keepEnd
|
|
12455
13048
|
});
|
|
@@ -12500,10 +13093,11 @@ function buildQueryOptions(host, context) {
|
|
|
12500
13093
|
permissionMode: needsCanUseTool ? "plan" : "bypassPermissions",
|
|
12501
13094
|
allowDangerouslySkipPermissions: !needsCanUseTool,
|
|
12502
13095
|
canUseTool: buildCanUseTool(host),
|
|
12503
|
-
//
|
|
12504
|
-
//
|
|
12505
|
-
//
|
|
12506
|
-
|
|
13096
|
+
// A spawned CLI never sees `systemPrompt` (an SDK-only option) — deliver the
|
|
13097
|
+
// same text via `appendSystemPrompt`, which each spawning harness routes its
|
|
13098
|
+
// own way (`claude --append-system-prompt`; opencode an `instructions` file).
|
|
13099
|
+
// Without this an opencode card would run with no task context at all.
|
|
13100
|
+
...host.harnessKind !== "sdk" && systemPromptText ? { appendSystemPrompt: systemPromptText } : {},
|
|
12507
13101
|
// Auto mode pre-exit (residual sessions only — auto now boots post-exit):
|
|
12508
13102
|
// after the ExitPlanMode hook allows the call, the CLI's plan dialog still
|
|
12509
13103
|
// renders — press Enter so the autonomous agent continues building in the
|
|
@@ -12529,7 +13123,7 @@ function buildQueryOptions(host, context) {
|
|
|
12529
13123
|
disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
|
|
12530
13124
|
enableFileCheckpointing: settings.enableFileCheckpointing,
|
|
12531
13125
|
stderr: (data) => {
|
|
12532
|
-
|
|
13126
|
+
logger5.warn("Claude Code stderr", { data: data.trimEnd() });
|
|
12533
13127
|
}
|
|
12534
13128
|
};
|
|
12535
13129
|
}
|
|
@@ -12580,16 +13174,16 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
|
|
|
12580
13174
|
|
|
12581
13175
|
The team says:
|
|
12582
13176
|
${followUpText}` : followUpText;
|
|
12583
|
-
const
|
|
13177
|
+
const skipImages = !supportsImageBlocks(host.harnessKind);
|
|
12584
13178
|
if (isPmMode) {
|
|
12585
|
-
const prompt = buildMultimodalPrompt(textPrompt, context,
|
|
13179
|
+
const prompt = buildMultimodalPrompt(textPrompt, context, skipImages);
|
|
12586
13180
|
if (followUpImages.length > 0 && Array.isArray(prompt)) {
|
|
12587
13181
|
prompt.push(...followUpImages);
|
|
12588
13182
|
}
|
|
12589
13183
|
return prompt;
|
|
12590
13184
|
}
|
|
12591
13185
|
if (followUpImages.length > 0) {
|
|
12592
|
-
if (
|
|
13186
|
+
if (skipImages) {
|
|
12593
13187
|
const refs = followUpImages.map(
|
|
12594
13188
|
() => `[Image attachment \u2014 use list_task_files / get_attachment to view]`
|
|
12595
13189
|
);
|
|
@@ -12639,7 +13233,7 @@ function abortWatchedTurn(host, stderrLine, chatMessage) {
|
|
|
12639
13233
|
host.connection.sendEvent({ type: "error", message: chatMessage });
|
|
12640
13234
|
host.abortController?.abort();
|
|
12641
13235
|
}
|
|
12642
|
-
async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMountDead) {
|
|
13236
|
+
async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMountDead, disableWedgeAbort) {
|
|
12643
13237
|
if (state.abortedAsWedged) {
|
|
12644
13238
|
process.stderr.write(
|
|
12645
13239
|
"[conveyor-agent] Wedged turn did not settle after abort \u2014 abandoning the turn stream\n"
|
|
@@ -12657,6 +13251,10 @@ async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMou
|
|
|
12657
13251
|
return "keep_waiting";
|
|
12658
13252
|
}
|
|
12659
13253
|
if (state.silentMs < silenceTimeoutMs) return "keep_waiting";
|
|
13254
|
+
if (disableWedgeAbort) {
|
|
13255
|
+
state.silentMs = 0;
|
|
13256
|
+
return "keep_waiting";
|
|
13257
|
+
}
|
|
12660
13258
|
if (isHeavyGateActive()) {
|
|
12661
13259
|
state.silentMs = 0;
|
|
12662
13260
|
return "keep_waiting";
|
|
@@ -12693,7 +13291,8 @@ async function* watchForParkedTui(inner, host, opts) {
|
|
|
12693
13291
|
state,
|
|
12694
13292
|
waitMs,
|
|
12695
13293
|
silenceTimeoutMs,
|
|
12696
|
-
opts?.probeMountDead
|
|
13294
|
+
opts?.probeMountDead,
|
|
13295
|
+
opts?.disableWedgeAbort
|
|
12697
13296
|
);
|
|
12698
13297
|
if (outcome === "abandon") return;
|
|
12699
13298
|
continue;
|
|
@@ -12740,7 +13339,7 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
|
|
|
12740
13339
|
await runFollowUpQuery(host, context, options, resume, followUpContent);
|
|
12741
13340
|
return;
|
|
12742
13341
|
}
|
|
12743
|
-
if (isDiscoveryLike && (resume || host.harnessKind
|
|
13342
|
+
if (isDiscoveryLike && (resume || host.harnessKind === "sdk")) {
|
|
12744
13343
|
return;
|
|
12745
13344
|
}
|
|
12746
13345
|
await runInitialQuery(host, context, options, resume, promptDelivery);
|
|
@@ -12795,9 +13394,12 @@ async function runPassiveTurn(host, context) {
|
|
|
12795
13394
|
}
|
|
12796
13395
|
async function trackAndRun(host, context, options, agentQuery) {
|
|
12797
13396
|
if (host.harnessKind === "pty" && options.promptDelivery !== "prefill") {
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
13397
|
+
const rawRelay = host.harness.emitsStructuredEvents === false;
|
|
13398
|
+
agentQuery = watchForParkedTui(
|
|
13399
|
+
agentQuery,
|
|
13400
|
+
host,
|
|
13401
|
+
rawRelay ? { disableWedgeAbort: true } : { probeMountDead: () => isConfigHomeMountDead(options.cwd) }
|
|
13402
|
+
);
|
|
12801
13403
|
}
|
|
12802
13404
|
host.activeQuery = agentQuery;
|
|
12803
13405
|
try {
|
|
@@ -12822,9 +13424,10 @@ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAp
|
|
|
12822
13424
|
};
|
|
12823
13425
|
}
|
|
12824
13426
|
return {
|
|
12825
|
-
//
|
|
12826
|
-
// the prompt body already links
|
|
12827
|
-
|
|
13427
|
+
// Only the SDK takes image blocks: a PTY would paste them into the TUI as
|
|
13428
|
+
// text and headless opencode passes argv — the prompt body already links
|
|
13429
|
+
// each image (get_attachment / downloadUrl).
|
|
13430
|
+
prompt: buildMultimodalPrompt(initialPrompt, context, !supportsImageBlocks(harnessKind)),
|
|
12828
13431
|
appendSystemPrompt: baseAppendSystemPrompt
|
|
12829
13432
|
};
|
|
12830
13433
|
}
|
|
@@ -12865,7 +13468,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
12865
13468
|
const retryPrompt = buildMultimodalPrompt(
|
|
12866
13469
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
12867
13470
|
context,
|
|
12868
|
-
lastErrorWasImage || host.harnessKind
|
|
13471
|
+
lastErrorWasImage || !supportsImageBlocks(host.harnessKind)
|
|
12869
13472
|
);
|
|
12870
13473
|
return host.harness.executeQuery({
|
|
12871
13474
|
prompt: host.createInputStream(retryPrompt),
|
|
@@ -12894,7 +13497,7 @@ async function handleAuthError(context, host, options) {
|
|
|
12894
13497
|
const freshPrompt = buildMultimodalPrompt(
|
|
12895
13498
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
12896
13499
|
context,
|
|
12897
|
-
host.harnessKind
|
|
13500
|
+
!supportsImageBlocks(host.harnessKind)
|
|
12898
13501
|
);
|
|
12899
13502
|
const freshQuery = host.harness.executeQuery({
|
|
12900
13503
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -12909,7 +13512,7 @@ async function handleStaleSession(context, host, options) {
|
|
|
12909
13512
|
const freshPrompt = buildMultimodalPrompt(
|
|
12910
13513
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
12911
13514
|
context,
|
|
12912
|
-
host.harnessKind
|
|
13515
|
+
!supportsImageBlocks(host.harnessKind)
|
|
12913
13516
|
);
|
|
12914
13517
|
const freshQuery = host.harness.executeQuery({
|
|
12915
13518
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -13003,7 +13606,7 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
|
|
|
13003
13606
|
const freshPrompt = buildMultimodalPrompt(
|
|
13004
13607
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
13005
13608
|
context,
|
|
13006
|
-
host.harnessKind
|
|
13609
|
+
!supportsImageBlocks(host.harnessKind)
|
|
13007
13610
|
);
|
|
13008
13611
|
const freshQuery = host.harness.executeQuery({
|
|
13009
13612
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -13076,9 +13679,14 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
13076
13679
|
}
|
|
13077
13680
|
|
|
13078
13681
|
// src/runner/query-bridge.ts
|
|
13079
|
-
var
|
|
13080
|
-
function resolveHarnessKind() {
|
|
13081
|
-
|
|
13682
|
+
var logger6 = createServiceLogger("QueryBridge");
|
|
13683
|
+
function resolveHarnessKind(mode) {
|
|
13684
|
+
if (process.env.CONVEYOR_FORCE_SDK_CARDS === "1") return "sdk";
|
|
13685
|
+
return resolveCardTui(mode) === "opencode" ? "opencode" : "pty";
|
|
13686
|
+
}
|
|
13687
|
+
function resolveCardTui(mode) {
|
|
13688
|
+
if (mode === "code-review") return "claude-code";
|
|
13689
|
+
return resolveTuiKindFromEnv(process.env);
|
|
13082
13690
|
}
|
|
13083
13691
|
function buildPtyBridge(connection, onBackgroundTaskDone) {
|
|
13084
13692
|
return {
|
|
@@ -13099,7 +13707,7 @@ var QueryBridge = class {
|
|
|
13099
13707
|
this.mode = mode;
|
|
13100
13708
|
this.runnerConfig = runnerConfig;
|
|
13101
13709
|
this.callbacks = callbacks;
|
|
13102
|
-
const harnessKind = resolveHarnessKind();
|
|
13710
|
+
const harnessKind = resolveHarnessKind(runnerConfig.mode);
|
|
13103
13711
|
this.harnessKind = harnessKind;
|
|
13104
13712
|
let bridge;
|
|
13105
13713
|
if (harnessKind === "pty") {
|
|
@@ -13109,7 +13717,11 @@ var QueryBridge = class {
|
|
|
13109
13717
|
);
|
|
13110
13718
|
bridge = this.directStream.bridge;
|
|
13111
13719
|
}
|
|
13112
|
-
this.harness = createHarness(
|
|
13720
|
+
this.harness = createHarness(
|
|
13721
|
+
harnessKind,
|
|
13722
|
+
bridge,
|
|
13723
|
+
harnessKind === "pty" ? resolveTuiAdapter(resolveCardTui(runnerConfig.mode)) : void 0
|
|
13724
|
+
);
|
|
13113
13725
|
}
|
|
13114
13726
|
connection;
|
|
13115
13727
|
mode;
|
|
@@ -13249,9 +13861,9 @@ var QueryBridge = class {
|
|
|
13249
13861
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13250
13862
|
const isAbort = this._stopped || /abort/i.test(msg);
|
|
13251
13863
|
if (isAbort) {
|
|
13252
|
-
|
|
13864
|
+
logger6.info("Query stopped by user", { error: msg });
|
|
13253
13865
|
} else {
|
|
13254
|
-
|
|
13866
|
+
logger6.error("Query execution failed", { error: msg });
|
|
13255
13867
|
this.connection.sendEvent({ type: "error", message: msg });
|
|
13256
13868
|
}
|
|
13257
13869
|
} finally {
|
|
@@ -13277,9 +13889,9 @@ var QueryBridge = class {
|
|
|
13277
13889
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13278
13890
|
const isAbort = this._stopped || /abort/i.test(msg);
|
|
13279
13891
|
if (isAbort) {
|
|
13280
|
-
|
|
13892
|
+
logger6.info("Passive turn stopped", { error: msg });
|
|
13281
13893
|
} else {
|
|
13282
|
-
|
|
13894
|
+
logger6.error("Passive turn failed", { error: msg });
|
|
13283
13895
|
this.connection.sendEvent({ type: "error", message: msg });
|
|
13284
13896
|
}
|
|
13285
13897
|
} finally {
|
|
@@ -13466,11 +14078,11 @@ function parseResetInstant(rowText, now, defaultTz = localTimeZone()) {
|
|
|
13466
14078
|
|
|
13467
14079
|
// src/usage/parse-usage.ts
|
|
13468
14080
|
var ESC = "\\u001b";
|
|
13469
|
-
var
|
|
14081
|
+
var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
13470
14082
|
var ANSI_OSC = new RegExp(`${ESC}\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\)`, "g");
|
|
13471
14083
|
var BAR_GLYPHS = /[─-▟]/g;
|
|
13472
14084
|
function normalizeUsageText(stdout) {
|
|
13473
|
-
return stdout.replace(
|
|
14085
|
+
return stdout.replace(ANSI_CSI, "").replace(ANSI_OSC, "").replace(BAR_GLYPHS, " ").replace(/\r/g, "");
|
|
13474
14086
|
}
|
|
13475
14087
|
function parseUsageGauges(stdout, now = Date.now(), defaultTz) {
|
|
13476
14088
|
const text = normalizeUsageText(stdout);
|
|
@@ -13571,10 +14183,10 @@ var UsageProbeRun = class {
|
|
|
13571
14183
|
}
|
|
13572
14184
|
};
|
|
13573
14185
|
async function runUsageProbe(deps = {}) {
|
|
13574
|
-
let
|
|
13575
|
-
if (!
|
|
14186
|
+
let spawn2 = deps.spawn;
|
|
14187
|
+
if (!spawn2) {
|
|
13576
14188
|
try {
|
|
13577
|
-
|
|
14189
|
+
spawn2 = await resolvePtySpawn();
|
|
13578
14190
|
} catch {
|
|
13579
14191
|
return "";
|
|
13580
14192
|
}
|
|
@@ -13590,7 +14202,7 @@ async function runUsageProbe(deps = {}) {
|
|
|
13590
14202
|
return new Promise((resolve) => {
|
|
13591
14203
|
let child;
|
|
13592
14204
|
try {
|
|
13593
|
-
child =
|
|
14205
|
+
child = spawn2(binary, [], {
|
|
13594
14206
|
name: "xterm-256color",
|
|
13595
14207
|
cols: 120,
|
|
13596
14208
|
rows: 45,
|
|
@@ -13606,7 +14218,7 @@ async function runUsageProbe(deps = {}) {
|
|
|
13606
14218
|
}
|
|
13607
14219
|
|
|
13608
14220
|
// src/execution/usage-sampler.ts
|
|
13609
|
-
var
|
|
14221
|
+
var logger7 = createServiceLogger("usage-sampler");
|
|
13610
14222
|
function isAttributable(identity, sessionToken) {
|
|
13611
14223
|
if (!identity) return { ok: true };
|
|
13612
14224
|
if (identity.hasRefreshToken) {
|
|
@@ -13622,7 +14234,7 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
|
|
|
13622
14234
|
try {
|
|
13623
14235
|
const attributable = isAttributable(await readIdentity(), token);
|
|
13624
14236
|
if (!attributable.ok) {
|
|
13625
|
-
|
|
14237
|
+
logger7.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
|
|
13626
14238
|
reason: attributable.reason
|
|
13627
14239
|
});
|
|
13628
14240
|
return [];
|
|
@@ -13649,14 +14261,14 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
|
|
|
13649
14261
|
});
|
|
13650
14262
|
}
|
|
13651
14263
|
if (samples.length === 0) {
|
|
13652
|
-
|
|
14264
|
+
logger7.info("usage sample produced no gauges", {
|
|
13653
14265
|
stdoutLength: stdout.length,
|
|
13654
14266
|
stdoutHead: stdout.slice(0, 200).replaceAll("\n", " ")
|
|
13655
14267
|
});
|
|
13656
14268
|
}
|
|
13657
14269
|
return samples;
|
|
13658
14270
|
} catch (error) {
|
|
13659
|
-
|
|
14271
|
+
logger7.info("usage sample failed", {
|
|
13660
14272
|
error: error instanceof Error ? error.message : String(error)
|
|
13661
14273
|
});
|
|
13662
14274
|
return [];
|
|
@@ -15232,12 +15844,12 @@ var SessionRunner = class _SessionRunner {
|
|
|
15232
15844
|
};
|
|
15233
15845
|
|
|
15234
15846
|
// src/setup/config.ts
|
|
15235
|
-
import { join as
|
|
15847
|
+
import { join as join11 } from "path";
|
|
15236
15848
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
15237
15849
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
15238
15850
|
async function loadForwardPorts(workspaceDir) {
|
|
15239
15851
|
try {
|
|
15240
|
-
const raw = await readWorkspaceFile(
|
|
15852
|
+
const raw = await readWorkspaceFile(join11(workspaceDir, DEVCONTAINER_PATH));
|
|
15241
15853
|
const parsed = JSON.parse(raw);
|
|
15242
15854
|
const ports = (parsed.forwardPorts ?? []).filter(
|
|
15243
15855
|
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
|
@@ -15292,16 +15904,15 @@ export {
|
|
|
15292
15904
|
applyBootstrapToEnv,
|
|
15293
15905
|
AgentConnection,
|
|
15294
15906
|
DEFAULT_SONNET_MODEL,
|
|
15295
|
-
TUI_KINDS,
|
|
15296
15907
|
isPermissionDeniedError,
|
|
15297
15908
|
DEFAULT_LIFECYCLE_CONFIG,
|
|
15298
15909
|
Lifecycle,
|
|
15299
|
-
cleanTerminalOutput,
|
|
15300
15910
|
buildSynthesizedCredentials,
|
|
15301
15911
|
claudeJsonPath,
|
|
15302
|
-
ClaudeTuiAdapter,
|
|
15303
15912
|
createServiceLogger,
|
|
15304
15913
|
PtyHarness,
|
|
15914
|
+
resolveTuiKindFromEnv,
|
|
15915
|
+
resolveTuiAdapter,
|
|
15305
15916
|
workspacePathExists,
|
|
15306
15917
|
GIT_TIMEOUT_MS,
|
|
15307
15918
|
hasUncommittedChanges,
|
|
@@ -15312,8 +15923,6 @@ export {
|
|
|
15312
15923
|
flushPendingChanges,
|
|
15313
15924
|
pushToOrigin,
|
|
15314
15925
|
buildProjectTools,
|
|
15315
|
-
TuiUnavailableError,
|
|
15316
|
-
findOnPath,
|
|
15317
15926
|
resolvePlaywrightMcpServer,
|
|
15318
15927
|
resolveSessionStart,
|
|
15319
15928
|
parseUsageGauges,
|
|
@@ -15328,4 +15937,4 @@ export {
|
|
|
15328
15937
|
loadConveyorConfig,
|
|
15329
15938
|
unshallowRepo
|
|
15330
15939
|
};
|
|
15331
|
-
//# sourceMappingURL=chunk-
|
|
15940
|
+
//# sourceMappingURL=chunk-EQO7QFGN.js.map
|