@retaw106/mmc-oc 2.0.0 → 2.1.0

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 (100) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/config/live-run-config.d.ts.map +1 -1
  3. package/dist/config-init.js +1 -1
  4. package/dist/{context-95zkksn3.js → context-vhcstgs3.js} +1812 -814
  5. package/dist/features/magic-context/memory/types.d.ts +1 -0
  6. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  7. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  8. package/dist/features/magic-context/session-project-storage.d.ts +8 -2
  9. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-meta-persisted.d.ts +12 -0
  11. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  12. package/dist/features/magic-context/storage-meta-session.d.ts +6 -0
  13. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-tags.d.ts +2 -2
  15. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  16. package/dist/features/magic-context/transform-decision-log.d.ts +3 -1
  17. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  18. package/dist/hooks/magic-context/compartment-runner-types.d.ts +3 -5
  19. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/compartment-runner.d.ts +2 -1
  21. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  24. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  25. package/dist/hooks/magic-context/historian-drain-gate.d.ts +11 -0
  26. package/dist/hooks/magic-context/historian-drain-gate.d.ts.map +1 -0
  27. package/dist/hooks/magic-context/lkg-replay-fit.d.ts +118 -0
  28. package/dist/hooks/magic-context/lkg-replay-fit.d.ts.map +1 -0
  29. package/dist/hooks/magic-context/lkg-replay.d.ts +2 -0
  30. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -28
  34. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  36. package/dist/{index-fg34g6a5.js → index-1j34a56j.js} +49 -8
  37. package/dist/{index-5pg0acf7.js → index-amycb7b1.js} +1 -1
  38. package/dist/{index-1mjhzqq7.js → index-dr0gepzw.js} +86 -23
  39. package/dist/{index-xt7y10x6.js → index-n0vnzz6h.js} +1 -1
  40. package/dist/{index-977gxfz0.js → index-ramz5kkm.js} +1 -1
  41. package/dist/{index-bdt9my9y.js → index-wtb9hzk6.js} +130 -50
  42. package/dist/index.js +1 -1
  43. package/dist/{message-time-backfill-zfernxbw.js → message-time-backfill-phrwp2fz.js} +1 -1
  44. package/dist/{read-session-chunk-hp464g13.js → read-session-chunk-82rkqa8j.js} +3 -3
  45. package/dist/{recomp-orchestrator-3qxeqwb9.js → recomp-orchestrator-vc5dkzy2.js} +6 -6
  46. package/dist/{send-session-notification-y29y9fsr.js → send-session-notification-rnc4nrsr.js} +3 -3
  47. package/dist/shared/harness.d.ts +1 -1
  48. package/dist/shared/harness.d.ts.map +1 -1
  49. package/dist/shared/historian-tool-expansions.d.ts +1 -1
  50. package/dist/shared/historian-tool-expansions.d.ts.map +1 -1
  51. package/dist/shared/historian-tool-template.d.ts +2 -2
  52. package/dist/shared/historian-tool-template.d.ts.map +1 -1
  53. package/dist/shared/model-resolution.d.ts +1 -1
  54. package/dist/shared/model-resolution.d.ts.map +1 -1
  55. package/dist/shared/sqlite.d.ts +14 -1
  56. package/dist/shared/sqlite.d.ts.map +1 -1
  57. package/dist/tools/ctx-expand/render.d.ts.map +1 -1
  58. package/dist/v2/fold/host-media.d.ts +4 -0
  59. package/dist/v2/fold/host-media.d.ts.map +1 -1
  60. package/dist/v2/fold/memory-cache.d.ts +70 -0
  61. package/dist/v2/fold/memory-cache.d.ts.map +1 -0
  62. package/dist/v2/fold/native-codec.d.ts +10 -0
  63. package/dist/v2/fold/native-codec.d.ts.map +1 -0
  64. package/dist/v2/fold/native-replay.d.ts +58 -0
  65. package/dist/v2/fold/native-replay.d.ts.map +1 -0
  66. package/dist/v2/fold/policy.d.ts +24 -0
  67. package/dist/v2/fold/policy.d.ts.map +1 -0
  68. package/dist/v2/hidden-child-record.d.ts +6 -0
  69. package/dist/v2/hidden-child-record.d.ts.map +1 -1
  70. package/dist/v2/hidden-completion.d.ts.map +1 -1
  71. package/dist/v2/hooks/context.d.ts.map +1 -1
  72. package/dist/v2/hooks/types.d.ts +6 -0
  73. package/dist/v2/hooks/types.d.ts.map +1 -1
  74. package/dist/v2/server.js +1763 -528
  75. package/dist/v2/store-reader.d.ts +16 -0
  76. package/dist/v2/store-reader.d.ts.map +1 -1
  77. package/dist/web/api.d.ts.map +1 -1
  78. package/dist/web/fixtures.d.ts +41 -0
  79. package/dist/web/fixtures.d.ts.map +1 -1
  80. package/dist/web/host-opencode.d.ts +9 -0
  81. package/dist/web/host-opencode.d.ts.map +1 -1
  82. package/dist/web/main.js +705 -53
  83. package/dist/web/memories.d.ts +18 -2
  84. package/dist/web/memories.d.ts.map +1 -1
  85. package/dist/web/ui/panel.css +2 -0
  86. package/dist/web/ui/panel.js +135 -77
  87. package/dist/web/views.d.ts +50 -9
  88. package/dist/web/views.d.ts.map +1 -1
  89. package/dist/{wrapup-orchestrator-wgb2nd19.js → wrapup-orchestrator-x9xamts2.js} +6 -6
  90. package/package.json +1 -1
  91. package/src/config/live-run-config.ts +28 -0
  92. package/src/shared/harness.ts +1 -1
  93. package/src/shared/historian-tool-defaults.json +6 -6
  94. package/src/shared/historian-tool-expansions.ts +65 -8
  95. package/src/shared/historian-tool-preview-defaults.json +13 -0
  96. package/src/shared/historian-tool-template.ts +48 -14
  97. package/src/shared/model-resolution.ts +1 -1
  98. package/src/shared/sqlite-acquisition-routing.test.ts +18 -0
  99. package/src/shared/sqlite.ts +100 -15
  100. package/src/shared/write-transaction-attribution.test.ts +2 -2
@@ -21,11 +21,11 @@ import {
21
21
  getActiveCompartmentRun,
22
22
  markActiveCompartmentRunPublished,
23
23
  registerActiveCompartmentRun
24
- } from "./index-bdt9my9y.js";
25
- import"./index-fg34g6a5.js";
24
+ } from "./index-wtb9hzk6.js";
25
+ import"./index-1j34a56j.js";
26
26
  import"./index-x37vg474.js";
27
- import"./index-1mjhzqq7.js";
28
- import"./index-977gxfz0.js";
27
+ import"./index-dr0gepzw.js";
28
+ import"./index-ramz5kkm.js";
29
29
  import {
30
30
  pushNotification2
31
31
  } from "./index-jdxxq8cz.js";
@@ -33,11 +33,11 @@ import"./index-5cep2r9n.js";
33
33
  import"./index-53g38ggt.js";
34
34
  import {
35
35
  sendStatusNotification2
36
- } from "./index-5pg0acf7.js";
36
+ } from "./index-amycb7b1.js";
37
37
  import {
38
38
  setRecompStarting2,
39
39
  setRecompTerminal2
40
- } from "./index-xt7y10x6.js";
40
+ } from "./index-n0vnzz6h.js";
41
41
  import"./index-33gprt9g.js";
42
42
 
43
43
  // src/hooks/magic-context/wrapup-orchestrator.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retaw106/mmc-oc",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -11,6 +11,14 @@ export function sampleLiveConfig<T extends MagicContextConfig>(boot: T, fresh: T
11
11
  const parts = path.split(".");
12
12
  let value: unknown = fresh;
13
13
  for (const part of parts) value = (value as Record<string, unknown> | undefined)?.[part];
14
+ if (value === undefined) {
15
+ // An unset live path must not create its parent objects. An empty `omp`
16
+ // block, for example, shadows the `pi` block it would otherwise fall back
17
+ // to, and the historian and dreamer lose their configured model. Only
18
+ // clear a leaf the boot config actually has, so removing a key live works.
19
+ removeLeaf(result, parts);
20
+ continue;
21
+ }
14
22
  let target = result;
15
23
  for (const part of parts.slice(0, -1)) {
16
24
  const child = target[part];
@@ -27,6 +35,26 @@ export function sampleLiveConfig<T extends MagicContextConfig>(boot: T, fresh: T
27
35
  return result as T;
28
36
  }
29
37
 
38
+ function removeLeaf(root: Record<string, unknown>, parts: string[]): void {
39
+ const parents = parts.slice(0, -1);
40
+ let node: Record<string, unknown> = root;
41
+ for (const part of parents) {
42
+ const child = node[part];
43
+ if (!child || typeof child !== "object" || Array.isArray(child)) return;
44
+ node = child as Record<string, unknown>;
45
+ }
46
+ const leaf = parts.at(-1);
47
+ if (leaf === undefined || !Object.hasOwn(node, leaf)) return;
48
+ // Copy the parent chain before deleting so the boot config object is never mutated.
49
+ let target = root;
50
+ for (const part of parents) {
51
+ const copy = { ...(target[part] as Record<string, unknown>) };
52
+ target[part] = copy;
53
+ target = copy;
54
+ }
55
+ delete target[leaf];
56
+ }
57
+
30
58
  export function changedLiveKeys(previous: MagicContextConfig, next: MagicContextConfig): string[] {
31
59
  const at = (config: MagicContextConfig, path: string): unknown =>
32
60
  path
@@ -20,7 +20,7 @@
20
20
  * boot-time constant per plugin instance. Cross-harness leakage is a
21
21
  * correctness bug, not a feature.
22
22
  */
23
- export type HarnessId = "opencode" | "opencode2" | "pi";
23
+ export type HarnessId = "opencode" | "opencode2" | "pi" | "omp";
24
24
 
25
25
  let currentHarness: HarnessId = "opencode";
26
26
  let harnessLocked = false;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "todowrite": "Todos (${input.todos.count}): ${input.todos.each(\"${status}: ${content}\")}",
3
3
  "question": "Asked user: ${input.questions[*].question.join(\" / \")} → ${output}",
4
- "task": "Task ${input.subagent_type}: ${input.prompt} → ${output.truncate(200)}",
5
- "ask": "Asked: ${input.question.truncate(250)} [options: ${input.options.join(\" / \")}] ${input.resolution} → ${output.truncate(300)}",
6
- "peer_send": "PM to ${input.agent}${input.agent_id}: ${input.message.truncate(400)}",
7
- "board": "Board ${input.verb}: ${input.ops.each(\"${op} ${item}${lane.title} ${state}${lane.status}${props.text} ${lane.items[*].text}\")}",
8
- "room": "Room ${input.action} ${input.room_id}: ${input.text.truncate(400)}",
9
- "work": "Work ${input.action} ${input.id}: ${input.prompt_file}${input.prompt.truncate(200)}${input.notes.truncate(200)} → ${output.truncate(160)}",
4
+ "task": "Task ${input.subagent_type}: ${input.prompt.truncate(600)} → ${output.truncate(200)}",
5
+ "ask": "Asked: ${input.question} [options: ${input.options.join(\" / \")}] ${input.text}${input.resolution} → ${output.truncate(300)}",
6
+ "peer_send": "PM ${input.recipient}: ${input.message}",
7
+ "board": "Board ${input.verb}${input.board_lane}: ${input.ops.each(\"${op} ${lane_label} ${item}${props.text}${lane.items} ${state}${props.state}${lane.status}${id}${title}\")}",
8
+ "room": "Room ${input.action} ${input.room_label}: ${input.room_detail}",
9
+ "work": "Work ${input.action} ${input.title}${input.id}: ${input.prompt.truncate(600)}${input.notes.truncate(600)} → ${output.truncate(160)}",
10
10
  "knowhow": "Looked up how-to: ${input.query}${input.id}",
11
11
  "mmc_note": "Note ${input.action} ${input.note_ids}: ${input.content} → ${output.truncate(160)}",
12
12
  "mmc_memory": "Memory ${input.action} ${input.category} ${input.ids}: ${input.content} → ${output.truncate(160)}"
@@ -1,18 +1,75 @@
1
1
  import defaults from "./historian-tool-defaults.json";
2
+ import previewDefaults from "./historian-tool-preview-defaults.json";
2
3
  import { renderToolTemplate, type ToolExpansionMap } from "./historian-tool-template";
3
4
 
4
5
  export { defaults as DEFAULT_TOOL_EXPANSIONS };
5
- export function expandToolPart(part: unknown, overrides?: ToolExpansionMap): string | null {
6
+
7
+ // Room actions, private-message addresses and board lanes have multiple input
8
+ // shapes. Derive readable labels only for built-ins; configured template overrides
9
+ // keep the unmodified tool input.
10
+ function builtinInput(tool: string, input: unknown): unknown {
11
+ if (!input || typeof input !== "object") return input;
12
+ const i = input as Record<string, unknown>;
13
+ if (tool === "peer_send") {
14
+ const external = i.external as Record<string, unknown> | undefined;
15
+ const recipient = i.reply_to_pmid
16
+ ? `reply to ${i.reply_to_pmid}`
17
+ : i.agent || i.agent_id
18
+ ? `to ${i.agent || i.agent_id}`
19
+ : external?.session_id || i.session_id
20
+ ? `to session ${external?.session_id || i.session_id}`
21
+ : "to unknown recipient";
22
+ return { ...i, recipient };
23
+ }
24
+ if (tool === "room") {
25
+ return {
26
+ ...i,
27
+ room_label: i.room_id || i.title,
28
+ room_detail:
29
+ i.action === "create" || i.action === "invite"
30
+ ? `invitees: ${renderToolTemplate("${input.invitees}", i)}`
31
+ : i.text,
32
+ };
33
+ }
34
+ if (tool === "board") {
35
+ return {
36
+ ...i,
37
+ board_lane: i.lane ? ` ${i.lane}` : "",
38
+ ops: Array.isArray(i.ops)
39
+ ? i.ops.map((op) =>
40
+ op && typeof op === "object"
41
+ ? {
42
+ ...op,
43
+ lane_label:
44
+ typeof op.lane === "string"
45
+ ? op.lane
46
+ : op.lane?.title || op.lane?.id,
47
+ }
48
+ : op,
49
+ )
50
+ : i.ops,
51
+ };
52
+ }
53
+ return input;
54
+ }
55
+
56
+ export function expandToolPart(
57
+ part: unknown,
58
+ overrides?: ToolExpansionMap,
59
+ legacyPreview = false,
60
+ ): string | null {
6
61
  if (!part || typeof part !== "object") return null;
7
62
  const p = part as Record<string, unknown>;
8
63
  if (p.type !== "tool" || typeof p.tool !== "string") return null;
9
- const template =
10
- overrides && Object.hasOwn(overrides, p.tool)
11
- ? overrides[p.tool]
12
- : Object.hasOwn(defaults, p.tool)
13
- ? defaults[p.tool as keyof typeof defaults]
14
- : undefined;
64
+ const builtinDefaults = legacyPreview ? previewDefaults : defaults;
65
+ const overridden = overrides && Object.hasOwn(overrides, p.tool);
66
+ const template = overridden
67
+ ? overrides[p.tool]
68
+ : Object.hasOwn(builtinDefaults, p.tool)
69
+ ? builtinDefaults[p.tool as keyof typeof defaults]
70
+ : undefined;
15
71
  if (typeof template !== "string") return null;
16
72
  const state = p.state as Record<string, unknown> | undefined;
17
- return renderToolTemplate(template, state?.input, state?.output ?? state?.error);
73
+ const input = overridden || legacyPreview ? state?.input : builtinInput(p.tool, state?.input);
74
+ return renderToolTemplate(template, input, state?.output ?? state?.error, legacyPreview);
18
75
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "todowrite": "Todos (${input.todos.count}): ${input.todos.each(\"${status}: ${content}\")}",
3
+ "question": "Asked user: ${input.questions[*].question.join(\" / \")} → ${output}",
4
+ "task": "Task ${input.subagent_type}: ${input.prompt} → ${output.truncate(200)}",
5
+ "ask": "Asked: ${input.question.truncate(250)} [options: ${input.options.join(\" / \")}] ${input.resolution} → ${output.truncate(300)}",
6
+ "peer_send": "PM to ${input.agent}${input.agent_id}: ${input.message.truncate(400)}",
7
+ "board": "Board ${input.verb}: ${input.ops.each(\"${op} ${item}${lane.title} ${state}${lane.status}${props.text} ${lane.items[*].text}\")}",
8
+ "room": "Room ${input.action} ${input.room_id}: ${input.text.truncate(400)}",
9
+ "work": "Work ${input.action} ${input.id}: ${input.prompt_file}${input.prompt.truncate(200)}${input.notes.truncate(200)} → ${output.truncate(160)}",
10
+ "knowhow": "Looked up how-to: ${input.query}${input.id}",
11
+ "mmc_note": "Note ${input.action} ${input.note_ids}: ${input.content} → ${output.truncate(160)}",
12
+ "mmc_memory": "Memory ${input.action} ${input.category} ${input.ids}: ${input.content} → ${output.truncate(160)}"
13
+ }
@@ -1,4 +1,4 @@
1
- /** A deliberately non-executable language for readable, bounded historian tool lines. */
1
+ /** A deliberately non-executable language for readable historian tool lines. */
2
2
  export type ToolExpansionMap = Record<string, string | false>;
3
3
  type Step = { field: string } | { index: number } | { project: string };
4
4
  type Expression = {
@@ -6,7 +6,7 @@ type Expression = {
6
6
  each?: Template;
7
7
  join?: string;
8
8
  count?: boolean;
9
- cap: number;
9
+ cap?: number;
10
10
  };
11
11
  type Template = Array<string | Expression>;
12
12
 
@@ -78,7 +78,7 @@ class Parser {
78
78
  if (this.source[this.pos] === "(") return this.error();
79
79
  } else break;
80
80
  }
81
- const expression: Expression = { steps, cap: 300 };
81
+ const expression: Expression = { steps };
82
82
  if (this.take(".each(")) {
83
83
  if (relative) return this.error();
84
84
  expression.each = parseTemplate(this.quoted(), true);
@@ -145,9 +145,24 @@ function compiled(source: string): Template | null {
145
145
  function oneLine(value: string): string {
146
146
  return value.replace(/[\r\n\u2028\u2029]+/g, " ");
147
147
  }
148
- function truncate(value: string, cap: number): string {
148
+ function truncate(value: string, cap?: number, legacyPreview = false): string {
149
149
  const chars = Array.from(oneLine(value));
150
- return chars.length > cap ? `${chars.slice(0, cap).join("")}…` : chars.join("");
150
+ if (cap === undefined || chars.length <= cap) return chars.join("");
151
+ if (legacyPreview) return `${chars.slice(0, cap).join("")}…`;
152
+ // Explicit caps stop after sentence-ending punctuation so a cut is not
153
+ // mistaken for an intact tool message. If no sentence fits, show only the marker.
154
+ let end = 0;
155
+ for (let i = 0; i < Math.min(cap, chars.length); i++) {
156
+ if (!".!?。!?".includes(chars[i])) continue;
157
+ let next = i + 1;
158
+ while (next < chars.length && "\"'”’)]".includes(chars[next])) next++;
159
+ if (
160
+ next <= cap &&
161
+ ("。!?".includes(chars[i]) || next === chars.length || /[ \t]/.test(chars[next]))
162
+ )
163
+ end = next;
164
+ }
165
+ return `${end ? `${chars.slice(0, end).join("")} ` : ""}[… ${chars.length - end} more characters]`;
151
166
  }
152
167
  function scalar(value: unknown): string {
153
168
  if (value === undefined) return "";
@@ -174,10 +189,11 @@ function field(value: unknown, key: string): unknown {
174
189
  ? (value as Record<string, unknown>)[key]
175
190
  : undefined;
176
191
  }
177
- function render(nodes: Template, root: unknown): string {
192
+ function render(nodes: Template, root: unknown, legacyPreview: boolean): string {
178
193
  return nodes
179
194
  .map((node) => {
180
195
  if (typeof node === "string") return node;
196
+ const cap = node.cap ?? (legacyPreview ? 300 : undefined);
181
197
  let value = root;
182
198
  let list = false;
183
199
  for (const step of node.steps) {
@@ -196,18 +212,31 @@ function render(nodes: Template, root: unknown): string {
196
212
  }
197
213
  }
198
214
  if (node.count)
199
- return truncate(Array.isArray(value) ? String(value.length) : "", node.cap);
215
+ return truncate(
216
+ Array.isArray(value) ? String(value.length) : "",
217
+ cap,
218
+ legacyPreview,
219
+ );
200
220
  if (node.each || list || node.join !== undefined) {
201
221
  if (!Array.isArray(value)) return "";
202
222
  const elements = value
203
- .slice(0, 10)
223
+ .slice(0, legacyPreview ? 10 : undefined)
204
224
  .map((item) =>
205
- truncate(node.each ? render(node.each, item) : scalar(item), 300),
225
+ truncate(
226
+ node.each ? render(node.each, item, legacyPreview) : scalar(item),
227
+ legacyPreview ? 300 : undefined,
228
+ legacyPreview,
229
+ ),
206
230
  );
207
- if (value.length > 10) elements.push(`… +${value.length - 10} more`);
208
- return truncate(elements.join(node.join ?? (node.each ? "; " : ", ")), node.cap);
231
+ if (legacyPreview && value.length > 10)
232
+ elements.push(`… +${value.length - 10} more`);
233
+ return truncate(
234
+ elements.join(node.join ?? (node.each ? "; " : ", ")),
235
+ cap,
236
+ legacyPreview,
237
+ );
209
238
  }
210
- return truncate(scalar(value), node.cap);
239
+ return truncate(scalar(value), cap, legacyPreview);
211
240
  })
212
241
  .join("");
213
242
  }
@@ -216,6 +245,7 @@ export function renderToolTemplate(
216
245
  source: string,
217
246
  input: unknown,
218
247
  output?: unknown,
248
+ legacyPreview = false,
219
249
  ): string | null {
220
250
  const nodes = compiled(source);
221
251
  if (!nodes) return null;
@@ -241,8 +271,12 @@ export function renderToolTemplate(
241
271
  !node.each &&
242
272
  !node.count &&
243
273
  node.join === undefined
244
- ? truncate(text ?? "", node.cap)
274
+ ? truncate(text ?? "", node.cap ?? (legacyPreview ? 300 : undefined), legacyPreview)
245
275
  : node,
246
276
  );
247
- return truncate(render(bareOutputNodes, root), 1000);
277
+ return truncate(
278
+ render(bareOutputNodes, root, legacyPreview),
279
+ legacyPreview ? 1000 : undefined,
280
+ legacyPreview,
281
+ );
248
282
  }
@@ -1,4 +1,4 @@
1
- export type ModelHarness = "opencode" | "pi";
1
+ export type ModelHarness = "opencode" | "pi" | "omp";
2
2
 
3
3
  export interface ResolvedModelEntry {
4
4
  /** Canonical provider/model reference used for identity and model selection. */
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
6
6
  Database,
7
+ SqliteAcquisitionBusyError,
7
8
  withAsyncPrivilegedWriter,
8
9
  withPrivilegedWriter,
9
10
  withSqliteTransformPass,
@@ -11,6 +12,23 @@ import {
11
12
  import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
12
13
  import { createTestTempDirFromPath } from "./test-temp-dir";
13
14
 
15
+ test("async writer never retries callback acquisition errors after work began", async () => {
16
+ const db = new Database(":memory:");
17
+ db.exec("CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)");
18
+ let calls = 0;
19
+ try {
20
+ await expect(
21
+ withAsyncPrivilegedWriter(db, () => {
22
+ calls++;
23
+ throw new SqliteAcquisitionBusyError(new Error("nested BEGIN"));
24
+ }),
25
+ ).rejects.toThrow("acquisition remained busy");
26
+ expect(calls).toBe(1);
27
+ } finally {
28
+ db.close();
29
+ }
30
+ });
31
+
14
32
  for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
15
33
  test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
16
34
  const dir = createTestTempDirFromPath(join(tmpdir(), "mc-routed-acquisition-"));
@@ -245,6 +245,7 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
245
245
  configurable: true,
246
246
  writable: true,
247
247
  value: (sql: string) => {
248
+ if (/^\s*(?:COMMIT|END)\b/i.test(sql)) assertTransformWrite();
248
249
  if (/^\s*BEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sql)) {
249
250
  // A transaction that ended without passing through exec (for
250
251
  // example an automatic rollback) must not be timed as this one.
@@ -556,6 +557,33 @@ interface StatementOwner {
556
557
  const statementOwners = new WeakMap<Database, StatementOwner>();
557
558
  const FIRST_PRUNE_AT = 256;
558
559
 
560
+ /**
561
+ * Bun's default close (`sqlite3_close_v2`) only releases the native connection
562
+ * once every statement created by prepare() is finalized. The close override
563
+ * in ownPreparedStatement can finalize only statements that are still
564
+ * reachable. Once the garbage collector
565
+ * has marked a one-shot statement dead, its weak reference is already cleared,
566
+ * but the statement is finalized only when its memory is later swept. Until
567
+ * then the closed connection keeps the database, WAL and shared-memory files
568
+ * open, for an unbounded time.
569
+ *
570
+ * `close(true)` finalizes every outstanding statement, reachable or not, and
571
+ * releases the connection immediately. When the caller did not ask for errors
572
+ * and that close fails, fall back to the caller's own close so a teardown path
573
+ * never starts throwing; the fallback can defer the release as before.
574
+ */
575
+ function closeReleasingStatements(
576
+ close: (...args: unknown[]) => unknown,
577
+ args: unknown[],
578
+ ): unknown {
579
+ if (args[0] === true) return close(true);
580
+ try {
581
+ return close(true);
582
+ } catch {
583
+ return close(...args);
584
+ }
585
+ }
586
+
559
587
  function ownPreparedStatement(db: Database, statement: Statement): Statement {
560
588
  const finalizable = statement as FinalizableStatement;
561
589
  // Node finalizes its statements on close and exposes no finalize() method.
@@ -579,7 +607,7 @@ function ownPreparedStatement(db: Database, statement: Statement): Statement {
579
607
  }
580
608
  }
581
609
  references.clear();
582
- const result = close(...args);
610
+ const result = closeReleasingStatements(close, args);
583
611
  if (failure) throw failure;
584
612
  return result;
585
613
  },
@@ -617,7 +645,12 @@ export function prepareCachedStatement(db: Database, sql: string): Statement {
617
645
 
618
646
  const privilegeDepth = new WeakMap<Database, number>();
619
647
  const transformPassScope = new AsyncLocalStorage<
620
- { active: boolean; remainingWaitMs: number } | undefined
648
+ | {
649
+ active: boolean;
650
+ remainingWaitMs: number;
651
+ guard?: { assert(): void; remainingMs(): number };
652
+ }
653
+ | undefined
621
654
  >();
622
655
  const admissionScope = new AsyncLocalStorage<boolean>();
623
656
  const backgroundWriterScope = new AsyncLocalStorage<boolean>();
@@ -655,6 +688,17 @@ export function withoutSqliteTransformPass<T>(operation: () => T): T {
655
688
  return transformPassScope.run(undefined, operation);
656
689
  }
657
690
 
691
+ /** Attach the adapter's existing generation/cancellation/deadline guard to writes. */
692
+ export function guardSqliteTransformPass(guard: { assert(): void; remainingMs(): number }): void {
693
+ const lease = transformPassScope.getStore();
694
+ if (lease) lease.guard = guard;
695
+ }
696
+
697
+ /** Check the attached generation/cancellation/deadline guard before SQL or cache publication. */
698
+ export function assertTransformWrite(): void {
699
+ if (!backgroundWriterScope.getStore()) transformPassScope.getStore()?.guard?.assert();
700
+ }
701
+
658
702
  /** Bun names SQLite codes; node:sqlite exposes the numeric (possibly extended) errcode. */
659
703
  export function isTransientSqliteError(error: unknown): boolean {
660
704
  if (!error || typeof error !== "object") return false;
@@ -698,6 +742,7 @@ interface WriterAcquisition {
698
742
  startedAt: number;
699
743
  attempts: number;
700
744
  claimed: boolean;
745
+ remainingMs?: () => number;
701
746
  }
702
747
 
703
748
  /** A write transaction whose BEGIN went through `acquireShort` and has not ended yet. */
@@ -724,6 +769,7 @@ function foregroundWaitLease() {
724
769
  * native API cannot separate lock waiting from statement execution. Exhaustion
725
770
  * switches to zero-wait acquisition, not a refusal when the writer is free. */
726
771
  function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
772
+ assertTransformWrite();
727
773
  const lease = foregroundWaitLease();
728
774
  if (isInTransaction(db) || (!lease && !backgroundWriterScope.getStore())) return operation();
729
775
  const previous = pragmaNumber(db, "busy_timeout");
@@ -749,6 +795,7 @@ function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
749
795
 
750
796
  /** The connection is never handed back to another caller with a shortened timeout. */
751
797
  function acquireShort(db: Database, acquire: () => unknown, site: string): void {
798
+ assertTransformWrite();
752
799
  const started = performance.now();
753
800
  const previous = pragmaNumber(db, "busy_timeout");
754
801
  const outer = writerAcquisitionScope.getStore();
@@ -763,7 +810,12 @@ function acquireShort(db: Database, acquire: () => unknown, site: string): void
763
810
  // stays short. An ordinary BEGIN inside a transform pass cannot yield;
764
811
  // give that single attempt enough time for a brief writer to finish.
765
812
  const lease = foregroundWaitLease();
766
- const timeout = lease ? Math.floor(lease.remainingWaitMs) : SHORT_BUSY_TIMEOUT_MS;
813
+ const timeout = Math.floor(
814
+ Math.min(
815
+ lease ? lease.remainingWaitMs : SHORT_BUSY_TIMEOUT_MS,
816
+ owner?.remainingMs?.() ?? lease?.guard?.remainingMs() ?? Infinity,
817
+ ),
818
+ );
767
819
  db.exec(`PRAGMA busy_timeout=${timeout}`);
768
820
  acquire();
769
821
  acquiredAt = performance.now();
@@ -871,11 +923,28 @@ export async function beginSqliteWriterAsync(db: Database, site: string): Promis
871
923
  }
872
924
 
873
925
  /** Retry admission before any turn work, yielding between short lock attempts. */
874
- export async function withAsyncPrivilegedWriter<T>(db: Database, operation: () => T): Promise<T> {
926
+ export async function withAsyncPrivilegedWriter<T>(
927
+ db: Database,
928
+ operation: () => T,
929
+ options: {
930
+ signal?: AbortSignal;
931
+ beforeRetry?: (error: SqliteAcquisitionBusyError) => void;
932
+ jitter?: boolean;
933
+ budgetMs?: number;
934
+ assertCurrentPass?: () => void;
935
+ } = {},
936
+ ): Promise<T> {
875
937
  const started = performance.now();
938
+ const budgetMs = Math.max(
939
+ 0,
940
+ Math.min(FOREGROUND_ACQUISITION_BUDGET_MS, options.budgetMs ?? Infinity),
941
+ );
876
942
  let attempts = 0;
877
943
  for (;;) {
944
+ options.signal?.throwIfAborted();
945
+ options.assertCurrentPass?.();
878
946
  attempts++;
947
+ let entered = false;
879
948
  try {
880
949
  const acquisition: WriterAcquisition = {
881
950
  site: "privileged_writer",
@@ -883,35 +952,51 @@ export async function withAsyncPrivilegedWriter<T>(db: Database, operation: () =
883
952
  startedAt: started,
884
953
  attempts,
885
954
  claimed: false,
955
+ remainingMs: () => Math.max(0, budgetMs - (performance.now() - started)),
886
956
  };
887
957
  // The acquisition and hold of this transaction are reported when it
888
958
  // commits or rolls back (see endWriterTransaction).
889
959
  return admissionScope.run(true, () =>
890
960
  withSqliteTransformPass(() =>
891
961
  writerAcquisitionScope.run(acquisition, () =>
892
- withPrivilegedWriter(db, operation),
962
+ withPrivilegedWriter(db, () => {
963
+ entered = true;
964
+ return operation();
965
+ }),
893
966
  ),
894
967
  ),
895
968
  );
896
969
  } catch (error) {
897
970
  // A callback failure can occur after writes; only retry a failed BEGIN.
898
- if (!(error instanceof SqliteAcquisitionBusyError)) throw error;
971
+ if (entered || !(error instanceof SqliteAcquisitionBusyError)) throw error;
972
+ // Adapters may serve a validated saved request instead of waiting.
973
+ // This runs only after a failed BEGIN, never after callback writes.
974
+ options.beforeRetry?.(error);
899
975
  const elapsed = performance.now() - started;
900
- if (elapsed >= FOREGROUND_ACQUISITION_BUDGET_MS) {
976
+ if (elapsed >= budgetMs) {
901
977
  reportSqliteDiagnostic(
902
978
  `sqlite writer site=privileged_writer lane=foreground acquire_ms=${Math.round(elapsed)} attempts=${attempts} outcome=busy`,
903
979
  );
904
980
  throw error;
905
981
  }
906
- await new Promise<void>((resolve) =>
907
- setTimeout(
908
- resolve,
909
- Math.min(
910
- attempts === 1 ? 500 : 1000,
911
- FOREGROUND_ACQUISITION_BUDGET_MS - elapsed,
912
- ),
913
- ),
982
+ const backoff = attempts === 1 ? 500 : 1000;
983
+ const delay = Math.min(
984
+ options.jitter ? backoff * (0.8 + Math.random() * 0.4) : backoff,
985
+ budgetMs - elapsed,
914
986
  );
987
+ await new Promise<void>((resolve, reject) => {
988
+ const abort = () => {
989
+ clearTimeout(timer);
990
+ options.signal?.removeEventListener("abort", abort);
991
+ reject(options.signal?.reason ?? new Error("Writer wait aborted"));
992
+ };
993
+ const timer = setTimeout(() => {
994
+ options.signal?.removeEventListener("abort", abort);
995
+ resolve();
996
+ }, delay);
997
+ options.signal?.addEventListener("abort", abort, { once: true });
998
+ if (options.signal?.aborted) abort();
999
+ });
915
1000
  }
916
1001
  }
917
1002
  }
@@ -70,7 +70,7 @@ const fencedSources: Array<{ path: string; sites: string[]; reporterCalls?: stri
70
70
  },
71
71
  {
72
72
  path: "packages/pi-plugin/src/context-handler.ts",
73
- sites: ["pi_compaction_queue"],
73
+ sites: ["pi_fallback_adoption"],
74
74
  },
75
75
  ];
76
76
 
@@ -116,7 +116,7 @@ const newlyCoveredSites = [
116
116
  "pi_materialize_cache",
117
117
  "pi_soft_refresh_cache",
118
118
  "pi_historian_publish",
119
- "pi_compaction_queue",
119
+ "pi_fallback_adoption",
120
120
  "user_memory_candidate_insert",
121
121
  ] as const;
122
122