@ordewell/core 0.4.17 → 0.4.18

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
@@ -9695,6 +9695,8 @@ ${item.text}` : item.text });
9695
9695
  var SERVER_READY_TIMEOUT_MS = 3e4;
9696
9696
  var STDERR_TAIL_CHARS2 = 4e3;
9697
9697
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
9698
+ var RECOVERY_POLL_INTERVAL_MS = 2e3;
9699
+ var RECOVERY_TIMEOUT_MS = 9e5;
9698
9700
  var DISABLED_TOOLS = {
9699
9701
  question: false,
9700
9702
  edit: false,
@@ -9707,6 +9709,33 @@ function splitModelId(id) {
9707
9709
  if (slash <= 0 || slash === id.length - 1) return null;
9708
9710
  return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
9709
9711
  }
9712
+ function describeError(err) {
9713
+ const visited = /* @__PURE__ */ new Set();
9714
+ const lines = [];
9715
+ let current = err;
9716
+ while (current && !visited.has(current)) {
9717
+ visited.add(current);
9718
+ if (!(current instanceof Error)) {
9719
+ lines.push(String(current));
9720
+ break;
9721
+ }
9722
+ const code = current.code;
9723
+ const suffix = typeof code === "string" && !current.message.includes(code) ? ` (${code})` : "";
9724
+ lines.push(`${current.message}${suffix}`);
9725
+ current = current.cause;
9726
+ }
9727
+ return lines.join(": ");
9728
+ }
9729
+ function delay(ms, signal) {
9730
+ return new Promise((resolve4) => {
9731
+ const timer = setTimeout(resolve4, ms);
9732
+ timer.unref?.();
9733
+ signal?.addEventListener("abort", () => {
9734
+ clearTimeout(timer);
9735
+ resolve4();
9736
+ }, { once: true });
9737
+ });
9738
+ }
9710
9739
  var OpenCodeAdapter = class {
9711
9740
  constructor(deps) {
9712
9741
  this.deps = deps;
@@ -9722,6 +9751,8 @@ var OpenCodeAdapter = class {
9722
9751
  opts = null;
9723
9752
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
9724
9753
  turnHasText = false;
9754
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
9755
+ lastAssistantId = null;
9725
9756
  async start(opts) {
9726
9757
  this.opts = opts;
9727
9758
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
@@ -9775,6 +9806,8 @@ ${this.stderrTail.trim()}` : ""}`);
9775
9806
  const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
9776
9807
  if (existing?.id) {
9777
9808
  this.sessionId = existing.id;
9809
+ const history = await this.json("GET", `/session/${existing.id}/message`).catch(() => null);
9810
+ this.lastAssistantId = this.newestAssistantId(history);
9778
9811
  return;
9779
9812
  }
9780
9813
  }
@@ -9825,29 +9858,76 @@ ${this.stderrTail.trim()}` : ""}`);
9825
9858
  this.dispose();
9826
9859
  return;
9827
9860
  }
9828
- const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
9829
- if (failure) {
9830
- onEvent({ type: "error", message: failure });
9831
- return;
9832
- }
9833
- const assistantId = reply?.info?.id;
9834
- for (const part of reply?.parts ?? []) {
9835
- if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
9836
- this.emitPart(part, seen, onEvent);
9837
- }
9838
- onEvent({ type: "turn_end" });
9861
+ this.settle(reply, seen, onEvent);
9839
9862
  } catch (err) {
9840
9863
  if (signal?.aborted) {
9841
9864
  this.dispose();
9842
9865
  return;
9843
9866
  }
9844
- onEvent({ type: "error", message: `The OpenCode planner turn failed: ${err instanceof Error ? err.message : String(err)}` });
9867
+ const recovered = this.exited ? null : await this.recoverReply(signal, onActivity);
9868
+ if (recovered) {
9869
+ this.settle(recovered, seen, onEvent);
9870
+ return;
9871
+ }
9872
+ onEvent({ type: "error", message: `The OpenCode planner turn failed: ${describeError(err)}` });
9845
9873
  } finally {
9846
9874
  streamAbort.abort();
9847
9875
  await live.catch(() => {
9848
9876
  });
9849
9877
  }
9850
9878
  }
9879
+ /**
9880
+ * Turn one settled assistant message into events. The settled response is
9881
+ * authoritative: it names the assistant message, so its parts are the ones
9882
+ * that make up the reply. Tool parts already seen live are deduplicated by
9883
+ * call id; anything the stream missed (including a stream that never
9884
+ * connected) arrives here.
9885
+ */
9886
+ settle(reply, seen, onEvent) {
9887
+ const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
9888
+ if (failure) {
9889
+ onEvent({ type: "error", message: failure });
9890
+ return;
9891
+ }
9892
+ const assistantId = reply?.info?.id;
9893
+ for (const part of reply?.parts ?? []) {
9894
+ if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
9895
+ this.emitPart(part, seen, onEvent);
9896
+ }
9897
+ if (assistantId) this.lastAssistantId = assistantId;
9898
+ onEvent({ type: "turn_end" });
9899
+ }
9900
+ /**
9901
+ * Poll the session for this turn's assistant message after the POST's socket
9902
+ * died under it. Node's global `fetch` is undici, which aborts a request
9903
+ * whose response headers have not arrived within 300s — and OpenCode sends
9904
+ * none until the turn is done, so any turn past five minutes fails as
9905
+ * `TypeError: fetch failed` while the server is still working. The message
9906
+ * exists server-side either way, so it is waited for and read back.
9907
+ *
9908
+ * A message that exists but has not completed is progress, not an answer:
9909
+ * it refreshes the watchdog and the poll continues.
9910
+ */
9911
+ async recoverReply(signal, onActivity) {
9912
+ const deadline = Date.now() + RECOVERY_TIMEOUT_MS;
9913
+ for (; ; ) {
9914
+ if (signal?.aborted || this.exited || this.disposed || !this.sessionId) return null;
9915
+ const messages = await this.json("GET", `/session/${this.sessionId}/message`).catch(() => null);
9916
+ const pending = Array.isArray(messages) ? [...messages].reverse().find((m) => m.info?.role === "assistant" && m.info.id && m.info.id !== this.lastAssistantId) : void 0;
9917
+ if (pending?.info?.time?.completed || pending?.info?.error) return pending;
9918
+ if (pending) onActivity?.();
9919
+ if (Date.now() >= deadline) return null;
9920
+ await delay(RECOVERY_POLL_INTERVAL_MS, signal);
9921
+ }
9922
+ }
9923
+ newestAssistantId(messages) {
9924
+ if (!Array.isArray(messages)) return null;
9925
+ for (let i = messages.length - 1; i >= 0; i--) {
9926
+ const info = messages[i]?.info;
9927
+ if (info?.role === "assistant" && info.id) return info.id;
9928
+ }
9929
+ return null;
9930
+ }
9851
9931
  /**
9852
9932
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
9853
9933
  * moves through pending → running → completed, so parts are keyed by id and