@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.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 pathLikeArgs(command, opts = {}) {
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
- return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn);
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(root, expandHome(target));
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 p of pathLikeArgs(command, { dialect: this.researchShell.dialect })) {
3382
- const { abs, inside } = resolveWithin(root, p);
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, { dialect: this.researchShell.dialect });
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
- function buildFallbackGrepArgs(pattern, opts, root) {
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 { tier, reason } = classifyCommand(command, { dialect });
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 = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
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 (event.type === "turn_start" && this.generations.get(task.id) === gen) this.resumeIdle(task.id);
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 STREAM_CONNECT_TIMEOUT_MS = 5e3;
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 AUTO_APPROVALS = "auto";
12784
- function splitModelId(id) {
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 delay(ms, signal) {
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 newTurnState() {
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 permissionReply(decision) {
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 = opts.kind === "task" ? randomBytes2(24).toString("base64url") : null;
13550
+ const password = randomBytes2(24).toString("base64url");
12905
13551
  this.process = this.deps.spawn(launch.file, launch.args, {
12906
- env: runnerEnv(PATH, password ? { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password } : workspaceEnv),
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
- if (password) this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
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 = newTurnState();
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 ? splitModelId(this.planner.model) : null;
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 = newTurnState();
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 ? splitModelId(task.model) : null;
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 delay(STATUS_POLL_INTERVAL_MS, signal);
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, permissionReply(decision)).catch(() => {
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 delay(RECOVERY_POLL_INTERVAL_MS, signal);
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 === AUTO_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, STREAM_CONNECT_TIMEOUT_MS);
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.output.liveTail(task.id, { maxLines: OPS_RETRY_TAIL_LINES })?.text : void 0
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,