@cortexkit/opencode-magic-context 0.31.2 → 0.31.4

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 (108) hide show
  1. package/dist/config/index.d.ts.map +1 -1
  2. package/dist/config/project-security.d.ts +16 -12
  3. package/dist/config/project-security.d.ts.map +1 -1
  4. package/dist/config/schema/magic-context.d.ts +13 -0
  5. package/dist/config/schema/magic-context.d.ts.map +1 -1
  6. package/dist/features/magic-context/dreamer/classify-prompt.d.ts +2 -2
  7. package/dist/features/magic-context/dreamer/classify-prompt.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/classify.d.ts +8 -0
  9. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  11. package/dist/features/magic-context/dreamer/lease.d.ts +1 -0
  12. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/manifest-parser.d.ts +7 -0
  14. package/dist/features/magic-context/dreamer/manifest-parser.d.ts.map +1 -0
  15. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +2 -3
  16. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/map-memories.d.ts +8 -0
  18. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/promote-primers.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +5 -0
  21. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +2 -2
  23. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  25. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +4 -0
  26. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  27. package/dist/features/magic-context/memory/storage-memory.d.ts +12 -0
  28. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  29. package/dist/features/magic-context/memory/verification-paths.d.ts +16 -0
  30. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  31. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  32. package/dist/features/magic-context/message-index.d.ts +6 -0
  33. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  34. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  35. package/dist/features/magic-context/search.d.ts.map +1 -1
  36. package/dist/features/magic-context/smart-notes/capabilities.d.ts +1 -0
  37. package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
  38. package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -2
  39. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  40. package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
  41. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +3 -2
  42. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  43. package/dist/features/magic-context/smart-notes/schedule.d.ts.map +1 -1
  44. package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts +17 -6
  45. package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts.map +1 -1
  46. package/dist/features/magic-context/smart-notes/types.d.ts +6 -1
  47. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  49. package/dist/features/magic-context/storage-meta-persisted.d.ts +8 -0
  50. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  51. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  52. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage.d.ts +1 -1
  54. package/dist/features/magic-context/storage.d.ts.map +1 -1
  55. package/dist/features/magic-context/workspaces.d.ts +8 -0
  56. package/dist/features/magic-context/workspaces.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/compartment-runner.d.ts +2 -0
  59. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/tag-messages.d.ts +1 -0
  64. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/todo-view.d.ts +27 -2
  66. package/dist/hooks/magic-context/todo-view.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/transform-message-helpers.d.ts +6 -3
  68. package/dist/hooks/magic-context/transform-message-helpers.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts +1 -0
  70. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  71. package/dist/index.js +1046 -333
  72. package/dist/shared/assistant-message-extractor.d.ts +1 -0
  73. package/dist/shared/assistant-message-extractor.d.ts.map +1 -1
  74. package/dist/shared/redaction.d.ts.map +1 -1
  75. package/dist/shared/rpc-notifications.d.ts +21 -16
  76. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  77. package/dist/shared/rpc-server.d.ts +1 -0
  78. package/dist/shared/rpc-server.d.ts.map +1 -1
  79. package/dist/shared/rpc-utils.d.ts +3 -1
  80. package/dist/shared/rpc-utils.d.ts.map +1 -1
  81. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  82. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  83. package/dist/tools/ctx-search/types.d.ts +1 -1
  84. package/dist/tools/ctx-search/types.d.ts.map +1 -1
  85. package/dist/tui/data/notification-socket.d.ts +1 -0
  86. package/dist/tui/data/notification-socket.d.ts.map +1 -1
  87. package/dist/tui-compiled/badge-contrast.d.ts +46 -0
  88. package/dist/tui-compiled/badge-contrast.d.ts.map +1 -0
  89. package/dist/tui-compiled/data/context-db.d.ts +45 -0
  90. package/dist/tui-compiled/data/context-db.d.ts.map +1 -0
  91. package/dist/tui-compiled/data/notification-socket.d.ts +40 -0
  92. package/dist/tui-compiled/data/notification-socket.d.ts.map +1 -0
  93. package/package.json +7 -4
  94. package/src/shared/assistant-message-extractor.ts +20 -0
  95. package/src/shared/redaction.ts +1 -0
  96. package/src/shared/rpc-client.test.ts +164 -5
  97. package/src/shared/rpc-notifications.ts +73 -21
  98. package/src/shared/rpc-server.ts +69 -18
  99. package/src/shared/rpc-utils.ts +12 -2
  100. package/src/tui/data/notification-socket.test.ts +131 -0
  101. package/src/tui/data/notification-socket.ts +81 -16
  102. package/src/tui/entry.mjs +36 -0
  103. package/src/tui-compiled/badge-contrast.ts +84 -0
  104. package/src/tui-compiled/data/context-db.ts +353 -0
  105. package/src/tui-compiled/data/notification-socket.ts +294 -0
  106. package/src/tui-compiled/index.tsx +1204 -0
  107. package/src/tui-compiled/slots/sidebar-content.tsx +1052 -0
  108. package/src/tui-compiled/types/opencode-plugin-tui.d.ts +232 -0
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Persistent WebSocket to the server plugin's RPC server, replacing the old
3
+ * 500ms HTTP notification poll.
4
+ *
5
+ * Why this exists: the TUI plugin and the server plugin run in separate Bun
6
+ * runners in the same process, so they bridge over a localhost socket. The old
7
+ * bridge polled `pending-notifications` over HTTP every 500ms — and each poll
8
+ * opened a NEW loopback TCP connection (Bun's fetch isn't pooled to our server),
9
+ * which was the entire source of idle TUI CPU (#200). A single long-lived WS
10
+ * carries server→TUI pushes with zero per-event connection cost, and the server
11
+ * pushes notifications the instant they're queued (no polling latency).
12
+ *
13
+ * Session scope: the socket carries the TUI's active session in its `hello` so
14
+ * the server delivers only that session's (plus global) notifications and its
15
+ * `isTuiConnected(session)` routing stays correct. The active session is tracked
16
+ * with a cheap watcher that only reads `api.route.current` (a property access,
17
+ * no IPC) and re-scopes the socket ONLY when the session actually changes — so
18
+ * unlike the old poll it does no network work at idle.
19
+ */
20
+
21
+ import { getRpcClient, getRpcGeneration } from "./context-db";
22
+
23
+ export interface SocketNotification {
24
+ id: number;
25
+ type: string;
26
+ payload: Record<string, unknown>;
27
+ sessionId?: string;
28
+ }
29
+
30
+ interface NotificationSocketOptions {
31
+ /** Current active session id (re-read cheaply to follow session switches). */
32
+ getSessionId: () => string | null;
33
+ /** Handle one delivered notification. Returns true if it was consumed (so its
34
+ * id can advance the ack cursor). Async because dialog handlers await. */
35
+ onNotification: (notification: SocketNotification) => boolean | Promise<boolean>;
36
+ }
37
+
38
+ const RECONNECT_BASE_MS = 500;
39
+ const RECONNECT_MAX_MS = 10_000;
40
+ /** Cheap session-watch interval. Reads a property only; no network. The CPU bug
41
+ * was the per-tick fetch, not the timer — this tick does zero IPC at idle. */
42
+ const SESSION_WATCH_MS = 1_000;
43
+
44
+ let socket: WebSocket | null = null;
45
+ let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
46
+ let sessionWatchTimer: ReturnType<typeof setInterval> | undefined;
47
+ let reconnectAttempt = 0;
48
+ let closed = false;
49
+ let helloedSession: string | null = null;
50
+ let opts: NotificationSocketOptions | null = null;
51
+ /** Generation of the rpc client at connect time; a dispose/reinit bumps it and
52
+ * invalidates an in-flight socket so its late callbacks are ignored. */
53
+ let connectGeneration = 0;
54
+
55
+ const GLOBAL_CURSOR_KEY = "global";
56
+ const SESSION_CURSOR_PREFIX = "session:";
57
+ const MAX_DEDUPED_NOTIFICATION_IDS = 500;
58
+
59
+ /**
60
+ * Notification ids are process-global, but acknowledgement cursors are scoped. A
61
+ * high id consumed in session A must not become the prune watermark for session B;
62
+ * global notifications also carry their own cursor so they cannot skip session
63
+ * backlog. The id set de-dupes at-least-once re-delivery when a reconnect sends
64
+ * a session cursor that is intentionally lower than the global cursor.
65
+ */
66
+ const lastHandledIdByCursor = new Map<string, number>();
67
+ const handledNotificationIds = new Set<number>();
68
+ const handledNotificationIdOrder: number[] = [];
69
+
70
+ /** Open the persistent notification socket. Idempotent: a second call while open
71
+ * is a no-op. Reconnects on its own after any drop. */
72
+ export function startNotificationSocket(options: NotificationSocketOptions): void {
73
+ opts = options;
74
+ closed = false;
75
+ connectGeneration = getRpcGeneration();
76
+ connect();
77
+ if (!sessionWatchTimer) {
78
+ sessionWatchTimer = setInterval(watchSession, SESSION_WATCH_MS);
79
+ }
80
+ }
81
+
82
+ /** Close the socket and stop reconnecting. Call on TUI dispose. */
83
+ export function stopNotificationSocket(): void {
84
+ closed = true;
85
+ if (reconnectTimer) {
86
+ clearTimeout(reconnectTimer);
87
+ reconnectTimer = undefined;
88
+ }
89
+ if (sessionWatchTimer) {
90
+ clearInterval(sessionWatchTimer);
91
+ sessionWatchTimer = undefined;
92
+ }
93
+ try {
94
+ socket?.close();
95
+ } catch {
96
+ // best-effort
97
+ }
98
+ socket = null;
99
+ helloedSession = null;
100
+ reconnectAttempt = 0;
101
+ }
102
+
103
+ function scheduleReconnect(): void {
104
+ if (closed) return;
105
+ if (reconnectTimer) return;
106
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_MAX_MS);
107
+ reconnectAttempt += 1;
108
+ reconnectTimer = setTimeout(() => {
109
+ reconnectTimer = undefined;
110
+ connect();
111
+ }, delay);
112
+ }
113
+
114
+ async function connect(): Promise<void> {
115
+ if (closed) return;
116
+ if (socket) return; // already connected/connecting
117
+
118
+ const client = getRpcClient();
119
+ if (!client) {
120
+ scheduleReconnect();
121
+ return;
122
+ }
123
+ const endpoint = await client.resolveEndpoint();
124
+ // The generation may have bumped (dispose/reinit) while resolving — abandon.
125
+ if (closed || getRpcGeneration() !== connectGeneration) return;
126
+ if (!endpoint) {
127
+ scheduleReconnect();
128
+ return;
129
+ }
130
+
131
+ let ws: WebSocket;
132
+ try {
133
+ const tokenQuery = `?token=${encodeURIComponent(endpoint.token ?? "")}`;
134
+ ws = new WebSocket(`ws://127.0.0.1:${endpoint.port}/ws${tokenQuery}`);
135
+ } catch {
136
+ client.reset();
137
+ scheduleReconnect();
138
+ return;
139
+ }
140
+ socket = ws;
141
+
142
+ ws.addEventListener("open", () => {
143
+ if (socket !== ws) return;
144
+ reconnectAttempt = 0;
145
+ sendHello(ws, endpoint.token);
146
+ });
147
+
148
+ ws.addEventListener("message", (event) => {
149
+ if (socket !== ws) return;
150
+ void handleSocketMessage(ws, String((event as MessageEvent).data));
151
+ });
152
+
153
+ const onDown = () => {
154
+ client.reset();
155
+ if (socket === ws) {
156
+ socket = null;
157
+ helloedSession = null;
158
+ }
159
+ scheduleReconnect();
160
+ };
161
+ ws.addEventListener("close", onDown);
162
+ ws.addEventListener("error", onDown);
163
+ }
164
+
165
+ function sendHello(ws: WebSocket, token: string | null): void {
166
+ const sessionId = opts?.getSessionId() ?? undefined;
167
+ helloedSession = sessionId ?? null;
168
+ ws.send(
169
+ JSON.stringify({
170
+ type: "hello",
171
+ token: token ?? "",
172
+ sessionId,
173
+ lastReceivedId: cursorForKey(cursorKeyForSession(sessionId)),
174
+ globalLastReceivedId: cursorForKey(GLOBAL_CURSOR_KEY),
175
+ }),
176
+ );
177
+ }
178
+
179
+ async function handleSocketMessage(ws: WebSocket, raw: string): Promise<void> {
180
+ let msg: { type?: string; notification?: SocketNotification; error?: string };
181
+ try {
182
+ msg = JSON.parse(raw);
183
+ } catch {
184
+ return;
185
+ }
186
+
187
+ if (msg.type === "notification" && msg.notification) {
188
+ const notification = msg.notification;
189
+ // Client-side session filter mirrors the old poller's per-message re-check:
190
+ // a session-scoped notification is only acted on while the TUI is actually
191
+ // viewing that session (the active session can change between queueing and
192
+ // delivery). Global (session-less) notifications always apply.
193
+ const active = opts?.getSessionId() ?? null;
194
+ if (notification.sessionId && active && notification.sessionId !== active) {
195
+ // Not for the session we're viewing — do NOT ack it (a TUI on the right
196
+ // session, or a later switch back, should still get it). Just skip.
197
+ return;
198
+ }
199
+ if (handledNotificationIds.has(notification.id)) {
200
+ sendAck(ws, notification);
201
+ return;
202
+ }
203
+
204
+ let consumed = false;
205
+ try {
206
+ consumed = await Promise.resolve(opts?.onNotification(notification) ?? false);
207
+ } catch {
208
+ consumed = false;
209
+ }
210
+ // A dispose/reinit during an awaited dialog handler invalidates this socket.
211
+ if (socket !== ws || getRpcGeneration() !== connectGeneration) return;
212
+ if (consumed) {
213
+ rememberHandledId(notification.id);
214
+ advanceCursor(notificationCursorKey(notification), notification.id);
215
+ // Ack only the notification's own cursor scope. A dropped ack is safe:
216
+ // the next hello sends the same per-scope cursors and duplicates are
217
+ // ignored locally.
218
+ sendAck(ws, notification);
219
+ }
220
+ return;
221
+ }
222
+
223
+ if (msg.type === "error") {
224
+ // Server rejected us (bad token, etc.). Close and let backoff retry after
225
+ // rediscovering the port/token (the server may have been replaced).
226
+ try {
227
+ ws.close();
228
+ } catch {
229
+ // best-effort
230
+ }
231
+ }
232
+ }
233
+
234
+ function cursorKeyForSession(sessionId: string | null | undefined): string {
235
+ return sessionId ? `${SESSION_CURSOR_PREFIX}${sessionId}` : GLOBAL_CURSOR_KEY;
236
+ }
237
+
238
+ function notificationCursorKey(notification: SocketNotification): string {
239
+ return notification.sessionId
240
+ ? `${SESSION_CURSOR_PREFIX}${notification.sessionId}`
241
+ : GLOBAL_CURSOR_KEY;
242
+ }
243
+
244
+ function cursorForKey(key: string): number {
245
+ return lastHandledIdByCursor.get(key) ?? 0;
246
+ }
247
+
248
+ function advanceCursor(key: string, id: number): void {
249
+ if (id > cursorForKey(key)) lastHandledIdByCursor.set(key, id);
250
+ }
251
+
252
+ function rememberHandledId(id: number): void {
253
+ if (handledNotificationIds.has(id)) return;
254
+ handledNotificationIds.add(id);
255
+ handledNotificationIdOrder.push(id);
256
+ while (handledNotificationIdOrder.length > MAX_DEDUPED_NOTIFICATION_IDS) {
257
+ const evicted = handledNotificationIdOrder.shift();
258
+ if (evicted !== undefined) handledNotificationIds.delete(evicted);
259
+ }
260
+ }
261
+
262
+ function sendAck(ws: WebSocket, notification: SocketNotification): void {
263
+ const lastReceivedId = cursorForKey(notificationCursorKey(notification));
264
+ const ack = notification.sessionId
265
+ ? { type: "ack", sessionId: notification.sessionId, lastReceivedId }
266
+ : { type: "ack", ackScope: "global", lastReceivedId };
267
+ try {
268
+ ws.send(JSON.stringify(ack));
269
+ } catch {
270
+ // best-effort; reconnect hello re-syncs via per-scope cursors
271
+ }
272
+ }
273
+
274
+ export function _resetNotificationSocketStateForTesting(): void {
275
+ stopNotificationSocket();
276
+ opts = null;
277
+ lastHandledIdByCursor.clear();
278
+ handledNotificationIds.clear();
279
+ handledNotificationIdOrder.length = 0;
280
+ }
281
+
282
+ /** Cheap session-change watcher: re-scope the socket only when the active session
283
+ * actually changes. Reads a property; no network at idle. */
284
+ function watchSession(): void {
285
+ if (closed || !socket || socket.readyState !== WebSocket.OPEN) return;
286
+ const current = opts?.getSessionId() ?? null;
287
+ if (current === helloedSession) return;
288
+ // Re-hello with the new session; the server replaces this socket's sink scope.
289
+ const client = getRpcClient();
290
+ void client?.resolveEndpoint().then((endpoint) => {
291
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
292
+ sendHello(socket, endpoint?.token ?? null);
293
+ });
294
+ }