@cortexkit/opencode-magic-context 0.21.8 → 0.22.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 (207) hide show
  1. package/README.md +116 -325
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/agents/permissions.d.ts +29 -14
  4. package/dist/agents/permissions.d.ts.map +1 -1
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/migrate-experimental.d.ts +29 -0
  7. package/dist/config/migrate-experimental.d.ts.map +1 -0
  8. package/dist/config/schema/magic-context.d.ts +80 -104
  9. package/dist/config/schema/magic-context.d.ts.map +1 -1
  10. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
  12. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
  13. package/dist/features/magic-context/compartment-events.d.ts +50 -0
  14. package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
  15. package/dist/features/magic-context/compartment-storage.d.ts +22 -0
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
  19. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
  21. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  23. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  24. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  25. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  26. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  27. package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
  28. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  29. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/constants.d.ts +4 -0
  31. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  33. package/dist/features/magic-context/memory/index.d.ts +1 -1
  34. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
  36. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
  37. package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
  38. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  40. package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
  41. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/types.d.ts +3 -1
  43. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  44. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  45. package/dist/features/magic-context/migrations.d.ts +7 -0
  46. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  47. package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
  48. package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
  49. package/dist/features/magic-context/project-identity.d.ts +2 -0
  50. package/dist/features/magic-context/project-identity.d.ts.map +1 -0
  51. package/dist/features/magic-context/storage-db.d.ts +51 -7
  52. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
  54. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
  55. package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
  56. package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
  57. package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
  58. package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
  59. package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
  60. package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
  61. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  62. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  63. package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
  64. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  65. package/dist/features/magic-context/storage-meta.d.ts +1 -0
  66. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  67. package/dist/features/magic-context/storage-project-state.d.ts +19 -0
  68. package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
  69. package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
  70. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
  71. package/dist/features/magic-context/storage-tags.d.ts +21 -1
  72. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  73. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
  74. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
  75. package/dist/features/magic-context/storage.d.ts +12 -3
  76. package/dist/features/magic-context/storage.d.ts.map +1 -1
  77. package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
  78. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  79. package/dist/features/magic-context/tagger.d.ts +15 -1
  80. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  81. package/dist/features/magic-context/types.d.ts +21 -0
  82. package/dist/features/magic-context/types.d.ts.map +1 -1
  83. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  84. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  85. package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
  86. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
  87. package/dist/features/magic-context/work-metrics.d.ts +66 -0
  88. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  89. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
  90. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
  92. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/command-handler.d.ts +13 -1
  94. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
  96. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
  98. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
  101. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
  104. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
  106. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
  110. package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
  111. package/dist/hooks/magic-context/decay-render.d.ts +67 -0
  112. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
  113. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  114. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  115. package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
  116. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  117. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
  119. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  121. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/hook.d.ts +9 -21
  123. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
  125. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
  127. package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
  128. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  129. package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
  130. package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
  131. package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
  132. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
  134. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  135. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
  136. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
  137. package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
  138. package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
  139. package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
  140. package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
  141. package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
  142. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  143. package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
  144. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  145. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  146. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
  147. package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
  148. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
  149. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  150. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
  151. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  152. package/dist/hooks/magic-context/transform.d.ts +9 -7
  153. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  154. package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
  155. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +9258 -3915
  158. package/dist/plugin/conflict-warning-hook.d.ts +13 -0
  159. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  160. package/dist/plugin/dream-timer.d.ts.map +1 -1
  161. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  162. package/dist/plugin/messages-transform.d.ts.map +1 -1
  163. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  164. package/dist/plugin/tool-registry.d.ts.map +1 -1
  165. package/dist/shared/announcement.d.ts +1 -1
  166. package/dist/shared/announcement.d.ts.map +1 -1
  167. package/dist/shared/rpc-client.d.ts +1 -0
  168. package/dist/shared/rpc-client.d.ts.map +1 -1
  169. package/dist/shared/rpc-notifications.d.ts +27 -5
  170. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  171. package/dist/shared/rpc-server.d.ts +1 -0
  172. package/dist/shared/rpc-server.d.ts.map +1 -1
  173. package/dist/shared/rpc-types.d.ts +30 -2
  174. package/dist/shared/rpc-types.d.ts.map +1 -1
  175. package/dist/shared/rpc-utils.d.ts +9 -0
  176. package/dist/shared/rpc-utils.d.ts.map +1 -1
  177. package/dist/shared/sqlite-helpers.d.ts +7 -7
  178. package/dist/shared/sqlite.d.ts +23 -14
  179. package/dist/shared/sqlite.d.ts.map +1 -1
  180. package/dist/shared/tag-transcript.d.ts +10 -1
  181. package/dist/shared/tag-transcript.d.ts.map +1 -1
  182. package/dist/tools/ctx-expand/tools.d.ts +5 -1
  183. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  184. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  185. package/dist/tui/data/context-db.d.ts +16 -1
  186. package/dist/tui/data/context-db.d.ts.map +1 -1
  187. package/package.json +2 -4
  188. package/src/shared/announcement.ts +6 -7
  189. package/src/shared/rpc-client.test.ts +49 -2
  190. package/src/shared/rpc-client.ts +19 -9
  191. package/src/shared/rpc-notifications.test.ts +54 -1
  192. package/src/shared/rpc-notifications.ts +82 -13
  193. package/src/shared/rpc-server.ts +33 -4
  194. package/src/shared/rpc-types.ts +30 -2
  195. package/src/shared/rpc-utils.ts +10 -0
  196. package/src/shared/sqlite-helpers.ts +9 -9
  197. package/src/shared/sqlite.ts +99 -80
  198. package/src/shared/tag-transcript.test.ts +280 -0
  199. package/src/shared/tag-transcript.ts +162 -33
  200. package/src/tui/data/context-db.ts +75 -11
  201. package/src/tui/index.tsx +223 -32
  202. package/src/tui/slots/sidebar-content.tsx +366 -34
  203. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
  204. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
  205. package/dist/shared/native-binding.d.ts +0 -87
  206. package/dist/shared/native-binding.d.ts.map +0 -1
  207. package/src/shared/native-binding.ts +0 -311
@@ -48,7 +48,7 @@
48
48
  import type { ContextDatabase } from "../features/magic-context/storage";
49
49
  import { saveSourceContent } from "../features/magic-context/storage-source";
50
50
  import { updateTagByteSize, updateTagInputByteSize } from "../features/magic-context/storage-tags";
51
- import type { Tagger } from "../features/magic-context/tagger";
51
+ import { makeToolCompositeKey, type Tagger } from "../features/magic-context/tagger";
52
52
  import {
53
53
  byteSize,
54
54
  prependTag,
@@ -67,6 +67,15 @@ export interface TagTranscriptOptions {
67
67
  * consistent across passes.
68
68
  */
69
69
  skipPrefixInjection?: boolean;
70
+ /**
71
+ * Pi-only: map of messageId → raw-message fingerprint. When a NEW message
72
+ * text tag is created, its fingerprint is persisted on the tag row so a
73
+ * later pass can adopt the fallback-id tag onto the real SessionEntry id
74
+ * (keeping tag_number/§N§ stable). OpenCode omits this → tags store NULL
75
+ * → adoption never fires. Keyed by the bare messageId (not the `:pN`
76
+ * contentId) since all parts of a message share one fingerprint.
77
+ */
78
+ entryFingerprintByMessageId?: ReadonlyMap<string, string>;
70
79
  }
71
80
 
72
81
  export interface TagTranscriptResult {
@@ -138,13 +147,13 @@ export function tagTranscript(
138
147
  const skipPrefixInjection = options.skipPrefixInjection === true;
139
148
  const targets = new Map<number, TagTarget>();
140
149
 
141
- // Per-callId tool aggregation tracked across the single walk. Tags
142
- // get allocated in transcript order (first occurrence reserves the
143
- // tag number); subsequent occurrences reuse the same tag and merge
144
- // their occurrence into the aggregate's TagTarget. This preserves
145
- // chronological tag numbering while still aggregating drop behavior
146
- // across both invocation and result.
150
+ // Tool aggregation is keyed by the same owner+callId identity used by
151
+ // assignToolTag. OpenCode/Pi callId counters can repeat across turns, so
152
+ // a bare callId key can merge distinct invocations and replay drops/status
153
+ // changes against the wrong tool pair.
147
154
  const toolAggregates = new Map<string, ToolAggregate & { tagId: number }>();
155
+ const openToolAggregateKeysByCallId = new Map<string, string[]>();
156
+ let activeToolResultRun: { callId: string; aggregateKey: string } | undefined;
148
157
 
149
158
  // v3.3.1 Layer C (plan v3.3.1 Finding #16): the previous outer
150
159
  // db.transaction() wrapper rolled back EVERY tag insert + savedSource
@@ -156,6 +165,7 @@ export function tagTranscript(
156
165
  for (let msgIndex = 0; msgIndex < transcript.messages.length; msgIndex += 1) {
157
166
  const message = transcript.messages[msgIndex];
158
167
  if (message === undefined) continue;
168
+ activeToolResultRun = undefined;
159
169
  const messageId = message.info.id;
160
170
 
161
171
  let textOrdinal = 0;
@@ -165,6 +175,10 @@ export function tagTranscript(
165
175
  const part = parts[partIndex];
166
176
  if (part === undefined) continue;
167
177
 
178
+ if (part.kind !== "tool_result") {
179
+ activeToolResultRun = undefined;
180
+ }
181
+
168
182
  if (part.kind === "text") {
169
183
  // Synthetic message ids (Pi tail synthetic user with
170
184
  // no id) cannot be tagged — there's no stable handle
@@ -186,19 +200,25 @@ export function tagTranscript(
186
200
  db,
187
201
  targets,
188
202
  skipPrefixInjection,
203
+ entryFingerprint: options.entryFingerprintByMessageId?.get(messageId) ?? null,
189
204
  });
190
205
  textOrdinal += 1;
191
206
  continue;
192
207
  }
193
208
 
194
209
  if (part.kind === "tool_use" || part.kind === "tool_result") {
195
- if (messageId === undefined) continue;
210
+ if (messageId === undefined) {
211
+ activeToolResultRun = undefined;
212
+ continue;
213
+ }
196
214
 
197
215
  const callId = part.id;
198
216
  const text = part.getText() ?? "";
217
+ const toolByteSize = getToolPartByteSize(part, text);
199
218
  const meta = part.getToolMetadata();
200
219
 
201
220
  if (typeof callId !== "string" || callId.length === 0) {
221
+ activeToolResultRun = undefined;
202
222
  // No stable callId to aggregate on. Tag independently.
203
223
  tagToolPart({
204
224
  sessionId,
@@ -215,21 +235,35 @@ export function tagTranscript(
215
235
  continue;
216
236
  }
217
237
 
218
- const existing = toolAggregates.get(callId);
238
+ const pendingKeys = openToolAggregateKeysByCallId.get(callId) ?? [];
239
+ let existingKey: string | undefined;
240
+ if (part.kind === "tool_result") {
241
+ if (
242
+ activeToolResultRun !== undefined &&
243
+ activeToolResultRun.callId === callId
244
+ ) {
245
+ existingKey = activeToolResultRun.aggregateKey;
246
+ } else {
247
+ existingKey = findLastUnresolvedToolAggregateKey(
248
+ pendingKeys,
249
+ toolAggregates,
250
+ );
251
+ }
252
+ }
253
+ const aggregateKey: string = existingKey ?? makeToolCompositeKey(messageId, callId);
254
+ const existing = toolAggregates.get(aggregateKey);
219
255
  if (existing) {
220
- // Second (or later) occurrence for this call_id.
221
- // Merge into the existing aggregate, update byte_size
222
- // in DB if larger, and rebuild the TagTarget so the
223
- // closures over `occurrences` see all parts.
256
+ // Later occurrence for this owner+callId pair. Merge into the
257
+ // aggregate, update byte accounting if larger, and rebuild the
258
+ // TagTarget so drops mutate both invocation and result.
224
259
  existing.occurrences.push({
225
260
  message,
226
261
  part,
227
262
  kind: part.kind,
228
263
  });
229
- const newByteSize = byteSize(text);
230
- if (newByteSize > existing.maxByteSize) {
231
- existing.maxByteSize = newByteSize;
232
- updateTagByteSize(db, sessionId, existing.tagId, newByteSize);
264
+ if (toolByteSize > existing.maxByteSize) {
265
+ existing.maxByteSize = toolByteSize;
266
+ updateTagByteSize(db, sessionId, existing.tagId, toolByteSize);
233
267
  }
234
268
  if (existing.toolName === null && meta.toolName) {
235
269
  existing.toolName = meta.toolName;
@@ -253,18 +287,23 @@ export function tagTranscript(
253
287
  existing.tagId,
254
288
  buildAggregateTarget(existing.tagId, existing.occurrences),
255
289
  );
290
+ if (part.kind === "tool_result") {
291
+ markToolAggregateResolved(
292
+ callId,
293
+ aggregateKey,
294
+ openToolAggregateKeysByCallId,
295
+ );
296
+ activeToolResultRun = { callId, aggregateKey };
297
+ }
256
298
  } else {
257
- // First occurrence reserve the tag number.
258
- // v3.3.1 Layer C: Pi main aggregation path. Owner
259
- // is the Pi message hosting the tool aggregate.
260
- // Owner stays stable across passes because Pi
261
- // re-emits the full transcript each time and
262
- // message ids are durable.
299
+ // First occurrence for this owner+callId identity — reserve
300
+ // the tag number. Owner stays stable across passes because
301
+ // transcript message ids are durable.
263
302
  const tagId = tagger.assignToolTag(
264
303
  sessionId,
265
304
  callId,
266
305
  messageId,
267
- byteSize(text),
306
+ toolByteSize,
268
307
  db,
269
308
  0,
270
309
  meta.toolName ?? null,
@@ -280,11 +319,14 @@ export function tagTranscript(
280
319
  kind: part.kind,
281
320
  },
282
321
  ],
283
- maxByteSize: byteSize(text),
322
+ maxByteSize: toolByteSize,
284
323
  toolName: meta.toolName ?? null,
285
324
  inputByteSize: part.kind === "tool_use" ? meta.inputByteSize : 0,
286
325
  };
287
- toolAggregates.set(callId, aggregate);
326
+ toolAggregates.set(aggregateKey, aggregate);
327
+ if (part.kind === "tool_use") {
328
+ openToolAggregateKeysByCallId.set(callId, [...pendingKeys, aggregateKey]);
329
+ }
288
330
  // Inject §N§ prefix into this occurrence's visible text
289
331
  // when it's a tool_result. (OpenCode parity: prefix
290
332
  // only goes on the result, not the invocation.)
@@ -292,6 +334,14 @@ export function tagTranscript(
292
334
  part.setText(prependTag(tagId, text));
293
335
  }
294
336
  targets.set(tagId, buildAggregateTarget(tagId, aggregate.occurrences));
337
+ if (part.kind === "tool_result") {
338
+ markToolAggregateResolved(
339
+ callId,
340
+ aggregateKey,
341
+ openToolAggregateKeysByCallId,
342
+ );
343
+ activeToolResultRun = { callId, aggregateKey };
344
+ }
295
345
  }
296
346
  }
297
347
  // thinking, image, file, structural, unknown → skip.
@@ -301,6 +351,69 @@ export function tagTranscript(
301
351
  return { targets };
302
352
  }
303
353
 
354
+ function findLastUnresolvedToolAggregateKey(
355
+ pendingKeys: string[],
356
+ toolAggregates: Map<string, ToolAggregate & { tagId: number }>,
357
+ ): string | undefined {
358
+ for (let i = pendingKeys.length - 1; i >= 0; i -= 1) {
359
+ const key = pendingKeys[i];
360
+ if (key === undefined) continue;
361
+ const aggregate = toolAggregates.get(key);
362
+ if (aggregate === undefined) continue;
363
+ if (!aggregate.occurrences.some((occ) => occ.kind === "tool_result")) {
364
+ return key;
365
+ }
366
+ }
367
+ return undefined;
368
+ }
369
+
370
+ function markToolAggregateResolved(
371
+ callId: string,
372
+ aggregateKey: string,
373
+ openToolAggregateKeysByCallId: Map<string, string[]>,
374
+ ): void {
375
+ const pendingKeys = openToolAggregateKeysByCallId.get(callId);
376
+ if (pendingKeys === undefined) return;
377
+ const nextPendingKeys = pendingKeys.filter((key) => key !== aggregateKey);
378
+ if (nextPendingKeys.length === 0) {
379
+ openToolAggregateKeysByCallId.delete(callId);
380
+ return;
381
+ }
382
+ openToolAggregateKeysByCallId.set(callId, nextPendingKeys);
383
+ }
384
+
385
+ function getToolPartByteSize(part: TranscriptPart, text: string): number {
386
+ const textByteSize = byteSize(text);
387
+ if (textByteSize > 0 || part.kind !== "tool_result") return textByteSize;
388
+ return getNonTextToolResultByteSize(part);
389
+ }
390
+
391
+ function getNonTextToolResultByteSize(part: TranscriptPart): number {
392
+ const record = isRecord(part) ? part : undefined;
393
+ const content =
394
+ record?.content ??
395
+ record?.rawContent ??
396
+ record?.rawPart ??
397
+ record?.part ??
398
+ record?.data ??
399
+ record?.image ??
400
+ record?.source;
401
+ const serialized = safeJsonStringify(content ?? part);
402
+ return serialized === undefined ? 0 : byteSize(serialized);
403
+ }
404
+
405
+ function safeJsonStringify(value: unknown): string | undefined {
406
+ try {
407
+ return JSON.stringify(value);
408
+ } catch {
409
+ return undefined;
410
+ }
411
+ }
412
+
413
+ function isRecord(value: unknown): value is Record<string, unknown> {
414
+ return typeof value === "object" && value !== null;
415
+ }
416
+
304
417
  interface TagTextPartArgs {
305
418
  sessionId: string;
306
419
  message: { info: { id?: string; role: string } };
@@ -312,6 +425,7 @@ interface TagTextPartArgs {
312
425
  db: ContextDatabase;
313
426
  targets: Map<number, TagTarget>;
314
427
  skipPrefixInjection: boolean;
428
+ entryFingerprint: string | null;
315
429
  }
316
430
 
317
431
  function tagTextPart(args: TagTextPartArgs): void {
@@ -323,6 +437,10 @@ function tagTextPart(args: TagTextPartArgs): void {
323
437
  "message",
324
438
  byteSize(text),
325
439
  args.db,
440
+ 0,
441
+ null,
442
+ 0,
443
+ args.entryFingerprint,
326
444
  );
327
445
 
328
446
  // Persist the original (pre-tagged) source content so caveman
@@ -371,6 +489,7 @@ function tagToolPart(args: TagToolPartArgs): void {
371
489
  const stableId = args.part.id;
372
490
  const contentId = stableId ?? `${args.messageId}:t${args.partIndex}`;
373
491
  const text = args.part.getText() ?? "";
492
+ const toolByteSize = getToolPartByteSize(args.part, text);
374
493
  const meta = args.part.getToolMetadata();
375
494
  // v3.3.1 Layer C: synthetic ownership for the no-callId Pi
376
495
  // fallback. Owner == callId == contentId. The composite key
@@ -382,7 +501,7 @@ function tagToolPart(args: TagToolPartArgs): void {
382
501
  args.sessionId,
383
502
  contentId,
384
503
  contentId,
385
- byteSize(text),
504
+ toolByteSize,
386
505
  args.db,
387
506
  0,
388
507
  meta.toolName ?? null,
@@ -400,6 +519,18 @@ function tagToolPart(args: TagToolPartArgs): void {
400
519
  args.targets.set(tagId, buildToolTarget(args.part, args.message));
401
520
  }
402
521
 
522
+ function setToolContentOrText(part: TranscriptPart, content: string): boolean {
523
+ try {
524
+ if (part.setToolOutput(content)) return true;
525
+ } catch {
526
+ // Pi assistant tool_use parts deliberately assert if callers try
527
+ // to write a nonexistent output slot. Truncated-mode drops still
528
+ // need to shrink the invocation, so fall back to visible text/args
529
+ // replacement while preserving the adapter-level invariant.
530
+ }
531
+ return part.setText(content);
532
+ }
533
+
403
534
  /**
404
535
  * Build a TagTarget that walks ALL occurrences of a tool call (invocation
405
536
  * + result) when mutating. This is the per-callId aggregate target used
@@ -428,9 +559,7 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
428
559
  for (const occ of occurrences) {
429
560
  // Try setToolOutput first (works on tool_result-shaped parts);
430
561
  // fall back to setText so tool_use parts also get sentinelized.
431
- if (occ.part.setToolOutput(content)) {
432
- changed = true;
433
- } else if (occ.part.setText(content)) {
562
+ if (setToolContentOrText(occ.part, content)) {
434
563
  changed = true;
435
564
  }
436
565
  }
@@ -461,7 +590,7 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
461
590
  const sentinel = "[truncated]";
462
591
  let any = false;
463
592
  for (const occ of occurrences) {
464
- if (occ.part.setToolOutput(sentinel) || occ.part.setText(sentinel)) {
593
+ if (setToolContentOrText(occ.part, sentinel)) {
465
594
  any = true;
466
595
  }
467
596
  }
@@ -521,7 +650,7 @@ function buildToolTarget(
521
650
  ): TagTarget {
522
651
  return {
523
652
  setContent(content: string): boolean {
524
- return part.setToolOutput(content) || part.setText(content);
653
+ return setToolContentOrText(part, content);
525
654
  },
526
655
  getContent(): string | null {
527
656
  return part.getText() ?? null;
@@ -542,7 +671,7 @@ function buildToolTarget(
542
671
  // via setToolOutput so the underlying tool_result content
543
672
  // gets the truncation; falls back to setText for cases
544
673
  // where the part type doesn't support setToolOutput.
545
- const ok = part.setToolOutput("[truncated]") || part.setText("[truncated]");
674
+ const ok = setToolContentOrText(part, "[truncated]");
546
675
  return ok ? "truncated" : "absent";
547
676
  },
548
677
  message: {
@@ -10,7 +10,8 @@ import type { RpcNotificationMessage, SidebarSnapshot, StatusDetail } from "../.
10
10
  export type { SidebarSnapshot, StatusDetail };
11
11
 
12
12
  let rpcClient: MagicContextRpcClient | null = null;
13
- let lastReceivedNotificationId = 0;
13
+ let rpcGeneration = 0;
14
+ const lastReceivedNotificationIdBySession = new Map<string, number>();
14
15
 
15
16
  function getStorageDir(): string {
16
17
  // Plugin v0.16+ uses the shared cortexkit/magic-context path so OpenCode
@@ -24,14 +25,25 @@ function getStorageDir(): string {
24
25
  /** Initialize the RPC client. Call once on TUI startup. */
25
26
  export function initRpcClient(directory: string): void {
26
27
  const storageDir = getStorageDir();
28
+ // Bump the generation before replacing the client so late notification
29
+ // responses from a disposed client cannot repopulate cleared cursors.
30
+ rpcGeneration += 1;
31
+ lastReceivedNotificationIdBySession.clear();
27
32
  rpcClient = new MagicContextRpcClient(storageDir, directory);
28
33
  }
29
34
 
35
+ export function getRpcGeneration(): number {
36
+ return rpcGeneration;
37
+ }
38
+
30
39
  /** Clean up the RPC client. */
31
40
  export function closeRpc(): void {
41
+ // Closing invalidates any already-issued RPC calls; their callbacks must
42
+ // observe the new generation and avoid advancing stale notification cursors.
43
+ rpcGeneration += 1;
32
44
  rpcClient?.reset();
33
45
  rpcClient = null;
34
- lastReceivedNotificationId = 0;
46
+ lastReceivedNotificationIdBySession.clear();
35
47
  }
36
48
 
37
49
  const EMPTY_SNAPSHOT: SidebarSnapshot = {
@@ -55,6 +67,8 @@ const EMPTY_SNAPSHOT: SidebarSnapshot = {
55
67
  compartmentTokens: 0,
56
68
  factTokens: 0,
57
69
  memoryTokens: 0,
70
+ docsTokens: 0,
71
+ profileTokens: 0,
58
72
  conversationTokens: 0,
59
73
  toolCallTokens: 0,
60
74
  toolDefinitionTokens: 0,
@@ -229,7 +243,35 @@ export async function requestRecomp(sessionId: string): Promise<boolean> {
229
243
  }
230
244
  }
231
245
 
246
+ /** Run `/ctx-session-upgrade` for the session (full recomp + once-per-project
247
+ * memory migration). Fired from the upgrade dialog's "Run upgrade now" action. */
248
+ export async function requestUpgrade(sessionId: string): Promise<boolean> {
249
+ if (!rpcClient) return false;
250
+ try {
251
+ const result = await rpcClient.call<{ ok: boolean }>("upgrade", { sessionId });
252
+ return result.ok ?? false;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+
258
+ /** Mark the upgrade reminder dismissed (the user made an explicit Confirm/Cancel
259
+ * choice), setting the durable stamp so the FRESH dialog won't re-show. Resume
260
+ * prompts are staging-driven and unaffected. */
261
+ export async function dismissUpgradeReminder(sessionId: string): Promise<boolean> {
262
+ if (!rpcClient) return false;
263
+ try {
264
+ const result = await rpcClient.call<{ ok: boolean }>("dismiss-upgrade-reminder", {
265
+ sessionId,
266
+ });
267
+ return result.ok ?? false;
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
232
273
  export interface TuiMessage {
274
+ id: number;
233
275
  type: string;
234
276
  payload: Record<string, unknown>;
235
277
  sessionId?: string;
@@ -279,20 +321,24 @@ export async function markAnnounced(): Promise<boolean> {
279
321
  }
280
322
 
281
323
  /** Poll for pending server→TUI notifications via RPC. */
282
- export async function consumeTuiMessages(): Promise<TuiMessage[]> {
324
+ export async function consumeTuiMessages(sessionId: string): Promise<TuiMessage[]> {
283
325
  if (!rpcClient) return [];
284
326
  try {
285
327
  const result = await rpcClient.call<{ messages: RpcNotificationMessage[] }>(
286
328
  "pending-notifications",
287
- { lastReceivedId: lastReceivedNotificationId },
329
+ // Pass the TUI's active session so the server only drains
330
+ // notifications scoped to it (or global ones). Without this, a
331
+ // notification for another session served by the same process (e.g.
332
+ // OpenCode Desktop on the same project) could surface here. The
333
+ // cursor is per-session and is advanced by the poller only after it
334
+ // has delivered the returned batch.
335
+ {
336
+ lastReceivedId: lastReceivedNotificationIdBySession.get(sessionId) ?? 0,
337
+ sessionId,
338
+ },
288
339
  );
289
- const messages = result.messages ?? [];
290
- for (const message of messages) {
291
- if (message.id > lastReceivedNotificationId) {
292
- lastReceivedNotificationId = message.id;
293
- }
294
- }
295
- return messages.map((m) => ({
340
+ return (result.messages ?? []).map((m) => ({
341
+ id: m.id,
296
342
  type: m.type,
297
343
  payload: m.payload,
298
344
  sessionId: m.sessionId,
@@ -301,3 +347,21 @@ export async function consumeTuiMessages(): Promise<TuiMessage[]> {
301
347
  return [];
302
348
  }
303
349
  }
350
+
351
+ /**
352
+ * Advance the delivered-message cursor for one active TUI session.
353
+ * Callers must pass only the contiguous handled prefix of the drained batch;
354
+ * this helper remains empty-safe and monotonic for that prefix.
355
+ */
356
+ export function markTuiMessagesHandled(sessionId: string, messages: TuiMessage[]): void {
357
+ const previous = lastReceivedNotificationIdBySession.get(sessionId) ?? 0;
358
+ let next = previous;
359
+ for (const message of messages) {
360
+ if (message.id > next) {
361
+ next = message.id;
362
+ }
363
+ }
364
+ if (next > previous) {
365
+ lastReceivedNotificationIdBySession.set(sessionId, next);
366
+ }
367
+ }