@cortexkit/opencode-magic-context 0.22.5 → 0.23.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 (160) hide show
  1. package/dist/agents/magic-context-prompt.d.ts +1 -1
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/agents/permissions.d.ts +4 -4
  4. package/dist/agents/permissions.d.ts.map +1 -1
  5. package/dist/config/project-security.d.ts +5 -0
  6. package/dist/config/project-security.d.ts.map +1 -1
  7. package/dist/config/schema/magic-context.d.ts +0 -13
  8. package/dist/config/schema/magic-context.d.ts.map +1 -1
  9. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  10. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  11. package/dist/features/magic-context/memory/constants.d.ts +7 -0
  12. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  13. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-db.d.ts +1 -1
  15. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  16. package/dist/features/magic-context/storage-meta-persisted.d.ts +83 -15
  17. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-meta-shared.d.ts +9 -2
  19. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  21. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
  23. package/dist/features/magic-context/storage-tags.d.ts +118 -1
  24. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  25. package/dist/features/magic-context/storage.d.ts +2 -2
  26. package/dist/features/magic-context/storage.d.ts.map +1 -1
  27. package/dist/features/magic-context/tagger.d.ts +12 -2
  28. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  29. package/dist/features/magic-context/tool-definition-tokens.d.ts +0 -21
  30. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  31. package/dist/features/magic-context/tool-owner-backfill.d.ts +2 -1
  32. package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -1
  33. package/dist/features/magic-context/types.d.ts +8 -0
  34. package/dist/features/magic-context/types.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/channel2-delivery.d.ts +22 -0
  38. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -0
  39. package/dist/hooks/magic-context/command-handler.d.ts +0 -1
  40. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
  45. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/compartment-trigger.d.ts +47 -2
  48. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +18 -0
  50. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -0
  51. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +117 -0
  52. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -0
  53. package/dist/hooks/magic-context/emergency-drop.d.ts +86 -0
  54. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -0
  55. package/dist/hooks/magic-context/event-handler.d.ts +6 -4
  56. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/execute-flush.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/execute-status.d.ts +1 -1
  59. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +10 -3
  61. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -9
  63. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/hook.d.ts +3 -5
  65. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/inject-compartments.d.ts +0 -2
  67. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/issue-135-wire-fixtures.d.ts +8 -0
  69. package/dist/hooks/magic-context/issue-135-wire-fixtures.d.ts.map +1 -0
  70. package/dist/hooks/magic-context/note-visibility.d.ts +1 -1
  71. package/dist/hooks/magic-context/openai-compat-adjacency.d.ts +38 -0
  72. package/dist/hooks/magic-context/openai-compat-adjacency.d.ts.map +1 -0
  73. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +132 -0
  74. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -0
  75. package/dist/hooks/magic-context/read-session-chunk.d.ts +55 -0
  76. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/read-session-raw.d.ts +91 -0
  79. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +70 -0
  81. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -0
  82. package/dist/hooks/magic-context/send-session-notification.d.ts +2 -1
  83. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/system-prompt-hash.d.ts +0 -1
  85. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/tag-messages.d.ts +3 -0
  87. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/todo-view.d.ts +1 -1
  89. package/dist/hooks/magic-context/tool-drop-target.d.ts +9 -0
  90. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +15 -0
  92. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +7 -10
  94. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/transform.d.ts +14 -17
  96. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/upgrade-reminder.d.ts +2 -1
  98. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +5215 -1140
  101. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  102. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -1
  103. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  104. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  105. package/dist/plugin/tool-registry.d.ts.map +1 -1
  106. package/dist/shared/announcement.d.ts +4 -6
  107. package/dist/shared/announcement.d.ts.map +1 -1
  108. package/dist/shared/live-server-client.d.ts +50 -0
  109. package/dist/shared/live-server-client.d.ts.map +1 -0
  110. package/dist/shared/prompt-context.d.ts +31 -0
  111. package/dist/shared/prompt-context.d.ts.map +1 -0
  112. package/dist/shared/rpc-types.d.ts +0 -3
  113. package/dist/shared/rpc-types.d.ts.map +1 -1
  114. package/dist/shared/safe-notification-target.d.ts +23 -0
  115. package/dist/shared/safe-notification-target.d.ts.map +1 -0
  116. package/dist/shared/tag-transcript.d.ts.map +1 -1
  117. package/dist/shared/transcript-opencode.d.ts.map +1 -1
  118. package/dist/shared/transcript.d.ts +15 -1
  119. package/dist/shared/transcript.d.ts.map +1 -1
  120. package/dist/tools/ctx-expand/constants.d.ts +1 -1
  121. package/dist/tools/ctx-expand/constants.d.ts.map +1 -1
  122. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  123. package/dist/tools/ctx-memory/constants.d.ts +1 -1
  124. package/dist/tools/ctx-memory/constants.d.ts.map +1 -1
  125. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  126. package/dist/tools/ctx-memory/types.d.ts +7 -3
  127. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  128. package/dist/tools/ctx-note/constants.d.ts +1 -1
  129. package/dist/tools/ctx-note/constants.d.ts.map +1 -1
  130. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  131. package/dist/tools/ctx-note/types.d.ts +4 -0
  132. package/dist/tools/ctx-note/types.d.ts.map +1 -1
  133. package/dist/tools/ctx-search/constants.d.ts +1 -1
  134. package/dist/tools/ctx-search/constants.d.ts.map +1 -1
  135. package/dist/tui/data/context-db.d.ts.map +1 -1
  136. package/package.json +2 -1
  137. package/src/shared/announcement.test.ts +18 -0
  138. package/src/shared/announcement.ts +35 -20
  139. package/src/shared/live-server-client.ts +152 -0
  140. package/src/shared/prompt-context.ts +135 -0
  141. package/src/shared/rpc-types.ts +0 -3
  142. package/src/shared/safe-notification-target.test.ts +97 -0
  143. package/src/shared/safe-notification-target.ts +102 -0
  144. package/src/shared/tag-transcript.test.ts +34 -8
  145. package/src/shared/tag-transcript.ts +110 -8
  146. package/src/shared/transcript-opencode.ts +15 -5
  147. package/src/shared/transcript.ts +20 -2
  148. package/src/tui/data/context-db.ts +0 -3
  149. package/src/tui/index.tsx +2 -5
  150. package/src/tui/slots/sidebar-content.tsx +1 -26
  151. package/dist/hooks/magic-context/apply-context-nudge.d.ts +0 -5
  152. package/dist/hooks/magic-context/apply-context-nudge.d.ts.map +0 -1
  153. package/dist/hooks/magic-context/nudge-bands.d.ts +0 -6
  154. package/dist/hooks/magic-context/nudge-bands.d.ts.map +0 -1
  155. package/dist/hooks/magic-context/nudge-injection.d.ts +0 -7
  156. package/dist/hooks/magic-context/nudge-injection.d.ts.map +0 -1
  157. package/dist/hooks/magic-context/nudge-placement-store.d.ts +0 -15
  158. package/dist/hooks/magic-context/nudge-placement-store.d.ts.map +0 -1
  159. package/dist/hooks/magic-context/nudger.d.ts +0 -21
  160. package/dist/hooks/magic-context/nudger.d.ts.map +0 -1
@@ -47,8 +47,15 @@
47
47
 
48
48
  import type { ContextDatabase } from "../features/magic-context/storage";
49
49
  import { saveSourceContent } from "../features/magic-context/storage-source";
50
- import { updateTagByteSize, updateTagInputByteSize } from "../features/magic-context/storage-tags";
50
+ import {
51
+ updateTagByteSize,
52
+ updateTagInputByteSize,
53
+ updateTagInputTokenCount,
54
+ updateTagTokenCount,
55
+ } from "../features/magic-context/storage-tags";
51
56
  import { makeToolCompositeKey, type Tagger } from "../features/magic-context/tagger";
57
+ import { estimateImageTokensFromDataUrl } from "../hooks/magic-context/image-token-estimate";
58
+ import { estimateTokens } from "../hooks/magic-context/read-session-formatting";
52
59
  import {
53
60
  byteSize,
54
61
  prependTag,
@@ -112,10 +119,11 @@ export interface TagTranscriptResult {
112
119
  /**
113
120
  * Per-callId aggregation of tool occurrences across the transcript.
114
121
  * Built up during the walk and used to:
115
- * 1. Assign one tag per call_id with byte_size from the LARGEST
116
- * occurrence (typically the result, ~4KB) instead of the args
117
- * (~58 bytes). Without this, drop projection underestimates
118
- * reclaimable bytes by ~70× per tool.
122
+ * 1. Assign one tag per call_id with byte_size = the tool_RESULT (output)
123
+ * size, and inputByteSize = the tool_use (args) size, tracked SEPARATELY
124
+ * (mirrors OpenCode tag-messages.ts). Reclaim accounting sums them
125
+ * (byteSize + inputByteSize + reasoning); folding args into byte_size too
126
+ * would double-count the args for a large-input/small-output tool.
119
127
  * 2. Build a single aggregate TagTarget that mutates BOTH the
120
128
  * invocation and result occurrences atomically, so a queued drop
121
129
  * replaces both halves with a sentinel instead of last-write-wins.
@@ -131,10 +139,14 @@ interface ToolAggregate {
131
139
  occurrences: ToolOccurrence[];
132
140
  /** Largest byteSize seen across occurrences — used as the tag size. */
133
141
  maxByteSize: number;
142
+ /** Token count paired with maxByteSize (the same output occurrence). */
143
+ maxTokenCount: number;
134
144
  /** Tool name from the first occurrence we see one on. */
135
145
  toolName: string | null;
136
146
  /** Input byte size from the invocation occurrence (for storage projection). */
137
147
  inputByteSize: number;
148
+ /** Whether input_token_count has been persisted (from the tool_use occurrence). */
149
+ inputTokenStored: boolean;
138
150
  }
139
151
 
140
152
  export function tagTranscript(
@@ -215,7 +227,9 @@ export function tagTranscript(
215
227
  const callId = part.id;
216
228
  const text = part.getText() ?? "";
217
229
  const toolByteSize = getToolPartByteSize(part, text);
230
+ const toolTokenCount = getToolPartTokenCount(part, text);
218
231
  const meta = part.getToolMetadata();
232
+ const inputTokenCount = meta.inputTokenCount;
219
233
 
220
234
  if (typeof callId !== "string" || callId.length === 0) {
221
235
  activeToolResultRun = undefined;
@@ -261,9 +275,19 @@ export function tagTranscript(
261
275
  part,
262
276
  kind: part.kind,
263
277
  });
264
- if (toolByteSize > existing.maxByteSize) {
278
+ // byte_size tracks OUTPUT bytes only (the tool_result
279
+ // occurrence). The invocation args are captured separately in
280
+ // inputByteSize below; counting tool_use bytes here too would
281
+ // double-count args in the emergency-drop reclaim formula
282
+ // (byteSize + inputByteSize + reasoning). Mirrors OpenCode,
283
+ // which assigns the tool tag on the result path.
284
+ if (part.kind === "tool_result" && toolByteSize > existing.maxByteSize) {
265
285
  existing.maxByteSize = toolByteSize;
286
+ existing.maxTokenCount = toolTokenCount;
266
287
  updateTagByteSize(db, sessionId, existing.tagId, toolByteSize);
288
+ // Keep token_count in lockstep with the byte bump so the
289
+ // grown output's tokens aren't undercounted by readers.
290
+ updateTagTokenCount(db, sessionId, existing.tagId, toolTokenCount);
267
291
  }
268
292
  if (existing.toolName === null && meta.toolName) {
269
293
  existing.toolName = meta.toolName;
@@ -276,6 +300,14 @@ export function tagTranscript(
276
300
  existing.inputByteSize = meta.inputByteSize;
277
301
  updateTagInputByteSize(db, sessionId, existing.tagId, meta.inputByteSize);
278
302
  }
303
+ if (
304
+ !existing.inputTokenStored &&
305
+ part.kind === "tool_use" &&
306
+ inputTokenCount > 0
307
+ ) {
308
+ existing.inputTokenStored = true;
309
+ updateTagInputTokenCount(db, sessionId, existing.tagId, inputTokenCount);
310
+ }
279
311
  // Inject §N§ prefix into this tool_result occurrence
280
312
  // (matches OpenCode behavior — only result gets the prefix).
281
313
  if (!skipPrefixInjection && part.kind === "tool_result") {
@@ -299,15 +331,28 @@ export function tagTranscript(
299
331
  // First occurrence for this owner+callId identity — reserve
300
332
  // the tag number. Owner stays stable across passes because
301
333
  // transcript message ids are durable.
334
+ // byte_size is OUTPUT-only (0 until the tool_result occurrence
335
+ // is seen); the invocation args live in inputByteSize. This
336
+ // keeps the emergency-drop reclaim formula (byteSize +
337
+ // inputByteSize + reasoning) from double-counting args when the
338
+ // first occurrence is a large tool_use. Mirrors OpenCode.
339
+ const outputByteSize = part.kind === "tool_result" ? toolByteSize : 0;
340
+ const outputTokenCount = part.kind === "tool_result" ? toolTokenCount : 0;
341
+ const firstInputTokenCount = part.kind === "tool_use" ? inputTokenCount : 0;
302
342
  const tagId = tagger.assignToolTag(
303
343
  sessionId,
304
344
  callId,
305
345
  messageId,
306
- toolByteSize,
346
+ outputByteSize,
307
347
  db,
308
348
  0,
309
349
  meta.toolName ?? null,
310
350
  meta.inputByteSize,
351
+ () => ({
352
+ tokenCount: outputTokenCount,
353
+ inputTokenCount: firstInputTokenCount,
354
+ reasoningTokenCount: null,
355
+ }),
311
356
  );
312
357
  const aggregate = {
313
358
  callId,
@@ -319,9 +364,11 @@ export function tagTranscript(
319
364
  kind: part.kind,
320
365
  },
321
366
  ],
322
- maxByteSize: toolByteSize,
367
+ maxByteSize: outputByteSize,
368
+ maxTokenCount: outputTokenCount,
323
369
  toolName: meta.toolName ?? null,
324
370
  inputByteSize: part.kind === "tool_use" ? meta.inputByteSize : 0,
371
+ inputTokenStored: part.kind === "tool_use" && firstInputTokenCount > 0,
325
372
  };
326
373
  toolAggregates.set(aggregateKey, aggregate);
327
374
  if (part.kind === "tool_use") {
@@ -382,13 +429,48 @@ function markToolAggregateResolved(
382
429
  openToolAggregateKeysByCallId.set(callId, nextPendingKeys);
383
430
  }
384
431
 
432
+ /** Real-tokenizer count for tagged text (images bill by visual tokens). */
433
+ function estimateTagTextTokens(text: string): number {
434
+ if (!text) return 0;
435
+ if (text.startsWith("data:image/")) return estimateImageTokensFromDataUrl(text);
436
+ return estimateTokens(text);
437
+ }
438
+
385
439
  function getToolPartByteSize(part: TranscriptPart, text: string): number {
386
440
  const textByteSize = byteSize(text);
387
441
  if (textByteSize > 0 || part.kind !== "tool_result") return textByteSize;
388
442
  return getNonTextToolResultByteSize(part);
389
443
  }
390
444
 
445
+ /**
446
+ * Real-tokenizer mirror of {@link getToolPartByteSize}: token count of a tool
447
+ * part's output text (falling back to the raw payload for non-text results,
448
+ * matching the byte path so token_count stays consistent with byte_size).
449
+ */
450
+ function getToolPartTokenCount(part: TranscriptPart, text: string): number {
451
+ if (text.length > 0 || part.kind !== "tool_result") return estimateTokens(text);
452
+ const raw = part.rawByteSize?.();
453
+ if (typeof raw === "number" && raw > 0) {
454
+ const record = isRecord(part) ? part : undefined;
455
+ const content =
456
+ record?.content ??
457
+ record?.rawContent ??
458
+ record?.rawPart ??
459
+ record?.part ??
460
+ record?.data ??
461
+ record?.image ??
462
+ record?.source;
463
+ const serialized = safeJsonStringify(content ?? part);
464
+ return serialized === undefined ? 0 : estimateTokens(serialized);
465
+ }
466
+ return 0;
467
+ }
468
+
391
469
  function getNonTextToolResultByteSize(part: TranscriptPart): number {
470
+ // Prefer the adapter's exact raw-payload size when available (Pi's
471
+ // tool_result proxy can serialize the real content array, incl. images).
472
+ const raw = part.rawByteSize?.();
473
+ if (typeof raw === "number" && raw > 0) return raw;
392
474
  const record = isRecord(part) ? part : undefined;
393
475
  const content =
394
476
  record?.content ??
@@ -441,6 +523,13 @@ function tagTextPart(args: TagTextPartArgs): void {
441
523
  null,
442
524
  0,
443
525
  args.entryFingerprint,
526
+ // Lazy: fires only on fresh insert. Strip any §N§ prefix so a re-tag
527
+ // from already-prefixed text still tokenizes the pristine content.
528
+ () => ({
529
+ tokenCount: estimateTagTextTokens(stripTagPrefix(text)),
530
+ inputTokenCount: null,
531
+ reasoningTokenCount: null,
532
+ }),
444
533
  );
445
534
 
446
535
  // Persist the original (pre-tagged) source content so caveman
@@ -490,6 +579,7 @@ function tagToolPart(args: TagToolPartArgs): void {
490
579
  const contentId = stableId ?? `${args.messageId}:t${args.partIndex}`;
491
580
  const text = args.part.getText() ?? "";
492
581
  const toolByteSize = getToolPartByteSize(args.part, text);
582
+ const toolTokenCount = getToolPartTokenCount(args.part, text);
493
583
  const meta = args.part.getToolMetadata();
494
584
  // v3.3.1 Layer C: synthetic ownership for the no-callId Pi
495
585
  // fallback. Owner == callId == contentId. The composite key
@@ -506,6 +596,11 @@ function tagToolPart(args: TagToolPartArgs): void {
506
596
  0,
507
597
  meta.toolName ?? null,
508
598
  meta.inputByteSize,
599
+ () => ({
600
+ tokenCount: toolTokenCount,
601
+ inputTokenCount: meta.inputTokenCount,
602
+ reasoningTokenCount: null,
603
+ }),
509
604
  );
510
605
 
511
606
  // For tool parts, the visible payload is the tool result text. We
@@ -596,6 +691,13 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
596
691
  }
597
692
  return any ? "truncated" : "absent";
598
693
  },
694
+ // Non-mutating reclaim predicate (Pi parity with OpenCode's canDrop).
695
+ // Pi sentinelizes BOTH halves, so unlike OpenCode there's no
696
+ // result-part requirement — a target reclaims as long as it still has
697
+ // at least one live occurrence to sentinelize.
698
+ canDrop(): boolean {
699
+ return occurrences.length > 0;
700
+ },
599
701
  message: {
600
702
  info: { id: messageId, role },
601
703
  parts: [],
@@ -43,6 +43,7 @@
43
43
  * tagging+drops layer to use Transcript instances.
44
44
  */
45
45
 
46
+ import { estimateTokens } from "../hooks/magic-context/read-session-formatting";
46
47
  import { isRecord } from "./record-type-guard";
47
48
  import type {
48
49
  Transcript,
@@ -129,7 +130,11 @@ function createOpenCodePart(
129
130
  setToolOutput(newText: string): boolean {
130
131
  return writeOpenCodeToolOutput(rawPart, newText);
131
132
  },
132
- getToolMetadata(): { toolName: string | undefined; inputByteSize: number } {
133
+ getToolMetadata(): {
134
+ toolName: string | undefined;
135
+ inputByteSize: number;
136
+ inputTokenCount: number;
137
+ } {
133
138
  return readOpenCodeToolMetadata(rawPart);
134
139
  },
135
140
  replaceWithSentinel(sentinelText: string): boolean {
@@ -227,9 +232,10 @@ function writeOpenCodeToolOutput(part: unknown, newText: string): boolean {
227
232
  function readOpenCodeToolMetadata(part: unknown): {
228
233
  toolName: string | undefined;
229
234
  inputByteSize: number;
235
+ inputTokenCount: number;
230
236
  } {
231
- if (!isRecord(part)) return { toolName: undefined, inputByteSize: 0 };
232
- if (part.type !== "tool") return { toolName: undefined, inputByteSize: 0 };
237
+ if (!isRecord(part)) return { toolName: undefined, inputByteSize: 0, inputTokenCount: 0 };
238
+ if (part.type !== "tool") return { toolName: undefined, inputByteSize: 0, inputTokenCount: 0 };
233
239
 
234
240
  // OpenCode parts use `tool` as the tool name field; some legacy
235
241
  // shapes use `toolName` or `name`. Match all three for forward
@@ -247,13 +253,17 @@ function readOpenCodeToolMetadata(part: unknown): {
247
253
  const input = state?.input ?? part.args ?? part.input;
248
254
 
249
255
  let inputByteSize = 0;
256
+ let inputTokenCount = 0;
250
257
  if (input !== undefined && input !== null) {
251
258
  try {
252
- inputByteSize = JSON.stringify(input).length;
259
+ const serialized = typeof input === "string" ? input : JSON.stringify(input);
260
+ inputByteSize = serialized.length;
261
+ inputTokenCount = serialized ? estimateTokens(serialized) : 0;
253
262
  } catch {
254
263
  inputByteSize = 0;
264
+ inputTokenCount = 0;
255
265
  }
256
266
  }
257
267
 
258
- return { toolName, inputByteSize };
268
+ return { toolName, inputByteSize, inputTokenCount };
259
269
  }
@@ -121,10 +121,17 @@ export interface TranscriptPart {
121
121
  * for non-tool parts.
122
122
  * - inputByteSize: serialized argument size; used by historian
123
123
  * pressure projection to estimate post-drop savings.
124
+ * - inputTokenCount: real-tokenizer count of the same serialized
125
+ * argument, stored on the tag so token-budget consumers SUM stored
126
+ * counts instead of re-tokenizing. 0 for non-tool parts.
124
127
  *
125
- * For non-tool parts both fields are undefined.
128
+ * For non-tool parts both byte fields are undefined/0.
126
129
  */
127
- getToolMetadata(): { toolName: string | undefined; inputByteSize: number };
130
+ getToolMetadata(): {
131
+ toolName: string | undefined;
132
+ inputByteSize: number;
133
+ inputTokenCount: number;
134
+ };
128
135
 
129
136
  /**
130
137
  * Replace this part with a sentinel placeholder. Sentinels look like
@@ -140,6 +147,17 @@ export interface TranscriptPart {
140
147
  * replaced (e.g. it's already a sentinel, or it's an image part).
141
148
  */
142
149
  replaceWithSentinel(sentinelText: string): boolean;
150
+
151
+ /**
152
+ * Optional: serialized byte size of the part's REAL payload, including
153
+ * non-text content (images, structured data) that `getText()` can't
154
+ * surface. Used by emergency-drop reclaim accounting so an image-only
155
+ * tool result is sized by its actual payload, not treated as ~0 bytes.
156
+ * Adapters that can compute this (e.g. Pi's tool_result proxy, which
157
+ * closes over the raw content array) should implement it; callers fall
158
+ * back to the text/JSON estimate when it's absent.
159
+ */
160
+ rawByteSize?(): number;
143
161
  }
144
162
 
145
163
  /**
@@ -186,7 +186,6 @@ export async function loadStatusDetail(
186
186
  activeBytes: 0,
187
187
  lastResponseTime: 0,
188
188
  lastNudgeTokens: 0,
189
- lastNudgeBand: "",
190
189
  lastTransformError: null,
191
190
  isSubagent: false,
192
191
  pendingOps: [],
@@ -197,9 +196,7 @@ export async function loadStatusDetail(
197
196
  executeThreshold: 65,
198
197
  executeThresholdMode: "percentage",
199
198
  protectedTagCount: 20,
200
- nudgeInterval: 20000,
201
199
  historyBudgetPercentage: 0.15,
202
- nextNudgeAfter: 0,
203
200
  historyBlockTokens: 0,
204
201
  compressionBudget: null,
205
202
  compressionUsage: null,
package/src/tui/index.tsx CHANGED
@@ -385,12 +385,9 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
385
385
  </box>
386
386
  {/* Right column */}
387
387
  <box flexDirection="column" flexGrow={1} flexBasis={0}>
388
- <text fg={t().text}><b>Rolling Nudges</b></text>
388
+ <text fg={t().text}><b>Reductions</b></text>
389
389
  <R t={t()} l="Execute threshold" v={`${formatThresholdPercent(s().executeThreshold)}%`} />
390
- <R t={t()} l="Nudge anchor" v={`${fmt(s().lastNudgeTokens)} tok`} />
391
- <R t={t()} l="Interval" v={`${fmt(s().nudgeInterval)} tok`} fg={t().textMuted} />
392
- <R t={t()} l="Next nudge after" v={`${fmt(s().nextNudgeAfter)} tok`} />
393
- {s().lastNudgeBand ? <R t={t()} l="Current band" v={s().lastNudgeBand} /> : null}
390
+ <R t={t()} l="Last reduce anchor" v={`${fmt(s().lastNudgeTokens)} tok`} />
394
391
  <box marginTop={1}>
395
392
  <text fg={t().text}><b>Context Details</b></text>
396
393
  </box>
@@ -1,32 +1,16 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { appendFileSync } from "node:fs"
3
- import { tmpdir } from "node:os"
4
- import { join } from "node:path"
5
2
  import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
6
3
  import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
7
4
  import packageJson from "../../../package.json"
8
5
  import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
9
6
  import { formatThresholdPercent } from "../../shared/format-threshold"
10
7
 
11
- // TEMP recomp-poll instrumentation (dogfood 2026-05-30). Writes to a dedicated
12
- // file so we can trace the client poll loop the server log can't see. Remove
13
- // once the freeze is diagnosed.
14
- const RECOMP_TRACE = join(tmpdir(), "mc-recomp-trace.log")
15
- function rtrace(msg: string): void {
16
- try {
17
- appendFileSync(RECOMP_TRACE, `[${new Date().toISOString()}] ${msg}\n`)
18
- } catch {
19
- // ignore
20
- }
21
- }
22
-
23
8
  // Module-level hook so the upgrade/recomp dialog can kick the sidebar into its
24
9
  // fast recomp self-poll the INSTANT the user confirms — without waiting for a
25
10
  // parent-session message event (the RPC upgrade/recomp call fires none). The
26
- // mounted SidebarContent registers its refresh here (dogfood 2026-05-30).
11
+ // mounted SidebarContent registers its refresh here.
27
12
  let activeRecompPollKick: (() => void) | null = null
28
13
  export function kickRecompProgressRefresh(): void {
29
- rtrace(`kickRecompProgressRefresh called; activeKick=${activeRecompPollKick ? "set" : "NULL"}`)
30
14
  activeRecompPollKick?.()
31
15
  }
32
16
 
@@ -473,9 +457,6 @@ const SidebarContent = (props: {
473
457
  void loadSidebarSnapshot(sid, directory)
474
458
  .then((data) => {
475
459
  const phase = data?.recompProgress?.phase
476
- rtrace(
477
- `poll#${recompPollCount} phase=${phase ?? "ABSENT"} passCount=${data?.recompProgress?.passCount ?? "-"} note=${data?.recompProgress?.note ?? "-"} sawPhase=${recompSawPhase} absent=${recompConsecutiveAbsent}`,
478
- )
479
460
  // While a recomp is known-active, a transient snapshot that lost
480
461
  // recompProgress (sticky cache / busy-DB empty) must NOT wipe the
481
462
  // visible bar — carry the last good progress forward so it stays
@@ -499,7 +480,6 @@ const SidebarContent = (props: {
499
480
  // Terminal state rendered — stop. The server keeps "done"/
500
481
  // "skipped" for a grace window and "failed" until the next run,
501
482
  // so the outcome stays visible without further polling.
502
- rtrace(`STOP: terminal phase=${phase}`)
503
483
  recompActive = false
504
484
  } else {
505
485
  // Phase absent this poll.
@@ -508,7 +488,6 @@ const SidebarContent = (props: {
508
488
  // Still waiting for the server's first "Starting…".
509
489
  if (recompPollCount < RECOMP_PROBE_MAX) scheduleRecompTick()
510
490
  else {
511
- rtrace("STOP: probe window exhausted, never saw phase")
512
491
  recompActive = false
513
492
  }
514
493
  } else if (recompConsecutiveAbsent < RECOMP_ABSENT_GIVEUP) {
@@ -519,7 +498,6 @@ const SidebarContent = (props: {
519
498
  scheduleRecompTick()
520
499
  } else {
521
500
  // Long continuous absence — the entry is genuinely gone.
522
- rtrace("STOP: absent giveup")
523
501
  recompActive = false
524
502
  }
525
503
  }
@@ -527,7 +505,6 @@ const SidebarContent = (props: {
527
505
  .catch((err) => {
528
506
  // CRITICAL: a failed/slow fetch must NOT kill the loop — keep
529
507
  // polling while active so we still catch the terminal state.
530
- rtrace(`poll#${recompPollCount} FETCH ERROR: ${String(err)}`)
531
508
  scheduleRecompTick()
532
509
  })
533
510
  }
@@ -536,7 +513,6 @@ const SidebarContent = (props: {
536
513
  // first detects an active recomp). The server emits an immediate "Starting…"
537
514
  // entry; the probe window covers the brief RPC race before it lands.
538
515
  function kickRecompPoll(): void {
539
- rtrace(`kickRecompPoll: recompActive=${recompActive} (${recompActive ? "SKIP" : "starting"})`)
540
516
  if (recompActive) return // already running
541
517
  recompActive = true
542
518
  recompSawPhase = false
@@ -546,7 +522,6 @@ const SidebarContent = (props: {
546
522
  }
547
523
 
548
524
  activeRecompPollKick = kickRecompPoll
549
- rtrace("SidebarContent mounted; registered activeRecompPollKick")
550
525
 
551
526
  onCleanup(() => {
552
527
  if (refreshTimer) clearTimeout(refreshTimer)
@@ -1,5 +0,0 @@
1
- import type { NudgePlacementStore } from "./nudge-placement-store";
2
- import type { ContextNudge } from "./nudger";
3
- import type { MessageLike } from "./tag-messages";
4
- export declare function applyContextNudge(messages: MessageLike[], nudge: ContextNudge | null, nudgePlacements: NudgePlacementStore, sessionId: string): void;
5
- //# sourceMappingURL=apply-context-nudge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"apply-context-nudge.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/apply-context-nudge.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,eAAe,EAAE,mBAAmB,EACpC,SAAS,EAAE,MAAM,GAClB,IAAI,CAgCN"}
@@ -1,6 +0,0 @@
1
- export type RollingNudgeBand = "far" | "near" | "urgent" | "critical";
2
- export declare function getRollingNudgeBand(percentage: number, executeThresholdPercentage: number): RollingNudgeBand;
3
- export declare function getRollingNudgeBandPriority(band: RollingNudgeBand | null): number;
4
- export declare function formatRollingNudgeBand(band: RollingNudgeBand | null): string;
5
- export declare function getRollingNudgeIntervalTokens(baseIntervalTokens: number, band: RollingNudgeBand): number;
6
- //# sourceMappingURL=nudge-bands.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nudge-bands.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/nudge-bands.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEtE,wBAAgB,mBAAmB,CAC/B,UAAU,EAAE,MAAM,EAClB,0BAA0B,EAAE,MAAM,GACnC,gBAAgB,CAWlB;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAajF;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAE5E;AAED,wBAAgB,6BAA6B,CACzC,kBAAkB,EAAE,MAAM,EAC1B,IAAI,EAAE,gBAAgB,GACvB,MAAM,CAWR"}
@@ -1,7 +0,0 @@
1
- import type { NudgePlacementStore } from "./nudge-placement-store";
2
- import type { MessageLike } from "./tag-messages";
3
- export declare function reinjectNudgeAtAnchor(messages: MessageLike[], nudgeText: string, nudgePlacements: NudgePlacementStore, sessionId: string): boolean;
4
- export declare function appendNudgeToAssistant(messages: MessageLike[], nudge: string, nudgePlacements: NudgePlacementStore, sessionId: string): void;
5
- export declare function appendSupplementalNudgeToAssistant(messages: MessageLike[], nudge: string, nudgePlacements: NudgePlacementStore, sessionId: string): boolean;
6
- export declare function canAppendSupplementalNudgeToAssistant(messages: MessageLike[], nudgePlacements: NudgePlacementStore, sessionId: string): boolean;
7
- //# sourceMappingURL=nudge-injection.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nudge-injection.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/nudge-injection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA6ElD,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,WAAW,EAAE,EACvB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,mBAAmB,EACpC,SAAS,EAAE,MAAM,GAClB,OAAO,CA+CT;AAED,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,mBAAmB,EACpC,SAAS,EAAE,MAAM,GAClB,IAAI,CAmDN;AAED,wBAAgB,kCAAkC,CAC9C,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,mBAAmB,EACpC,SAAS,EAAE,MAAM,GAClB,OAAO,CA4BT;AAED,wBAAgB,qCAAqC,CACjD,QAAQ,EAAE,WAAW,EAAE,EACvB,eAAe,EAAE,mBAAmB,EACpC,SAAS,EAAE,MAAM,GAClB,OAAO,CAMT"}
@@ -1,15 +0,0 @@
1
- import type { Database } from "../../shared/sqlite";
2
- interface NudgePlacement {
3
- messageId: string;
4
- nudgeText: string;
5
- }
6
- export interface NudgePlacementStore {
7
- set(sessionId: string, messageId: string, nudgeText: string): void;
8
- get(sessionId: string): NudgePlacement | null;
9
- clear(sessionId: string, options?: {
10
- persist?: boolean;
11
- }): void;
12
- }
13
- export declare function createNudgePlacementStore(db?: Database): NudgePlacementStore;
14
- export {};
15
- //# sourceMappingURL=nudge-placement-store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nudge-placement-store.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/nudge-placement-store.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,UAAU,cAAc;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAChC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACnE,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAAC;IAC9C,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACnE;AAED,wBAAgB,yBAAyB,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,mBAAmB,CAgC5E"}
@@ -1,21 +0,0 @@
1
- import { getOrCreateSessionMeta, type getTopNBySize } from "../../features/magic-context/storage";
2
- import type { ContextUsage, SessionMeta, TagEntry } from "../../features/magic-context/types";
3
- type ContextDatabase = Parameters<typeof getOrCreateSessionMeta>[0];
4
- export type ContextNudge = {
5
- type: "assistant";
6
- text: string;
7
- };
8
- export declare const RECENT_CTX_REDUCE_WINDOW_MS: number;
9
- export declare function createNudger(config: {
10
- protected_tags: number;
11
- nudge_interval_tokens: number;
12
- iteration_nudge_threshold: number;
13
- execute_threshold_percentage: number | {
14
- default: number;
15
- [modelKey: string]: number;
16
- };
17
- now?: () => number;
18
- recentReduceBySession?: Map<string, number>;
19
- }): (sessionId: string, contextUsage: ContextUsage, db: ContextDatabase, topNFn: typeof getTopNBySize, preloadedTags?: TagEntry[], messagesSinceLastUser?: number, preloadedSessionMeta?: SessionMeta) => ContextNudge | null;
20
- export {};
21
- //# sourceMappingURL=nudger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nudger.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/nudger.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,sBAAsB,EAEtB,KAAK,aAAa,EAErB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAY9F,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;AACpE,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAC/D,eAAO,MAAM,2BAA2B,QAAgB,CAAC;AA+BzD,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,yBAAyB,EAAE,MAAM,CAAC;IAClC,4BAA4B,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/C,IAIO,WAAW,MAAM,EACjB,cAAc,YAAY,EAC1B,IAAI,eAAe,EACnB,QAAQ,OAAO,aAAa,EAC5B,gBAAgB,QAAQ,EAAE,EAC1B,wBAAwB,MAAM,EAC9B,uBAAuB,WAAW,KACnC,YAAY,GAAG,IAAI,CA2IzB"}