@cortexkit/opencode-magic-context 0.32.0 → 0.32.1

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 (107) hide show
  1. package/README.md +3 -3
  2. package/dist/features/magic-context/dreamer/cron.d.ts +2 -2
  3. package/dist/features/magic-context/dreamer/cron.d.ts.map +1 -1
  4. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  5. package/dist/features/magic-context/message-index-async.d.ts +4 -1
  6. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  7. package/dist/features/magic-context/message-index.d.ts +7 -4
  8. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  9. package/dist/features/magic-context/migrations.d.ts +8 -16
  10. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  11. package/dist/features/magic-context/plugin-messages.d.ts.map +1 -1
  12. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  13. package/dist/features/magic-context/session-project-backfill.d.ts +6 -3
  14. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  15. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  16. package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
  17. package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -1
  18. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  19. package/dist/features/magic-context/smart-notes/runner.d.ts +1 -0
  20. package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
  21. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +8 -1
  22. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  23. package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts +10 -0
  24. package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts.map +1 -1
  25. package/dist/features/magic-context/smart-notes/storage.d.ts +16 -1
  26. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  27. package/dist/features/magic-context/storage-db.d.ts +1 -1
  28. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  29. package/dist/features/magic-context/storage-meta-persisted.d.ts +8 -5
  30. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  31. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  32. package/dist/features/magic-context/storage-schema-helpers.d.ts +3 -4
  33. package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  35. package/dist/features/magic-context/tool-owner-backfill.d.ts +2 -0
  36. package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -1
  37. package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
  38. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  39. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/compartment-render-epoch.d.ts +12 -0
  41. package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -0
  42. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/emergency-drop.d.ts +1 -0
  45. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +29 -0
  47. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -0
  48. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
  49. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
  51. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/inject-compartments.d.ts +1 -0
  53. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/read-session-chunk.d.ts +15 -1
  55. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/read-session-raw.d.ts +24 -2
  57. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/shadow-sender.d.ts +65 -27
  59. package/dist/hooks/magic-context/shadow-sender.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +27 -0
  61. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +4128 -1207
  65. package/dist/plugin/rpc-handlers.d.ts +3 -0
  66. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  67. package/dist/shared/models-dev-cache.d.ts +5 -0
  68. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  69. package/dist/shared/rpc-client.d.ts +3 -1
  70. package/dist/shared/rpc-client.d.ts.map +1 -1
  71. package/dist/shared/rpc-notifications.d.ts +11 -6
  72. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  73. package/dist/shared/rpc-server.d.ts +2 -2
  74. package/dist/shared/rpc-server.d.ts.map +1 -1
  75. package/dist/shared/sqlite.d.ts.map +1 -1
  76. package/dist/shared/tui-config.d.ts +5 -3
  77. package/dist/shared/tui-config.d.ts.map +1 -1
  78. package/dist/tui/data/context-db.d.ts +9 -2
  79. package/dist/tui/data/context-db.d.ts.map +1 -1
  80. package/dist/tui/data/notification-socket.d.ts +4 -5
  81. package/dist/tui/data/notification-socket.d.ts.map +1 -1
  82. package/dist/tui-compiled/data/context-db.d.ts +9 -2
  83. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  84. package/dist/tui-compiled/data/notification-socket.d.ts +4 -5
  85. package/dist/tui-compiled/data/notification-socket.d.ts.map +1 -1
  86. package/package.json +2 -1
  87. package/src/shared/models-dev-cache.test.ts +5 -0
  88. package/src/shared/models-dev-cache.ts +44 -6
  89. package/src/shared/opencode-compaction-detector.test.ts +11 -2
  90. package/src/shared/rpc-client.test.ts +213 -9
  91. package/src/shared/rpc-client.ts +48 -39
  92. package/src/shared/rpc-notifications.test.ts +26 -5
  93. package/src/shared/rpc-notifications.ts +44 -35
  94. package/src/shared/rpc-server.ts +69 -33
  95. package/src/shared/sqlite.ts +22 -3
  96. package/src/shared/tui-config.test.ts +4 -12
  97. package/src/shared/tui-config.ts +6 -6
  98. package/src/tui/data/context-db.test.ts +113 -0
  99. package/src/tui/data/context-db.ts +26 -19
  100. package/src/tui/data/notification-socket.test.ts +277 -11
  101. package/src/tui/data/notification-socket.ts +257 -86
  102. package/src/tui/index.tsx +33 -25
  103. package/src/tui/slots/sidebar-content.tsx +62 -24
  104. package/src/tui-compiled/data/context-db.ts +26 -19
  105. package/src/tui-compiled/data/notification-socket.ts +257 -86
  106. package/src/tui-compiled/index.tsx +41 -24
  107. package/src/tui-compiled/slots/sidebar-content.tsx +45 -23
@@ -1,13 +1,15 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import {
6
+ __resetNotificationStateForTests,
6
7
  drainNotifications,
7
8
  isTuiConnected,
8
9
  pushNotification,
9
10
  } from "../../shared/rpc-notifications";
10
11
  import { MagicContextRpcServer } from "../../shared/rpc-server";
12
+ import { rpcPortFilePath } from "../../shared/rpc-utils";
11
13
  import { closeRpc, initRpcClient } from "./context-db";
12
14
  import {
13
15
  _resetNotificationSocketStateForTesting,
@@ -18,6 +20,7 @@ import {
18
20
  const originalXdgDataHome = process.env.XDG_DATA_HOME;
19
21
  const tempDirs: string[] = [];
20
22
  const servers: MagicContextRpcServer[] = [];
23
+ const legacyServers: Array<ReturnType<typeof Bun.serve>> = [];
21
24
 
22
25
  afterEach(() => {
23
26
  _resetNotificationSocketStateForTesting();
@@ -25,7 +28,8 @@ afterEach(() => {
25
28
  for (const server of servers.splice(0)) {
26
29
  server.stop();
27
30
  }
28
- drainNotifications(Number.MAX_SAFE_INTEGER);
31
+ for (const server of legacyServers.splice(0)) server.stop(true);
32
+ __resetNotificationStateForTests();
29
33
  for (const dir of tempDirs.splice(0)) {
30
34
  rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
31
35
  }
@@ -54,6 +58,77 @@ async function startServer(dataHome: string, directory: string): Promise<MagicCo
54
58
  return server;
55
59
  }
56
60
 
61
+ function startLegacyServer(
62
+ dataHome: string,
63
+ directory: string,
64
+ notifications: SocketNotification[],
65
+ ): {
66
+ sockets: Set<{ close(): void }>;
67
+ helloCursors: number[];
68
+ ackCursors: number[];
69
+ } {
70
+ const token = "v032-query-token";
71
+ const sockets = new Set<{ close(): void }>();
72
+ const helloCursors: number[] = [];
73
+ const ackCursors: number[] = [];
74
+ const server = Bun.serve({
75
+ hostname: "127.0.0.1",
76
+ port: 0,
77
+ fetch(req, bunServer) {
78
+ const url = new URL(req.url);
79
+ if (url.pathname === "/health") {
80
+ // Frozen v0.32 response: pid only, with no instance_id.
81
+ return Response.json({ ok: true, pid: process.pid });
82
+ }
83
+ if (
84
+ url.pathname === "/ws" &&
85
+ req.headers.get("authorization") === `Bearer ${token}` &&
86
+ bunServer.upgrade(req, { data: {} })
87
+ ) {
88
+ return undefined;
89
+ }
90
+ return new Response("Not Found", { status: 404 });
91
+ },
92
+ websocket: {
93
+ open(ws) {
94
+ sockets.add(ws);
95
+ },
96
+ message(ws, raw) {
97
+ const msg = JSON.parse(String(raw)) as {
98
+ type?: string;
99
+ lastReceivedId?: number;
100
+ cursor?: number;
101
+ };
102
+ if (msg.type === "hello") {
103
+ const cursor = Number(msg.lastReceivedId ?? 0);
104
+ helloCursors.push(cursor);
105
+ for (const notification of notifications) {
106
+ if (notification.id > cursor) {
107
+ ws.send(JSON.stringify({ type: "notification", notification }));
108
+ }
109
+ }
110
+ // Frozen v0.32 acknowledgement: protocol epoch fields did not exist.
111
+ ws.send(JSON.stringify({ type: "hello-ack" }));
112
+ } else if (msg.type === "ack" && typeof msg.cursor === "number") {
113
+ ackCursors.push(msg.cursor);
114
+ }
115
+ },
116
+ close(ws) {
117
+ sockets.delete(ws);
118
+ },
119
+ },
120
+ });
121
+ legacyServers.push(server);
122
+
123
+ const portFile = rpcPortFilePath(storageDir(dataHome), directory, process.pid);
124
+ mkdirSync(dirname(portFile), { recursive: true });
125
+ writeFileSync(
126
+ portFile,
127
+ JSON.stringify({ port: server.port, pid: process.pid, started_at: Date.now(), token }),
128
+ );
129
+ return { sockets, helloCursors, ackCursors };
130
+ }
131
+
57
132
  async function waitFor(condition: () => boolean, label: string, timeoutMs = 4_000): Promise<void> {
58
133
  const start = Date.now();
59
134
  while (Date.now() - start < timeoutMs) {
@@ -64,6 +139,58 @@ async function waitFor(condition: () => boolean, label: string, timeoutMs = 4_00
64
139
  }
65
140
 
66
141
  describe("notification socket", () => {
142
+ test("a home-screen socket receives globals but leaves scoped notifications pending", async () => {
143
+ drainNotifications(Number.MAX_SAFE_INTEGER);
144
+ const dataHome = makeDataHome();
145
+ const directory = "/repo-home-screen";
146
+ await startServer(dataHome, directory);
147
+ initRpcClient(directory);
148
+
149
+ const received: SocketNotification[] = [];
150
+ startNotificationSocket({
151
+ getSessionId: () => null,
152
+ onNotification: (notification) => {
153
+ received.push(notification);
154
+ return true;
155
+ },
156
+ });
157
+ await waitFor(() => isTuiConnected(), "home-screen socket connection");
158
+ expect(isTuiConnected("ses_hidden")).toBe(false);
159
+
160
+ pushNotification("scoped", { action: "show-status-dialog" }, "ses_hidden");
161
+ pushNotification("global", { action: "show-status-dialog" });
162
+ await waitFor(
163
+ () => received.some((notification) => notification.type === "global"),
164
+ "global home-screen notification",
165
+ );
166
+ expect(received.some((notification) => notification.type === "scoped")).toBe(false);
167
+ expect(drainNotifications(0, "ses_hidden", { sessionOnly: true })).toHaveLength(1);
168
+ });
169
+
170
+ test("overlapping starts create one socket and one delivery", async () => {
171
+ drainNotifications(Number.MAX_SAFE_INTEGER);
172
+ const dataHome = makeDataHome();
173
+ const directory = "/repo-overlapping-start";
174
+ await startServer(dataHome, directory);
175
+ initRpcClient(directory);
176
+
177
+ let deliveries = 0;
178
+ const options = {
179
+ getSessionId: () => "ses_once",
180
+ onNotification: () => {
181
+ deliveries += 1;
182
+ return true;
183
+ },
184
+ };
185
+ startNotificationSocket(options);
186
+ startNotificationSocket(options);
187
+ await waitFor(() => isTuiConnected("ses_once"), "single overlapping connection");
188
+ pushNotification("once", { ok: true }, "ses_once");
189
+ await waitFor(() => deliveries === 1, "single notification delivery");
190
+ await new Promise((resolve) => setTimeout(resolve, 75));
191
+ expect(deliveries).toBe(1);
192
+ });
193
+
67
194
  test("uses the active session cursor when switching sessions", async () => {
68
195
  drainNotifications(Number.MAX_SAFE_INTEGER);
69
196
  const dataHome = makeDataHome();
@@ -99,8 +226,8 @@ describe("notification socket", () => {
99
226
  expect(received.map((notification) => notification.type)).toContain("for-b");
100
227
  });
101
228
 
102
- test("resets the cached endpoint after websocket close so reconnect discovers a new server", async () => {
103
- drainNotifications(Number.MAX_SAFE_INTEGER);
229
+ test("clears cursors and deduplication when a fresh server reuses notification ids", async () => {
230
+ __resetNotificationStateForTests();
104
231
  const dataHome = makeDataHome();
105
232
  const directory = "/repo-reconnect";
106
233
  const first = await startServer(dataHome, directory);
@@ -116,16 +243,155 @@ describe("notification socket", () => {
116
243
  });
117
244
  await waitFor(() => isTuiConnected("ses_R"), "initial websocket connection");
118
245
 
119
- const second = await startServer(dataHome, directory);
120
- expect(second).toBeDefined();
246
+ pushNotification("before-restart", { action: "show-status-dialog" }, "ses_R");
247
+ await waitFor(() => received.length === 1, "first notification acknowledgement");
248
+ await waitFor(
249
+ () => drainNotifications(0, "ses_R").length === 0,
250
+ "first server queue to empty",
251
+ );
252
+ expect(received[0].id).toBe(1);
253
+
121
254
  first.stop();
122
255
  await waitFor(() => !isTuiConnected("ses_R"), "old websocket sink removed");
123
- await waitFor(() => isTuiConnected("ses_R"), "websocket reconnected to replacement server");
256
+ // A process restart recreates module state as well as the RPC server. The
257
+ // client state intentionally survives so reused ids exercise epoch reset.
258
+ __resetNotificationStateForTests();
259
+ pushNotification("queued-after-restart", { action: "show-status-dialog" }, "ses_R");
260
+ const second = await startServer(dataHome, directory);
261
+ expect(second).toBeDefined();
124
262
 
125
- pushNotification("after-restart", { action: "show-status-dialog" }, "ses_R");
126
263
  await waitFor(
127
- () => received.some((notification) => notification.type === "after-restart"),
128
- "notification from replacement server",
264
+ () => received.some((notification) => notification.type === "queued-after-restart"),
265
+ "reused id from replacement server backlog",
129
266
  );
267
+ const restarted = received.find(
268
+ (notification) => notification.type === "queued-after-restart",
269
+ );
270
+ expect(restarted?.id).toBe(received[0].id);
271
+ });
272
+
273
+ test("acknowledges only consumed ids and redelivers a failed earlier notification", async () => {
274
+ __resetNotificationStateForTests();
275
+ const dataHome = makeDataHome();
276
+ const directory = "/repo-exact-ack";
277
+ await startServer(dataHome, directory);
278
+ initRpcClient(directory);
279
+
280
+ let releaseFirst: ((consumed: boolean) => void) | undefined;
281
+ const firstResult = new Promise<boolean>((resolve) => {
282
+ releaseFirst = resolve;
283
+ });
284
+ let slowCalls = 0;
285
+ let fastCalls = 0;
286
+ startNotificationSocket({
287
+ getSessionId: () => "ses_exact",
288
+ onNotification: (notification) => {
289
+ if (notification.type === "slow") {
290
+ slowCalls += 1;
291
+ return slowCalls === 1 ? firstResult : true;
292
+ }
293
+ fastCalls += 1;
294
+ return true;
295
+ },
296
+ });
297
+ await waitFor(() => isTuiConnected("ses_exact"), "exact ack socket connection");
298
+
299
+ pushNotification("slow", { action: "show-status-dialog" }, "ses_exact");
300
+ pushNotification("fast", { action: "show-toast" }, "ses_exact");
301
+ await waitFor(() => slowCalls === 1, "blocked first handler");
302
+ expect(fastCalls).toBe(0);
303
+ releaseFirst?.(false);
304
+ await waitFor(() => fastCalls === 1, "later handler completion");
305
+ await waitFor(() => {
306
+ const pending = drainNotifications(0, "ses_exact");
307
+ return pending.length === 1 && pending[0].type === "slow";
308
+ }, "only failed notification to remain queued");
309
+
310
+ _resetNotificationSocketStateForTesting();
311
+ await waitFor(() => !isTuiConnected("ses_exact"), "socket disconnect before redelivery");
312
+ startNotificationSocket({
313
+ getSessionId: () => "ses_exact",
314
+ onNotification: (notification) => {
315
+ if (notification.type === "slow") slowCalls += 1;
316
+ if (notification.type === "fast") fastCalls += 1;
317
+ return true;
318
+ },
319
+ });
320
+ await waitFor(() => slowCalls === 2, "failed notification redelivery");
321
+ expect(fastCalls).toBe(1);
322
+ });
323
+
324
+ test("legacy mode keeps its watermark behind a declined notification across reconnect", async () => {
325
+ const dataHome = makeDataHome();
326
+ const directory = "/repo-v032-notifications";
327
+ const sessionId = "ses_v032";
328
+ const legacy = startLegacyServer(dataHome, directory, [
329
+ { id: 1, type: "declined", payload: {}, sessionId },
330
+ { id: 2, type: "consumed", payload: {}, sessionId },
331
+ ]);
332
+ initRpcClient(directory);
333
+
334
+ let declinedCalls = 0;
335
+ let consumedCalls = 0;
336
+ startNotificationSocket({
337
+ getSessionId: () => sessionId,
338
+ onNotification: (notification) => {
339
+ if (notification.type === "declined") {
340
+ declinedCalls += 1;
341
+ return declinedCalls > 1;
342
+ }
343
+ consumedCalls += 1;
344
+ return true;
345
+ },
346
+ });
347
+
348
+ await waitFor(() => declinedCalls === 1 && consumedCalls === 1, "legacy backlog handling");
349
+ expect(Math.max(0, ...legacy.ackCursors)).toBe(0);
350
+ for (const ws of legacy.sockets) ws.close();
351
+
352
+ await waitFor(() => legacy.helloCursors.length >= 2, "legacy reconnect hello");
353
+ await waitFor(() => declinedCalls === 2, "declined notification redelivery");
354
+ expect(legacy.helloCursors[1]).toBe(0);
355
+ expect(consumedCalls).toBe(1);
356
+ await waitFor(() => legacy.ackCursors.includes(2), "gap-safe legacy watermark advance");
357
+ });
358
+
359
+ test("serializes back-to-back dialog notification handlers", async () => {
360
+ __resetNotificationStateForTests();
361
+ const dataHome = makeDataHome();
362
+ const directory = "/repo-serialized-dialogs";
363
+ await startServer(dataHome, directory);
364
+ initRpcClient(directory);
365
+
366
+ let releaseFirst: (() => void) | undefined;
367
+ const firstSettled = new Promise<void>((resolve) => {
368
+ releaseFirst = resolve;
369
+ });
370
+ const events: string[] = [];
371
+ startNotificationSocket({
372
+ getSessionId: () => "ses_dialogs",
373
+ onNotification: async (notification) => {
374
+ events.push(`start:${notification.type}`);
375
+ if (notification.type === "dialog-one") await firstSettled;
376
+ events.push(`finish:${notification.type}`);
377
+ return true;
378
+ },
379
+ });
380
+ await waitFor(() => isTuiConnected("ses_dialogs"), "dialog socket connection");
381
+
382
+ pushNotification("dialog-one", { action: "show-status-dialog" }, "ses_dialogs");
383
+ pushNotification("dialog-two", { action: "show-upgrade-dialog" }, "ses_dialogs");
384
+ await waitFor(() => events.includes("start:dialog-one"), "first dialog handler start");
385
+ await new Promise((resolve) => setTimeout(resolve, 75));
386
+ expect(events).toEqual(["start:dialog-one"]);
387
+
388
+ releaseFirst?.();
389
+ await waitFor(() => events.includes("finish:dialog-two"), "second dialog handler finish");
390
+ expect(events).toEqual([
391
+ "start:dialog-one",
392
+ "finish:dialog-one",
393
+ "start:dialog-two",
394
+ "finish:dialog-two",
395
+ ]);
130
396
  });
131
397
  });