@retaw106/mmc-oc 2.4.0 → 2.6.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 (188) hide show
  1. package/dist/{context-c72brxe5.js → context-cfbjrr0n.js} +1636 -513
  2. package/dist/features/magic-context/compaction-marker.d.ts +34 -6
  3. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  4. package/dist/features/magic-context/message-index-async.d.ts +6 -2
  5. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  6. package/dist/features/magic-context/message-index.d.ts +0 -26
  7. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  8. package/dist/features/magic-context/search.d.ts +5 -1
  9. package/dist/features/magic-context/search.d.ts.map +1 -1
  10. package/dist/features/magic-context/session-decision-calibration.d.ts.map +1 -1
  11. package/dist/features/magic-context/session-retention.fixture.d.ts +4 -0
  12. package/dist/features/magic-context/session-retention.fixture.d.ts.map +1 -0
  13. package/dist/features/magic-context/storage-db.d.ts +2 -0
  14. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage-host-instruction-retention.d.ts +4 -0
  16. package/dist/features/magic-context/storage-host-instruction-retention.d.ts.map +1 -0
  17. package/dist/features/magic-context/storage-meta-persisted.d.ts +21 -1
  18. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
  21. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-native-replay.d.ts +2 -0
  23. package/dist/features/magic-context/storage-native-replay.d.ts.map +1 -1
  24. package/dist/features/magic-context/storage-session-tables.d.ts +5 -4
  25. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  26. package/dist/features/magic-context/tool-definition-tokens.d.ts +113 -58
  27. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  28. package/dist/features/magic-context/work-metrics.d.ts +5 -3
  29. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/apply-operations.d.ts +32 -0
  31. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/ask-answer-notice.d.ts +25 -0
  33. package/dist/hooks/magic-context/ask-answer-notice.d.ts.map +1 -0
  34. package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
  35. package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +5 -5
  39. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/compaction-marker-publication.d.ts +14 -0
  41. package/dist/hooks/magic-context/compaction-marker-publication.d.ts.map +1 -0
  42. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -2
  44. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +11 -0
  45. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
  49. package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
  50. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/file-token-coverage.d.ts +11 -0
  52. package/dist/hooks/magic-context/file-token-coverage.d.ts.map +1 -0
  53. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +11 -3
  54. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/historian-history-wait.d.ts +16 -0
  56. package/dist/hooks/magic-context/historian-history-wait.d.ts.map +1 -0
  57. package/dist/hooks/magic-context/history-boundary-repair.d.ts +1 -1
  58. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/host-served-rows.d.ts +10 -0
  61. package/dist/hooks/magic-context/host-served-rows.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
  63. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +3 -1
  67. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/lkg-replay-fit.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/lkg-slot.d.ts +3 -1
  71. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
  73. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
  74. package/dist/hooks/magic-context/pass-outcome.d.ts +2 -0
  75. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  76. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +2 -0
  78. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/read-session-db.d.ts +8 -2
  81. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/read-session-raw.d.ts +20 -0
  83. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
  85. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/signed-thinking-fit-snapshot.d.ts +42 -0
  87. package/dist/hooks/magic-context/signed-thinking-fit-snapshot.d.ts.map +1 -0
  88. package/dist/hooks/magic-context/system-date-refresh.d.ts +3 -2
  89. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +4 -0
  92. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
  93. package/dist/hooks/magic-context/tag-content-primitives.d.ts +1 -1
  94. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/tag-messages.d.ts +17 -2
  96. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/text-complete.d.ts +1 -1
  98. package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
  100. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +1 -0
  102. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +6 -0
  104. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
  106. package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/transform.d.ts +4 -2
  108. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  109. package/dist/{index-27ddyx39.js → index-1np16gxq.js} +1 -1
  110. package/dist/{index-ykq24x1n.js → index-8rmt6s4f.js} +78 -32
  111. package/dist/{index-dwh99z66.js → index-pte3cfs9.js} +194 -5
  112. package/dist/{index-r7s7d350.js → index-s3ebcy63.js} +1 -1
  113. package/dist/{index-48e9znrc.js → index-t0zx3tgz.js} +481 -109
  114. package/dist/{index-kahkxcqw.js → index-xk1dz6q9.js} +870 -315
  115. package/dist/index.js +2 -2
  116. package/dist/{message-time-backfill-j96x1sab.js → message-time-backfill-g9py24yt.js} +1 -1
  117. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  118. package/dist/plugin/subagent-tool-policy.d.ts +4 -0
  119. package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
  120. package/dist/{read-session-chunk-t8bg6wkz.js → read-session-chunk-2s8x6xps.js} +3 -3
  121. package/dist/{recomp-orchestrator-k760qy1n.js → recomp-orchestrator-51xyaexp.js} +6 -6
  122. package/dist/{send-session-notification-njjf3b4s.js → send-session-notification-vdppp5x6.js} +3 -3
  123. package/dist/shared/model-suggestion-retry.d.ts +2 -0
  124. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  125. package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
  126. package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
  127. package/dist/shared/rpc-server.d.ts +2 -0
  128. package/dist/shared/rpc-server.d.ts.map +1 -1
  129. package/dist/shared/search-text.d.ts +8 -0
  130. package/dist/shared/search-text.d.ts.map +1 -0
  131. package/dist/shared/sqlite.d.ts +6 -1
  132. package/dist/shared/sqlite.d.ts.map +1 -1
  133. package/dist/shared/storage-file-mutex.d.ts.map +1 -1
  134. package/dist/shared/system-directive.d.ts +2 -0
  135. package/dist/shared/system-directive.d.ts.map +1 -1
  136. package/dist/shared/tag-transcript.d.ts +2 -0
  137. package/dist/shared/tag-transcript.d.ts.map +1 -1
  138. package/dist/shared/transcript.d.ts +3 -1
  139. package/dist/shared/transcript.d.ts.map +1 -1
  140. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  141. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  142. package/dist/tools/parameter-descriptions.d.ts +6 -6
  143. package/dist/tools/parameter-descriptions.d.ts.map +1 -1
  144. package/dist/v2/fold/native-replay.d.ts +11 -2
  145. package/dist/v2/fold/native-replay.d.ts.map +1 -1
  146. package/dist/v2/fold/policy.d.ts.map +1 -1
  147. package/dist/v2/fold/restore.d.ts.map +1 -1
  148. package/dist/v2/hooks/context.d.ts +4 -0
  149. package/dist/v2/hooks/context.d.ts.map +1 -1
  150. package/dist/v2/hooks/generate.d.ts +36 -1
  151. package/dist/v2/hooks/generate.d.ts.map +1 -1
  152. package/dist/v2/hooks/lkg-system.d.ts +35 -4
  153. package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
  154. package/dist/v2/hooks/payload.d.ts +10 -1
  155. package/dist/v2/hooks/payload.d.ts.map +1 -1
  156. package/dist/v2/hooks/store.d.ts +2 -0
  157. package/dist/v2/hooks/store.d.ts.map +1 -1
  158. package/dist/v2/hooks/types.d.ts +12 -0
  159. package/dist/v2/hooks/types.d.ts.map +1 -1
  160. package/dist/v2/message-times.d.ts +17 -0
  161. package/dist/v2/message-times.d.ts.map +1 -0
  162. package/dist/v2/server.js +4692 -2394
  163. package/dist/v2/store-reader.d.ts +21 -0
  164. package/dist/v2/store-reader.d.ts.map +1 -1
  165. package/dist/web/main.js +307 -12
  166. package/dist/{wrapup-orchestrator-b2gswatj.js → wrapup-orchestrator-g0mww1h4.js} +6 -6
  167. package/package.json +1 -1
  168. package/src/config/latch-permanence-guard.test.ts +4 -5
  169. package/src/shared/model-suggestion-retry.test.ts +52 -0
  170. package/src/shared/model-suggestion-retry.ts +8 -3
  171. package/src/shared/prompt-surface-a1-golden.md +6 -7
  172. package/src/shared/rpc-discovery-cleanup.test.ts +125 -0
  173. package/src/shared/rpc-discovery-cleanup.ts +57 -0
  174. package/src/shared/rpc-server.ts +36 -13
  175. package/src/shared/search-text.test.ts +30 -0
  176. package/src/shared/search-text.ts +62 -0
  177. package/src/shared/sqlite-transaction-mode-guard.test.ts +2 -0
  178. package/src/shared/sqlite-turn-deadline.test.ts +286 -0
  179. package/src/shared/sqlite.ts +237 -9
  180. package/src/shared/storage-file-mutex.test.ts +51 -2
  181. package/src/shared/storage-file-mutex.ts +11 -1
  182. package/src/shared/system-directive.test.ts +37 -0
  183. package/src/shared/system-directive.ts +19 -1
  184. package/src/shared/tag-transcript.ts +63 -8
  185. package/src/shared/transcript.ts +1 -1
  186. package/src/shared/write-transaction-attribution.test.ts +1 -6
  187. package/dist/features/magic-context/message-index-sweep.d.ts +0 -11
  188. package/dist/features/magic-context/message-index-sweep.d.ts.map +0 -1
@@ -71,6 +71,8 @@ export interface PromptAttemptInfo {
71
71
  export interface PromptRetryOptions {
72
72
  transport?: PromptTransport;
73
73
  timeoutMs?: number;
74
+ /** False for callers whose configured model chain must not accept provider suggestions. Defaults to true. */
75
+ allowModelSuggestions?: boolean;
74
76
  /** External abort signal — cancels the in-flight LLM prompt immediately when aborted */
75
77
  signal?: AbortSignal;
76
78
  /**
@@ -488,6 +490,7 @@ async function attemptOnce(
488
490
  callContext: string,
489
491
  label: string,
490
492
  transport?: PromptTransport,
493
+ allowModelSuggestions = true,
491
494
  ): Promise<void> {
492
495
  // Keep this snapshot separate from the object passed to the client. A
493
496
  // failed prompt facade may rewrite its request body before rejecting; the
@@ -498,9 +501,8 @@ async function attemptOnce(
498
501
  await promptWithTimeout(client, attemptArgs, timeoutMs, signal, transport);
499
502
  return;
500
503
  } catch (error) {
501
- // If non-retryable (abort, overflow, timeout), bubble up immediately.
502
- // Don't even try suggestion retry — caller needs the original error.
503
- if (isNonRetryable(error, signal)) throw error;
504
+ // Surface the original error when suggestions are disabled or cannot help.
505
+ if (isNonRetryable(error, signal) || !allowModelSuggestions) throw error;
504
506
 
505
507
  const suggestion = parseModelSuggestion(error);
506
508
  if (!suggestion || !originalBody.model) {
@@ -576,6 +578,7 @@ export async function promptSyncWithModelSuggestionRetry(
576
578
  callContext,
577
579
  explicitPrimaryLabel,
578
580
  options.transport,
581
+ options.allowModelSuggestions,
579
582
  );
580
583
  return;
581
584
  } catch (error) {
@@ -620,6 +623,7 @@ export async function promptSyncWithModelSuggestionRetry(
620
623
  callContext,
621
624
  label,
622
625
  options.transport,
626
+ options.allowModelSuggestions,
623
627
  );
624
628
  log(
625
629
  `[${callContext}] fallback succeeded with ${label} (attempt ${i + 2}/${fallbacks.length + 1})`,
@@ -665,6 +669,7 @@ async function attemptAndValidate<TOutput, TValidated>(
665
669
  callContext,
666
670
  attempt.label,
667
671
  options.transport,
672
+ options.allowModelSuggestions,
668
673
  );
669
674
  } catch (error) {
670
675
  throw {
@@ -1,6 +1,6 @@
1
1
  # Magic Context agent-facing surface (exported from source)
2
2
 
3
- Generated by `packages/plugin/scripts/export-agent-surface.ts` at 2026-10-01.
3
+ Generated by `packages/plugin/scripts/export-agent-surface.ts` at 2026-11-01.
4
4
  Token counts are Claude BPE estimates on the raw text.
5
5
 
6
6
  ## 1. System-prompt guidance section
@@ -201,7 +201,7 @@ Finer recovery:
201
201
  }
202
202
  ```
203
203
 
204
- ### mmc_note — description ~319 tokens, params ~203 tokens (total ~522)
204
+ ### mmc_note — description ~319 tokens, params ~196 tokens (total ~515)
205
205
 
206
206
  **Description:**
207
207
 
@@ -228,7 +228,7 @@ First line is the title (under 80 chars), followed by detail. Operations:
228
228
  ```json
229
229
  {
230
230
  "action": {
231
- "description": "write | read | update | dismiss. Defaults to write when content is given, else read.",
231
+ "description": "write | read | update | dismiss.",
232
232
  "type": "string",
233
233
  "enum": [
234
234
  "write",
@@ -259,8 +259,7 @@ First line is the title (under 80 chars), followed by detail. Operations:
259
259
  "type": "number"
260
260
  },
261
261
  "note_ids": {
262
- "description": "Note ids: one for update, 1–50 for dismiss or read (read returns full bodies). Ignored by write.",
263
- "minItems": 1,
262
+ "description": "Note ids: one for update, 1–50 for dismiss, or 1–50 to read those notes in full. Write and a plain read take an empty list.",
264
263
  "maxItems": 50,
265
264
  "type": "array",
266
265
  "items": {
@@ -272,7 +271,7 @@ First line is the title (under 80 chars), followed by detail. Operations:
272
271
  }
273
272
  ```
274
273
 
275
- ### mmc_memory — description ~279 tokens, params ~172 tokens (total ~451)
274
+ ### mmc_memory — description ~279 tokens, params ~175 tokens (total ~454)
276
275
 
277
276
  **Description:**
278
277
 
@@ -310,7 +309,7 @@ Examples: category="CONFIG_VALUES", content="OpenCode source is at ~/Work/OSS/op
310
309
  "type": "string"
311
310
  },
312
311
  "category": {
313
- "description": "Kind of fact (required for write; on update/merge optional, omitted keeps the current category).",
312
+ "description": "Kind of fact (required for write; on update/merge, pass the memory's current category to keep it).",
314
313
  "type": "string",
315
314
  "enum": [
316
315
  "PROJECT_RULES",
@@ -0,0 +1,125 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { reapDeadRpcDiscovery } from "./rpc-discovery-cleanup";
6
+ import { MagicContextRpcServer } from "./rpc-server";
7
+ import * as utils from "./rpc-utils";
8
+ import * as storage from "./storage-permissions";
9
+ import { createTestTempDirFromPath } from "./test-temp-dir";
10
+
11
+ test("RPC startup reaps dead records across projects but keeps live and inconclusive records", async () => {
12
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-reap-"));
13
+ const server = new MagicContextRpcServer(root, "/current-project");
14
+ const dir = utils.rpcPortDir(root, "/other-project");
15
+ const empty = utils.rpcPortDir(root, "/empty-project");
16
+ mkdirSync(dir, { recursive: true });
17
+ mkdirSync(empty, { recursive: true });
18
+ for (const pid of [999999, 888888, process.pid])
19
+ writeFileSync(
20
+ join(dir, `port-${pid}.json`),
21
+ JSON.stringify({ port: 1234, pid, token: "do-not-log" }),
22
+ );
23
+ const probe = spyOn(utils, "inspectProcessesAsync").mockResolvedValue({
24
+ pi: { state: "absent", processIds: [] },
25
+ evidence: () => ({ processStartTime: null, commandLine: null, imageName: null }),
26
+ liveness: (pid: number) =>
27
+ pid === 999999 ? "dead" : pid === 888888 ? "inconclusive" : "alive",
28
+ } as Awaited<ReturnType<typeof utils.inspectProcessesAsync>>);
29
+ try {
30
+ await server.start();
31
+ expect(readdirSync(dir).sort()).toEqual(
32
+ [`port-${process.pid}.json`, "port-888888.json"].sort(),
33
+ );
34
+ expect(existsSync(empty)).toBe(false);
35
+ } finally {
36
+ server.stop();
37
+ probe.mockRestore();
38
+ rmSync(root, { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ test("cleanup keeps changed, malformed and out-of-scope records and requests fresh evidence", async () => {
43
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-reap-race-"));
44
+ const directory = utils.rpcPortDir(root, "/project");
45
+ const outside = join(root, "rpc", "not-a-project-hash");
46
+ mkdirSync(directory, { recursive: true });
47
+ mkdirSync(outside, { recursive: true });
48
+ const path = join(directory, "port-999999.json");
49
+ writeFileSync(path, JSON.stringify({ port: 1234, pid: 999999, token: "old" }));
50
+ writeFileSync(join(directory, "port-888888.json"), "not json");
51
+ writeFileSync(join(directory, "unrelated.json"), "keep");
52
+ mkdirSync(join(directory, "port-777777.json"));
53
+ writeFileSync(join(outside, "port-999999.json"), "keep");
54
+ const probe = spyOn(utils, "inspectProcessesAsync").mockImplementation(async (fresh) => {
55
+ expect(fresh).toBe(true);
56
+ writeFileSync(path, JSON.stringify({ port: 1234, pid: 999999, token: "new" }));
57
+ return {
58
+ pi: { state: "absent", processIds: [] },
59
+ evidence: () => ({ startTime: null, commandLine: null }),
60
+ liveness: () => "dead",
61
+ };
62
+ });
63
+ try {
64
+ await reapDeadRpcDiscovery(root);
65
+ expect(probe).toHaveBeenCalledTimes(1);
66
+ expect(readdirSync(directory)).toHaveLength(4);
67
+ expect(existsSync(join(outside, "port-999999.json"))).toBe(true);
68
+ } finally {
69
+ probe.mockRestore();
70
+ rmSync(root, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test("stop during startup prevents listening and publishing", async () => {
75
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-start-stop-"));
76
+ const server = new MagicContextRpcServer(root, root);
77
+ try {
78
+ const starting = server.start();
79
+ server.stop();
80
+ expect(await starting).toBe(0);
81
+ expect(existsSync(utils.rpcPortDir(root, root))).toBe(false);
82
+ } finally {
83
+ server.stop();
84
+ rmSync(root, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ test.each(["ENOENT", "EACCES"])("publishing retries only ENOENT (%s)", async (code) => {
89
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-publish-race-"));
90
+ const server = new MagicContextRpcServer(root, root);
91
+ const original = storage.writeStorageFileAtomicSync;
92
+ let calls = 0;
93
+ const writer = spyOn(storage, "writeStorageFileAtomicSync").mockImplementation((...args) => {
94
+ if (++calls === 1) throw Object.assign(new Error("publish race"), { code });
95
+ return original(...args);
96
+ });
97
+ try {
98
+ await server.start();
99
+ expect(calls).toBe(code === "ENOENT" ? 2 : 1);
100
+ expect(existsSync(utils.rpcPortDir(root, root))).toBe(code === "ENOENT");
101
+ } finally {
102
+ server.stop();
103
+ writer.mockRestore();
104
+ rmSync(root, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ test("RPC stop removes its empty discovery directory but preserves other instances", async () => {
109
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-stop-reap-"));
110
+ const first = new MagicContextRpcServer(root, root);
111
+ const second = new MagicContextRpcServer(root, root);
112
+ const directory = utils.rpcPortDir(root, root);
113
+ try {
114
+ await first.start();
115
+ await second.start();
116
+ first.stop();
117
+ expect(readdirSync(directory)).toHaveLength(1);
118
+ second.stop();
119
+ expect(existsSync(directory)).toBe(false);
120
+ } finally {
121
+ first.stop();
122
+ second.stop();
123
+ rmSync(root, { recursive: true, force: true });
124
+ }
125
+ });
@@ -0,0 +1,57 @@
1
+ import { type Dirent, promises as fs } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { inspectProcessesAsync, parseRpcPortFile } from "./rpc-utils";
4
+
5
+ /** Reap only records proven dead; live, unreadable and uncertain records stay. */
6
+ export async function reapDeadRpcDiscovery(storageDir: string): Promise<void> {
7
+ const root = join(storageDir, "rpc");
8
+ let directories: Dirent[];
9
+ try {
10
+ directories = await fs.readdir(root, { withFileTypes: true });
11
+ } catch {
12
+ return;
13
+ }
14
+ const records: { path: string; content: string; pid: number }[] = [];
15
+ const projectDirs: string[] = [];
16
+ for (const entry of directories) {
17
+ if (!entry.isDirectory() || !/^[0-9a-f]{16}$/.test(entry.name)) continue;
18
+ const directory = join(root, entry.name);
19
+ projectDirs.push(directory);
20
+ try {
21
+ for (const name of await fs.readdir(directory)) {
22
+ const match = /^port-(\d+)(?:-[0-9a-f]+)?\.json$/.exec(name);
23
+ if (!match) continue;
24
+ const path = join(directory, name);
25
+ try {
26
+ const content = await fs.readFile(path, "utf8");
27
+ const record = parseRpcPortFile(content, Number(match[1]));
28
+ if (record && record.pid === Number(match[1]))
29
+ records.push({ path, content, pid: record.pid });
30
+ } catch {
31
+ // Another host may be publishing or stopping this record.
32
+ }
33
+ }
34
+ } catch {
35
+ // An inaccessible project must not prevent cleanup of other projects.
36
+ }
37
+ }
38
+ // Read records before a fresh process snapshot so a recently started host
39
+ // cannot be mistaken for a PID missing from an older cached snapshot.
40
+ const processes = records.length ? await inspectProcessesAsync(true) : undefined;
41
+ for (const record of records) {
42
+ if (processes?.liveness(record.pid) !== "dead") continue;
43
+ try {
44
+ if ((await fs.readFile(record.path, "utf8")) === record.content)
45
+ await fs.unlink(record.path);
46
+ } catch {
47
+ // Leave the record for a later pass; never recursively remove its directory.
48
+ }
49
+ }
50
+ for (const directory of projectDirs) {
51
+ try {
52
+ await fs.rmdir(directory);
53
+ } catch {
54
+ // Leave non-empty directories untouched; their owners may not be proven dead.
55
+ }
56
+ }
57
+ }
@@ -1,7 +1,8 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
- import { readdirSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { readdirSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
3
3
  import type { Server, ServerWebSocket } from "bun";
4
4
  import { log } from "./logger";
5
+ import { reapDeadRpcDiscovery } from "./rpc-discovery-cleanup";
5
6
  import {
6
7
  acknowledgeNotifications,
7
8
  drainNotifications,
@@ -82,6 +83,7 @@ export class MagicContextRpcServer {
82
83
  private portFilePath: string;
83
84
  private portDir: string;
84
85
  private startedAt = Date.now();
86
+ private stopped = false;
85
87
  private readonly instanceId = randomBytes(8).toString("hex");
86
88
  private unregisterOwnInstance: (() => void) | null = null;
87
89
  /** Every authenticated WS socket, so dispose can close them all. */
@@ -93,7 +95,10 @@ export class MagicContextRpcServer {
93
95
  // discovers/guesses the port.
94
96
  private readonly token = randomBytes(32).toString("hex");
95
97
 
96
- constructor(storageDir: string, directory: string) {
98
+ constructor(
99
+ private storageDir: string,
100
+ directory: string,
101
+ ) {
97
102
  this.portFilePath = rpcPortFilePath(storageDir, directory, process.pid, this.instanceId);
98
103
  this.portDir = rpcPortDir(storageDir, directory);
99
104
  }
@@ -131,6 +136,9 @@ export class MagicContextRpcServer {
131
136
  log("rpc server skipped: Bun runtime not available (no TUI consumer)");
132
137
  return 0;
133
138
  }
139
+ this.stopped = false;
140
+ await reapDeadRpcDiscovery(this.storageDir);
141
+ if (this.stopped) return 0;
134
142
  this.startedAt = Date.now();
135
143
  const self = this;
136
144
  const server = Bun.serve<WsData>({
@@ -170,17 +178,26 @@ export class MagicContextRpcServer {
170
178
  // The discovery file carries the RPC bearer token. The shared writer
171
179
  // creates its directory and temporary file with owner-only modes.
172
180
  this.unregisterOwnInstance ??= registerOwnRpcServerInstance(this.instanceId);
173
- writeStorageFileAtomicSync(
174
- this.portFilePath,
175
- JSON.stringify({
176
- port: this.port,
177
- pid: process.pid,
178
- started_at: this.startedAt,
179
- kind: "OpenCode server",
180
- token: this.token,
181
- instance_id: this.instanceId,
182
- }),
183
- );
181
+ const publish = () =>
182
+ writeStorageFileAtomicSync(
183
+ this.portFilePath,
184
+ JSON.stringify({
185
+ port: this.port,
186
+ pid: process.pid,
187
+ started_at: this.startedAt,
188
+ kind: "OpenCode server",
189
+ token: this.token,
190
+ instance_id: this.instanceId,
191
+ }),
192
+ );
193
+ try {
194
+ publish();
195
+ } catch (error) {
196
+ // An empty-directory reaper can race the writer before its temp
197
+ // file exists. Recreate the directory and retry that case only.
198
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
199
+ publish();
200
+ }
184
201
  log(`[rpc] server listening on 127.0.0.1:${this.port}`);
185
202
  } catch (err) {
186
203
  log(`[rpc] failed to write port file: ${err}`);
@@ -191,6 +208,7 @@ export class MagicContextRpcServer {
191
208
 
192
209
  /** Stop the server: close every socket, stop accepting, remove port file. */
193
210
  stop(): void {
211
+ this.stopped = true;
194
212
  for (const ws of this.sockets) {
195
213
  try {
196
214
  if (ws.data.authTimer) clearTimeout(ws.data.authTimer);
@@ -211,6 +229,11 @@ export class MagicContextRpcServer {
211
229
  } catch {
212
230
  // Intentional: port file may already be gone
213
231
  }
232
+ try {
233
+ rmdirSync(this.portDir);
234
+ } catch {
235
+ // Other instances' discovery records must survive this server's stop.
236
+ }
214
237
  this.unregisterOwnInstance?.();
215
238
  this.unregisterOwnInstance = null;
216
239
  }
@@ -0,0 +1,30 @@
1
+ import { expect, test } from "bun:test";
2
+ import { cleanSearchText, keywordRelevance, matchedSearchSnippet } from "./search-text";
3
+
4
+ test("notification trees never contribute relevance or display text", () => {
5
+ expect(
6
+ cleanSearchText(
7
+ '<task_cohort id="parent"><task>hidden</task>status</task_cohort> actual answer',
8
+ ),
9
+ ).toBe("actual answer");
10
+ expect(cleanSearchText("kept <task>hidden </task_cohort> still hidden</task> kept")).toBe(
11
+ "kept kept",
12
+ );
13
+ expect(cleanSearchText('<task id="truncated">background result…')).toBe("");
14
+ expect(keywordRelevance("historian cache wiring", "<task>historian cache wiring</task>")).toBe(
15
+ 0,
16
+ );
17
+ });
18
+
19
+ test("keyword relevance measures distinct coverage rather than rank", () => {
20
+ expect(keywordRelevance("explain historian cache wiring", "cache icons")).toBeCloseTo(0.2);
21
+ expect(keywordRelevance("historian cache wiring", "historian cache wiring")).toBeCloseTo(0.8);
22
+ });
23
+
24
+ test("snippets prefer dense passages over an irrelevant header", () => {
25
+ const content = `Header cache. ${"padding ".repeat(150)}Historian cache wiring uses incremental pools. ${"trailing ".repeat(100)}`;
26
+ const snippet = matchedSearchSnippet(content, "historian cache wiring", 80);
27
+ expect(snippet).toContain("Historian cache wiring");
28
+ expect(snippet).not.toContain("Header");
29
+ expect(matchedSearchSnippet(content, "unrelated deployment")).toBe("");
30
+ });
@@ -0,0 +1,62 @@
1
+ import { removeNestedBlocks } from "./system-directive";
2
+
3
+ export function removeTaskNotifications(text: string): string {
4
+ return removeNestedBlocks(text, ["task", "task_cohort"]);
5
+ }
6
+
7
+ /** Remove injected notifications before scoring or rendering; saved hints replay unchanged. */
8
+ export function cleanSearchText(text: string): string {
9
+ return removeNestedBlocks(text, ["system-reminder", "task", "task_cohort"])
10
+ .replace(/\s+/g, " ")
11
+ .trim();
12
+ }
13
+
14
+ const QUERY_STOP_WORDS = new Set(
15
+ "a an and are as at be been by can could did do does for from how i in is it me my of on or our please should that the their them there these they this to us was we were what when where which who why will with would you your".split(
16
+ " ",
17
+ ),
18
+ );
19
+
20
+ function terms(text: string): string[] {
21
+ return [
22
+ ...new Set(
23
+ (text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter(
24
+ (term) => !QUERY_STOP_WORDS.has(term),
25
+ ),
26
+ ),
27
+ ];
28
+ }
29
+
30
+ /** Score distinct query coverage independently of other results. */
31
+ export function keywordRelevance(query: string, content: string): number {
32
+ const needles = terms(query);
33
+ if (needles.length === 0) return 0;
34
+ const words = new Set(terms(cleanSearchText(content)));
35
+ return (0.8 * needles.filter((term) => words.has(term)).length) / needles.length;
36
+ }
37
+
38
+ /** Pick the densest matching passage before compression discards matched words. */
39
+ export function matchedSearchSnippet(content: string, query: string, cap = 220): string {
40
+ const text = cleanSearchText(content);
41
+ const needles = new Set(terms(query));
42
+ const matches = [...text.matchAll(/[\p{L}\p{N}_]+/gu)].filter((match) =>
43
+ needles.has(match[0].toLowerCase()),
44
+ );
45
+ if (matches.length === 0) return "";
46
+ let bestStart = matches[0].index;
47
+ let bestCoverage = -1;
48
+ for (const match of matches) {
49
+ const start = Math.max(0, match.index - Math.floor(cap / 5));
50
+ const coverage = keywordRelevance(query, text.slice(start, start + cap));
51
+ if (coverage > bestCoverage) {
52
+ bestStart = start;
53
+ bestCoverage = coverage;
54
+ }
55
+ }
56
+ if (bestStart > 0) {
57
+ const boundary = text.indexOf(" ", bestStart);
58
+ if (boundary >= 0 && boundary < matches[matches.length - 1].index) bestStart = boundary + 1;
59
+ }
60
+ const passage = text.slice(bestStart, bestStart + cap).trim();
61
+ return `${bestStart > 0 ? "…" : ""}${passage}${bestStart + cap < text.length ? "…" : ""}`;
62
+ }
@@ -27,6 +27,8 @@ const READ_ONLY_DEFERRED_TRANSACTIONS: Record<string, string> = {
27
27
  "pins one read snapshot so the session's rowid coverage proof and the filtered message search see the same rows; never writes (read-only connections use it too)",
28
28
  "packages/plugin/src/hooks/magic-context/lkg-persist.ts#loadPersistedLkgSlot":
29
29
  "reads an LKG slot row and its prefix slices as one consistent snapshot even while another connection saves the slot; never writes (a failed verification clear opens its own transaction)",
30
+ "packages/plugin/src/hooks/magic-context/compaction-marker-manager.ts#reconcileForkOrphanedCompactionMarkers":
31
+ "reads publication intent and finalized ownership as one context.db snapshot while host-order probes use the already-open host connection; never writes context.db",
30
32
  };
31
33
 
32
34
  const REPOSITORY_ROOT = resolve(import.meta.dir, "../../../..");