@ordewell/core 0.6.1 → 0.6.2
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/index.d.mts +55 -18
- package/dist/index.d.ts +55 -18
- package/dist/index.js +763 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +752 -46
- package/dist/index.mjs.map +1 -1
- package/dist/{plan-utils-DvHPzAfV.d.ts → plan-utils-2K_N5ZJe.d.ts} +6 -1
- package/dist/{plan-utils-D4ioaM8q.d.mts → plan-utils-D94CJ7CA.d.mts} +6 -1
- package/dist/plan-utils.d.mts +1 -1
- package/dist/plan-utils.d.ts +1 -1
- 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,8 @@ 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;
|
|
2449
2480
|
let tokens = [];
|
|
2450
2481
|
let expandable = false;
|
|
2451
2482
|
let current = "";
|
|
@@ -2475,13 +2506,14 @@ function lex(command, nested, dialect) {
|
|
|
2475
2506
|
current = "";
|
|
2476
2507
|
started = false;
|
|
2477
2508
|
};
|
|
2478
|
-
const endSegment = (nextPiped) => {
|
|
2509
|
+
const endSegment = (nextPiped, next = "other") => {
|
|
2479
2510
|
endToken();
|
|
2480
|
-
if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs });
|
|
2511
|
+
if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs, joinedBy });
|
|
2481
2512
|
tokens = [];
|
|
2482
2513
|
inputs = [];
|
|
2483
2514
|
expandable = false;
|
|
2484
2515
|
piped = nextPiped;
|
|
2516
|
+
joinedBy = next;
|
|
2485
2517
|
};
|
|
2486
2518
|
let i = 0;
|
|
2487
2519
|
while (i < command.length) {
|
|
@@ -2603,17 +2635,18 @@ function lex(command, nested, dialect) {
|
|
|
2603
2635
|
}
|
|
2604
2636
|
if (c === "(" || c === ")") {
|
|
2605
2637
|
endToken();
|
|
2638
|
+
grouped = true;
|
|
2606
2639
|
i++;
|
|
2607
2640
|
continue;
|
|
2608
2641
|
}
|
|
2609
2642
|
if (c === "|") {
|
|
2610
2643
|
const double = command[i + 1] === "|";
|
|
2611
|
-
endSegment(!double);
|
|
2644
|
+
endSegment(!double, double ? "other" : "pipe");
|
|
2612
2645
|
i += double || command[i + 1] === "&" ? 2 : 1;
|
|
2613
2646
|
continue;
|
|
2614
2647
|
}
|
|
2615
2648
|
if (c === "&" || c === ";" || c === "\n") {
|
|
2616
|
-
endSegment(false);
|
|
2649
|
+
endSegment(false, c === "&" && command[i + 1] === "&" ? "and" : "other");
|
|
2617
2650
|
i += command[i + 1] === c ? 2 : 1;
|
|
2618
2651
|
continue;
|
|
2619
2652
|
}
|
|
@@ -2631,7 +2664,7 @@ function lex(command, nested, dialect) {
|
|
|
2631
2664
|
}
|
|
2632
2665
|
if (quote !== "") unbalanced = true;
|
|
2633
2666
|
endSegment(false);
|
|
2634
|
-
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
2667
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped };
|
|
2635
2668
|
}
|
|
2636
2669
|
function binaryName(token, dialect) {
|
|
2637
2670
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -2771,10 +2804,11 @@ function unwrap(seg, dialect) {
|
|
|
2771
2804
|
}
|
|
2772
2805
|
return { seg: { ...current, assignments }, wrappers, runner };
|
|
2773
2806
|
}
|
|
2774
|
-
function lexAll(command, dialect) {
|
|
2807
|
+
function lexAll(command, dialect, cdpathSet = false) {
|
|
2775
2808
|
const queue = [];
|
|
2776
2809
|
const top = lex(command, queue, dialect);
|
|
2777
2810
|
const all = { ...top, segments: top.segments.filter((s) => s.binary) };
|
|
2811
|
+
if (!top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
|
|
2778
2812
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
2779
2813
|
const inner = lex(queue.shift(), queue, dialect);
|
|
2780
2814
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
@@ -2784,15 +2818,32 @@ function lexAll(command, dialect) {
|
|
|
2784
2818
|
}
|
|
2785
2819
|
return all;
|
|
2786
2820
|
}
|
|
2821
|
+
function literalCdTarget(seg) {
|
|
2822
|
+
if (seg.binary !== "cd" || seg.assignments.length > 0 || seg.expandable || seg.inputs.length > 0) return void 0;
|
|
2823
|
+
if (seg.args.length !== 1) return void 0;
|
|
2824
|
+
const [target] = seg.args;
|
|
2825
|
+
return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
|
|
2826
|
+
}
|
|
2827
|
+
function followCd(segments) {
|
|
2828
|
+
if (segments.some((s) => s.joinedBy === "other")) return;
|
|
2829
|
+
let chain = [];
|
|
2830
|
+
segments.forEach((seg, i) => {
|
|
2831
|
+
seg.cwd = chain;
|
|
2832
|
+
const inPipeline = seg.joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
|
|
2833
|
+
const target = inPipeline ? void 0 : literalCdTarget(seg);
|
|
2834
|
+
if (target !== void 0) chain = [...chain, target];
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2787
2837
|
function looksLikePath(arg) {
|
|
2788
2838
|
if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
|
|
2789
2839
|
return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
|
|
2790
2840
|
}
|
|
2791
|
-
function
|
|
2841
|
+
function pathRefs(command, opts = {}) {
|
|
2792
2842
|
const dialect = dialectFor(opts.dialect);
|
|
2793
|
-
return lexAll(command, dialect).segments.flatMap((seg) => {
|
|
2843
|
+
return lexAll(command, dialect, opts.cdpathSet).segments.flatMap((seg) => {
|
|
2794
2844
|
const patterns = patternArgs(seg);
|
|
2795
|
-
|
|
2845
|
+
const cwd = seg.cwd ?? [];
|
|
2846
|
+
return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path26) => ({ path: path26, cwd }));
|
|
2796
2847
|
});
|
|
2797
2848
|
}
|
|
2798
2849
|
function pathIn(a) {
|
|
@@ -2956,7 +3007,8 @@ function scopeFor(seg) {
|
|
|
2956
3007
|
}
|
|
2957
3008
|
return [seg.binary, ...lead].join(" ");
|
|
2958
3009
|
}
|
|
2959
|
-
function isAuto(seg) {
|
|
3010
|
+
function isAuto(seg, opts) {
|
|
3011
|
+
if (seg.binary === "cd") return !opts.cdpathSet && literalCdTarget(seg) !== void 0;
|
|
2960
3012
|
if (!AUTO_COMMANDS.includes(seg.binary)) return false;
|
|
2961
3013
|
if (seg.expandable) return false;
|
|
2962
3014
|
if (seg.binary !== "git") return true;
|
|
@@ -3051,7 +3103,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3051
3103
|
const trimmed = command.trim();
|
|
3052
3104
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
3053
3105
|
const dialect = dialectFor(opts.dialect);
|
|
3054
|
-
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
3106
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect, opts.cdpathSet);
|
|
3055
3107
|
if (unbalanced) {
|
|
3056
3108
|
return {
|
|
3057
3109
|
tier: "refuse",
|
|
@@ -3088,7 +3140,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3088
3140
|
};
|
|
3089
3141
|
}
|
|
3090
3142
|
}
|
|
3091
|
-
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg));
|
|
3143
|
+
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
|
|
3092
3144
|
if (nonAuto.length === 0) return { tier: "auto", scope: "" };
|
|
3093
3145
|
const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
|
|
3094
3146
|
return { tier: "ask", scope };
|
|
@@ -3168,11 +3220,15 @@ function expandHome(target) {
|
|
|
3168
3220
|
if (target === "~" || target.startsWith("~/")) return path2.join(home, target.slice(1));
|
|
3169
3221
|
return path2.join(path2.dirname(home), target.slice(1));
|
|
3170
3222
|
}
|
|
3171
|
-
function resolveWithin(root, target) {
|
|
3172
|
-
const abs = path2.resolve(
|
|
3223
|
+
function resolveWithin(root, target, from = root) {
|
|
3224
|
+
const abs = path2.resolve(from, expandHome(target));
|
|
3173
3225
|
const rel = path2.relative(root, abs);
|
|
3174
3226
|
return { abs, inside: rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel) };
|
|
3175
3227
|
}
|
|
3228
|
+
function resolveRef(root, ref) {
|
|
3229
|
+
const from = ref.cwd.reduce((dir, target) => resolveWithin(root, target, dir).abs, root);
|
|
3230
|
+
return resolveWithin(root, ref.path, from);
|
|
3231
|
+
}
|
|
3176
3232
|
function grantScopeFor(abs, kind) {
|
|
3177
3233
|
if (kind === "directory") return path2.join(abs, "*");
|
|
3178
3234
|
const parent = path2.dirname(abs);
|
|
@@ -3378,14 +3434,17 @@ ${refs.output.trim()}`);
|
|
|
3378
3434
|
outsidePaths(command) {
|
|
3379
3435
|
const root = this.getWorkspaceRoot();
|
|
3380
3436
|
const found = /* @__PURE__ */ new Map();
|
|
3381
|
-
for (const
|
|
3382
|
-
const { abs, inside } =
|
|
3437
|
+
for (const ref of pathRefs(command, this.policyOptions())) {
|
|
3438
|
+
const { abs, inside } = resolveRef(root, ref);
|
|
3383
3439
|
if (inside || isInertDevice(abs)) continue;
|
|
3384
3440
|
const scope = grantScopeFor(abs, "file");
|
|
3385
3441
|
if (!found.has(scope)) found.set(scope, abs);
|
|
3386
3442
|
}
|
|
3387
3443
|
return [...found].map(([scope, abs]) => ({ abs, scope }));
|
|
3388
3444
|
}
|
|
3445
|
+
policyOptions() {
|
|
3446
|
+
return { dialect: this.researchShell.dialect, cdpathSet: !!process.env.CDPATH };
|
|
3447
|
+
}
|
|
3389
3448
|
/**
|
|
3390
3449
|
* Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
|
|
3391
3450
|
* anything else asks once and is remembered, and writes are refused outright
|
|
@@ -3397,7 +3456,7 @@ ${refs.output.trim()}`);
|
|
|
3397
3456
|
* directory the next command names.
|
|
3398
3457
|
*/
|
|
3399
3458
|
async bash(command, signal) {
|
|
3400
|
-
const { tier, scope, reason } = classifyCommand(command,
|
|
3459
|
+
const { tier, scope, reason } = classifyCommand(command, this.policyOptions());
|
|
3401
3460
|
if (tier === "refuse") {
|
|
3402
3461
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
3403
3462
|
}
|
|
@@ -3896,10 +3955,78 @@ function buildGrepArgs(pattern, opts, root) {
|
|
|
3896
3955
|
function buildGlobArgs(pattern, root) {
|
|
3897
3956
|
return ["--files", ...SORT_BY_RECENCY, "--glob", pattern, ...exclusionArgs(), "--", root];
|
|
3898
3957
|
}
|
|
3899
|
-
|
|
3958
|
+
var GREP_PCRE_PROBE_ARGS = ["-P", "-e", "x", "/dev/null"];
|
|
3959
|
+
function grepRegexFromProbe(exitCode) {
|
|
3960
|
+
return exitCode === 0 || exitCode === 1 ? "pcre" : "ere";
|
|
3961
|
+
}
|
|
3962
|
+
function toPosixClasses(pattern) {
|
|
3963
|
+
const CLASS_ESCAPES = { s: "[:space:]", w: "[:alnum:]_", d: "[:digit:]", t: " " };
|
|
3964
|
+
let out = "";
|
|
3965
|
+
let i = 0;
|
|
3966
|
+
while (i < pattern.length) {
|
|
3967
|
+
const c = pattern[i];
|
|
3968
|
+
if (c === "\\") {
|
|
3969
|
+
out += pattern.slice(i, i + 2);
|
|
3970
|
+
i += 2;
|
|
3971
|
+
continue;
|
|
3972
|
+
}
|
|
3973
|
+
if (c !== "[") {
|
|
3974
|
+
out += c;
|
|
3975
|
+
i++;
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
const start = i;
|
|
3979
|
+
i++;
|
|
3980
|
+
let negate = false;
|
|
3981
|
+
if (pattern[i] === "^") {
|
|
3982
|
+
negate = true;
|
|
3983
|
+
i++;
|
|
3984
|
+
}
|
|
3985
|
+
let body = "";
|
|
3986
|
+
let closeBracket = false;
|
|
3987
|
+
let openBracket = false;
|
|
3988
|
+
let dash = false;
|
|
3989
|
+
let closed = false;
|
|
3990
|
+
while (i < pattern.length) {
|
|
3991
|
+
const ch = pattern[i];
|
|
3992
|
+
if (ch === "]") {
|
|
3993
|
+
closed = true;
|
|
3994
|
+
i++;
|
|
3995
|
+
break;
|
|
3996
|
+
}
|
|
3997
|
+
if (ch === "[" && pattern[i + 1] === ":") {
|
|
3998
|
+
const end = pattern.indexOf(":]", i + 2);
|
|
3999
|
+
if (end !== -1) {
|
|
4000
|
+
body += pattern.slice(i, end + 2);
|
|
4001
|
+
i = end + 2;
|
|
4002
|
+
continue;
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
4006
|
+
const e = pattern[i + 1];
|
|
4007
|
+
i += 2;
|
|
4008
|
+
if (e in CLASS_ESCAPES) body += CLASS_ESCAPES[e];
|
|
4009
|
+
else if (e === "]") closeBracket = true;
|
|
4010
|
+
else if (e === "[") openBracket = true;
|
|
4011
|
+
else if (e === "-") dash = true;
|
|
4012
|
+
else body += e;
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
body += ch;
|
|
4016
|
+
i++;
|
|
4017
|
+
}
|
|
4018
|
+
if (!closed) {
|
|
4019
|
+
out += pattern.slice(start);
|
|
4020
|
+
break;
|
|
4021
|
+
}
|
|
4022
|
+
out += `[${negate ? "^" : ""}${closeBracket ? "]" : ""}${body}${openBracket ? "[" : ""}${dash ? "-" : ""}]`;
|
|
4023
|
+
}
|
|
4024
|
+
return out;
|
|
4025
|
+
}
|
|
4026
|
+
function buildFallbackGrepArgs(pattern, opts, root, regex = "pcre") {
|
|
3900
4027
|
const args = ["-r", "-n"];
|
|
3901
4028
|
if (opts.literal) args.push("-F");
|
|
3902
|
-
else args.push("-P");
|
|
4029
|
+
else args.push(regex === "pcre" ? "-P" : "-E");
|
|
3903
4030
|
if (opts.caseInsensitive) args.push("-i");
|
|
3904
4031
|
if (opts.outputMode === "files") args.push("-l");
|
|
3905
4032
|
if (opts.outputMode === "count") args.push("-c");
|
|
@@ -3909,7 +4036,7 @@ function buildFallbackGrepArgs(pattern, opts, root) {
|
|
|
3909
4036
|
}
|
|
3910
4037
|
if (opts.include) args.push(`--include=${opts.include}`);
|
|
3911
4038
|
for (const dir of SEARCH_EXCLUSIONS) args.push(`--exclude-dir=${dir}`);
|
|
3912
|
-
args.push("-e", pattern, root);
|
|
4039
|
+
args.push("-e", !opts.literal && regex === "ere" ? toPosixClasses(pattern) : pattern, root);
|
|
3913
4040
|
return args;
|
|
3914
4041
|
}
|
|
3915
4042
|
function globSegmentToRegex(segment) {
|
|
@@ -7600,14 +7727,15 @@ function nonPromptingFs(fs16) {
|
|
|
7600
7727
|
listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
7601
7728
|
bash: async (command, signal) => {
|
|
7602
7729
|
const dialect = resolveResearchShell().dialect;
|
|
7603
|
-
const
|
|
7730
|
+
const policy = { dialect, cdpathSet: !!process.env.CDPATH };
|
|
7731
|
+
const { tier, reason } = classifyCommand(command, policy);
|
|
7604
7732
|
if (tier !== "auto") {
|
|
7605
7733
|
return refused2(
|
|
7606
7734
|
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
7735
|
);
|
|
7608
7736
|
}
|
|
7609
|
-
const escaping =
|
|
7610
|
-
if (escaping) return outsideRefusal(escaping);
|
|
7737
|
+
const escaping = pathRefs(command, policy).find((ref) => !resolveRef(fs16.getWorkspaceRoot(), ref).inside);
|
|
7738
|
+
if (escaping) return outsideRefusal(escaping.path);
|
|
7611
7739
|
return fs16.bash(command, signal);
|
|
7612
7740
|
}
|
|
7613
7741
|
};
|
|
@@ -8923,6 +9051,12 @@ var VerdictEngine = class {
|
|
|
8923
9051
|
idleSince = /* @__PURE__ */ new Map();
|
|
8924
9052
|
/** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
|
|
8925
9053
|
idlePaused = /* @__PURE__ */ new Set();
|
|
9054
|
+
/**
|
|
9055
|
+
* Open tool approvals per structured task. They arrive mid-turn and leave the
|
|
9056
|
+
* status alone (ADR-0018, W1), but a task waiting on one is waiting on the
|
|
9057
|
+
* user, so it is not idle either.
|
|
9058
|
+
*/
|
|
9059
|
+
openApprovals = /* @__PURE__ */ new Map();
|
|
8926
9060
|
onVerdict(listener) {
|
|
8927
9061
|
this.listeners.push(listener);
|
|
8928
9062
|
}
|
|
@@ -8951,7 +9085,7 @@ var VerdictEngine = class {
|
|
|
8951
9085
|
}
|
|
8952
9086
|
/** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
|
|
8953
9087
|
touchIdle(taskId, gen) {
|
|
8954
|
-
if (this.idlePaused.has(taskId)) return;
|
|
9088
|
+
if (this.idlePaused.has(taskId) || this.openApprovals.get(taskId)?.size) return;
|
|
8955
9089
|
const existing = this.idleTimers.get(taskId);
|
|
8956
9090
|
if (existing) clearTimeout(existing);
|
|
8957
9091
|
if (this.idleSince.get(taskId)) {
|
|
@@ -9033,7 +9167,10 @@ ${line}
|
|
|
9033
9167
|
});
|
|
9034
9168
|
if (isStructuredSession(session)) {
|
|
9035
9169
|
session.onEvent((event) => {
|
|
9036
|
-
if (
|
|
9170
|
+
if (this.generations.get(task.id) !== gen) return;
|
|
9171
|
+
if (event.type === "turn_start") this.resumeIdle(task.id);
|
|
9172
|
+
else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
|
|
9173
|
+
else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
|
|
9037
9174
|
});
|
|
9038
9175
|
}
|
|
9039
9176
|
session.onExit((exitCode) => {
|
|
@@ -9044,6 +9181,18 @@ ${line}
|
|
|
9044
9181
|
for (const l of this.listeners) l(task.id, verdict);
|
|
9045
9182
|
});
|
|
9046
9183
|
}
|
|
9184
|
+
approvalOpened(taskId, approvalId) {
|
|
9185
|
+
const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
9186
|
+
open.add(approvalId);
|
|
9187
|
+
this.openApprovals.set(taskId, open);
|
|
9188
|
+
this.clearIdle(taskId);
|
|
9189
|
+
}
|
|
9190
|
+
approvalClosed(taskId, approvalId, gen) {
|
|
9191
|
+
const open = this.openApprovals.get(taskId);
|
|
9192
|
+
if (!open?.delete(approvalId) || open.size) return;
|
|
9193
|
+
this.openApprovals.delete(taskId);
|
|
9194
|
+
this.touchIdle(taskId, gen);
|
|
9195
|
+
}
|
|
9047
9196
|
/** Scan only the new text plus the unmatched carry, so a long run stays linear. */
|
|
9048
9197
|
scanCheckpoints(taskId, session, text) {
|
|
9049
9198
|
const scan = (this.checkpointCarry.get(taskId) ?? "") + text;
|
|
@@ -9065,6 +9214,7 @@ ${line}
|
|
|
9065
9214
|
this.checkpointCarry.delete(taskId);
|
|
9066
9215
|
this.pausedSessions.delete(taskId);
|
|
9067
9216
|
this.idlePaused.delete(taskId);
|
|
9217
|
+
this.openApprovals.delete(taskId);
|
|
9068
9218
|
this.clearIdle(taskId);
|
|
9069
9219
|
}
|
|
9070
9220
|
/** Manual "Mark complete" override: a pass verdict that bypasses evidence. */
|
|
@@ -9102,6 +9252,7 @@ ${line}
|
|
|
9102
9252
|
this.idleTimers.clear();
|
|
9103
9253
|
this.idleSince.clear();
|
|
9104
9254
|
this.idlePaused.clear();
|
|
9255
|
+
this.openApprovals.clear();
|
|
9105
9256
|
this.generations.clear();
|
|
9106
9257
|
}
|
|
9107
9258
|
decide(task, exitCode) {
|
|
@@ -12765,9 +12916,502 @@ ${item.text}` : item.text });
|
|
|
12765
12916
|
|
|
12766
12917
|
// src/services/harness/OpenCodeAdapter.ts
|
|
12767
12918
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
12919
|
+
|
|
12920
|
+
// src/services/harness/OpenCodeV2.ts
|
|
12921
|
+
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
12922
|
+
var ACTIVE_POLL_INTERVAL_MS = 1e3;
|
|
12923
|
+
var IDLE_POLLS_BEFORE_START = 3;
|
|
12924
|
+
var PLANNER_RULES = [
|
|
12925
|
+
{ action: "question", resource: "*", effect: "deny" },
|
|
12926
|
+
{ action: "edit", resource: "*", effect: "deny" }
|
|
12927
|
+
];
|
|
12928
|
+
var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
|
|
12929
|
+
var AUTO_APPROVALS = "auto";
|
|
12930
|
+
function newTurnState() {
|
|
12931
|
+
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() };
|
|
12932
|
+
}
|
|
12933
|
+
function splitModelId(id) {
|
|
12934
|
+
const slash = id.indexOf("/");
|
|
12935
|
+
if (slash <= 0 || slash === id.length - 1) return null;
|
|
12936
|
+
return { providerID: id.slice(0, slash), id: id.slice(slash + 1) };
|
|
12937
|
+
}
|
|
12938
|
+
function delay(ms, signal) {
|
|
12939
|
+
return new Promise((resolve7) => {
|
|
12940
|
+
const timer = setTimeout(resolve7, ms);
|
|
12941
|
+
timer.unref?.();
|
|
12942
|
+
signal?.addEventListener("abort", () => {
|
|
12943
|
+
clearTimeout(timer);
|
|
12944
|
+
resolve7();
|
|
12945
|
+
}, { once: true });
|
|
12946
|
+
});
|
|
12947
|
+
}
|
|
12948
|
+
function contentText(content) {
|
|
12949
|
+
return (content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
|
|
12950
|
+
}
|
|
12951
|
+
function subagentDigest(output) {
|
|
12952
|
+
const inner = output.match(/<subagent[^>]*>\n?([\s\S]*?)\n?<\/subagent>/);
|
|
12953
|
+
return inner ? inner[1] : output;
|
|
12954
|
+
}
|
|
12955
|
+
function permissionReply(decision) {
|
|
12956
|
+
if (decision.decision === "allow") return { decision: "once" };
|
|
12957
|
+
if (decision.decision === "allowForTask") return { decision: "always" };
|
|
12958
|
+
const note = decision.note?.trim();
|
|
12959
|
+
return note ? { decision: "reject", message: note } : { decision: "reject" };
|
|
12960
|
+
}
|
|
12961
|
+
var OpenCodeV2 = class {
|
|
12962
|
+
constructor(host, opts, role) {
|
|
12963
|
+
this.host = host;
|
|
12964
|
+
this.opts = opts;
|
|
12965
|
+
this.role = role;
|
|
12966
|
+
this.agent = opts.kind === "task" ? opts.flags.permissionMode : "plan";
|
|
12967
|
+
const split = opts.model ? splitModelId(opts.model) : null;
|
|
12968
|
+
const variant = opts.kind === "task" ? opts.flags.effort : opts.effort;
|
|
12969
|
+
if (split) this.model = { ...split, ...variant ? { variant } : {} };
|
|
12970
|
+
}
|
|
12971
|
+
host;
|
|
12972
|
+
opts;
|
|
12973
|
+
role;
|
|
12974
|
+
sessionId = null;
|
|
12975
|
+
model = null;
|
|
12976
|
+
agent;
|
|
12977
|
+
turn = null;
|
|
12978
|
+
turnHasText = false;
|
|
12979
|
+
interruptRequested = false;
|
|
12980
|
+
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
12981
|
+
openPermissions = /* @__PURE__ */ new Map();
|
|
12982
|
+
nativeSessionId() {
|
|
12983
|
+
return this.sessionId;
|
|
12984
|
+
}
|
|
12985
|
+
async start() {
|
|
12986
|
+
const resume = this.opts.resumeSessionId;
|
|
12987
|
+
if (resume) {
|
|
12988
|
+
const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
|
|
12989
|
+
if (existing?.data?.id) {
|
|
12990
|
+
this.sessionId = existing.data.id;
|
|
12991
|
+
if (this.model) await this.host.request("POST", `/api/session/${this.sessionId}/model`, { model: this.model }).catch(() => void 0);
|
|
12992
|
+
await this.host.request("POST", `/api/session/${this.sessionId}/agent`, { agent: this.agent }).catch(() => void 0);
|
|
12993
|
+
await this.applyInstructions();
|
|
12994
|
+
return;
|
|
12995
|
+
}
|
|
12996
|
+
if (this.opts.kind === "task") throw new Error(`OpenCode could not resume session ${resume}: the server does not know it.`);
|
|
12997
|
+
}
|
|
12998
|
+
const created = await this.host.json("POST", "/api/session", {
|
|
12999
|
+
agent: this.agent,
|
|
13000
|
+
...this.model ? { model: this.model } : {},
|
|
13001
|
+
permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
|
|
13002
|
+
});
|
|
13003
|
+
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
13004
|
+
this.sessionId = created.data.id;
|
|
13005
|
+
await this.applyInstructions();
|
|
13006
|
+
}
|
|
13007
|
+
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
13008
|
+
async applyInstructions() {
|
|
13009
|
+
if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
|
|
13010
|
+
const response = await this.host.request("PUT", `/api/experimental/session/${this.sessionId}/instructions/entries/ordewell-planner`, { value: this.opts.systemPrompt });
|
|
13011
|
+
if (!response.ok) throw new Error(`OpenCode did not take the planner instructions: ${response.status} ${response.statusText}`);
|
|
13012
|
+
}
|
|
13013
|
+
async send(message, onEvent, signal, onActivity) {
|
|
13014
|
+
if (!this.sessionId) throw new Error(`OpenCode ${this.role} session is not started`);
|
|
13015
|
+
if (this.host.isExited()) {
|
|
13016
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
13017
|
+
return;
|
|
13018
|
+
}
|
|
13019
|
+
const state = newTurnState();
|
|
13020
|
+
this.turnHasText = false;
|
|
13021
|
+
this.interruptRequested = false;
|
|
13022
|
+
let markDone = () => {
|
|
13023
|
+
};
|
|
13024
|
+
const ended = new Promise((resolve7) => {
|
|
13025
|
+
markDone = resolve7;
|
|
13026
|
+
});
|
|
13027
|
+
const turn = { live: false, done: false, outcome: null, failure: null, ended, finish: () => {
|
|
13028
|
+
turn.done = true;
|
|
13029
|
+
markDone();
|
|
13030
|
+
} };
|
|
13031
|
+
this.turn = turn;
|
|
13032
|
+
const closeStream = await this.openStream(state, turn, onEvent, onActivity);
|
|
13033
|
+
const poll = new AbortController();
|
|
13034
|
+
try {
|
|
13035
|
+
try {
|
|
13036
|
+
await this.host.json("POST", `/api/session/${this.sessionId}/prompt`, { text: message }, signal);
|
|
13037
|
+
} catch (err) {
|
|
13038
|
+
if (signal?.aborted) return;
|
|
13039
|
+
onEvent({ type: "error", message: `OpenCode did not take the message: ${err instanceof Error ? err.message : String(err)}` });
|
|
13040
|
+
return;
|
|
13041
|
+
}
|
|
13042
|
+
void this.pollActive(turn, poll.signal);
|
|
13043
|
+
const aborted = new Promise((resolve7) => {
|
|
13044
|
+
if (signal?.aborted) resolve7();
|
|
13045
|
+
signal?.addEventListener("abort", () => resolve7(), { once: true });
|
|
13046
|
+
});
|
|
13047
|
+
await Promise.race([turn.ended, aborted, this.host.processEnded]);
|
|
13048
|
+
if (signal?.aborted) return;
|
|
13049
|
+
if (!turn.done) {
|
|
13050
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
13051
|
+
return;
|
|
13052
|
+
}
|
|
13053
|
+
await this.readBack(state, onEvent);
|
|
13054
|
+
for (const id of [...this.openPermissions.keys()]) {
|
|
13055
|
+
this.openPermissions.delete(id);
|
|
13056
|
+
onEvent({ type: "permission_cancelled", id });
|
|
13057
|
+
}
|
|
13058
|
+
if (turn.outcome === "interrupted" || this.interruptRequested) onEvent({ type: "turn_end", interrupted: true });
|
|
13059
|
+
else if (turn.outcome === "failed") onEvent({ type: "error", message: turn.failure ?? "OpenCode reported that the turn failed." });
|
|
13060
|
+
else onEvent({ type: "turn_end" });
|
|
13061
|
+
} finally {
|
|
13062
|
+
this.turn = null;
|
|
13063
|
+
poll.abort();
|
|
13064
|
+
await closeStream();
|
|
13065
|
+
}
|
|
13066
|
+
}
|
|
13067
|
+
/**
|
|
13068
|
+
* The stream can drop the end frame while still delivering the rest, so the
|
|
13069
|
+
* turn also asks which sessions are running. A session that is not, after
|
|
13070
|
+
* this turn's work was seen, is over; one never seen working after a few
|
|
13071
|
+
* polls had already finished before the stream connected.
|
|
13072
|
+
*/
|
|
13073
|
+
async pollActive(turn, signal) {
|
|
13074
|
+
let idlePolls = 0;
|
|
13075
|
+
while (!turn.done && !signal.aborted) {
|
|
13076
|
+
await delay(ACTIVE_POLL_INTERVAL_MS, signal);
|
|
13077
|
+
if (turn.done || signal.aborted) return;
|
|
13078
|
+
const active = await this.host.json("GET", "/api/session/active").catch(() => null);
|
|
13079
|
+
if (!active?.data) continue;
|
|
13080
|
+
if (this.sessionId && this.sessionId in active.data) {
|
|
13081
|
+
turn.live = true;
|
|
13082
|
+
idlePolls = 0;
|
|
13083
|
+
continue;
|
|
13084
|
+
}
|
|
13085
|
+
idlePolls++;
|
|
13086
|
+
if (turn.live || idlePolls >= IDLE_POLLS_BEFORE_START) {
|
|
13087
|
+
const messages = await this.messages();
|
|
13088
|
+
const idle = [...messages ?? []].reverse().find((m) => m.type === "idle");
|
|
13089
|
+
if (idle?.outcome === "failed" || idle?.outcome === "interrupted" || idle?.outcome === "succeeded") turn.outcome ??= idle.outcome;
|
|
13090
|
+
turn.outcome ??= "succeeded";
|
|
13091
|
+
if (!turn.done) turn.finish();
|
|
13092
|
+
}
|
|
13093
|
+
}
|
|
13094
|
+
}
|
|
13095
|
+
async messages() {
|
|
13096
|
+
const response = await this.host.json("GET", `/api/session/${this.sessionId}/message`).catch(() => null);
|
|
13097
|
+
return Array.isArray(response?.data) ? response.data : null;
|
|
13098
|
+
}
|
|
13099
|
+
/**
|
|
13100
|
+
* Whatever of the turn's own replies the stream missed. A message whose text
|
|
13101
|
+
* the stream delivered adds nothing here; one that lost its text frames still
|
|
13102
|
+
* reaches the plain-text channel, where the done marker is looked for.
|
|
13103
|
+
*/
|
|
13104
|
+
async readBack(state, onEvent) {
|
|
13105
|
+
const messages = await this.messages();
|
|
13106
|
+
if (!messages) return;
|
|
13107
|
+
const latestUser = messages.findIndex((m) => m.type === "user");
|
|
13108
|
+
const own = latestUser < 0 ? messages : messages.slice(0, latestUser);
|
|
13109
|
+
for (const message of [...own].reverse()) {
|
|
13110
|
+
if (message.type !== "assistant" || !message.id || state.streamedMessages.has(message.id)) continue;
|
|
13111
|
+
(message.content ?? []).forEach((part, index) => {
|
|
13112
|
+
if (part.type === "text" && part.text) this.emitText(`${message.id}:readback:${index}`, part.text, state, onEvent);
|
|
13113
|
+
});
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
/**
|
|
13117
|
+
* Abort the running turn, keeping the server and its session. OpenCode
|
|
13118
|
+
* acknowledges with `session.execution.interrupted`, which also ends the turn.
|
|
13119
|
+
*/
|
|
13120
|
+
async interrupt(timeoutMs) {
|
|
13121
|
+
if (!this.sessionId || this.host.isExited()) return false;
|
|
13122
|
+
const turn = this.turn;
|
|
13123
|
+
this.interruptRequested = true;
|
|
13124
|
+
const response = await this.host.json("POST", `/api/session/${this.sessionId}/interrupt`).catch(() => null);
|
|
13125
|
+
if (!response) return false;
|
|
13126
|
+
if (!turn || turn.done) return true;
|
|
13127
|
+
let timer;
|
|
13128
|
+
const timedOut = new Promise((resolve7) => {
|
|
13129
|
+
timer = setTimeout(() => resolve7(false), timeoutMs);
|
|
13130
|
+
timer.unref?.();
|
|
13131
|
+
});
|
|
13132
|
+
const acknowledged = await Promise.race([turn.ended.then(() => true), this.host.processEnded.then(() => false), timedOut]);
|
|
13133
|
+
clearTimeout(timer);
|
|
13134
|
+
return acknowledged;
|
|
13135
|
+
}
|
|
13136
|
+
answerPermission(id, decision) {
|
|
13137
|
+
const sessionId = this.openPermissions.get(id);
|
|
13138
|
+
if (sessionId === void 0) return false;
|
|
13139
|
+
this.openPermissions.delete(id);
|
|
13140
|
+
void this.replyPermission(id, sessionId, permissionReply(decision)).catch(() => {
|
|
13141
|
+
});
|
|
13142
|
+
return true;
|
|
13143
|
+
}
|
|
13144
|
+
async replyPermission(id, sessionId, reply) {
|
|
13145
|
+
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
|
|
13146
|
+
}
|
|
13147
|
+
async openStream(state, turn, onEvent, onActivity) {
|
|
13148
|
+
const streamAbort = new AbortController();
|
|
13149
|
+
let connected = () => {
|
|
13150
|
+
};
|
|
13151
|
+
const ready = new Promise((resolve7) => {
|
|
13152
|
+
connected = resolve7;
|
|
13153
|
+
});
|
|
13154
|
+
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, state, turn, onEvent), connected, onActivity);
|
|
13155
|
+
await Promise.race([ready, new Promise((r) => {
|
|
13156
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS);
|
|
13157
|
+
t.unref?.();
|
|
13158
|
+
})]);
|
|
13159
|
+
return async () => {
|
|
13160
|
+
streamAbort.abort();
|
|
13161
|
+
await live.catch(() => {
|
|
13162
|
+
});
|
|
13163
|
+
};
|
|
13164
|
+
}
|
|
13165
|
+
async streamEvents(signal, onFrame, onConnected, onActivity) {
|
|
13166
|
+
const response = await this.host.openEvents(signal).catch(() => null);
|
|
13167
|
+
const body = response?.body;
|
|
13168
|
+
if (!body) {
|
|
13169
|
+
onConnected();
|
|
13170
|
+
return;
|
|
13171
|
+
}
|
|
13172
|
+
onConnected();
|
|
13173
|
+
const reader = body.getReader();
|
|
13174
|
+
const decoder = new TextDecoder();
|
|
13175
|
+
let buffer = "";
|
|
13176
|
+
for (; ; ) {
|
|
13177
|
+
const { done, value } = await reader.read().catch(() => ({ done: true, value: void 0 }));
|
|
13178
|
+
if (done) return;
|
|
13179
|
+
onActivity?.();
|
|
13180
|
+
buffer += decoder.decode(value, { stream: true });
|
|
13181
|
+
let newline;
|
|
13182
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
13183
|
+
const line = buffer.slice(0, newline).trim();
|
|
13184
|
+
buffer = buffer.slice(newline + 1);
|
|
13185
|
+
if (!line.startsWith("data:")) continue;
|
|
13186
|
+
try {
|
|
13187
|
+
onFrame(JSON.parse(line.slice(5).trim()));
|
|
13188
|
+
} catch {
|
|
13189
|
+
}
|
|
13190
|
+
}
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13193
|
+
/**
|
|
13194
|
+
* One frame. Only this session and its children are followed: the stream is
|
|
13195
|
+
* the whole server's, and another client's session is none of this turn's business.
|
|
13196
|
+
*/
|
|
13197
|
+
onFrame(frame, state, turn, onEvent) {
|
|
13198
|
+
const data = frame.data;
|
|
13199
|
+
if (!data || !frame.type) return;
|
|
13200
|
+
if (frame.type === "session.created") {
|
|
13201
|
+
if (data.parentID && data.parentID === this.sessionId && data.sessionID && !state.children.has(data.sessionID)) state.children.set(data.sessionID, null);
|
|
13202
|
+
return;
|
|
13203
|
+
}
|
|
13204
|
+
const session = data.sessionID;
|
|
13205
|
+
if (!session) return;
|
|
13206
|
+
const isChild = session !== this.sessionId;
|
|
13207
|
+
if (isChild && !state.children.has(session)) return;
|
|
13208
|
+
if (!isChild) {
|
|
13209
|
+
if (frame.type === "session.execution.started") {
|
|
13210
|
+
turn.live = true;
|
|
13211
|
+
return;
|
|
13212
|
+
}
|
|
13213
|
+
if (frame.type === "session.execution.succeeded" || frame.type === "session.execution.failed" || frame.type === "session.execution.interrupted") {
|
|
13214
|
+
if (!turn.live || turn.done) return;
|
|
13215
|
+
turn.outcome = frame.type === "session.execution.succeeded" ? "succeeded" : frame.type === "session.execution.failed" ? "failed" : "interrupted";
|
|
13216
|
+
if (turn.outcome === "failed") turn.failure = data.error?.message ?? data.error?.type ?? null;
|
|
13217
|
+
turn.finish();
|
|
13218
|
+
return;
|
|
13219
|
+
}
|
|
13220
|
+
}
|
|
13221
|
+
if (frame.type === "permission.asked") {
|
|
13222
|
+
if (this.role === "task") this.askPermission(data, session, state, onEvent);
|
|
13223
|
+
else this.denyPermission(data, session, state, onEvent);
|
|
13224
|
+
return;
|
|
13225
|
+
}
|
|
13226
|
+
if (frame.type === "permission.replied") {
|
|
13227
|
+
const id = data.requestID;
|
|
13228
|
+
if (id && this.openPermissions.delete(id)) onEvent({ type: "permission_cancelled", id });
|
|
13229
|
+
return;
|
|
13230
|
+
}
|
|
13231
|
+
let subagentId;
|
|
13232
|
+
if (isChild) {
|
|
13233
|
+
const owner = state.children.get(session);
|
|
13234
|
+
if (!owner) {
|
|
13235
|
+
state.heldFrames.set(session, [...state.heldFrames.get(session) ?? [], frame]);
|
|
13236
|
+
return;
|
|
13237
|
+
}
|
|
13238
|
+
subagentId = owner;
|
|
13239
|
+
}
|
|
13240
|
+
this.onContentFrame(frame, data, state, turn, onEvent, subagentId);
|
|
13241
|
+
}
|
|
13242
|
+
onContentFrame(frame, data, state, turn, onEvent, subagentId) {
|
|
13243
|
+
const message = data.assistantMessageID ?? "";
|
|
13244
|
+
switch (frame.type) {
|
|
13245
|
+
case "session.step.started":
|
|
13246
|
+
if (message && data.model?.providerID && data.model.id) state.stepModels.set(message, `${data.model.providerID}/${data.model.id}`);
|
|
13247
|
+
return;
|
|
13248
|
+
case "session.reasoning.delta":
|
|
13249
|
+
if (data.delta) onEvent({ type: "thinking_delta", text: data.delta, subagentId });
|
|
13250
|
+
return;
|
|
13251
|
+
case "session.reasoning.ended": {
|
|
13252
|
+
const key = `reasoning:${message}:${data.ordinal ?? 0}`;
|
|
13253
|
+
if (!data.text || state.seen.has(key)) return;
|
|
13254
|
+
state.seen.add(key);
|
|
13255
|
+
onEvent({ type: "thinking", text: data.text, subagentId });
|
|
13256
|
+
return;
|
|
13257
|
+
}
|
|
13258
|
+
case "session.text.delta":
|
|
13259
|
+
if (!subagentId && data.delta) this.onTextDelta(`${message}:${data.ordinal ?? 0}`, data.delta, state, onEvent);
|
|
13260
|
+
return;
|
|
13261
|
+
case "session.text.ended":
|
|
13262
|
+
if (subagentId || !data.text) return;
|
|
13263
|
+
if (message) state.streamedMessages.add(message);
|
|
13264
|
+
this.emitText(`${message}:${data.ordinal ?? 0}`, data.text, state, onEvent);
|
|
13265
|
+
return;
|
|
13266
|
+
case "session.tool.input.started":
|
|
13267
|
+
if (data.id && data.name) state.toolNames.set(data.id, data.name);
|
|
13268
|
+
return;
|
|
13269
|
+
case "session.tool.called": {
|
|
13270
|
+
const id = data.id;
|
|
13271
|
+
if (!id || state.seen.has(`call:${id}`)) return;
|
|
13272
|
+
state.seen.add(`call:${id}`);
|
|
13273
|
+
const input = data.input ?? {};
|
|
13274
|
+
const name = state.toolNames.get(id) ?? "tool";
|
|
13275
|
+
const brief = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
13276
|
+
state.briefs.set(id, brief);
|
|
13277
|
+
onEvent({ type: "tool_call", id, name, args: input, subagentId });
|
|
13278
|
+
return;
|
|
13279
|
+
}
|
|
13280
|
+
case "session.tool.progress":
|
|
13281
|
+
if (!subagentId) this.adoptChild(data, state, onEvent, turn);
|
|
13282
|
+
return;
|
|
13283
|
+
case "session.tool.success":
|
|
13284
|
+
case "session.tool.failed": {
|
|
13285
|
+
const id = data.id;
|
|
13286
|
+
if (!id || state.seen.has(`result:${id}`)) return;
|
|
13287
|
+
state.seen.add(`result:${id}`);
|
|
13288
|
+
const success = frame.type === "session.tool.success";
|
|
13289
|
+
const output = success ? contentText(data.content) : data.error?.message ?? data.error?.type ?? "";
|
|
13290
|
+
onEvent({ type: "tool_result", id, name: state.toolNames.get(id) ?? "tool", output, success, subagentId });
|
|
13291
|
+
if (!subagentId) this.finishChild(id, success, output, state, onEvent);
|
|
13292
|
+
return;
|
|
13293
|
+
}
|
|
13294
|
+
case "session.step.ended":
|
|
13295
|
+
case "session.step.failed":
|
|
13296
|
+
this.countUsage(data, state, onEvent, subagentId);
|
|
13297
|
+
return;
|
|
13298
|
+
default:
|
|
13299
|
+
}
|
|
13300
|
+
}
|
|
13301
|
+
adoptChild(data, state, onEvent, turn) {
|
|
13302
|
+
const child = data.metadata?.sessionID;
|
|
13303
|
+
const callId = data.id;
|
|
13304
|
+
if (!child || !callId || state.children.get(child)) return;
|
|
13305
|
+
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
13306
|
+
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
13307
|
+
state.children.set(child, callId);
|
|
13308
|
+
const held = state.heldFrames.get(child) ?? [];
|
|
13309
|
+
state.heldFrames.delete(child);
|
|
13310
|
+
for (const frame of held) this.onFrame(frame, state, turn, onEvent);
|
|
13311
|
+
}
|
|
13312
|
+
finishChild(callId, success, output, state, onEvent) {
|
|
13313
|
+
if (![...state.children.values()].includes(callId)) return;
|
|
13314
|
+
onEvent({ type: "subagent_finished", subagentId: callId, outcome: success ? "done" : "failed", digest: subagentDigest(output) });
|
|
13315
|
+
}
|
|
13316
|
+
/** One model call's usage, once, when its step ends. Zeros mean the call failed before the provider answered. */
|
|
13317
|
+
countUsage(data, state, onEvent, subagentId) {
|
|
13318
|
+
const message = data.assistantMessageID;
|
|
13319
|
+
const tokens = data.tokens;
|
|
13320
|
+
if (!message || !tokens || state.seen.has(`usage:${message}`)) return;
|
|
13321
|
+
state.seen.add(`usage:${message}`);
|
|
13322
|
+
const prompt = partedPromptUsage({ uncached: tokens.input, cacheRead: tokens.cache?.read, cacheWrite: tokens.cache?.write });
|
|
13323
|
+
const outputTokens = (tokens.output ?? 0) + (tokens.reasoning ?? 0);
|
|
13324
|
+
if ((prompt.inputTokens ?? 0) + outputTokens === 0) return;
|
|
13325
|
+
const record = { source: "opencode", ...prompt, outputTokens };
|
|
13326
|
+
const model = state.stepModels.get(message);
|
|
13327
|
+
if (model) record.model = model;
|
|
13328
|
+
if (typeof data.cost === "number" && data.cost > 0) record.reportedCost = { amount: data.cost, currency: "USD" };
|
|
13329
|
+
if (subagentId) record.subagentId = subagentId;
|
|
13330
|
+
onEvent({ type: "usage", record });
|
|
13331
|
+
}
|
|
13332
|
+
/** 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. */
|
|
13333
|
+
emitText(id, text, state, onEvent) {
|
|
13334
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
13335
|
+
state.seen.add(`text:${id}`);
|
|
13336
|
+
if (!text.trim()) return;
|
|
13337
|
+
const lead = state.textRuns.get(id)?.lead ?? (this.turnHasText ? "\n\n" : "");
|
|
13338
|
+
onEvent({ type: "assistant_text", text: `${lead}${text}` });
|
|
13339
|
+
this.turnHasText = true;
|
|
13340
|
+
}
|
|
13341
|
+
/**
|
|
13342
|
+
* Stream one piece of a reply text. The paragraph break goes out with the
|
|
13343
|
+
* first visible delta, so the deltas add up to exactly the text the completed
|
|
13344
|
+
* run then re-sends; a run that is only whitespace so far is held back.
|
|
13345
|
+
*/
|
|
13346
|
+
onTextDelta(id, delta, state, onEvent) {
|
|
13347
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
13348
|
+
const run = state.textRuns.get(id) ?? { held: "", lead: null };
|
|
13349
|
+
state.textRuns.set(id, run);
|
|
13350
|
+
if (run.lead !== null) {
|
|
13351
|
+
onEvent({ type: "assistant_text_delta", text: delta });
|
|
13352
|
+
return;
|
|
13353
|
+
}
|
|
13354
|
+
run.held += delta;
|
|
13355
|
+
if (!run.held.trim()) return;
|
|
13356
|
+
run.lead = this.turnHasText ? "\n\n" : "";
|
|
13357
|
+
this.turnHasText = true;
|
|
13358
|
+
onEvent({ type: "assistant_text_delta", text: `${run.lead}${run.held}` });
|
|
13359
|
+
}
|
|
13360
|
+
permissionRequest(ask) {
|
|
13361
|
+
const id = ask.id;
|
|
13362
|
+
if (!id) return null;
|
|
13363
|
+
const scope = (ask.resources ?? []).join(", ");
|
|
13364
|
+
return {
|
|
13365
|
+
type: "permission_request",
|
|
13366
|
+
id,
|
|
13367
|
+
name: ask.action ?? "permission",
|
|
13368
|
+
detail: JSON.stringify(scope ? { scope } : {}),
|
|
13369
|
+
input: ask.resources ? { resources: ask.resources } : {},
|
|
13370
|
+
// The patterns `always` would grant: the runner's own offer, and the only grounds for "Allow for this task".
|
|
13371
|
+
...ask.save?.length ? { suggestions: ask.save } : {},
|
|
13372
|
+
...ask.source?.id ? { toolUseId: ask.source.id } : {}
|
|
13373
|
+
};
|
|
13374
|
+
}
|
|
13375
|
+
/**
|
|
13376
|
+
* Deny one planner request. OpenCode blocks the turn until it is answered, so
|
|
13377
|
+
* this must answer: `reject` rather than a silent drop is the "absent answer
|
|
13378
|
+
* is a denial" invariant ADR-0008 states. The refusal is announced so the
|
|
13379
|
+
* timeline shows the planner reaching for something it may not have.
|
|
13380
|
+
*/
|
|
13381
|
+
denyPermission(ask, session, state, onEvent) {
|
|
13382
|
+
const request = this.permissionRequest(ask);
|
|
13383
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
13384
|
+
state.seen.add(`perm:${request.id}`);
|
|
13385
|
+
onEvent(request);
|
|
13386
|
+
void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
|
|
13387
|
+
});
|
|
13388
|
+
}
|
|
13389
|
+
/**
|
|
13390
|
+
* A task's request. Under a mode whose manifest sets `approvals: auto` it is
|
|
13391
|
+
* answered at once with what `opencode run --auto` answers, so the same plan
|
|
13392
|
+
* behaves the same on both transports (ADR-0001), and announced already
|
|
13393
|
+
* decided so the log still shows it. Any other mode leaves it open for an
|
|
13394
|
+
* approval card.
|
|
13395
|
+
*/
|
|
13396
|
+
askPermission(ask, session, state, onEvent) {
|
|
13397
|
+
const request = this.permissionRequest(ask);
|
|
13398
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
13399
|
+
state.seen.add(`perm:${request.id}`);
|
|
13400
|
+
if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
13401
|
+
onEvent({ ...request, decided: { decision: "allow" } });
|
|
13402
|
+
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
13403
|
+
});
|
|
13404
|
+
return;
|
|
13405
|
+
}
|
|
13406
|
+
this.openPermissions.set(request.id, session);
|
|
13407
|
+
onEvent(request);
|
|
13408
|
+
}
|
|
13409
|
+
};
|
|
13410
|
+
|
|
13411
|
+
// src/services/harness/OpenCodeAdapter.ts
|
|
12768
13412
|
var SERVER_READY_TIMEOUT_MS = 3e4;
|
|
12769
13413
|
var STDERR_TAIL_CHARS2 = 4e3;
|
|
12770
|
-
var
|
|
13414
|
+
var STREAM_CONNECT_TIMEOUT_MS2 = 5e3;
|
|
12771
13415
|
var RECOVERY_POLL_INTERVAL_MS = 2e3;
|
|
12772
13416
|
var RECOVERY_TIMEOUT_MS = 9e5;
|
|
12773
13417
|
var STATUS_POLL_INTERVAL_MS = 1e3;
|
|
@@ -12780,8 +13424,8 @@ var DISABLED_TOOLS = {
|
|
|
12780
13424
|
todowrite: false
|
|
12781
13425
|
};
|
|
12782
13426
|
var TASK_DISABLED_TOOLS = { question: false };
|
|
12783
|
-
var
|
|
12784
|
-
function
|
|
13427
|
+
var AUTO_APPROVALS2 = "auto";
|
|
13428
|
+
function splitModelId2(id) {
|
|
12785
13429
|
const slash = id.indexOf("/");
|
|
12786
13430
|
if (slash <= 0 || slash === id.length - 1) return null;
|
|
12787
13431
|
return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
|
|
@@ -12837,7 +13481,7 @@ function unwrapFileToolOutput(output) {
|
|
|
12837
13481
|
const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
|
|
12838
13482
|
return inner ? inner[1] : output;
|
|
12839
13483
|
}
|
|
12840
|
-
function
|
|
13484
|
+
function delay2(ms, signal) {
|
|
12841
13485
|
return new Promise((resolve7) => {
|
|
12842
13486
|
const timer = setTimeout(resolve7, ms);
|
|
12843
13487
|
timer.unref?.();
|
|
@@ -12847,10 +13491,10 @@ function delay(ms, signal) {
|
|
|
12847
13491
|
}, { once: true });
|
|
12848
13492
|
});
|
|
12849
13493
|
}
|
|
12850
|
-
function
|
|
13494
|
+
function newTurnState2() {
|
|
12851
13495
|
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
13496
|
}
|
|
12853
|
-
function
|
|
13497
|
+
function permissionReply2(decision) {
|
|
12854
13498
|
if (decision.decision === "allow") return { reply: "once" };
|
|
12855
13499
|
if (decision.decision === "allowForTask") return { reply: "always" };
|
|
12856
13500
|
const note = decision.note?.trim();
|
|
@@ -12887,6 +13531,8 @@ var OpenCodeAdapter = class {
|
|
|
12887
13531
|
interruptRequested = false;
|
|
12888
13532
|
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
12889
13533
|
openPermissions = /* @__PURE__ */ new Map();
|
|
13534
|
+
/** Set once the server turns out to speak the 2.x API, which then owns the session. */
|
|
13535
|
+
v2 = null;
|
|
12890
13536
|
async start(opts) {
|
|
12891
13537
|
if (opts.kind === "task") this.task = opts;
|
|
12892
13538
|
else this.planner = opts;
|
|
@@ -12901,14 +13547,14 @@ var OpenCodeAdapter = class {
|
|
|
12901
13547
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
12902
13548
|
}
|
|
12903
13549
|
const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
12904
|
-
const password =
|
|
13550
|
+
const password = randomBytes2(24).toString("base64url");
|
|
12905
13551
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
12906
|
-
env: runnerEnv(PATH,
|
|
13552
|
+
env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
|
|
12907
13553
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12908
13554
|
cwd: opts.cwd,
|
|
12909
13555
|
windowsVerbatimArguments: launch.verbatim
|
|
12910
13556
|
});
|
|
12911
|
-
|
|
13557
|
+
this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
|
|
12912
13558
|
this.process.stdin?.on("error", () => {
|
|
12913
13559
|
});
|
|
12914
13560
|
const banner = new Promise((resolve7) => {
|
|
@@ -12945,6 +13591,24 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
|
12945
13591
|
|
|
12946
13592
|
${this.stderrTail.trim()}` : ""}`);
|
|
12947
13593
|
}
|
|
13594
|
+
if (await this.speaksV2()) {
|
|
13595
|
+
this.v2 = new OpenCodeV2({
|
|
13596
|
+
request: (method, path26, body, signal) => this.request(method, path26, body, signal),
|
|
13597
|
+
json: (method, path26, body, signal) => this.json(method, path26, body, signal),
|
|
13598
|
+
openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
|
|
13599
|
+
processEnded: this.processEnded,
|
|
13600
|
+
isExited: () => this.exited,
|
|
13601
|
+
exitMessage: () => this.exitMessage()
|
|
13602
|
+
}, opts, this.role());
|
|
13603
|
+
try {
|
|
13604
|
+
await this.v2.start();
|
|
13605
|
+
} catch (err) {
|
|
13606
|
+
this.dispose();
|
|
13607
|
+
throw err;
|
|
13608
|
+
}
|
|
13609
|
+
this.sessionId = this.v2.nativeSessionId();
|
|
13610
|
+
return;
|
|
13611
|
+
}
|
|
12948
13612
|
if (opts.resumeSessionId) {
|
|
12949
13613
|
const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
|
|
12950
13614
|
if (existing?.id) {
|
|
@@ -12968,12 +13632,17 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
12968
13632
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
12969
13633
|
return;
|
|
12970
13634
|
}
|
|
13635
|
+
if (this.v2) {
|
|
13636
|
+
await this.v2.send(message, onEvent, signal, onActivity);
|
|
13637
|
+
if (signal?.aborted) this.dispose();
|
|
13638
|
+
return;
|
|
13639
|
+
}
|
|
12971
13640
|
if (this.task) return this.sendTask(this.task, message, onEvent, signal, onActivity);
|
|
12972
|
-
const turn =
|
|
13641
|
+
const turn = newTurnState2();
|
|
12973
13642
|
this.turnHasText = false;
|
|
12974
13643
|
const closeStream = await this.openStream(turn, onEvent, onActivity);
|
|
12975
13644
|
try {
|
|
12976
|
-
const model = this.planner?.model ?
|
|
13645
|
+
const model = this.planner?.model ? splitModelId2(this.planner.model) : null;
|
|
12977
13646
|
const body = {
|
|
12978
13647
|
parts: [{ type: "text", text: message }],
|
|
12979
13648
|
// The read-only guarantee: OpenCode's own plan agent has no write tools.
|
|
@@ -13014,7 +13683,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13014
13683
|
* {@link recoverReply}, never applies.
|
|
13015
13684
|
*/
|
|
13016
13685
|
async sendTask(task, message, onEvent, signal, onActivity) {
|
|
13017
|
-
const state =
|
|
13686
|
+
const state = newTurnState2();
|
|
13018
13687
|
this.turnHasText = false;
|
|
13019
13688
|
this.interruptRequested = false;
|
|
13020
13689
|
let markDone = () => {
|
|
@@ -13030,7 +13699,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13030
13699
|
const closeStream = await this.openStream(state, onEvent, onActivity);
|
|
13031
13700
|
const poll = new AbortController();
|
|
13032
13701
|
try {
|
|
13033
|
-
const model = task.model ?
|
|
13702
|
+
const model = task.model ? splitModelId2(task.model) : null;
|
|
13034
13703
|
const body = {
|
|
13035
13704
|
parts: [{ type: "text", text: message }],
|
|
13036
13705
|
// The manifest's meaning of the task's mode (ADR-0001): for OpenCode a mode is an agent.
|
|
@@ -13110,7 +13779,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13110
13779
|
*/
|
|
13111
13780
|
async pollStatus(turn, signal) {
|
|
13112
13781
|
while (!turn.done && !signal.aborted) {
|
|
13113
|
-
await
|
|
13782
|
+
await delay2(STATUS_POLL_INTERVAL_MS, signal);
|
|
13114
13783
|
if (turn.done || signal.aborted) return;
|
|
13115
13784
|
const busy = await this.sessionBusy();
|
|
13116
13785
|
if (busy === true) turn.live = true;
|
|
@@ -13157,6 +13826,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13157
13826
|
*/
|
|
13158
13827
|
async interrupt(timeoutMs) {
|
|
13159
13828
|
if (!this.process || !this.sessionId || this.exited) return false;
|
|
13829
|
+
if (this.v2) return this.v2.interrupt(timeoutMs);
|
|
13160
13830
|
const turn = this.taskTurn;
|
|
13161
13831
|
this.interruptRequested = true;
|
|
13162
13832
|
const posted = await this.json("POST", `/session/${this.sessionId}/abort`).then(() => true, () => false);
|
|
@@ -13177,13 +13847,23 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13177
13847
|
void this.processEnded.then(() => listener(this.exitCode));
|
|
13178
13848
|
}
|
|
13179
13849
|
answerPermission(id, decision) {
|
|
13850
|
+
if (this.v2) return this.process ? this.v2.answerPermission(id, decision) : false;
|
|
13180
13851
|
const sessionId = this.openPermissions.get(id);
|
|
13181
13852
|
if (sessionId === void 0 || !this.process) return false;
|
|
13182
13853
|
this.openPermissions.delete(id);
|
|
13183
|
-
void this.replyPermission(id, sessionId,
|
|
13854
|
+
void this.replyPermission(id, sessionId, permissionReply2(decision)).catch(() => {
|
|
13184
13855
|
});
|
|
13185
13856
|
return true;
|
|
13186
13857
|
}
|
|
13858
|
+
/**
|
|
13859
|
+
* OpenCode 2.x replaced the 1.x HTTP surface rather than extending it, and
|
|
13860
|
+
* only 2.x answers `/api/info` with its version. A 1.x server — or one that
|
|
13861
|
+
* answers with anything else — keeps the 1.x protocol this class speaks.
|
|
13862
|
+
*/
|
|
13863
|
+
async speaksV2() {
|
|
13864
|
+
const info = await this.json("GET", "/api/info").catch(() => null);
|
|
13865
|
+
return typeof info === "object" && info !== null && typeof info.version === "string";
|
|
13866
|
+
}
|
|
13187
13867
|
role() {
|
|
13188
13868
|
return this.task ? "task" : "planner";
|
|
13189
13869
|
}
|
|
@@ -13234,7 +13914,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13234
13914
|
if (pending?.info?.time?.completed || pending?.info?.error) return pending;
|
|
13235
13915
|
if (pending) onActivity?.();
|
|
13236
13916
|
if (Date.now() >= deadline) return null;
|
|
13237
|
-
await
|
|
13917
|
+
await delay2(RECOVERY_POLL_INTERVAL_MS, signal);
|
|
13238
13918
|
}
|
|
13239
13919
|
}
|
|
13240
13920
|
newestAssistantId(messages) {
|
|
@@ -13447,7 +14127,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13447
14127
|
...ask.always?.length ? { suggestions: ask.always } : {},
|
|
13448
14128
|
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
|
|
13449
14129
|
};
|
|
13450
|
-
if (this.task?.flags.modeSettings.approvals ===
|
|
14130
|
+
if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
13451
14131
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
13452
14132
|
void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
|
|
13453
14133
|
});
|
|
@@ -13481,7 +14161,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
13481
14161
|
});
|
|
13482
14162
|
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, turn, onEvent), connected, onActivity);
|
|
13483
14163
|
await Promise.race([streamReady, new Promise((r) => {
|
|
13484
|
-
const t = setTimeout(r,
|
|
14164
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS2);
|
|
13485
14165
|
t.unref?.();
|
|
13486
14166
|
})]);
|
|
13487
14167
|
return async () => {
|
|
@@ -13670,6 +14350,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13670
14350
|
observers = [];
|
|
13671
14351
|
tddEnabled;
|
|
13672
14352
|
readRunnerTransport;
|
|
14353
|
+
previousAttemptFromLog;
|
|
13673
14354
|
/**
|
|
13674
14355
|
* The setting as the latest run copied it when it opened. Every spawn of
|
|
13675
14356
|
* that run uses this, so flipping the setting mid-run changes the next run
|
|
@@ -13689,6 +14370,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13689
14370
|
this.workspaceEnv = deps.workspaceEnv;
|
|
13690
14371
|
this.tddEnabled = deps.tddEnabled;
|
|
13691
14372
|
this.readRunnerTransport = deps.runnerTransport;
|
|
14373
|
+
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
13692
14374
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
13693
14375
|
this.verifier.onVerdict((taskId, verdict) => this.onVerdict(taskId, verdict));
|
|
13694
14376
|
this.verifier.onCheckpoint((taskId, summary) => {
|
|
@@ -13744,7 +14426,8 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13744
14426
|
workspaceRoot,
|
|
13745
14427
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
13746
14428
|
tddEnabled: options.tddEnabled ?? (() => false),
|
|
13747
|
-
runnerTransport: options.runnerTransport ?? (() => "terminal")
|
|
14429
|
+
runnerTransport: options.runnerTransport ?? (() => "terminal"),
|
|
14430
|
+
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null)
|
|
13748
14431
|
});
|
|
13749
14432
|
return orchestrator;
|
|
13750
14433
|
}
|
|
@@ -13752,6 +14435,18 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
13752
14435
|
getIdleSince(taskId) {
|
|
13753
14436
|
return this.verifier.getIdleSince(taskId);
|
|
13754
14437
|
}
|
|
14438
|
+
/**
|
|
14439
|
+
* What an ops retry is told about the attempt before. The saved log is read
|
|
14440
|
+
* whether or not the session was reloaded, so both behave alike; the terminal
|
|
14441
|
+
* buffer is the fallback for a runner that has no log. A reload clears the
|
|
14442
|
+
* attempt count along with the buffer, so a retry made since is what says
|
|
14443
|
+
* there was an attempt to report on.
|
|
14444
|
+
*/
|
|
14445
|
+
opsPreviousAttempt(taskId) {
|
|
14446
|
+
const saved = this.previousAttemptFromLog(taskId) ?? this.output.liveTail(taskId, { maxLines: OPS_RETRY_TAIL_LINES })?.text;
|
|
14447
|
+
if (saved !== void 0) return saved;
|
|
14448
|
+
return (this.retryCounts.get(taskId) ?? 0) > 0 ? "Its output is not available: this session was reloaded since it ran." : void 0;
|
|
14449
|
+
}
|
|
13755
14450
|
/** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
|
|
13756
14451
|
getLiveOutput(taskId, opts) {
|
|
13757
14452
|
return this.output.liveTail(taskId, opts);
|
|
@@ -14609,7 +15304,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
14609
15304
|
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
14610
15305
|
// An ops task's effects outlive a failed attempt and are never rolled
|
|
14611
15306
|
// back, so the next one is told what the last one did (ADR-0020).
|
|
14612
|
-
previousAttempt: attempt.ops ? this.
|
|
15307
|
+
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
|
|
14613
15308
|
});
|
|
14614
15309
|
this.lingering.close(task.id);
|
|
14615
15310
|
const env = await this.envForTask(cwd);
|
|
@@ -18228,6 +18923,13 @@ var PlanEditError = class extends Error {
|
|
|
18228
18923
|
this.name = "PlanEditError";
|
|
18229
18924
|
}
|
|
18230
18925
|
};
|
|
18926
|
+
var OPS_RETRY_DIGEST_CALLS = 20;
|
|
18927
|
+
function lastAttemptDigest(location, taskId) {
|
|
18928
|
+
const attempt = listTaskLogAttempts(location, taskId).pop();
|
|
18929
|
+
if (attempt === void 0) return null;
|
|
18930
|
+
const events = readTaskLog(location, taskId, attempt);
|
|
18931
|
+
return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
|
|
18932
|
+
}
|
|
18231
18933
|
function sessionRuntimeSettings(settings) {
|
|
18232
18934
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
|
|
18233
18935
|
}
|
|
@@ -18312,7 +19014,8 @@ function createSession(deps) {
|
|
|
18312
19014
|
registry: deps.registry,
|
|
18313
19015
|
workspaceRoot: deps.workspaceRoot,
|
|
18314
19016
|
tddEnabled: () => deps.settings().tddEnabled,
|
|
18315
|
-
runnerTransport: () => deps.settings().runnerTransport ?? "terminal"
|
|
19017
|
+
runnerTransport: () => deps.settings().runnerTransport ?? "terminal",
|
|
19018
|
+
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId)
|
|
18316
19019
|
});
|
|
18317
19020
|
const usage = new PlannerUsageLedger();
|
|
18318
19021
|
const events = new SessionEventRelay({
|
|
@@ -21475,6 +22178,7 @@ export {
|
|
|
21475
22178
|
FsPluginStore,
|
|
21476
22179
|
GIT_READONLY_SUBCOMMANDS,
|
|
21477
22180
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
22181
|
+
GREP_PCRE_PROBE_ARGS,
|
|
21478
22182
|
GeminiService,
|
|
21479
22183
|
HeadlessRunner,
|
|
21480
22184
|
HeadlessSession,
|
|
@@ -21614,6 +22318,7 @@ export {
|
|
|
21614
22318
|
dependencyCandidates,
|
|
21615
22319
|
dependentsOf,
|
|
21616
22320
|
describeMergeResult,
|
|
22321
|
+
digestTaskLog,
|
|
21617
22322
|
discoverGeminiModels,
|
|
21618
22323
|
drainNext,
|
|
21619
22324
|
effectiveAllowlist,
|
|
@@ -21646,6 +22351,7 @@ export {
|
|
|
21646
22351
|
getStateDir,
|
|
21647
22352
|
globalDataDir,
|
|
21648
22353
|
grantScopeFor,
|
|
22354
|
+
grepRegexFromProbe,
|
|
21649
22355
|
hasHiddenDetail,
|
|
21650
22356
|
hasTmux,
|
|
21651
22357
|
holdPrompt,
|