@ordewell/core 0.4.16 → 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.d.mts CHANGED
@@ -2255,9 +2255,32 @@ declare class OpenCodeAdapter implements AgentAdapter {
2255
2255
  private opts;
2256
2256
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
2257
2257
  private turnHasText;
2258
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
2259
+ private lastAssistantId;
2258
2260
  constructor(deps: AgentProcessDeps);
2259
2261
  start(opts: AgentStartOptions): Promise<void>;
2260
2262
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2263
+ /**
2264
+ * Turn one settled assistant message into events. The settled response is
2265
+ * authoritative: it names the assistant message, so its parts are the ones
2266
+ * that make up the reply. Tool parts already seen live are deduplicated by
2267
+ * call id; anything the stream missed (including a stream that never
2268
+ * connected) arrives here.
2269
+ */
2270
+ private settle;
2271
+ /**
2272
+ * Poll the session for this turn's assistant message after the POST's socket
2273
+ * died under it. Node's global `fetch` is undici, which aborts a request
2274
+ * whose response headers have not arrived within 300s — and OpenCode sends
2275
+ * none until the turn is done, so any turn past five minutes fails as
2276
+ * `TypeError: fetch failed` while the server is still working. The message
2277
+ * exists server-side either way, so it is waited for and read back.
2278
+ *
2279
+ * A message that exists but has not completed is progress, not an answer:
2280
+ * it refreshes the watchdog and the poll continues.
2281
+ */
2282
+ private recoverReply;
2283
+ private newestAssistantId;
2261
2284
  /**
2262
2285
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
2263
2286
  * moves through pending → running → completed, so parts are keyed by id and
@@ -2302,7 +2325,7 @@ declare function mapAgentTool(name: string): MappedTool;
2302
2325
  */
2303
2326
  declare function normalizeAgentArgs(tool: ResearchToolType, args: Record<string, unknown>): Record<string, unknown>;
2304
2327
 
2305
- declare const BUILTIN_SKILL_NAMES: readonly ["grilling", "to-spec"];
2328
+ declare const BUILTIN_SKILL_NAMES: readonly ["grilling", "to-spec", "improve-codebase-architecture"];
2306
2329
  interface SkillMetadata {
2307
2330
  name: string;
2308
2331
  description: string;
package/dist/index.d.ts CHANGED
@@ -2255,9 +2255,32 @@ declare class OpenCodeAdapter implements AgentAdapter {
2255
2255
  private opts;
2256
2256
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
2257
2257
  private turnHasText;
2258
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
2259
+ private lastAssistantId;
2258
2260
  constructor(deps: AgentProcessDeps);
2259
2261
  start(opts: AgentStartOptions): Promise<void>;
2260
2262
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2263
+ /**
2264
+ * Turn one settled assistant message into events. The settled response is
2265
+ * authoritative: it names the assistant message, so its parts are the ones
2266
+ * that make up the reply. Tool parts already seen live are deduplicated by
2267
+ * call id; anything the stream missed (including a stream that never
2268
+ * connected) arrives here.
2269
+ */
2270
+ private settle;
2271
+ /**
2272
+ * Poll the session for this turn's assistant message after the POST's socket
2273
+ * died under it. Node's global `fetch` is undici, which aborts a request
2274
+ * whose response headers have not arrived within 300s — and OpenCode sends
2275
+ * none until the turn is done, so any turn past five minutes fails as
2276
+ * `TypeError: fetch failed` while the server is still working. The message
2277
+ * exists server-side either way, so it is waited for and read back.
2278
+ *
2279
+ * A message that exists but has not completed is progress, not an answer:
2280
+ * it refreshes the watchdog and the poll continues.
2281
+ */
2282
+ private recoverReply;
2283
+ private newestAssistantId;
2261
2284
  /**
2262
2285
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
2263
2286
  * moves through pending → running → completed, so parts are keyed by id and
@@ -2302,7 +2325,7 @@ declare function mapAgentTool(name: string): MappedTool;
2302
2325
  */
2303
2326
  declare function normalizeAgentArgs(tool: ResearchToolType, args: Record<string, unknown>): Record<string, unknown>;
2304
2327
 
2305
- declare const BUILTIN_SKILL_NAMES: readonly ["grilling", "to-spec"];
2328
+ declare const BUILTIN_SKILL_NAMES: readonly ["grilling", "to-spec", "improve-codebase-architecture"];
2306
2329
  interface SkillMetadata {
2307
2330
  name: string;
2308
2331
  description: string;
package/dist/index.js CHANGED
@@ -11528,6 +11528,8 @@ ${item.text}` : item.text });
11528
11528
  var SERVER_READY_TIMEOUT_MS = 3e4;
11529
11529
  var STDERR_TAIL_CHARS2 = 4e3;
11530
11530
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
11531
+ var RECOVERY_POLL_INTERVAL_MS = 2e3;
11532
+ var RECOVERY_TIMEOUT_MS = 9e5;
11531
11533
  var DISABLED_TOOLS = {
11532
11534
  question: false,
11533
11535
  edit: false,
@@ -11540,6 +11542,33 @@ function splitModelId(id) {
11540
11542
  if (slash <= 0 || slash === id.length - 1) return null;
11541
11543
  return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
11542
11544
  }
11545
+ function describeError(err) {
11546
+ const visited = /* @__PURE__ */ new Set();
11547
+ const lines = [];
11548
+ let current = err;
11549
+ while (current && !visited.has(current)) {
11550
+ visited.add(current);
11551
+ if (!(current instanceof Error)) {
11552
+ lines.push(String(current));
11553
+ break;
11554
+ }
11555
+ const code = current.code;
11556
+ const suffix = typeof code === "string" && !current.message.includes(code) ? ` (${code})` : "";
11557
+ lines.push(`${current.message}${suffix}`);
11558
+ current = current.cause;
11559
+ }
11560
+ return lines.join(": ");
11561
+ }
11562
+ function delay(ms, signal) {
11563
+ return new Promise((resolve4) => {
11564
+ const timer = setTimeout(resolve4, ms);
11565
+ timer.unref?.();
11566
+ signal?.addEventListener("abort", () => {
11567
+ clearTimeout(timer);
11568
+ resolve4();
11569
+ }, { once: true });
11570
+ });
11571
+ }
11543
11572
  var OpenCodeAdapter = class {
11544
11573
  constructor(deps) {
11545
11574
  this.deps = deps;
@@ -11555,6 +11584,8 @@ var OpenCodeAdapter = class {
11555
11584
  opts = null;
11556
11585
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
11557
11586
  turnHasText = false;
11587
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
11588
+ lastAssistantId = null;
11558
11589
  async start(opts) {
11559
11590
  this.opts = opts;
11560
11591
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
@@ -11608,6 +11639,8 @@ ${this.stderrTail.trim()}` : ""}`);
11608
11639
  const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
11609
11640
  if (existing?.id) {
11610
11641
  this.sessionId = existing.id;
11642
+ const history = await this.json("GET", `/session/${existing.id}/message`).catch(() => null);
11643
+ this.lastAssistantId = this.newestAssistantId(history);
11611
11644
  return;
11612
11645
  }
11613
11646
  }
@@ -11658,29 +11691,76 @@ ${this.stderrTail.trim()}` : ""}`);
11658
11691
  this.dispose();
11659
11692
  return;
11660
11693
  }
11661
- const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
11662
- if (failure) {
11663
- onEvent({ type: "error", message: failure });
11664
- return;
11665
- }
11666
- const assistantId = reply?.info?.id;
11667
- for (const part of reply?.parts ?? []) {
11668
- if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
11669
- this.emitPart(part, seen, onEvent);
11670
- }
11671
- onEvent({ type: "turn_end" });
11694
+ this.settle(reply, seen, onEvent);
11672
11695
  } catch (err) {
11673
11696
  if (signal?.aborted) {
11674
11697
  this.dispose();
11675
11698
  return;
11676
11699
  }
11677
- onEvent({ type: "error", message: `The OpenCode planner turn failed: ${err instanceof Error ? err.message : String(err)}` });
11700
+ const recovered = this.exited ? null : await this.recoverReply(signal, onActivity);
11701
+ if (recovered) {
11702
+ this.settle(recovered, seen, onEvent);
11703
+ return;
11704
+ }
11705
+ onEvent({ type: "error", message: `The OpenCode planner turn failed: ${describeError(err)}` });
11678
11706
  } finally {
11679
11707
  streamAbort.abort();
11680
11708
  await live.catch(() => {
11681
11709
  });
11682
11710
  }
11683
11711
  }
11712
+ /**
11713
+ * Turn one settled assistant message into events. The settled response is
11714
+ * authoritative: it names the assistant message, so its parts are the ones
11715
+ * that make up the reply. Tool parts already seen live are deduplicated by
11716
+ * call id; anything the stream missed (including a stream that never
11717
+ * connected) arrives here.
11718
+ */
11719
+ settle(reply, seen, onEvent) {
11720
+ const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
11721
+ if (failure) {
11722
+ onEvent({ type: "error", message: failure });
11723
+ return;
11724
+ }
11725
+ const assistantId = reply?.info?.id;
11726
+ for (const part of reply?.parts ?? []) {
11727
+ if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
11728
+ this.emitPart(part, seen, onEvent);
11729
+ }
11730
+ if (assistantId) this.lastAssistantId = assistantId;
11731
+ onEvent({ type: "turn_end" });
11732
+ }
11733
+ /**
11734
+ * Poll the session for this turn's assistant message after the POST's socket
11735
+ * died under it. Node's global `fetch` is undici, which aborts a request
11736
+ * whose response headers have not arrived within 300s — and OpenCode sends
11737
+ * none until the turn is done, so any turn past five minutes fails as
11738
+ * `TypeError: fetch failed` while the server is still working. The message
11739
+ * exists server-side either way, so it is waited for and read back.
11740
+ *
11741
+ * A message that exists but has not completed is progress, not an answer:
11742
+ * it refreshes the watchdog and the poll continues.
11743
+ */
11744
+ async recoverReply(signal, onActivity) {
11745
+ const deadline = Date.now() + RECOVERY_TIMEOUT_MS;
11746
+ for (; ; ) {
11747
+ if (signal?.aborted || this.exited || this.disposed || !this.sessionId) return null;
11748
+ const messages = await this.json("GET", `/session/${this.sessionId}/message`).catch(() => null);
11749
+ const pending = Array.isArray(messages) ? [...messages].reverse().find((m) => m.info?.role === "assistant" && m.info.id && m.info.id !== this.lastAssistantId) : void 0;
11750
+ if (pending?.info?.time?.completed || pending?.info?.error) return pending;
11751
+ if (pending) onActivity?.();
11752
+ if (Date.now() >= deadline) return null;
11753
+ await delay(RECOVERY_POLL_INTERVAL_MS, signal);
11754
+ }
11755
+ }
11756
+ newestAssistantId(messages) {
11757
+ if (!Array.isArray(messages)) return null;
11758
+ for (let i = messages.length - 1; i >= 0; i--) {
11759
+ const info = messages[i]?.info;
11760
+ if (info?.role === "assistant" && info.id) return info.id;
11761
+ }
11762
+ return null;
11763
+ }
11684
11764
  /**
11685
11765
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
11686
11766
  * moves through pending → running → completed, so parts are keyed by id and
@@ -12586,7 +12666,7 @@ function builtinSkillsDir() {
12586
12666
  }
12587
12667
 
12588
12668
  // src/services/SkillsService.ts
12589
- var BUILTIN_SKILL_NAMES = ["grilling", "to-spec"];
12669
+ var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
12590
12670
  var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
12591
12671
  function parseSkillFile(filePath, name, source) {
12592
12672
  const raw = fs4.readFileSync(filePath, "utf8");