@ordewell/core 0.6.1 → 0.6.3
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/{IFileSystem-DuEiG_kA.d.ts → IFileSystem-DQEKyC6H.d.ts} +1 -1
- package/dist/{IFileSystem-d_SV7jAH.d.mts → IFileSystem-Qj2MWHn_.d.mts} +1 -1
- package/dist/{ModeResolver-DXxV4n_V.d.ts → ModeResolver-ByNpC0EP.d.ts} +1 -1
- package/dist/{ModeResolver-DIReYAOq.d.mts → ModeResolver-rqaY_4Ex.d.mts} +1 -1
- package/dist/{Task-ib_U0YP7.d.mts → Task-xqdxugt2.d.mts} +31 -1
- package/dist/{Task-ib_U0YP7.d.ts → Task-xqdxugt2.d.ts} +31 -1
- package/dist/index.d.mts +139 -97
- package/dist/index.d.ts +139 -97
- package/dist/index.js +934 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +914 -61
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +1 -1
- package/dist/order-labels.d.ts +1 -1
- package/dist/{parsing-C8Z22twN.d.ts → parsing-COPG8-Ep.d.ts} +2 -2
- package/dist/{parsing-DjRJJX-y.d.mts → parsing-DDZ1rUYa.d.mts} +2 -2
- package/dist/parsing.d.mts +3 -3
- package/dist/parsing.d.ts +3 -3
- package/dist/{plan-utils-DvHPzAfV.d.ts → plan-utils-Br8UAoiu.d.ts} +8 -3
- package/dist/{plan-utils-D4ioaM8q.d.mts → plan-utils-DMgajMvd.d.mts} +8 -3
- package/dist/plan-utils.d.mts +3 -3
- package/dist/plan-utils.d.ts +3 -3
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -127,6 +127,8 @@ import {
|
|
|
127
127
|
var HEAD_LINES = 60;
|
|
128
128
|
var TAIL_LINES = 40;
|
|
129
129
|
var MAX_LINE_CHARS = 2e3;
|
|
130
|
+
var DIGEST_ARGS_CHARS = 120;
|
|
131
|
+
var DIGEST_MESSAGE_CHARS = 1e3;
|
|
130
132
|
function capLine(line) {
|
|
131
133
|
return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
132
134
|
}
|
|
@@ -209,6 +211,33 @@ function coalesceTaskLog(events) {
|
|
|
209
211
|
}
|
|
210
212
|
return out;
|
|
211
213
|
}
|
|
214
|
+
function digestTaskLog(events, maxCalls) {
|
|
215
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
216
|
+
let lastText;
|
|
217
|
+
let lastError;
|
|
218
|
+
for (const event of events) {
|
|
219
|
+
if (event.type === "tool_result") outcomes.set(event.id, event.success);
|
|
220
|
+
else if (event.type === "text") lastText = event.text;
|
|
221
|
+
else if (event.type === "error") lastError = event.message;
|
|
222
|
+
}
|
|
223
|
+
const lines = [];
|
|
224
|
+
for (const event of events) {
|
|
225
|
+
if (event.type !== "tool_call") continue;
|
|
226
|
+
const outcome = outcomes.get(event.id);
|
|
227
|
+
const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
|
|
228
|
+
lines.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
|
|
229
|
+
}
|
|
230
|
+
const omitted = lines.length - maxCalls;
|
|
231
|
+
const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines.slice(omitted)] : lines;
|
|
232
|
+
const gap = () => out.length > 0 ? [""] : [];
|
|
233
|
+
const said2 = lastText?.trim();
|
|
234
|
+
if (said2) {
|
|
235
|
+
const end = said2.length > DIGEST_MESSAGE_CHARS ? `\u2026${said2.slice(-DIGEST_MESSAGE_CHARS)}` : said2;
|
|
236
|
+
out.push(...gap(), "Its last message:", ...end.split("\n").map((l) => ` ${l}`));
|
|
237
|
+
}
|
|
238
|
+
if (lastError) out.push(...gap(), `It stopped with an error: ${lastError}`);
|
|
239
|
+
return out.join("\n");
|
|
240
|
+
}
|
|
212
241
|
|
|
213
242
|
// src/interfaces/IFileSystem.ts
|
|
214
243
|
var GREP_DEFAULT_HEAD_LIMIT = 100;
|
|
@@ -2446,6 +2475,10 @@ function lex(command, nested, dialect) {
|
|
|
2446
2475
|
let unsafeRedirect;
|
|
2447
2476
|
let processSubstitution = false;
|
|
2448
2477
|
let unbalanced = false;
|
|
2478
|
+
let grouped = false;
|
|
2479
|
+
let joinedBy;
|
|
2480
|
+
let followable = true;
|
|
2481
|
+
let bareSegment = false;
|
|
2449
2482
|
let tokens = [];
|
|
2450
2483
|
let expandable = false;
|
|
2451
2484
|
let current = "";
|
|
@@ -2475,13 +2508,25 @@ function lex(command, nested, dialect) {
|
|
|
2475
2508
|
current = "";
|
|
2476
2509
|
started = false;
|
|
2477
2510
|
};
|
|
2478
|
-
const endSegment = (nextPiped) => {
|
|
2511
|
+
const endSegment = (nextPiped, next = "other", last = false) => {
|
|
2479
2512
|
endToken();
|
|
2480
|
-
if (
|
|
2513
|
+
if (!last && next === "other") followable = false;
|
|
2514
|
+
if (tokens.length > 0) {
|
|
2515
|
+
const seg = toSegment(tokens, piped, dialect);
|
|
2516
|
+
if (!seg.binary) {
|
|
2517
|
+
followable = false;
|
|
2518
|
+
bareSegment = true;
|
|
2519
|
+
}
|
|
2520
|
+
segments.push({ ...seg, expandable, inputs, joinedBy });
|
|
2521
|
+
} else if (!last || segments.length > 0 || joinedBy !== void 0) {
|
|
2522
|
+
followable = false;
|
|
2523
|
+
bareSegment = true;
|
|
2524
|
+
}
|
|
2481
2525
|
tokens = [];
|
|
2482
2526
|
inputs = [];
|
|
2483
2527
|
expandable = false;
|
|
2484
2528
|
piped = nextPiped;
|
|
2529
|
+
joinedBy = next;
|
|
2485
2530
|
};
|
|
2486
2531
|
let i = 0;
|
|
2487
2532
|
while (i < command.length) {
|
|
@@ -2603,17 +2648,18 @@ function lex(command, nested, dialect) {
|
|
|
2603
2648
|
}
|
|
2604
2649
|
if (c === "(" || c === ")") {
|
|
2605
2650
|
endToken();
|
|
2651
|
+
grouped = true;
|
|
2606
2652
|
i++;
|
|
2607
2653
|
continue;
|
|
2608
2654
|
}
|
|
2609
2655
|
if (c === "|") {
|
|
2610
2656
|
const double = command[i + 1] === "|";
|
|
2611
|
-
endSegment(!double);
|
|
2657
|
+
endSegment(!double, double ? "other" : "pipe");
|
|
2612
2658
|
i += double || command[i + 1] === "&" ? 2 : 1;
|
|
2613
2659
|
continue;
|
|
2614
2660
|
}
|
|
2615
2661
|
if (c === "&" || c === ";" || c === "\n") {
|
|
2616
|
-
endSegment(false);
|
|
2662
|
+
endSegment(false, c === "&" && command[i + 1] === "&" ? "and" : "other");
|
|
2617
2663
|
i += command[i + 1] === c ? 2 : 1;
|
|
2618
2664
|
continue;
|
|
2619
2665
|
}
|
|
@@ -2630,8 +2676,8 @@ function lex(command, nested, dialect) {
|
|
|
2630
2676
|
i++;
|
|
2631
2677
|
}
|
|
2632
2678
|
if (quote !== "") unbalanced = true;
|
|
2633
|
-
endSegment(false);
|
|
2634
|
-
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
2679
|
+
endSegment(false, "other", true);
|
|
2680
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped, followable, bareSegment };
|
|
2635
2681
|
}
|
|
2636
2682
|
function binaryName(token, dialect) {
|
|
2637
2683
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -2771,10 +2817,12 @@ function unwrap(seg, dialect) {
|
|
|
2771
2817
|
}
|
|
2772
2818
|
return { seg: { ...current, assignments }, wrappers, runner };
|
|
2773
2819
|
}
|
|
2774
|
-
function lexAll(command, dialect) {
|
|
2820
|
+
function lexAll(command, dialect, cdpathSet = false) {
|
|
2775
2821
|
const queue = [];
|
|
2776
2822
|
const top = lex(command, queue, dialect);
|
|
2777
2823
|
const all = { ...top, segments: top.segments.filter((s) => s.binary) };
|
|
2824
|
+
markShellState(all.segments, top.bareSegment);
|
|
2825
|
+
if (top.followable && !top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
|
|
2778
2826
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
2779
2827
|
const inner = lex(queue.shift(), queue, dialect);
|
|
2780
2828
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
@@ -2784,15 +2832,61 @@ function lexAll(command, dialect) {
|
|
|
2784
2832
|
}
|
|
2785
2833
|
return all;
|
|
2786
2834
|
}
|
|
2835
|
+
function literalCdTarget(seg) {
|
|
2836
|
+
if (seg.binary !== "cd" || seg.assignments.length > 0 || seg.expandable || seg.inputs.length > 0) return void 0;
|
|
2837
|
+
if (seg.args.length !== 1) return void 0;
|
|
2838
|
+
const [target] = seg.args;
|
|
2839
|
+
return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
|
|
2840
|
+
}
|
|
2841
|
+
function followCd(segments) {
|
|
2842
|
+
if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
|
|
2843
|
+
const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
|
|
2844
|
+
if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
|
|
2845
|
+
let chain = [];
|
|
2846
|
+
segments.forEach((seg, i) => {
|
|
2847
|
+
seg.cwd = chain;
|
|
2848
|
+
const target = inPipeline(i) ? void 0 : literalCdTarget(seg);
|
|
2849
|
+
if (target !== void 0) chain = [...chain, target];
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
function markShellState(segments, bareSegment) {
|
|
2853
|
+
if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
|
|
2854
|
+
for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
|
|
2855
|
+
}
|
|
2856
|
+
var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
|
|
2857
|
+
"export",
|
|
2858
|
+
"set",
|
|
2859
|
+
"shopt",
|
|
2860
|
+
"declare",
|
|
2861
|
+
"typeset",
|
|
2862
|
+
"readonly",
|
|
2863
|
+
"local",
|
|
2864
|
+
"unset",
|
|
2865
|
+
"let",
|
|
2866
|
+
"source",
|
|
2867
|
+
".",
|
|
2868
|
+
"eval",
|
|
2869
|
+
"exec",
|
|
2870
|
+
"pushd",
|
|
2871
|
+
"popd",
|
|
2872
|
+
"alias",
|
|
2873
|
+
"unalias",
|
|
2874
|
+
"builtin",
|
|
2875
|
+
"command",
|
|
2876
|
+
"enable",
|
|
2877
|
+
"hash",
|
|
2878
|
+
"trap"
|
|
2879
|
+
]);
|
|
2787
2880
|
function looksLikePath(arg) {
|
|
2788
2881
|
if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
|
|
2789
2882
|
return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
|
|
2790
2883
|
}
|
|
2791
|
-
function
|
|
2884
|
+
function pathRefs(command, opts = {}) {
|
|
2792
2885
|
const dialect = dialectFor(opts.dialect);
|
|
2793
|
-
return lexAll(command, dialect).segments.flatMap((seg) => {
|
|
2886
|
+
return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
|
|
2794
2887
|
const patterns = patternArgs(seg);
|
|
2795
|
-
|
|
2888
|
+
const cwd = seg.cwd ?? [];
|
|
2889
|
+
return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path26) => ({ path: path26, cwd }));
|
|
2796
2890
|
});
|
|
2797
2891
|
}
|
|
2798
2892
|
function pathIn(a) {
|
|
@@ -2956,7 +3050,8 @@ function scopeFor(seg) {
|
|
|
2956
3050
|
}
|
|
2957
3051
|
return [seg.binary, ...lead].join(" ");
|
|
2958
3052
|
}
|
|
2959
|
-
function isAuto(seg) {
|
|
3053
|
+
function isAuto(seg, opts) {
|
|
3054
|
+
if (seg.binary === "cd") return !opts.cdpathSet && !seg.cdSteered && literalCdTarget(seg) !== void 0;
|
|
2960
3055
|
if (!AUTO_COMMANDS.includes(seg.binary)) return false;
|
|
2961
3056
|
if (seg.expandable) return false;
|
|
2962
3057
|
if (seg.binary !== "git") return true;
|
|
@@ -3051,7 +3146,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3051
3146
|
const trimmed = command.trim();
|
|
3052
3147
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
3053
3148
|
const dialect = dialectFor(opts.dialect);
|
|
3054
|
-
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
3149
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect, opts.cdpathSet);
|
|
3055
3150
|
if (unbalanced) {
|
|
3056
3151
|
return {
|
|
3057
3152
|
tier: "refuse",
|
|
@@ -3088,7 +3183,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3088
3183
|
};
|
|
3089
3184
|
}
|
|
3090
3185
|
}
|
|
3091
|
-
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg));
|
|
3186
|
+
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
|
|
3092
3187
|
if (nonAuto.length === 0) return { tier: "auto", scope: "" };
|
|
3093
3188
|
const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
|
|
3094
3189
|
return { tier: "ask", scope };
|
|
@@ -3158,6 +3253,9 @@ function researchShellWarning(shell) {
|
|
|
3158
3253
|
if (shell.dialect !== "cmd") return null;
|
|
3159
3254
|
return "No POSIX shell was found on this machine, so research commands run through cmd.exe, where most read-only tools (ls, cat, wc, head, grep, find) do not exist. Installing Git for Windows provides one and restores the full research toolset.";
|
|
3160
3255
|
}
|
|
3256
|
+
function researchPolicyOptions(shell) {
|
|
3257
|
+
return { dialect: shell.dialect, cdpathSet: !!process.env.CDPATH };
|
|
3258
|
+
}
|
|
3161
3259
|
|
|
3162
3260
|
// src/services/pathScope.ts
|
|
3163
3261
|
import * as os from "os";
|
|
@@ -3168,11 +3266,15 @@ function expandHome(target) {
|
|
|
3168
3266
|
if (target === "~" || target.startsWith("~/")) return path2.join(home, target.slice(1));
|
|
3169
3267
|
return path2.join(path2.dirname(home), target.slice(1));
|
|
3170
3268
|
}
|
|
3171
|
-
function resolveWithin(root, target) {
|
|
3172
|
-
const abs = path2.resolve(
|
|
3269
|
+
function resolveWithin(root, target, from = root) {
|
|
3270
|
+
const abs = path2.resolve(from, expandHome(target));
|
|
3173
3271
|
const rel = path2.relative(root, abs);
|
|
3174
3272
|
return { abs, inside: rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel) };
|
|
3175
3273
|
}
|
|
3274
|
+
function resolveRef(root, ref) {
|
|
3275
|
+
const from = ref.cwd.reduce((dir, target) => resolveWithin(root, target, dir).abs, root);
|
|
3276
|
+
return resolveWithin(root, ref.path, from);
|
|
3277
|
+
}
|
|
3176
3278
|
function grantScopeFor(abs, kind) {
|
|
3177
3279
|
if (kind === "directory") return path2.join(abs, "*");
|
|
3178
3280
|
const parent = path2.dirname(abs);
|
|
@@ -3378,14 +3480,17 @@ ${refs.output.trim()}`);
|
|
|
3378
3480
|
outsidePaths(command) {
|
|
3379
3481
|
const root = this.getWorkspaceRoot();
|
|
3380
3482
|
const found = /* @__PURE__ */ new Map();
|
|
3381
|
-
for (const
|
|
3382
|
-
const { abs, inside } =
|
|
3483
|
+
for (const ref of pathRefs(command, this.policyOptions())) {
|
|
3484
|
+
const { abs, inside } = resolveRef(root, ref);
|
|
3383
3485
|
if (inside || isInertDevice(abs)) continue;
|
|
3384
3486
|
const scope = grantScopeFor(abs, "file");
|
|
3385
3487
|
if (!found.has(scope)) found.set(scope, abs);
|
|
3386
3488
|
}
|
|
3387
3489
|
return [...found].map(([scope, abs]) => ({ abs, scope }));
|
|
3388
3490
|
}
|
|
3491
|
+
policyOptions() {
|
|
3492
|
+
return researchPolicyOptions(this.researchShell);
|
|
3493
|
+
}
|
|
3389
3494
|
/**
|
|
3390
3495
|
* Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
|
|
3391
3496
|
* anything else asks once and is remembered, and writes are refused outright
|
|
@@ -3397,7 +3502,7 @@ ${refs.output.trim()}`);
|
|
|
3397
3502
|
* directory the next command names.
|
|
3398
3503
|
*/
|
|
3399
3504
|
async bash(command, signal) {
|
|
3400
|
-
const { tier, scope, reason } = classifyCommand(command,
|
|
3505
|
+
const { tier, scope, reason } = classifyCommand(command, this.policyOptions());
|
|
3401
3506
|
if (tier === "refuse") {
|
|
3402
3507
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
3403
3508
|
}
|
|
@@ -3896,10 +4001,78 @@ function buildGrepArgs(pattern, opts, root) {
|
|
|
3896
4001
|
function buildGlobArgs(pattern, root) {
|
|
3897
4002
|
return ["--files", ...SORT_BY_RECENCY, "--glob", pattern, ...exclusionArgs(), "--", root];
|
|
3898
4003
|
}
|
|
3899
|
-
|
|
4004
|
+
var GREP_PCRE_PROBE_ARGS = ["-P", "-e", "x", "/dev/null"];
|
|
4005
|
+
function grepRegexFromProbe(exitCode) {
|
|
4006
|
+
return exitCode === 0 || exitCode === 1 ? "pcre" : "ere";
|
|
4007
|
+
}
|
|
4008
|
+
function toPosixClasses(pattern) {
|
|
4009
|
+
const CLASS_ESCAPES = { s: "[:space:]", w: "[:alnum:]_", d: "[:digit:]", t: " " };
|
|
4010
|
+
let out = "";
|
|
4011
|
+
let i = 0;
|
|
4012
|
+
while (i < pattern.length) {
|
|
4013
|
+
const c = pattern[i];
|
|
4014
|
+
if (c === "\\") {
|
|
4015
|
+
out += pattern.slice(i, i + 2);
|
|
4016
|
+
i += 2;
|
|
4017
|
+
continue;
|
|
4018
|
+
}
|
|
4019
|
+
if (c !== "[") {
|
|
4020
|
+
out += c;
|
|
4021
|
+
i++;
|
|
4022
|
+
continue;
|
|
4023
|
+
}
|
|
4024
|
+
const start = i;
|
|
4025
|
+
i++;
|
|
4026
|
+
let negate = false;
|
|
4027
|
+
if (pattern[i] === "^") {
|
|
4028
|
+
negate = true;
|
|
4029
|
+
i++;
|
|
4030
|
+
}
|
|
4031
|
+
let body = "";
|
|
4032
|
+
let closeBracket = false;
|
|
4033
|
+
let openBracket = false;
|
|
4034
|
+
let dash = false;
|
|
4035
|
+
let closed = false;
|
|
4036
|
+
while (i < pattern.length) {
|
|
4037
|
+
const ch = pattern[i];
|
|
4038
|
+
if (ch === "]") {
|
|
4039
|
+
closed = true;
|
|
4040
|
+
i++;
|
|
4041
|
+
break;
|
|
4042
|
+
}
|
|
4043
|
+
if (ch === "[" && pattern[i + 1] === ":") {
|
|
4044
|
+
const end = pattern.indexOf(":]", i + 2);
|
|
4045
|
+
if (end !== -1) {
|
|
4046
|
+
body += pattern.slice(i, end + 2);
|
|
4047
|
+
i = end + 2;
|
|
4048
|
+
continue;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
4052
|
+
const e = pattern[i + 1];
|
|
4053
|
+
i += 2;
|
|
4054
|
+
if (e in CLASS_ESCAPES) body += CLASS_ESCAPES[e];
|
|
4055
|
+
else if (e === "]") closeBracket = true;
|
|
4056
|
+
else if (e === "[") openBracket = true;
|
|
4057
|
+
else if (e === "-") dash = true;
|
|
4058
|
+
else body += e;
|
|
4059
|
+
continue;
|
|
4060
|
+
}
|
|
4061
|
+
body += ch;
|
|
4062
|
+
i++;
|
|
4063
|
+
}
|
|
4064
|
+
if (!closed) {
|
|
4065
|
+
out += pattern.slice(start);
|
|
4066
|
+
break;
|
|
4067
|
+
}
|
|
4068
|
+
out += `[${negate ? "^" : ""}${closeBracket ? "]" : ""}${body}${openBracket ? "[" : ""}${dash ? "-" : ""}]`;
|
|
4069
|
+
}
|
|
4070
|
+
return out;
|
|
4071
|
+
}
|
|
4072
|
+
function buildFallbackGrepArgs(pattern, opts, root, regex = "pcre") {
|
|
3900
4073
|
const args = ["-r", "-n"];
|
|
3901
4074
|
if (opts.literal) args.push("-F");
|
|
3902
|
-
else args.push("-P");
|
|
4075
|
+
else args.push(regex === "pcre" ? "-P" : "-E");
|
|
3903
4076
|
if (opts.caseInsensitive) args.push("-i");
|
|
3904
4077
|
if (opts.outputMode === "files") args.push("-l");
|
|
3905
4078
|
if (opts.outputMode === "count") args.push("-c");
|
|
@@ -3909,7 +4082,7 @@ function buildFallbackGrepArgs(pattern, opts, root) {
|
|
|
3909
4082
|
}
|
|
3910
4083
|
if (opts.include) args.push(`--include=${opts.include}`);
|
|
3911
4084
|
for (const dir of SEARCH_EXCLUSIONS) args.push(`--exclude-dir=${dir}`);
|
|
3912
|
-
args.push("-e", pattern, root);
|
|
4085
|
+
args.push("-e", !opts.literal && regex === "ere" ? toPosixClasses(pattern) : pattern, root);
|
|
3913
4086
|
return args;
|
|
3914
4087
|
}
|
|
3915
4088
|
function globSegmentToRegex(segment) {
|
|
@@ -7599,15 +7772,15 @@ function nonPromptingFs(fs16) {
|
|
|
7599
7772
|
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs16.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
7600
7773
|
listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
7601
7774
|
bash: async (command, signal) => {
|
|
7602
|
-
const
|
|
7603
|
-
const { tier, reason } = classifyCommand(command,
|
|
7775
|
+
const policy = researchPolicyOptions(resolveResearchShell());
|
|
7776
|
+
const { tier, reason } = classifyCommand(command, policy);
|
|
7604
7777
|
if (tier !== "auto") {
|
|
7605
7778
|
return refused2(
|
|
7606
7779
|
tier === "refuse" ? `Command refused: ${reason}` : `"${command}" needs user approval, and research agents run in the background where nothing can be approved. Use the read-only research tools, and note in your digest that this command would answer the question.`
|
|
7607
7780
|
);
|
|
7608
7781
|
}
|
|
7609
|
-
const escaping =
|
|
7610
|
-
if (escaping) return outsideRefusal(escaping);
|
|
7782
|
+
const escaping = pathRefs(command, policy).map((ref) => resolveRef(fs16.getWorkspaceRoot(), ref)).find((r) => !r.inside);
|
|
7783
|
+
if (escaping) return outsideRefusal(escaping.abs);
|
|
7611
7784
|
return fs16.bash(command, signal);
|
|
7612
7785
|
}
|
|
7613
7786
|
};
|
|
@@ -8923,6 +9096,12 @@ var VerdictEngine = class {
|
|
|
8923
9096
|
idleSince = /* @__PURE__ */ new Map();
|
|
8924
9097
|
/** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
|
|
8925
9098
|
idlePaused = /* @__PURE__ */ new Set();
|
|
9099
|
+
/**
|
|
9100
|
+
* Open tool approvals per structured task. They arrive mid-turn and leave the
|
|
9101
|
+
* status alone (ADR-0018, W1), but a task waiting on one is waiting on the
|
|
9102
|
+
* user, so it is not idle either.
|
|
9103
|
+
*/
|
|
9104
|
+
openApprovals = /* @__PURE__ */ new Map();
|
|
8926
9105
|
onVerdict(listener) {
|
|
8927
9106
|
this.listeners.push(listener);
|
|
8928
9107
|
}
|
|
@@ -8951,7 +9130,7 @@ var VerdictEngine = class {
|
|
|
8951
9130
|
}
|
|
8952
9131
|
/** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
|
|
8953
9132
|
touchIdle(taskId, gen) {
|
|
8954
|
-
if (this.idlePaused.has(taskId)) return;
|
|
9133
|
+
if (this.idlePaused.has(taskId) || this.openApprovals.get(taskId)?.size) return;
|
|
8955
9134
|
const existing = this.idleTimers.get(taskId);
|
|
8956
9135
|
if (existing) clearTimeout(existing);
|
|
8957
9136
|
if (this.idleSince.get(taskId)) {
|
|
@@ -9033,7 +9212,10 @@ ${line}
|
|
|
9033
9212
|
});
|
|
9034
9213
|
if (isStructuredSession(session)) {
|
|
9035
9214
|
session.onEvent((event) => {
|
|
9036
|
-
if (
|
|
9215
|
+
if (this.generations.get(task.id) !== gen) return;
|
|
9216
|
+
if (event.type === "turn_start") this.resumeIdle(task.id);
|
|
9217
|
+
else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
|
|
9218
|
+
else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
|
|
9037
9219
|
});
|
|
9038
9220
|
}
|
|
9039
9221
|
session.onExit((exitCode) => {
|
|
@@ -9044,6 +9226,18 @@ ${line}
|
|
|
9044
9226
|
for (const l of this.listeners) l(task.id, verdict);
|
|
9045
9227
|
});
|
|
9046
9228
|
}
|
|
9229
|
+
approvalOpened(taskId, approvalId) {
|
|
9230
|
+
const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
9231
|
+
open.add(approvalId);
|
|
9232
|
+
this.openApprovals.set(taskId, open);
|
|
9233
|
+
this.clearIdle(taskId);
|
|
9234
|
+
}
|
|
9235
|
+
approvalClosed(taskId, approvalId, gen) {
|
|
9236
|
+
const open = this.openApprovals.get(taskId);
|
|
9237
|
+
if (!open?.delete(approvalId) || open.size) return;
|
|
9238
|
+
this.openApprovals.delete(taskId);
|
|
9239
|
+
this.touchIdle(taskId, gen);
|
|
9240
|
+
}
|
|
9047
9241
|
/** Scan only the new text plus the unmatched carry, so a long run stays linear. */
|
|
9048
9242
|
scanCheckpoints(taskId, session, text) {
|
|
9049
9243
|
const scan = (this.checkpointCarry.get(taskId) ?? "") + text;
|
|
@@ -9065,6 +9259,7 @@ ${line}
|
|
|
9065
9259
|
this.checkpointCarry.delete(taskId);
|
|
9066
9260
|
this.pausedSessions.delete(taskId);
|
|
9067
9261
|
this.idlePaused.delete(taskId);
|
|
9262
|
+
this.openApprovals.delete(taskId);
|
|
9068
9263
|
this.clearIdle(taskId);
|
|
9069
9264
|
}
|
|
9070
9265
|
/** Manual "Mark complete" override: a pass verdict that bypasses evidence. */
|
|
@@ -9102,6 +9297,7 @@ ${line}
|
|
|
9102
9297
|
this.idleTimers.clear();
|
|
9103
9298
|
this.idleSince.clear();
|
|
9104
9299
|
this.idlePaused.clear();
|
|
9300
|
+
this.openApprovals.clear();
|
|
9105
9301
|
this.generations.clear();
|
|
9106
9302
|
}
|
|
9107
9303
|
decide(task, exitCode) {
|
|
@@ -10623,11 +10819,17 @@ function buildShellInvocation(command, args) {
|
|
|
10623
10819
|
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
10624
10820
|
return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
|
|
10625
10821
|
}
|
|
10822
|
+
var BSD_SCRIPT_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "freebsd", "openbsd", "netbsd"]);
|
|
10823
|
+
var BSD_SCRIPT_FEED = 'exec 5<&0; p=$(mktemp) || exit 1; { cat <&5 & echo $! >"$p"; wait; } 2>/dev/null | { script -q -e -F /dev/null /bin/sh -c "$0"; s=$?; kill "$(cat "$p")" 2>/dev/null; rm -f "$p"; exit $s; }';
|
|
10626
10824
|
function wrapWithPty(command, args, opts = {}) {
|
|
10627
10825
|
const inner = [command, ...args].map(posixShellQuote).join(" ");
|
|
10628
10826
|
const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
|
|
10629
10827
|
const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
|
|
10630
|
-
|
|
10828
|
+
const line = `${watcher}${setup}${inner}`;
|
|
10829
|
+
if (BSD_SCRIPT_PLATFORMS.has(opts.platform ?? process.platform)) {
|
|
10830
|
+
return { command: "/bin/sh", args: ["-c", BSD_SCRIPT_FEED, line] };
|
|
10831
|
+
}
|
|
10832
|
+
return { command: "script", args: ["-q", "-e", "-f", "-c", line, "/dev/null"] };
|
|
10631
10833
|
}
|
|
10632
10834
|
|
|
10633
10835
|
// src/services/blockingPrompts.ts
|
|
@@ -10822,24 +11024,37 @@ var Block = /* @__PURE__ */ ((Block2) => {
|
|
|
10822
11024
|
Block2[Block2["IfHeadlessSession"] = 8] = "IfHeadlessSession";
|
|
10823
11025
|
Block2[Block2["IfInteractiveVariant"] = 9] = "IfInteractiveVariant";
|
|
10824
11026
|
Block2[Block2["IfProjectTrust"] = 10] = "IfProjectTrust";
|
|
11027
|
+
Block2[Block2["IfHeadlessModel"] = 11] = "IfHeadlessModel";
|
|
10825
11028
|
return Block2;
|
|
10826
11029
|
})(Block || {});
|
|
10827
11030
|
function isInteractive(ctx) {
|
|
10828
11031
|
return ctx.interactive ?? !ctx.headless;
|
|
10829
11032
|
}
|
|
11033
|
+
function majorVersion(version) {
|
|
11034
|
+
const match = version?.match(/(\d+)\.\d+/);
|
|
11035
|
+
return match ? Number(match[1]) : void 0;
|
|
11036
|
+
}
|
|
11037
|
+
function versionedInvocation(manifest, ctx) {
|
|
11038
|
+
const major = majorVersion(ctx.runnerVersion);
|
|
11039
|
+
if (major === void 0) return void 0;
|
|
11040
|
+
return (manifest.runner.versioned ?? []).filter((v) => major >= v.minMajor).sort((a, b) => b.minMajor - a.minMajor)[0];
|
|
11041
|
+
}
|
|
10830
11042
|
function resolveArgs(manifest, ctx) {
|
|
10831
11043
|
const args = [];
|
|
10832
11044
|
const env = {};
|
|
10833
|
-
|
|
10834
|
-
|
|
11045
|
+
const versioned = versionedInvocation(manifest, ctx);
|
|
11046
|
+
const argsTemplate = versioned?.argsTemplate ?? manifest.runner.argsTemplate;
|
|
11047
|
+
const envTemplate = versioned ? versioned.env : manifest.runner.env;
|
|
11048
|
+
if (envTemplate) {
|
|
11049
|
+
for (const [key, value] of Object.entries(envTemplate)) {
|
|
10835
11050
|
const resolved = substituteSimple(value, ctx);
|
|
10836
11051
|
if (resolved.length > 0) env[key] = resolved;
|
|
10837
11052
|
}
|
|
10838
11053
|
}
|
|
10839
11054
|
let block = 0 /* None */;
|
|
10840
11055
|
let include = true;
|
|
10841
|
-
for (let i = 0; i <
|
|
10842
|
-
const raw =
|
|
11056
|
+
for (let i = 0; i < argsTemplate.length; i++) {
|
|
11057
|
+
const raw = argsTemplate[i];
|
|
10843
11058
|
const blockOpen = parseBlockOpen(raw);
|
|
10844
11059
|
if (blockOpen !== null) {
|
|
10845
11060
|
if (block !== 0 /* None */) throw new ResolveError(`Nested conditional block at position ${i}: "${raw}"`);
|
|
@@ -10898,6 +11113,8 @@ function parseBlockOpen(raw) {
|
|
|
10898
11113
|
return 9 /* IfInteractiveVariant */;
|
|
10899
11114
|
case "{{if projectTrust}}":
|
|
10900
11115
|
return 10 /* IfProjectTrust */;
|
|
11116
|
+
case "{{if headlessModel}}":
|
|
11117
|
+
return 11 /* IfHeadlessModel */;
|
|
10901
11118
|
default:
|
|
10902
11119
|
return null;
|
|
10903
11120
|
}
|
|
@@ -10937,6 +11154,9 @@ function shouldIncludeBlock(block, ctx) {
|
|
|
10937
11154
|
// there is no cwd to name.
|
|
10938
11155
|
case 10 /* IfProjectTrust */:
|
|
10939
11156
|
return isInteractive(ctx) && !!ctx.cwd;
|
|
11157
|
+
// A model flag that only the non-interactive subcommand takes.
|
|
11158
|
+
case 11 /* IfHeadlessModel */:
|
|
11159
|
+
return !!ctx.model && !isInteractive(ctx);
|
|
10940
11160
|
default:
|
|
10941
11161
|
return true;
|
|
10942
11162
|
}
|
|
@@ -10982,6 +11202,10 @@ function resolveToken(token, manifest, ctx) {
|
|
|
10982
11202
|
if (token === "{{model}}") return ctx.model || "";
|
|
10983
11203
|
if (token === "{{thinkingEffort}}") return ctx.thinkingEffort || "";
|
|
10984
11204
|
if (token === "{{mode}}") return ctx.mode || "build";
|
|
11205
|
+
if (token === "{{modelWithVariant}}") {
|
|
11206
|
+
if (!ctx.model) return "";
|
|
11207
|
+
return ctx.thinkingEffort ? `${ctx.model}#${ctx.thinkingEffort}` : ctx.model;
|
|
11208
|
+
}
|
|
10985
11209
|
if (token === "{{projectTrust}}") {
|
|
10986
11210
|
if (!ctx.cwd) return "";
|
|
10987
11211
|
return `projects.${JSON.stringify(ctx.cwd)}.trust_level="trusted"`;
|
|
@@ -11056,13 +11280,21 @@ function resolveOpencodeVariantConfig(ctx) {
|
|
|
11056
11280
|
}
|
|
11057
11281
|
return JSON.stringify(config);
|
|
11058
11282
|
}
|
|
11283
|
+
function resolveOpencodeSessionConfig(ctx) {
|
|
11284
|
+
const mode = ctx.mode || "build";
|
|
11285
|
+
const pinned = resolveOpencodeVariantConfig(ctx);
|
|
11286
|
+
const config = pinned ? JSON.parse(pinned) : {};
|
|
11287
|
+
config.default_agent = mode;
|
|
11288
|
+
if (ctx.model && !pinned) config.agent = { [mode]: { model: ctx.model } };
|
|
11289
|
+
return JSON.stringify(config);
|
|
11290
|
+
}
|
|
11059
11291
|
function substituteSimple(value, ctx) {
|
|
11060
11292
|
const conditioned = value.replace(/\{\{if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, name, body) => {
|
|
11061
11293
|
const block = parseBlockOpen(`{{if ${name}}}`);
|
|
11062
11294
|
if (block === null) return match;
|
|
11063
11295
|
return shouldIncludeBlock(block, ctx) ? body : "";
|
|
11064
11296
|
});
|
|
11065
|
-
return conditioned.replace(/\{\{opencodeVariantConfig\}\}/g, () => resolveOpencodeVariantConfig(ctx)).replace(/\{\{prompt\}\}/g, ctx.prompt).replace(/\{\{model\}\}/g, ctx.model || "").replace(/\{\{thinkingEffort\}\}/g, ctx.thinkingEffort || "").replace(/\{\{mode\}\}/g, ctx.mode || "build");
|
|
11297
|
+
return conditioned.replace(/\{\{opencodeVariantConfig\}\}/g, () => resolveOpencodeVariantConfig(ctx)).replace(/\{\{opencodeSessionConfig\}\}/g, () => resolveOpencodeSessionConfig(ctx)).replace(/\{\{prompt\}\}/g, ctx.prompt).replace(/\{\{model\}\}/g, ctx.model || "").replace(/\{\{thinkingEffort\}\}/g, ctx.thinkingEffort || "").replace(/\{\{mode\}\}/g, ctx.mode || "build");
|
|
11066
11298
|
}
|
|
11067
11299
|
|
|
11068
11300
|
// src/utils/launch.ts
|
|
@@ -12765,9 +12997,502 @@ ${item.text}` : item.text });
|
|
|
12765
12997
|
|
|
12766
12998
|
// src/services/harness/OpenCodeAdapter.ts
|
|
12767
12999
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
13000
|
+
|
|
13001
|
+
// src/services/harness/OpenCodeV2.ts
|
|
13002
|
+
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
13003
|
+
var ACTIVE_POLL_INTERVAL_MS = 1e3;
|
|
13004
|
+
var IDLE_POLLS_BEFORE_START = 3;
|
|
13005
|
+
var PLANNER_RULES = [
|
|
13006
|
+
{ action: "question", resource: "*", effect: "deny" },
|
|
13007
|
+
{ action: "edit", resource: "*", effect: "deny" }
|
|
13008
|
+
];
|
|
13009
|
+
var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
|
|
13010
|
+
var AUTO_APPROVALS = "auto";
|
|
13011
|
+
function newTurnState() {
|
|
13012
|
+
return { seen: /* @__PURE__ */ new Set(), toolNames: /* @__PURE__ */ new Map(), stepModels: /* @__PURE__ */ new Map(), textRuns: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map(), briefs: /* @__PURE__ */ new Map(), heldFrames: /* @__PURE__ */ new Map(), streamedMessages: /* @__PURE__ */ new Set() };
|
|
13013
|
+
}
|
|
13014
|
+
function splitModelId(id) {
|
|
13015
|
+
const slash = id.indexOf("/");
|
|
13016
|
+
if (slash <= 0 || slash === id.length - 1) return null;
|
|
13017
|
+
return { providerID: id.slice(0, slash), id: id.slice(slash + 1) };
|
|
13018
|
+
}
|
|
13019
|
+
function delay(ms, signal) {
|
|
13020
|
+
return new Promise((resolve7) => {
|
|
13021
|
+
const timer = setTimeout(resolve7, ms);
|
|
13022
|
+
timer.unref?.();
|
|
13023
|
+
signal?.addEventListener("abort", () => {
|
|
13024
|
+
clearTimeout(timer);
|
|
13025
|
+
resolve7();
|
|
13026
|
+
}, { once: true });
|
|
13027
|
+
});
|
|
13028
|
+
}
|
|
13029
|
+
function contentText(content) {
|
|
13030
|
+
return (content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
|
|
13031
|
+
}
|
|
13032
|
+
function subagentDigest(output) {
|
|
13033
|
+
const inner = output.match(/<subagent[^>]*>\n?([\s\S]*?)\n?<\/subagent>/);
|
|
13034
|
+
return inner ? inner[1] : output;
|
|
13035
|
+
}
|
|
13036
|
+
function permissionReply(decision) {
|
|
13037
|
+
if (decision.decision === "allow") return { decision: "once" };
|
|
13038
|
+
if (decision.decision === "allowForTask") return { decision: "always" };
|
|
13039
|
+
const note = decision.note?.trim();
|
|
13040
|
+
return note ? { decision: "reject", message: note } : { decision: "reject" };
|
|
13041
|
+
}
|
|
13042
|
+
var OpenCodeV2 = class {
|
|
13043
|
+
constructor(host, opts, role) {
|
|
13044
|
+
this.host = host;
|
|
13045
|
+
this.opts = opts;
|
|
13046
|
+
this.role = role;
|
|
13047
|
+
this.agent = opts.kind === "task" ? opts.flags.permissionMode : "plan";
|
|
13048
|
+
const split = opts.model ? splitModelId(opts.model) : null;
|
|
13049
|
+
const variant = opts.kind === "task" ? opts.flags.effort : opts.effort;
|
|
13050
|
+
if (split) this.model = { ...split, ...variant ? { variant } : {} };
|
|
13051
|
+
}
|
|
13052
|
+
host;
|
|
13053
|
+
opts;
|
|
13054
|
+
role;
|
|
13055
|
+
sessionId = null;
|
|
13056
|
+
model = null;
|
|
13057
|
+
agent;
|
|
13058
|
+
turn = null;
|
|
13059
|
+
turnHasText = false;
|
|
13060
|
+
interruptRequested = false;
|
|
13061
|
+
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
13062
|
+
openPermissions = /* @__PURE__ */ new Map();
|
|
13063
|
+
nativeSessionId() {
|
|
13064
|
+
return this.sessionId;
|
|
13065
|
+
}
|
|
13066
|
+
async start() {
|
|
13067
|
+
const resume = this.opts.resumeSessionId;
|
|
13068
|
+
if (resume) {
|
|
13069
|
+
const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
|
|
13070
|
+
if (existing?.data?.id) {
|
|
13071
|
+
this.sessionId = existing.data.id;
|
|
13072
|
+
if (this.model) await this.host.request("POST", `/api/session/${this.sessionId}/model`, { model: this.model }).catch(() => void 0);
|
|
13073
|
+
await this.host.request("POST", `/api/session/${this.sessionId}/agent`, { agent: this.agent }).catch(() => void 0);
|
|
13074
|
+
await this.applyInstructions();
|
|
13075
|
+
return;
|
|
13076
|
+
}
|
|
13077
|
+
if (this.opts.kind === "task") throw new Error(`OpenCode could not resume session ${resume}: the server does not know it.`);
|
|
13078
|
+
}
|
|
13079
|
+
const created = await this.host.json("POST", "/api/session", {
|
|
13080
|
+
agent: this.agent,
|
|
13081
|
+
...this.model ? { model: this.model } : {},
|
|
13082
|
+
permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
|
|
13083
|
+
});
|
|
13084
|
+
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
13085
|
+
this.sessionId = created.data.id;
|
|
13086
|
+
await this.applyInstructions();
|
|
13087
|
+
}
|
|
13088
|
+
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
13089
|
+
async applyInstructions() {
|
|
13090
|
+
if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
|
|
13091
|
+
const response = await this.host.request("PUT", `/api/experimental/session/${this.sessionId}/instructions/entries/ordewell-planner`, { value: this.opts.systemPrompt });
|
|
13092
|
+
if (!response.ok) throw new Error(`OpenCode did not take the planner instructions: ${response.status} ${response.statusText}`);
|
|
13093
|
+
}
|
|
13094
|
+
async send(message, onEvent, signal, onActivity) {
|
|
13095
|
+
if (!this.sessionId) throw new Error(`OpenCode ${this.role} session is not started`);
|
|
13096
|
+
if (this.host.isExited()) {
|
|
13097
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
13100
|
+
const state = newTurnState();
|
|
13101
|
+
this.turnHasText = false;
|
|
13102
|
+
this.interruptRequested = false;
|
|
13103
|
+
let markDone = () => {
|
|
13104
|
+
};
|
|
13105
|
+
const ended = new Promise((resolve7) => {
|
|
13106
|
+
markDone = resolve7;
|
|
13107
|
+
});
|
|
13108
|
+
const turn = { live: false, done: false, outcome: null, failure: null, ended, finish: () => {
|
|
13109
|
+
turn.done = true;
|
|
13110
|
+
markDone();
|
|
13111
|
+
} };
|
|
13112
|
+
this.turn = turn;
|
|
13113
|
+
const closeStream = await this.openStream(state, turn, onEvent, onActivity);
|
|
13114
|
+
const poll = new AbortController();
|
|
13115
|
+
try {
|
|
13116
|
+
try {
|
|
13117
|
+
await this.host.json("POST", `/api/session/${this.sessionId}/prompt`, { text: message }, signal);
|
|
13118
|
+
} catch (err) {
|
|
13119
|
+
if (signal?.aborted) return;
|
|
13120
|
+
onEvent({ type: "error", message: `OpenCode did not take the message: ${err instanceof Error ? err.message : String(err)}` });
|
|
13121
|
+
return;
|
|
13122
|
+
}
|
|
13123
|
+
void this.pollActive(turn, poll.signal);
|
|
13124
|
+
const aborted = new Promise((resolve7) => {
|
|
13125
|
+
if (signal?.aborted) resolve7();
|
|
13126
|
+
signal?.addEventListener("abort", () => resolve7(), { once: true });
|
|
13127
|
+
});
|
|
13128
|
+
await Promise.race([turn.ended, aborted, this.host.processEnded]);
|
|
13129
|
+
if (signal?.aborted) return;
|
|
13130
|
+
if (!turn.done) {
|
|
13131
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
13132
|
+
return;
|
|
13133
|
+
}
|
|
13134
|
+
await this.readBack(state, onEvent);
|
|
13135
|
+
for (const id of [...this.openPermissions.keys()]) {
|
|
13136
|
+
this.openPermissions.delete(id);
|
|
13137
|
+
onEvent({ type: "permission_cancelled", id });
|
|
13138
|
+
}
|
|
13139
|
+
if (turn.outcome === "interrupted" || this.interruptRequested) onEvent({ type: "turn_end", interrupted: true });
|
|
13140
|
+
else if (turn.outcome === "failed") onEvent({ type: "error", message: turn.failure ?? "OpenCode reported that the turn failed." });
|
|
13141
|
+
else onEvent({ type: "turn_end" });
|
|
13142
|
+
} finally {
|
|
13143
|
+
this.turn = null;
|
|
13144
|
+
poll.abort();
|
|
13145
|
+
await closeStream();
|
|
13146
|
+
}
|
|
13147
|
+
}
|
|
13148
|
+
/**
|
|
13149
|
+
* The stream can drop the end frame while still delivering the rest, so the
|
|
13150
|
+
* turn also asks which sessions are running. A session that is not, after
|
|
13151
|
+
* this turn's work was seen, is over; one never seen working after a few
|
|
13152
|
+
* polls had already finished before the stream connected.
|
|
13153
|
+
*/
|
|
13154
|
+
async pollActive(turn, signal) {
|
|
13155
|
+
let idlePolls = 0;
|
|
13156
|
+
while (!turn.done && !signal.aborted) {
|
|
13157
|
+
await delay(ACTIVE_POLL_INTERVAL_MS, signal);
|
|
13158
|
+
if (turn.done || signal.aborted) return;
|
|
13159
|
+
const active = await this.host.json("GET", "/api/session/active").catch(() => null);
|
|
13160
|
+
if (!active?.data) continue;
|
|
13161
|
+
if (this.sessionId && this.sessionId in active.data) {
|
|
13162
|
+
turn.live = true;
|
|
13163
|
+
idlePolls = 0;
|
|
13164
|
+
continue;
|
|
13165
|
+
}
|
|
13166
|
+
idlePolls++;
|
|
13167
|
+
if (turn.live || idlePolls >= IDLE_POLLS_BEFORE_START) {
|
|
13168
|
+
const messages = await this.messages();
|
|
13169
|
+
const idle = [...messages ?? []].reverse().find((m) => m.type === "idle");
|
|
13170
|
+
if (idle?.outcome === "failed" || idle?.outcome === "interrupted" || idle?.outcome === "succeeded") turn.outcome ??= idle.outcome;
|
|
13171
|
+
turn.outcome ??= "succeeded";
|
|
13172
|
+
if (!turn.done) turn.finish();
|
|
13173
|
+
}
|
|
13174
|
+
}
|
|
13175
|
+
}
|
|
13176
|
+
async messages() {
|
|
13177
|
+
const response = await this.host.json("GET", `/api/session/${this.sessionId}/message`).catch(() => null);
|
|
13178
|
+
return Array.isArray(response?.data) ? response.data : null;
|
|
13179
|
+
}
|
|
13180
|
+
/**
|
|
13181
|
+
* Whatever of the turn's own replies the stream missed. A message whose text
|
|
13182
|
+
* the stream delivered adds nothing here; one that lost its text frames still
|
|
13183
|
+
* reaches the plain-text channel, where the done marker is looked for.
|
|
13184
|
+
*/
|
|
13185
|
+
async readBack(state, onEvent) {
|
|
13186
|
+
const messages = await this.messages();
|
|
13187
|
+
if (!messages) return;
|
|
13188
|
+
const latestUser = messages.findIndex((m) => m.type === "user");
|
|
13189
|
+
const own = latestUser < 0 ? messages : messages.slice(0, latestUser);
|
|
13190
|
+
for (const message of [...own].reverse()) {
|
|
13191
|
+
if (message.type !== "assistant" || !message.id || state.streamedMessages.has(message.id)) continue;
|
|
13192
|
+
(message.content ?? []).forEach((part, index) => {
|
|
13193
|
+
if (part.type === "text" && part.text) this.emitText(`${message.id}:readback:${index}`, part.text, state, onEvent);
|
|
13194
|
+
});
|
|
13195
|
+
}
|
|
13196
|
+
}
|
|
13197
|
+
/**
|
|
13198
|
+
* Abort the running turn, keeping the server and its session. OpenCode
|
|
13199
|
+
* acknowledges with `session.execution.interrupted`, which also ends the turn.
|
|
13200
|
+
*/
|
|
13201
|
+
async interrupt(timeoutMs) {
|
|
13202
|
+
if (!this.sessionId || this.host.isExited()) return false;
|
|
13203
|
+
const turn = this.turn;
|
|
13204
|
+
this.interruptRequested = true;
|
|
13205
|
+
const response = await this.host.json("POST", `/api/session/${this.sessionId}/interrupt`).catch(() => null);
|
|
13206
|
+
if (!response) return false;
|
|
13207
|
+
if (!turn || turn.done) return true;
|
|
13208
|
+
let timer;
|
|
13209
|
+
const timedOut = new Promise((resolve7) => {
|
|
13210
|
+
timer = setTimeout(() => resolve7(false), timeoutMs);
|
|
13211
|
+
timer.unref?.();
|
|
13212
|
+
});
|
|
13213
|
+
const acknowledged = await Promise.race([turn.ended.then(() => true), this.host.processEnded.then(() => false), timedOut]);
|
|
13214
|
+
clearTimeout(timer);
|
|
13215
|
+
return acknowledged;
|
|
13216
|
+
}
|
|
13217
|
+
answerPermission(id, decision) {
|
|
13218
|
+
const sessionId = this.openPermissions.get(id);
|
|
13219
|
+
if (sessionId === void 0) return false;
|
|
13220
|
+
this.openPermissions.delete(id);
|
|
13221
|
+
void this.replyPermission(id, sessionId, permissionReply(decision)).catch(() => {
|
|
13222
|
+
});
|
|
13223
|
+
return true;
|
|
13224
|
+
}
|
|
13225
|
+
async replyPermission(id, sessionId, reply) {
|
|
13226
|
+
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
|
|
13227
|
+
}
|
|
13228
|
+
async openStream(state, turn, onEvent, onActivity) {
|
|
13229
|
+
const streamAbort = new AbortController();
|
|
13230
|
+
let connected = () => {
|
|
13231
|
+
};
|
|
13232
|
+
const ready = new Promise((resolve7) => {
|
|
13233
|
+
connected = resolve7;
|
|
13234
|
+
});
|
|
13235
|
+
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, state, turn, onEvent), connected, onActivity);
|
|
13236
|
+
await Promise.race([ready, new Promise((r) => {
|
|
13237
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS);
|
|
13238
|
+
t.unref?.();
|
|
13239
|
+
})]);
|
|
13240
|
+
return async () => {
|
|
13241
|
+
streamAbort.abort();
|
|
13242
|
+
await live.catch(() => {
|
|
13243
|
+
});
|
|
13244
|
+
};
|
|
13245
|
+
}
|
|
13246
|
+
async streamEvents(signal, onFrame, onConnected, onActivity) {
|
|
13247
|
+
const response = await this.host.openEvents(signal).catch(() => null);
|
|
13248
|
+
const body = response?.body;
|
|
13249
|
+
if (!body) {
|
|
13250
|
+
onConnected();
|
|
13251
|
+
return;
|
|
13252
|
+
}
|
|
13253
|
+
onConnected();
|
|
13254
|
+
const reader = body.getReader();
|
|
13255
|
+
const decoder = new TextDecoder();
|
|
13256
|
+
let buffer = "";
|
|
13257
|
+
for (; ; ) {
|
|
13258
|
+
const { done, value } = await reader.read().catch(() => ({ done: true, value: void 0 }));
|
|
13259
|
+
if (done) return;
|
|
13260
|
+
onActivity?.();
|
|
13261
|
+
buffer += decoder.decode(value, { stream: true });
|
|
13262
|
+
let newline;
|
|
13263
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
13264
|
+
const line = buffer.slice(0, newline).trim();
|
|
13265
|
+
buffer = buffer.slice(newline + 1);
|
|
13266
|
+
if (!line.startsWith("data:")) continue;
|
|
13267
|
+
try {
|
|
13268
|
+
onFrame(JSON.parse(line.slice(5).trim()));
|
|
13269
|
+
} catch {
|
|
13270
|
+
}
|
|
13271
|
+
}
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13274
|
+
/**
|
|
13275
|
+
* One frame. Only this session and its children are followed: the stream is
|
|
13276
|
+
* the whole server's, and another client's session is none of this turn's business.
|
|
13277
|
+
*/
|
|
13278
|
+
onFrame(frame, state, turn, onEvent) {
|
|
13279
|
+
const data = frame.data;
|
|
13280
|
+
if (!data || !frame.type) return;
|
|
13281
|
+
if (frame.type === "session.created") {
|
|
13282
|
+
if (data.parentID && data.parentID === this.sessionId && data.sessionID && !state.children.has(data.sessionID)) state.children.set(data.sessionID, null);
|
|
13283
|
+
return;
|
|
13284
|
+
}
|
|
13285
|
+
const session = data.sessionID;
|
|
13286
|
+
if (!session) return;
|
|
13287
|
+
const isChild = session !== this.sessionId;
|
|
13288
|
+
if (isChild && !state.children.has(session)) return;
|
|
13289
|
+
if (!isChild) {
|
|
13290
|
+
if (frame.type === "session.execution.started") {
|
|
13291
|
+
turn.live = true;
|
|
13292
|
+
return;
|
|
13293
|
+
}
|
|
13294
|
+
if (frame.type === "session.execution.succeeded" || frame.type === "session.execution.failed" || frame.type === "session.execution.interrupted") {
|
|
13295
|
+
if (!turn.live || turn.done) return;
|
|
13296
|
+
turn.outcome = frame.type === "session.execution.succeeded" ? "succeeded" : frame.type === "session.execution.failed" ? "failed" : "interrupted";
|
|
13297
|
+
if (turn.outcome === "failed") turn.failure = data.error?.message ?? data.error?.type ?? null;
|
|
13298
|
+
turn.finish();
|
|
13299
|
+
return;
|
|
13300
|
+
}
|
|
13301
|
+
}
|
|
13302
|
+
if (frame.type === "permission.asked") {
|
|
13303
|
+
if (this.role === "task") this.askPermission(data, session, state, onEvent);
|
|
13304
|
+
else this.denyPermission(data, session, state, onEvent);
|
|
13305
|
+
return;
|
|
13306
|
+
}
|
|
13307
|
+
if (frame.type === "permission.replied") {
|
|
13308
|
+
const id = data.requestID;
|
|
13309
|
+
if (id && this.openPermissions.delete(id)) onEvent({ type: "permission_cancelled", id });
|
|
13310
|
+
return;
|
|
13311
|
+
}
|
|
13312
|
+
let subagentId;
|
|
13313
|
+
if (isChild) {
|
|
13314
|
+
const owner = state.children.get(session);
|
|
13315
|
+
if (!owner) {
|
|
13316
|
+
state.heldFrames.set(session, [...state.heldFrames.get(session) ?? [], frame]);
|
|
13317
|
+
return;
|
|
13318
|
+
}
|
|
13319
|
+
subagentId = owner;
|
|
13320
|
+
}
|
|
13321
|
+
this.onContentFrame(frame, data, state, turn, onEvent, subagentId);
|
|
13322
|
+
}
|
|
13323
|
+
onContentFrame(frame, data, state, turn, onEvent, subagentId) {
|
|
13324
|
+
const message = data.assistantMessageID ?? "";
|
|
13325
|
+
switch (frame.type) {
|
|
13326
|
+
case "session.step.started":
|
|
13327
|
+
if (message && data.model?.providerID && data.model.id) state.stepModels.set(message, `${data.model.providerID}/${data.model.id}`);
|
|
13328
|
+
return;
|
|
13329
|
+
case "session.reasoning.delta":
|
|
13330
|
+
if (data.delta) onEvent({ type: "thinking_delta", text: data.delta, subagentId });
|
|
13331
|
+
return;
|
|
13332
|
+
case "session.reasoning.ended": {
|
|
13333
|
+
const key = `reasoning:${message}:${data.ordinal ?? 0}`;
|
|
13334
|
+
if (!data.text || state.seen.has(key)) return;
|
|
13335
|
+
state.seen.add(key);
|
|
13336
|
+
onEvent({ type: "thinking", text: data.text, subagentId });
|
|
13337
|
+
return;
|
|
13338
|
+
}
|
|
13339
|
+
case "session.text.delta":
|
|
13340
|
+
if (!subagentId && data.delta) this.onTextDelta(`${message}:${data.ordinal ?? 0}`, data.delta, state, onEvent);
|
|
13341
|
+
return;
|
|
13342
|
+
case "session.text.ended":
|
|
13343
|
+
if (subagentId || !data.text) return;
|
|
13344
|
+
if (message) state.streamedMessages.add(message);
|
|
13345
|
+
this.emitText(`${message}:${data.ordinal ?? 0}`, data.text, state, onEvent);
|
|
13346
|
+
return;
|
|
13347
|
+
case "session.tool.input.started":
|
|
13348
|
+
if (data.id && data.name) state.toolNames.set(data.id, data.name);
|
|
13349
|
+
return;
|
|
13350
|
+
case "session.tool.called": {
|
|
13351
|
+
const id = data.id;
|
|
13352
|
+
if (!id || state.seen.has(`call:${id}`)) return;
|
|
13353
|
+
state.seen.add(`call:${id}`);
|
|
13354
|
+
const input = data.input ?? {};
|
|
13355
|
+
const name = state.toolNames.get(id) ?? "tool";
|
|
13356
|
+
const brief = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
13357
|
+
state.briefs.set(id, brief);
|
|
13358
|
+
onEvent({ type: "tool_call", id, name, args: input, subagentId });
|
|
13359
|
+
return;
|
|
13360
|
+
}
|
|
13361
|
+
case "session.tool.progress":
|
|
13362
|
+
if (!subagentId) this.adoptChild(data, state, onEvent, turn);
|
|
13363
|
+
return;
|
|
13364
|
+
case "session.tool.success":
|
|
13365
|
+
case "session.tool.failed": {
|
|
13366
|
+
const id = data.id;
|
|
13367
|
+
if (!id || state.seen.has(`result:${id}`)) return;
|
|
13368
|
+
state.seen.add(`result:${id}`);
|
|
13369
|
+
const success = frame.type === "session.tool.success";
|
|
13370
|
+
const output = success ? contentText(data.content) : data.error?.message ?? data.error?.type ?? "";
|
|
13371
|
+
onEvent({ type: "tool_result", id, name: state.toolNames.get(id) ?? "tool", output, success, subagentId });
|
|
13372
|
+
if (!subagentId) this.finishChild(id, success, output, state, onEvent);
|
|
13373
|
+
return;
|
|
13374
|
+
}
|
|
13375
|
+
case "session.step.ended":
|
|
13376
|
+
case "session.step.failed":
|
|
13377
|
+
this.countUsage(data, state, onEvent, subagentId);
|
|
13378
|
+
return;
|
|
13379
|
+
default:
|
|
13380
|
+
}
|
|
13381
|
+
}
|
|
13382
|
+
adoptChild(data, state, onEvent, turn) {
|
|
13383
|
+
const child = data.metadata?.sessionID;
|
|
13384
|
+
const callId = data.id;
|
|
13385
|
+
if (!child || !callId || state.children.get(child)) return;
|
|
13386
|
+
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
13387
|
+
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
13388
|
+
state.children.set(child, callId);
|
|
13389
|
+
const held = state.heldFrames.get(child) ?? [];
|
|
13390
|
+
state.heldFrames.delete(child);
|
|
13391
|
+
for (const frame of held) this.onFrame(frame, state, turn, onEvent);
|
|
13392
|
+
}
|
|
13393
|
+
finishChild(callId, success, output, state, onEvent) {
|
|
13394
|
+
if (![...state.children.values()].includes(callId)) return;
|
|
13395
|
+
onEvent({ type: "subagent_finished", subagentId: callId, outcome: success ? "done" : "failed", digest: subagentDigest(output) });
|
|
13396
|
+
}
|
|
13397
|
+
/** One model call's usage, once, when its step ends. Zeros mean the call failed before the provider answered. */
|
|
13398
|
+
countUsage(data, state, onEvent, subagentId) {
|
|
13399
|
+
const message = data.assistantMessageID;
|
|
13400
|
+
const tokens = data.tokens;
|
|
13401
|
+
if (!message || !tokens || state.seen.has(`usage:${message}`)) return;
|
|
13402
|
+
state.seen.add(`usage:${message}`);
|
|
13403
|
+
const prompt = partedPromptUsage({ uncached: tokens.input, cacheRead: tokens.cache?.read, cacheWrite: tokens.cache?.write });
|
|
13404
|
+
const outputTokens = (tokens.output ?? 0) + (tokens.reasoning ?? 0);
|
|
13405
|
+
if ((prompt.inputTokens ?? 0) + outputTokens === 0) return;
|
|
13406
|
+
const record = { source: "opencode", ...prompt, outputTokens };
|
|
13407
|
+
const model = state.stepModels.get(message);
|
|
13408
|
+
if (model) record.model = model;
|
|
13409
|
+
if (typeof data.cost === "number" && data.cost > 0) record.reportedCost = { amount: data.cost, currency: "USD" };
|
|
13410
|
+
if (subagentId) record.subagentId = subagentId;
|
|
13411
|
+
onEvent({ type: "usage", record });
|
|
13412
|
+
}
|
|
13413
|
+
/** One complete reply text, once. A message can carry text on both sides of a tool call, so each run after the first opens a paragraph. */
|
|
13414
|
+
emitText(id, text, state, onEvent) {
|
|
13415
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
13416
|
+
state.seen.add(`text:${id}`);
|
|
13417
|
+
if (!text.trim()) return;
|
|
13418
|
+
const lead = state.textRuns.get(id)?.lead ?? (this.turnHasText ? "\n\n" : "");
|
|
13419
|
+
onEvent({ type: "assistant_text", text: `${lead}${text}` });
|
|
13420
|
+
this.turnHasText = true;
|
|
13421
|
+
}
|
|
13422
|
+
/**
|
|
13423
|
+
* Stream one piece of a reply text. The paragraph break goes out with the
|
|
13424
|
+
* first visible delta, so the deltas add up to exactly the text the completed
|
|
13425
|
+
* run then re-sends; a run that is only whitespace so far is held back.
|
|
13426
|
+
*/
|
|
13427
|
+
onTextDelta(id, delta, state, onEvent) {
|
|
13428
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
13429
|
+
const run = state.textRuns.get(id) ?? { held: "", lead: null };
|
|
13430
|
+
state.textRuns.set(id, run);
|
|
13431
|
+
if (run.lead !== null) {
|
|
13432
|
+
onEvent({ type: "assistant_text_delta", text: delta });
|
|
13433
|
+
return;
|
|
13434
|
+
}
|
|
13435
|
+
run.held += delta;
|
|
13436
|
+
if (!run.held.trim()) return;
|
|
13437
|
+
run.lead = this.turnHasText ? "\n\n" : "";
|
|
13438
|
+
this.turnHasText = true;
|
|
13439
|
+
onEvent({ type: "assistant_text_delta", text: `${run.lead}${run.held}` });
|
|
13440
|
+
}
|
|
13441
|
+
permissionRequest(ask) {
|
|
13442
|
+
const id = ask.id;
|
|
13443
|
+
if (!id) return null;
|
|
13444
|
+
const scope = (ask.resources ?? []).join(", ");
|
|
13445
|
+
return {
|
|
13446
|
+
type: "permission_request",
|
|
13447
|
+
id,
|
|
13448
|
+
name: ask.action ?? "permission",
|
|
13449
|
+
detail: JSON.stringify(scope ? { scope } : {}),
|
|
13450
|
+
input: ask.resources ? { resources: ask.resources } : {},
|
|
13451
|
+
// The patterns `always` would grant: the runner's own offer, and the only grounds for "Allow for this task".
|
|
13452
|
+
...ask.save?.length ? { suggestions: ask.save } : {},
|
|
13453
|
+
...ask.source?.id ? { toolUseId: ask.source.id } : {}
|
|
13454
|
+
};
|
|
13455
|
+
}
|
|
13456
|
+
/**
|
|
13457
|
+
* Deny one planner request. OpenCode blocks the turn until it is answered, so
|
|
13458
|
+
* this must answer: `reject` rather than a silent drop is the "absent answer
|
|
13459
|
+
* is a denial" invariant ADR-0008 states. The refusal is announced so the
|
|
13460
|
+
* timeline shows the planner reaching for something it may not have.
|
|
13461
|
+
*/
|
|
13462
|
+
denyPermission(ask, session, state, onEvent) {
|
|
13463
|
+
const request = this.permissionRequest(ask);
|
|
13464
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
13465
|
+
state.seen.add(`perm:${request.id}`);
|
|
13466
|
+
onEvent(request);
|
|
13467
|
+
void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
|
|
13468
|
+
});
|
|
13469
|
+
}
|
|
13470
|
+
/**
|
|
13471
|
+
* A task's request. Under a mode whose manifest sets `approvals: auto` it is
|
|
13472
|
+
* answered at once with what `opencode run --auto` answers, so the same plan
|
|
13473
|
+
* behaves the same on both transports (ADR-0001), and announced already
|
|
13474
|
+
* decided so the log still shows it. Any other mode leaves it open for an
|
|
13475
|
+
* approval card.
|
|
13476
|
+
*/
|
|
13477
|
+
askPermission(ask, session, state, onEvent) {
|
|
13478
|
+
const request = this.permissionRequest(ask);
|
|
13479
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
13480
|
+
state.seen.add(`perm:${request.id}`);
|
|
13481
|
+
if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
13482
|
+
onEvent({ ...request, decided: { decision: "allow" } });
|
|
13483
|
+
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
13484
|
+
});
|
|
13485
|
+
return;
|
|
13486
|
+
}
|
|
13487
|
+
this.openPermissions.set(request.id, session);
|
|
13488
|
+
onEvent(request);
|
|
13489
|
+
}
|
|
13490
|
+
};
|
|
13491
|
+
|
|
13492
|
+
// src/services/harness/OpenCodeAdapter.ts
|
|
12768
13493
|
var SERVER_READY_TIMEOUT_MS = 3e4;
|
|
12769
13494
|
var STDERR_TAIL_CHARS2 = 4e3;
|
|
12770
|
-
var
|
|
13495
|
+
var STREAM_CONNECT_TIMEOUT_MS2 = 5e3;
|
|
12771
13496
|
var RECOVERY_POLL_INTERVAL_MS = 2e3;
|
|
12772
13497
|
var RECOVERY_TIMEOUT_MS = 9e5;
|
|
12773
13498
|
var STATUS_POLL_INTERVAL_MS = 1e3;
|
|
@@ -12780,8 +13505,8 @@ var DISABLED_TOOLS = {
|
|
|
12780
13505
|
todowrite: false
|
|
12781
13506
|
};
|
|
12782
13507
|
var TASK_DISABLED_TOOLS = { question: false };
|
|
12783
|
-
var
|
|
12784
|
-
function
|
|
13508
|
+
var AUTO_APPROVALS2 = "auto";
|
|
13509
|
+
function splitModelId2(id) {
|
|
12785
13510
|
const slash = id.indexOf("/");
|
|
12786
13511
|
if (slash <= 0 || slash === id.length - 1) return null;
|
|
12787
13512
|
return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
|
|
@@ -12837,7 +13562,7 @@ function unwrapFileToolOutput(output) {
|
|
|
12837
13562
|
const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
|
|
12838
13563
|
return inner ? inner[1] : output;
|
|
12839
13564
|
}
|
|
12840
|
-
function
|
|
13565
|
+
function delay2(ms, signal) {
|
|
12841
13566
|
return new Promise((resolve7) => {
|
|
12842
13567
|
const timer = setTimeout(resolve7, ms);
|
|
12843
13568
|
timer.unref?.();
|
|
@@ -12847,10 +13572,10 @@ function delay(ms, signal) {
|
|
|
12847
13572
|
}, { once: true });
|
|
12848
13573
|
});
|
|
12849
13574
|
}
|
|
12850
|
-
function
|
|
13575
|
+
function newTurnState2() {
|
|
12851
13576
|
return { seen: /* @__PURE__ */ new Set(), assistantMessages: /* @__PURE__ */ new Set(), partTypes: /* @__PURE__ */ new Map(), textRuns: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map(), heldFrames: /* @__PURE__ */ new Map() };
|
|
12852
13577
|
}
|
|
12853
|
-
function
|
|
13578
|
+
function permissionReply2(decision) {
|
|
12854
13579
|
if (decision.decision === "allow") return { reply: "once" };
|
|
12855
13580
|
if (decision.decision === "allowForTask") return { reply: "always" };
|
|
12856
13581
|
const note = decision.note?.trim();
|
|
@@ -12887,6 +13612,8 @@ var OpenCodeAdapter = class {
|
|
|
12887
13612
|
interruptRequested = false;
|
|
12888
13613
|
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
12889
13614
|
openPermissions = /* @__PURE__ */ new Map();
|
|
13615
|
+
/** Set once the server turns out to speak the 2.x API, which then owns the session. */
|
|
13616
|
+
v2 = null;
|
|
12890
13617
|
async start(opts) {
|
|
12891
13618
|
if (opts.kind === "task") this.task = opts;
|
|
12892
13619
|
else this.planner = opts;
|
|
@@ -12901,14 +13628,14 @@ var OpenCodeAdapter = class {
|
|
|
12901
13628
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
12902
13629
|
}
|
|
12903
13630
|
const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
12904
|
-
const password =
|
|
13631
|
+
const password = randomBytes2(24).toString("base64url");
|
|
12905
13632
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
12906
|
-
env: runnerEnv(PATH,
|
|
13633
|
+
env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
|
|
12907
13634
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12908
13635
|
cwd: opts.cwd,
|
|
12909
13636
|
windowsVerbatimArguments: launch.verbatim
|
|
12910
13637
|
});
|
|
12911
|
-
|
|
13638
|
+
this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
|
|
12912
13639
|
this.process.stdin?.on("error", () => {
|
|
12913
13640
|
});
|
|
12914
13641
|
const banner = new Promise((resolve7) => {
|
|
@@ -12945,6 +13672,24 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
|
12945
13672
|
|
|
12946
13673
|
${this.stderrTail.trim()}` : ""}`);
|
|
12947
13674
|
}
|
|
13675
|
+
if (await this.speaksV2()) {
|
|
13676
|
+
this.v2 = new OpenCodeV2({
|
|
13677
|
+
request: (method, path26, body, signal) => this.request(method, path26, body, signal),
|
|
13678
|
+
json: (method, path26, body, signal) => this.json(method, path26, body, signal),
|
|
13679
|
+
openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
|
|
13680
|
+
processEnded: this.processEnded,
|
|
13681
|
+
isExited: () => this.exited,
|
|
13682
|
+
exitMessage: () => this.exitMessage()
|
|
13683
|
+
}, opts, this.role());
|
|
13684
|
+
try {
|
|
13685
|
+
await this.v2.start();
|
|
13686
|
+
} catch (err) {
|
|
13687
|
+
this.dispose();
|
|
13688
|
+
throw err;
|
|
13689
|
+
}
|
|
13690
|
+
this.sessionId = this.v2.nativeSessionId();
|
|
13691
|
+
return;
|
|
13692
|
+
}
|
|
12948
13693
|
if (opts.resumeSessionId) {
|
|
12949
13694
|
const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
|
|
12950
13695
|
if (existing?.id) {
|
|
@@ -12968,12 +13713,17 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
12968
13713
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
12969
13714
|
return;
|
|
12970
13715
|
}
|
|
13716
|
+
if (this.v2) {
|
|
13717
|
+
await this.v2.send(message, onEvent, signal, onActivity);
|
|
13718
|
+
if (signal?.aborted) this.dispose();
|
|
13719
|
+
return;
|
|
13720
|
+
}
|
|
12971
13721
|
if (this.task) return this.sendTask(this.task, message, onEvent, signal, onActivity);
|
|
12972
|
-
const turn =
|
|
13722
|
+
const turn = newTurnState2();
|
|
12973
13723
|
this.turnHasText = false;
|
|
12974
13724
|
const closeStream = await this.openStream(turn, onEvent, onActivity);
|
|
12975
13725
|
try {
|
|
12976
|
-
const model = this.planner?.model ?
|
|
13726
|
+
const model = this.planner?.model ? splitModelId2(this.planner.model) : null;
|
|
12977
13727
|
const body = {
|
|
12978
13728
|
parts: [{ type: "text", text: message }],
|
|
12979
13729
|
// The read-only guarantee: OpenCode's own plan agent has no write tools.
|
|
@@ -13014,7 +13764,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13014
13764
|
* {@link recoverReply}, never applies.
|
|
13015
13765
|
*/
|
|
13016
13766
|
async sendTask(task, message, onEvent, signal, onActivity) {
|
|
13017
|
-
const state =
|
|
13767
|
+
const state = newTurnState2();
|
|
13018
13768
|
this.turnHasText = false;
|
|
13019
13769
|
this.interruptRequested = false;
|
|
13020
13770
|
let markDone = () => {
|
|
@@ -13030,7 +13780,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13030
13780
|
const closeStream = await this.openStream(state, onEvent, onActivity);
|
|
13031
13781
|
const poll = new AbortController();
|
|
13032
13782
|
try {
|
|
13033
|
-
const model = task.model ?
|
|
13783
|
+
const model = task.model ? splitModelId2(task.model) : null;
|
|
13034
13784
|
const body = {
|
|
13035
13785
|
parts: [{ type: "text", text: message }],
|
|
13036
13786
|
// The manifest's meaning of the task's mode (ADR-0001): for OpenCode a mode is an agent.
|
|
@@ -13110,7 +13860,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13110
13860
|
*/
|
|
13111
13861
|
async pollStatus(turn, signal) {
|
|
13112
13862
|
while (!turn.done && !signal.aborted) {
|
|
13113
|
-
await
|
|
13863
|
+
await delay2(STATUS_POLL_INTERVAL_MS, signal);
|
|
13114
13864
|
if (turn.done || signal.aborted) return;
|
|
13115
13865
|
const busy = await this.sessionBusy();
|
|
13116
13866
|
if (busy === true) turn.live = true;
|
|
@@ -13157,6 +13907,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13157
13907
|
*/
|
|
13158
13908
|
async interrupt(timeoutMs) {
|
|
13159
13909
|
if (!this.process || !this.sessionId || this.exited) return false;
|
|
13910
|
+
if (this.v2) return this.v2.interrupt(timeoutMs);
|
|
13160
13911
|
const turn = this.taskTurn;
|
|
13161
13912
|
this.interruptRequested = true;
|
|
13162
13913
|
const posted = await this.json("POST", `/session/${this.sessionId}/abort`).then(() => true, () => false);
|
|
@@ -13177,13 +13928,23 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13177
13928
|
void this.processEnded.then(() => listener(this.exitCode));
|
|
13178
13929
|
}
|
|
13179
13930
|
answerPermission(id, decision) {
|
|
13931
|
+
if (this.v2) return this.process ? this.v2.answerPermission(id, decision) : false;
|
|
13180
13932
|
const sessionId = this.openPermissions.get(id);
|
|
13181
13933
|
if (sessionId === void 0 || !this.process) return false;
|
|
13182
13934
|
this.openPermissions.delete(id);
|
|
13183
|
-
void this.replyPermission(id, sessionId,
|
|
13935
|
+
void this.replyPermission(id, sessionId, permissionReply2(decision)).catch(() => {
|
|
13184
13936
|
});
|
|
13185
13937
|
return true;
|
|
13186
13938
|
}
|
|
13939
|
+
/**
|
|
13940
|
+
* OpenCode 2.x replaced the 1.x HTTP surface rather than extending it, and
|
|
13941
|
+
* only 2.x answers `/api/info` with its version. A 1.x server — or one that
|
|
13942
|
+
* answers with anything else — keeps the 1.x protocol this class speaks.
|
|
13943
|
+
*/
|
|
13944
|
+
async speaksV2() {
|
|
13945
|
+
const info = await this.json("GET", "/api/info").catch(() => null);
|
|
13946
|
+
return typeof info === "object" && info !== null && typeof info.version === "string";
|
|
13947
|
+
}
|
|
13187
13948
|
role() {
|
|
13188
13949
|
return this.task ? "task" : "planner";
|
|
13189
13950
|
}
|
|
@@ -13234,7 +13995,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13234
13995
|
if (pending?.info?.time?.completed || pending?.info?.error) return pending;
|
|
13235
13996
|
if (pending) onActivity?.();
|
|
13236
13997
|
if (Date.now() >= deadline) return null;
|
|
13237
|
-
await
|
|
13998
|
+
await delay2(RECOVERY_POLL_INTERVAL_MS, signal);
|
|
13238
13999
|
}
|
|
13239
14000
|
}
|
|
13240
14001
|
newestAssistantId(messages) {
|
|
@@ -13447,7 +14208,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13447
14208
|
...ask.always?.length ? { suggestions: ask.always } : {},
|
|
13448
14209
|
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
|
|
13449
14210
|
};
|
|
13450
|
-
if (this.task?.flags.modeSettings.approvals ===
|
|
14211
|
+
if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
13451
14212
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
13452
14213
|
void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
|
|
13453
14214
|
});
|
|
@@ -13481,7 +14242,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13481
14242
|
});
|
|
13482
14243
|
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, turn, onEvent), connected, onActivity);
|
|
13483
14244
|
await Promise.race([streamReady, new Promise((r) => {
|
|
13484
|
-
const t = setTimeout(r,
|
|
14245
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS2);
|
|
13485
14246
|
t.unref?.();
|
|
13486
14247
|
})]);
|
|
13487
14248
|
return async () => {
|
|
@@ -13670,6 +14431,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13670
14431
|
observers = [];
|
|
13671
14432
|
tddEnabled;
|
|
13672
14433
|
readRunnerTransport;
|
|
14434
|
+
previousAttemptFromLog;
|
|
13673
14435
|
/**
|
|
13674
14436
|
* The setting as the latest run copied it when it opened. Every spawn of
|
|
13675
14437
|
* that run uses this, so flipping the setting mid-run changes the next run
|
|
@@ -13689,6 +14451,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13689
14451
|
this.workspaceEnv = deps.workspaceEnv;
|
|
13690
14452
|
this.tddEnabled = deps.tddEnabled;
|
|
13691
14453
|
this.readRunnerTransport = deps.runnerTransport;
|
|
14454
|
+
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
13692
14455
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
13693
14456
|
this.verifier.onVerdict((taskId, verdict) => this.onVerdict(taskId, verdict));
|
|
13694
14457
|
this.verifier.onCheckpoint((taskId, summary) => {
|
|
@@ -13744,7 +14507,8 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13744
14507
|
workspaceRoot,
|
|
13745
14508
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
13746
14509
|
tddEnabled: options.tddEnabled ?? (() => false),
|
|
13747
|
-
runnerTransport: options.runnerTransport ?? (() => "terminal")
|
|
14510
|
+
runnerTransport: options.runnerTransport ?? (() => "terminal"),
|
|
14511
|
+
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null)
|
|
13748
14512
|
});
|
|
13749
14513
|
return orchestrator;
|
|
13750
14514
|
}
|
|
@@ -13752,6 +14516,18 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13752
14516
|
getIdleSince(taskId) {
|
|
13753
14517
|
return this.verifier.getIdleSince(taskId);
|
|
13754
14518
|
}
|
|
14519
|
+
/**
|
|
14520
|
+
* What an ops retry is told about the attempt before. The saved log is read
|
|
14521
|
+
* whether or not the session was reloaded, so both behave alike; the terminal
|
|
14522
|
+
* buffer is the fallback for a runner that has no log. A reload clears the
|
|
14523
|
+
* attempt count along with the buffer, so a retry made since is what says
|
|
14524
|
+
* there was an attempt to report on.
|
|
14525
|
+
*/
|
|
14526
|
+
opsPreviousAttempt(taskId) {
|
|
14527
|
+
const saved = this.previousAttemptFromLog(taskId) ?? this.output.liveTail(taskId, { maxLines: OPS_RETRY_TAIL_LINES })?.text;
|
|
14528
|
+
if (saved !== void 0) return saved;
|
|
14529
|
+
return (this.retryCounts.get(taskId) ?? 0) > 0 ? "Its output is not available: this session was reloaded since it ran." : void 0;
|
|
14530
|
+
}
|
|
13755
14531
|
/** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
|
|
13756
14532
|
getLiveOutput(taskId, opts) {
|
|
13757
14533
|
return this.output.liveTail(taskId, opts);
|
|
@@ -14609,7 +15385,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
14609
15385
|
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
14610
15386
|
// An ops task's effects outlive a failed attempt and are never rolled
|
|
14611
15387
|
// back, so the next one is told what the last one did (ADR-0020).
|
|
14612
|
-
previousAttempt: attempt.ops ? this.
|
|
15388
|
+
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
|
|
14613
15389
|
});
|
|
14614
15390
|
this.lingering.close(task.id);
|
|
14615
15391
|
const env = await this.envForTask(cwd);
|
|
@@ -18228,6 +19004,13 @@ var PlanEditError = class extends Error {
|
|
|
18228
19004
|
this.name = "PlanEditError";
|
|
18229
19005
|
}
|
|
18230
19006
|
};
|
|
19007
|
+
var OPS_RETRY_DIGEST_CALLS = 20;
|
|
19008
|
+
function lastAttemptDigest(location, taskId) {
|
|
19009
|
+
const attempt = listTaskLogAttempts(location, taskId).pop();
|
|
19010
|
+
if (attempt === void 0) return null;
|
|
19011
|
+
const events = readTaskLog(location, taskId, attempt);
|
|
19012
|
+
return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
|
|
19013
|
+
}
|
|
18231
19014
|
function sessionRuntimeSettings(settings) {
|
|
18232
19015
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
|
|
18233
19016
|
}
|
|
@@ -18312,7 +19095,8 @@ function createSession(deps) {
|
|
|
18312
19095
|
registry: deps.registry,
|
|
18313
19096
|
workspaceRoot: deps.workspaceRoot,
|
|
18314
19097
|
tddEnabled: () => deps.settings().tddEnabled,
|
|
18315
|
-
runnerTransport: () => deps.settings().runnerTransport ?? "terminal"
|
|
19098
|
+
runnerTransport: () => deps.settings().runnerTransport ?? "terminal",
|
|
19099
|
+
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId)
|
|
18316
19100
|
});
|
|
18317
19101
|
const usage = new PlannerUsageLedger();
|
|
18318
19102
|
const events = new SessionEventRelay({
|
|
@@ -19435,13 +20219,33 @@ function buildRunnerInvocation(opts) {
|
|
|
19435
20219
|
mode: opts.mode ?? "build",
|
|
19436
20220
|
headless: opts.headless,
|
|
19437
20221
|
interactive: opts.interactive,
|
|
19438
|
-
cwd: opts.cwd
|
|
20222
|
+
cwd: opts.cwd,
|
|
20223
|
+
runnerVersion: opts.runnerVersion
|
|
19439
20224
|
});
|
|
19440
20225
|
}
|
|
19441
20226
|
|
|
19442
20227
|
// src/services/HeadlessRunner.ts
|
|
19443
20228
|
import { spawn as nodeSpawn2, execSync } from "child_process";
|
|
19444
20229
|
|
|
20230
|
+
// src/services/runnerVersion.ts
|
|
20231
|
+
import { execFile as execFile4 } from "child_process";
|
|
20232
|
+
var VERSION_TIMEOUT_MS = 5e3;
|
|
20233
|
+
var cache = /* @__PURE__ */ new Map();
|
|
20234
|
+
var probeRunnerVersion = (command, PATH) => {
|
|
20235
|
+
const key = `${command}\0${PATH}`;
|
|
20236
|
+
let pending = cache.get(key);
|
|
20237
|
+
if (!pending) {
|
|
20238
|
+
pending = new Promise((resolve7) => {
|
|
20239
|
+
execFile4(command, ["--version"], { env: { ...process.env, PATH }, timeout: VERSION_TIMEOUT_MS }, (err, stdout) => {
|
|
20240
|
+
const line = String(stdout ?? "").trim();
|
|
20241
|
+
resolve7(err || !line ? void 0 : line);
|
|
20242
|
+
});
|
|
20243
|
+
});
|
|
20244
|
+
cache.set(key, pending);
|
|
20245
|
+
}
|
|
20246
|
+
return pending;
|
|
20247
|
+
};
|
|
20248
|
+
|
|
19445
20249
|
// src/services/AbstractRunner.ts
|
|
19446
20250
|
import { EventEmitter } from "events";
|
|
19447
20251
|
var AbstractTerminalSession = class {
|
|
@@ -19562,6 +20366,10 @@ Process error: ${err.message}
|
|
|
19562
20366
|
this.process.stdin.write(text);
|
|
19563
20367
|
}
|
|
19564
20368
|
}
|
|
20369
|
+
/** Closes the runner's stdin: what it has read so far is all it will get. */
|
|
20370
|
+
endInput() {
|
|
20371
|
+
this.process?.stdin?.end();
|
|
20372
|
+
}
|
|
19565
20373
|
/** PTY resize requests from a surface that owns the terminal rendering the wrapper. */
|
|
19566
20374
|
writeControl(text) {
|
|
19567
20375
|
if (this.controlStream && !this.controlStream.destroyed) {
|
|
@@ -19574,6 +20382,7 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
19574
20382
|
hasScriptCmd;
|
|
19575
20383
|
resolvePath;
|
|
19576
20384
|
launchDeps;
|
|
20385
|
+
runnerVersion;
|
|
19577
20386
|
spawnCount = 0;
|
|
19578
20387
|
/**
|
|
19579
20388
|
* Session shape: a piped subprocess is not a terminal, so runners get their
|
|
@@ -19589,6 +20398,7 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
19589
20398
|
this.hasScriptCmd = deps.hasScriptCmd ?? defaultHasScriptCmd;
|
|
19590
20399
|
this.resolvePath = deps.resolvePath ?? augmentedPath;
|
|
19591
20400
|
this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
|
|
20401
|
+
this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
|
|
19592
20402
|
}
|
|
19593
20403
|
/**
|
|
19594
20404
|
* Unique per spawn, not per task: a retry reuses its task id and ids often
|
|
@@ -19606,6 +20416,9 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
19606
20416
|
/** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
|
|
19607
20417
|
async prepareLaunch(opts, ptyOptions) {
|
|
19608
20418
|
const interactive = this.defaultInteractive;
|
|
20419
|
+
const resolvedPath = await this.resolvePath();
|
|
20420
|
+
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
20421
|
+
const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, resolvedPath) : void 0;
|
|
19609
20422
|
const invocation = buildRunnerInvocation({
|
|
19610
20423
|
runner: opts.runner,
|
|
19611
20424
|
prompt: opts.prompt,
|
|
@@ -19616,18 +20429,18 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
19616
20429
|
headless: opts.headless ?? true,
|
|
19617
20430
|
interactive,
|
|
19618
20431
|
cwd: opts.cwd,
|
|
20432
|
+
runnerVersion,
|
|
19619
20433
|
registry: opts.registry
|
|
19620
20434
|
});
|
|
19621
|
-
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
19622
20435
|
const pty = (manifest?.runner.requiresTty === true || interactive) && this.hasScriptCmd();
|
|
19623
|
-
const
|
|
19624
|
-
const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, ptyOptions) : { command: invocation.command, args: invocation.args };
|
|
20436
|
+
const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, { platform: this.launchDeps.platform, ...ptyOptions }) : { command: invocation.command, args: invocation.args };
|
|
19625
20437
|
return {
|
|
19626
20438
|
launch: await planDirectLaunch(command, args, this.launchDeps),
|
|
19627
20439
|
resolvedPath,
|
|
19628
20440
|
env: { ...opts.env, ...invocation.env },
|
|
19629
20441
|
pty,
|
|
19630
|
-
submitPromptKey: invocation.submitPromptKey
|
|
20442
|
+
submitPromptKey: invocation.submitPromptKey,
|
|
20443
|
+
closeStdin: !pty && !interactive && invocation.promptInArgs
|
|
19631
20444
|
};
|
|
19632
20445
|
}
|
|
19633
20446
|
async spawn(opts) {
|
|
@@ -19638,13 +20451,14 @@ var HeadlessRunner = class extends AbstractRunner {
|
|
|
19638
20451
|
console.error(`[headless] Starting ${opts.runner} [${modeStr}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}${prepared.pty ? " (PTY)" : ""}`);
|
|
19639
20452
|
session.start(prepared.launch, opts.cwd, prepared.resolvedPath, prepared.env);
|
|
19640
20453
|
if (prepared.submitPromptKey) session.write("\r");
|
|
20454
|
+
if (prepared.closeStdin) session.endInput();
|
|
19641
20455
|
this.registerSession(id, session);
|
|
19642
20456
|
return session;
|
|
19643
20457
|
}
|
|
19644
20458
|
};
|
|
19645
20459
|
|
|
19646
20460
|
// src/services/TmuxRunner.ts
|
|
19647
|
-
import { execFile as
|
|
20461
|
+
import { execFile as execFile5 } from "child_process";
|
|
19648
20462
|
import { promisify as promisify5 } from "util";
|
|
19649
20463
|
import { tmpdir } from "os";
|
|
19650
20464
|
import { join as join19 } from "path";
|
|
@@ -19701,7 +20515,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
|
|
|
19701
20515
|
var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
|
|
19702
20516
|
var LIVENESS_INTERVAL_MS = 5e3;
|
|
19703
20517
|
var LIVENESS_MISSES = 2;
|
|
19704
|
-
var execFileAsync2 = promisify5(
|
|
20518
|
+
var execFileAsync2 = promisify5(execFile5);
|
|
19705
20519
|
var defaultExecFile3 = async (command, args) => {
|
|
19706
20520
|
const { stdout, stderr } = await execFileAsync2(command, args);
|
|
19707
20521
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
@@ -19851,6 +20665,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
19851
20665
|
pollIntervalMs;
|
|
19852
20666
|
logDir;
|
|
19853
20667
|
clipboardCommand;
|
|
20668
|
+
runnerVersion;
|
|
19854
20669
|
/** Retries get a freshly named window so a failed attempt's output stays inspectable. */
|
|
19855
20670
|
attempts = /* @__PURE__ */ new Map();
|
|
19856
20671
|
ready = null;
|
|
@@ -19863,6 +20678,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
19863
20678
|
this.pollIntervalMs = deps.pollIntervalMs ?? 500;
|
|
19864
20679
|
this.logDir = deps.logDir ?? tmpdir();
|
|
19865
20680
|
this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
|
|
20681
|
+
this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
|
|
19866
20682
|
}
|
|
19867
20683
|
/**
|
|
19868
20684
|
* Reaps a session orphaned by a crashed prior daemon on the same port, then
|
|
@@ -19946,6 +20762,8 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
19946
20762
|
}
|
|
19947
20763
|
}
|
|
19948
20764
|
async spawn(opts) {
|
|
20765
|
+
const manifest = opts.registry?.get(opts.runner)?.manifest;
|
|
20766
|
+
const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, await this.resolvePath()) : void 0;
|
|
19949
20767
|
const invocation = buildRunnerInvocation({
|
|
19950
20768
|
runner: opts.runner,
|
|
19951
20769
|
prompt: opts.prompt,
|
|
@@ -19961,6 +20779,7 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
19961
20779
|
headless: opts.headless ?? true,
|
|
19962
20780
|
interactive: true,
|
|
19963
20781
|
cwd: opts.cwd,
|
|
20782
|
+
runnerVersion,
|
|
19964
20783
|
registry: opts.registry
|
|
19965
20784
|
});
|
|
19966
20785
|
await this.ensureSession();
|
|
@@ -20877,6 +21696,37 @@ var OPENCODE_MANIFEST = {
|
|
|
20877
21696
|
env: {
|
|
20878
21697
|
OPENCODE_CONFIG_CONTENT: "{{if interactiveVariant}}{{opencodeVariantConfig}}{{/if}}"
|
|
20879
21698
|
},
|
|
21699
|
+
// OpenCode 2.x changed both shapes. Its TUI takes no `--model`/`--agent`,
|
|
21700
|
+
// so they ride on OPENCODE_CONFIG_CONTENT instead, and its `run` has no
|
|
21701
|
+
// `--variant` — the variant is a `#suffix` on the model id. Both run
|
|
21702
|
+
// `--standalone`: without it the TUI and `run` attach to the user's
|
|
21703
|
+
// background service, which neither reads this process's config nor
|
|
21704
|
+
// starts in the task's directory and environment.
|
|
21705
|
+
versioned: [{
|
|
21706
|
+
minMajor: 2,
|
|
21707
|
+
argsTemplate: [
|
|
21708
|
+
"{{if headlessSession}}",
|
|
21709
|
+
"run",
|
|
21710
|
+
"--agent",
|
|
21711
|
+
"{{mode}}",
|
|
21712
|
+
"{{/if}}",
|
|
21713
|
+
"{{if headlessModel}}",
|
|
21714
|
+
"--model",
|
|
21715
|
+
"{{modelWithVariant}}",
|
|
21716
|
+
"{{/if}}",
|
|
21717
|
+
"--standalone",
|
|
21718
|
+
"{{if buildMode}}",
|
|
21719
|
+
"--auto",
|
|
21720
|
+
"{{/if}}",
|
|
21721
|
+
"{{if interactive}}",
|
|
21722
|
+
"--prompt",
|
|
21723
|
+
"{{/if}}",
|
|
21724
|
+
"{{prompt}}"
|
|
21725
|
+
],
|
|
21726
|
+
env: {
|
|
21727
|
+
OPENCODE_CONFIG_CONTENT: "{{if interactive}}{{opencodeSessionConfig}}{{/if}}"
|
|
21728
|
+
}
|
|
21729
|
+
}],
|
|
20880
21730
|
promptInArgs: true,
|
|
20881
21731
|
requiresTty: true,
|
|
20882
21732
|
// `--prompt` only pre-fills the TUI's composer; it does not submit it.
|
|
@@ -21475,6 +22325,7 @@ export {
|
|
|
21475
22325
|
FsPluginStore,
|
|
21476
22326
|
GIT_READONLY_SUBCOMMANDS,
|
|
21477
22327
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
22328
|
+
GREP_PCRE_PROBE_ARGS,
|
|
21478
22329
|
GeminiService,
|
|
21479
22330
|
HeadlessRunner,
|
|
21480
22331
|
HeadlessSession,
|
|
@@ -21614,6 +22465,7 @@ export {
|
|
|
21614
22465
|
dependencyCandidates,
|
|
21615
22466
|
dependentsOf,
|
|
21616
22467
|
describeMergeResult,
|
|
22468
|
+
digestTaskLog,
|
|
21617
22469
|
discoverGeminiModels,
|
|
21618
22470
|
drainNext,
|
|
21619
22471
|
effectiveAllowlist,
|
|
@@ -21646,6 +22498,7 @@ export {
|
|
|
21646
22498
|
getStateDir,
|
|
21647
22499
|
globalDataDir,
|
|
21648
22500
|
grantScopeFor,
|
|
22501
|
+
grepRegexFromProbe,
|
|
21649
22502
|
hasHiddenDetail,
|
|
21650
22503
|
hasTmux,
|
|
21651
22504
|
holdPrompt,
|