@cortexkit/opencode-magic-context 0.49.6 → 0.49.7

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 (153) hide show
  1. package/dist/auto-search-worker.js +10 -7
  2. package/dist/{compaction-marker-candidates-cz72pm1t.js → compaction-marker-candidates-a0edbqrr.js} +2 -2
  3. package/dist/compaction-marker-candidates-worker.js +2 -2
  4. package/dist/{conflict-warning-hook-1jq1rxx7.js → conflict-warning-hook-myk5hbt9.js} +5 -5
  5. package/dist/{context-ae3gxdye.js → context-ffr7e0g7.js} +46 -34
  6. package/dist/{embedding-backfill-dtjmyktb.js → embedding-backfill-re16xncd.js} +6 -6
  7. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts +6 -3
  9. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -1
  11. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/map-memories.d.ts +2 -2
  13. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/primer-seed.d.ts +2 -0
  15. package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -2
  17. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -3
  19. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  21. package/dist/features/magic-context/storage-replay-document.d.ts +4 -0
  22. package/dist/features/magic-context/storage-replay-document.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/apply-operations.d.ts +7 -0
  24. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  25. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/note-nudger.d.ts +5 -3
  29. package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/read-session-chunk.d.ts +7 -0
  32. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/replay-committed-drops.d.ts +9 -0
  34. package/dist/hooks/magic-context/replay-committed-drops.d.ts.map +1 -0
  35. package/dist/hooks/magic-context/rust-lkg-freeze-registry.d.ts +2 -2
  36. package/dist/hooks/magic-context/rust-lkg-freeze-registry.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  38. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/rust-served-raw.d.ts +29 -0
  40. package/dist/hooks/magic-context/rust-served-raw.d.ts.map +1 -0
  41. package/dist/hooks/magic-context/strip-content.d.ts +23 -24
  42. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
  44. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
  46. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  48. package/dist/{index-6q5h70h2.js → index-0gyq3djm.js} +1 -1
  49. package/dist/{index-vnjhmkwk.js → index-37q80veb.js} +1 -1
  50. package/dist/{index-besxhcsh.js → index-4btknbqh.js} +9 -8
  51. package/dist/{index-vh3e2mvr.js → index-5tbvdff4.js} +59 -28
  52. package/dist/{index-cm4hmxyk.js → index-9yyqq3mm.js} +1 -1
  53. package/dist/{index-ab3bgm2m.js → index-a8acx4gc.js} +2 -2
  54. package/dist/{index-j55vn694.js → index-ax2jhztn.js} +1 -1
  55. package/dist/{index-zgzdm6dg.js → index-ebnngykq.js} +145 -94
  56. package/dist/{index-g9exahnx.js → index-g8qbdb03.js} +1 -1
  57. package/dist/{index-ch08qwgd.js → index-j58gqrn2.js} +65 -65
  58. package/dist/{index-xqyj5c85.js → index-j5hce1ab.js} +2 -2
  59. package/dist/{index-0nnd4bry.js → index-j8epsc0g.js} +34 -13
  60. package/dist/{index-vztbgcdk.js → index-n5a170gs.js} +227 -38
  61. package/dist/{index-s0gsmthp.js → index-n7kwfw2r.js} +4 -4
  62. package/dist/{index-ck6y3td2.js → index-rwmpkss5.js} +3 -3
  63. package/dist/{index-bg5qqene.js → index-rzmjkdd6.js} +1 -1
  64. package/dist/{index-9s58tq95.js → index-sf94at64.js} +3 -3
  65. package/dist/{index-f3dnvnw6.js → index-sh91t19a.js} +1 -1
  66. package/dist/{index-mj1fgfrk.js → index-tagss15p.js} +857 -125
  67. package/dist/{index-1j8jcjpa.js → index-tmtty2x5.js} +1 -1
  68. package/dist/{index-hr64sth2.js → index-vt4sf4ep.js} +1 -1
  69. package/dist/{index-9m7bn21d.js → index-w4mw92w3.js} +1 -1
  70. package/dist/{index-7q1m1qjk.js → index-w6bd03dc.js} +1 -1
  71. package/dist/{index-a796ct3s.js → index-w6vr754t.js} +4 -4
  72. package/dist/{index-1xaw2pq8.js → index-wq16ahsn.js} +5 -5
  73. package/dist/index.js +36 -36
  74. package/dist/{message-index-bounded-630j53ps.js → message-index-bounded-04rycpx4.js} +9 -9
  75. package/dist/{message-index-c84wm2g0.js → message-index-rqtse28y.js} +9 -9
  76. package/dist/{message-time-backfill-8kj57qe3.js → message-time-backfill-a5ytnp53.js} +1 -1
  77. package/dist/migration-worker.js +6 -6
  78. package/dist/plugin/messages-transform.d.ts.map +1 -1
  79. package/dist/{project-embedding-registry-vv59t7ah.js → project-embedding-registry-k0sd9k7v.js} +5 -5
  80. package/dist/raw-ordinal-warmup-worker.js +17 -15
  81. package/dist/{read-session-chunk-t3vjd0h0.js → read-session-chunk-h1btx938.js} +7 -5
  82. package/dist/{read-session-raw-gf7eytnm.js → read-session-raw-vdf4wh78.js} +2 -2
  83. package/dist/{recomp-orchestrator-1d08g57a.js → recomp-orchestrator-trcgmm23.js} +20 -20
  84. package/dist/{search-kth2p6cc.js → search-3rbdx6hw.js} +16 -16
  85. package/dist/{send-session-notification-3j6krd4w.js → send-session-notification-2tp44tfa.js} +4 -4
  86. package/dist/shared/processed-image-eligibility.d.ts +22 -0
  87. package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
  88. package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
  89. package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
  90. package/dist/shared/sqlite.d.ts +1 -1
  91. package/dist/shared/sqlite.d.ts.map +1 -1
  92. package/dist/shared/tag-transcript.d.ts +9 -0
  93. package/dist/shared/tag-transcript.d.ts.map +1 -1
  94. package/dist/{sqlite-d7edbk1s.js → sqlite-j3tvg8r0.js} +1 -1
  95. package/dist/{storage-memory-af5zdz98.js → storage-memory-8168bgef.js} +2 -2
  96. package/dist/{storage-tags-dyj7mmbq.js → storage-tags-61z5j2f9.js} +2 -2
  97. package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
  98. package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
  99. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  100. package/dist/{transform-postprocess-phase-bv3kb5s1.js → transform-postprocess-phase-7qts7h54.js} +20 -20
  101. package/dist/v2/auto-search-worker.js +10 -7
  102. package/dist/v2/{compaction-marker-candidates-6gqp5wrc.js → compaction-marker-candidates-6nypjkpf.js} +2 -2
  103. package/dist/v2/compaction-marker-candidates-worker.js +2 -2
  104. package/dist/v2/{context-p8f2shp1.js → context-xq4rtywr.js} +1019 -233
  105. package/dist/v2/{embedding-backfill-cg4rv90f.js → embedding-backfill-xd9ygzgd.js} +6 -6
  106. package/dist/v2/hooks/payload.d.ts.map +1 -1
  107. package/dist/v2/{message-index-3ws17x1g.js → message-index-8e5z2xx8.js} +9 -9
  108. package/dist/v2/{message-index-bounded-yzxmf6vp.js → message-index-bounded-5vh1h2g7.js} +9 -9
  109. package/dist/v2/{message-time-backfill-5nf10c65.js → message-time-backfill-2z365239.js} +1 -1
  110. package/dist/v2/migration-worker.js +6 -6
  111. package/dist/v2/{project-embedding-registry-rsnf2263.js → project-embedding-registry-b77awytr.js} +5 -5
  112. package/dist/v2/raw-ordinal-warmup-worker.js +17 -15
  113. package/dist/v2/{read-session-chunk-vtb0w2kd.js → read-session-chunk-4sat83cx.js} +7 -5
  114. package/dist/v2/{read-session-raw-gpcddemt.js → read-session-raw-a3hw9jsp.js} +2 -2
  115. package/dist/v2/{recomp-orchestrator-v64r4s4r.js → recomp-orchestrator-n11zv1f1.js} +19 -19
  116. package/dist/v2/{search-bpa4fxq1.js → search-mvgqtkkz.js} +16 -16
  117. package/dist/v2/{send-session-notification-5aj2j7vf.js → send-session-notification-ppgc8qdz.js} +4 -4
  118. package/dist/v2/{server-wrem2nmz.js → server-1rfgbjfn.js} +1 -1
  119. package/dist/v2/{server-qn23p4h7.js → server-7srtwsa4.js} +5 -5
  120. package/dist/v2/{server-cm4hmxyk.js → server-9yyqq3mm.js} +1 -1
  121. package/dist/v2/{server-yezz0cp2.js → server-brhj8qtd.js} +1 -1
  122. package/dist/v2/{server-387vkhjq.js → server-dd0nja67.js} +1 -1
  123. package/dist/v2/{server-keswwkvv.js → server-fpqe39as.js} +1 -1
  124. package/dist/v2/{server-158ehmk2.js → server-fw31krgq.js} +3 -3
  125. package/dist/v2/{server-fby0cxbq.js → server-gtv9wbhq.js} +227 -38
  126. package/dist/v2/{server-ww9nr8pe.js → server-hcvcxzx7.js} +2 -2
  127. package/dist/v2/{server-0nnd4bry.js → server-j8epsc0g.js} +34 -13
  128. package/dist/v2/{server-x2r63c0x.js → server-jdwd03ep.js} +4 -4
  129. package/dist/v2/{server-btgnsv7w.js → server-kdrezdy0.js} +1 -1
  130. package/dist/v2/{server-7skbfb81.js → server-mv1w7t6z.js} +61 -30
  131. package/dist/v2/{server-aa9w15cs.js → server-nfan6zb6.js} +65 -65
  132. package/dist/v2/{server-9zqewm6g.js → server-qcw43339.js} +1 -1
  133. package/dist/v2/{server-f9py7s7a.js → server-sy4b1sra.js} +1 -1
  134. package/dist/v2/{server-09g9pkvx.js → server-v4fwha43.js} +9 -8
  135. package/dist/v2/{server-yd01snxq.js → server-w3zeywp1.js} +1 -1
  136. package/dist/v2/{server-4tn95znb.js → server-wkdgjjch.js} +1 -1
  137. package/dist/v2/{server-6a9mxnb2.js → server-x08d1xf3.js} +1 -1
  138. package/dist/v2/server.js +4 -4
  139. package/dist/v2/{sqlite-d7edbk1s.js → sqlite-j3tvg8r0.js} +1 -1
  140. package/dist/v2/{storage-memory-af5zdz98.js → storage-memory-8168bgef.js} +2 -2
  141. package/dist/v2/{storage-tags-d47bpaqa.js → storage-tags-j2fzq3sw.js} +2 -2
  142. package/dist/v2/{wrapup-orchestrator-cahk37n7.js → wrapup-orchestrator-s92ebfzv.js} +19 -19
  143. package/dist/{wrapup-orchestrator-mqp0v195.js → wrapup-orchestrator-df61jmz9.js} +21 -21
  144. package/package.json +3 -3
  145. package/src/shared/processed-image-eligibility.test.ts +68 -0
  146. package/src/shared/processed-image-eligibility.ts +41 -0
  147. package/src/shared/processed-image-position-review.json +20 -0
  148. package/src/shared/processed-image-position-review.test.ts +19 -0
  149. package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
  150. package/src/shared/recovery-reasoning-prefix.ts +35 -0
  151. package/src/shared/sqlite-writer-diagnostics.test.ts +31 -0
  152. package/src/shared/sqlite.ts +63 -22
  153. package/src/shared/tag-transcript.ts +118 -0
@@ -72,14 +72,21 @@ export function registerSlowWriteReporter(reporter: SlowWriteReporter): void {
72
72
  */
73
73
  type SqliteDiagnosticSink = (message: string) => void;
74
74
  let sqliteDiagnosticSink: SqliteDiagnosticSink | undefined;
75
+ let sqliteDiagnosticHost: () => string = () => "unknown";
75
76
 
76
- export function registerSqliteDiagnosticSink(sink: SqliteDiagnosticSink): void {
77
+ export function registerSqliteDiagnosticSink(
78
+ sink: SqliteDiagnosticSink,
79
+ host?: () => string,
80
+ ): void {
77
81
  sqliteDiagnosticSink = sink;
82
+ if (host) sqliteDiagnosticHost = host;
78
83
  }
79
84
 
80
85
  function reportSqliteDiagnostic(message: string): void {
81
86
  try {
82
- sqliteDiagnosticSink?.(`[magic-context] ${message}`);
87
+ sqliteDiagnosticSink?.(
88
+ `[magic-context] ${message} pid=${process.pid} host=${sqliteDiagnosticHost()}`,
89
+ );
83
90
  } catch {
84
91
  // Diagnostics must never change the outcome of the write they describe.
85
92
  }
@@ -393,9 +400,18 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
393
400
  // A transaction that ended without passing through exec (for
394
401
  // example an automatic rollback) must not be timed as this one.
395
402
  openWriterTransactions.delete(db);
396
- if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore())
403
+ if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore()) {
397
404
  acquireShort(db, () => nativeExec(sql), sql.trim());
398
- else nativeExec(sql);
405
+ } else {
406
+ nativeExec(sql);
407
+ openWriterTransactions.set(db, {
408
+ site: sql.trim(),
409
+ lane: "background",
410
+ acquireMs: 0,
411
+ attempts: 1,
412
+ acquiredAt: performance.now(),
413
+ });
414
+ }
399
415
  return db;
400
416
  }
401
417
  if (WRITER_TRANSACTION_END.test(sql) && openWriterTransactions.has(db))
@@ -943,7 +959,7 @@ const SHORT_BUSY_TIMEOUT_MS = 25;
943
959
  const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 2_000;
944
960
  const FOREGROUND_ACQUISITION_BUDGET_MS = 16_500;
945
961
  /** Waiting this long for the writer lock, or holding it this long, is logged. */
946
- const SLOW_WRITER_LOG_MS = 250;
962
+ const SLOW_WRITER_LOG_MS = 100;
947
963
  const WRITER_TRANSACTION_END = /^\s*(?:COMMIT|END|ROLLBACK)(?:\s+TRANSACTION)?\s*;?\s*$/i;
948
964
 
949
965
  type WriterLane = "foreground" | "background";
@@ -991,25 +1007,33 @@ function foregroundWaitLease() {
991
1007
  function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
992
1008
  assertTransformWrite();
993
1009
  const lease = foregroundWaitLease();
994
- if (isInTransaction(db) || (!lease && !backgroundWriterScope.getStore())) return operation();
995
- const previous = pragmaNumber(db, "busy_timeout");
996
- db.exec(
997
- `PRAGMA busy_timeout=${lease ? Math.floor(Math.min(lease.remainingWaitMs, lease.guard?.remainingMs() ?? Infinity)) : SHORT_BUSY_TIMEOUT_MS}`,
998
- );
1010
+ if (isInTransaction(db)) return operation();
1011
+ const scoped = lease || backgroundWriterScope.getStore();
1012
+ const previous = scoped ? pragmaNumber(db, "busy_timeout") : null;
1013
+ if (scoped)
1014
+ db.exec(
1015
+ `PRAGMA busy_timeout=${lease ? Math.floor(Math.min(lease.remainingWaitMs, lease.guard?.remainingMs() ?? Infinity)) : SHORT_BUSY_TIMEOUT_MS}`,
1016
+ );
999
1017
  const started = performance.now();
1018
+ let completed = false;
1000
1019
  try {
1001
- return operation();
1020
+ const result = operation();
1021
+ completed = true;
1022
+ return result;
1002
1023
  } catch (error) {
1003
1024
  if (lease && isTransientSqliteError(error))
1004
1025
  throw new SqliteAcquisitionBusyError(error, "autocommit");
1005
1026
  throw error;
1006
1027
  } finally {
1007
- if (lease)
1008
- lease.remainingWaitMs = Math.max(
1009
- 0,
1010
- lease.remainingWaitMs - (performance.now() - started),
1011
- );
1028
+ const elapsed = performance.now() - started;
1029
+ if (lease) lease.remainingWaitMs = Math.max(0, lease.remainingWaitMs - elapsed);
1012
1030
  if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
1031
+ // Native autocommit does not expose the instant its lock was acquired.
1032
+ // Label the upper bound so a waiting statement is not blamed as a holder.
1033
+ if (elapsed >= SLOW_WRITER_LOG_MS)
1034
+ reportSqliteDiagnostic(
1035
+ `sqlite writer site=autocommit lane=${lease ? "foreground" : "background"} elapsed_ms=${Math.round(elapsed)} timing=statement_including_acquisition outcome=${completed ? "completed" : "failed"} caller=${writerCaller()}`,
1036
+ );
1013
1037
  }
1014
1038
  }
1015
1039
 
@@ -1071,20 +1095,18 @@ function acquireShort(db: Database, acquire: () => unknown, site: string): void
1071
1095
  * `acquireShort`, then report how long the writer waited for the lock
1072
1096
  * (`acquire_ms`) and how long it then held it (`hold_ms`) when either crosses
1073
1097
  * the threshold. A long hold blocks every other process's writer, so it is
1074
- * worth seeing even when the lock itself came quickly. The native end statement's
1075
- * `end_ms` is reported separately, so a slow FTS merge or checkpoint at COMMIT
1076
- * is not mistaken for acquisition time.
1098
+ * worth seeing even when the lock itself came quickly. This interval includes
1099
+ * COMMIT: an automatic PASSIVE checkpoint can run after writer-lock release,
1100
+ * so hold_ms is a conservative upper bound, not proof of continuous ownership.
1077
1101
  */
1078
1102
  function endWriterTransaction(db: Database, end: () => unknown, sql: string): unknown {
1079
1103
  let ended = false;
1080
- const endStarted = performance.now();
1081
1104
  try {
1082
1105
  const result = end();
1083
1106
  ended = true;
1084
1107
  return result;
1085
1108
  } finally {
1086
1109
  const open = openWriterTransactions.get(db);
1087
- const endMs = performance.now() - endStarted;
1088
1110
  // A COMMIT that failed with the transaction still open has not ended it.
1089
1111
  if (open && !isInTransaction(db)) {
1090
1112
  openWriterTransactions.delete(db);
@@ -1096,13 +1118,32 @@ function endWriterTransaction(db: Database, end: () => unknown, sql: string): un
1096
1118
  ? "committed"
1097
1119
  : "failed";
1098
1120
  reportSqliteDiagnostic(
1099
- `sqlite writer site=${open.site} lane=${open.lane} acquire_ms=${Math.round(open.acquireMs)} hold_ms=${Math.round(holdMs)} end_ms=${Math.round(endMs)} attempts=${open.attempts} outcome=${outcome}`,
1121
+ `sqlite writer site=${open.site} lane=${open.lane} acquire_ms=${Math.round(open.acquireMs)} hold_ms=${Math.round(holdMs)} attempts=${open.attempts} outcome=${outcome} timing=through_commit_upper_bound caller=${writerCaller()}`,
1100
1122
  );
1101
1123
  }
1102
1124
  }
1103
1125
  }
1104
1126
  }
1105
1127
 
1128
+ // Capture a stack only for slow writes. Synchronous COMMIT retains the writer's
1129
+ // caller, so fast transactions need neither stack allocation nor source mapping.
1130
+ function writerCaller(): string {
1131
+ return (
1132
+ new Error().stack
1133
+ ?.split("\n")
1134
+ .find(
1135
+ (line) =>
1136
+ line.includes(" at ") &&
1137
+ !/[/\\]sqlite\.[tj]s[:)]/.test(line) &&
1138
+ !/\b(?:writerCaller|endWriterTransaction|withAutocommitTimeout|value)\b/.test(
1139
+ line,
1140
+ ),
1141
+ )
1142
+ ?.trim()
1143
+ .replace(/\s+/g, "_") ?? "unknown"
1144
+ );
1145
+ }
1146
+
1106
1147
  /** Begin an immediate transaction with bounded asynchronous retries on writer contention.
1107
1148
  * Callers must recheck any lease or source snapshot after this function returns.
1108
1149
  * Resolves with the `performance.now()` time at which BEGIN IMMEDIATE succeeded,
@@ -55,6 +55,7 @@ import {
55
55
  updateTagTokenCount,
56
56
  } from "../features/magic-context/storage-tags";
57
57
  import { makeToolCompositeKey, type Tagger } from "../features/magic-context/tagger";
58
+ import type { TagEntry } from "../features/magic-context/types";
58
59
  import { droppedInputMarker } from "../hooks/magic-context/dropped-input-guard";
59
60
  import { applyEditMarkerToInput } from "../hooks/magic-context/edit-marker";
60
61
  import { estimateImageTokensFromDataUrl } from "../hooks/magic-context/image-token-estimate";
@@ -121,6 +122,122 @@ export interface TagTranscriptResult {
121
122
  targets: Map<number, TagTarget>;
122
123
  }
123
124
 
125
+ /**
126
+ * Bind durable drops to a saved request without tagging it again. Recovery must
127
+ * not allocate identities, inject prefixes, or write accounting while the store
128
+ * is failing. Reuse the normal targets so persisted modes render identically.
129
+ */
130
+ export function buildPersistedDropTargets(
131
+ transcript: Transcript,
132
+ tags: readonly TagEntry[],
133
+ options: { allowReasoningPrefixRewrite?: boolean } = {},
134
+ ): Map<number, TagTarget> {
135
+ const targets = new Map<number, TagTarget>();
136
+ const tools = new Map(
137
+ tags
138
+ .filter((tag) => tag.type === "tool")
139
+ .map((tag) => [makeToolCompositeKey(tag.toolOwnerMessageId ?? "", tag.messageId), tag]),
140
+ );
141
+ const textTags = new Map(
142
+ tags.filter((tag) => tag.type !== "tool").map((tag) => [tag.messageId, tag]),
143
+ );
144
+ const byNumber = new Map(tags.map((tag) => [tag.tagNumber, tag]));
145
+ const aggregates = new Map<
146
+ string,
147
+ { tag?: TagEntry; occurrences: ToolOccurrence[]; reasoning: boolean }
148
+ >();
149
+ const pending = new Map<string, string[]>();
150
+ const resolved = new Map<string, string>();
151
+ const ambiguousCalls = new Set<string>();
152
+ const end = new ConversationEndGuard(transcript.messages);
153
+ for (const message of transcript.messages) {
154
+ let ordinal = 0;
155
+ let resultRun: { callId: string; key: string } | undefined;
156
+ const parts = message.parts;
157
+ const reasoning =
158
+ message.requiresToolArcSkeleton ??
159
+ (message.info.role === "assistant" && parts.some((part) => part.kind === "thinking"));
160
+ for (const part of parts) {
161
+ if (part.kind !== "tool_result") resultRun = undefined;
162
+ if (part.kind === "text") {
163
+ const prefix = /^§(\d+)§\s/.exec(part.getText() ?? "");
164
+ const tag =
165
+ (prefix ? byNumber.get(Number(prefix[1])) : undefined) ??
166
+ textTags.get(`${message.info.id}:p${ordinal}`);
167
+ ordinal++;
168
+ if (tag && tag.type !== "tool")
169
+ targets.set(tag.tagNumber, buildTextTarget(part, message, ""));
170
+ } else if ((part.kind === "tool_use" || part.kind === "tool_result") && part.id) {
171
+ const open = pending.get(part.id) ?? [];
172
+ if (open.length > 1) ambiguousCalls.add(part.id);
173
+ const key =
174
+ (part.kind === "tool_result"
175
+ ? resultRun?.callId === part.id
176
+ ? resultRun.key
177
+ : (open.at(-1) ?? resolved.get(part.id))
178
+ : undefined) ?? makeToolCompositeKey(message.info.id ?? "", part.id);
179
+ if (part.kind === "tool_use") open.push(key);
180
+ else {
181
+ const position = open.lastIndexOf(key);
182
+ if (position >= 0) open.splice(position, 1);
183
+ resultRun = { callId: part.id, key };
184
+ resolved.set(part.id, key);
185
+ }
186
+ pending.set(part.id, open);
187
+ let aggregate = aggregates.get(key);
188
+ if (!aggregate) {
189
+ aggregate = {
190
+ tag: tools.get(key),
191
+ occurrences: [],
192
+ reasoning: false,
193
+ };
194
+ aggregates.set(key, aggregate);
195
+ }
196
+ aggregate.occurrences.push({ message, part, kind: part.kind });
197
+ aggregate.reasoning ||= reasoning;
198
+ }
199
+ }
200
+ }
201
+ const arcsByCall = new Map<string, number>();
202
+ for (const { occurrences } of aggregates.values()) {
203
+ const callId = occurrences[0]?.part.id ?? "";
204
+ arcsByCall.set(callId, (arcsByCall.get(callId) ?? 0) + 1);
205
+ }
206
+ for (const aggregate of aggregates.values()) {
207
+ const { occurrences, reasoning } = aggregate;
208
+ const callId = occurrences[0]?.part.id ?? "";
209
+ if (ambiguousCalls.has(callId)) continue;
210
+ const tag =
211
+ aggregate.tag ??
212
+ (arcsByCall.get(callId) === 1
213
+ ? tools.get(makeToolCompositeKey("", callId))
214
+ : undefined);
215
+ if (tag)
216
+ targets.set(
217
+ tag.tagNumber,
218
+ buildAggregateTarget(tag.tagNumber, occurrences, reasoning, end),
219
+ );
220
+ }
221
+ if (!options.allowReasoningPrefixRewrite) {
222
+ // This target builder cannot persist removal of thinking. By default,
223
+ // withhold edits before reasoning rather than invalidate its signature.
224
+ // The Pi recovery handler opts in only to test edits on a saved copy;
225
+ // it must compare final request prefixes and refuse unsafe changes.
226
+ let lastThinking = -1;
227
+ transcript.messages.forEach((message, index) => {
228
+ if (message.parts.some((part) => part.kind === "thinking")) lastThinking = index;
229
+ });
230
+ const protectedIds = new Set(
231
+ transcript.messages.slice(0, lastThinking + 1).map((message) => message.info.id),
232
+ );
233
+ for (const [number, target] of targets) {
234
+ if (target.mutationParts?.some(({ message }) => protectedIds.has(message.info.id)))
235
+ targets.delete(number);
236
+ }
237
+ }
238
+ return targets;
239
+ }
240
+
124
241
  /**
125
242
  * Tag eligible parts of a transcript and build TagTargets for them.
126
243
  *
@@ -1289,6 +1406,7 @@ function buildAggregateTarget(
1289
1406
  return any ? "truncated" : "absent";
1290
1407
  },
1291
1408
  // Open invocations still belong to the current turn; only complete arcs reclaim.
1409
+ prepareDrop: () => !occurrences.some((occ) => occ.part.canRemove?.() === "defer"),
1292
1410
  canDrop: () => complete() && !occurrences.some((occ) => occ.part.hasUserAnswer?.()),
1293
1411
  requiresToolArcSkeleton,
1294
1412
  // Non-mutating read of the invocation input (the tool_use occurrence