@retaw106/mmc-oc 2.4.0 → 2.5.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 (132) hide show
  1. package/dist/{context-c72brxe5.js → context-xx09sh1g.js} +1279 -408
  2. package/dist/features/magic-context/message-index-async.d.ts +6 -2
  3. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  4. package/dist/features/magic-context/message-index.d.ts +5 -8
  5. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  6. package/dist/features/magic-context/search.d.ts +5 -1
  7. package/dist/features/magic-context/search.d.ts.map +1 -1
  8. package/dist/features/magic-context/storage-db.d.ts +2 -0
  9. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-host-instruction-retention.d.ts +4 -0
  11. package/dist/features/magic-context/storage-host-instruction-retention.d.ts.map +1 -0
  12. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
  14. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
  16. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  17. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  18. package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
  19. package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  21. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -2
  23. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
  25. package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
  26. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/file-token-coverage.d.ts +11 -0
  28. package/dist/hooks/magic-context/file-token-coverage.d.ts.map +1 -0
  29. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/host-served-rows.d.ts +10 -0
  31. package/dist/hooks/magic-context/host-served-rows.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
  33. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/lkg-slot.d.ts +1 -1
  37. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
  39. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
  40. package/dist/hooks/magic-context/pass-outcome.d.ts +2 -0
  41. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +2 -0
  44. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
  47. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/system-date-refresh.d.ts +3 -2
  49. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +4 -0
  52. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
  53. package/dist/hooks/magic-context/tag-content-primitives.d.ts +1 -1
  54. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/tag-messages.d.ts +15 -2
  56. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/text-complete.d.ts +1 -1
  58. package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/todo-view.d.ts +2 -2
  60. package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
  61. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +1 -0
  63. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +6 -0
  65. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
  67. package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/transform.d.ts +4 -2
  69. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  70. package/dist/{index-kahkxcqw.js → index-0d40b4q8.js} +14740 -14292
  71. package/dist/{index-48e9znrc.js → index-1vxffj5j.js} +75 -22
  72. package/dist/{index-ykq24x1n.js → index-bpjjcdy9.js} +1 -1
  73. package/dist/{index-r7s7d350.js → index-k25thxq7.js} +1 -1
  74. package/dist/{index-27ddyx39.js → index-mjf0p7yq.js} +1 -1
  75. package/dist/{index-dwh99z66.js → index-pte3cfs9.js} +194 -5
  76. package/dist/index.js +2 -2
  77. package/dist/{message-time-backfill-j96x1sab.js → message-time-backfill-g9py24yt.js} +1 -1
  78. package/dist/plugin/subagent-tool-policy.d.ts +4 -0
  79. package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
  80. package/dist/{read-session-chunk-t8bg6wkz.js → read-session-chunk-eb3fy45y.js} +3 -3
  81. package/dist/{recomp-orchestrator-k760qy1n.js → recomp-orchestrator-mmpy5cb7.js} +6 -6
  82. package/dist/{send-session-notification-njjf3b4s.js → send-session-notification-zpqs213d.js} +3 -3
  83. package/dist/shared/model-suggestion-retry.d.ts +2 -0
  84. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  85. package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
  86. package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
  87. package/dist/shared/rpc-server.d.ts +2 -0
  88. package/dist/shared/rpc-server.d.ts.map +1 -1
  89. package/dist/shared/search-text.d.ts +8 -0
  90. package/dist/shared/search-text.d.ts.map +1 -0
  91. package/dist/shared/sqlite.d.ts +6 -1
  92. package/dist/shared/sqlite.d.ts.map +1 -1
  93. package/dist/shared/storage-file-mutex.d.ts.map +1 -1
  94. package/dist/shared/system-directive.d.ts +2 -0
  95. package/dist/shared/system-directive.d.ts.map +1 -1
  96. package/dist/v2/fold/native-replay.d.ts +11 -2
  97. package/dist/v2/fold/native-replay.d.ts.map +1 -1
  98. package/dist/v2/fold/policy.d.ts.map +1 -1
  99. package/dist/v2/fold/restore.d.ts.map +1 -1
  100. package/dist/v2/hooks/context.d.ts +1 -1
  101. package/dist/v2/hooks/context.d.ts.map +1 -1
  102. package/dist/v2/hooks/generate.d.ts +36 -1
  103. package/dist/v2/hooks/generate.d.ts.map +1 -1
  104. package/dist/v2/hooks/lkg-system.d.ts +35 -4
  105. package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
  106. package/dist/v2/hooks/payload.d.ts +10 -1
  107. package/dist/v2/hooks/payload.d.ts.map +1 -1
  108. package/dist/v2/hooks/store.d.ts +2 -0
  109. package/dist/v2/hooks/store.d.ts.map +1 -1
  110. package/dist/v2/hooks/types.d.ts +12 -0
  111. package/dist/v2/hooks/types.d.ts.map +1 -1
  112. package/dist/v2/server.js +9033 -7461
  113. package/dist/v2/store-reader.d.ts +11 -0
  114. package/dist/v2/store-reader.d.ts.map +1 -1
  115. package/dist/web/main.js +229 -8
  116. package/dist/{wrapup-orchestrator-b2gswatj.js → wrapup-orchestrator-jq589pgr.js} +6 -6
  117. package/package.json +1 -1
  118. package/src/config/latch-permanence-guard.test.ts +4 -0
  119. package/src/shared/model-suggestion-retry.test.ts +52 -0
  120. package/src/shared/model-suggestion-retry.ts +8 -3
  121. package/src/shared/rpc-discovery-cleanup.test.ts +125 -0
  122. package/src/shared/rpc-discovery-cleanup.ts +57 -0
  123. package/src/shared/rpc-server.ts +36 -13
  124. package/src/shared/search-text.test.ts +30 -0
  125. package/src/shared/search-text.ts +62 -0
  126. package/src/shared/sqlite-turn-deadline.test.ts +286 -0
  127. package/src/shared/sqlite.ts +237 -9
  128. package/src/shared/storage-file-mutex.test.ts +51 -2
  129. package/src/shared/storage-file-mutex.ts +11 -1
  130. package/src/shared/system-directive.test.ts +37 -0
  131. package/src/shared/system-directive.ts +19 -1
  132. package/src/shared/tag-transcript.ts +16 -4
@@ -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
+ }
@@ -0,0 +1,286 @@
1
+ import { Database as NativeDatabase } from "bun:sqlite";
2
+ import { expect, spyOn, test } from "bun:test";
3
+ import { rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ createSqliteTurnDatabase,
8
+ Database,
9
+ hasSqliteTurnDeadlineRouting,
10
+ withoutSqliteTransformPass,
11
+ withSqliteBackgroundWriter,
12
+ withSqliteTransformPass,
13
+ withSqliteTurnDeadline,
14
+ } from "./sqlite";
15
+ import { createTestTempDirFromPath } from "./test-temp-dir";
16
+
17
+ async function fixture(run: (db: Database, path: string) => Promise<void>) {
18
+ const root = createTestTempDirFromPath(join(tmpdir(), "sqlite-turn-deadline-"));
19
+ const path = join(root, "context.db");
20
+ const db = createSqliteTurnDatabase(path);
21
+ db.exec(
22
+ "PRAGMA journal_mode=DELETE; PRAGMA busy_timeout=900; CREATE TABLE t(value); INSERT INTO t VALUES(1)",
23
+ );
24
+ try {
25
+ await run(db, path);
26
+ } finally {
27
+ db.close();
28
+ rmSync(root, { recursive: true, force: true });
29
+ }
30
+ }
31
+
32
+ async function hold(path: string, reader = false) {
33
+ const child = Bun.spawn(
34
+ [
35
+ process.execPath,
36
+ "-e",
37
+ `
38
+ const { Database } = require('bun:sqlite');
39
+ const db = new Database(process.argv[1]);
40
+ db.exec(${JSON.stringify(reader ? "BEGIN; SELECT * FROM t" : "BEGIN EXCLUSIVE")});
41
+ console.log('HELD');
42
+ setInterval(() => {}, 1000);
43
+ `,
44
+ path,
45
+ ],
46
+ { stdout: "pipe", stderr: "pipe", windowsHide: true },
47
+ );
48
+ const output = child.stdout.getReader();
49
+ const first = await output.read();
50
+ output.releaseLock();
51
+ if (!new TextDecoder().decode(first.value).includes("HELD")) {
52
+ child.kill();
53
+ throw new Error(`holder failed: ${await new Response(child.stderr).text()}`);
54
+ }
55
+ return async () => {
56
+ child.kill();
57
+ await child.exited;
58
+ };
59
+ }
60
+
61
+ function timeout(db: Database): number {
62
+ return (db.prepare("PRAGMA busy_timeout").get() as { timeout: number }).timeout;
63
+ }
64
+
65
+ function bounded(operation: () => unknown) {
66
+ const start = performance.now();
67
+ expect(() =>
68
+ withSqliteTurnDeadline(() => Math.max(0, 80 - (performance.now() - start)), operation),
69
+ ).toThrow(/locked|busy/i);
70
+ expect(performance.now() - start).toBeLessThan(400);
71
+ expect(performance.now() - start).toBeGreaterThan(40);
72
+ }
73
+
74
+ test("ordinary connections keep native cached SELECT methods and do not opt in", () => {
75
+ const db = new Database(":memory:");
76
+ const native = new NativeDatabase(":memory:");
77
+ try {
78
+ expect(hasSqliteTurnDeadlineRouting(db)).toBe(false);
79
+ expect(db.prepare("SELECT 1").get).toBe(native.prepare("SELECT 1").get);
80
+ } finally {
81
+ db.close();
82
+ native.close();
83
+ }
84
+ });
85
+
86
+ test("native deadline timeout is read once, not on every cached SELECT", () => {
87
+ let reads = 0;
88
+ const prepare = Database.prototype.prepare;
89
+ const spy = spyOn(Database.prototype, "prepare").mockImplementation(function (
90
+ this: Database,
91
+ sql: string,
92
+ ) {
93
+ const statement = prepare.call(this, sql);
94
+ if (sql === "PRAGMA busy_timeout") {
95
+ const get = statement.get.bind(statement);
96
+ statement.get = (...args: unknown[]) => {
97
+ reads++;
98
+ return get(...args);
99
+ };
100
+ }
101
+ return statement;
102
+ });
103
+ let db: Database | undefined;
104
+ try {
105
+ db = createSqliteTurnDatabase(":memory:");
106
+ db.exec("PRAGMA busy_timeout=900");
107
+ const read = db.prepare("SELECT 1 AS value");
108
+ withSqliteTurnDeadline(
109
+ () => 80,
110
+ () => {
111
+ for (let i = 0; i < 100; i++) expect(read.get()).toEqual({ value: 1 });
112
+ },
113
+ );
114
+ expect(reads).toBe(1);
115
+ } finally {
116
+ db?.close();
117
+ spy.mockRestore();
118
+ }
119
+ });
120
+
121
+ for (const method of ["get", "all", "run", "values", "iterate"] as const) {
122
+ test(`turn deadline bounds cached SELECT ${method} and restores timeout`, async () => {
123
+ await fixture(async (db, path) => {
124
+ expect(hasSqliteTurnDeadlineRouting(db)).toBe(true);
125
+ const statement: any = db.prepare("SELECT * FROM t");
126
+ const release = await hold(path);
127
+ try {
128
+ bounded(() =>
129
+ method === "iterate" ? statement.iterate().next() : statement[method](),
130
+ );
131
+ expect(timeout(db)).toBe(900);
132
+ } finally {
133
+ await release();
134
+ }
135
+ expect(statement.get()).toEqual({ value: 1 });
136
+ });
137
+ });
138
+ }
139
+
140
+ test("turn deadline bounds exec, prepare, deferred reads and COMMIT", async () => {
141
+ await fixture(async (db, path) => {
142
+ const read = db.prepare("SELECT * FROM t");
143
+ const fresh = createSqliteTurnDatabase(path);
144
+ fresh.exec("PRAGMA busy_timeout=900");
145
+ const release = await hold(path);
146
+ try {
147
+ bounded(() => db.exec("SELECT * FROM t"));
148
+ bounded(() => fresh.prepare("SELECT * FROM t"));
149
+ db.exec("BEGIN DEFERRED");
150
+ bounded(() => read.get());
151
+ db.exec("ROLLBACK");
152
+ } finally {
153
+ fresh.close();
154
+ await release();
155
+ }
156
+ db.exec("BEGIN IMMEDIATE; UPDATE t SET value=2");
157
+ const releaseReader = await hold(path, true);
158
+ try {
159
+ bounded(() => db.exec("COMMIT"));
160
+ expect(db.inTransaction).toBe(true);
161
+ expect(timeout(db)).toBe(900);
162
+ db.exec("ROLLBACK");
163
+ } finally {
164
+ await releaseReader();
165
+ }
166
+ expect(read.get()).toEqual({ value: 1 });
167
+ });
168
+ });
169
+
170
+ test("timeout mirror follows setters, prepare-time effects and failed script prefixes", async () => {
171
+ await fixture(async (db, path) => {
172
+ db.exec(
173
+ "PRAGMA main.busy_timeout(850); SELECT '; PRAGMA busy_timeout=0;'; -- PRAGMA busy_timeout=1",
174
+ );
175
+ const setter = db.prepare("PRAGMA busy_timeout=800");
176
+ expect(timeout(db)).toBe(800);
177
+ const success = () =>
178
+ withSqliteTurnDeadline(
179
+ () => 80,
180
+ () => db.prepare("SELECT 1").get(),
181
+ );
182
+ success();
183
+ expect(timeout(db)).toBe(800);
184
+ db.exec("PRAGMA busy_timeout=750");
185
+ setter.get();
186
+ const before = timeout(db);
187
+ success();
188
+ expect(timeout(db)).toBe(before);
189
+ const iterator = db.prepare("PRAGMA busy_timeout=600").iterate();
190
+ db.exec("PRAGMA busy_timeout=500");
191
+ iterator.next();
192
+ const after = timeout(db);
193
+ success();
194
+ expect(timeout(db)).toBe(after);
195
+ iterator.return?.();
196
+ expect(() => db.exec("PRAGMA busy_timeout=700; SELECT * FROM missing_table")).toThrow();
197
+ const read = db.prepare("SELECT * FROM t");
198
+ const release = await hold(path);
199
+ try {
200
+ bounded(() => read.get());
201
+ expect(timeout(db)).toBe(700);
202
+ } finally {
203
+ await release();
204
+ }
205
+ });
206
+ });
207
+
208
+ test("deadline leaves zero-wait probes and existing writer acquisition semantics intact", async () => {
209
+ await fixture(async (db, path) => {
210
+ const release = await hold(path);
211
+ try {
212
+ bounded(() => withSqliteTransformPass(() => db.exec("BEGIN IMMEDIATE")));
213
+ expect(timeout(db)).toBe(900);
214
+ db.exec("PRAGMA busy_timeout=0");
215
+ const start = performance.now();
216
+ expect(() =>
217
+ withSqliteTurnDeadline(
218
+ () => 80,
219
+ () => db.exec("SELECT * FROM t"),
220
+ ),
221
+ ).toThrow();
222
+ expect(performance.now() - start).toBeLessThan(100);
223
+ expect(timeout(db)).toBe(0);
224
+ } finally {
225
+ await release();
226
+ }
227
+ });
228
+ });
229
+
230
+ for (const detach of [withoutSqliteTransformPass, withSqliteBackgroundWriter]) {
231
+ test(`detached native reads retain configured timeout (${detach.name})`, async () => {
232
+ await fixture(async (db, path) => {
233
+ const read = db.prepare("SELECT * FROM t");
234
+ const release = await hold(path);
235
+ try {
236
+ const start = performance.now();
237
+ expect(() =>
238
+ withSqliteTurnDeadline(
239
+ () => 0,
240
+ () => detach(() => read.get()),
241
+ ),
242
+ ).toThrow();
243
+ expect(performance.now() - start).toBeGreaterThan(700);
244
+ expect(timeout(db)).toBe(900);
245
+ } finally {
246
+ await release();
247
+ }
248
+ });
249
+ });
250
+ }
251
+
252
+ for (const outcome of ["success", "throw", "reject"] as const) {
253
+ test(`completed native lease stops consulting request callback (${outcome})`, async () => {
254
+ const db = createSqliteTurnDatabase(":memory:");
255
+ let reads = 0;
256
+ let descendant!: () => unknown;
257
+ let resume!: () => void;
258
+ const gate = new Promise<void>((resolve) => {
259
+ resume = resolve;
260
+ });
261
+ let pending!: Promise<unknown>;
262
+ try {
263
+ try {
264
+ await withSqliteTurnDeadline(
265
+ () => {
266
+ reads++;
267
+ return 80;
268
+ },
269
+ () => {
270
+ pending = gate.then(() => descendant());
271
+ descendant = () => db.prepare("SELECT 1").get();
272
+ if (outcome === "throw") throw new Error("sync failure");
273
+ if (outcome === "reject") return Promise.reject(new Error("async failure"));
274
+ },
275
+ );
276
+ } catch (error) {
277
+ expect(String(error)).toContain("failure");
278
+ }
279
+ resume();
280
+ await pending;
281
+ expect(reads).toBe(0);
282
+ } finally {
283
+ db.close();
284
+ }
285
+ });
286
+ }