@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.js
CHANGED
|
@@ -67,6 +67,7 @@ __export(index_exports, {
|
|
|
67
67
|
FsPluginStore: () => FsPluginStore,
|
|
68
68
|
GIT_READONLY_SUBCOMMANDS: () => GIT_READONLY_SUBCOMMANDS,
|
|
69
69
|
GREP_DEFAULT_HEAD_LIMIT: () => GREP_DEFAULT_HEAD_LIMIT,
|
|
70
|
+
GREP_PCRE_PROBE_ARGS: () => GREP_PCRE_PROBE_ARGS,
|
|
70
71
|
GeminiService: () => GeminiService,
|
|
71
72
|
HeadlessRunner: () => HeadlessRunner,
|
|
72
73
|
HeadlessSession: () => HeadlessSession,
|
|
@@ -206,6 +207,7 @@ __export(index_exports, {
|
|
|
206
207
|
dependencyCandidates: () => dependencyCandidates,
|
|
207
208
|
dependentsOf: () => dependentsOf,
|
|
208
209
|
describeMergeResult: () => describeMergeResult,
|
|
210
|
+
digestTaskLog: () => digestTaskLog,
|
|
209
211
|
discoverGeminiModels: () => discoverGeminiModels,
|
|
210
212
|
drainNext: () => drainNext,
|
|
211
213
|
effectiveAllowlist: () => effectiveAllowlist,
|
|
@@ -238,6 +240,7 @@ __export(index_exports, {
|
|
|
238
240
|
getStateDir: () => getStateDir,
|
|
239
241
|
globalDataDir: () => globalDataDir,
|
|
240
242
|
grantScopeFor: () => grantScopeFor,
|
|
243
|
+
grepRegexFromProbe: () => grepRegexFromProbe,
|
|
241
244
|
hasHiddenDetail: () => hasHiddenDetail,
|
|
242
245
|
hasTmux: () => hasTmux,
|
|
243
246
|
holdPrompt: () => holdPrompt,
|
|
@@ -766,6 +769,8 @@ function plannerContextFill(usage) {
|
|
|
766
769
|
var HEAD_LINES = 60;
|
|
767
770
|
var TAIL_LINES = 40;
|
|
768
771
|
var MAX_LINE_CHARS = 2e3;
|
|
772
|
+
var DIGEST_ARGS_CHARS = 120;
|
|
773
|
+
var DIGEST_MESSAGE_CHARS = 1e3;
|
|
769
774
|
function capLine(line) {
|
|
770
775
|
return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
771
776
|
}
|
|
@@ -848,6 +853,33 @@ function coalesceTaskLog(events) {
|
|
|
848
853
|
}
|
|
849
854
|
return out;
|
|
850
855
|
}
|
|
856
|
+
function digestTaskLog(events, maxCalls) {
|
|
857
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
858
|
+
let lastText;
|
|
859
|
+
let lastError;
|
|
860
|
+
for (const event of events) {
|
|
861
|
+
if (event.type === "tool_result") outcomes.set(event.id, event.success);
|
|
862
|
+
else if (event.type === "text") lastText = event.text;
|
|
863
|
+
else if (event.type === "error") lastError = event.message;
|
|
864
|
+
}
|
|
865
|
+
const lines = [];
|
|
866
|
+
for (const event of events) {
|
|
867
|
+
if (event.type !== "tool_call") continue;
|
|
868
|
+
const outcome = outcomes.get(event.id);
|
|
869
|
+
const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
|
|
870
|
+
lines.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
|
|
871
|
+
}
|
|
872
|
+
const omitted = lines.length - maxCalls;
|
|
873
|
+
const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines.slice(omitted)] : lines;
|
|
874
|
+
const gap = () => out.length > 0 ? [""] : [];
|
|
875
|
+
const said2 = lastText?.trim();
|
|
876
|
+
if (said2) {
|
|
877
|
+
const end = said2.length > DIGEST_MESSAGE_CHARS ? `\u2026${said2.slice(-DIGEST_MESSAGE_CHARS)}` : said2;
|
|
878
|
+
out.push(...gap(), "Its last message:", ...end.split("\n").map((l) => ` ${l}`));
|
|
879
|
+
}
|
|
880
|
+
if (lastError) out.push(...gap(), `It stopped with an error: ${lastError}`);
|
|
881
|
+
return out.join("\n");
|
|
882
|
+
}
|
|
851
883
|
|
|
852
884
|
// src/interfaces/IFileSystem.ts
|
|
853
885
|
var GREP_DEFAULT_HEAD_LIMIT = 100;
|
|
@@ -3085,6 +3117,8 @@ function lex(command, nested, dialect) {
|
|
|
3085
3117
|
let unsafeRedirect;
|
|
3086
3118
|
let processSubstitution = false;
|
|
3087
3119
|
let unbalanced = false;
|
|
3120
|
+
let grouped = false;
|
|
3121
|
+
let joinedBy;
|
|
3088
3122
|
let tokens = [];
|
|
3089
3123
|
let expandable = false;
|
|
3090
3124
|
let current = "";
|
|
@@ -3114,13 +3148,14 @@ function lex(command, nested, dialect) {
|
|
|
3114
3148
|
current = "";
|
|
3115
3149
|
started = false;
|
|
3116
3150
|
};
|
|
3117
|
-
const endSegment = (nextPiped) => {
|
|
3151
|
+
const endSegment = (nextPiped, next = "other") => {
|
|
3118
3152
|
endToken();
|
|
3119
|
-
if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs });
|
|
3153
|
+
if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs, joinedBy });
|
|
3120
3154
|
tokens = [];
|
|
3121
3155
|
inputs = [];
|
|
3122
3156
|
expandable = false;
|
|
3123
3157
|
piped = nextPiped;
|
|
3158
|
+
joinedBy = next;
|
|
3124
3159
|
};
|
|
3125
3160
|
let i = 0;
|
|
3126
3161
|
while (i < command.length) {
|
|
@@ -3242,17 +3277,18 @@ function lex(command, nested, dialect) {
|
|
|
3242
3277
|
}
|
|
3243
3278
|
if (c === "(" || c === ")") {
|
|
3244
3279
|
endToken();
|
|
3280
|
+
grouped = true;
|
|
3245
3281
|
i++;
|
|
3246
3282
|
continue;
|
|
3247
3283
|
}
|
|
3248
3284
|
if (c === "|") {
|
|
3249
3285
|
const double = command[i + 1] === "|";
|
|
3250
|
-
endSegment(!double);
|
|
3286
|
+
endSegment(!double, double ? "other" : "pipe");
|
|
3251
3287
|
i += double || command[i + 1] === "&" ? 2 : 1;
|
|
3252
3288
|
continue;
|
|
3253
3289
|
}
|
|
3254
3290
|
if (c === "&" || c === ";" || c === "\n") {
|
|
3255
|
-
endSegment(false);
|
|
3291
|
+
endSegment(false, c === "&" && command[i + 1] === "&" ? "and" : "other");
|
|
3256
3292
|
i += command[i + 1] === c ? 2 : 1;
|
|
3257
3293
|
continue;
|
|
3258
3294
|
}
|
|
@@ -3270,7 +3306,7 @@ function lex(command, nested, dialect) {
|
|
|
3270
3306
|
}
|
|
3271
3307
|
if (quote !== "") unbalanced = true;
|
|
3272
3308
|
endSegment(false);
|
|
3273
|
-
return { segments, unsafeRedirect, processSubstitution, unbalanced };
|
|
3309
|
+
return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped };
|
|
3274
3310
|
}
|
|
3275
3311
|
function binaryName(token, dialect) {
|
|
3276
3312
|
const base = token.split(/[/\\]/).pop() ?? "";
|
|
@@ -3410,10 +3446,11 @@ function unwrap(seg, dialect) {
|
|
|
3410
3446
|
}
|
|
3411
3447
|
return { seg: { ...current, assignments }, wrappers, runner };
|
|
3412
3448
|
}
|
|
3413
|
-
function lexAll(command, dialect) {
|
|
3449
|
+
function lexAll(command, dialect, cdpathSet = false) {
|
|
3414
3450
|
const queue = [];
|
|
3415
3451
|
const top = lex(command, queue, dialect);
|
|
3416
3452
|
const all = { ...top, segments: top.segments.filter((s) => s.binary) };
|
|
3453
|
+
if (!top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
|
|
3417
3454
|
for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
|
|
3418
3455
|
const inner = lex(queue.shift(), queue, dialect);
|
|
3419
3456
|
all.segments.push(...inner.segments.filter((s) => s.binary));
|
|
@@ -3423,15 +3460,32 @@ function lexAll(command, dialect) {
|
|
|
3423
3460
|
}
|
|
3424
3461
|
return all;
|
|
3425
3462
|
}
|
|
3463
|
+
function literalCdTarget(seg) {
|
|
3464
|
+
if (seg.binary !== "cd" || seg.assignments.length > 0 || seg.expandable || seg.inputs.length > 0) return void 0;
|
|
3465
|
+
if (seg.args.length !== 1) return void 0;
|
|
3466
|
+
const [target] = seg.args;
|
|
3467
|
+
return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
|
|
3468
|
+
}
|
|
3469
|
+
function followCd(segments) {
|
|
3470
|
+
if (segments.some((s) => s.joinedBy === "other")) return;
|
|
3471
|
+
let chain = [];
|
|
3472
|
+
segments.forEach((seg, i) => {
|
|
3473
|
+
seg.cwd = chain;
|
|
3474
|
+
const inPipeline = seg.joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
|
|
3475
|
+
const target = inPipeline ? void 0 : literalCdTarget(seg);
|
|
3476
|
+
if (target !== void 0) chain = [...chain, target];
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3426
3479
|
function looksLikePath(arg) {
|
|
3427
3480
|
if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
|
|
3428
3481
|
return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
|
|
3429
3482
|
}
|
|
3430
|
-
function
|
|
3483
|
+
function pathRefs(command, opts = {}) {
|
|
3431
3484
|
const dialect = dialectFor(opts.dialect);
|
|
3432
|
-
return lexAll(command, dialect).segments.flatMap((seg) => {
|
|
3485
|
+
return lexAll(command, dialect, opts.cdpathSet).segments.flatMap((seg) => {
|
|
3433
3486
|
const patterns = patternArgs(seg);
|
|
3434
|
-
|
|
3487
|
+
const cwd = seg.cwd ?? [];
|
|
3488
|
+
return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path27) => ({ path: path27, cwd }));
|
|
3435
3489
|
});
|
|
3436
3490
|
}
|
|
3437
3491
|
function pathIn(a) {
|
|
@@ -3595,7 +3649,8 @@ function scopeFor(seg) {
|
|
|
3595
3649
|
}
|
|
3596
3650
|
return [seg.binary, ...lead].join(" ");
|
|
3597
3651
|
}
|
|
3598
|
-
function isAuto(seg) {
|
|
3652
|
+
function isAuto(seg, opts) {
|
|
3653
|
+
if (seg.binary === "cd") return !opts.cdpathSet && literalCdTarget(seg) !== void 0;
|
|
3599
3654
|
if (!AUTO_COMMANDS.includes(seg.binary)) return false;
|
|
3600
3655
|
if (seg.expandable) return false;
|
|
3601
3656
|
if (seg.binary !== "git") return true;
|
|
@@ -3690,7 +3745,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3690
3745
|
const trimmed = command.trim();
|
|
3691
3746
|
if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
|
|
3692
3747
|
const dialect = dialectFor(opts.dialect);
|
|
3693
|
-
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
|
|
3748
|
+
const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect, opts.cdpathSet);
|
|
3694
3749
|
if (unbalanced) {
|
|
3695
3750
|
return {
|
|
3696
3751
|
tier: "refuse",
|
|
@@ -3727,7 +3782,7 @@ function classifyCommand(command, opts = {}) {
|
|
|
3727
3782
|
};
|
|
3728
3783
|
}
|
|
3729
3784
|
}
|
|
3730
|
-
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg));
|
|
3785
|
+
const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
|
|
3731
3786
|
if (nonAuto.length === 0) return { tier: "auto", scope: "" };
|
|
3732
3787
|
const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
|
|
3733
3788
|
return { tier: "ask", scope };
|
|
@@ -3807,11 +3862,15 @@ function expandHome(target) {
|
|
|
3807
3862
|
if (target === "~" || target.startsWith("~/")) return path2.join(home, target.slice(1));
|
|
3808
3863
|
return path2.join(path2.dirname(home), target.slice(1));
|
|
3809
3864
|
}
|
|
3810
|
-
function resolveWithin(root, target) {
|
|
3811
|
-
const abs = path2.resolve(
|
|
3865
|
+
function resolveWithin(root, target, from = root) {
|
|
3866
|
+
const abs = path2.resolve(from, expandHome(target));
|
|
3812
3867
|
const rel = path2.relative(root, abs);
|
|
3813
3868
|
return { abs, inside: rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel) };
|
|
3814
3869
|
}
|
|
3870
|
+
function resolveRef(root, ref) {
|
|
3871
|
+
const from = ref.cwd.reduce((dir, target) => resolveWithin(root, target, dir).abs, root);
|
|
3872
|
+
return resolveWithin(root, ref.path, from);
|
|
3873
|
+
}
|
|
3815
3874
|
function grantScopeFor(abs, kind) {
|
|
3816
3875
|
if (kind === "directory") return path2.join(abs, "*");
|
|
3817
3876
|
const parent = path2.dirname(abs);
|
|
@@ -4017,14 +4076,17 @@ ${refs.output.trim()}`);
|
|
|
4017
4076
|
outsidePaths(command) {
|
|
4018
4077
|
const root = this.getWorkspaceRoot();
|
|
4019
4078
|
const found = /* @__PURE__ */ new Map();
|
|
4020
|
-
for (const
|
|
4021
|
-
const { abs, inside } =
|
|
4079
|
+
for (const ref of pathRefs(command, this.policyOptions())) {
|
|
4080
|
+
const { abs, inside } = resolveRef(root, ref);
|
|
4022
4081
|
if (inside || isInertDevice(abs)) continue;
|
|
4023
4082
|
const scope = grantScopeFor(abs, "file");
|
|
4024
4083
|
if (!found.has(scope)) found.set(scope, abs);
|
|
4025
4084
|
}
|
|
4026
4085
|
return [...found].map(([scope, abs]) => ({ abs, scope }));
|
|
4027
4086
|
}
|
|
4087
|
+
policyOptions() {
|
|
4088
|
+
return { dialect: this.researchShell.dialect, cdpathSet: !!process.env.CDPATH };
|
|
4089
|
+
}
|
|
4028
4090
|
/**
|
|
4029
4091
|
* Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
|
|
4030
4092
|
* anything else asks once and is remembered, and writes are refused outright
|
|
@@ -4036,7 +4098,7 @@ ${refs.output.trim()}`);
|
|
|
4036
4098
|
* directory the next command names.
|
|
4037
4099
|
*/
|
|
4038
4100
|
async bash(command, signal) {
|
|
4039
|
-
const { tier, scope, reason } = classifyCommand(command,
|
|
4101
|
+
const { tier, scope, reason } = classifyCommand(command, this.policyOptions());
|
|
4040
4102
|
if (tier === "refuse") {
|
|
4041
4103
|
return { success: false, output: `Command refused: ${reason}`, truncated: false };
|
|
4042
4104
|
}
|
|
@@ -5212,10 +5274,78 @@ function buildGrepArgs(pattern, opts, root) {
|
|
|
5212
5274
|
function buildGlobArgs(pattern, root) {
|
|
5213
5275
|
return ["--files", ...SORT_BY_RECENCY, "--glob", pattern, ...exclusionArgs(), "--", root];
|
|
5214
5276
|
}
|
|
5215
|
-
|
|
5277
|
+
var GREP_PCRE_PROBE_ARGS = ["-P", "-e", "x", "/dev/null"];
|
|
5278
|
+
function grepRegexFromProbe(exitCode) {
|
|
5279
|
+
return exitCode === 0 || exitCode === 1 ? "pcre" : "ere";
|
|
5280
|
+
}
|
|
5281
|
+
function toPosixClasses(pattern) {
|
|
5282
|
+
const CLASS_ESCAPES = { s: "[:space:]", w: "[:alnum:]_", d: "[:digit:]", t: " " };
|
|
5283
|
+
let out = "";
|
|
5284
|
+
let i = 0;
|
|
5285
|
+
while (i < pattern.length) {
|
|
5286
|
+
const c = pattern[i];
|
|
5287
|
+
if (c === "\\") {
|
|
5288
|
+
out += pattern.slice(i, i + 2);
|
|
5289
|
+
i += 2;
|
|
5290
|
+
continue;
|
|
5291
|
+
}
|
|
5292
|
+
if (c !== "[") {
|
|
5293
|
+
out += c;
|
|
5294
|
+
i++;
|
|
5295
|
+
continue;
|
|
5296
|
+
}
|
|
5297
|
+
const start = i;
|
|
5298
|
+
i++;
|
|
5299
|
+
let negate = false;
|
|
5300
|
+
if (pattern[i] === "^") {
|
|
5301
|
+
negate = true;
|
|
5302
|
+
i++;
|
|
5303
|
+
}
|
|
5304
|
+
let body = "";
|
|
5305
|
+
let closeBracket = false;
|
|
5306
|
+
let openBracket = false;
|
|
5307
|
+
let dash = false;
|
|
5308
|
+
let closed = false;
|
|
5309
|
+
while (i < pattern.length) {
|
|
5310
|
+
const ch = pattern[i];
|
|
5311
|
+
if (ch === "]") {
|
|
5312
|
+
closed = true;
|
|
5313
|
+
i++;
|
|
5314
|
+
break;
|
|
5315
|
+
}
|
|
5316
|
+
if (ch === "[" && pattern[i + 1] === ":") {
|
|
5317
|
+
const end = pattern.indexOf(":]", i + 2);
|
|
5318
|
+
if (end !== -1) {
|
|
5319
|
+
body += pattern.slice(i, end + 2);
|
|
5320
|
+
i = end + 2;
|
|
5321
|
+
continue;
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
5325
|
+
const e = pattern[i + 1];
|
|
5326
|
+
i += 2;
|
|
5327
|
+
if (e in CLASS_ESCAPES) body += CLASS_ESCAPES[e];
|
|
5328
|
+
else if (e === "]") closeBracket = true;
|
|
5329
|
+
else if (e === "[") openBracket = true;
|
|
5330
|
+
else if (e === "-") dash = true;
|
|
5331
|
+
else body += e;
|
|
5332
|
+
continue;
|
|
5333
|
+
}
|
|
5334
|
+
body += ch;
|
|
5335
|
+
i++;
|
|
5336
|
+
}
|
|
5337
|
+
if (!closed) {
|
|
5338
|
+
out += pattern.slice(start);
|
|
5339
|
+
break;
|
|
5340
|
+
}
|
|
5341
|
+
out += `[${negate ? "^" : ""}${closeBracket ? "]" : ""}${body}${openBracket ? "[" : ""}${dash ? "-" : ""}]`;
|
|
5342
|
+
}
|
|
5343
|
+
return out;
|
|
5344
|
+
}
|
|
5345
|
+
function buildFallbackGrepArgs(pattern, opts, root, regex = "pcre") {
|
|
5216
5346
|
const args = ["-r", "-n"];
|
|
5217
5347
|
if (opts.literal) args.push("-F");
|
|
5218
|
-
else args.push("-P");
|
|
5348
|
+
else args.push(regex === "pcre" ? "-P" : "-E");
|
|
5219
5349
|
if (opts.caseInsensitive) args.push("-i");
|
|
5220
5350
|
if (opts.outputMode === "files") args.push("-l");
|
|
5221
5351
|
if (opts.outputMode === "count") args.push("-c");
|
|
@@ -5225,7 +5355,7 @@ function buildFallbackGrepArgs(pattern, opts, root) {
|
|
|
5225
5355
|
}
|
|
5226
5356
|
if (opts.include) args.push(`--include=${opts.include}`);
|
|
5227
5357
|
for (const dir of SEARCH_EXCLUSIONS) args.push(`--exclude-dir=${dir}`);
|
|
5228
|
-
args.push("-e", pattern, root);
|
|
5358
|
+
args.push("-e", !opts.literal && regex === "ere" ? toPosixClasses(pattern) : pattern, root);
|
|
5229
5359
|
return args;
|
|
5230
5360
|
}
|
|
5231
5361
|
function globSegmentToRegex(segment) {
|
|
@@ -9264,7 +9394,7 @@ function parseTaskOpsObject(json, text2) {
|
|
|
9264
9394
|
}
|
|
9265
9395
|
return ops;
|
|
9266
9396
|
}
|
|
9267
|
-
function
|
|
9397
|
+
function resolveRef2(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
|
|
9268
9398
|
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
9269
9399
|
const s = String(ref).trim();
|
|
9270
9400
|
const ownerIdx = handleOwner.get(s);
|
|
@@ -9477,7 +9607,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9477
9607
|
const bad = [];
|
|
9478
9608
|
const liveFlat = flattenTasks(tasks);
|
|
9479
9609
|
for (const d of Array.isArray(deps) ? deps : []) {
|
|
9480
|
-
const r =
|
|
9610
|
+
const r = resolveRef2(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
|
|
9481
9611
|
if ("error" in r) {
|
|
9482
9612
|
bad.push(`${String(d)} (${r.error})`);
|
|
9483
9613
|
continue;
|
|
@@ -9494,7 +9624,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9494
9624
|
const label = `op ${i + 1} (${op.op})`;
|
|
9495
9625
|
switch (op.op) {
|
|
9496
9626
|
case "update": {
|
|
9497
|
-
const ref =
|
|
9627
|
+
const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
9498
9628
|
if ("error" in ref) {
|
|
9499
9629
|
errors.push(`${label}: ${ref.error}`);
|
|
9500
9630
|
break;
|
|
@@ -9565,7 +9695,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9565
9695
|
break;
|
|
9566
9696
|
}
|
|
9567
9697
|
case "remove": {
|
|
9568
|
-
const ref =
|
|
9698
|
+
const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
9569
9699
|
if ("error" in ref) {
|
|
9570
9700
|
errors.push(`${label}: ${ref.error}`);
|
|
9571
9701
|
break;
|
|
@@ -9588,7 +9718,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9588
9718
|
break;
|
|
9589
9719
|
}
|
|
9590
9720
|
case "reorder": {
|
|
9591
|
-
const refs = (op.taskIds ?? []).map((r) =>
|
|
9721
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef2(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
9592
9722
|
const badRef = refs.find((r) => "error" in r);
|
|
9593
9723
|
if (badRef) {
|
|
9594
9724
|
errors.push(`${label}: ${badRef.error}`);
|
|
@@ -9610,7 +9740,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9610
9740
|
break;
|
|
9611
9741
|
}
|
|
9612
9742
|
case "merge": {
|
|
9613
|
-
const refs = (op.taskIds ?? []).map((r) =>
|
|
9743
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef2(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
9614
9744
|
const badRef = refs.find((r) => "error" in r);
|
|
9615
9745
|
if (badRef) {
|
|
9616
9746
|
errors.push(`${label}: ${badRef.error}`);
|
|
@@ -9680,7 +9810,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9680
9810
|
break;
|
|
9681
9811
|
}
|
|
9682
9812
|
case "split": {
|
|
9683
|
-
const ref =
|
|
9813
|
+
const ref = resolveRef2(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
9684
9814
|
if ("error" in ref) {
|
|
9685
9815
|
errors.push(`${label}: ${ref.error}`);
|
|
9686
9816
|
break;
|
|
@@ -9759,7 +9889,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9759
9889
|
break;
|
|
9760
9890
|
}
|
|
9761
9891
|
case "rearm": {
|
|
9762
|
-
const ref =
|
|
9892
|
+
const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
9763
9893
|
if ("error" in ref) {
|
|
9764
9894
|
errors.push(`${label}: ${ref.error}`);
|
|
9765
9895
|
break;
|
|
@@ -10403,14 +10533,15 @@ function nonPromptingFs(fs16) {
|
|
|
10403
10533
|
listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
10404
10534
|
bash: async (command, signal) => {
|
|
10405
10535
|
const dialect = resolveResearchShell().dialect;
|
|
10406
|
-
const
|
|
10536
|
+
const policy = { dialect, cdpathSet: !!process.env.CDPATH };
|
|
10537
|
+
const { tier, reason } = classifyCommand(command, policy);
|
|
10407
10538
|
if (tier !== "auto") {
|
|
10408
10539
|
return refused2(
|
|
10409
10540
|
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.`
|
|
10410
10541
|
);
|
|
10411
10542
|
}
|
|
10412
|
-
const escaping =
|
|
10413
|
-
if (escaping) return outsideRefusal(escaping);
|
|
10543
|
+
const escaping = pathRefs(command, policy).find((ref) => !resolveRef(fs16.getWorkspaceRoot(), ref).inside);
|
|
10544
|
+
if (escaping) return outsideRefusal(escaping.path);
|
|
10414
10545
|
return fs16.bash(command, signal);
|
|
10415
10546
|
}
|
|
10416
10547
|
};
|
|
@@ -11724,6 +11855,12 @@ var VerdictEngine = class {
|
|
|
11724
11855
|
idleSince = /* @__PURE__ */ new Map();
|
|
11725
11856
|
/** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
|
|
11726
11857
|
idlePaused = /* @__PURE__ */ new Set();
|
|
11858
|
+
/**
|
|
11859
|
+
* Open tool approvals per structured task. They arrive mid-turn and leave the
|
|
11860
|
+
* status alone (ADR-0018, W1), but a task waiting on one is waiting on the
|
|
11861
|
+
* user, so it is not idle either.
|
|
11862
|
+
*/
|
|
11863
|
+
openApprovals = /* @__PURE__ */ new Map();
|
|
11727
11864
|
onVerdict(listener) {
|
|
11728
11865
|
this.listeners.push(listener);
|
|
11729
11866
|
}
|
|
@@ -11752,7 +11889,7 @@ var VerdictEngine = class {
|
|
|
11752
11889
|
}
|
|
11753
11890
|
/** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
|
|
11754
11891
|
touchIdle(taskId, gen) {
|
|
11755
|
-
if (this.idlePaused.has(taskId)) return;
|
|
11892
|
+
if (this.idlePaused.has(taskId) || this.openApprovals.get(taskId)?.size) return;
|
|
11756
11893
|
const existing = this.idleTimers.get(taskId);
|
|
11757
11894
|
if (existing) clearTimeout(existing);
|
|
11758
11895
|
if (this.idleSince.get(taskId)) {
|
|
@@ -11834,7 +11971,10 @@ ${line}
|
|
|
11834
11971
|
});
|
|
11835
11972
|
if (isStructuredSession(session)) {
|
|
11836
11973
|
session.onEvent((event) => {
|
|
11837
|
-
if (
|
|
11974
|
+
if (this.generations.get(task.id) !== gen) return;
|
|
11975
|
+
if (event.type === "turn_start") this.resumeIdle(task.id);
|
|
11976
|
+
else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
|
|
11977
|
+
else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
|
|
11838
11978
|
});
|
|
11839
11979
|
}
|
|
11840
11980
|
session.onExit((exitCode) => {
|
|
@@ -11845,6 +11985,18 @@ ${line}
|
|
|
11845
11985
|
for (const l of this.listeners) l(task.id, verdict);
|
|
11846
11986
|
});
|
|
11847
11987
|
}
|
|
11988
|
+
approvalOpened(taskId, approvalId) {
|
|
11989
|
+
const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
11990
|
+
open.add(approvalId);
|
|
11991
|
+
this.openApprovals.set(taskId, open);
|
|
11992
|
+
this.clearIdle(taskId);
|
|
11993
|
+
}
|
|
11994
|
+
approvalClosed(taskId, approvalId, gen) {
|
|
11995
|
+
const open = this.openApprovals.get(taskId);
|
|
11996
|
+
if (!open?.delete(approvalId) || open.size) return;
|
|
11997
|
+
this.openApprovals.delete(taskId);
|
|
11998
|
+
this.touchIdle(taskId, gen);
|
|
11999
|
+
}
|
|
11848
12000
|
/** Scan only the new text plus the unmatched carry, so a long run stays linear. */
|
|
11849
12001
|
scanCheckpoints(taskId, session, text2) {
|
|
11850
12002
|
const scan = (this.checkpointCarry.get(taskId) ?? "") + text2;
|
|
@@ -11866,6 +12018,7 @@ ${line}
|
|
|
11866
12018
|
this.checkpointCarry.delete(taskId);
|
|
11867
12019
|
this.pausedSessions.delete(taskId);
|
|
11868
12020
|
this.idlePaused.delete(taskId);
|
|
12021
|
+
this.openApprovals.delete(taskId);
|
|
11869
12022
|
this.clearIdle(taskId);
|
|
11870
12023
|
}
|
|
11871
12024
|
/** Manual "Mark complete" override: a pass verdict that bypasses evidence. */
|
|
@@ -11903,6 +12056,7 @@ ${line}
|
|
|
11903
12056
|
this.idleTimers.clear();
|
|
11904
12057
|
this.idleSince.clear();
|
|
11905
12058
|
this.idlePaused.clear();
|
|
12059
|
+
this.openApprovals.clear();
|
|
11906
12060
|
this.generations.clear();
|
|
11907
12061
|
}
|
|
11908
12062
|
decide(task, exitCode) {
|
|
@@ -15566,9 +15720,502 @@ ${item.text}` : item.text });
|
|
|
15566
15720
|
|
|
15567
15721
|
// src/services/harness/OpenCodeAdapter.ts
|
|
15568
15722
|
var import_crypto2 = require("crypto");
|
|
15723
|
+
|
|
15724
|
+
// src/services/harness/OpenCodeV2.ts
|
|
15725
|
+
var STREAM_CONNECT_TIMEOUT_MS = 5e3;
|
|
15726
|
+
var ACTIVE_POLL_INTERVAL_MS = 1e3;
|
|
15727
|
+
var IDLE_POLLS_BEFORE_START = 3;
|
|
15728
|
+
var PLANNER_RULES = [
|
|
15729
|
+
{ action: "question", resource: "*", effect: "deny" },
|
|
15730
|
+
{ action: "edit", resource: "*", effect: "deny" }
|
|
15731
|
+
];
|
|
15732
|
+
var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
|
|
15733
|
+
var AUTO_APPROVALS = "auto";
|
|
15734
|
+
function newTurnState() {
|
|
15735
|
+
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() };
|
|
15736
|
+
}
|
|
15737
|
+
function splitModelId(id) {
|
|
15738
|
+
const slash = id.indexOf("/");
|
|
15739
|
+
if (slash <= 0 || slash === id.length - 1) return null;
|
|
15740
|
+
return { providerID: id.slice(0, slash), id: id.slice(slash + 1) };
|
|
15741
|
+
}
|
|
15742
|
+
function delay(ms, signal) {
|
|
15743
|
+
return new Promise((resolve7) => {
|
|
15744
|
+
const timer = setTimeout(resolve7, ms);
|
|
15745
|
+
timer.unref?.();
|
|
15746
|
+
signal?.addEventListener("abort", () => {
|
|
15747
|
+
clearTimeout(timer);
|
|
15748
|
+
resolve7();
|
|
15749
|
+
}, { once: true });
|
|
15750
|
+
});
|
|
15751
|
+
}
|
|
15752
|
+
function contentText(content) {
|
|
15753
|
+
return (content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
|
|
15754
|
+
}
|
|
15755
|
+
function subagentDigest(output) {
|
|
15756
|
+
const inner = output.match(/<subagent[^>]*>\n?([\s\S]*?)\n?<\/subagent>/);
|
|
15757
|
+
return inner ? inner[1] : output;
|
|
15758
|
+
}
|
|
15759
|
+
function permissionReply(decision) {
|
|
15760
|
+
if (decision.decision === "allow") return { decision: "once" };
|
|
15761
|
+
if (decision.decision === "allowForTask") return { decision: "always" };
|
|
15762
|
+
const note = decision.note?.trim();
|
|
15763
|
+
return note ? { decision: "reject", message: note } : { decision: "reject" };
|
|
15764
|
+
}
|
|
15765
|
+
var OpenCodeV2 = class {
|
|
15766
|
+
constructor(host, opts, role) {
|
|
15767
|
+
this.host = host;
|
|
15768
|
+
this.opts = opts;
|
|
15769
|
+
this.role = role;
|
|
15770
|
+
this.agent = opts.kind === "task" ? opts.flags.permissionMode : "plan";
|
|
15771
|
+
const split = opts.model ? splitModelId(opts.model) : null;
|
|
15772
|
+
const variant = opts.kind === "task" ? opts.flags.effort : opts.effort;
|
|
15773
|
+
if (split) this.model = { ...split, ...variant ? { variant } : {} };
|
|
15774
|
+
}
|
|
15775
|
+
host;
|
|
15776
|
+
opts;
|
|
15777
|
+
role;
|
|
15778
|
+
sessionId = null;
|
|
15779
|
+
model = null;
|
|
15780
|
+
agent;
|
|
15781
|
+
turn = null;
|
|
15782
|
+
turnHasText = false;
|
|
15783
|
+
interruptRequested = false;
|
|
15784
|
+
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
15785
|
+
openPermissions = /* @__PURE__ */ new Map();
|
|
15786
|
+
nativeSessionId() {
|
|
15787
|
+
return this.sessionId;
|
|
15788
|
+
}
|
|
15789
|
+
async start() {
|
|
15790
|
+
const resume = this.opts.resumeSessionId;
|
|
15791
|
+
if (resume) {
|
|
15792
|
+
const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
|
|
15793
|
+
if (existing?.data?.id) {
|
|
15794
|
+
this.sessionId = existing.data.id;
|
|
15795
|
+
if (this.model) await this.host.request("POST", `/api/session/${this.sessionId}/model`, { model: this.model }).catch(() => void 0);
|
|
15796
|
+
await this.host.request("POST", `/api/session/${this.sessionId}/agent`, { agent: this.agent }).catch(() => void 0);
|
|
15797
|
+
await this.applyInstructions();
|
|
15798
|
+
return;
|
|
15799
|
+
}
|
|
15800
|
+
if (this.opts.kind === "task") throw new Error(`OpenCode could not resume session ${resume}: the server does not know it.`);
|
|
15801
|
+
}
|
|
15802
|
+
const created = await this.host.json("POST", "/api/session", {
|
|
15803
|
+
agent: this.agent,
|
|
15804
|
+
...this.model ? { model: this.model } : {},
|
|
15805
|
+
permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
|
|
15806
|
+
});
|
|
15807
|
+
if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
|
|
15808
|
+
this.sessionId = created.data.id;
|
|
15809
|
+
await this.applyInstructions();
|
|
15810
|
+
}
|
|
15811
|
+
/** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
|
|
15812
|
+
async applyInstructions() {
|
|
15813
|
+
if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
|
|
15814
|
+
const response = await this.host.request("PUT", `/api/experimental/session/${this.sessionId}/instructions/entries/ordewell-planner`, { value: this.opts.systemPrompt });
|
|
15815
|
+
if (!response.ok) throw new Error(`OpenCode did not take the planner instructions: ${response.status} ${response.statusText}`);
|
|
15816
|
+
}
|
|
15817
|
+
async send(message, onEvent, signal, onActivity) {
|
|
15818
|
+
if (!this.sessionId) throw new Error(`OpenCode ${this.role} session is not started`);
|
|
15819
|
+
if (this.host.isExited()) {
|
|
15820
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
15821
|
+
return;
|
|
15822
|
+
}
|
|
15823
|
+
const state = newTurnState();
|
|
15824
|
+
this.turnHasText = false;
|
|
15825
|
+
this.interruptRequested = false;
|
|
15826
|
+
let markDone = () => {
|
|
15827
|
+
};
|
|
15828
|
+
const ended = new Promise((resolve7) => {
|
|
15829
|
+
markDone = resolve7;
|
|
15830
|
+
});
|
|
15831
|
+
const turn = { live: false, done: false, outcome: null, failure: null, ended, finish: () => {
|
|
15832
|
+
turn.done = true;
|
|
15833
|
+
markDone();
|
|
15834
|
+
} };
|
|
15835
|
+
this.turn = turn;
|
|
15836
|
+
const closeStream = await this.openStream(state, turn, onEvent, onActivity);
|
|
15837
|
+
const poll = new AbortController();
|
|
15838
|
+
try {
|
|
15839
|
+
try {
|
|
15840
|
+
await this.host.json("POST", `/api/session/${this.sessionId}/prompt`, { text: message }, signal);
|
|
15841
|
+
} catch (err) {
|
|
15842
|
+
if (signal?.aborted) return;
|
|
15843
|
+
onEvent({ type: "error", message: `OpenCode did not take the message: ${err instanceof Error ? err.message : String(err)}` });
|
|
15844
|
+
return;
|
|
15845
|
+
}
|
|
15846
|
+
void this.pollActive(turn, poll.signal);
|
|
15847
|
+
const aborted = new Promise((resolve7) => {
|
|
15848
|
+
if (signal?.aborted) resolve7();
|
|
15849
|
+
signal?.addEventListener("abort", () => resolve7(), { once: true });
|
|
15850
|
+
});
|
|
15851
|
+
await Promise.race([turn.ended, aborted, this.host.processEnded]);
|
|
15852
|
+
if (signal?.aborted) return;
|
|
15853
|
+
if (!turn.done) {
|
|
15854
|
+
onEvent({ type: "error", message: this.host.exitMessage() });
|
|
15855
|
+
return;
|
|
15856
|
+
}
|
|
15857
|
+
await this.readBack(state, onEvent);
|
|
15858
|
+
for (const id of [...this.openPermissions.keys()]) {
|
|
15859
|
+
this.openPermissions.delete(id);
|
|
15860
|
+
onEvent({ type: "permission_cancelled", id });
|
|
15861
|
+
}
|
|
15862
|
+
if (turn.outcome === "interrupted" || this.interruptRequested) onEvent({ type: "turn_end", interrupted: true });
|
|
15863
|
+
else if (turn.outcome === "failed") onEvent({ type: "error", message: turn.failure ?? "OpenCode reported that the turn failed." });
|
|
15864
|
+
else onEvent({ type: "turn_end" });
|
|
15865
|
+
} finally {
|
|
15866
|
+
this.turn = null;
|
|
15867
|
+
poll.abort();
|
|
15868
|
+
await closeStream();
|
|
15869
|
+
}
|
|
15870
|
+
}
|
|
15871
|
+
/**
|
|
15872
|
+
* The stream can drop the end frame while still delivering the rest, so the
|
|
15873
|
+
* turn also asks which sessions are running. A session that is not, after
|
|
15874
|
+
* this turn's work was seen, is over; one never seen working after a few
|
|
15875
|
+
* polls had already finished before the stream connected.
|
|
15876
|
+
*/
|
|
15877
|
+
async pollActive(turn, signal) {
|
|
15878
|
+
let idlePolls = 0;
|
|
15879
|
+
while (!turn.done && !signal.aborted) {
|
|
15880
|
+
await delay(ACTIVE_POLL_INTERVAL_MS, signal);
|
|
15881
|
+
if (turn.done || signal.aborted) return;
|
|
15882
|
+
const active = await this.host.json("GET", "/api/session/active").catch(() => null);
|
|
15883
|
+
if (!active?.data) continue;
|
|
15884
|
+
if (this.sessionId && this.sessionId in active.data) {
|
|
15885
|
+
turn.live = true;
|
|
15886
|
+
idlePolls = 0;
|
|
15887
|
+
continue;
|
|
15888
|
+
}
|
|
15889
|
+
idlePolls++;
|
|
15890
|
+
if (turn.live || idlePolls >= IDLE_POLLS_BEFORE_START) {
|
|
15891
|
+
const messages = await this.messages();
|
|
15892
|
+
const idle = [...messages ?? []].reverse().find((m) => m.type === "idle");
|
|
15893
|
+
if (idle?.outcome === "failed" || idle?.outcome === "interrupted" || idle?.outcome === "succeeded") turn.outcome ??= idle.outcome;
|
|
15894
|
+
turn.outcome ??= "succeeded";
|
|
15895
|
+
if (!turn.done) turn.finish();
|
|
15896
|
+
}
|
|
15897
|
+
}
|
|
15898
|
+
}
|
|
15899
|
+
async messages() {
|
|
15900
|
+
const response = await this.host.json("GET", `/api/session/${this.sessionId}/message`).catch(() => null);
|
|
15901
|
+
return Array.isArray(response?.data) ? response.data : null;
|
|
15902
|
+
}
|
|
15903
|
+
/**
|
|
15904
|
+
* Whatever of the turn's own replies the stream missed. A message whose text
|
|
15905
|
+
* the stream delivered adds nothing here; one that lost its text frames still
|
|
15906
|
+
* reaches the plain-text channel, where the done marker is looked for.
|
|
15907
|
+
*/
|
|
15908
|
+
async readBack(state, onEvent) {
|
|
15909
|
+
const messages = await this.messages();
|
|
15910
|
+
if (!messages) return;
|
|
15911
|
+
const latestUser = messages.findIndex((m) => m.type === "user");
|
|
15912
|
+
const own = latestUser < 0 ? messages : messages.slice(0, latestUser);
|
|
15913
|
+
for (const message of [...own].reverse()) {
|
|
15914
|
+
if (message.type !== "assistant" || !message.id || state.streamedMessages.has(message.id)) continue;
|
|
15915
|
+
(message.content ?? []).forEach((part, index) => {
|
|
15916
|
+
if (part.type === "text" && part.text) this.emitText(`${message.id}:readback:${index}`, part.text, state, onEvent);
|
|
15917
|
+
});
|
|
15918
|
+
}
|
|
15919
|
+
}
|
|
15920
|
+
/**
|
|
15921
|
+
* Abort the running turn, keeping the server and its session. OpenCode
|
|
15922
|
+
* acknowledges with `session.execution.interrupted`, which also ends the turn.
|
|
15923
|
+
*/
|
|
15924
|
+
async interrupt(timeoutMs) {
|
|
15925
|
+
if (!this.sessionId || this.host.isExited()) return false;
|
|
15926
|
+
const turn = this.turn;
|
|
15927
|
+
this.interruptRequested = true;
|
|
15928
|
+
const response = await this.host.json("POST", `/api/session/${this.sessionId}/interrupt`).catch(() => null);
|
|
15929
|
+
if (!response) return false;
|
|
15930
|
+
if (!turn || turn.done) return true;
|
|
15931
|
+
let timer;
|
|
15932
|
+
const timedOut = new Promise((resolve7) => {
|
|
15933
|
+
timer = setTimeout(() => resolve7(false), timeoutMs);
|
|
15934
|
+
timer.unref?.();
|
|
15935
|
+
});
|
|
15936
|
+
const acknowledged = await Promise.race([turn.ended.then(() => true), this.host.processEnded.then(() => false), timedOut]);
|
|
15937
|
+
clearTimeout(timer);
|
|
15938
|
+
return acknowledged;
|
|
15939
|
+
}
|
|
15940
|
+
answerPermission(id, decision) {
|
|
15941
|
+
const sessionId = this.openPermissions.get(id);
|
|
15942
|
+
if (sessionId === void 0) return false;
|
|
15943
|
+
this.openPermissions.delete(id);
|
|
15944
|
+
void this.replyPermission(id, sessionId, permissionReply(decision)).catch(() => {
|
|
15945
|
+
});
|
|
15946
|
+
return true;
|
|
15947
|
+
}
|
|
15948
|
+
async replyPermission(id, sessionId, reply) {
|
|
15949
|
+
await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
|
|
15950
|
+
}
|
|
15951
|
+
async openStream(state, turn, onEvent, onActivity) {
|
|
15952
|
+
const streamAbort = new AbortController();
|
|
15953
|
+
let connected = () => {
|
|
15954
|
+
};
|
|
15955
|
+
const ready = new Promise((resolve7) => {
|
|
15956
|
+
connected = resolve7;
|
|
15957
|
+
});
|
|
15958
|
+
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, state, turn, onEvent), connected, onActivity);
|
|
15959
|
+
await Promise.race([ready, new Promise((r) => {
|
|
15960
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS);
|
|
15961
|
+
t.unref?.();
|
|
15962
|
+
})]);
|
|
15963
|
+
return async () => {
|
|
15964
|
+
streamAbort.abort();
|
|
15965
|
+
await live.catch(() => {
|
|
15966
|
+
});
|
|
15967
|
+
};
|
|
15968
|
+
}
|
|
15969
|
+
async streamEvents(signal, onFrame, onConnected, onActivity) {
|
|
15970
|
+
const response = await this.host.openEvents(signal).catch(() => null);
|
|
15971
|
+
const body = response?.body;
|
|
15972
|
+
if (!body) {
|
|
15973
|
+
onConnected();
|
|
15974
|
+
return;
|
|
15975
|
+
}
|
|
15976
|
+
onConnected();
|
|
15977
|
+
const reader = body.getReader();
|
|
15978
|
+
const decoder = new TextDecoder();
|
|
15979
|
+
let buffer = "";
|
|
15980
|
+
for (; ; ) {
|
|
15981
|
+
const { done, value } = await reader.read().catch(() => ({ done: true, value: void 0 }));
|
|
15982
|
+
if (done) return;
|
|
15983
|
+
onActivity?.();
|
|
15984
|
+
buffer += decoder.decode(value, { stream: true });
|
|
15985
|
+
let newline;
|
|
15986
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
15987
|
+
const line = buffer.slice(0, newline).trim();
|
|
15988
|
+
buffer = buffer.slice(newline + 1);
|
|
15989
|
+
if (!line.startsWith("data:")) continue;
|
|
15990
|
+
try {
|
|
15991
|
+
onFrame(JSON.parse(line.slice(5).trim()));
|
|
15992
|
+
} catch {
|
|
15993
|
+
}
|
|
15994
|
+
}
|
|
15995
|
+
}
|
|
15996
|
+
}
|
|
15997
|
+
/**
|
|
15998
|
+
* One frame. Only this session and its children are followed: the stream is
|
|
15999
|
+
* the whole server's, and another client's session is none of this turn's business.
|
|
16000
|
+
*/
|
|
16001
|
+
onFrame(frame, state, turn, onEvent) {
|
|
16002
|
+
const data = frame.data;
|
|
16003
|
+
if (!data || !frame.type) return;
|
|
16004
|
+
if (frame.type === "session.created") {
|
|
16005
|
+
if (data.parentID && data.parentID === this.sessionId && data.sessionID && !state.children.has(data.sessionID)) state.children.set(data.sessionID, null);
|
|
16006
|
+
return;
|
|
16007
|
+
}
|
|
16008
|
+
const session = data.sessionID;
|
|
16009
|
+
if (!session) return;
|
|
16010
|
+
const isChild = session !== this.sessionId;
|
|
16011
|
+
if (isChild && !state.children.has(session)) return;
|
|
16012
|
+
if (!isChild) {
|
|
16013
|
+
if (frame.type === "session.execution.started") {
|
|
16014
|
+
turn.live = true;
|
|
16015
|
+
return;
|
|
16016
|
+
}
|
|
16017
|
+
if (frame.type === "session.execution.succeeded" || frame.type === "session.execution.failed" || frame.type === "session.execution.interrupted") {
|
|
16018
|
+
if (!turn.live || turn.done) return;
|
|
16019
|
+
turn.outcome = frame.type === "session.execution.succeeded" ? "succeeded" : frame.type === "session.execution.failed" ? "failed" : "interrupted";
|
|
16020
|
+
if (turn.outcome === "failed") turn.failure = data.error?.message ?? data.error?.type ?? null;
|
|
16021
|
+
turn.finish();
|
|
16022
|
+
return;
|
|
16023
|
+
}
|
|
16024
|
+
}
|
|
16025
|
+
if (frame.type === "permission.asked") {
|
|
16026
|
+
if (this.role === "task") this.askPermission(data, session, state, onEvent);
|
|
16027
|
+
else this.denyPermission(data, session, state, onEvent);
|
|
16028
|
+
return;
|
|
16029
|
+
}
|
|
16030
|
+
if (frame.type === "permission.replied") {
|
|
16031
|
+
const id = data.requestID;
|
|
16032
|
+
if (id && this.openPermissions.delete(id)) onEvent({ type: "permission_cancelled", id });
|
|
16033
|
+
return;
|
|
16034
|
+
}
|
|
16035
|
+
let subagentId;
|
|
16036
|
+
if (isChild) {
|
|
16037
|
+
const owner = state.children.get(session);
|
|
16038
|
+
if (!owner) {
|
|
16039
|
+
state.heldFrames.set(session, [...state.heldFrames.get(session) ?? [], frame]);
|
|
16040
|
+
return;
|
|
16041
|
+
}
|
|
16042
|
+
subagentId = owner;
|
|
16043
|
+
}
|
|
16044
|
+
this.onContentFrame(frame, data, state, turn, onEvent, subagentId);
|
|
16045
|
+
}
|
|
16046
|
+
onContentFrame(frame, data, state, turn, onEvent, subagentId) {
|
|
16047
|
+
const message = data.assistantMessageID ?? "";
|
|
16048
|
+
switch (frame.type) {
|
|
16049
|
+
case "session.step.started":
|
|
16050
|
+
if (message && data.model?.providerID && data.model.id) state.stepModels.set(message, `${data.model.providerID}/${data.model.id}`);
|
|
16051
|
+
return;
|
|
16052
|
+
case "session.reasoning.delta":
|
|
16053
|
+
if (data.delta) onEvent({ type: "thinking_delta", text: data.delta, subagentId });
|
|
16054
|
+
return;
|
|
16055
|
+
case "session.reasoning.ended": {
|
|
16056
|
+
const key = `reasoning:${message}:${data.ordinal ?? 0}`;
|
|
16057
|
+
if (!data.text || state.seen.has(key)) return;
|
|
16058
|
+
state.seen.add(key);
|
|
16059
|
+
onEvent({ type: "thinking", text: data.text, subagentId });
|
|
16060
|
+
return;
|
|
16061
|
+
}
|
|
16062
|
+
case "session.text.delta":
|
|
16063
|
+
if (!subagentId && data.delta) this.onTextDelta(`${message}:${data.ordinal ?? 0}`, data.delta, state, onEvent);
|
|
16064
|
+
return;
|
|
16065
|
+
case "session.text.ended":
|
|
16066
|
+
if (subagentId || !data.text) return;
|
|
16067
|
+
if (message) state.streamedMessages.add(message);
|
|
16068
|
+
this.emitText(`${message}:${data.ordinal ?? 0}`, data.text, state, onEvent);
|
|
16069
|
+
return;
|
|
16070
|
+
case "session.tool.input.started":
|
|
16071
|
+
if (data.id && data.name) state.toolNames.set(data.id, data.name);
|
|
16072
|
+
return;
|
|
16073
|
+
case "session.tool.called": {
|
|
16074
|
+
const id = data.id;
|
|
16075
|
+
if (!id || state.seen.has(`call:${id}`)) return;
|
|
16076
|
+
state.seen.add(`call:${id}`);
|
|
16077
|
+
const input = data.input ?? {};
|
|
16078
|
+
const name = state.toolNames.get(id) ?? "tool";
|
|
16079
|
+
const brief = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
16080
|
+
state.briefs.set(id, brief);
|
|
16081
|
+
onEvent({ type: "tool_call", id, name, args: input, subagentId });
|
|
16082
|
+
return;
|
|
16083
|
+
}
|
|
16084
|
+
case "session.tool.progress":
|
|
16085
|
+
if (!subagentId) this.adoptChild(data, state, onEvent, turn);
|
|
16086
|
+
return;
|
|
16087
|
+
case "session.tool.success":
|
|
16088
|
+
case "session.tool.failed": {
|
|
16089
|
+
const id = data.id;
|
|
16090
|
+
if (!id || state.seen.has(`result:${id}`)) return;
|
|
16091
|
+
state.seen.add(`result:${id}`);
|
|
16092
|
+
const success = frame.type === "session.tool.success";
|
|
16093
|
+
const output = success ? contentText(data.content) : data.error?.message ?? data.error?.type ?? "";
|
|
16094
|
+
onEvent({ type: "tool_result", id, name: state.toolNames.get(id) ?? "tool", output, success, subagentId });
|
|
16095
|
+
if (!subagentId) this.finishChild(id, success, output, state, onEvent);
|
|
16096
|
+
return;
|
|
16097
|
+
}
|
|
16098
|
+
case "session.step.ended":
|
|
16099
|
+
case "session.step.failed":
|
|
16100
|
+
this.countUsage(data, state, onEvent, subagentId);
|
|
16101
|
+
return;
|
|
16102
|
+
default:
|
|
16103
|
+
}
|
|
16104
|
+
}
|
|
16105
|
+
adoptChild(data, state, onEvent, turn) {
|
|
16106
|
+
const child = data.metadata?.sessionID;
|
|
16107
|
+
const callId = data.id;
|
|
16108
|
+
if (!child || !callId || state.children.get(child)) return;
|
|
16109
|
+
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
16110
|
+
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
16111
|
+
state.children.set(child, callId);
|
|
16112
|
+
const held = state.heldFrames.get(child) ?? [];
|
|
16113
|
+
state.heldFrames.delete(child);
|
|
16114
|
+
for (const frame of held) this.onFrame(frame, state, turn, onEvent);
|
|
16115
|
+
}
|
|
16116
|
+
finishChild(callId, success, output, state, onEvent) {
|
|
16117
|
+
if (![...state.children.values()].includes(callId)) return;
|
|
16118
|
+
onEvent({ type: "subagent_finished", subagentId: callId, outcome: success ? "done" : "failed", digest: subagentDigest(output) });
|
|
16119
|
+
}
|
|
16120
|
+
/** One model call's usage, once, when its step ends. Zeros mean the call failed before the provider answered. */
|
|
16121
|
+
countUsage(data, state, onEvent, subagentId) {
|
|
16122
|
+
const message = data.assistantMessageID;
|
|
16123
|
+
const tokens = data.tokens;
|
|
16124
|
+
if (!message || !tokens || state.seen.has(`usage:${message}`)) return;
|
|
16125
|
+
state.seen.add(`usage:${message}`);
|
|
16126
|
+
const prompt = partedPromptUsage({ uncached: tokens.input, cacheRead: tokens.cache?.read, cacheWrite: tokens.cache?.write });
|
|
16127
|
+
const outputTokens = (tokens.output ?? 0) + (tokens.reasoning ?? 0);
|
|
16128
|
+
if ((prompt.inputTokens ?? 0) + outputTokens === 0) return;
|
|
16129
|
+
const record = { source: "opencode", ...prompt, outputTokens };
|
|
16130
|
+
const model = state.stepModels.get(message);
|
|
16131
|
+
if (model) record.model = model;
|
|
16132
|
+
if (typeof data.cost === "number" && data.cost > 0) record.reportedCost = { amount: data.cost, currency: "USD" };
|
|
16133
|
+
if (subagentId) record.subagentId = subagentId;
|
|
16134
|
+
onEvent({ type: "usage", record });
|
|
16135
|
+
}
|
|
16136
|
+
/** 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. */
|
|
16137
|
+
emitText(id, text2, state, onEvent) {
|
|
16138
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
16139
|
+
state.seen.add(`text:${id}`);
|
|
16140
|
+
if (!text2.trim()) return;
|
|
16141
|
+
const lead = state.textRuns.get(id)?.lead ?? (this.turnHasText ? "\n\n" : "");
|
|
16142
|
+
onEvent({ type: "assistant_text", text: `${lead}${text2}` });
|
|
16143
|
+
this.turnHasText = true;
|
|
16144
|
+
}
|
|
16145
|
+
/**
|
|
16146
|
+
* Stream one piece of a reply text. The paragraph break goes out with the
|
|
16147
|
+
* first visible delta, so the deltas add up to exactly the text the completed
|
|
16148
|
+
* run then re-sends; a run that is only whitespace so far is held back.
|
|
16149
|
+
*/
|
|
16150
|
+
onTextDelta(id, delta, state, onEvent) {
|
|
16151
|
+
if (state.seen.has(`text:${id}`)) return;
|
|
16152
|
+
const run = state.textRuns.get(id) ?? { held: "", lead: null };
|
|
16153
|
+
state.textRuns.set(id, run);
|
|
16154
|
+
if (run.lead !== null) {
|
|
16155
|
+
onEvent({ type: "assistant_text_delta", text: delta });
|
|
16156
|
+
return;
|
|
16157
|
+
}
|
|
16158
|
+
run.held += delta;
|
|
16159
|
+
if (!run.held.trim()) return;
|
|
16160
|
+
run.lead = this.turnHasText ? "\n\n" : "";
|
|
16161
|
+
this.turnHasText = true;
|
|
16162
|
+
onEvent({ type: "assistant_text_delta", text: `${run.lead}${run.held}` });
|
|
16163
|
+
}
|
|
16164
|
+
permissionRequest(ask) {
|
|
16165
|
+
const id = ask.id;
|
|
16166
|
+
if (!id) return null;
|
|
16167
|
+
const scope = (ask.resources ?? []).join(", ");
|
|
16168
|
+
return {
|
|
16169
|
+
type: "permission_request",
|
|
16170
|
+
id,
|
|
16171
|
+
name: ask.action ?? "permission",
|
|
16172
|
+
detail: JSON.stringify(scope ? { scope } : {}),
|
|
16173
|
+
input: ask.resources ? { resources: ask.resources } : {},
|
|
16174
|
+
// The patterns `always` would grant: the runner's own offer, and the only grounds for "Allow for this task".
|
|
16175
|
+
...ask.save?.length ? { suggestions: ask.save } : {},
|
|
16176
|
+
...ask.source?.id ? { toolUseId: ask.source.id } : {}
|
|
16177
|
+
};
|
|
16178
|
+
}
|
|
16179
|
+
/**
|
|
16180
|
+
* Deny one planner request. OpenCode blocks the turn until it is answered, so
|
|
16181
|
+
* this must answer: `reject` rather than a silent drop is the "absent answer
|
|
16182
|
+
* is a denial" invariant ADR-0008 states. The refusal is announced so the
|
|
16183
|
+
* timeline shows the planner reaching for something it may not have.
|
|
16184
|
+
*/
|
|
16185
|
+
denyPermission(ask, session, state, onEvent) {
|
|
16186
|
+
const request = this.permissionRequest(ask);
|
|
16187
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
16188
|
+
state.seen.add(`perm:${request.id}`);
|
|
16189
|
+
onEvent(request);
|
|
16190
|
+
void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
|
|
16191
|
+
});
|
|
16192
|
+
}
|
|
16193
|
+
/**
|
|
16194
|
+
* A task's request. Under a mode whose manifest sets `approvals: auto` it is
|
|
16195
|
+
* answered at once with what `opencode run --auto` answers, so the same plan
|
|
16196
|
+
* behaves the same on both transports (ADR-0001), and announced already
|
|
16197
|
+
* decided so the log still shows it. Any other mode leaves it open for an
|
|
16198
|
+
* approval card.
|
|
16199
|
+
*/
|
|
16200
|
+
askPermission(ask, session, state, onEvent) {
|
|
16201
|
+
const request = this.permissionRequest(ask);
|
|
16202
|
+
if (!request || state.seen.has(`perm:${request.id}`)) return;
|
|
16203
|
+
state.seen.add(`perm:${request.id}`);
|
|
16204
|
+
if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
|
|
16205
|
+
onEvent({ ...request, decided: { decision: "allow" } });
|
|
16206
|
+
void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
|
|
16207
|
+
});
|
|
16208
|
+
return;
|
|
16209
|
+
}
|
|
16210
|
+
this.openPermissions.set(request.id, session);
|
|
16211
|
+
onEvent(request);
|
|
16212
|
+
}
|
|
16213
|
+
};
|
|
16214
|
+
|
|
16215
|
+
// src/services/harness/OpenCodeAdapter.ts
|
|
15569
16216
|
var SERVER_READY_TIMEOUT_MS = 3e4;
|
|
15570
16217
|
var STDERR_TAIL_CHARS2 = 4e3;
|
|
15571
|
-
var
|
|
16218
|
+
var STREAM_CONNECT_TIMEOUT_MS2 = 5e3;
|
|
15572
16219
|
var RECOVERY_POLL_INTERVAL_MS = 2e3;
|
|
15573
16220
|
var RECOVERY_TIMEOUT_MS = 9e5;
|
|
15574
16221
|
var STATUS_POLL_INTERVAL_MS = 1e3;
|
|
@@ -15581,8 +16228,8 @@ var DISABLED_TOOLS = {
|
|
|
15581
16228
|
todowrite: false
|
|
15582
16229
|
};
|
|
15583
16230
|
var TASK_DISABLED_TOOLS = { question: false };
|
|
15584
|
-
var
|
|
15585
|
-
function
|
|
16231
|
+
var AUTO_APPROVALS2 = "auto";
|
|
16232
|
+
function splitModelId2(id) {
|
|
15586
16233
|
const slash = id.indexOf("/");
|
|
15587
16234
|
if (slash <= 0 || slash === id.length - 1) return null;
|
|
15588
16235
|
return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
|
|
@@ -15638,7 +16285,7 @@ function unwrapFileToolOutput(output) {
|
|
|
15638
16285
|
const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
|
|
15639
16286
|
return inner ? inner[1] : output;
|
|
15640
16287
|
}
|
|
15641
|
-
function
|
|
16288
|
+
function delay2(ms, signal) {
|
|
15642
16289
|
return new Promise((resolve7) => {
|
|
15643
16290
|
const timer = setTimeout(resolve7, ms);
|
|
15644
16291
|
timer.unref?.();
|
|
@@ -15648,10 +16295,10 @@ function delay(ms, signal) {
|
|
|
15648
16295
|
}, { once: true });
|
|
15649
16296
|
});
|
|
15650
16297
|
}
|
|
15651
|
-
function
|
|
16298
|
+
function newTurnState2() {
|
|
15652
16299
|
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() };
|
|
15653
16300
|
}
|
|
15654
|
-
function
|
|
16301
|
+
function permissionReply2(decision) {
|
|
15655
16302
|
if (decision.decision === "allow") return { reply: "once" };
|
|
15656
16303
|
if (decision.decision === "allowForTask") return { reply: "always" };
|
|
15657
16304
|
const note = decision.note?.trim();
|
|
@@ -15688,6 +16335,8 @@ var OpenCodeAdapter = class {
|
|
|
15688
16335
|
interruptRequested = false;
|
|
15689
16336
|
/** A task's requests waiting for an answer, by request id, with the session that asked. */
|
|
15690
16337
|
openPermissions = /* @__PURE__ */ new Map();
|
|
16338
|
+
/** Set once the server turns out to speak the 2.x API, which then owns the session. */
|
|
16339
|
+
v2 = null;
|
|
15691
16340
|
async start(opts) {
|
|
15692
16341
|
if (opts.kind === "task") this.task = opts;
|
|
15693
16342
|
else this.planner = opts;
|
|
@@ -15702,14 +16351,14 @@ var OpenCodeAdapter = class {
|
|
|
15702
16351
|
throw new ExecutableNotFoundError("opencode", PATH);
|
|
15703
16352
|
}
|
|
15704
16353
|
const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
|
|
15705
|
-
const password =
|
|
16354
|
+
const password = (0, import_crypto2.randomBytes)(24).toString("base64url");
|
|
15706
16355
|
this.process = this.deps.spawn(launch.file, launch.args, {
|
|
15707
|
-
env: runnerEnv(PATH,
|
|
16356
|
+
env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
|
|
15708
16357
|
stdio: ["pipe", "pipe", "pipe"],
|
|
15709
16358
|
cwd: opts.cwd,
|
|
15710
16359
|
windowsVerbatimArguments: launch.verbatim
|
|
15711
16360
|
});
|
|
15712
|
-
|
|
16361
|
+
this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
|
|
15713
16362
|
this.process.stdin?.on("error", () => {
|
|
15714
16363
|
});
|
|
15715
16364
|
const banner = new Promise((resolve7) => {
|
|
@@ -15746,6 +16395,24 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS2);
|
|
|
15746
16395
|
|
|
15747
16396
|
${this.stderrTail.trim()}` : ""}`);
|
|
15748
16397
|
}
|
|
16398
|
+
if (await this.speaksV2()) {
|
|
16399
|
+
this.v2 = new OpenCodeV2({
|
|
16400
|
+
request: (method, path27, body, signal) => this.request(method, path27, body, signal),
|
|
16401
|
+
json: (method, path27, body, signal) => this.json(method, path27, body, signal),
|
|
16402
|
+
openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
|
|
16403
|
+
processEnded: this.processEnded,
|
|
16404
|
+
isExited: () => this.exited,
|
|
16405
|
+
exitMessage: () => this.exitMessage()
|
|
16406
|
+
}, opts, this.role());
|
|
16407
|
+
try {
|
|
16408
|
+
await this.v2.start();
|
|
16409
|
+
} catch (err) {
|
|
16410
|
+
this.dispose();
|
|
16411
|
+
throw err;
|
|
16412
|
+
}
|
|
16413
|
+
this.sessionId = this.v2.nativeSessionId();
|
|
16414
|
+
return;
|
|
16415
|
+
}
|
|
15749
16416
|
if (opts.resumeSessionId) {
|
|
15750
16417
|
const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
|
|
15751
16418
|
if (existing?.id) {
|
|
@@ -15769,12 +16436,17 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15769
16436
|
onEvent({ type: "error", message: this.exitMessage() });
|
|
15770
16437
|
return;
|
|
15771
16438
|
}
|
|
16439
|
+
if (this.v2) {
|
|
16440
|
+
await this.v2.send(message, onEvent, signal, onActivity);
|
|
16441
|
+
if (signal?.aborted) this.dispose();
|
|
16442
|
+
return;
|
|
16443
|
+
}
|
|
15772
16444
|
if (this.task) return this.sendTask(this.task, message, onEvent, signal, onActivity);
|
|
15773
|
-
const turn =
|
|
16445
|
+
const turn = newTurnState2();
|
|
15774
16446
|
this.turnHasText = false;
|
|
15775
16447
|
const closeStream = await this.openStream(turn, onEvent, onActivity);
|
|
15776
16448
|
try {
|
|
15777
|
-
const model = this.planner?.model ?
|
|
16449
|
+
const model = this.planner?.model ? splitModelId2(this.planner.model) : null;
|
|
15778
16450
|
const body = {
|
|
15779
16451
|
parts: [{ type: "text", text: message }],
|
|
15780
16452
|
// The read-only guarantee: OpenCode's own plan agent has no write tools.
|
|
@@ -15815,7 +16487,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15815
16487
|
* {@link recoverReply}, never applies.
|
|
15816
16488
|
*/
|
|
15817
16489
|
async sendTask(task, message, onEvent, signal, onActivity) {
|
|
15818
|
-
const state =
|
|
16490
|
+
const state = newTurnState2();
|
|
15819
16491
|
this.turnHasText = false;
|
|
15820
16492
|
this.interruptRequested = false;
|
|
15821
16493
|
let markDone = () => {
|
|
@@ -15831,7 +16503,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15831
16503
|
const closeStream = await this.openStream(state, onEvent, onActivity);
|
|
15832
16504
|
const poll = new AbortController();
|
|
15833
16505
|
try {
|
|
15834
|
-
const model = task.model ?
|
|
16506
|
+
const model = task.model ? splitModelId2(task.model) : null;
|
|
15835
16507
|
const body = {
|
|
15836
16508
|
parts: [{ type: "text", text: message }],
|
|
15837
16509
|
// The manifest's meaning of the task's mode (ADR-0001): for OpenCode a mode is an agent.
|
|
@@ -15911,7 +16583,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15911
16583
|
*/
|
|
15912
16584
|
async pollStatus(turn, signal) {
|
|
15913
16585
|
while (!turn.done && !signal.aborted) {
|
|
15914
|
-
await
|
|
16586
|
+
await delay2(STATUS_POLL_INTERVAL_MS, signal);
|
|
15915
16587
|
if (turn.done || signal.aborted) return;
|
|
15916
16588
|
const busy = await this.sessionBusy();
|
|
15917
16589
|
if (busy === true) turn.live = true;
|
|
@@ -15958,6 +16630,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15958
16630
|
*/
|
|
15959
16631
|
async interrupt(timeoutMs) {
|
|
15960
16632
|
if (!this.process || !this.sessionId || this.exited) return false;
|
|
16633
|
+
if (this.v2) return this.v2.interrupt(timeoutMs);
|
|
15961
16634
|
const turn = this.taskTurn;
|
|
15962
16635
|
this.interruptRequested = true;
|
|
15963
16636
|
const posted = await this.json("POST", `/session/${this.sessionId}/abort`).then(() => true, () => false);
|
|
@@ -15978,13 +16651,23 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
15978
16651
|
void this.processEnded.then(() => listener(this.exitCode));
|
|
15979
16652
|
}
|
|
15980
16653
|
answerPermission(id, decision) {
|
|
16654
|
+
if (this.v2) return this.process ? this.v2.answerPermission(id, decision) : false;
|
|
15981
16655
|
const sessionId = this.openPermissions.get(id);
|
|
15982
16656
|
if (sessionId === void 0 || !this.process) return false;
|
|
15983
16657
|
this.openPermissions.delete(id);
|
|
15984
|
-
void this.replyPermission(id, sessionId,
|
|
16658
|
+
void this.replyPermission(id, sessionId, permissionReply2(decision)).catch(() => {
|
|
15985
16659
|
});
|
|
15986
16660
|
return true;
|
|
15987
16661
|
}
|
|
16662
|
+
/**
|
|
16663
|
+
* OpenCode 2.x replaced the 1.x HTTP surface rather than extending it, and
|
|
16664
|
+
* only 2.x answers `/api/info` with its version. A 1.x server — or one that
|
|
16665
|
+
* answers with anything else — keeps the 1.x protocol this class speaks.
|
|
16666
|
+
*/
|
|
16667
|
+
async speaksV2() {
|
|
16668
|
+
const info = await this.json("GET", "/api/info").catch(() => null);
|
|
16669
|
+
return typeof info === "object" && info !== null && typeof info.version === "string";
|
|
16670
|
+
}
|
|
15988
16671
|
role() {
|
|
15989
16672
|
return this.task ? "task" : "planner";
|
|
15990
16673
|
}
|
|
@@ -16035,7 +16718,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16035
16718
|
if (pending?.info?.time?.completed || pending?.info?.error) return pending;
|
|
16036
16719
|
if (pending) onActivity?.();
|
|
16037
16720
|
if (Date.now() >= deadline) return null;
|
|
16038
|
-
await
|
|
16721
|
+
await delay2(RECOVERY_POLL_INTERVAL_MS, signal);
|
|
16039
16722
|
}
|
|
16040
16723
|
}
|
|
16041
16724
|
newestAssistantId(messages) {
|
|
@@ -16248,7 +16931,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16248
16931
|
...ask.always?.length ? { suggestions: ask.always } : {},
|
|
16249
16932
|
...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
|
|
16250
16933
|
};
|
|
16251
|
-
if (this.task?.flags.modeSettings.approvals ===
|
|
16934
|
+
if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
|
|
16252
16935
|
onEvent({ ...request, decided: { decision: "allow" } });
|
|
16253
16936
|
void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
|
|
16254
16937
|
});
|
|
@@ -16282,7 +16965,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
16282
16965
|
});
|
|
16283
16966
|
const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, turn, onEvent), connected, onActivity);
|
|
16284
16967
|
await Promise.race([streamReady, new Promise((r) => {
|
|
16285
|
-
const t = setTimeout(r,
|
|
16968
|
+
const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS2);
|
|
16286
16969
|
t.unref?.();
|
|
16287
16970
|
})]);
|
|
16288
16971
|
return async () => {
|
|
@@ -16487,6 +17170,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16487
17170
|
observers = [];
|
|
16488
17171
|
tddEnabled;
|
|
16489
17172
|
readRunnerTransport;
|
|
17173
|
+
previousAttemptFromLog;
|
|
16490
17174
|
/**
|
|
16491
17175
|
* The setting as the latest run copied it when it opened. Every spawn of
|
|
16492
17176
|
* that run uses this, so flipping the setting mid-run changes the next run
|
|
@@ -16506,6 +17190,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16506
17190
|
this.workspaceEnv = deps.workspaceEnv;
|
|
16507
17191
|
this.tddEnabled = deps.tddEnabled;
|
|
16508
17192
|
this.readRunnerTransport = deps.runnerTransport;
|
|
17193
|
+
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
16509
17194
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
16510
17195
|
this.verifier.onVerdict((taskId, verdict) => this.onVerdict(taskId, verdict));
|
|
16511
17196
|
this.verifier.onCheckpoint((taskId, summary) => {
|
|
@@ -16561,7 +17246,8 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16561
17246
|
workspaceRoot,
|
|
16562
17247
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
16563
17248
|
tddEnabled: options.tddEnabled ?? (() => false),
|
|
16564
|
-
runnerTransport: options.runnerTransport ?? (() => "terminal")
|
|
17249
|
+
runnerTransport: options.runnerTransport ?? (() => "terminal"),
|
|
17250
|
+
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null)
|
|
16565
17251
|
});
|
|
16566
17252
|
return orchestrator;
|
|
16567
17253
|
}
|
|
@@ -16569,6 +17255,18 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16569
17255
|
getIdleSince(taskId) {
|
|
16570
17256
|
return this.verifier.getIdleSince(taskId);
|
|
16571
17257
|
}
|
|
17258
|
+
/**
|
|
17259
|
+
* What an ops retry is told about the attempt before. The saved log is read
|
|
17260
|
+
* whether or not the session was reloaded, so both behave alike; the terminal
|
|
17261
|
+
* buffer is the fallback for a runner that has no log. A reload clears the
|
|
17262
|
+
* attempt count along with the buffer, so a retry made since is what says
|
|
17263
|
+
* there was an attempt to report on.
|
|
17264
|
+
*/
|
|
17265
|
+
opsPreviousAttempt(taskId) {
|
|
17266
|
+
const saved = this.previousAttemptFromLog(taskId) ?? this.output.liveTail(taskId, { maxLines: OPS_RETRY_TAIL_LINES })?.text;
|
|
17267
|
+
if (saved !== void 0) return saved;
|
|
17268
|
+
return (this.retryCounts.get(taskId) ?? 0) > 0 ? "Its output is not available: this session was reloaded since it ran." : void 0;
|
|
17269
|
+
}
|
|
16572
17270
|
/** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
|
|
16573
17271
|
getLiveOutput(taskId, opts) {
|
|
16574
17272
|
return this.output.liveTail(taskId, opts);
|
|
@@ -17426,7 +18124,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
17426
18124
|
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
17427
18125
|
// An ops task's effects outlive a failed attempt and are never rolled
|
|
17428
18126
|
// back, so the next one is told what the last one did (ADR-0020).
|
|
17429
|
-
previousAttempt: attempt.ops ? this.
|
|
18127
|
+
previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
|
|
17430
18128
|
});
|
|
17431
18129
|
this.lingering.close(task.id);
|
|
17432
18130
|
const env = await this.envForTask(cwd);
|
|
@@ -21107,6 +21805,13 @@ var PlanEditError = class extends Error {
|
|
|
21107
21805
|
this.name = "PlanEditError";
|
|
21108
21806
|
}
|
|
21109
21807
|
};
|
|
21808
|
+
var OPS_RETRY_DIGEST_CALLS = 20;
|
|
21809
|
+
function lastAttemptDigest(location, taskId) {
|
|
21810
|
+
const attempt = listTaskLogAttempts(location, taskId).pop();
|
|
21811
|
+
if (attempt === void 0) return null;
|
|
21812
|
+
const events = readTaskLog(location, taskId, attempt);
|
|
21813
|
+
return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
|
|
21814
|
+
}
|
|
21110
21815
|
function sessionRuntimeSettings(settings) {
|
|
21111
21816
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
|
|
21112
21817
|
}
|
|
@@ -21191,7 +21896,8 @@ function createSession(deps) {
|
|
|
21191
21896
|
registry: deps.registry,
|
|
21192
21897
|
workspaceRoot: deps.workspaceRoot,
|
|
21193
21898
|
tddEnabled: () => deps.settings().tddEnabled,
|
|
21194
|
-
runnerTransport: () => deps.settings().runnerTransport ?? "terminal"
|
|
21899
|
+
runnerTransport: () => deps.settings().runnerTransport ?? "terminal",
|
|
21900
|
+
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId)
|
|
21195
21901
|
});
|
|
21196
21902
|
const usage = new PlannerUsageLedger();
|
|
21197
21903
|
const events = new SessionEventRelay({
|
|
@@ -24780,6 +25486,7 @@ var PlannerModelMemory = class {
|
|
|
24780
25486
|
FsPluginStore,
|
|
24781
25487
|
GIT_READONLY_SUBCOMMANDS,
|
|
24782
25488
|
GREP_DEFAULT_HEAD_LIMIT,
|
|
25489
|
+
GREP_PCRE_PROBE_ARGS,
|
|
24783
25490
|
GeminiService,
|
|
24784
25491
|
HeadlessRunner,
|
|
24785
25492
|
HeadlessSession,
|
|
@@ -24919,6 +25626,7 @@ var PlannerModelMemory = class {
|
|
|
24919
25626
|
dependencyCandidates,
|
|
24920
25627
|
dependentsOf,
|
|
24921
25628
|
describeMergeResult,
|
|
25629
|
+
digestTaskLog,
|
|
24922
25630
|
discoverGeminiModels,
|
|
24923
25631
|
drainNext,
|
|
24924
25632
|
effectiveAllowlist,
|
|
@@ -24951,6 +25659,7 @@ var PlannerModelMemory = class {
|
|
|
24951
25659
|
getStateDir,
|
|
24952
25660
|
globalDataDir,
|
|
24953
25661
|
grantScopeFor,
|
|
25662
|
+
grepRegexFromProbe,
|
|
24954
25663
|
hasHiddenDetail,
|
|
24955
25664
|
hasTmux,
|
|
24956
25665
|
holdPrompt,
|