@cortexkit/opencode-magic-context 0.49.18 → 0.49.19

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.
Files changed (86) hide show
  1. package/dist/auto-search-worker.js +6 -2
  2. package/dist/chunk-embedding-write-worker.js +2 -2
  3. package/dist/{context-m897gfp9.js → context-j31m2dv7.js} +16 -12
  4. package/dist/features/magic-context/dreamer/classify.d.ts +3 -0
  5. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  6. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts +11 -0
  7. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/map-memories.d.ts +3 -0
  9. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/verify.d.ts +3 -0
  11. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  12. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-meta-persisted.d.ts +13 -0
  15. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  16. package/dist/hooks/magic-context/auto-search-worker-evaluation.fixture.d.ts +2 -0
  17. package/dist/hooks/magic-context/auto-search-worker-evaluation.fixture.d.ts.map +1 -0
  18. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  19. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +13 -0
  20. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
  21. package/dist/hooks/magic-context/system-date-refresh.d.ts +15 -2
  22. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +2 -0
  25. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  27. package/dist/{index-936py2wm.js → index-3esx8tkf.js} +60 -26
  28. package/dist/{index-v8awdgvd.js → index-3h4dp9wb.js} +1 -1
  29. package/dist/{index-24jbw52d.js → index-4911rn0b.js} +3 -3
  30. package/dist/{index-qn7xstt2.js → index-63k48rv9.js} +1 -2
  31. package/dist/{index-rmwykbbb.js → index-9zcsy3hh.js} +16 -1
  32. package/dist/{index-r4nkrs05.js → index-c1p88z1y.js} +3 -3
  33. package/dist/{index-s7j8tvhv.js → index-dxmq29t1.js} +3 -3
  34. package/dist/{index-6s5941tm.js → index-edmpdxcw.js} +1 -1
  35. package/dist/{index-e4xv8890.js → index-jhghdnsm.js} +26 -12
  36. package/dist/{index-fgncb0ng.js → index-jrr131e0.js} +2 -2
  37. package/dist/{index-s1ex9y6p.js → index-rpxdjkm0.js} +165 -27
  38. package/dist/{index-x4njr8df.js → index-se4m2g6w.js} +1 -1
  39. package/dist/index.js +20 -16
  40. package/dist/{message-index-bounded-n1frdn85.js → message-index-bounded-zyc8h32y.js} +2 -2
  41. package/dist/{message-index-66jv6fh4.js → message-index-gtpqnj8w.js} +2 -2
  42. package/dist/migration-worker.js +2 -2
  43. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  44. package/dist/plugin/messages-transform.d.ts.map +1 -1
  45. package/dist/plugin/tool-registry.d.ts.map +1 -1
  46. package/dist/raw-ordinal-warmup-worker.js +3 -3
  47. package/dist/{read-session-chunk-v06g3vka.js → read-session-chunk-jjb48bt9.js} +1 -1
  48. package/dist/{recomp-orchestrator-ghpk2dtq.js → recomp-orchestrator-999jj4bc.js} +7 -7
  49. package/dist/{search-archive-4d53y23e.js → search-archive-6vm0e2f5.js} +2 -2
  50. package/dist/{search-nkf271d4.js → search-xsqk1bj4.js} +5 -5
  51. package/dist/shared/model-suggestion-retry.d.ts +30 -1
  52. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  53. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  54. package/dist/tools/ctx-expand/rust-mode-tag.test-support.d.ts +15 -0
  55. package/dist/tools/ctx-expand/rust-mode-tag.test-support.d.ts.map +1 -0
  56. package/dist/tools/ctx-expand/tools.d.ts +11 -0
  57. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  58. package/dist/{transform-postprocess-phase-zpgav0h7.js → transform-postprocess-phase-jj99r7z2.js} +7 -7
  59. package/dist/v2/auto-search-worker.js +6 -2
  60. package/dist/v2/chunk-embedding-write-worker.js +2 -2
  61. package/dist/v2/{context-f2xh7ba4.js → context-y5yamxak.js} +85 -33
  62. package/dist/v2/hooks/tools.d.ts.map +1 -1
  63. package/dist/v2/{message-index-48f7hw7a.js → message-index-8vz2pq76.js} +2 -2
  64. package/dist/v2/{message-index-bounded-w3hfm29z.js → message-index-bounded-spd8r7pj.js} +2 -2
  65. package/dist/v2/migration-worker.js +2 -2
  66. package/dist/v2/raw-ordinal-warmup-worker.js +3 -3
  67. package/dist/v2/{read-session-chunk-zpgmxwte.js → read-session-chunk-cwkxrmns.js} +1 -1
  68. package/dist/v2/{recomp-orchestrator-4vfxej9t.js → recomp-orchestrator-f6m9jr7p.js} +6 -6
  69. package/dist/v2/{search-archive-vv48s2aa.js → search-archive-wve3tq1k.js} +2 -2
  70. package/dist/v2/{search-kza414f0.js → search-hwg7jfbj.js} +5 -5
  71. package/dist/v2/{server-2ehj0bw7.js → server-2ar62dyx.js} +1 -1
  72. package/dist/v2/{server-qv7f0hhe.js → server-45q69hkq.js} +1 -1
  73. package/dist/v2/{server-bfpda1tb.js → server-6vdzn1td.js} +165 -27
  74. package/dist/v2/{server-xhab8jx8.js → server-9hx7g5jp.js} +16 -1
  75. package/dist/v2/{server-a2eqhkxr.js → server-bmymkx73.js} +2 -2
  76. package/dist/v2/{server-1nyybqan.js → server-gsxjv2q2.js} +3 -3
  77. package/dist/v2/{server-my7k4eds.js → server-hm38q7r0.js} +1 -1
  78. package/dist/v2/{server-gpvay0xz.js → server-xq7dfjke.js} +1 -2
  79. package/dist/v2/server.js +2 -2
  80. package/dist/v2/{wrapup-orchestrator-psqz7yh0.js → wrapup-orchestrator-embdz8gj.js} +6 -6
  81. package/dist/{wrapup-orchestrator-s9zc5b00.js → wrapup-orchestrator-whhbjmaz.js} +8 -8
  82. package/package.json +1 -1
  83. package/src/shared/model-suggestion-retry.test.ts +166 -0
  84. package/src/shared/model-suggestion-retry.ts +165 -14
  85. package/src/shared/prompt-async-transport.test.ts +107 -0
  86. package/src/shared/prompt-async-transport.ts +39 -0
@@ -645,3 +645,110 @@ test("session errors belonging to another child do not fail the run", async () =
645
645
  await promptAsyncAndWaitForIdle(host.client, request(), { pollIntervalMs: 1 });
646
646
  expect(host.polls()).toBe(1);
647
647
  });
648
+
649
+ describe("prompt_async transport and the first-output deadline", () => {
650
+ /**
651
+ * A child session where each model answers its own way. "dead" leaves the
652
+ * empty assistant row the host writes before the provider responds and stays
653
+ * busy forever; "partial" streams a text part and then stays busy; "answer"
654
+ * settles with a final assistant message on the next poll.
655
+ */
656
+ function modelHost(behaviours: Record<string, "dead" | "partial" | "answer">) {
657
+ const messages: Message[] = [];
658
+ let current: "dead" | "partial" | "answer" | null = null;
659
+ const sent: string[] = [];
660
+ const promptAsync = mock(
661
+ async (request: { body: { model?: { providerID: string; modelID: string } } }) => {
662
+ const model = request.body.model
663
+ ? `${request.body.model.providerID}/${request.body.model.modelID}`
664
+ : "primary";
665
+ sent.push(model);
666
+ current = behaviours[model] ?? "answer";
667
+ messages.push({
668
+ info: { id: `msg_user_${messages.length}`, role: "user", time: { created: 1 } },
669
+ parts: [{ type: "text", text: "go" }],
670
+ });
671
+ messages.push({
672
+ info: {
673
+ id: `msg_assistant_${messages.length}`,
674
+ role: "assistant",
675
+ time: { created: 2 },
676
+ },
677
+ parts: current === "partial" ? [{ type: "text", text: "<classify>" }] : [],
678
+ });
679
+ return { data: undefined };
680
+ },
681
+ );
682
+ const abort = mock(async () => {
683
+ current = null;
684
+ return { data: true };
685
+ });
686
+ const status = mock(async () => {
687
+ if (current === "dead" || current === "partial")
688
+ return { data: { "ses-child": { type: "busy" } } };
689
+ if (current === "answer") {
690
+ const last = messages.at(-1);
691
+ if (last && !last.info.time.completed) {
692
+ last.info.time.completed = 3;
693
+ last.info.finish = "stop";
694
+ last.parts = [{ type: "text", text: "<classify></classify>" }];
695
+ }
696
+ }
697
+ return { data: {} };
698
+ });
699
+ const list = mock(async () => ({ data: messages.map((message) => ({ ...message })) }));
700
+ return {
701
+ client: { session: { promptAsync, abort, status, messages: list } } as never,
702
+ sent,
703
+ abort,
704
+ };
705
+ }
706
+
707
+ async function runChain(
708
+ host: ReturnType<typeof modelHost>,
709
+ timeoutMs: number,
710
+ firstOutputTimeoutMs: number,
711
+ ) {
712
+ const transport = createPromptAsyncTransport(host.client, "ses-child", {
713
+ pollIntervalMs: 5,
714
+ });
715
+ return promptSyncWithValidatedOutputRetry(
716
+ host.client,
717
+ {
718
+ ...request(),
719
+ body: {
720
+ model: { providerID: "opencode", modelID: "free-model" },
721
+ parts: [{ type: "text", text: "go" }],
722
+ },
723
+ },
724
+ {
725
+ transport,
726
+ timeoutMs,
727
+ firstOutputTimeoutMs,
728
+ fallbackModels: ["anthropic/claude-sonnet-5-5"],
729
+ fetchOutput: async () => "<classify></classify>",
730
+ validateOutput: (output: string) => output,
731
+ },
732
+ );
733
+ }
734
+
735
+ test("a model that never answers is cut at the deadline and the fallback completes", async () => {
736
+ const host = modelHost({ "opencode/free-model": "dead" });
737
+ const startedAt = Date.now();
738
+
739
+ const run = await runChain(host, 5_000, 60);
740
+
741
+ expect(run.attempt.label).toBe("anthropic/claude-sonnet-5-5");
742
+ expect(host.sent).toEqual(["opencode/free-model", "anthropic/claude-sonnet-5-5"]);
743
+ // The dead model's run is stopped on the host before the fallback is sent.
744
+ expect(host.abort).toHaveBeenCalledWith({ path: { id: "ses-child" } });
745
+ expect(Date.now() - startedAt).toBeLessThan(2_000);
746
+ });
747
+
748
+ test("a model that streamed part of an answer keeps its timeout and does not fall back", async () => {
749
+ const host = modelHost({ "opencode/free-model": "partial" });
750
+
751
+ await expect(runChain(host, 200, 40)).rejects.toThrow("prompt timed out after 200ms");
752
+ expect(host.sent).toEqual(["opencode/free-model"]);
753
+ });
754
+ });
@@ -11,6 +11,7 @@ import { sumTokensFromChildMessages } from "../features/magic-context/subagent-t
11
11
  import { describeAssistantError } from "./assistant-message-extractor";
12
12
  import { sessionLog } from "./logger";
13
13
  import type { PromptArgs, PromptTransport } from "./model-suggestion-retry";
14
+ import { noteModelActivityMonitored, noteModelOutputReceived } from "./model-suggestion-retry";
14
15
 
15
16
  type Client = ReturnType<typeof createOpencodeClient> | undefined;
16
17
 
@@ -202,6 +203,29 @@ function isTerminalAssistant(message: unknown): boolean {
202
203
  );
203
204
  }
204
205
 
206
+ /**
207
+ * True when an assistant row shows the model has answered: any token usage, or a
208
+ * non-empty text or reasoning part, or a tool call. A row the host creates before
209
+ * the provider responds carries none of these.
210
+ */
211
+ function showsModelOutput(message: unknown): boolean {
212
+ if (infoOf(message).role !== "assistant") return false;
213
+ const tokens = sumTokensFromChildMessages([message]);
214
+ if (tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite > 0) return true;
215
+ const parts = (message as { parts?: unknown } | null)?.parts;
216
+ if (!Array.isArray(parts)) return false;
217
+ return parts.some((part) => {
218
+ if (!part || typeof part !== "object") return false;
219
+ const { type, text } = part as { type?: unknown; text?: unknown };
220
+ if (type === "tool") return true;
221
+ return (
222
+ (type === "text" || type === "reasoning") &&
223
+ typeof text === "string" &&
224
+ text.trim().length > 0
225
+ );
226
+ });
227
+ }
228
+
205
229
  function unwrapData(response: unknown): unknown {
206
230
  if (response && typeof response === "object" && "data" in response) {
207
231
  return (response as { data?: unknown }).data;
@@ -324,6 +348,10 @@ export async function promptAsyncAndWaitForIdle(
324
348
  }
325
349
  let sentAt = Date.now();
326
350
  let sawBusy = false;
351
+ // From here on this wait reports whether the model has produced anything,
352
+ // which lets the model chain tell a silent model from a slow answer.
353
+ noteModelActivityMonitored(signal);
354
+ let sawOutput = false;
327
355
 
328
356
  for (;;) {
329
357
  await sleep(pollIntervalMs, signal);
@@ -341,6 +369,17 @@ export async function promptAsyncAndWaitForIdle(
341
369
  }
342
370
  const status = await readStatus(session, sessionId, dir, signal);
343
371
  const messages = await readMessages(session, sessionId, dir, signal);
372
+ if (
373
+ !sawOutput &&
374
+ messages.some((message) => {
375
+ const id = messageId(message);
376
+ // Rows from before this send belong to an earlier attempt.
377
+ return id !== null && !initialBaseline.has(id) && showsModelOutput(message);
378
+ })
379
+ ) {
380
+ sawOutput = true;
381
+ noteModelOutputReceived(signal);
382
+ }
344
383
  if (budget) {
345
384
  const latest = messages.at(-1);
346
385
  const latestId = messageId(latest);