@cortexkit/opencode-magic-context 0.46.1 → 0.47.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 (203) hide show
  1. package/dist/auto-search-worker.js +115 -0
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/live-run-config.d.ts.map +1 -1
  4. package/dist/config/migrate-config-location.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +11 -3
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/{conflict-warning-hook-7fj2b9jw.js → conflict-warning-hook-z44ynfz7.js} +5 -4
  8. package/dist/{context-sh2m2vde.js → context-wq4ffwyb.js} +108 -23
  9. package/dist/{derive-budgets-7nj7r1rv.js → derive-budgets-ke12jnnh.js} +1 -1
  10. package/dist/embedding-worker.js +2 -2
  11. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  12. package/dist/features/magic-context/overflow-detection.d.ts +2 -0
  13. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  14. package/dist/features/magic-context/project-embedding-registry.d.ts +7 -0
  15. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  16. package/dist/features/magic-context/search.d.ts +2 -0
  17. package/dist/features/magic-context/search.d.ts.map +1 -1
  18. package/dist/features/magic-context/session-project-storage.d.ts +8 -2
  19. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-historian-runs.d.ts +2 -5
  21. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-meta-persisted.d.ts +12 -0
  23. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  24. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  25. package/dist/features/magic-context/storage-tags.d.ts +7 -2
  26. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  27. package/dist/features/magic-context/storage.d.ts +1 -0
  28. package/dist/features/magic-context/storage.d.ts.map +1 -1
  29. package/dist/features/magic-context/transform-decision-log.d.ts +3 -1
  30. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +8 -0
  32. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -0
  33. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/auto-search-blocking.fixture.d.ts +2 -0
  35. package/dist/hooks/magic-context/auto-search-blocking.fixture.d.ts.map +1 -0
  36. package/dist/hooks/magic-context/auto-search-deadline.d.ts +7 -0
  37. package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/auto-search-host-review.fixture.d.ts +3 -0
  39. package/dist/hooks/magic-context/auto-search-host-review.fixture.d.ts.map +1 -0
  40. package/dist/hooks/magic-context/auto-search-lock-review.fixture.d.ts +2 -0
  41. package/dist/hooks/magic-context/auto-search-lock-review.fixture.d.ts.map +1 -0
  42. package/dist/hooks/magic-context/auto-search-long-lock-review.fixture.d.ts +2 -0
  43. package/dist/hooks/magic-context/auto-search-long-lock-review.fixture.d.ts.map +1 -0
  44. package/dist/hooks/magic-context/auto-search-omp-host-review.fixture.d.ts +4 -0
  45. package/dist/hooks/magic-context/auto-search-omp-host-review.fixture.d.ts.map +1 -0
  46. package/dist/hooks/magic-context/auto-search-omp-loader.fixture.d.ts +3 -0
  47. package/dist/hooks/magic-context/auto-search-omp-loader.fixture.d.ts.map +1 -0
  48. package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
  49. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/auto-search-snapshot.fixture.d.ts +4 -0
  51. package/dist/hooks/magic-context/auto-search-snapshot.fixture.d.ts.map +1 -0
  52. package/dist/hooks/magic-context/auto-search-worker-client.d.ts +69 -0
  53. package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -0
  54. package/dist/hooks/magic-context/auto-search-worker.d.ts +2 -0
  55. package/dist/hooks/magic-context/auto-search-worker.d.ts.map +1 -0
  56. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/compartment-runner-types.d.ts +3 -5
  58. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/compartment-runner.d.ts +2 -1
  60. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-trigger.d.ts +3 -1
  62. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  64. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  68. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/historian-drain-gate.d.ts +11 -0
  71. package/dist/hooks/magic-context/historian-drain-gate.d.ts.map +1 -0
  72. package/dist/hooks/magic-context/hook.d.ts +2 -1
  73. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/latest-assistant-turn.d.ts +25 -0
  75. package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -0
  76. package/dist/hooks/magic-context/latest-thinking-recovery.d.ts +61 -0
  77. package/dist/hooks/magic-context/latest-thinking-recovery.d.ts.map +1 -0
  78. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/opencode-proven-floor.d.ts +6 -0
  80. package/dist/hooks/magic-context/opencode-proven-floor.d.ts.map +1 -0
  81. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/reasoning-budget-status.d.ts +15 -0
  85. package/dist/hooks/magic-context/reasoning-budget-status.d.ts.map +1 -0
  86. package/dist/hooks/magic-context/reasoning-budget.d.ts +40 -0
  87. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -0
  88. package/dist/hooks/magic-context/reasoning-removal.d.ts +6 -6
  89. package/dist/hooks/magic-context/reasoning-removal.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/sentinel.d.ts +4 -0
  92. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/strip-content.d.ts +7 -3
  94. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/tag-messages.d.ts +13 -0
  96. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/tool-drop-target.d.ts +5 -0
  98. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -28
  100. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +28 -6
  102. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/transform.d.ts +3 -1
  104. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  105. package/dist/{index-0d3g3461.js → index-0c82743p.js} +2 -1
  106. package/dist/{index-5p8jdswx.js → index-5a2r0p9f.js} +3 -3
  107. package/dist/{index-sjthx78w.js → index-6d9e1m08.js} +4647 -11000
  108. package/dist/{index-majxda46.js → index-6wkcswft.js} +1 -1
  109. package/dist/index-92bp57hg.js +2063 -0
  110. package/dist/{index-sfnj6h8h.js → index-9pk9exrb.js} +3 -3
  111. package/dist/{index-7qw742ev.js → index-adwfkpex.js} +5 -24
  112. package/dist/{index-3h0ybwah.js → index-bb36vgr3.js} +80 -12
  113. package/dist/{index-pkbcrdqs.js → index-cwf6s3ay.js} +1072 -2422
  114. package/dist/index-fnn5pjtt.js +6554 -0
  115. package/dist/{index-sgthv3f7.js → index-gg8j07zz.js} +1 -1
  116. package/dist/{index-8x2v7gg9.js → index-m5fe6jch.js} +7 -3
  117. package/dist/{v2/server-g0fjrkta.js → index-rrbbspwh.js} +42 -27
  118. package/dist/index-rrrsepyw.js +3464 -0
  119. package/dist/{index-hpecdzec.js → index-w2t4ah5j.js} +948 -501
  120. package/dist/{v2/server-fg34g6a5.js → index-wx8a6bw3.js} +48 -8
  121. package/dist/{index-r5hmbx3t.js → index-x3kwrmqk.js} +146 -3571
  122. package/dist/index-yr3zj7e7.js +23 -0
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +624 -65
  125. package/dist/{message-time-backfill-68gj6t3e.js → message-time-backfill-r5epkzj3.js} +1 -1
  126. package/dist/migration-worker.js +7 -5
  127. package/dist/plugin/hooks/create-session-hooks.d.ts +2 -1
  128. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  129. package/dist/plugin/host-stall-profiler-report.d.ts +67 -0
  130. package/dist/plugin/host-stall-profiler-report.d.ts.map +1 -0
  131. package/dist/plugin/host-stall-profiler.d.ts +96 -0
  132. package/dist/plugin/host-stall-profiler.d.ts.map +1 -0
  133. package/dist/plugin/messages-transform.d.ts +21 -6
  134. package/dist/plugin/messages-transform.d.ts.map +1 -1
  135. package/dist/{read-session-chunk-egz7r68d.js → read-session-chunk-4jey7hpt.js} +4 -4
  136. package/dist/{recomp-orchestrator-y15j0rb9.js → recomp-orchestrator-rervg6q4.js} +13 -10
  137. package/dist/{send-session-notification-dyw42dtr.js → send-session-notification-0m68ms3n.js} +4 -3
  138. package/dist/shared/auto-search-lifecycle.d.ts +7 -0
  139. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -0
  140. package/dist/shared/historian-tool-expansions.d.ts +1 -1
  141. package/dist/shared/historian-tool-expansions.d.ts.map +1 -1
  142. package/dist/shared/historian-tool-template.d.ts +2 -2
  143. package/dist/shared/historian-tool-template.d.ts.map +1 -1
  144. package/dist/shared/sqlite.d.ts +16 -1
  145. package/dist/shared/sqlite.d.ts.map +1 -1
  146. package/dist/shared/status-view.d.ts.map +1 -1
  147. package/dist/shared/window-geometry.d.ts +4 -0
  148. package/dist/shared/window-geometry.d.ts.map +1 -1
  149. package/dist/tools/ctx-expand/render.d.ts.map +1 -1
  150. package/dist/{transform-postprocess-phase-bqhz6vdr.js → transform-postprocess-phase-z5hzpsxv.js} +13 -10
  151. package/dist/v2/auto-search-worker.js +115 -0
  152. package/dist/v2/{context-zecmzk5s.js → context-14v1hr8b.js} +3783 -4668
  153. package/dist/v2/{derive-budgets-kdgp4s3r.js → derive-budgets-nc9avxz8.js} +2 -2
  154. package/dist/v2/embedding-worker.js +3 -3
  155. package/dist/v2/hidden-completion.d.ts.map +1 -1
  156. package/dist/v2/hooks/context.d.ts.map +1 -1
  157. package/dist/v2/{message-time-backfill-r6qkem1x.js → message-time-backfill-1f9tzvcs.js} +1 -1
  158. package/dist/v2/migration-worker.js +7 -5
  159. package/dist/v2/{read-session-chunk-8vjp6581.js → read-session-chunk-gyyv7w2r.js} +4 -4
  160. package/dist/v2/{recomp-orchestrator-8ejxvrzz.js → recomp-orchestrator-42vjba7p.js} +17 -14
  161. package/dist/v2/{send-session-notification-qs0j78v6.js → send-session-notification-c0wtscea.js} +5 -4
  162. package/dist/v2/server-3trjk1hx.js +6360 -0
  163. package/dist/v2/{server-e29d7p7c.js → server-50hvkg3v.js} +7 -3
  164. package/dist/v2/{server-y8maxeft.js → server-cpepxy2q.js} +2 -1
  165. package/dist/v2/server-d0sraw7m.js +3403 -0
  166. package/dist/v2/server-e0c1b7a7.js +2063 -0
  167. package/dist/v2/{server-mcfawjgc.js → server-efhwzx05.js} +5 -24
  168. package/dist/v2/{server-3n4q9xme.js → server-fcdehk1s.js} +1 -1
  169. package/dist/{index-q4yp4b52.js → v2/server-fq7kq32k.js} +42 -27
  170. package/dist/v2/{server-jc7y02q0.js → server-nf82q6pt.js} +1117 -7290
  171. package/dist/v2/{server-frbm99q5.js → server-njt3np1k.js} +99 -3465
  172. package/dist/v2/{server-qp5hx1ad.js → server-nm9n3aqq.js} +107 -12
  173. package/dist/v2/{server-c5gz8wph.js → server-rz514f14.js} +7 -2
  174. package/dist/v2/{server-wvjvfhck.js → server-vd4e7174.js} +1 -1
  175. package/dist/{index-fg34g6a5.js → v2/server-wx8a6bw3.js} +48 -8
  176. package/dist/v2/{server-xa4xe3rv.js → server-y10vbq6y.js} +1 -1
  177. package/dist/v2/server-yr3zj7e7.js +23 -0
  178. package/dist/v2/server.js +5 -5
  179. package/dist/v2/{wrapup-orchestrator-0snhvwcv.js → wrapup-orchestrator-nbz854vc.js} +16 -13
  180. package/dist/wrapup-orchestrator-b2dztrar.js +29 -0
  181. package/package.json +3 -3
  182. package/src/config/index.ts +6 -0
  183. package/src/config/live-run-config.test.ts +37 -1
  184. package/src/config/live-run-config.ts +28 -0
  185. package/src/config/migrate-config-location.ts +10 -3
  186. package/src/config/schema/magic-context.test.ts +4 -3
  187. package/src/config/schema/magic-context.ts +18 -6
  188. package/src/shared/auto-search-lifecycle.ts +27 -0
  189. package/src/shared/historian-tool-defaults.json +6 -6
  190. package/src/shared/historian-tool-expansions.ts +65 -8
  191. package/src/shared/historian-tool-preview-defaults.json +13 -0
  192. package/src/shared/historian-tool-template.ts +48 -14
  193. package/src/shared/sqlite-640-review-r2.test.ts +136 -0
  194. package/src/shared/sqlite-640-review.test.ts +183 -0
  195. package/src/shared/sqlite-acquisition-routing.test.ts +18 -0
  196. package/src/shared/sqlite.ts +84 -14
  197. package/src/shared/status-view.test.ts +6 -1
  198. package/src/shared/status-view.ts +15 -1
  199. package/src/shared/tag-transcript.ts +6 -0
  200. package/src/shared/window-geometry.ts +10 -0
  201. package/dist/wrapup-orchestrator-39amhv9s.js +0 -26
  202. /package/dist/{index-en1k8h8m.js → index-5prbx026.js} +0 -0
  203. /package/dist/v2/{index-en1k8h8m.js → index-5prbx026.js} +0 -0
@@ -6,17 +6,14 @@ import {
6
6
  sessionLog
7
7
  } from "./index-hybnrwq0.js";
8
8
  import {
9
- isKnownAutocommit
10
- } from "./index-b35pxds2.js";
11
- import {
9
+ getSqliteDatabasePath,
10
+ assertTransformWrite,
12
11
  isTransientSqliteError
13
- } from "./index-fg34g6a5.js";
12
+ } from "./index-wx8a6bw3.js";
14
13
  import {
15
14
  BoundedSessionMap,
16
15
  renderUserFacingFailure,
17
- loadCompartmentChunkEmbeddingsForSearch,
18
16
  getMaxM0MutationId,
19
- getIndexedMessageCorpusSize,
20
17
  escalationBands,
21
18
  MERGED_REASONING_PARTS_PREFIX,
22
19
  readFrozenMergedReasoningParts,
@@ -62,42 +59,13 @@ import {
62
59
  setPendingCompactionMarkerState,
63
60
  replacePendingCompactionMarkerStateIf,
64
61
  clearPendingCompactionMarkerStateIf,
65
- recordEmbeddingMeasurement,
62
+ registerAutoSearchWork,
63
+ cancelAutoSearchWork,
66
64
  updateSessionMeta,
67
65
  advanceToolReclaimWatermark,
68
- getNotes,
69
- getActivePrimers,
70
66
  saveSourceContent,
71
67
  replaceSourceContent,
72
68
  getSourceContents,
73
- cosineSimilarity,
74
- saveCommitEmbedding,
75
- loadProjectCommitEmbeddings,
76
- loadUnembeddedCommits,
77
- countEmbeddedCommits,
78
- upsertCommits,
79
- getLatestIndexedCommitSha,
80
- enforceProjectCap,
81
- getProjectEmbeddings,
82
- contentSha256,
83
- getShadowEmbeddingMeasurementCohort,
84
- getPrimaryEmbeddingMeasurementCohort,
85
- embedShadowTextForProject,
86
- enqueueShadowEmbeddingItems,
87
- getProjectEmbeddingSnapshot,
88
- embedTextForProject,
89
- embedItemsForProject,
90
- isEmbeddingEnabled,
91
- embedText,
92
- ensureMemoryEmbeddings,
93
- ModuleMemoryAuthorityError,
94
- getMemoriesByProject,
95
- getMemoriesByProjects,
96
- updateMemoryRetrievalCount,
97
- relaxedFtsQuery,
98
- sanitizeFtsQuery,
99
- searchMemoriesFTS,
100
- searchMemoriesFTSUnion,
101
69
  compareOpenCodeMessagesByCanonicalOrder,
102
70
  MARKER_SUMMARY_TEXT,
103
71
  applyDeferredCompactionMarker,
@@ -115,7 +83,7 @@ import {
115
83
  peekNoteNudgeText,
116
84
  markNoteNudgeDelivered,
117
85
  getActiveCompartmentRun
118
- } from "./index-sjthx78w.js";
86
+ } from "./index-6d9e1m08.js";
119
87
  import {
120
88
  protectedToolTagNumbers,
121
89
  stripWellFormedLeadingTagPrefix,
@@ -124,6 +92,7 @@ import {
124
92
  prependTag,
125
93
  isThinkingPart,
126
94
  isRecord,
95
+ contentTagOwnerMessageId,
127
96
  estimateImageTokensFromDataUrl,
128
97
  estimateToolAttachmentImageTokens,
129
98
  buildSyntheticTodoPart,
@@ -146,6 +115,7 @@ import {
146
115
  AGE_RECLAIM_MIN_TOKENS,
147
116
  getOldestActiveUnprotectedToolTags,
148
117
  getActiveToolTagsForAgeReclaim,
118
+ getReasoningTokenEstimatesByMessage,
149
119
  tagTokenCountIsNull,
150
120
  backfillTagTokenCounts,
151
121
  insertTag,
@@ -159,6 +129,7 @@ import {
159
129
  getMaxTagNumberBySession,
160
130
  getTagNumberByMessageId,
161
131
  getAssignableTagNumberByMessageId,
132
+ getTagsBySession,
162
133
  getActiveTagsBySession,
163
134
  getTailHygieneTags,
164
135
  getTagsByNumbers,
@@ -178,9 +149,11 @@ import {
178
149
  toolPartHasUserAnswer,
179
150
  isEditTool,
180
151
  modelAcceptsEmptyContent,
152
+ isAnthropicFamilyRoute,
181
153
  neutralizeDroppedReasoningPart,
182
154
  restoreNeutralizedReasoningPart,
183
155
  isNeutralizedReasoningPart,
156
+ neutralizedReasoningSource,
184
157
  makeSentinel,
185
158
  makeWholeMessageSentinel,
186
159
  isSentinel,
@@ -190,22 +163,21 @@ import {
190
163
  extractToolCallObservation,
191
164
  ToolMutationBatch,
192
165
  createToolDropTarget
193
- } from "./index-3h0ybwah.js";
166
+ } from "./index-bb36vgr3.js";
194
167
  import {
195
168
  textMentionsRecentCommit,
196
169
  hasMeaningfulUserText2,
197
170
  estimateTokens
198
171
  } from "./index-5eqk6z3s.js";
199
172
  import {
200
- isValidLanguageCode
201
- } from "./index-8x2v7gg9.js";
173
+ isValidLanguageCode,
174
+ resolveModelConfigValue
175
+ } from "./index-m5fe6jch.js";
202
176
  import {
203
- resolveWorkspaceShareCategories,
204
- resolveWorkspaceIdentitySet,
205
- expandWorkspaceIdentitySetWithAliases,
206
- resolveStoredPathWorkspaceIdentity,
207
- sourceNameForMemory
208
- } from "./index-r5hmbx3t.js";
177
+ getProjectEmbeddingSnapshot,
178
+ embedTextForProject,
179
+ embedBatchForProject
180
+ } from "./index-fnn5pjtt.js";
209
181
  import {
210
182
  getTemporalDecisions,
211
183
  freezeTemporalDecisions,
@@ -214,12 +186,15 @@ import {
214
186
  import {
215
187
  normalizeProtectedToolName,
216
188
  mergeProtectedTools
217
- } from "./index-q4yp4b52.js";
189
+ } from "./index-rrbbspwh.js";
218
190
  import {
219
191
  openCodeDbExists,
220
192
  withReadOnlySessionDb,
221
193
  getMessageTimesFromOpenCodeDb
222
- } from "./index-majxda46.js";
194
+ } from "./index-6wkcswft.js";
195
+ import {
196
+ isEmbeddingHostBusy
197
+ } from "./index-yr3zj7e7.js";
223
198
  import {
224
199
  getErrorMessage
225
200
  } from "./index-y8v6yerm.js";
@@ -299,6 +274,39 @@ function markDropLeavesReasoning(db, sessionId) {
299
274
  });
300
275
  }
301
276
 
277
+ // src/hooks/magic-context/active-anthropic-turn.ts
278
+ function isInActiveAnthropicTurn(messages, index, anthropic) {
279
+ if (!anthropic)
280
+ return false;
281
+ for (let user = messages.length - 1;user >= 0; user--) {
282
+ const message = messages[user];
283
+ if (!isRecord(message))
284
+ continue;
285
+ const info = isRecord(message.info) ? message.info : message;
286
+ if (info.role !== "user" || info.synthetic === true || message.synthetic === true)
287
+ continue;
288
+ const id = typeof info.id === "string" ? info.id : "";
289
+ if (id.startsWith("synth-user-"))
290
+ continue;
291
+ const parts = Array.isArray(message.parts) ? message.parts : Array.isArray(message.content) ? message.content : [];
292
+ if (parts.length > 0 && parts.every((part) => isRecord(part) && (part.synthetic === true || part.ignored === true || ["tool_result", "toolResult", "tool-result"].includes(String(part.type)))))
293
+ continue;
294
+ return index > user;
295
+ }
296
+ return false;
297
+ }
298
+ function hasAnthropicReasoning(messages) {
299
+ return messages.some((message) => {
300
+ if (!isRecord(message))
301
+ return false;
302
+ const info = isRecord(message.info) ? message.info : message;
303
+ if (isAnthropicFamilyRoute(typeof info.providerID === "string" ? info.providerID : typeof info.provider === "string" ? info.provider : undefined, typeof info.modelID === "string" ? info.modelID : typeof info.model === "string" ? info.model : undefined))
304
+ return true;
305
+ const parts = Array.isArray(message.parts) ? message.parts : [];
306
+ return parts.some((part) => isRecord(part) && isRecord(part.metadata) && isRecord(part.metadata.anthropic));
307
+ });
308
+ }
309
+
302
310
  // src/hooks/magic-context/apply-operations.ts
303
311
  var RECENT_TOOL_SKELETON_WINDOW = 20;
304
312
  function buildReplacementContent(tagId) {
@@ -440,6 +448,10 @@ function applyPendingOperations(sessionId, db, targets, protectedTagIds, preload
440
448
  continue;
441
449
  }
442
450
  const target = targets.get(pendingOp.tagId);
451
+ if (target?.thinkingDropProtected) {
452
+ reject("active_thinking");
453
+ continue;
454
+ }
443
455
  const isToolTag = tagTypeById.get(pendingOp.tagId) === "tool";
444
456
  if (synthetic) {
445
457
  if (!isToolTag) {
@@ -541,2070 +553,104 @@ function applyPendingOperations(sessionId, db, targets, protectedTagIds, preload
541
553
  }
542
554
  function applyFlushedStatuses(sessionId, db, targets, preloadedTags) {
543
555
  let didMutateMessage = false;
544
- const tags = preloadedTags ?? getDroppedTagsByNumbers(db, sessionId, [...targets.keys()]);
545
- for (const tag of tags) {
546
- if (tag.status === "dropped") {
547
- const target = targets.get(tag.tagNumber);
548
- if (tag.type === "tool") {
549
- if (tag.dropMode === "edit_marker_stripped") {
550
- const result = target?.editMarkerStripped?.() ?? "absent";
551
- if (result === "truncated")
552
- didMutateMessage = true;
553
- } else if (tag.dropMode === "edit_marker") {
554
- const markResult = target?.editMarker?.() ?? "absent";
555
- if (markResult === "truncated") {
556
- didMutateMessage = true;
557
- }
558
- } else if (tag.dropMode === "skeleton_stripped") {
559
- const result = target?.skeletonStripped?.() ?? "absent";
560
- if (result === "truncated")
561
- didMutateMessage = true;
562
- } else if (tag.dropMode === "skeleton_real") {
563
- const result = target?.skeletonReal?.() ?? "absent";
564
- if (result === "truncated") {
565
- didMutateMessage = true;
566
- }
567
- } else if (tag.dropMode === "truncated") {
568
- const truncResult = target?.truncate?.() ?? "absent";
569
- if (truncResult === "truncated") {
570
- didMutateMessage = true;
571
- }
572
- } else {
573
- const dropResult = target?.drop?.() ?? "absent";
574
- if (dropResult === "removed" || dropResult === "truncated") {
575
- didMutateMessage = true;
576
- }
577
- }
578
- } else if (target) {
579
- const changed = target.setContent(buildReplacementContent(tag.tagNumber));
580
- if (changed)
581
- didMutateMessage = true;
582
- }
583
- }
584
- }
585
- return didMutateMessage;
586
- }
587
-
588
- // src/features/magic-context/git-commits/git-log-reader.ts
589
- import { execFile } from "node:child_process";
590
- import { promisify } from "node:util";
591
- var execFileAsync = promisify(execFile);
592
- var GIT_TIMEOUT_MS = 1e4;
593
- var DEFAULT_MAX_COMMITS = 5000;
594
- var RECORD_SEPARATOR = "\x1E";
595
- var FIELD_SEPARATOR = "\x1F";
596
- var FULL_OBJECT_NAME = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
597
- function classifyGitLogFailure(message) {
598
- if (message.includes("not a git repository"))
599
- return "not_a_repo";
600
- if (message.includes("unknown revision or path not in the working tree") || message.includes("does not have any commits yet") || message.includes("bad revision")) {
601
- return "no_head";
602
- }
603
- return "transient";
604
- }
605
- async function readGitCommitsResult(directory, options = {}) {
606
- const revision = options.branch ?? "HEAD";
607
- if (revision.startsWith("-")) {
608
- throw new Error(`readGitCommits: refusing revision that looks like an option: "${revision}"`);
609
- }
610
- const projectLabel = options.projectIdentity ?? "<project>";
611
- const exclusion = options.excludeReachableFrom && FULL_OBJECT_NAME.test(options.excludeReachableFrom) ? options.excludeReachableFrom : null;
612
- const args = [
613
- "log",
614
- revision,
615
- ...exclusion ? [`^${exclusion}`] : [],
616
- "--no-merges",
617
- `--max-count=${options.maxCommits ?? DEFAULT_MAX_COMMITS}`,
618
- `--format=%H${FIELD_SEPARATOR}%s${FIELD_SEPARATOR}%ae${FIELD_SEPARATOR}%ct${FIELD_SEPARATOR}%b${RECORD_SEPARATOR}`
619
- ];
620
- if (options.sinceMs !== undefined && options.sinceMs > 0) {
621
- const iso = new Date(options.sinceMs).toISOString();
622
- args.push(`--since=${iso}`);
623
- }
624
- args.push("--");
625
- let stdout;
626
- try {
627
- const result = await execFileAsync("git", args, {
628
- windowsHide: true,
629
- cwd: directory,
630
- timeout: GIT_TIMEOUT_MS,
631
- maxBuffer: 32 * 1024 * 1024,
632
- encoding: "utf8"
633
- });
634
- stdout = result.stdout;
635
- } catch (error) {
636
- const message = error instanceof Error ? error.message : String(error);
637
- if (exclusion && message.includes("bad object")) {
638
- log(`[git-commits] indexed tip ${exclusion.slice(0, 7)} is missing for ${projectLabel}; reading the full window`);
639
- return readGitCommitsResult(directory, { ...options, excludeReachableFrom: undefined });
640
- }
641
- const failure = classifyGitLogFailure(message);
642
- if (failure === "transient") {
643
- log(`[git-commits] readGitCommits failed for ${projectLabel}: ${message.slice(0, 500)}`);
644
- } else {
645
- log(`[git-commits] ${projectLabel} is not indexable (${failure})`);
646
- }
647
- return { commits: [], failure };
648
- }
649
- if (stdout.trim().length === 0) {
650
- log(`[git-commits] readGitCommits returned empty stdout for ${projectLabel} (sinceMs=${options.sinceMs ?? "none"} args=${args.slice(0, 4).join(" ")})`);
651
- }
652
- return { commits: parseGitLogOutput(stdout), failure: null };
653
- }
654
- function parseGitLogOutput(stdout) {
655
- const commits = [];
656
- const records = stdout.split(RECORD_SEPARATOR);
657
- for (const rawRecord of records) {
658
- const record = rawRecord.replace(/^\s+/, "");
659
- if (!record)
660
- continue;
661
- const fields = [];
662
- let remaining = record;
663
- for (let i = 0;i < 4; i++) {
664
- const idx = remaining.indexOf(FIELD_SEPARATOR);
665
- if (idx < 0)
666
- break;
667
- fields.push(remaining.slice(0, idx));
668
- remaining = remaining.slice(idx + FIELD_SEPARATOR.length);
669
- }
670
- fields.push(remaining);
671
- if (fields.length < 5)
672
- continue;
673
- const sha = fields[0].trim();
674
- const subject = fields[1].trim();
675
- const author = fields[2].trim();
676
- const timeSec = Number.parseInt(fields[3].trim(), 10);
677
- const body = fields[4].trim();
678
- if (!FULL_OBJECT_NAME.test(sha) || !Number.isFinite(timeSec) || timeSec <= 0) {
679
- continue;
680
- }
681
- const message = body.length > 0 ? `${subject}
682
-
683
- ${body}` : subject;
684
- commits.push({
685
- sha,
686
- shortSha: sha.slice(0, 7),
687
- message,
688
- author: author.length > 0 ? author : null,
689
- committedAtMs: timeSec * 1000
690
- });
691
- }
692
- return commits;
693
- }
694
- // src/features/magic-context/git-commits/indexer.ts
695
- var MS_PER_DAY = 24 * 60 * 60 * 1000;
696
- var EMBED_BATCH_SIZE = 16;
697
- var EMBED_MAX_PER_SWEEP = 500;
698
- var EMBED_SWEEP_MAX_WALL_CLOCK_MS = 5 * 60 * 1000;
699
- var indexInProgress = new Set;
700
- var embedInProgress = new Set;
701
- async function indexCommitsForProject(db, projectPath, directory, options) {
702
- const result = {
703
- scanned: 0,
704
- inserted: 0,
705
- updated: 0,
706
- evicted: 0,
707
- embedded: 0,
708
- nonIndexable: false
709
- };
710
- if (indexInProgress.has(projectPath)) {
711
- log(`[git-commits] index already in progress for ${projectPath}, skipping`);
712
- return result;
713
- }
714
- indexInProgress.add(projectPath);
715
- try {
716
- const latestIndexed = getLatestIndexedCommitSha(db, projectPath);
717
- const sinceMs = Date.now() - options.sinceDays * MS_PER_DAY;
718
- const read = await readGitCommitsResult(directory, {
719
- sinceMs,
720
- maxCommits: options.maxCommits,
721
- projectIdentity: projectPath,
722
- excludeReachableFrom: latestIndexed ?? undefined
723
- });
724
- const commits = read.commits;
725
- result.scanned = commits.length;
726
- if (read.failure === "not_a_repo" || read.failure === "no_head") {
727
- result.nonIndexable = true;
728
- return result;
729
- }
730
- if (commits.length === 0) {
731
- result.evicted = enforceProjectCap(db, projectPath, options.maxCommits);
732
- log(`[git-commits] no new commits for ${projectPath} (sinceMs=${sinceMs} latestIndexed=${latestIndexed?.slice(0, 7) ?? "none"} evicted=${result.evicted})`);
733
- return result;
734
- }
735
- log(`[git-commits] read ${commits.length} commits for ${projectPath} (sinceMs=${sinceMs} latestIndexed=${latestIndexed?.slice(0, 7) ?? "none"})`);
736
- const upsert = upsertCommits(db, projectPath, commits);
737
- result.inserted = upsert.inserted;
738
- result.updated = upsert.updated;
739
- result.evicted = enforceProjectCap(db, projectPath, options.maxCommits);
740
- const snapshot = getProjectEmbeddingSnapshot(projectPath);
741
- if (options.skipEmbed || !snapshot?.gitCommitEnabled) {
742
- log(`[git-commits] indexed ${projectPath}: scanned=${result.scanned} inserted=${result.inserted} updated=${result.updated} evicted=${result.evicted} embedded=0 (embedding skipped: skipEmbed=${options.skipEmbed === true} gitCommitEnabled=${snapshot?.gitCommitEnabled === true})`);
743
- return result;
744
- }
745
- result.embedded = await embedUnembeddedCommits(db, projectPath);
746
- log(`[git-commits] indexed ${projectPath}: scanned=${result.scanned} inserted=${result.inserted} updated=${result.updated} evicted=${result.evicted} embedded=${result.embedded}`);
747
- return result;
748
- } finally {
749
- indexInProgress.delete(projectPath);
750
- }
751
- }
752
- async function embedUnembeddedCommits(db, projectPath) {
753
- if (embedInProgress.has(projectPath)) {
754
- return 0;
755
- }
756
- const snapshot = getProjectEmbeddingSnapshot(projectPath);
757
- if (!snapshot?.gitCommitEnabled) {
758
- return 0;
759
- }
760
- embedInProgress.add(projectPath);
761
- const startedAt = Date.now();
762
- const deadline = startedAt + EMBED_SWEEP_MAX_WALL_CLOCK_MS;
763
- let total = 0;
764
- try {
765
- while (Date.now() < deadline && total < EMBED_MAX_PER_SWEEP) {
766
- const rows = loadUnembeddedCommits(db, projectPath, snapshot.modelId, EMBED_BATCH_SIZE);
767
- if (rows.length === 0)
768
- break;
769
- let embeddedThisBatch = 0;
770
- try {
771
- const result = await embedItemsForProject(projectPath, rows.map((row) => ({
772
- id: `commit:${row.sha}`,
773
- text: row.message,
774
- contentSha256: contentSha256(row.message)
775
- })), undefined, db, projectPath);
776
- if (!result)
777
- break;
778
- db.transaction(() => {
779
- for (const row of rows) {
780
- const embedding = result.vectors.get(`commit:${row.sha}`);
781
- if (!embedding)
782
- continue;
783
- saveCommitEmbedding(db, row.sha, embedding, result.modelId);
784
- embeddedThisBatch += 1;
785
- }
786
- }).immediate();
787
- enqueueShadowEmbeddingItems(projectPath, "commit", rows.filter((row) => result.vectors.has(`commit:${row.sha}`)).map((row) => row.sha));
788
- } catch (error) {
789
- log(`[git-commits] embed batch failed for ${projectPath}: ${error instanceof Error ? error.message : String(error)}`);
790
- break;
791
- }
792
- if (embeddedThisBatch === 0)
793
- break;
794
- total += embeddedThisBatch;
795
- if (embeddedThisBatch < rows.length)
796
- break;
797
- }
798
- if (total > 0) {
799
- const totalEmbedded = countEmbeddedCommits(db, projectPath, snapshot.modelId);
800
- log(`[git-commits] embedded ${total} commits for ${projectPath} (total embedded: ${totalEmbedded})`);
801
- }
802
- return total;
803
- } finally {
804
- embedInProgress.delete(projectPath);
805
- }
806
- }
807
- // src/features/magic-context/git-commits/search-git-commits.ts
808
- var ftsStatements = new WeakMap;
809
- var datedFtsStatements = new WeakMap;
810
- var ftsPlainStatements = new WeakMap;
811
- var datedFtsPlainStatements = new WeakMap;
812
- var getBySHAsStatements = new WeakMap;
813
- var datedGetBySHAsStatements = new WeakMap;
814
- var embeddingCache = new WeakMap;
815
- function getSearchEmbeddings(db, projectPath, modelId) {
816
- if (!isKnownAutocommit(db)) {
817
- embeddingCache.delete(db);
818
- return loadProjectCommitEmbeddings(db, projectPath, modelId);
819
- }
820
- let cache = embeddingCache.get(db);
821
- if (!cache) {
822
- cache = {
823
- revisionStatement: db.prepare(`SELECT total_changes() AS writes,
824
- (SELECT data_version FROM pragma_data_version) AS dataVersion,
825
- (SELECT schema_version FROM pragma_schema_version) AS schemaVersion`),
826
- revision: "",
827
- entries: new Map
828
- };
829
- embeddingCache.set(db, cache);
830
- }
831
- const revision = JSON.stringify(cache.revisionStatement.get());
832
- if (revision !== cache.revision) {
833
- cache.entries.clear();
834
- cache.revision = revision;
835
- }
836
- const key = JSON.stringify([projectPath, modelId]);
837
- const cached = cache.entries.get(key);
838
- if (cached) {
839
- cache.entries.delete(key);
840
- cache.entries.set(key, cached);
841
- return cached;
842
- }
843
- const vectors = loadProjectCommitEmbeddings(db, projectPath, modelId);
844
- const size = (entries) => [...entries.values()].reduce((sum, vector) => sum + vector.byteLength, 0);
845
- if (size(vectors) <= 32 * 1024 * 1024)
846
- cache.entries.set(key, vectors);
847
- while (cache.entries.size > 16 || [...cache.entries.values()].reduce((sum, entry) => sum + size(entry), 0) > 32 * 1024 * 1024) {
848
- const oldest = cache.entries.keys().next().value;
849
- if (oldest === undefined)
850
- break;
851
- cache.entries.delete(oldest);
852
- }
853
- return vectors;
854
- }
855
- function rowToCommit(row) {
856
- return {
857
- sha: row.sha,
858
- shortSha: row.short_sha,
859
- projectPath: row.project_path,
860
- message: row.message,
861
- author: row.author,
862
- committedAtMs: row.committed_at,
863
- indexedAtMs: row.indexed_at
864
- };
865
- }
866
- function getFtsStatement(db, dated = false) {
867
- const statements = dated ? datedFtsStatements : ftsStatements;
868
- let stmt = statements.get(db);
869
- if (!stmt) {
870
- stmt = db.prepare(`SELECT c.sha AS sha, c.project_path AS project_path, c.short_sha AS short_sha,
871
- c.message AS message, c.author AS author,
872
- c.committed_at AS committed_at, c.indexed_at AS indexed_at
873
- FROM git_commits_fts
874
- INNER JOIN git_commits c ON c.sha = git_commits_fts.sha
875
- WHERE c.project_path = ?
876
- ${dated ? "AND c.committed_at BETWEEN ? AND ?" : ""}
877
- AND git_commits_fts MATCH ?
878
- ORDER BY bm25(git_commits_fts) LIMIT ?`);
879
- statements.set(db, stmt);
880
- }
881
- return stmt;
882
- }
883
- function escapeLikePattern(text) {
884
- return text.replace(/[\\%_]/g, (char) => `\\${char}`);
885
- }
886
- function getLikeFallbackStatement(db, dated = false) {
887
- const statements = dated ? datedFtsPlainStatements : ftsPlainStatements;
888
- let stmt = statements.get(db);
889
- if (!stmt) {
890
- stmt = db.prepare(`SELECT sha, project_path, short_sha, message, author, committed_at, indexed_at
891
- FROM git_commits
892
- WHERE project_path = ?
893
- ${dated ? "AND committed_at BETWEEN ? AND ?" : ""}
894
- AND lower(message) LIKE '%' || lower(?) || '%' ESCAPE '\\'
895
- ORDER BY committed_at DESC LIMIT ?`);
896
- statements.set(db, stmt);
897
- }
898
- return stmt;
899
- }
900
- function getBySHAsStatement(db, dated = false) {
901
- const statements = dated ? datedGetBySHAsStatements : getBySHAsStatements;
902
- let stmt = statements.get(db);
903
- if (!stmt) {
904
- stmt = db.prepare(`SELECT sha, project_path, short_sha, message, author, committed_at, indexed_at
905
- FROM git_commits
906
- WHERE project_path = ?
907
- ${dated ? "AND committed_at BETWEEN ? AND ?" : ""}
908
- AND sha IN (SELECT value FROM json_each(?))`);
909
- statements.set(db, stmt);
910
- }
911
- return stmt;
912
- }
913
- function clamp01(value) {
914
- if (!Number.isFinite(value))
915
- return 0;
916
- return Math.min(1, Math.max(0, value));
917
- }
918
- function searchGitCommitsSync(db, projectPath, query, options) {
919
- const trimmed = query.trim();
920
- if (trimmed.length === 0 || options.limit <= 0)
921
- return [];
922
- const semanticWeight = options.semanticWeight ?? 0.7;
923
- const ftsWeight = options.ftsWeight ?? 0.3;
924
- const singleSourcePenalty = options.singleSourcePenalty ?? 0.8;
925
- const fetchLimit = Math.max(options.limit * 3, 30);
926
- const dated = options.from !== undefined || options.to !== undefined;
927
- const from = options.from ?? Number.MIN_SAFE_INTEGER;
928
- const to = options.to ?? Number.MAX_SAFE_INTEGER;
929
- const ftsCandidates = [];
930
- const sanitized = sanitizeFtsQuery(trimmed);
931
- if (sanitized.length > 0) {
932
- try {
933
- for (const row of getFtsStatement(db, dated).all(projectPath, ...dated ? [from, to] : [], sanitized, fetchLimit)) {
934
- ftsCandidates.push(rowToCommit(row));
935
- }
936
- } catch (error) {
937
- log(`[git-commits] FTS query failed for "${trimmed}": ${error instanceof Error ? error.message : String(error)}`);
938
- }
939
- }
940
- if (ftsCandidates.length === 0) {
941
- for (const row of getLikeFallbackStatement(db, dated).all(projectPath, ...dated ? [from, to] : [], escapeLikePattern(trimmed), fetchLimit)) {
942
- ftsCandidates.push(rowToCommit(row));
943
- }
944
- }
945
- const ftsScores = new Map;
946
- ftsCandidates.forEach((commit, rank) => {
947
- ftsScores.set(commit.sha, 1 / (rank + 1));
948
- });
949
- const semanticScores = new Map;
950
- if (options.queryEmbedding && options.queryModelId && options.queryModelId !== "off") {
951
- const embeddings = getSearchEmbeddings(db, projectPath, options.queryModelId);
952
- for (const [sha, embedding] of embeddings.entries()) {
953
- const similarity = clamp01(cosineSimilarity(options.queryEmbedding, embedding));
954
- if (similarity > 0) {
955
- semanticScores.set(sha, similarity);
956
- }
957
- }
958
- }
959
- const bySha = new Map;
960
- for (const commit of ftsCandidates)
961
- bySha.set(commit.sha, commit);
962
- const semanticOnlyShas = [...semanticScores.keys()].filter((sha) => !bySha.has(sha));
963
- if (semanticOnlyShas.length > 0) {
964
- const rows = getBySHAsStatement(db, dated).all(projectPath, ...dated ? [from, to] : [], JSON.stringify(semanticOnlyShas));
965
- for (const row of rows)
966
- bySha.set(row.sha, rowToCommit(row));
967
- }
968
- const results = [];
969
- for (const [sha, commit] of bySha.entries()) {
970
- const sem = semanticScores.get(sha);
971
- const fts = ftsScores.get(sha);
972
- let score = 0;
973
- let matchType = "fts";
974
- if (sem !== undefined && fts !== undefined) {
975
- score = semanticWeight * sem + ftsWeight * fts;
976
- matchType = "hybrid";
977
- } else if (sem !== undefined) {
978
- score = sem * singleSourcePenalty;
979
- matchType = "semantic";
980
- } else if (fts !== undefined) {
981
- score = fts * singleSourcePenalty;
982
- matchType = "fts";
983
- }
984
- if (score <= 0)
985
- continue;
986
- results.push({ commit, score, matchType });
987
- }
988
- results.sort((left, right) => {
989
- if (right.score !== left.score)
990
- return right.score - left.score;
991
- return right.commit.committedAtMs - left.commit.committedAtMs;
992
- });
993
- return results.slice(0, options.limit);
994
- }
995
- // src/features/magic-context/literal-probes.ts
996
- var MAX_PROBES = 5;
997
- var MIN_PROBE_LENGTH = 3;
998
- var SLASH_COMMAND_RE = /\/[a-z][a-z0-9]*(?:-[a-z0-9]+)+/gi;
999
- var KEBAB_SNAKE_RE = /[a-z][a-z0-9]*(?:[-_][a-z0-9]+)+/gi;
1000
- var DOTTED_RE = /[a-z0-9][a-z0-9_-]*(?:\.[a-z0-9_-]+)+/gi;
1001
- var CAMEL_RE = /\b[a-zA-Z][a-z0-9]*(?:[A-Z][a-z0-9]*)+\b/g;
1002
- var SHA_RE = /\b[0-9a-f]{7,40}\b/gi;
1003
- var ERROR_CODE_RE = /\b(?:TS\d{4,}|ERR_[A-Z][A-Z0-9_]*)\b/g;
1004
- var QUOTED_RE = /["`]([^"`]{3,80})["`]/g;
1005
- function looksLikeSha(token) {
1006
- return /[0-9]/.test(token) && /^[0-9a-f]{7,40}$/i.test(token);
1007
- }
1008
- function extractLiteralProbes(query) {
1009
- const trimmed = query.trim();
1010
- if (trimmed.length === 0)
1011
- return [];
1012
- const ordered = [];
1013
- const seen = new Set;
1014
- const add = (raw) => {
1015
- if (!raw)
1016
- return;
1017
- const probe = raw.trim();
1018
- if (probe.length < MIN_PROBE_LENGTH)
1019
- return;
1020
- const key = probe.toLowerCase();
1021
- if (seen.has(key))
1022
- return;
1023
- seen.add(key);
1024
- ordered.push(probe);
1025
- };
1026
- for (const m of trimmed.matchAll(QUOTED_RE))
1027
- add(m[1]);
1028
- for (const m of trimmed.matchAll(SLASH_COMMAND_RE))
1029
- add(m[0]);
1030
- for (const m of trimmed.matchAll(ERROR_CODE_RE))
1031
- add(m[0]);
1032
- for (const m of trimmed.matchAll(DOTTED_RE))
1033
- add(m[0]);
1034
- for (const m of trimmed.matchAll(KEBAB_SNAKE_RE))
1035
- add(m[0]);
1036
- for (const m of trimmed.matchAll(CAMEL_RE))
1037
- add(m[0]);
1038
- for (const m of trimmed.matchAll(SHA_RE)) {
1039
- if (looksLikeSha(m[0]))
1040
- add(m[0]);
1041
- }
1042
- return ordered.slice(0, MAX_PROBES);
1043
- }
1044
- function containsProbeVerbatim(text, probes) {
1045
- if (probes.length === 0)
1046
- return false;
1047
- const haystack = text.toLowerCase();
1048
- return probes.some((probe) => haystack.includes(probe.toLowerCase()));
1049
- }
1050
-
1051
- // src/features/magic-context/message-fts-session-filter.ts
1052
- var MESSAGE_FTS_SESSION_FILTER_SQL = `+message_history_fts.rowid IN (
1053
- SELECT fts_rowid FROM message_fts_rowid_map WHERE session_id = ?1
1054
- ) AND `;
1055
- var coverageCaches = new WeakMap;
1056
- function getCoverageCache(db) {
1057
- let cache = coverageCaches.get(db);
1058
- if (!cache) {
1059
- cache = {
1060
- pin: db.prepare("SELECT fts_rowid FROM message_fts_rowid_map LIMIT 1"),
1061
- stamp: db.prepare("SELECT total_changes() AS changes, data_version AS version FROM pragma_data_version"),
1062
- missing: db.prepare(`SELECT DISTINCT session_id AS sessionId
1063
- FROM message_history_fts WHERE rowid IN (
1064
- SELECT id FROM message_history_fts_docsize
1065
- EXCEPT SELECT fts_rowid FROM message_fts_rowid_map
1066
- )`)
1067
- };
1068
- coverageCaches.set(db, cache);
1069
- }
1070
- return cache;
1071
- }
1072
- function withMessageFtsSessionFilter(db, sessionId, read) {
1073
- let cacheable = false;
1074
- try {
1075
- const transaction = db;
1076
- cacheable = transaction.inTransaction === false || transaction.isTransaction === false;
1077
- if (!cacheable)
1078
- coverageCaches.delete(db);
1079
- } catch {
1080
- coverageCaches.delete(db);
1081
- }
1082
- return db.transaction(() => {
1083
- let sessionFirst = false;
1084
- try {
1085
- const cache = getCoverageCache(db);
1086
- cache.pin.get();
1087
- const stamp = cache.stamp.get();
1088
- let incomplete = cache.incompleteSessions;
1089
- if (!cacheable || !incomplete || cache.version !== stamp.version || cache.changes !== stamp.changes) {
1090
- const rows = cache.missing.all();
1091
- incomplete = new Set(rows.map((row) => row.sessionId));
1092
- if (cacheable) {
1093
- cache.version = stamp.version;
1094
- cache.changes = stamp.changes;
1095
- cache.incompleteSessions = incomplete;
1096
- }
1097
- }
1098
- sessionFirst = !incomplete.has(sessionId);
1099
- } catch {
1100
- coverageCaches.delete(db);
1101
- }
1102
- return read(sessionFirst);
1103
- }).deferred();
1104
- }
1105
-
1106
- // src/features/magic-context/search-measurement.ts
1107
- import { createHash } from "node:crypto";
1108
- function resultId(result) {
1109
- switch (result.source) {
1110
- case "memory":
1111
- return `memory:${result.memoryId}`;
1112
- case "message":
1113
- return `message:${result.messageId}`;
1114
- case "compartment":
1115
- return `chunk:${result.compartmentId}`;
1116
- case "git_commit":
1117
- return `commit:${result.sha}`;
1118
- case "primer":
1119
- return `primer:${result.primerId}`;
1120
- case "note":
1121
- return `note:${result.noteId}`;
1122
- }
1123
- }
1124
- async function recordShadowMeasurement(args) {
1125
- try {
1126
- const shadowCohort = getShadowEmbeddingMeasurementCohort(args.projectPath);
1127
- if (!shadowCohort)
1128
- return;
1129
- const primaryCohort = getPrimaryEmbeddingMeasurementCohort(args.projectPath);
1130
- const shadowStartedAt = Date.now();
1131
- let shadowFailed = false;
1132
- let shadowResults = [];
1133
- try {
1134
- const vector = await embedShadowTextForProject(args.projectPath, args.query);
1135
- if (!vector) {
1136
- shadowFailed = true;
1137
- } else {
1138
- shadowResults = await args.search(args.db, args.sessionId, args.projectPath, args.query, {
1139
- ...args.options,
1140
- embedQuery: async () => vector,
1141
- isEmbeddingRuntimeEnabled: () => true,
1142
- embeddingEnabled: true,
1143
- embeddingModelIdOverride: shadowCohort.modelId,
1144
- chunkModelIdOverride: shadowCohort.chunkModelId,
1145
- measurementDisabled: true,
1146
- countRetrievals: false
1147
- });
1148
- }
1149
- } catch {
1150
- shadowFailed = true;
1151
- }
1152
- const primaryModelId = args.primaryQuery?.modelId ?? primaryCohort?.modelId ?? "";
1153
- const primaryFingerprint = primaryCohort?.fingerprint ?? "";
1154
- const primaryEpoch = primaryCohort?.epoch ?? 0;
1155
- const cohortKey = JSON.stringify({
1156
- primaryFingerprint,
1157
- primaryEpoch,
1158
- shadowFingerprint: shadowCohort.fingerprint,
1159
- shadowEpoch: shadowCohort.epoch
1160
- });
1161
- const primaryIds = args.primaryResults.map(resultId);
1162
- const shadowIds = shadowResults.map(resultId);
1163
- const corpusHash = sha256(JSON.stringify({ query: args.query, primaryIds, shadowIds }));
1164
- recordEmbeddingMeasurement(args.db, {
1165
- sessionId: args.sessionId,
1166
- projectPath: args.projectPath,
1167
- queryText: args.query,
1168
- cohortKey,
1169
- primaryResultIds: primaryIds,
1170
- shadowResultIds: shadowIds,
1171
- primaryLatencyMs: args.primaryLatencyMs,
1172
- shadowLatencyMs: Date.now() - shadowStartedAt,
1173
- primaryFailed: args.primaryQuery === null,
1174
- shadowFailed,
1175
- primaryModelId,
1176
- shadowModelId: shadowCohort.modelId,
1177
- primaryFingerprint,
1178
- shadowFingerprint: shadowCohort.fingerprint,
1179
- primaryEpoch,
1180
- shadowEpoch: shadowCohort.epoch,
1181
- corpusHash,
1182
- coverage: {
1183
- primaryResultCount: primaryIds.length,
1184
- shadowResultCount: shadowIds.length,
1185
- primaryAnswered: args.primaryQuery !== null,
1186
- shadowAnswered: !shadowFailed
1187
- }
1188
- });
1189
- } catch (error) {
1190
- log("[magic-context] shadow embedding measurement failed:", error);
1191
- }
1192
- }
1193
- function sha256(value) {
1194
- return createHash("sha256").update(value).digest("hex");
1195
- }
1196
-
1197
- // src/features/magic-context/search.ts
1198
- var DEFAULT_UNIFIED_SEARCH_LIMIT = 10;
1199
- var MAX_UNIFIED_SEARCH_LIMIT = 25;
1200
- var FTS_SEMANTIC_CANDIDATE_LIMIT = 50;
1201
- var SEMANTIC_WEIGHT = 0.7;
1202
- var FTS_WEIGHT = 0.3;
1203
- var SINGLE_SOURCE_PENALTY = 0.8;
1204
- var RESULT_PREVIEW_LIMIT = 220;
1205
- var MEMORY_SOURCE_BOOST = 1.3;
1206
- var MESSAGE_SOURCE_BOOST = 1.275;
1207
- var GIT_COMMIT_SOURCE_BOOST = 1.2;
1208
- var PRIMER_SOURCE_BOOST = 1.25;
1209
- var messageSearchStatements = new WeakMap;
1210
- var sessionFirstMessageSearchStatements = new WeakMap;
1211
- var messageSearchStatementsWithCutoff = new WeakMap;
1212
- var sessionFirstMessageSearchStatementsWithCutoff = new WeakMap;
1213
- var messageSearchStatementsWithDateRange = new WeakMap;
1214
- var messageSearchDiagnosticStatements = new WeakMap;
1215
- var sessionFirstMessageSearchDiagnosticStatements = new WeakMap;
1216
- var messageSearchDiagnosticStatementsWithDateRange = new WeakMap;
1217
- var batchedMessageSearchStatements = new WeakMap;
1218
- var batchedFtsCountStatements = new WeakMap;
1219
- function createUnifiedSearchDiagnostics() {
1220
- return {
1221
- suppressedVisibleMemoryIds: [],
1222
- suppressedLiveMessageMatches: 0,
1223
- gitCommitUnavailable: null
1224
- };
1225
- }
1226
- function normalizeLimit(limit) {
1227
- if (typeof limit !== "number" || !Number.isFinite(limit)) {
1228
- return DEFAULT_UNIFIED_SEARCH_LIMIT;
1229
- }
1230
- return Math.min(MAX_UNIFIED_SEARCH_LIMIT, Math.max(1, Math.floor(limit)));
1231
- }
1232
- function normalizeDateRange(from, to) {
1233
- if (from === undefined && to === undefined)
1234
- return null;
1235
- return {
1236
- from: typeof from === "number" && Number.isSafeInteger(from) ? from : Number.MIN_SAFE_INTEGER,
1237
- to: typeof to === "number" && Number.isSafeInteger(to) ? to : Number.MAX_SAFE_INTEGER
1238
- };
1239
- }
1240
- function timestampIsInRange(timestamp, range) {
1241
- return range === null || timestamp >= range.from && timestamp <= range.to;
1242
- }
1243
- var ID_SHAPED_QUERY_MAX_TOKENS = 5;
1244
- var ID_SHAPED_TOKEN = /^#?\d+$/;
1245
- function parseIdShapedQuery(query) {
1246
- const trimmed = query.trim();
1247
- if (trimmed.length === 0) {
1248
- return null;
1249
- }
1250
- const tokens = trimmed.split(/[\s,]+/).filter((token) => token.length > 0);
1251
- if (tokens.length === 0 || tokens.length > ID_SHAPED_QUERY_MAX_TOKENS) {
1252
- return null;
1253
- }
1254
- const ids = [];
1255
- for (const token of tokens) {
1256
- if (!ID_SHAPED_TOKEN.test(token)) {
1257
- return null;
1258
- }
1259
- const parsed = Number.parseInt(token.replace(/^#/, ""), 10);
1260
- if (!Number.isFinite(parsed) || parsed <= 0) {
1261
- return null;
1262
- }
1263
- ids.push(parsed);
1264
- }
1265
- return ids;
1266
- }
1267
- function normalizeCosineScore(score) {
1268
- if (!Number.isFinite(score)) {
1269
- return 0;
1270
- }
1271
- return Math.min(1, Math.max(0, score));
1272
- }
1273
- function previewText(text) {
1274
- const normalized = text.replace(/\s+/g, " ").trim();
1275
- if (normalized.length <= RESULT_PREVIEW_LIMIT) {
1276
- return normalized;
1277
- }
1278
- return `${normalized.slice(0, RESULT_PREVIEW_LIMIT - 1).trimEnd()}…`;
1279
- }
1280
- function resolveSearchWorkspaceContext(db, projectPath, identitySet) {
1281
- const resolved = identitySet ?? resolveWorkspaceIdentitySet(db, projectPath);
1282
- const isWorkspaced = resolved.identities.length > 1;
1283
- const expanded = expandWorkspaceIdentitySetWithAliases(db, resolved.identities);
1284
- const expandedIdentities = isWorkspaced ? expanded.expandedIdentities : resolved.identities;
1285
- const canonicalIdentityByStoredPath = isWorkspaced ? expanded.canonicalIdentityByStoredPath : new Map(resolved.identities.map((identity) => [identity, identity]));
1286
- const ownIdentities = expandedIdentities.filter((identity) => canonicalIdentityByStoredPath.get(identity) === projectPath);
1287
- return {
1288
- identities: resolved.identities,
1289
- expandedIdentities,
1290
- ownIdentities,
1291
- shareCategories: isWorkspaced ? resolveWorkspaceShareCategories(db, projectPath) : null,
1292
- namesByIdentity: resolved.namesByIdentity,
1293
- canonicalIdentityByStoredPath,
1294
- isWorkspaced
1295
- };
1296
- }
1297
- function memoryWorkspaceIdentity(memory, workspace) {
1298
- return resolveStoredPathWorkspaceIdentity(memory.projectPath, workspace.identities, workspace.canonicalIdentityByStoredPath);
1299
- }
1300
- function sourceNamesForSearchMemories(args) {
1301
- if (!args.workspace.isWorkspaced)
1302
- return;
1303
- const sourceNames = new Map;
1304
- for (const memory of args.memories) {
1305
- const source = sourceNameForMemory(memory.projectPath, args.projectPath, args.workspace.identities, args.workspace.namesByIdentity, args.workspace.canonicalIdentityByStoredPath);
1306
- if (source)
1307
- sourceNames.set(memory.id, source);
1308
- }
1309
- return sourceNames.size > 0 ? sourceNames : undefined;
1310
- }
1311
- function getMessageSearchStatement(db, sessionFirst) {
1312
- const statements = sessionFirst ? sessionFirstMessageSearchStatements : messageSearchStatements;
1313
- let stmt = statements.get(db);
1314
- if (!stmt) {
1315
- stmt = db.prepare(`SELECT message_ordinal AS messageOrdinal, message_id AS messageId, role, content FROM message_history_fts WHERE ${sessionFirst ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}session_id = ?1 AND message_history_fts MATCH ? ORDER BY bm25(message_history_fts), CAST(message_ordinal AS INTEGER) ASC LIMIT ?`);
1316
- statements.set(db, stmt);
1317
- }
1318
- return stmt;
1319
- }
1320
- function getMessageSearchStatementWithCutoff(db, sessionFirst) {
1321
- const statements = sessionFirst ? sessionFirstMessageSearchStatementsWithCutoff : messageSearchStatementsWithCutoff;
1322
- let stmt = statements.get(db);
1323
- if (!stmt) {
1324
- stmt = db.prepare(`SELECT message_ordinal AS messageOrdinal, message_id AS messageId, role, content FROM message_history_fts WHERE ${sessionFirst ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}session_id = ?1 AND message_history_fts MATCH ? AND CAST(message_ordinal AS INTEGER) <= ? ORDER BY bm25(message_history_fts), CAST(message_ordinal AS INTEGER) ASC LIMIT ?`);
1325
- statements.set(db, stmt);
1326
- }
1327
- return stmt;
1328
- }
1329
- function getMessageSearchStatementWithDateRange(db, withCutoff) {
1330
- let statements = messageSearchStatementsWithDateRange.get(db);
1331
- if (!statements) {
1332
- statements = new Map;
1333
- messageSearchStatementsWithDateRange.set(db, statements);
1334
- }
1335
- const key = withCutoff ? "cutoff" : "all";
1336
- let statement = statements.get(key);
1337
- if (!statement) {
1338
- statement = db.prepare(`SELECT message_history_fts.message_ordinal AS messageOrdinal,
1339
- message_history_fts.message_id AS messageId,
1340
- message_history_fts.role AS role,
1341
- message_history_fts.content AS content
1342
- FROM message_history_fts
1343
- JOIN message_fts_rowid_map AS map
1344
- ON map.session_id = message_history_fts.session_id
1345
- AND map.fts_rowid = message_history_fts.rowid
1346
- WHERE ${MESSAGE_FTS_SESSION_FILTER_SQL}message_history_fts.session_id = ?1
1347
- AND message_history_fts MATCH ?
1348
- AND map.message_time_ms BETWEEN ? AND ?
1349
- ${withCutoff ? "AND CAST(message_history_fts.message_ordinal AS INTEGER) <= ?" : ""}
1350
- ORDER BY bm25(message_history_fts),
1351
- CAST(message_history_fts.message_ordinal AS INTEGER) ASC
1352
- LIMIT ?`);
1353
- statements.set(key, statement);
1354
- }
1355
- return statement;
1356
- }
1357
- function getMessageSearchDiagnosticStatement(db, sessionFirst) {
1358
- const statements = sessionFirst ? sessionFirstMessageSearchDiagnosticStatements : messageSearchDiagnosticStatements;
1359
- let stmt = statements.get(db);
1360
- if (!stmt) {
1361
- stmt = db.prepare(`
1362
- WITH matches AS MATERIALIZED (
1363
- SELECT
1364
- message_ordinal AS messageOrdinal,
1365
- message_id AS messageId,
1366
- role,
1367
- content,
1368
- CAST(message_ordinal AS INTEGER) AS ordinalValue,
1369
- bm25(message_history_fts) AS ftsRank
1370
- FROM message_history_fts
1371
- WHERE ${sessionFirst ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}session_id = ?1 AND message_history_fts MATCH ?
1372
- ),
1373
- eligible AS (
1374
- SELECT * FROM matches
1375
- WHERE ordinalValue <= ?
1376
- ORDER BY ftsRank, ordinalValue ASC
1377
- LIMIT ?
1378
- ),
1379
- summary AS (
1380
- SELECT COUNT(*) AS suppressedCount
1381
- FROM matches
1382
- WHERE ordinalValue > ?
1383
- )
1384
- SELECT
1385
- eligible.messageOrdinal,
1386
- eligible.messageId,
1387
- eligible.role,
1388
- eligible.content,
1389
- eligible.ftsRank,
1390
- summary.suppressedCount,
1391
- 0 AS summaryOnly
1392
- FROM eligible CROSS JOIN summary
1393
- UNION ALL
1394
- SELECT NULL, NULL, NULL, NULL, NULL, summary.suppressedCount, 1
1395
- FROM summary
1396
- WHERE NOT EXISTS (SELECT 1 FROM eligible)
1397
- ORDER BY summaryOnly ASC, ftsRank ASC, messageOrdinal ASC
1398
- `);
1399
- statements.set(db, stmt);
1400
- }
1401
- return stmt;
1402
- }
1403
- function getMessageSearchDiagnosticStatementWithDateRange(db) {
1404
- let statement = messageSearchDiagnosticStatementsWithDateRange.get(db);
1405
- if (!statement) {
1406
- statement = db.prepare(`
1407
- WITH matches AS MATERIALIZED (
1408
- SELECT
1409
- message_history_fts.message_ordinal AS messageOrdinal,
1410
- message_history_fts.message_id AS messageId,
1411
- message_history_fts.role AS role,
1412
- message_history_fts.content AS content,
1413
- CAST(message_history_fts.message_ordinal AS INTEGER) AS ordinalValue,
1414
- bm25(message_history_fts) AS ftsRank
1415
- FROM message_history_fts
1416
- JOIN message_fts_rowid_map AS map
1417
- ON map.session_id = message_history_fts.session_id
1418
- AND map.fts_rowid = message_history_fts.rowid
1419
- WHERE ${MESSAGE_FTS_SESSION_FILTER_SQL}message_history_fts.session_id = ?1
1420
- AND message_history_fts MATCH ?
1421
- AND map.message_time_ms BETWEEN ? AND ?
1422
- ),
1423
- eligible AS (
1424
- SELECT * FROM matches
1425
- WHERE ordinalValue <= ?
1426
- ORDER BY ftsRank, ordinalValue ASC
1427
- LIMIT ?
1428
- ),
1429
- summary AS (
1430
- SELECT COUNT(*) AS suppressedCount
1431
- FROM matches
1432
- WHERE ordinalValue > ?
1433
- )
1434
- SELECT
1435
- eligible.messageOrdinal,
1436
- eligible.messageId,
1437
- eligible.role,
1438
- eligible.content,
1439
- eligible.ftsRank,
1440
- summary.suppressedCount,
1441
- 0 AS summaryOnly
1442
- FROM eligible CROSS JOIN summary
1443
- UNION ALL
1444
- SELECT NULL, NULL, NULL, NULL, NULL, summary.suppressedCount, 1
1445
- FROM summary
1446
- WHERE NOT EXISTS (SELECT 1 FROM eligible)
1447
- ORDER BY summaryOnly ASC, ftsRank ASC, messageOrdinal ASC
1448
- `);
1449
- messageSearchDiagnosticStatementsWithDateRange.set(db, statement);
1450
- }
1451
- return statement;
1452
- }
1453
- function getBatchedFtsCountStatement(db, queryCount, cutoff, dateRange, sessionFirst) {
1454
- let statements = batchedFtsCountStatements.get(db);
1455
- if (!statements) {
1456
- statements = new Map;
1457
- batchedFtsCountStatements.set(db, statements);
1458
- }
1459
- const key = `${queryCount}:${cutoff === null ? "all" : "cutoff"}:${dateRange === null ? "all-dates" : "dated"}:${sessionFirst}`;
1460
- let statement = statements.get(key);
1461
- if (!statement) {
1462
- const cutoffSql = cutoff === null ? "" : " AND CAST(message_history_fts.message_ordinal AS INTEGER) <= ?";
1463
- const joinSql = dateRange === null ? "" : ` JOIN message_fts_rowid_map AS map
1464
- ON map.session_id = message_history_fts.session_id
1465
- AND map.fts_rowid = message_history_fts.rowid`;
1466
- const dateSql = dateRange === null ? "" : " AND map.message_time_ms BETWEEN ? AND ?";
1467
- statement = db.prepare(Array.from({ length: queryCount }, (_, index) => `SELECT ${index} AS queryIndex, COUNT(*) AS count
1468
- FROM message_history_fts${joinSql}
1469
- WHERE ${sessionFirst || dateRange !== null ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}message_history_fts.session_id = ${index === 0 ? "?1" : "?"}
1470
- AND message_history_fts MATCH ?${dateSql}${cutoffSql}`).join(`
1471
- UNION ALL
1472
- `));
1473
- statements.set(key, statement);
1474
- }
1475
- return statement;
1476
- }
1477
- function contentOnlyMessageQuery(ftsQuery) {
1478
- return ftsQuery.length === 0 ? "" : `content : (${ftsQuery})`;
1479
- }
1480
- function countSessionFtsMatchesBatch(db, sessionId, ftsQueries, cutoff, dateRange, sessionFirst) {
1481
- if (ftsQueries.length === 0)
1482
- return [];
1483
- const bindings = [];
1484
- for (const query of ftsQueries) {
1485
- bindings.push(sessionId, contentOnlyMessageQuery(query));
1486
- if (dateRange !== null)
1487
- bindings.push(dateRange.from, dateRange.to);
1488
- if (cutoff !== null)
1489
- bindings.push(cutoff);
1490
- }
1491
- try {
1492
- const rows = getBatchedFtsCountStatement(db, ftsQueries.length, cutoff, dateRange, sessionFirst).all(...bindings);
1493
- const counts = Array.from({ length: ftsQueries.length }, () => 0);
1494
- for (const row of rows) {
1495
- if (typeof row.queryIndex === "number" && row.queryIndex >= 0 && row.queryIndex < counts.length && typeof row.count === "number") {
1496
- counts[row.queryIndex] = row.count;
1497
- }
1498
- }
1499
- return counts;
1500
- } catch {
1501
- return Array.from({ length: ftsQueries.length }, () => 0);
1502
- }
1503
- }
1504
- function getMessageOrdinal(value) {
1505
- if (typeof value === "number" && Number.isFinite(value)) {
1506
- return value;
1507
- }
1508
- if (typeof value === "string" && value.trim().length > 0) {
1509
- const parsed = Number.parseInt(value, 10);
1510
- return Number.isFinite(parsed) ? parsed : null;
1511
- }
1512
- return null;
1513
- }
1514
- async function getSemanticScores(args) {
1515
- const semanticScores = new Map;
1516
- if (!args.queryEmbedding || args.memories.length === 0 || !args.queryModelId || args.queryModelId === "off") {
1517
- return semanticScores;
1518
- }
1519
- if (!args.workspace?.isWorkspaced) {
1520
- const cachedEmbeddings = getProjectEmbeddings(args.db, args.projectPath, args.queryModelId);
1521
- const embeddings = await ensureMemoryEmbeddings({
1522
- db: args.db,
1523
- projectIdentity: args.projectPath,
1524
- memories: args.memories,
1525
- existingEmbeddings: cachedEmbeddings
1526
- });
1527
- for (const memory of args.memories) {
1528
- const memoryEmbedding = embeddings.get(memory.id);
1529
- if (!memoryEmbedding) {
1530
- continue;
1531
- }
1532
- semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(args.queryEmbedding, memoryEmbedding.embedding)));
1533
- }
1534
- return semanticScores;
1535
- }
1536
- const workspace = args.workspace;
1537
- const memoriesByIdentity = new Map;
1538
- for (const memory of args.memories) {
1539
- const identity = memoryWorkspaceIdentity(memory, workspace);
1540
- if (!identity)
1541
- continue;
1542
- const list = memoriesByIdentity.get(identity) ?? [];
1543
- list.push(memory);
1544
- memoriesByIdentity.set(identity, list);
1545
- }
1546
- const ownMemories = memoriesByIdentity.get(args.projectPath) ?? [];
1547
- if (ownMemories.length > 0) {
1548
- const ownEmbeddings = getProjectEmbeddings(args.db, args.projectPath, args.queryModelId);
1549
- await ensureMemoryEmbeddings({
1550
- db: args.db,
1551
- projectIdentity: args.projectPath,
1552
- memories: ownMemories,
1553
- existingEmbeddings: ownEmbeddings
1554
- });
1555
- }
1556
- for (const identity of workspace.identities) {
1557
- const memberMemories = memoriesByIdentity.get(identity) ?? [];
1558
- if (memberMemories.length === 0)
1559
- continue;
1560
- const cachedEmbeddings = getProjectEmbeddings(args.db, identity, args.queryModelId);
1561
- for (const memory of memberMemories) {
1562
- const memoryEmbedding = cachedEmbeddings.get(memory.id);
1563
- if (!memoryEmbedding || memoryEmbedding.modelId !== args.queryModelId)
1564
- continue;
1565
- semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(args.queryEmbedding, memoryEmbedding.embedding)));
1566
- }
1567
- }
1568
- return semanticScores;
1569
- }
1570
- function getFtsMatches(args) {
1571
- try {
1572
- return args.workspace?.isWorkspaced ? searchMemoriesFTSUnion(args.db, args.workspace.expandedIdentities, args.query, args.limit, args.workspace.ownIdentities, args.workspace.shareCategories, args.dateRange) : searchMemoriesFTS(args.db, args.projectPath, args.query, args.limit, args.dateRange);
1573
- } catch (error) {
1574
- log(`[search] FTS query failed for "${args.query}": ${error instanceof Error ? error.message : String(error)}`);
1575
- return [];
1576
- }
1577
- }
1578
- function getFtsScores(matches) {
1579
- return new Map(matches.map((memory, rank) => [memory.id, 1 / (rank + 1)]));
1580
- }
1581
- function selectSemanticCandidates(args) {
1582
- if (args.ftsMatches.length === 0) {
1583
- return args.memories;
1584
- }
1585
- const candidateIds = new Set(args.ftsMatches.map((memory) => memory.id));
1586
- if (args.queryModelId && args.queryModelId !== "off") {
1587
- const embeddingProjects = args.workspace?.isWorkspaced ? args.workspace.identities : [args.projectPath];
1588
- for (const projectPath of embeddingProjects) {
1589
- const storedEmbeddings = getProjectEmbeddings(args.db, projectPath, args.queryModelId);
1590
- for (const memoryId of storedEmbeddings.keys()) {
1591
- candidateIds.add(memoryId);
1592
- }
1593
- }
1594
- }
1595
- return args.memories.filter((memory) => candidateIds.has(memory.id));
1596
- }
1597
- function mergeMemoryResults(args) {
1598
- const memoryById = new Map(args.memories.map((memory) => [memory.id, memory]));
1599
- const candidateIds = new Set([...args.semanticScores.keys(), ...args.ftsScores.keys()]);
1600
- const results = [];
1601
- const suppressedVisibleIds = [];
1602
- for (const id of candidateIds) {
1603
- const memory = memoryById.get(id);
1604
- if (!memory) {
1605
- continue;
1606
- }
1607
- const semanticScore = args.semanticScores.get(id);
1608
- const ftsScore = args.ftsScores.get(id);
1609
- let score = 0;
1610
- let matchType = "fts";
1611
- if (semanticScore !== undefined && ftsScore !== undefined) {
1612
- score = SEMANTIC_WEIGHT * semanticScore + FTS_WEIGHT * ftsScore;
1613
- matchType = "hybrid";
1614
- } else if (semanticScore !== undefined) {
1615
- score = semanticScore * SINGLE_SOURCE_PENALTY;
1616
- matchType = "semantic";
1617
- } else if (ftsScore !== undefined) {
1618
- score = ftsScore * SINGLE_SOURCE_PENALTY;
1619
- matchType = "fts";
1620
- }
1621
- if (score <= 0) {
1622
- continue;
1623
- }
1624
- if (args.visibleMemoryIds?.has(id)) {
1625
- suppressedVisibleIds.push(id);
1626
- continue;
1627
- }
1628
- results.push({
1629
- source: "memory",
1630
- content: previewText(memory.content),
1631
- score,
1632
- memoryId: memory.id,
1633
- category: memory.category,
1634
- matchType,
1635
- sourceName: args.sourceNameByMemoryId?.get(memory.id)
1636
- });
1637
- }
1638
- return {
1639
- results: results.sort((left, right) => {
1640
- if (right.score !== left.score) {
1641
- return right.score - left.score;
1642
- }
1643
- return left.memoryId - right.memoryId;
1644
- }).slice(0, args.limit),
1645
- suppressedVisibleIds: suppressedVisibleIds.sort((left, right) => left - right)
1646
- };
1647
- }
1648
- async function searchMemories(args) {
1649
- if (!args.memoryEnabled) {
1650
- return { results: [], suppressedVisibleIds: [] };
1651
- }
1652
- const unfilteredMemories = args.workspace?.isWorkspaced ? getMemoriesByProjects(args.db, args.workspace.expandedIdentities, ["active", "permanent"], Date.now(), args.workspace.ownIdentities, args.workspace.shareCategories) : getMemoriesByProject(args.db, args.projectPath);
1653
- const memories = unfilteredMemories.filter((memory) => timestampIsInRange(memory.createdAt, args.dateRange));
1654
- if (memories.length === 0) {
1655
- return { results: [], suppressedVisibleIds: [] };
1656
- }
1657
- const ftsMatches = getFtsMatches({
1658
- db: args.db,
1659
- projectPath: args.projectPath,
1660
- query: args.query,
1661
- limit: FTS_SEMANTIC_CANDIDATE_LIMIT,
1662
- workspace: args.workspace,
1663
- dateRange: args.dateRange
1664
- });
1665
- const ftsScores = getFtsScores(ftsMatches);
1666
- const semanticCandidates = selectSemanticCandidates({
1667
- db: args.db,
1668
- memories,
1669
- projectPath: args.projectPath,
1670
- ftsMatches,
1671
- queryModelId: args.queryModelId,
1672
- workspace: args.workspace
1673
- });
1674
- const semanticScores = await getSemanticScores({
1675
- db: args.db,
1676
- projectPath: args.projectPath,
1677
- memories: semanticCandidates,
1678
- queryEmbedding: args.queryEmbedding,
1679
- queryModelId: args.queryModelId,
1680
- workspace: args.workspace
1681
- });
1682
- return mergeMemoryResults({
1683
- memories,
1684
- semanticScores,
1685
- ftsScores,
1686
- limit: args.limit,
1687
- visibleMemoryIds: args.visibleMemoryIds,
1688
- sourceNameByMemoryId: sourceNamesForSearchMemories({
1689
- memories,
1690
- projectPath: args.projectPath,
1691
- workspace: args.workspace ?? {
1692
- identities: [args.projectPath],
1693
- expandedIdentities: [args.projectPath],
1694
- namesByIdentity: new Map,
1695
- canonicalIdentityByStoredPath: new Map([[args.projectPath, args.projectPath]]),
1696
- ownIdentities: [args.projectPath],
1697
- shareCategories: null,
1698
- isWorkspaced: false
1699
- }
1700
- })
1701
- });
1702
- }
1703
- function linearDecayScore(rank, total) {
1704
- if (total <= 0)
1705
- return 0;
1706
- return Math.max(0, 1 - rank / total);
1707
- }
1708
- function normalizeMessageSearchRow(row, cutoff) {
1709
- const messageOrdinal = getMessageOrdinal(row.messageOrdinal);
1710
- if (messageOrdinal === null || typeof row.messageId !== "string" || typeof row.role !== "string" || typeof row.content !== "string") {
1711
- return null;
1712
- }
1713
- if (cutoff !== null && messageOrdinal > cutoff)
1714
- return null;
1715
- return {
1716
- messageOrdinal,
1717
- messageId: row.messageId,
1718
- role: row.role,
1719
- content: row.content
1720
- };
1721
- }
1722
- function runMessageFtsQuery(db, sessionId, ftsQuery, fetchLimit, cutoff, dateRange, sessionFirst) {
1723
- if (ftsQuery.length === 0)
1724
- return [];
1725
- let rawRows;
1726
- const matchQuery = contentOnlyMessageQuery(ftsQuery);
1727
- if (dateRange !== null) {
1728
- const bindings = [sessionId, matchQuery, dateRange.from, dateRange.to];
1729
- if (cutoff !== null)
1730
- bindings.push(cutoff);
1731
- bindings.push(fetchLimit);
1732
- rawRows = getMessageSearchStatementWithDateRange(db, cutoff !== null).all(...bindings);
1733
- } else {
1734
- rawRows = cutoff !== null ? getMessageSearchStatementWithCutoff(db, sessionFirst).all(sessionId, matchQuery, cutoff, fetchLimit) : getMessageSearchStatement(db, sessionFirst).all(sessionId, matchQuery, fetchLimit);
1735
- }
1736
- const rows = rawRows.map((row) => row);
1737
- const result = [];
1738
- for (const row of rows) {
1739
- const normalized = normalizeMessageSearchRow(row, cutoff);
1740
- if (normalized)
1741
- result.push(normalized);
1742
- }
1743
- return result;
1744
- }
1745
- function runMessageFtsQueryWithDiagnostics(args) {
1746
- if (args.ftsQuery.length === 0)
1747
- return { rows: [], suppressedCount: 0 };
1748
- const matchQuery = contentOnlyMessageQuery(args.ftsQuery);
1749
- const rawRows = (args.dateRange === null ? getMessageSearchDiagnosticStatement(args.db, args.sessionFirst).all(args.sessionId, matchQuery, args.cutoff, args.fetchLimit, args.cutoff) : getMessageSearchDiagnosticStatementWithDateRange(args.db).all(args.sessionId, matchQuery, args.dateRange.from, args.dateRange.to, args.cutoff, args.fetchLimit, args.cutoff)).map((row) => row);
1750
- const suppressedCount = rawRows[0]?.suppressedCount ?? 0;
1751
- const rows = [];
1752
- for (const row of rawRows) {
1753
- if (row.summaryOnly === 1)
1754
- continue;
1755
- const normalized = normalizeMessageSearchRow(row, args.cutoff);
1756
- if (normalized)
1757
- rows.push(normalized);
1758
- }
1759
- return { rows, suppressedCount };
1760
- }
1761
- function getBatchedMessageSearchStatement(db, queryCount, cutoff, dateRange, sessionFirst) {
1762
- let statements = batchedMessageSearchStatements.get(db);
1763
- if (!statements) {
1764
- statements = new Map;
1765
- batchedMessageSearchStatements.set(db, statements);
1766
- }
1767
- const key = `${queryCount}:${cutoff === null ? "all" : "cutoff"}:${dateRange === null ? "all-dates" : "dated"}:${sessionFirst}`;
1768
- let statement = statements.get(key);
1769
- if (!statement) {
1770
- const cutoffSql = cutoff === null ? "" : " AND CAST(message_history_fts.message_ordinal AS INTEGER) <= ?";
1771
- const joinSql = dateRange === null ? "" : ` JOIN message_fts_rowid_map AS map
1772
- ON map.session_id = message_history_fts.session_id
1773
- AND map.fts_rowid = message_history_fts.rowid`;
1774
- const dateSql = dateRange === null ? "" : " AND map.message_time_ms BETWEEN ? AND ?";
1775
- const branches = Array.from({ length: queryCount }, (_, index) => `SELECT * FROM (
1776
- SELECT ${index} AS queryIndex,
1777
- message_history_fts.message_ordinal AS messageOrdinal,
1778
- message_history_fts.message_id AS messageId,
1779
- message_history_fts.role AS role,
1780
- message_history_fts.content AS content,
1781
- bm25(message_history_fts) AS ftsRank
1782
- FROM message_history_fts${joinSql}
1783
- WHERE ${sessionFirst || dateRange !== null ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}message_history_fts.session_id = ${index === 0 ? "?1" : "?"}
1784
- AND message_history_fts MATCH ?${dateSql}${cutoffSql}
1785
- ORDER BY ftsRank
1786
- LIMIT ?
1787
- )`);
1788
- statement = db.prepare(`${branches.join(`
1789
- UNION ALL
1790
- `)}
1791
- ORDER BY queryIndex ASC, ftsRank ASC`);
1792
- statements.set(key, statement);
1793
- }
1794
- return statement;
1795
- }
1796
- function runMessageFtsQueriesBatch(db, sessionId, ftsQueries, fetchLimit, cutoff, dateRange, sessionFirst) {
1797
- if (ftsQueries.length === 0)
1798
- return [];
1799
- const bindings = [];
1800
- for (const query of ftsQueries) {
1801
- bindings.push(sessionId, contentOnlyMessageQuery(query));
1802
- if (dateRange !== null)
1803
- bindings.push(dateRange.from, dateRange.to);
1804
- if (cutoff !== null)
1805
- bindings.push(cutoff);
1806
- bindings.push(fetchLimit);
1807
- }
1808
- const rows = getBatchedMessageSearchStatement(db, ftsQueries.length, cutoff, dateRange, sessionFirst).all(...bindings);
1809
- const result = Array.from({ length: ftsQueries.length }, () => []);
1810
- for (const row of rows) {
1811
- if (typeof row.queryIndex !== "number" || row.queryIndex < 0 || row.queryIndex >= result.length) {
1812
- continue;
1813
- }
1814
- const normalized = normalizeMessageSearchRow(row, cutoff);
1815
- if (normalized)
1816
- result[row.queryIndex].push(normalized);
1817
- }
1818
- return result;
1819
- }
1820
- var RRF_K = 60;
1821
- var VERBATIM_RANK_BONUS = 1 / RRF_K;
1822
- var IDF_FALLOFF = 100;
1823
- function probeDiscriminationWeight(df, corpusSize) {
1824
- if (corpusSize <= 0 || df <= 0)
1825
- return 1;
1826
- return 1 / (1 + IDF_FALLOFF * df / corpusSize);
1827
- }
1828
- function searchMessages(args) {
1829
- return withMessageFtsSessionFilter(args.db, args.sessionId, (sessionFirst) => searchMessagesInSnapshot(args, sessionFirst));
1830
- }
1831
- function searchMessagesInSnapshot(args, sessionFirst) {
1832
- const cutoff = args.maxOrdinal != null && args.maxOrdinal >= 0 ? args.maxOrdinal : null;
1833
- const fetchLimit = args.maxOrdinal != null && args.maxOrdinal >= 0 ? args.limit * 3 : args.limit;
1834
- const baseQuery = sanitizeFtsQuery(args.query.trim());
1835
- const probes = args.probes ?? [];
1836
- if (probes.length === 0) {
1837
- const outcome = args.diagnostics && cutoff !== null ? runMessageFtsQueryWithDiagnostics({
1838
- db: args.db,
1839
- sessionId: args.sessionId,
1840
- ftsQuery: baseQuery,
1841
- fetchLimit,
1842
- cutoff,
1843
- dateRange: args.dateRange,
1844
- sessionFirst
1845
- }) : {
1846
- rows: runMessageFtsQuery(args.db, args.sessionId, baseQuery, fetchLimit, cutoff, args.dateRange, sessionFirst),
1847
- suppressedCount: 0
1848
- };
1849
- if (args.diagnostics) {
1850
- args.diagnostics.suppressedLiveMessageMatches = outcome.suppressedCount;
1851
- }
1852
- const rows = outcome.rows.length > 0 || !args.relaxedRecall ? outcome.rows : runMessageFtsQuery(args.db, args.sessionId, relaxedFtsQuery(args.query), fetchLimit, cutoff, args.dateRange, sessionFirst);
1853
- const filtered = rows.slice(0, args.limit);
1854
- return filtered.map((row, rank) => ({
1855
- source: "message",
1856
- content: previewText(row.content),
1857
- score: linearDecayScore(rank, filtered.length),
1858
- messageOrdinal: row.messageOrdinal,
1859
- messageId: row.messageId,
1860
- role: row.role
1861
- }));
1862
- }
1863
- const sanitizedProbes = probes.map((probe) => ({ probe, query: sanitizeFtsQuery(probe) })).filter((entry) => entry.query.length > 0);
1864
- const corpusSize = getIndexedMessageCorpusSize(args.db, args.sessionId, cutoff);
1865
- const probeCounts = countSessionFtsMatchesBatch(args.db, args.sessionId, sanitizedProbes.map((entry) => entry.query), cutoff, args.dateRange, sessionFirst);
1866
- const collectBaseDiagnostics = args.diagnostics !== undefined && cutoff !== null;
1867
- const baseOutcome = collectBaseDiagnostics && baseQuery.length > 0 ? runMessageFtsQueryWithDiagnostics({
1868
- db: args.db,
1869
- sessionId: args.sessionId,
1870
- ftsQuery: baseQuery,
1871
- fetchLimit,
1872
- cutoff,
1873
- dateRange: args.dateRange,
1874
- sessionFirst
1875
- }) : null;
1876
- if (args.diagnostics) {
1877
- args.diagnostics.suppressedLiveMessageMatches = baseOutcome?.suppressedCount ?? 0;
1878
- }
1879
- const searchQueries = [
1880
- ...!collectBaseDiagnostics && baseQuery.length > 0 ? [baseQuery] : [],
1881
- ...sanitizedProbes.map((entry) => entry.query)
1882
- ];
1883
- const rowsByQuery = runMessageFtsQueriesBatch(args.db, args.sessionId, searchQueries, fetchLimit, cutoff, args.dateRange, sessionFirst);
1884
- const queryLists = [];
1885
- let queryIndex = 0;
1886
- if (baseQuery.length > 0) {
1887
- queryLists.push({
1888
- rows: baseOutcome?.rows ?? rowsByQuery[queryIndex] ?? [],
1889
- weight: 1
1890
- });
1891
- if (!collectBaseDiagnostics)
1892
- queryIndex += 1;
1893
- }
1894
- const probeWeights = new Map;
1895
- sanitizedProbes.forEach((entry, probeIndex) => {
1896
- const weight = probeDiscriminationWeight(probeCounts[probeIndex] ?? 0, corpusSize);
1897
- probeWeights.set(entry.probe, weight);
1898
- queryLists.push({ rows: rowsByQuery[queryIndex] ?? [], weight });
1899
- queryIndex += 1;
1900
- });
1901
- const fused = new Map;
1902
- for (const list of queryLists) {
1903
- list.rows.forEach((row, rank) => {
1904
- const rrf = list.weight / (RRF_K + rank);
1905
- const existing = fused.get(row.messageId);
1906
- if (existing) {
1907
- existing.score += rrf;
1908
- } else {
1909
- fused.set(row.messageId, { row, score: rrf });
1910
- }
1911
- });
1912
- }
1913
- for (const entry of fused.values()) {
1914
- let best = 0;
1915
- for (const probe of probes) {
1916
- const weight = probeWeights.get(probe) ?? 0;
1917
- if (weight > best && containsProbeVerbatim(entry.row.content, [probe])) {
1918
- best = weight;
1919
- }
1920
- }
1921
- if (best > 0) {
1922
- entry.score += best * VERBATIM_RANK_BONUS;
1923
- }
1924
- }
1925
- const ranked = [...fused.values()].sort((a, b) => b.score !== a.score ? b.score - a.score : a.row.messageOrdinal - b.row.messageOrdinal).slice(0, args.limit);
1926
- return ranked.map((entry, rank) => ({
1927
- source: "message",
1928
- content: previewText(entry.row.content),
1929
- score: linearDecayScore(rank, ranked.length),
1930
- messageOrdinal: entry.row.messageOrdinal,
1931
- messageId: entry.row.messageId,
1932
- role: entry.row.role
1933
- }));
1934
- }
1935
- var NOTE_SEARCHABLE_STATUSES = ["active", "pending", "ready"];
1936
- var MAX_NOTE_KEYWORD_SCORE = 3.5;
1937
- function noteSearchText(note) {
1938
- const reason = note.readyReason?.trim();
1939
- return reason ? `${note.content}
1940
- Reason: ${reason}` : note.content;
1941
- }
1942
- function tokenizeKeywordNeedle(text) {
1943
- const matches = text.toLowerCase().match(/[a-z0-9/._:-]+/g) ?? [];
1944
- const seen = new Set;
1945
- const tokens = [];
1946
- for (const match of matches) {
1947
- if (match.length <= 1 || !/[a-z0-9]/.test(match) || seen.has(match)) {
1948
- continue;
1949
- }
1950
- seen.add(match);
1951
- tokens.push(match);
1952
- }
1953
- return tokens;
1954
- }
1955
- function normalizeNoteKeywordScore(score) {
1956
- return normalizeCosineScore(score / MAX_NOTE_KEYWORD_SCORE) * SINGLE_SOURCE_PENALTY;
1957
- }
1958
- function rankNotesForNeedle(notes, needle) {
1959
- const normalizedNeedle = needle.trim().toLowerCase();
1960
- if (normalizedNeedle.length === 0) {
1961
- return [];
1962
- }
1963
- const needleTokens = tokenizeKeywordNeedle(normalizedNeedle);
1964
- const ranked = [];
1965
- for (const note of notes) {
1966
- const text = noteSearchText(note);
1967
- const normalizedText = text.toLowerCase();
1968
- const noteTokens = new Set(tokenizeKeywordNeedle(normalizedText));
1969
- const exact = normalizedText.includes(normalizedNeedle);
1970
- const matchedTokens = needleTokens.filter((token) => noteTokens.has(token)).length;
1971
- if (!exact && matchedTokens === 0) {
1972
- continue;
1973
- }
1974
- const matchedUniqueTokens = new Set(needleTokens.filter((token) => noteTokens.has(token))).size;
1975
- const coverage = needleTokens.length > 0 ? matchedTokens / needleTokens.length : 0;
1976
- const density = noteTokens.size > 0 ? matchedUniqueTokens / noteTokens.size : 0;
1977
- const exactPhrase = exact && (needleTokens.length > 1 || normalizedText.trim() === normalizedNeedle);
1978
- const allTokens = needleTokens.length > 1 && matchedTokens === needleTokens.length;
1979
- const score = (exactPhrase ? 2 : 0) + coverage * density + (allTokens ? 0.5 * density : 0);
1980
- ranked.push({ note, score, text });
1981
- }
1982
- return ranked.sort((left, right) => {
1983
- if (right.score !== left.score) {
1984
- return right.score - left.score;
1985
- }
1986
- if (right.note.createdAt !== left.note.createdAt) {
1987
- return right.note.createdAt - left.note.createdAt;
1988
- }
1989
- return left.note.id - right.note.id;
1990
- });
1991
- }
1992
- function searchNotes(args) {
1993
- if (args.limit <= 0) {
1994
- return [];
1995
- }
1996
- const notes = [
1997
- ...getNotes(args.db, {
1998
- sessionId: args.sessionId,
1999
- type: "session",
2000
- status: NOTE_SEARCHABLE_STATUSES
2001
- }),
2002
- ...getNotes(args.db, {
2003
- projectPath: args.projectPath,
2004
- type: "smart",
2005
- status: NOTE_SEARCHABLE_STATUSES
2006
- })
2007
- ].filter((note) => timestampIsInRange(note.createdAt, args.dateRange));
2008
- if (notes.length === 0) {
2009
- return [];
2010
- }
2011
- const baseList = rankNotesForNeedle(notes, args.query);
2012
- const probes = args.probes ?? [];
2013
- if (probes.length === 0) {
2014
- const ranked = baseList.slice(0, args.limit);
2015
- return ranked.map((entry) => ({
2016
- source: "note",
2017
- content: previewText(entry.text),
2018
- score: normalizeNoteKeywordScore(entry.score),
2019
- noteId: entry.note.id,
2020
- status: entry.note.status,
2021
- createdAt: entry.note.createdAt,
2022
- anchorOrdinal: entry.note.anchorOrdinal,
2023
- sourceSessionId: entry.note.sessionId
2024
- }));
2025
- }
2026
- const queryLists = [];
2027
- if (baseList.length > 0) {
2028
- queryLists.push({ rows: baseList, weight: 1 });
2029
- }
2030
- for (const probe of probes) {
2031
- const rows = rankNotesForNeedle(notes, probe);
2032
- if (rows.length === 0) {
2033
- continue;
2034
- }
2035
- const weight = probeDiscriminationWeight(rows.length, notes.length);
2036
- queryLists.push({ rows, weight });
2037
- }
2038
- const fused = new Map;
2039
- for (const list of queryLists) {
2040
- for (const row of list.rows) {
2041
- const relevance = row.score * list.weight;
2042
- const existing = fused.get(row.note.id);
2043
- if (existing) {
2044
- if (relevance > existing.score) {
2045
- existing.entry = row;
2046
- existing.score = relevance;
2047
- }
2048
- } else {
2049
- fused.set(row.note.id, { entry: row, score: relevance });
2050
- }
2051
- }
2052
- }
2053
- const ranked = [...fused.values()].sort((left, right) => {
2054
- if (right.score !== left.score) {
2055
- return right.score - left.score;
2056
- }
2057
- if (right.entry.note.createdAt !== left.entry.note.createdAt) {
2058
- return right.entry.note.createdAt - left.entry.note.createdAt;
2059
- }
2060
- return left.entry.note.id - right.entry.note.id;
2061
- }).slice(0, args.limit);
2062
- return ranked.map((entry) => ({
2063
- source: "note",
2064
- content: previewText(entry.entry.text),
2065
- score: normalizeNoteKeywordScore(entry.score),
2066
- noteId: entry.entry.note.id,
2067
- status: entry.entry.note.status,
2068
- createdAt: entry.entry.note.createdAt,
2069
- anchorOrdinal: entry.entry.note.anchorOrdinal,
2070
- sourceSessionId: entry.entry.note.sessionId
2071
- }));
2072
- }
2073
- function searchCompartmentChunks(args) {
2074
- if (!args.queryEmbedding || args.limit <= 0 || !args.modelId || args.modelId === "off")
2075
- return [];
2076
- const cutoff = args.maxOrdinal != null && args.maxOrdinal >= 0 ? args.maxOrdinal : null;
2077
- const rows = loadCompartmentChunkEmbeddingsForSearch(args.db, args.sessionId, args.projectPath, args.modelId, args.dateRange);
2078
- if (rows.length === 0)
2079
- return [];
2080
- const byCompartment = new Map;
2081
- for (const row of rows) {
2082
- if (cutoff !== null && row.endOrdinal > cutoff) {
2083
- continue;
2084
- }
2085
- const score = normalizeCosineScore(cosineSimilarity(args.queryEmbedding, row.vector));
2086
- if (score <= 0)
2087
- continue;
2088
- const existing = byCompartment.get(row.compartmentId);
2089
- if (!existing || score > existing.score) {
2090
- byCompartment.set(row.compartmentId, { row, score });
2091
- }
2092
- }
2093
- return [...byCompartment.values()].sort((left, right) => right.score !== left.score ? right.score - left.score : left.row.startOrdinal - right.row.startOrdinal).slice(0, args.limit).map(({ row, score }) => ({
2094
- source: "compartment",
2095
- content: previewText(row.title),
2096
- score: score * SINGLE_SOURCE_PENALTY,
2097
- compartmentId: row.compartmentId,
2098
- sessionId: row.sessionId,
2099
- title: row.title,
2100
- startOrdinal: row.startOrdinal,
2101
- endOrdinal: row.endOrdinal,
2102
- matchType: "semantic"
2103
- }));
2104
- }
2105
- function mergeMessageAndCompartmentResults(args) {
2106
- if (args.compartments.length === 0)
2107
- return args.messages;
2108
- if (args.messages.length === 0)
2109
- return args.compartments;
2110
- const fused = new Map;
2111
- const add = (key, result, score, tieOrdinal) => {
2112
- const existing = fused.get(key);
2113
- if (existing) {
2114
- existing.score += score;
2115
- return existing;
2116
- }
2117
- const entry = { result, score, tieOrdinal, snippetScore: -1 };
2118
- fused.set(key, entry);
2119
- return entry;
2120
- };
2121
- args.compartments.forEach((compartment, rank) => {
2122
- add(`compartment:${compartment.compartmentId}`, compartment, 1 / (RRF_K + rank), compartment.startOrdinal);
2123
- });
2124
- for (const [rank, message] of args.messages.entries()) {
2125
- const containing = args.compartments.find((compartment) => message.messageOrdinal >= compartment.startOrdinal && message.messageOrdinal <= compartment.endOrdinal);
2126
- const contribution = 1 / (RRF_K + rank);
2127
- if (!containing) {
2128
- add(`message:${message.messageId}`, message, contribution, message.messageOrdinal);
2129
- continue;
2130
- }
2131
- const entry = add(`compartment:${containing.compartmentId}`, containing, contribution, containing.startOrdinal);
2132
- if (message.score > entry.snippetScore && entry.result.source === "compartment") {
2133
- entry.snippetScore = message.score;
2134
- entry.result = {
2135
- ...entry.result,
2136
- matchType: "hybrid",
2137
- snippet: message.content
2138
- };
2139
- }
2140
- }
2141
- const ranked = [...fused.values()].sort((left, right) => right.score !== left.score ? right.score - left.score : left.tieOrdinal - right.tieOrdinal).slice(0, args.limit);
2142
- return ranked.map((entry, rank) => ({
2143
- ...entry.result,
2144
- score: linearDecayScore(rank, ranked.length)
2145
- }));
2146
- }
2147
- function getSourceBoost(result) {
2148
- switch (result.source) {
2149
- case "memory":
2150
- return MEMORY_SOURCE_BOOST;
2151
- case "message":
2152
- case "compartment":
2153
- return MESSAGE_SOURCE_BOOST;
2154
- case "git_commit":
2155
- return GIT_COMMIT_SOURCE_BOOST;
2156
- case "primer":
2157
- return PRIMER_SOURCE_BOOST;
2158
- case "note":
2159
- return 1;
2160
- }
2161
- }
2162
- function compareUnifiedResults(left, right) {
2163
- const leftEffective = left.score * getSourceBoost(left);
2164
- const rightEffective = right.score * getSourceBoost(right);
2165
- if (rightEffective !== leftEffective) {
2166
- return rightEffective - leftEffective;
2167
- }
2168
- if (left.source === "memory" && right.source === "memory") {
2169
- return left.memoryId - right.memoryId;
2170
- }
2171
- if (left.source === "message" && right.source === "message") {
2172
- return left.messageOrdinal - right.messageOrdinal;
2173
- }
2174
- if (left.source === "compartment" && right.source === "compartment") {
2175
- return left.startOrdinal - right.startOrdinal;
2176
- }
2177
- if (left.source === "git_commit" && right.source === "git_commit") {
2178
- return right.committedAtMs - left.committedAtMs;
2179
- }
2180
- if (left.source === "primer" && right.source === "primer") {
2181
- return right.support - left.support || left.primerId - right.primerId;
2182
- }
2183
- if (left.source === "note" && right.source === "note") {
2184
- return right.createdAt - left.createdAt || left.noteId - right.noteId;
2185
- }
2186
- return 0;
2187
- }
2188
- function toGitCommitResult(hit) {
2189
- return {
2190
- source: "git_commit",
2191
- content: previewText(hit.commit.message),
2192
- score: hit.score,
2193
- sha: hit.commit.sha,
2194
- shortSha: hit.commit.shortSha,
2195
- author: hit.commit.author,
2196
- committedAtMs: hit.commit.committedAtMs,
2197
- matchType: hit.matchType
2198
- };
2199
- }
2200
- function searchGitCommits(args) {
2201
- if (args.limit <= 0)
2202
- return [];
2203
- const hits = searchGitCommitsSync(args.db, args.projectPath, args.query, {
2204
- limit: args.limit,
2205
- queryEmbedding: args.queryEmbedding,
2206
- queryModelId: args.queryModelId,
2207
- from: args.dateRange?.from,
2208
- to: args.dateRange?.to
2209
- });
2210
- return hits.map(toGitCommitResult);
2211
- }
2212
- function primerText(primer) {
2213
- const answer = primer.answer.trim();
2214
- return answer ? `Q: ${primer.question}
2215
- A: ${answer}` : `Q: ${primer.question}`;
2216
- }
2217
- function searchPrimers(args) {
2218
- const primers = getActivePrimers(args.db, args.projectPath).filter((primer) => timestampIsInRange(primer.createdAt, args.dateRange));
2219
- if (primers.length === 0 || args.limit <= 0)
2220
- return [];
2221
- const ftsQuery = sanitizeFtsQuery(args.query);
2222
- const ftsRanks = new Map;
2223
- if (ftsQuery) {
2224
- const rows = args.db.prepare(`SELECT p.id AS id, bm25(primers_fts) AS rank
2225
- FROM primers_fts
2226
- JOIN primers p ON p.id = primers_fts.rowid
2227
- WHERE primers_fts MATCH ? AND p.project_path = ? AND p.status = 'active'
2228
- ${args.dateRange === null ? "" : "AND p.created_at BETWEEN ? AND ?"}
2229
- ORDER BY rank ASC
2230
- LIMIT ?`).all(ftsQuery, args.projectPath, ...args.dateRange === null ? [] : [args.dateRange.from, args.dateRange.to], args.limit * 3);
2231
- rows.forEach((row, index) => {
2232
- ftsRanks.set(row.id, linearDecayScore(index, rows.length));
2233
- });
2234
- }
2235
- const scored = primers.map((primer) => {
2236
- const semantic = args.queryEmbedding && primer.questionEmbedding && primer.questionEmbeddingModelId === args.queryModelId ? normalizeCosineScore(cosineSimilarity(args.queryEmbedding, primer.questionEmbedding)) : 0;
2237
- const fts = ftsRanks.get(primer.id) ?? 0;
2238
- if (semantic <= 0 && fts <= 0)
2239
- return null;
2240
- const score = semantic > 0 && fts > 0 ? semantic * SEMANTIC_WEIGHT + fts * FTS_WEIGHT : Math.max(semantic, fts);
2241
- return {
2242
- source: "primer",
2243
- content: previewText(primerText(primer)),
2244
- score,
2245
- primerId: primer.id,
2246
- question: primer.question,
2247
- support: primer.totalSupport,
2248
- lastObservedAt: primer.lastObservedAt,
2249
- matchType: semantic > 0 && fts > 0 ? "hybrid" : semantic > 0 ? "semantic" : "fts"
2250
- };
2251
- }).filter((result) => result !== null).sort((a, b) => b.score - a.score || b.support - a.support || a.primerId - b.primerId).slice(0, args.limit);
2252
- return scored;
2253
- }
2254
- function resolveSources(sources) {
2255
- if (sources === undefined) {
2256
- return new Set(["memory", "message", "git_commit", "primer", "note"]);
2257
- }
2258
- const set = new Set;
2259
- for (const source of sources) {
2260
- if (source === "memory" || source === "message" || source === "git_commit" || source === "primer" || source === "note") {
2261
- set.add(source);
2262
- }
2263
- }
2264
- return set;
2265
- }
2266
- function memoriesToIdLookupResults(args) {
2267
- const ordered = args.memories.slice(0, args.limit);
2268
- return ordered.map((memory, rank) => ({
2269
- source: "memory",
2270
- content: previewText(memory.content),
2271
- score: 1 - rank * 0.01,
2272
- memoryId: memory.id,
2273
- category: memory.category,
2274
- matchType: "fts",
2275
- sourceName: args.sourceNameByMemoryId?.get(memory.id)
2276
- }));
2277
- }
2278
- function resolveMemoriesByIdsForSearch(args) {
2279
- if (args.diagnostics) {
2280
- args.diagnostics.suppressedVisibleMemoryIds = [];
2281
- }
2282
- if (args.ids.length === 0) {
2283
- return null;
2284
- }
2285
- const workspace = resolveSearchWorkspaceContext(args.db, args.projectPath);
2286
- const fetched = workspace.isWorkspaced ? getMemoriesByProjects(args.db, workspace.expandedIdentities, ["active", "permanent", "archived"], Date.now(), workspace.ownIdentities, workspace.shareCategories) : getMemoriesByProject(args.db, args.projectPath, ["active", "permanent", "archived"]);
2287
- if (fetched.length === 0) {
2288
- return null;
2289
- }
2290
- const dateRange = normalizeDateRange(args.from, args.to);
2291
- const memoriesById = new Map(fetched.filter((memory) => timestampIsInRange(memory.createdAt, dateRange)).map((memory) => [memory.id, memory]));
2292
- const ordered = [];
2293
- const suppressedVisibleIds = new Set;
2294
- for (const id of args.ids) {
2295
- const memory = memoriesById.get(id);
2296
- if (!memory)
2297
- continue;
2298
- if (args.visibleMemoryIds?.has(id)) {
2299
- suppressedVisibleIds.add(id);
2300
- continue;
2301
- }
2302
- ordered.push(memory);
2303
- if (ordered.length >= args.limit)
2304
- break;
2305
- }
2306
- if (args.diagnostics) {
2307
- args.diagnostics.suppressedVisibleMemoryIds = [...suppressedVisibleIds].sort((left, right) => left - right);
2308
- }
2309
- if (ordered.length === 0) {
2310
- return null;
2311
- }
2312
- return memoriesToIdLookupResults({
2313
- memories: ordered,
2314
- limit: args.limit,
2315
- sourceNameByMemoryId: sourceNamesForSearchMemories({
2316
- memories: ordered,
2317
- projectPath: args.projectPath,
2318
- workspace
2319
- })
2320
- });
2321
- }
2322
- async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
2323
- const trimmedQuery = query.trim();
2324
- const measurementStartedAt = Date.now();
2325
- if (trimmedQuery.length === 0 || options.signal?.aborted) {
2326
- return [];
2327
- }
2328
- const limit = normalizeLimit(options.limit);
2329
- const dateRange = normalizeDateRange(options.from, options.to);
2330
- const tierLimit = Math.max(limit * 3, DEFAULT_UNIFIED_SEARCH_LIMIT);
2331
- if (options.diagnostics) {
2332
- options.diagnostics.suppressedVisibleMemoryIds = [];
2333
- options.diagnostics.suppressedLiveMessageMatches = 0;
2334
- options.diagnostics.gitCommitUnavailable = null;
2335
- }
2336
- const embeddingEnabled = options.embeddingEnabled ?? true;
2337
- const embedQuery = options.embedQuery ?? embedText;
2338
- const isEmbeddingRuntimeEnabled = options.isEmbeddingRuntimeEnabled ?? isEmbeddingEnabled;
2339
- const gitCommitsEnabled = options.gitCommitsEnabled ?? false;
2340
- const activeSources = resolveSources(options.sources);
2341
- const memoryFeatureEnabled = options.memoryEnabled ?? true;
2342
- const runMemory = activeSources.has("memory") && memoryFeatureEnabled;
2343
- const runMessages = activeSources.has("message");
2344
- const runGitCommits = activeSources.has("git_commit") && gitCommitsEnabled;
2345
- if (options.diagnostics && activeSources.has("git_commit") && options.gitRepositoryAvailable === false) {
2346
- options.diagnostics.gitCommitUnavailable = "no_git_repository";
2347
- }
2348
- const runPrimers = activeSources.has("primer") && memoryFeatureEnabled;
2349
- const runNotes = activeSources.has("note");
2350
- const runCompartmentChunks = runMessages && embeddingEnabled;
2351
- const needsEmbedding = (runMemory || runGitCommits || runCompartmentChunks || runPrimers) && embeddingEnabled && isEmbeddingRuntimeEnabled();
2352
- const queryEmbeddingPromise = needsEmbedding ? embedQuery(trimmedQuery, options.signal).catch((error) => {
2353
- log(`[search] query embedding failed: ${error instanceof Error ? error.message : String(error)}`);
2354
- return null;
2355
- }) : Promise.resolve(null);
2356
- await Promise.resolve();
2357
- if (options.signal?.aborted)
2358
- return [];
2359
- const messageProbes = options.explicitSearch ? extractLiteralProbes(trimmedQuery) : [];
2360
- const messageResults = runMessages ? searchMessages({
2361
- db,
2362
- sessionId,
2363
- query: trimmedQuery,
2364
- limit: tierLimit,
2365
- maxOrdinal: options.maxMessageOrdinal,
2366
- probes: messageProbes,
2367
- relaxedRecall: options.explicitSearch,
2368
- diagnostics: options.diagnostics,
2369
- dateRange
2370
- }) : [];
2371
- const capturedQuery = await queryEmbeddingPromise;
2372
- if (options.signal?.aborted)
2373
- return [];
2374
- const embeddingSnapshot = getProjectEmbeddingSnapshot(projectPath);
2375
- const queryContract = capturedQuery instanceof Float32Array || capturedQuery === null ? null : capturedQuery;
2376
- const generationIsCurrent = queryContract === null || embeddingSnapshot !== null && embeddingSnapshot.generation === queryContract.generation;
2377
- const queryEmbedding = generationIsCurrent ? queryContract?.vector ?? (capturedQuery instanceof Float32Array ? capturedQuery : null) : null;
2378
- const workspace = resolveSearchWorkspaceContext(db, projectPath);
2379
- const embeddingModelId = queryContract?.modelId ?? options.embeddingModelIdOverride ?? embeddingSnapshot?.modelId;
2380
- const chunkModelId = queryContract?.chunkModelId ?? options.chunkModelIdOverride ?? embeddingSnapshot?.chunkModelId;
2381
- const compartmentResults = runCompartmentChunks ? searchCompartmentChunks({
2382
- db,
2383
- sessionId,
2384
- projectPath,
2385
- queryEmbedding,
2386
- limit: tierLimit,
2387
- maxOrdinal: options.maxMessageOrdinal,
2388
- modelId: chunkModelId && chunkModelId !== "off" ? chunkModelId : null,
2389
- dateRange
2390
- }) : [];
2391
- const messageLikeResults = mergeMessageAndCompartmentResults({
2392
- messages: messageResults,
2393
- compartments: compartmentResults,
2394
- limit: tierLimit
2395
- });
2396
- const [memoryOutcome, gitCommitResults, primerResults, noteResults] = await Promise.all([
2397
- runMemory ? searchMemories({
2398
- db,
2399
- projectPath,
2400
- query: trimmedQuery,
2401
- limit: tierLimit,
2402
- memoryEnabled: true,
2403
- queryEmbedding,
2404
- queryModelId: embeddingModelId && embeddingModelId !== "off" ? embeddingModelId : null,
2405
- workspace,
2406
- visibleMemoryIds: options.visibleMemoryIds,
2407
- dateRange
2408
- }) : Promise.resolve({
2409
- results: [],
2410
- suppressedVisibleIds: []
2411
- }),
2412
- runGitCommits ? Promise.resolve(searchGitCommits({
2413
- db,
2414
- projectPath,
2415
- query: trimmedQuery,
2416
- limit: tierLimit,
2417
- queryEmbedding,
2418
- queryModelId: embeddingModelId && embeddingModelId !== "off" ? embeddingModelId : null,
2419
- dateRange
2420
- })) : Promise.resolve([]),
2421
- runPrimers ? Promise.resolve(searchPrimers({
2422
- db,
2423
- projectPath,
2424
- query: trimmedQuery,
2425
- limit: tierLimit,
2426
- queryEmbedding,
2427
- queryModelId: embeddingModelId && embeddingModelId !== "off" ? embeddingModelId : null,
2428
- dateRange
2429
- })) : Promise.resolve([]),
2430
- runNotes ? Promise.resolve(searchNotes({
2431
- db,
2432
- sessionId,
2433
- projectPath,
2434
- query: trimmedQuery,
2435
- limit: tierLimit,
2436
- probes: messageProbes,
2437
- dateRange
2438
- })) : Promise.resolve([])
2439
- ]);
2440
- if (options.diagnostics) {
2441
- options.diagnostics.suppressedVisibleMemoryIds = memoryOutcome.suppressedVisibleIds;
2442
- }
2443
- const results = [
2444
- ...memoryOutcome.results,
2445
- ...primerResults,
2446
- ...messageLikeResults,
2447
- ...gitCommitResults,
2448
- ...noteResults
2449
- ].sort(compareUnifiedResults).slice(0, limit);
2450
- if (!options.measurementDisabled) {
2451
- recordShadowMeasurement({
2452
- db,
2453
- sessionId,
2454
- projectPath,
2455
- query: trimmedQuery,
2456
- options,
2457
- primaryResults: results,
2458
- primaryQuery: queryContract,
2459
- primaryLatencyMs: Date.now() - measurementStartedAt,
2460
- search: unifiedSearch
2461
- });
2462
- }
2463
- const countRetrievals = options.countRetrievals ?? true;
2464
- if (countRetrievals) {
2465
- const memoryIds = results.filter((result) => result.source === "memory").map((result) => result.memoryId);
2466
- if (memoryIds.length > 0) {
2467
- db.transaction(() => {
2468
- for (const memoryId of memoryIds) {
2469
- try {
2470
- updateMemoryRetrievalCount(db, memoryId);
2471
- } catch (error) {
2472
- if (error instanceof ModuleMemoryAuthorityError)
2473
- continue;
2474
- throw error;
2475
- }
2476
- }
2477
- }).immediate();
2478
- }
2479
- }
2480
- return results;
2481
- }
2482
- var SEARCH_NOTE_EXPAND_HINT = "Use ctx_expand(start=N-10, end=N) around any note @msg anchor above to read the surrounding conversation context.";
2483
- function formatSearchAge(timestampMs) {
2484
- const ageMs = Date.now() - timestampMs;
2485
- if (ageMs < 0)
2486
- return "future";
2487
- const days = Math.floor(ageMs / (24 * 60 * 60 * 1000));
2488
- if (days <= 0)
2489
- return "today";
2490
- if (days === 1)
2491
- return "1d ago";
2492
- if (days < 30)
2493
- return `${days}d ago`;
2494
- const months = Math.floor(days / 30);
2495
- if (months === 1)
2496
- return "1mo ago";
2497
- if (months < 12)
2498
- return `${months}mo ago`;
2499
- const years = Math.floor(days / 365);
2500
- return years === 1 ? "1y ago" : `${years}y ago`;
2501
- }
2502
- function formatUnifiedSearchResult(result, index, currentSessionId) {
2503
- if (result.source === "memory") {
2504
- const source = result.sourceName ? ` source=${result.sourceName}` : "";
2505
- return [
2506
- `[${index}] [memory] score=${result.score.toFixed(2)} id=${result.memoryId} category=${result.category}${source} match=${result.matchType}`,
2507
- result.content
2508
- ].join(`
2509
- `);
2510
- }
2511
- if (result.source === "git_commit") {
2512
- return [
2513
- `[${index}] [git_commit] score=${result.score.toFixed(2)} sha=${result.shortSha} ${formatSearchAge(result.committedAtMs)} match=${result.matchType}`,
2514
- result.content
2515
- ].join(`
2516
- `);
2517
- }
2518
- if (result.source === "primer") {
2519
- return [
2520
- `[${index}] [primer] score=${result.score.toFixed(2)} id=${result.primerId} support=${result.support} match=${result.matchType}`,
2521
- result.content
2522
- ].join(`
2523
- `);
2524
- }
2525
- if (result.source === "note") {
2526
- const anchor = result.anchorOrdinal !== null && result.sourceSessionId === currentSessionId ? ` @msg ${result.anchorOrdinal}` : "";
2527
- return [
2528
- `[${index}] [note] score=${result.score.toFixed(2)} id=#${result.noteId} status=${result.status} ${formatSearchAge(result.createdAt)}${anchor}`,
2529
- result.content
2530
- ].join(`
2531
- `);
2532
- }
2533
- if (result.source === "compartment") {
2534
- return [
2535
- `[${index}] [message] score=${result.score.toFixed(2)} compartment_id=${result.compartmentId} range=${result.startOrdinal}-${result.endOrdinal} match=${result.matchType} title=${result.title}`,
2536
- result.snippet ? `Snippet: ${result.snippet}` : result.content
2537
- ].join(`
2538
- `);
2539
- }
2540
- const expandStart = Math.max(1, result.messageOrdinal - 3);
2541
- const expandEnd = result.messageOrdinal + 3;
2542
- return [
2543
- `[${index}] [message] score=${result.score.toFixed(2)} ordinal=${result.messageOrdinal} range=${expandStart}-${expandEnd} role=${result.role}`,
2544
- result.content
2545
- ].join(`
2546
- `);
2547
- }
2548
- function formatSearchDiagnosticLines(results, diagnostics) {
2549
- if (!diagnostics)
2550
- return [];
2551
- const lines = [];
2552
- const visibleIds = [...diagnostics.suppressedVisibleMemoryIds].sort((left, right) => left - right);
2553
- if (visibleIds.length > 0) {
2554
- const count = visibleIds.length;
2555
- const noun = count === 1 ? "match" : "matches";
2556
- const ids = visibleIds.join(", ");
2557
- if (results.some((result) => result.source === "memory")) {
2558
- lines.push(`Memories: ${count} additional ${noun} suppressed because ${count === 1 ? "it is" : "they are"} already visible in your project-memory block (ids ${ids}).`);
2559
- } else {
2560
- lines.push(`Memories: ${count} ${noun} found, all already visible in your project-memory block (ids ${ids}).`);
2561
- }
2562
- }
2563
- if (diagnostics.suppressedLiveMessageMatches > 0) {
2564
- const count = diagnostics.suppressedLiveMessageMatches;
2565
- lines.push(`Message history: ${count} raw-message ${count === 1 ? "match is" : "matches are"} newer than the last compartment boundary (already in your context).`);
2566
- }
2567
- if (diagnostics.gitCommitUnavailable === "no_git_repository") {
2568
- lines.push("Git commits: no git repository — commit search unavailable for this project.");
2569
- }
2570
- return lines;
2571
- }
2572
- function formatSearchResults(query, results, currentSessionId, diagnostics) {
2573
- const diagnosticLines = formatSearchDiagnosticLines(results, diagnostics);
2574
- if (results.length === 0) {
2575
- if (diagnosticLines.length > 0) {
2576
- return `No hidden results found for "${query}".
2577
-
2578
- ${diagnosticLines.join(`
2579
- `)}`;
556
+ const tags = preloadedTags ?? getDroppedTagsByNumbers(db, sessionId, [...targets.keys()]);
557
+ for (const tag of tags) {
558
+ if (tag.status === "dropped") {
559
+ const target = targets.get(tag.tagNumber);
560
+ if (tag.type === "tool") {
561
+ if (tag.dropMode === "edit_marker_stripped") {
562
+ const result = target?.editMarkerStripped?.() ?? "absent";
563
+ if (result === "truncated")
564
+ didMutateMessage = true;
565
+ } else if (tag.dropMode === "edit_marker") {
566
+ const markResult = target?.editMarker?.() ?? "absent";
567
+ if (markResult === "truncated") {
568
+ didMutateMessage = true;
569
+ }
570
+ } else if (tag.dropMode === "skeleton_stripped") {
571
+ const result = target?.skeletonStripped?.() ?? "absent";
572
+ if (result === "truncated")
573
+ didMutateMessage = true;
574
+ } else if (tag.dropMode === "skeleton_real") {
575
+ const result = target?.skeletonReal?.() ?? "absent";
576
+ if (result === "truncated") {
577
+ didMutateMessage = true;
578
+ }
579
+ } else if (tag.dropMode === "truncated") {
580
+ const truncResult = target?.truncate?.() ?? "absent";
581
+ if (truncResult === "truncated") {
582
+ didMutateMessage = true;
583
+ }
584
+ } else {
585
+ const dropResult = target?.drop?.() ?? "absent";
586
+ if (dropResult === "removed" || dropResult === "truncated") {
587
+ didMutateMessage = true;
588
+ }
589
+ }
590
+ } else if (target) {
591
+ const changed = target.setContent(buildReplacementContent(tag.tagNumber));
592
+ if (changed)
593
+ didMutateMessage = true;
594
+ }
2580
595
  }
2581
- return `No results found for "${query}" across notes, memories, primers, git commits, or message history.`;
2582
- }
2583
- const bodyParts = results.map((result, index) => formatUnifiedSearchResult(result, index + 1, currentSessionId));
2584
- if (diagnosticLines.length > 0)
2585
- bodyParts.push(diagnosticLines.join(`
2586
- `));
2587
- if (results.some((result) => result.source === "message" || result.source === "compartment")) {
2588
- bodyParts.push("Use ctx_expand(start, end) with the range from any message result above to read the full conversation context.");
2589
596
  }
2590
- if (results.some((result) => result.source === "note" && result.anchorOrdinal !== null && result.sourceSessionId === currentSessionId)) {
2591
- bodyParts.push(SEARCH_NOTE_EXPAND_HINT);
2592
- }
2593
- return `Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}":
2594
-
2595
- ${bodyParts.join(`
2596
-
2597
- `)}`;
597
+ return didMutateMessage;
2598
598
  }
2599
599
 
2600
600
  // src/hooks/magic-context/auto-search-deadline.ts
2601
601
  var AUTO_SEARCH_TIMEOUT_MS = 3000;
602
+ var operationDeadlines = new WeakMap;
603
+ function autoSearchDeadlineUnixMs(signal) {
604
+ const deadline = signal && operationDeadlines.get(signal);
605
+ return Date.now() + (deadline === undefined ? AUTO_SEARCH_TIMEOUT_MS : Math.max(0, deadline - performance.now()));
606
+ }
607
+ var skippedTurns = new Map;
608
+ function wasAutoSearchSkipped(db, sessionId, messageId) {
609
+ return skippedTurns.get(sessionId)?.get(db)?.has(messageId) ?? false;
610
+ }
611
+ function clearAutoSearchTimeoutForSession(sessionId) {
612
+ cancelAutoSearchWork(sessionId);
613
+ if (sessionId === undefined)
614
+ skippedTurns.clear();
615
+ else
616
+ skippedTurns.delete(sessionId);
617
+ }
618
+ function persistAutoSearchSkip(db, sessionId, messageId, reason = "timeout") {
619
+ assertTransformWrite();
620
+ let turns = skippedTurns.get(sessionId);
621
+ if (!turns) {
622
+ turns = new WeakMap;
623
+ skippedTurns.set(sessionId, turns);
624
+ }
625
+ let messages = turns.get(db);
626
+ if (!messages) {
627
+ messages = new Map;
628
+ turns.set(db, messages);
629
+ }
630
+ const prior = messages.get(messageId);
631
+ if (prior)
632
+ return prior;
633
+ let ok = false;
634
+ try {
635
+ const outcome = appendAutoSearchHintDecision(db, sessionId, {
636
+ messageId,
637
+ decision: "no-hint",
638
+ reason
639
+ });
640
+ ok = outcome.ok && outcome.decision.decision === "no-hint";
641
+ } catch {}
642
+ assertTransformWrite();
643
+ const persistence = Promise.resolve(ok);
644
+ messages.set(messageId, persistence);
645
+ return persistence;
646
+ }
2602
647
  async function withAutoSearchDeadline(operation, startedAt = performance.now()) {
2603
648
  const deadline = startedAt + AUTO_SEARCH_TIMEOUT_MS;
2604
649
  const remaining = deadline - performance.now();
2605
650
  if (remaining <= 0)
2606
651
  return null;
2607
652
  const controller = new AbortController;
653
+ operationDeadlines.set(controller.signal, deadline);
2608
654
  const checkDeadline = () => {
2609
655
  if (!controller.signal.aborted && performance.now() >= deadline)
2610
656
  controller.abort();
@@ -3080,7 +1126,7 @@ function cavemanCompressAsciiV1(text, level) {
3080
1126
  var MAX_FRAGMENTS = 3;
3081
1127
  var FRAGMENT_CHAR_CAP = 80;
3082
1128
  var MAX_HINT_CHARS = 800;
3083
- var MS_PER_DAY2 = 24 * 60 * 60 * 1000;
1129
+ var MS_PER_DAY = 24 * 60 * 60 * 1000;
3084
1130
  function truncate(text, limit) {
3085
1131
  const normalized = text.replace(/\s+/g, " ").trim();
3086
1132
  if (normalized.length <= limit)
@@ -3091,7 +1137,7 @@ function formatAge(committedAtMs) {
3091
1137
  const delta = Date.now() - committedAtMs;
3092
1138
  if (delta < 0)
3093
1139
  return "future";
3094
- const days = Math.floor(delta / MS_PER_DAY2);
1140
+ const days = Math.floor(delta / MS_PER_DAY);
3095
1141
  if (days <= 0)
3096
1142
  return "today";
3097
1143
  if (days === 1)
@@ -3166,6 +1212,205 @@ ${trimmedBody}…
3166
1212
  return wrapped;
3167
1213
  }
3168
1214
 
1215
+ // src/hooks/magic-context/auto-search-worker-client.ts
1216
+ import { Worker } from "node:worker_threads";
1217
+ var pendingRegistrations = new WeakMap;
1218
+ var pendingTurns = new WeakMap;
1219
+ async function coalesceAutoSearchTurn(db, sessionId, messageId, operation, replay) {
1220
+ let sessions = pendingTurns.get(db);
1221
+ if (!sessions) {
1222
+ sessions = new Map;
1223
+ pendingTurns.set(db, sessions);
1224
+ }
1225
+ let turns = sessions.get(sessionId);
1226
+ if (!turns) {
1227
+ turns = new Map;
1228
+ sessions.set(sessionId, turns);
1229
+ }
1230
+ const pending = turns.get(messageId);
1231
+ if (pending && !pending.signal.aborted) {
1232
+ const result = await pending.promise;
1233
+ if (!pending.signal.aborted)
1234
+ replay();
1235
+ return result;
1236
+ }
1237
+ const lifecycle = registerAutoSearchWork(sessionId);
1238
+ const work = Promise.resolve().then(() => operation(lifecycle.signal));
1239
+ const turn = { promise: work, signal: lifecycle.signal };
1240
+ turns.set(messageId, turn);
1241
+ try {
1242
+ return await work;
1243
+ } finally {
1244
+ lifecycle.done();
1245
+ if (turns.get(messageId) === turn)
1246
+ turns.delete(messageId);
1247
+ if (turns.size === 0)
1248
+ sessions.delete(sessionId);
1249
+ }
1250
+ }
1251
+ function queueAutoSearchRegistration(db, projectPath, register) {
1252
+ if (!register)
1253
+ return;
1254
+ let pending = pendingRegistrations.get(db);
1255
+ if (!pending) {
1256
+ pending = new Set;
1257
+ pendingRegistrations.set(db, pending);
1258
+ }
1259
+ if (pending.has(projectPath))
1260
+ return;
1261
+ pending.add(projectPath);
1262
+ const timer = setTimeout(() => {
1263
+ if (getProjectEmbeddingSnapshot(projectPath)) {
1264
+ pending.delete(projectPath);
1265
+ return;
1266
+ }
1267
+ Promise.resolve().then(register).catch((error) => log(`[auto-search] background registration failed: ${error instanceof Error ? error.message : String(error)}`)).finally(() => pending.delete(projectPath));
1268
+ }, 0);
1269
+ timer.unref?.();
1270
+ }
1271
+ function persistAutoSearchDecision(db, sessionId, decision, startedAt) {
1272
+ if (performance.now() - startedAt >= AUTO_SEARCH_TIMEOUT_MS)
1273
+ return null;
1274
+ return appendAutoSearchHintDecision(db, sessionId, decision);
1275
+ }
1276
+ async function searchAutoHint(db, sessionId, projectPath, query, options, entry = new URL(new URL(import.meta.url).pathname.endsWith(".ts") ? "./auto-search-worker.ts" : "./auto-search-worker.js", import.meta.url)) {
1277
+ return executeAutoSearchWorker(db, sessionId, projectPath, query, options, entry, "search");
1278
+ }
1279
+ var pendingBackfills = new WeakMap;
1280
+ function queueAutoSearchBackfill(db, sessionId, projectPath, query) {
1281
+ if (isEmbeddingHostBusy())
1282
+ return Promise.resolve();
1283
+ const key = `${sessionId}\x00${projectPath}`;
1284
+ const pending = pendingBackfills.get(db) ?? new Map;
1285
+ pendingBackfills.set(db, pending);
1286
+ const prior = pending.get(key);
1287
+ if (prior)
1288
+ return prior;
1289
+ const lifecycle = registerAutoSearchWork(sessionId);
1290
+ const work = new Promise((resolve) => {
1291
+ const done = () => {
1292
+ lifecycle.done();
1293
+ pending.delete(key);
1294
+ resolve();
1295
+ };
1296
+ const queued = setTimeout(() => {
1297
+ lifecycle.signal.removeEventListener("abort", cancel);
1298
+ if (lifecycle.signal.aborted)
1299
+ return done();
1300
+ const controller = new AbortController;
1301
+ const timeout = setTimeout(() => controller.abort(), 1e4);
1302
+ timeout.unref?.();
1303
+ executeAutoSearchWorker(db, sessionId, projectPath, query, { signal: AbortSignal.any([controller.signal, lifecycle.signal]) }, new URL(new URL(import.meta.url).pathname.endsWith(".ts") ? "./auto-search-worker.ts" : "./auto-search-worker.js", import.meta.url), "backfill").catch((error) => log("[auto-search] background backfill failed:", error)).finally(() => {
1304
+ clearTimeout(timeout);
1305
+ done();
1306
+ });
1307
+ }, 0);
1308
+ const cancel = () => {
1309
+ clearTimeout(queued);
1310
+ done();
1311
+ };
1312
+ lifecycle.signal.addEventListener("abort", cancel, { once: true });
1313
+ queued.unref?.();
1314
+ });
1315
+ pending.set(key, work);
1316
+ return work;
1317
+ }
1318
+ async function executeAutoSearchWorker(db, sessionId, projectPath, query, options, entry, job) {
1319
+ if (options.signal?.aborted)
1320
+ return [];
1321
+ const path = getSqliteDatabasePath(db);
1322
+ if (!path)
1323
+ throw new Error("auto-search requires a file-backed database for off-thread search");
1324
+ const {
1325
+ embedQuery,
1326
+ isEmbeddingRuntimeEnabled,
1327
+ signal: deadlineSignal,
1328
+ readMessages: _readMessages,
1329
+ ...serializable
1330
+ } = options;
1331
+ const lifecycle = registerAutoSearchWork(sessionId);
1332
+ const signal = deadlineSignal ? AbortSignal.any([deadlineSignal, lifecycle.signal]) : lifecycle.signal;
1333
+ const snapshot = getProjectEmbeddingSnapshot(projectPath);
1334
+ const input = {
1335
+ job,
1336
+ path,
1337
+ harness: getHarness(),
1338
+ sessionId,
1339
+ projectPath,
1340
+ query,
1341
+ options: {
1342
+ ...serializable,
1343
+ backfillMemoryEmbeddings: false,
1344
+ countRetrievals: false,
1345
+ measurementDisabled: true
1346
+ },
1347
+ embeddingRuntimeEnabled: isEmbeddingRuntimeEnabled?.() ?? false,
1348
+ embeddingHostBusy: isEmbeddingHostBusy(),
1349
+ snapshot,
1350
+ deadlineUnixMs: autoSearchDeadlineUnixMs(deadlineSignal)
1351
+ };
1352
+ return new Promise((resolve, reject) => {
1353
+ const worker = new Worker(entry, { workerData: input });
1354
+ let settled = false;
1355
+ const finish = (results, error) => {
1356
+ if (settled)
1357
+ return;
1358
+ settled = true;
1359
+ signal?.removeEventListener("abort", abort);
1360
+ lifecycle.signal.removeEventListener("abort", abort);
1361
+ lifecycle.done();
1362
+ worker.terminate();
1363
+ worker.unref();
1364
+ if (error)
1365
+ reject(error);
1366
+ else
1367
+ resolve(results);
1368
+ };
1369
+ const abort = () => finish([]);
1370
+ signal?.addEventListener("abort", abort, { once: true });
1371
+ lifecycle.signal.addEventListener("abort", abort, { once: true });
1372
+ worker.on("error", (error) => finish([], error));
1373
+ worker.on("exit", (code) => finish([], new Error(`auto-search worker exited (${code})`)));
1374
+ worker.on("message", async (reply) => {
1375
+ if (settled)
1376
+ return;
1377
+ if (reply.kind === "result") {
1378
+ if (snapshot && getProjectEmbeddingSnapshot(projectPath)?.generation !== snapshot.generation)
1379
+ finish([]);
1380
+ else
1381
+ finish(reply.results);
1382
+ return;
1383
+ }
1384
+ if (reply.kind === "error") {
1385
+ finish([], new Error(reply.error));
1386
+ return;
1387
+ }
1388
+ const response = { id: reply.id };
1389
+ try {
1390
+ if (reply.kind === "query")
1391
+ response.result = await (job === "backfill" ? embedTextForProject(projectPath, reply.text, signal, "query") : embedQuery?.(reply.text, signal)) ?? null;
1392
+ else {
1393
+ const passage = await embedBatchForProject(projectPath, reply.texts, signal, reply.purpose);
1394
+ const current = getProjectEmbeddingSnapshot(projectPath);
1395
+ response.passage = passage && current ? {
1396
+ ...passage,
1397
+ providerIdentity: current.providerIdentity,
1398
+ runtimeFingerprint: current.runtimeFingerprint,
1399
+ dimensions: passage.vectors.find((vector) => vector !== null)?.length ?? null
1400
+ } : null;
1401
+ }
1402
+ } catch (error) {
1403
+ response.error = error instanceof Error ? error.message : String(error);
1404
+ }
1405
+ response.embeddingHostBusy = isEmbeddingHostBusy();
1406
+ if (!settled && !signal?.aborted)
1407
+ worker.postMessage(response);
1408
+ });
1409
+ if (signal?.aborted)
1410
+ abort();
1411
+ });
1412
+ }
1413
+
3169
1414
  // src/hooks/magic-context/transform-message-helpers.ts
3170
1415
  function isMeaningfulUserMessage(msg) {
3171
1416
  return msg.info.role === "user" && hasMeaningfulUserText2(msg.parts);
@@ -3340,109 +1585,142 @@ async function runAutoSearchHint(args) {
3340
1585
  if (!userMsg || typeof userMsg.info.id !== "string")
3341
1586
  return AUTO_SEARCH_OK;
3342
1587
  const userMsgId = userMsg.info.id;
3343
- const existing = getAutoSearchHintDecisions(db, sessionId);
3344
- const existingForMessage = existing.find((decision) => decision.messageId === userMsgId);
3345
- if (existingForMessage) {
3346
- if (existingForMessage.decision === "hint") {
3347
- appendReminderToUserMessageById(messages, userMsgId, existingForMessage.text);
1588
+ return coalesceAutoSearchTurn(db, sessionId, userMsgId, async (lifecycleSignal) => {
1589
+ if (lifecycleSignal.aborted)
1590
+ return AUTO_SEARCH_OK;
1591
+ if (wasAutoSearchSkipped(db, sessionId, userMsgId))
1592
+ return AUTO_SEARCH_OK;
1593
+ const existing = getAutoSearchHintDecisions(db, sessionId);
1594
+ const existingForMessage = existing.find((decision) => decision.messageId === userMsgId);
1595
+ if (existingForMessage) {
1596
+ if (existingForMessage.decision === "hint") {
1597
+ appendReminderToUserMessageById(messages, userMsgId, existingForMessage.text);
1598
+ }
1599
+ return AUTO_SEARCH_OK;
1600
+ }
1601
+ if (messages.length === 0 || messages[messages.length - 1].info.id !== userMsgId) {
1602
+ return AUTO_SEARCH_OK;
1603
+ }
1604
+ const embeddingSnapshot = getProjectEmbeddingSnapshot(options.projectPath);
1605
+ if (!embeddingSnapshot) {
1606
+ queueAutoSearchRegistration(db, options.projectPath, options.directory && options.ensureProjectRegistered ? () => options.ensureProjectRegistered?.(options.directory, db) ?? Promise.resolve() : undefined);
1607
+ persistAutoSearchSkip(db, sessionId, userMsgId, "empty");
1608
+ return AUTO_SEARCH_OK;
1609
+ }
1610
+ const writeNoHintAndReconcile = async (reason) => {
1611
+ const outcome = persistAutoSearchDecision(db, sessionId, {
1612
+ messageId: userMsgId,
1613
+ decision: "no-hint",
1614
+ reason
1615
+ }, startedAt);
1616
+ if (outcome === null) {
1617
+ persistAutoSearchSkip(db, sessionId, userMsgId);
1618
+ return { ok: false, kind: "timeout" };
1619
+ }
1620
+ if (!outcome.ok)
1621
+ return { ok: false, kind: "cas-exhaustion" };
1622
+ if (outcome.kind === "already-present" && outcome.decision.decision === "hint") {
1623
+ appendReminderToUserMessageById(messages, userMsgId, outcome.decision.text);
1624
+ }
1625
+ return AUTO_SEARCH_OK;
1626
+ };
1627
+ const rawPartsText = collectUserPromptParts(userMsg);
1628
+ if (hasStackedAugmentation(rawPartsText)) {
1629
+ sessionLog(sessionId, "auto-search: skipping — user message already carries augmentation/hint");
1630
+ return writeNoHintAndReconcile("stacked");
3348
1631
  }
3349
- return AUTO_SEARCH_OK;
3350
- }
3351
- if (messages.length === 0 || messages[messages.length - 1].info.id !== userMsgId) {
3352
- return AUTO_SEARCH_OK;
3353
- }
3354
- const writeNoHintAndReconcile = (reason) => {
3355
- const outcome = appendAutoSearchHintDecision(db, sessionId, {
3356
- messageId: userMsgId,
3357
- decision: "no-hint",
3358
- reason
3359
- });
3360
- if (!outcome.ok)
3361
- return { ok: false, kind: "cas-exhaustion" };
3362
- if (outcome.kind === "already-present" && outcome.decision.decision === "hint") {
3363
- appendReminderToUserMessageById(messages, userMsgId, outcome.decision.text);
1632
+ const rawPrompt = extractUserPromptText(userMsg);
1633
+ if (rawPrompt.length < options.minPromptChars) {
1634
+ return writeNoHintAndReconcile("too-short");
3364
1635
  }
3365
- return AUTO_SEARCH_OK;
3366
- };
3367
- const rawPartsText = collectUserPromptParts(userMsg);
3368
- if (hasStackedAugmentation(rawPartsText)) {
3369
- sessionLog(sessionId, "auto-search: skipping — user message already carries augmentation/hint");
3370
- return writeNoHintAndReconcile("stacked");
3371
- }
3372
- const rawPrompt = extractUserPromptText(userMsg);
3373
- if (rawPrompt.length < options.minPromptChars) {
3374
- return writeNoHintAndReconcile("too-short");
3375
- }
3376
- let results;
3377
- try {
3378
- results = await withAutoSearchDeadline(async (signal, checkDeadline) => {
3379
- if (options.directory) {
3380
- await options.ensureProjectRegistered?.(options.directory, db);
1636
+ let results;
1637
+ try {
1638
+ results = await withAutoSearchDeadline(async (signal, checkDeadline) => {
1639
+ if (checkDeadline())
1640
+ return null;
1641
+ const memoryEnabled = embeddingSnapshot?.features.memoryEnabled ?? options.memoryEnabled;
1642
+ const embeddingEnabled = embeddingSnapshot ? embeddingSnapshot.historyEnabled : options.embeddingEnabled;
1643
+ const gitCommitsEnabled = embeddingSnapshot?.gitCommitEnabled ?? options.gitCommitsEnabled ?? false;
1644
+ const searchOptions = {
1645
+ limit: 10,
1646
+ memoryEnabled,
1647
+ embeddingEnabled,
1648
+ gitCommitsEnabled,
1649
+ embedQuery: async (text, signal) => {
1650
+ const result = await embedTextForProject(options.projectPath, text, signal, "query");
1651
+ checkDeadline();
1652
+ return result;
1653
+ },
1654
+ isEmbeddingRuntimeEnabled: () => embeddingEnabled === true,
1655
+ visibleMemoryIds: options.visibleMemoryIds ?? null,
1656
+ sources: ["memory", "message", "git_commit"]
1657
+ };
1658
+ return searchAutoHint(db, sessionId, options.projectPath, rawPrompt, {
1659
+ ...searchOptions,
1660
+ signal,
1661
+ countRetrievals: false
1662
+ });
1663
+ }, startedAt);
1664
+ } catch (error) {
1665
+ log(`[auto-search] unified search failed for session ${sessionId} (will retry next pass): ${error instanceof Error ? error.message : String(error)}`);
1666
+ return { ok: false, kind: "search-failure" };
1667
+ }
1668
+ if (lifecycleSignal.aborted)
1669
+ return AUTO_SEARCH_OK;
1670
+ try {
1671
+ if (results === null) {
1672
+ sessionLog(sessionId, `auto-search: timed out after ${AUTO_SEARCH_TIMEOUT_MS}ms, skipping hint for this turn`);
1673
+ persistAutoSearchSkip(db, sessionId, userMsgId);
1674
+ sessionLog(sessionId, "auto-search: skip frozen; retries for this turn are replay-only");
1675
+ return { ok: false, kind: "timeout" };
3381
1676
  }
3382
- if (checkDeadline())
3383
- return null;
3384
- const embeddingSnapshot = getProjectEmbeddingSnapshot(options.projectPath);
3385
- const memoryEnabled = embeddingSnapshot?.features.memoryEnabled ?? options.memoryEnabled;
3386
- const embeddingEnabled = embeddingSnapshot ? embeddingSnapshot.historyEnabled : options.embeddingEnabled;
3387
- const gitCommitsEnabled = embeddingSnapshot?.gitCommitEnabled ?? options.gitCommitsEnabled ?? false;
3388
- const searchOptions = {
3389
- limit: 10,
3390
- memoryEnabled,
3391
- embeddingEnabled,
3392
- gitCommitsEnabled,
3393
- embedQuery: async (text, signal) => {
3394
- const result = await embedTextForProject(options.projectPath, text, signal, "query");
3395
- checkDeadline();
3396
- return result;
3397
- },
3398
- isEmbeddingRuntimeEnabled: () => embeddingEnabled === true,
3399
- visibleMemoryIds: options.visibleMemoryIds ?? null,
3400
- sources: ["memory", "message", "git_commit"]
3401
- };
3402
- return unifiedSearch(db, sessionId, options.projectPath, rawPrompt, {
3403
- ...searchOptions,
3404
- signal,
3405
- countRetrievals: false
3406
- });
3407
- }, startedAt);
3408
- } catch (error) {
3409
- log(`[auto-search] unified search failed for session ${sessionId} (will retry next pass): ${error instanceof Error ? error.message : String(error)}`);
3410
- return { ok: false, kind: "search-failure" };
3411
- }
3412
- if (results === null) {
3413
- sessionLog(sessionId, `auto-search: timed out after ${AUTO_SEARCH_TIMEOUT_MS}ms, skipping hint for this turn (will retry)`);
3414
- return { ok: false, kind: "timeout" };
3415
- }
3416
- if (results.length === 0) {
3417
- return writeNoHintAndReconcile("empty");
3418
- }
3419
- if (results[0].score < options.scoreThreshold) {
3420
- sessionLog(sessionId, `auto-search: top score ${results[0].score.toFixed(3)} below threshold ${options.scoreThreshold}`);
3421
- return writeNoHintAndReconcile("below-threshold");
3422
- }
3423
- const hintText = buildAutoSearchHint(results, { wordRules: options.wordRules });
3424
- if (!hintText) {
3425
- return writeNoHintAndReconcile("empty");
3426
- }
3427
- const payload = `
1677
+ if (results.length === 0) {
1678
+ return writeNoHintAndReconcile("empty");
1679
+ }
1680
+ if (results[0].score < options.scoreThreshold) {
1681
+ sessionLog(sessionId, `auto-search: top score ${results[0].score.toFixed(3)} below threshold ${options.scoreThreshold}`);
1682
+ return writeNoHintAndReconcile("below-threshold");
1683
+ }
1684
+ const hintText = buildAutoSearchHint(results, { wordRules: options.wordRules });
1685
+ if (!hintText) {
1686
+ return writeNoHintAndReconcile("empty");
1687
+ }
1688
+ const payload = `
3428
1689
 
3429
1690
  ${hintText}`;
3430
- const outcome = appendAutoSearchHintDecision(db, sessionId, {
3431
- messageId: userMsgId,
3432
- decision: "hint",
3433
- text: payload
1691
+ const outcome = persistAutoSearchDecision(db, sessionId, {
1692
+ messageId: userMsgId,
1693
+ decision: "hint",
1694
+ text: payload
1695
+ }, startedAt);
1696
+ if (outcome === null) {
1697
+ persistAutoSearchSkip(db, sessionId, userMsgId);
1698
+ return { ok: false, kind: "timeout" };
1699
+ }
1700
+ if (!outcome.ok) {
1701
+ sessionLog(sessionId, `auto-search: CAS exhausted for ${userMsgId}; skipping wire append`);
1702
+ return { ok: false, kind: "cas-exhaustion" };
1703
+ }
1704
+ if (outcome.decision.decision === "hint") {
1705
+ appendReminderToUserMessageById(messages, userMsgId, outcome.decision.text);
1706
+ }
1707
+ sessionLog(sessionId, `auto-search: attached hint to ${userMsgId} (${results.length} fragments, top score ${results[0].score.toFixed(3)})`);
1708
+ return AUTO_SEARCH_OK;
1709
+ } finally {
1710
+ if (!lifecycleSignal.aborted && results !== null)
1711
+ queueAutoSearchBackfill(db, sessionId, options.projectPath, rawPrompt);
1712
+ }
1713
+ }, () => {
1714
+ if (wasAutoSearchSkipped(db, sessionId, userMsgId))
1715
+ return;
1716
+ const decision = getAutoSearchHintDecisions(db, sessionId).find((entry) => entry.messageId === userMsgId);
1717
+ if (decision?.decision === "hint")
1718
+ appendReminderToUserMessageById(messages, userMsgId, decision.text);
3434
1719
  });
3435
- if (!outcome.ok) {
3436
- sessionLog(sessionId, `auto-search: CAS exhausted for ${userMsgId}; skipping wire append`);
3437
- return { ok: false, kind: "cas-exhaustion" };
3438
- }
3439
- if (outcome.decision.decision === "hint") {
3440
- appendReminderToUserMessageById(messages, userMsgId, outcome.decision.text);
3441
- }
3442
- sessionLog(sessionId, `auto-search: attached hint to ${userMsgId} (${results.length} fragments, top score ${results[0].score.toFixed(3)})`);
3443
- return AUTO_SEARCH_OK;
3444
1720
  }
3445
- function clearAutoSearchForSession(_sessionId) {}
1721
+ function clearAutoSearchForSession(sessionId) {
1722
+ clearAutoSearchTimeoutForSession(sessionId);
1723
+ }
3446
1724
 
3447
1725
  // src/hooks/magic-context/cache-busting-signals.ts
3448
1726
  function canConsumeDeferredOnThisPass(args) {
@@ -4078,7 +2356,7 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config) {
4078
2356
  const eligible = tags.filter((tag) => tag.type === "message" && tag.status === "active" && (protectedCutoff === null || tag.tagNumber < protectedCutoff) && tag.byteSize >= config.minChars).sort((a, b) => a.tagNumber - b.tagNumber);
4079
2357
  const tagsNeedingCompression = eligible.filter((tag, index) => {
4080
2358
  const target = targets.get(tag.tagNumber);
4081
- if (!target?.getContent || !target.setContent)
2359
+ if (!target?.getContent || !target.setContent || target.thinkingRewriteProtected)
4082
2360
  return false;
4083
2361
  const targetDepth = computeTargetDepth(index, eligible.length);
4084
2362
  return targetDepth > tag.cavemanDepth;
@@ -4087,7 +2365,7 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config) {
4087
2365
  let state = getCavemanReplayState(db, sessionId);
4088
2366
  if (!state.currentRules || state.englishWordRules !== englishWordRules) {
4089
2367
  const compressedBefore = tags.filter((tag) => tag.type === "message" && tag.status === "active" && tag.cavemanDepth > 0);
4090
- const switched = recordCavemanCurrentRules(db, sessionId, englishWordRules, compressedBefore.map((tag) => tag.tagNumber));
2368
+ const switched = compressedBefore.some((tag) => targets.get(tag.tagNumber)?.thinkingRewriteProtected) ? null : recordCavemanCurrentRules(db, sessionId, englishWordRules, compressedBefore.map((tag) => tag.tagNumber));
4091
2369
  if (switched) {
4092
2370
  state = switched;
4093
2371
  const deepened = new Set(tagsNeedingCompression.map((tag) => tag.tagNumber));
@@ -4309,6 +2587,8 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
4309
2587
  continue;
4310
2588
  const strippedSource = stripTagPrefix(stripped);
4311
2589
  if (strippedSource.trim().length === 0) {
2590
+ if (target.thinkingDropProtected)
2591
+ continue;
4312
2592
  const dropResult = target.drop?.() ?? "absent";
4313
2593
  const replacement = `[dropped §${tag.tagNumber}§]`;
4314
2594
  const didReplace = dropResult === "absent" ? target.setContent(replacement) : false;
@@ -4429,147 +2709,41 @@ function extractToolInfo(part) {
4429
2709
  if (part.type === "tool_use" && typeof part.name === "string" && DEDUP_SAFE_TOOLS.has(part.name)) {
4430
2710
  return { toolName: part.name, args: part.input ?? {} };
4431
2711
  }
4432
- return null;
4433
- }
4434
- function buildToolFingerprints(messages) {
4435
- const fingerprints = new Map;
4436
- for (const message of messages) {
4437
- if (message.info.role !== "assistant")
4438
- continue;
4439
- const ownerMsgId = typeof message.info.id === "string" ? message.info.id : null;
4440
- if (!ownerMsgId)
4441
- continue;
4442
- for (const part of message.parts) {
4443
- const record = part;
4444
- const info = extractToolInfo(record);
4445
- if (!info)
4446
- continue;
4447
- const callId = extractCallId(record);
4448
- if (!callId)
4449
- continue;
4450
- try {
4451
- const fingerprint = `${ownerMsgId}:${info.toolName}:${JSON.stringify(info.args)}`;
4452
- const compositeKey = `${ownerMsgId}\x00${callId}`;
4453
- fingerprints.set(compositeKey, fingerprint);
4454
- } catch {}
4455
- }
4456
- }
4457
- return fingerprints;
4458
- }
4459
- function extractCallId(part) {
4460
- if (part.type === "tool" && typeof part.callID === "string")
4461
- return part.callID;
4462
- if (part.type === "tool-invocation" && typeof part.callID === "string")
4463
- return part.callID;
4464
- if (part.type === "tool_use" && typeof part.id === "string")
4465
- return part.id;
4466
- return null;
4467
- }
4468
-
4469
- // src/hooks/magic-context/note-visibility.ts
4470
- var NOTE_TOOL_NAMES = new Set(["ctx_note"]);
4471
- var READ_ACTION = "read";
4472
- function hasVisibleNoteReadCall(messages) {
4473
- for (let i = messages.length - 1;i >= 0; i -= 1) {
4474
- const parts = messages[i]?.parts;
4475
- if (!Array.isArray(parts))
4476
- continue;
4477
- for (const part of parts) {
4478
- if (isSentinel(part))
4479
- continue;
4480
- if (isVisibleNoteReadPart(part))
4481
- return true;
4482
- }
4483
- }
4484
- return false;
4485
- }
4486
- function isVisibleNoteReadPart(part) {
4487
- if (!isRecord(part))
4488
- return false;
4489
- if (part.type === "tool" && typeof part.tool === "string" && NOTE_TOOL_NAMES.has(part.tool)) {
4490
- const state = part.state;
4491
- if (isRecord(state) && isRecord(state.input)) {
4492
- return state.input.action === READ_ACTION;
4493
- }
4494
- return false;
4495
- }
4496
- if (part.type === "tool_use" && typeof part.name === "string" && NOTE_TOOL_NAMES.has(part.name)) {
4497
- if (isRecord(part.input)) {
4498
- return part.input.action === READ_ACTION;
4499
- }
4500
- return false;
4501
- }
4502
- if (part.type === "tool-invocation" && typeof part.toolName === "string" && NOTE_TOOL_NAMES.has(part.toolName)) {
4503
- const argsCandidate = part.args ?? part.input;
4504
- if (isRecord(argsCandidate)) {
4505
- return argsCandidate.action === READ_ACTION;
4506
- }
4507
- return false;
4508
- }
4509
- return false;
4510
- }
4511
-
4512
- // src/hooks/magic-context/postprocess-read-cache.ts
4513
- var connections = new WeakMap;
4514
- function snapshot(db, sessionId) {
4515
- const transaction = db;
4516
- try {
4517
- if (transaction.inTransaction || transaction.isTransaction) {
4518
- connections.delete(db);
4519
- return { version: -1, changes: -1 };
4520
- }
4521
- } catch {
4522
- connections.delete(db);
4523
- return { version: -1, changes: -1 };
4524
- }
4525
- let connection = connections.get(db);
4526
- if (!connection) {
4527
- connection = {
4528
- stamp: db.prepare("SELECT total_changes() AS changes, data_version AS version FROM pragma_data_version"),
4529
- sessions: new BoundedSessionMap(100)
4530
- };
4531
- connections.set(db, connection);
4532
- }
4533
- const stamp = connection.stamp.get();
4534
- const previous = connection.sessions.get(sessionId);
4535
- if (previous?.version === stamp.version && previous.changes === stamp.changes) {
4536
- return previous;
4537
- }
4538
- const value = { ...stamp };
4539
- connection.sessions.set(sessionId, value);
4540
- return value;
4541
- }
4542
- function postprocessTailTags(db, sessionId) {
4543
- const state = snapshot(db, sessionId);
4544
- return state.tags ??= getTailHygieneTags(db, sessionId);
2712
+ return null;
4545
2713
  }
4546
- function postprocessOldestTags(db, sessionId, protectedNumbers) {
4547
- const state = snapshot(db, sessionId);
4548
- const hints = state.hints;
4549
- if (hints && hints.protectedNumbers.size === protectedNumbers.size && [...protectedNumbers].every((number) => hints.protectedNumbers.has(number))) {
4550
- return hints.value;
2714
+ function buildToolFingerprints(messages) {
2715
+ const fingerprints = new Map;
2716
+ for (const message of messages) {
2717
+ if (message.info.role !== "assistant")
2718
+ continue;
2719
+ const ownerMsgId = typeof message.info.id === "string" ? message.info.id : null;
2720
+ if (!ownerMsgId)
2721
+ continue;
2722
+ for (const part of message.parts) {
2723
+ const record = part;
2724
+ const info = extractToolInfo(record);
2725
+ if (!info)
2726
+ continue;
2727
+ const callId = extractCallId(record);
2728
+ if (!callId)
2729
+ continue;
2730
+ try {
2731
+ const fingerprint = `${ownerMsgId}:${info.toolName}:${JSON.stringify(info.args)}`;
2732
+ const compositeKey = `${ownerMsgId}\x00${callId}`;
2733
+ fingerprints.set(compositeKey, fingerprint);
2734
+ } catch {}
2735
+ }
4551
2736
  }
4552
- const value = getOldestActiveUnprotectedToolTags(db, sessionId, protectedNumbers);
4553
- state.hints = { protectedNumbers: new Set(protectedNumbers), value };
4554
- return value;
2737
+ return fingerprints;
4555
2738
  }
4556
- function postprocessReplaySnapshot(db, sessionId) {
4557
- const state = snapshot(db, sessionId);
4558
- const replay = state.replay ??= loadPostprocessReplaySnapshot(db, sessionId);
4559
- return {
4560
- ...replay,
4561
- staleReduceStrippedIds: new Set(replay.staleReduceStrippedIds),
4562
- processedImageStrippedIds: new Set(replay.processedImageStrippedIds),
4563
- strippedPlaceholderIds: new Set(replay.strippedPlaceholderIds),
4564
- hiddenSeamPlaceholderIds: new Set(replay.hiddenSeamPlaceholderIds),
4565
- mergedReasoningStrippedIds: new Set(replay.mergedReasoningStrippedIds),
4566
- trailingBlankDecisions: new Map(replay.trailingBlankDecisions),
4567
- noteNudgeAnchors: structuredClone(replay.noteNudgeAnchors),
4568
- autoSearchHintDecisions: structuredClone(replay.autoSearchHintDecisions),
4569
- todoSyntheticAnchor: structuredClone(replay.todoSyntheticAnchor),
4570
- pendingCompactionMarker: structuredClone(replay.pendingCompactionMarker),
4571
- compactionMarker: structuredClone(replay.compactionMarker)
4572
- };
2739
+ function extractCallId(part) {
2740
+ if (part.type === "tool" && typeof part.callID === "string")
2741
+ return part.callID;
2742
+ if (part.type === "tool-invocation" && typeof part.callID === "string")
2743
+ return part.callID;
2744
+ if (part.type === "tool_use" && typeof part.id === "string")
2745
+ return part.id;
2746
+ return null;
4573
2747
  }
4574
2748
 
4575
2749
  // src/hooks/magic-context/strip-content.ts
@@ -4752,11 +2926,8 @@ function replayStrippedInlineThinking(messages, messageTagNumbers, persistedWate
4752
2926
  }
4753
2927
  return stripped;
4754
2928
  }
4755
- function clearOldReasoning(messages, reasoningByMessage, messageTagNumbers, clearReasoningAge) {
4756
- const maxTag = findMaxTag(messageTagNumbers);
4757
- if (maxTag === 0)
4758
- return 0;
4759
- const ageCutoff = maxTag - clearReasoningAge;
2929
+ function clearOldReasoning(messages, reasoningByMessage, messageTagNumbers, cutoff) {
2930
+ const ageCutoff = cutoff;
4760
2931
  let cleared = 0;
4761
2932
  for (const message of messages) {
4762
2933
  const msgTag = messageTagNumbers.get(message) ?? 0;
@@ -4780,14 +2951,6 @@ function clearOldReasoning(messages, reasoningByMessage, messageTagNumbers, clea
4780
2951
  }
4781
2952
  return cleared;
4782
2953
  }
4783
- function findMaxTag(messageTagNumbers) {
4784
- let max = 0;
4785
- for (const tag of messageTagNumbers.values()) {
4786
- if (tag > max)
4787
- max = tag;
4788
- }
4789
- return max;
4790
- }
4791
2954
  var CLEARED_REASONING_TYPES = new Set(["thinking", "reasoning"]);
4792
2955
  function stripClearedReasoning(messages) {
4793
2956
  let stripped = 0;
@@ -4815,11 +2978,8 @@ function stripClearedReasoning(messages) {
4815
2978
  return stripped;
4816
2979
  }
4817
2980
  var INLINE_THINKING_PATTERN = /<(?:thinking|think)>[\s\S]*?<\/(?:thinking|think)>\s*/g;
4818
- function stripInlineThinking(messages, messageTagNumbers, clearReasoningAge) {
4819
- const maxTag = findMaxTag(messageTagNumbers);
4820
- if (maxTag === 0)
4821
- return 0;
4822
- const ageCutoff = maxTag - clearReasoningAge;
2981
+ function stripInlineThinking(messages, messageTagNumbers, cutoff) {
2982
+ const ageCutoff = cutoff;
4823
2983
  let stripped = 0;
4824
2984
  for (const message of messages) {
4825
2985
  if (message.info.role !== "assistant")
@@ -4882,7 +3042,7 @@ function findLatestAssistantReasoningMutationExemptMessage(messages) {
4882
3042
  }
4883
3043
  return;
4884
3044
  }
4885
- function planMergedAssistantReasoningStrip(messages, mutationExemptMessage, frozenMessageIds) {
3045
+ function planMergedAssistantReasoningStrip(messages, mutationExemptMessage, frozenMessageIds, protectedMessages) {
4886
3046
  const plan = [];
4887
3047
  let prevRole;
4888
3048
  let keptReasoningInRun = false;
@@ -4896,7 +3056,7 @@ function planMergedAssistantReasoningStrip(messages, mutationExemptMessage, froz
4896
3056
  const firstInRun = prevRole !== "assistant";
4897
3057
  if (firstInRun)
4898
3058
  keptReasoningInRun = false;
4899
- if (message === mutationExemptMessage || frozenMessageIds?.has(message.info.id ?? "")) {
3059
+ if (protectedMessages?.has(message) || message === mutationExemptMessage || frozenMessageIds?.has(message.info.id ?? "")) {
4900
3060
  prevRole = role;
4901
3061
  continue;
4902
3062
  }
@@ -5058,12 +3218,14 @@ function applyFrozenTrailingBlankDecisions(messages, frozenDecisions) {
5058
3218
  }
5059
3219
  return mutations;
5060
3220
  }
5061
- function stripReasoningFromAssistantIds(messages, providerID, messageIds) {
3221
+ function stripReasoningFromAssistantIds(messages, providerID, messageIds, protectedMessages) {
5062
3222
  if (messageIds.size === 0)
5063
3223
  return 0;
5064
3224
  const emptySentinels = modelAcceptsEmptyContent(providerID);
5065
3225
  let stripped = 0;
5066
3226
  for (const message of messages) {
3227
+ if (protectedMessages?.has(message))
3228
+ continue;
5067
3229
  const id = message.info.id;
5068
3230
  if (typeof id !== "string" || !messageIds.has(id))
5069
3231
  continue;
@@ -5089,7 +3251,7 @@ function findMergedReasoningStripDecisions(messages, providerID, frozenIds, opti
5089
3251
  return [];
5090
3252
  const frozenParts = readFrozenMergedReasoningParts(frozenIds);
5091
3253
  const decisions = [];
5092
- for (const entry of planMergedAssistantReasoningStrip(messages, options?.mutationExemptMessage, new Set(frozenParts.keys()))) {
3254
+ for (const entry of planMergedAssistantReasoningStrip(messages, options?.mutationExemptMessage, new Set(frozenParts.keys()), options?.protectedMessages)) {
5093
3255
  const id = entry.message.info.id;
5094
3256
  if (typeof id !== "string" || id.length === 0 || frozenParts.has(id))
5095
3257
  continue;
@@ -5107,7 +3269,7 @@ function stripReasoningFromMergedAssistants(messages, providerID, options) {
5107
3269
  let stripped = 0;
5108
3270
  const frozenParts = readFrozenMergedReasoningParts(options?.frozenMessageIds ?? new Set);
5109
3271
  for (const message of messages) {
5110
- if (message === options?.mutationExemptMessage || message.info.role !== "assistant")
3272
+ if (message.info.role !== "assistant" || options?.restoreMessages?.has(message))
5111
3273
  continue;
5112
3274
  const parts = frozenParts.get(message.info.id ?? "");
5113
3275
  if (!parts)
@@ -5122,7 +3284,7 @@ function stripReasoningFromMergedAssistants(messages, providerID, options) {
5122
3284
  stripped++;
5123
3285
  }
5124
3286
  }
5125
- for (const entry of planMergedAssistantReasoningStrip(messages, options?.mutationExemptMessage, new Set(frozenParts.keys()))) {
3287
+ for (const entry of planMergedAssistantReasoningStrip(messages, options?.mutationExemptMessage, new Set(frozenParts.keys()), options?.frozenMessageIds ? options.restoreMessages : options?.protectedMessages)) {
5126
3288
  if (options?.frozenMessageIds) {
5127
3289
  const id = entry.message.info.id;
5128
3290
  if (typeof id !== "string" || !options.frozenMessageIds.has(id))
@@ -5240,12 +3402,8 @@ function stripOpenRouterReasoningDetails(message) {
5240
3402
  return stripped;
5241
3403
  }
5242
3404
  function selectReasoningRemovals(args) {
5243
- let maxTag = 0;
5244
- for (const tag of args.messageTagNumbers.values())
5245
- if (tag > maxTag)
5246
- maxTag = tag;
5247
- const cutoff = maxTag - args.clearReasoningAge;
5248
- if (maxTag === 0 || cutoff <= 0)
3405
+ const cutoff = args.cutoff;
3406
+ if (cutoff <= 0)
5249
3407
  return [];
5250
3408
  const newest = newestAssistant(args.messages);
5251
3409
  const exempt = findLatestAssistantReasoningMutationExemptMessage(args.messages);
@@ -5259,7 +3417,7 @@ function selectReasoningRemovals(args) {
5259
3417
  if (id !== undefined && (args.alreadyRemoved.has(id) || args.prefixBound && args.alsoGone?.has(id)))
5260
3418
  continue;
5261
3419
  const tag = args.messageTagNumbers.get(message) ?? 0;
5262
- const removable = id !== undefined && message !== newest && message !== exempt && tag > 0 && tag <= cutoff && hasWireContentBesideReasoning(message) && reasoningPayloadLeavesWithParts(message);
3420
+ const removable = !args.protectedMessages?.has(message) && id !== undefined && message !== newest && message !== exempt && tag > 0 && tag <= cutoff && hasWireContentBesideReasoning(message) && reasoningPayloadLeavesWithParts(message);
5263
3421
  if (removable && id !== undefined)
5264
3422
  selected.push(id);
5265
3423
  else if (args.prefixBound)
@@ -5270,10 +3428,9 @@ function selectReasoningRemovals(args) {
5270
3428
  function removeReasoningParts(messages, ids, providerID) {
5271
3429
  if (ids.size === 0)
5272
3430
  return 0;
5273
- const exempt = findLatestAssistantReasoningMutationExemptMessage(messages);
5274
3431
  let removed = 0;
5275
3432
  for (const message of messages) {
5276
- if (message.info.role !== "assistant" || message === exempt)
3433
+ if (message.info.role !== "assistant")
5277
3434
  continue;
5278
3435
  const id = message.info.id;
5279
3436
  if (typeof id !== "string" || !ids.has(id))
@@ -5309,6 +3466,441 @@ function settleDroppedReasoningParts(messages, mode) {
5309
3466
  return settled;
5310
3467
  }
5311
3468
 
3469
+ // src/hooks/magic-context/latest-assistant-turn.ts
3470
+ var THINKING_TYPES = new Set(["reasoning", "thinking", "redacted_thinking"]);
3471
+ function role(message) {
3472
+ if (!isRecord(message))
3473
+ return;
3474
+ return isRecord(message.info) ? message.info.role : message.role;
3475
+ }
3476
+ function content(message) {
3477
+ if (!isRecord(message))
3478
+ return;
3479
+ return Array.isArray(message.parts) ? message.parts : message.content;
3480
+ }
3481
+ function latestAssistantTurnStart(messages) {
3482
+ let lo = 0;
3483
+ let hi = messages.length;
3484
+ while (lo < hi) {
3485
+ const mid = lo + hi >>> 1;
3486
+ if (isInActiveAnthropicTurn(messages, mid, true))
3487
+ hi = mid;
3488
+ else
3489
+ lo = mid + 1;
3490
+ }
3491
+ return lo;
3492
+ }
3493
+ function latestAssistantTurnMessages(messages) {
3494
+ return new Set(messages.slice(latestAssistantTurnStart(messages)));
3495
+ }
3496
+ function hasActiveAnthropicThinkingTurn(messages, provider, model) {
3497
+ const familyRoute = isAnthropicFamilyRoute(provider, model);
3498
+ for (const message of messages.slice(latestAssistantTurnStart(messages))) {
3499
+ if (role(message) !== "assistant")
3500
+ continue;
3501
+ const parts = content(message);
3502
+ if (!Array.isArray(parts))
3503
+ continue;
3504
+ if (parts.some((part) => isRecord(part) && THINKING_TYPES.has(String(part.type ?? part.kind)) && (familyRoute || isRecord(part.metadata) && isRecord(part.metadata.anthropic))))
3505
+ return true;
3506
+ }
3507
+ return false;
3508
+ }
3509
+ function protectNewTagMutations(messages, targets, protectedParts, prefixBound) {
3510
+ if (protectedParts.size === 0)
3511
+ return targets;
3512
+ const positions = new Map;
3513
+ let ordinal = 0;
3514
+ for (const message of messages)
3515
+ for (const part of message.parts)
3516
+ positions.set(part, ordinal++);
3517
+ let lastProtected = -1;
3518
+ for (const part of protectedParts)
3519
+ lastProtected = Math.max(lastProtected, positions.get(part) ?? -1);
3520
+ const result = new Map;
3521
+ for (const [tag, target] of targets) {
3522
+ const coordinates = target.mutationParts ?? (target.message ? target.message.parts.map((part) => ({ message: target.message, part })) : []);
3523
+ const prefixEdit = prefixBound && coordinates.some(({ part }) => (positions.get(part) ?? Infinity) < lastProtected);
3524
+ const dropsThinking = target.dropReasoningParts?.some((part) => protectedParts.has(part)) === true;
3525
+ if (!prefixEdit && !dropsThinking) {
3526
+ result.set(tag, target);
3527
+ continue;
3528
+ }
3529
+ result.set(tag, {
3530
+ ...target,
3531
+ thinkingDropProtected: true,
3532
+ thinkingRewriteProtected: prefixEdit,
3533
+ canDrop: () => false,
3534
+ drop: () => "incomplete",
3535
+ truncate: () => "incomplete",
3536
+ skeletonReal: () => "incomplete",
3537
+ skeletonStripped: () => "incomplete",
3538
+ editMarker: () => "incomplete",
3539
+ editMarkerStripped: () => "incomplete",
3540
+ setContent: (content, options) => prefixEdit || options?.keepReasoning !== true ? false : target.setContent(content, options)
3541
+ });
3542
+ }
3543
+ return result;
3544
+ }
3545
+ function retainedActiveThinkingParts(args) {
3546
+ if (!hasActiveAnthropicThinkingTurn(args.messages, args.providerID, args.modelID))
3547
+ return new Set;
3548
+ const copies = args.messages.map((message) => ({
3549
+ info: message.info,
3550
+ parts: message.parts.map((part) => {
3551
+ if (!isRecord(part) || THINKING_TYPES.has(String(part.type)) || !isRecord(part.metadata))
3552
+ return part;
3553
+ return {
3554
+ ...part,
3555
+ metadata: {
3556
+ ...part.metadata,
3557
+ ...isRecord(part.metadata.openrouter) ? { openrouter: { ...part.metadata.openrouter } } : {}
3558
+ }
3559
+ };
3560
+ })
3561
+ }));
3562
+ stripClearedReasoning(copies);
3563
+ stripReasoningFromAssistantIds(copies, args.providerID, args.bindingIds);
3564
+ stripReasoningFromMergedAssistants(copies, args.providerID, {
3565
+ frozenMessageIds: args.mergedIds
3566
+ });
3567
+ if (args.removedIds?.size)
3568
+ removeReasoningParts(copies, args.removedIds, args.providerID);
3569
+ const retained = new Set;
3570
+ for (let i = latestAssistantTurnStart(args.messages);i < copies.length; i++) {
3571
+ for (const part of copies[i].parts) {
3572
+ if (isRecord(part) && THINKING_TYPES.has(String(part.type)))
3573
+ retained.add(part);
3574
+ }
3575
+ }
3576
+ return retained;
3577
+ }
3578
+
3579
+ // src/hooks/magic-context/note-visibility.ts
3580
+ var NOTE_TOOL_NAMES = new Set(["ctx_note"]);
3581
+ var READ_ACTION = "read";
3582
+ function hasVisibleNoteReadCall(messages) {
3583
+ for (let i = messages.length - 1;i >= 0; i -= 1) {
3584
+ const parts = messages[i]?.parts;
3585
+ if (!Array.isArray(parts))
3586
+ continue;
3587
+ for (const part of parts) {
3588
+ if (isSentinel(part))
3589
+ continue;
3590
+ if (isVisibleNoteReadPart(part))
3591
+ return true;
3592
+ }
3593
+ }
3594
+ return false;
3595
+ }
3596
+ function isVisibleNoteReadPart(part) {
3597
+ if (!isRecord(part))
3598
+ return false;
3599
+ if (part.type === "tool" && typeof part.tool === "string" && NOTE_TOOL_NAMES.has(part.tool)) {
3600
+ const state = part.state;
3601
+ if (isRecord(state) && isRecord(state.input)) {
3602
+ return state.input.action === READ_ACTION;
3603
+ }
3604
+ return false;
3605
+ }
3606
+ if (part.type === "tool_use" && typeof part.name === "string" && NOTE_TOOL_NAMES.has(part.name)) {
3607
+ if (isRecord(part.input)) {
3608
+ return part.input.action === READ_ACTION;
3609
+ }
3610
+ return false;
3611
+ }
3612
+ if (part.type === "tool-invocation" && typeof part.toolName === "string" && NOTE_TOOL_NAMES.has(part.toolName)) {
3613
+ const argsCandidate = part.args ?? part.input;
3614
+ if (isRecord(argsCandidate)) {
3615
+ return argsCandidate.action === READ_ACTION;
3616
+ }
3617
+ return false;
3618
+ }
3619
+ return false;
3620
+ }
3621
+
3622
+ // src/hooks/magic-context/postprocess-read-cache.ts
3623
+ var connections = new WeakMap;
3624
+ function snapshot(db, sessionId) {
3625
+ const transaction = db;
3626
+ try {
3627
+ if (transaction.inTransaction || transaction.isTransaction) {
3628
+ connections.delete(db);
3629
+ return { version: -1, changes: -1 };
3630
+ }
3631
+ } catch {
3632
+ connections.delete(db);
3633
+ return { version: -1, changes: -1 };
3634
+ }
3635
+ let connection = connections.get(db);
3636
+ if (!connection) {
3637
+ connection = {
3638
+ stamp: db.prepare("SELECT total_changes() AS changes, data_version AS version FROM pragma_data_version"),
3639
+ sessions: new BoundedSessionMap(100)
3640
+ };
3641
+ connections.set(db, connection);
3642
+ }
3643
+ const stamp = connection.stamp.get();
3644
+ const previous = connection.sessions.get(sessionId);
3645
+ if (previous?.version === stamp.version && previous.changes === stamp.changes) {
3646
+ return previous;
3647
+ }
3648
+ const value = { ...stamp };
3649
+ connection.sessions.set(sessionId, value);
3650
+ return value;
3651
+ }
3652
+ function postprocessTailTags(db, sessionId) {
3653
+ const state = snapshot(db, sessionId);
3654
+ return state.tags ??= getTailHygieneTags(db, sessionId);
3655
+ }
3656
+ function postprocessOldestTags(db, sessionId, protectedNumbers) {
3657
+ const state = snapshot(db, sessionId);
3658
+ const hints = state.hints;
3659
+ if (hints && hints.protectedNumbers.size === protectedNumbers.size && [...protectedNumbers].every((number) => hints.protectedNumbers.has(number))) {
3660
+ return hints.value;
3661
+ }
3662
+ const value = getOldestActiveUnprotectedToolTags(db, sessionId, protectedNumbers);
3663
+ state.hints = { protectedNumbers: new Set(protectedNumbers), value };
3664
+ return value;
3665
+ }
3666
+ function postprocessReplaySnapshot(db, sessionId) {
3667
+ const state = snapshot(db, sessionId);
3668
+ const replay = state.replay ??= loadPostprocessReplaySnapshot(db, sessionId);
3669
+ return {
3670
+ ...replay,
3671
+ staleReduceStrippedIds: new Set(replay.staleReduceStrippedIds),
3672
+ processedImageStrippedIds: new Set(replay.processedImageStrippedIds),
3673
+ strippedPlaceholderIds: new Set(replay.strippedPlaceholderIds),
3674
+ hiddenSeamPlaceholderIds: new Set(replay.hiddenSeamPlaceholderIds),
3675
+ mergedReasoningStrippedIds: new Set(replay.mergedReasoningStrippedIds),
3676
+ trailingBlankDecisions: new Map(replay.trailingBlankDecisions),
3677
+ noteNudgeAnchors: structuredClone(replay.noteNudgeAnchors),
3678
+ autoSearchHintDecisions: structuredClone(replay.autoSearchHintDecisions),
3679
+ todoSyntheticAnchor: structuredClone(replay.todoSyntheticAnchor),
3680
+ pendingCompactionMarker: structuredClone(replay.pendingCompactionMarker),
3681
+ compactionMarker: structuredClone(replay.compactionMarker)
3682
+ };
3683
+ }
3684
+
3685
+ // src/hooks/magic-context/reasoning-budget.ts
3686
+ function projectOpencodeReasoningBudgetCutoff(db, sessionId, messages, budget, watermark, proseRatio) {
3687
+ const maxTags = new Map;
3688
+ for (const tag of getTagsBySession(db, sessionId)) {
3689
+ const id = tag.type === "tool" ? tag.toolOwnerMessageId : contentTagOwnerMessageId(tag.messageId);
3690
+ if (id)
3691
+ maxTags.set(id, Math.max(maxTags.get(id) ?? 0, tag.tagNumber));
3692
+ }
3693
+ const messageTags = new Map;
3694
+ const gone = new Set(getReasoningRemovalState(db, sessionId).messageIds);
3695
+ for (const message of messages) {
3696
+ if (typeof message.info.id !== "string")
3697
+ continue;
3698
+ const tag = maxTags.get(message.info.id) ?? 0;
3699
+ messageTags.set(message, tag);
3700
+ if (tag > 0 && tag <= watermark)
3701
+ gone.add(message.info.id);
3702
+ }
3703
+ return opencodeReasoningBudgetCutoff({
3704
+ messages,
3705
+ messageTagNumbers: messageTags,
3706
+ budget,
3707
+ alreadyRemoved: gone,
3708
+ textEstimateByMessageId: getReasoningTokenEstimatesByMessage(db, sessionId, proseRatio),
3709
+ proseRatio,
3710
+ frozenMergedIds: getMergedReasoningStrippedIds(db, sessionId),
3711
+ alsoGone: new Set([...getMergedReasoningStrippedIds(db, sessionId)].filter((id) => id.startsWith(THINKING_BINDING_RECOVERY_FROZEN_PREFIX)).map((id) => id.slice(THINKING_BINDING_RECOVERY_FROZEN_PREFIX.length)))
3712
+ });
3713
+ }
3714
+ var DEFAULT_KEEP_REASONING_TOKENS = 1e4;
3715
+ var UNKNOWN_REASONING_STEP_TOKENS = 1000;
3716
+ function resolveKeepReasoningTokens(config, modelKey) {
3717
+ const values = config && typeof config === "object" ? Object.assign(Object.create(null), config) : undefined;
3718
+ return typeof config === "number" ? config : resolveModelConfigValue(values, modelKey)?.value ?? values?.default ?? DEFAULT_KEEP_REASONING_TOKENS;
3719
+ }
3720
+ function reasoningStepCost(reported, textEstimate, opaque) {
3721
+ if (reported !== undefined && Number.isFinite(reported) && reported > 0)
3722
+ return reported;
3723
+ if (textEstimate > 0)
3724
+ return Math.ceil(textEstimate);
3725
+ return opaque ? UNKNOWN_REASONING_STEP_TOKENS : 0;
3726
+ }
3727
+ function reasoningBudgetCutoff(steps, budget) {
3728
+ const cost = (step) => step.alreadyRemoved ? 0 : typeof step.cost === "function" ? step.cost() : step.cost;
3729
+ let kept = 0;
3730
+ let upperBound = Number.POSITIVE_INFINITY;
3731
+ for (const step of steps) {
3732
+ if (!step.exempt)
3733
+ continue;
3734
+ kept += cost(step);
3735
+ if (step.tag > 0)
3736
+ upperBound = Math.min(upperBound, step.tag - 1);
3737
+ }
3738
+ for (let index = steps.length - 1;index >= 0; index--) {
3739
+ const step = steps[index];
3740
+ if (step.exempt || step.alreadyRemoved)
3741
+ continue;
3742
+ const tokens = cost(step);
3743
+ if (kept + tokens <= budget) {
3744
+ kept += tokens;
3745
+ if (step.tag > 0)
3746
+ upperBound = Math.min(upperBound, step.tag - 1);
3747
+ continue;
3748
+ }
3749
+ let tag = step.tag;
3750
+ for (let older = index - 1;tag <= 0 && older >= 0; older--)
3751
+ tag = steps[older].tag;
3752
+ return Math.max(0, Math.min(tag, upperBound));
3753
+ }
3754
+ return 0;
3755
+ }
3756
+ function reasoningTextAndOpaque(parts) {
3757
+ let text = "";
3758
+ let inlineText = "";
3759
+ let opaque = false;
3760
+ let hasReasoning = false;
3761
+ const detailText = new Set;
3762
+ for (const part of parts) {
3763
+ if (!isRecord(part))
3764
+ continue;
3765
+ if (part.ignored === true)
3766
+ continue;
3767
+ if (isRecord(part.metadata)) {
3768
+ const router = part.metadata.openrouter;
3769
+ if (isRecord(router) && Array.isArray(router.reasoning_details) && router.reasoning_details.length > 0) {
3770
+ hasReasoning = true;
3771
+ opaque = true;
3772
+ for (const detail of router.reasoning_details) {
3773
+ if (!isRecord(detail))
3774
+ continue;
3775
+ if (detail.type === "reasoning.text" && typeof detail.text === "string")
3776
+ detailText.add(detail.text);
3777
+ if (detail.type === "reasoning.summary" && typeof detail.summary === "string")
3778
+ detailText.add(detail.summary);
3779
+ }
3780
+ }
3781
+ }
3782
+ if (["reasoning", "thinking", "redacted_thinking"].includes(String(part.type))) {
3783
+ hasReasoning = true;
3784
+ const visible = part.type === "redacted_thinking" || part.redacted === true ? "" : typeof part.thinking === "string" ? part.thinking : typeof part.text === "string" ? part.text : "";
3785
+ if (visible !== "[cleared]")
3786
+ text += visible;
3787
+ opaque ||= part.type === "redacted_thinking" || part.redacted === true || visible === "" && (part.metadata !== undefined || part.signature !== undefined || part.thinkingSignature !== undefined);
3788
+ } else if (part.type === "text" && typeof part.text === "string") {
3789
+ for (const match of part.text.matchAll(/<(?:thinking|think)>([\s\S]*?)<\/(?:thinking|think)>/gi))
3790
+ inlineText += match[1];
3791
+ }
3792
+ }
3793
+ return { text: text || [...detailText].join(""), inlineText, opaque, hasReasoning };
3794
+ }
3795
+ function opencodeReasoningBudgetCutoff(args) {
3796
+ const assistants = args.messages.filter((message) => message.info.role === "assistant");
3797
+ const newest = assistants.at(-1);
3798
+ const exempt = findLatestAssistantReasoningMutationExemptMessage(args.messages);
3799
+ return reasoningBudgetCutoff(assistants.map((message) => ({
3800
+ tag: args.messageTagNumbers.get(message) ?? 0,
3801
+ exempt: message === newest || message === exempt || isInActiveAnthropicTurn(args.messages, args.messages.indexOf(message), args.anthropic ?? hasAnthropicReasoning(args.messages)),
3802
+ cost: () => {
3803
+ const costMessage = { ...message, parts: [...message.parts] };
3804
+ if (args.frozenMergedIds)
3805
+ stripReasoningFromMergedAssistants([costMessage], "anthropic", {
3806
+ frozenMessageIds: args.frozenMergedIds
3807
+ });
3808
+ const visible = reasoningTextAndOpaque(args.countNeutralized ? costMessage.parts.map(neutralizedReasoningSource) : costMessage.parts);
3809
+ if (!visible.hasReasoning && !visible.inlineText)
3810
+ return 0;
3811
+ const info = message.info;
3812
+ const tokens = isRecord(info.tokens) ? info.tokens.reasoning : undefined;
3813
+ const typedGone = typeof message.info.id === "string" && (args.alreadyRemoved?.has(message.info.id) === true || args.alsoGone?.has(message.info.id) === true);
3814
+ return (visible.hasReasoning && !typedGone ? reasoningStepCost(typeof tokens === "number" ? tokens : undefined, estimateTokens(visible.text) * (args.proseRatio ?? 1), visible.opaque) : 0) + Math.ceil(estimateTokens(visible.inlineText) * (args.proseRatio ?? 1));
3815
+ }
3816
+ })), args.budget);
3817
+ }
3818
+
3819
+ // src/hooks/magic-context/reasoning-budget-status.ts
3820
+ var sources = new Map;
3821
+ function captureOpencodeReasoningBudgetStatus(sessionId, messages, budget, prefixBound, proseRatio = 1) {
3822
+ if (messages.some((message) => !isRecord(message) || !isRecord(message.info) || typeof message.info.role !== "string" || !Array.isArray(message.parts))) {
3823
+ sources.delete(sessionId);
3824
+ return;
3825
+ }
3826
+ const compact = messages.filter((message) => message.info.role === "assistant").map((message) => {
3827
+ const info = message.info;
3828
+ const visible = reasoningTextAndOpaque(message.parts);
3829
+ const parts = message.parts.filter(isRecord).map((part) => ({
3830
+ type: typeof part.type === "string" ? part.type : "",
3831
+ text: typeof part.text === "string" && part.type !== "redacted_thinking" && part.redacted !== true ? part.text : undefined,
3832
+ thinking: typeof part.thinking === "string" && part.redacted !== true ? part.thinking : undefined,
3833
+ ignored: part.ignored === true,
3834
+ redacted: part.redacted === true,
3835
+ metadata: part.metadata === undefined ? undefined : {},
3836
+ signature: part.signature === undefined ? undefined : true,
3837
+ thinkingSignature: part.thinkingSignature === undefined ? undefined : true
3838
+ }));
3839
+ if (visible.hasReasoning && !reasoningTextAndOpaque(parts).hasReasoning)
3840
+ parts.push({
3841
+ type: "reasoning",
3842
+ text: visible.text,
3843
+ thinking: undefined,
3844
+ ignored: false,
3845
+ redacted: false,
3846
+ metadata: visible.opaque ? {} : undefined,
3847
+ signature: undefined,
3848
+ thinkingSignature: undefined
3849
+ });
3850
+ return {
3851
+ info: {
3852
+ role: info.role,
3853
+ tokens: isRecord(info.tokens) && typeof info.tokens.reasoning === "number" ? { reasoning: info.tokens.reasoning } : undefined
3854
+ },
3855
+ parts
3856
+ };
3857
+ });
3858
+ sources.delete(sessionId);
3859
+ sources.set(sessionId, { messages: compact, budget, prefixBound, proseRatio });
3860
+ if (sources.size > 32)
3861
+ sources.delete(sources.keys().next().value);
3862
+ }
3863
+ function reasoningBudgetStatusLine(sessionId) {
3864
+ try {
3865
+ const source = sources.get(sessionId);
3866
+ if (!source)
3867
+ return;
3868
+ source.status ??= opencodeReasoningBudgetStatus(source.messages, source.budget, source.prefixBound, source.proseRatio);
3869
+ return formatReasoningBudgetStatus(source.status);
3870
+ } catch {
3871
+ return;
3872
+ }
3873
+ }
3874
+ function formatReasoningBudgetStatus(status) {
3875
+ const count = (tokens) => tokens >= 1000 ? `${Number((tokens / 1000).toFixed(1))}k` : String(Math.round(tokens));
3876
+ return `Reasoning kept: ${count(status.kept)} of ${count(status.budget)} (${status.estimated ? "estimated" : "reported"})${status.overrun ? ` (over budget: ${status.overrun})` : ""}`;
3877
+ }
3878
+ function opencodeReasoningBudgetStatus(messages, budget, prefixBound, proseRatio = 1) {
3879
+ const assistants = messages.filter((message) => message.info.role === "assistant");
3880
+ const newest = assistants.at(-1);
3881
+ const exempt = findLatestAssistantReasoningMutationExemptMessage(messages);
3882
+ let kept = 0;
3883
+ let exemptCost = 0;
3884
+ let estimated = false;
3885
+ for (const message of assistants) {
3886
+ const visible = reasoningTextAndOpaque(message.parts);
3887
+ const info = message.info;
3888
+ const reported = isRecord(info.tokens) && typeof info.tokens.reasoning === "number" ? info.tokens.reasoning : undefined;
3889
+ const cost = (visible.hasReasoning ? reported !== undefined && Number.isFinite(reported) && reported > 0 ? reported : reasoningStepCost(reported, estimateTokens(visible.text) * proseRatio, visible.opaque) : 0) + Math.ceil(estimateTokens(visible.inlineText) * proseRatio);
3890
+ kept += cost;
3891
+ if (message === newest || message === exempt)
3892
+ exemptCost += cost;
3893
+ if (cost > 0 && (!(reported && Number.isFinite(reported) && reported > 0) || visible.inlineText !== ""))
3894
+ estimated = true;
3895
+ }
3896
+ return {
3897
+ kept,
3898
+ budget,
3899
+ estimated,
3900
+ overrun: kept > budget ? exemptCost > budget ? "newest step" : prefixBound ? "signed prefix" : "awaiting rebuild" : undefined
3901
+ };
3902
+ }
3903
+
5312
3904
  // src/hooks/magic-context/supersession-reclaim.ts
5313
3905
  var SUPERSESSION_RECENT_MESSAGE_WINDOW = 20;
5314
3906
  function recentSupersessionOwnerMessageIds(db, sessionId) {
@@ -6073,6 +4665,7 @@ function hasRecentAssistantCommit(messages) {
6073
4665
  return false;
6074
4666
  }
6075
4667
  function tagMessages(sessionId, messages, tagger, db, options = {}) {
4668
+ const protectedThinkingParts = new Set([...options.protectedThinkingMessages ?? []].flatMap((message) => message.parts));
6076
4669
  const skipPrefixInjection = options.skipPrefixInjection === true;
6077
4670
  const onToolOwnerFallbackLookup = options.onToolOwnerFallbackLookup;
6078
4671
  const targets = new Map;
@@ -6280,6 +4873,8 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
6280
4873
  }
6281
4874
  targets.set(tagId, {
6282
4875
  message,
4876
+ dropReasoningParts: thinkingParts,
4877
+ mutationParts: [{ message, part: textPart }],
6283
4878
  textPrefix: skipPrefixInjection ? "" : prependTag(tagId, ""),
6284
4879
  setContent: (content, options) => {
6285
4880
  if (textPart.text === content)
@@ -6291,6 +4886,8 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
6291
4886
  if (options?.keepReasoning === true)
6292
4887
  return true;
6293
4888
  for (const tp of thinkingParts) {
4889
+ if (protectedThinkingParts.has(tp))
4890
+ continue;
6294
4891
  if (partialEnd !== undefined && message.parts.indexOf(tp) > partialEnd)
6295
4892
  continue;
6296
4893
  neutralizeDroppedReasoningPart(tp);
@@ -6360,6 +4957,7 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
6360
4957
  messageTagNumbers.set(message, Math.max(messageTagNumbers.get(message) ?? 0, tagId));
6361
4958
  targets.set(tagId, {
6362
4959
  message,
4960
+ mutationParts: [{ message, part: filePart }],
6363
4961
  setContent: (content) => {
6364
4962
  const prev = messageParts[partIndex];
6365
4963
  const prevText = typeof prev === "object" && prev !== null && "text" in prev ? prev.text : "";
@@ -6403,7 +5001,7 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
6403
5001
  if (touchesUncoveredPart)
6404
5002
  continue;
6405
5003
  const thinkingParts = toolThinkingByCallId.get(compositeKey) ?? [];
6406
- targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch, tagId));
5004
+ targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts.filter((part) => !protectedThinkingParts.has(part)), toolCallIndex, batch, tagId));
6407
5005
  }
6408
5006
  const hasRecentReduceCall = lastReduceMessageIndex >= 0 && messages.length - lastReduceMessageIndex <= RECENT_REDUCE_LOOKBACK;
6409
5007
  return {
@@ -6546,7 +5144,8 @@ async function applyTodoSynthesis2(args) {
6546
5144
  return 0;
6547
5145
  }
6548
5146
  function freezeAllReasoningForBindingRecovery(args) {
6549
- const messageIds = findReasoningBearingAssistantIds(args.messages);
5147
+ const protectedMessages = args.protectedMessages ?? new Set;
5148
+ const messageIds = findReasoningBearingAssistantIds(args.messages.filter((message) => !protectedMessages.has(message)));
6550
5149
  const newIds = messageIds.filter((id) => !args.recoveredMessageIds.has(id));
6551
5150
  if (newIds.length > 0 && !addMergedReasoningStrippedIds(args.db, args.sessionId, newIds.map(thinkingBindingRecoveryFrozenId))) {
6552
5151
  return null;
@@ -6559,7 +5158,8 @@ function countAutoSearchHints(db, sessionId) {
6559
5158
  return getAutoSearchHintDecisions(db, sessionId).filter((decision) => decision.decision === "hint").length;
6560
5159
  }
6561
5160
  function freezeReasoningOnBustingPass2(args) {
6562
- const messageIds = findReasoningBearingAssistantIds(args.messages).filter((id) => !args.alreadyFrozen.has(id));
5161
+ const protectedMessages = args.protectedMessages ?? new Set;
5162
+ const messageIds = findReasoningBearingAssistantIds(args.messages.filter((message) => !protectedMessages.has(message))).filter((id) => !args.alreadyFrozen.has(id));
6563
5163
  if (messageIds.length === 0)
6564
5164
  return { strip: null, persistenceFailed: false };
6565
5165
  let persisted = false;
@@ -6646,7 +5246,7 @@ function replayRustModeBindingMismatchStrips2(args) {
6646
5246
  if (messageId.length > 0)
6647
5247
  recoveryMessageIds.add(messageId);
6648
5248
  }
6649
- stripReasoningFromAssistantIds(args.messages, args.resolvedProviderID, recoveryMessageIds);
5249
+ stripReasoningFromAssistantIds(args.messages, args.resolvedProviderID, recoveryMessageIds, args.protectedThinkingMessages);
6650
5250
  }
6651
5251
  var RUST_MARKER_LOCK_SKIP_LOG2 = "rust compaction-marker: pending target write skipped on lock contention; next cache-busting pass retries";
6652
5252
  function isSqliteLockContentionError(error) {
@@ -6759,13 +5359,14 @@ function applyRustModeThinkingStrips(args, hostEditBeforeNewestThinking) {
6759
5359
  if (messageId.length > 0)
6760
5360
  recoveryMessageIds.add(messageId);
6761
5361
  }
6762
- const flagTarget = args.thinkingBindingRecoveryEnabledForModel ? getThinkingBindingRecoveryTarget(args.db, args.sessionId) : null;
5362
+ const flagTarget = !args.activeThinkingTurn && args.thinkingBindingRecoveryEnabledForModel ? getThinkingBindingRecoveryTarget(args.db, args.sessionId) : null;
6763
5363
  if (flagTarget) {
6764
5364
  thinkingBindingRecovery = freezeAllReasoningForBindingRecovery({
6765
5365
  db: args.db,
6766
5366
  sessionId: args.sessionId,
6767
5367
  messages: args.messages,
6768
5368
  flagTarget,
5369
+ protectedMessages: args.protectedThinkingMessages,
6769
5370
  recoveredMessageIds: recoveryMessageIds
6770
5371
  });
6771
5372
  if (!thinkingBindingRecovery) {
@@ -6775,24 +5376,26 @@ function applyRustModeThinkingStrips(args, hostEditBeforeNewestThinking) {
6775
5376
  } catch (error) {
6776
5377
  sessionLog(args.sessionId, "rust thinking binding recovery failed:", error);
6777
5378
  }
6778
- stripReasoningFromAssistantIds(args.messages, args.resolvedProviderID, recoveryMessageIds);
6779
- const moduleBustStrips = args.cacheBustingPass === true && (args.moduleReasoningTrimOnly !== true || hostEditBeforeNewestThinking);
5379
+ stripReasoningFromAssistantIds(args.messages, args.resolvedProviderID, recoveryMessageIds, args.protectedThinkingMessages);
5380
+ const moduleBustStrips = !args.activeThinkingTurn && args.cacheBustingPass === true && (args.moduleReasoningTrimOnly !== true || hostEditBeforeNewestThinking);
6780
5381
  const stripFrom = args.thinkingBindingRecoveryEnabledForModel === true ? proactiveStripStartIndex({ ...args, cacheBustingPass: moduleBustStrips }) : null;
6781
5382
  if (stripFrom !== null) {
6782
5383
  const outcome = freezeReasoningOnBustingPass2({
6783
5384
  db: args.db,
6784
5385
  sessionId: args.sessionId,
6785
5386
  messages: args.messages.slice(stripFrom),
5387
+ protectedMessages: args.protectedThinkingMessages,
6786
5388
  alreadyFrozen: recoveryMessageIds
6787
5389
  });
6788
5390
  if (outcome.strip) {
6789
5391
  proactiveThinkingStrip = outcome.strip;
6790
5392
  for (const id of outcome.strip.messageIds)
6791
5393
  recoveryMessageIds.add(id);
6792
- stripReasoningFromAssistantIds(args.messages, args.resolvedProviderID, new Set(outcome.strip.messageIds));
5394
+ stripReasoningFromAssistantIds(args.messages, args.resolvedProviderID, new Set(outcome.strip.messageIds), args.protectedThinkingMessages);
6793
5395
  }
6794
5396
  }
6795
5397
  }
5398
+ args.restoreLatestTurnOriginals?.();
6796
5399
  return { thinkingBindingRecovery, proactiveThinkingStrip };
6797
5400
  }
6798
5401
  function runRustModePostprocess2(args) {
@@ -7042,6 +5645,8 @@ function evaluateEmergencyFailClosed2(input) {
7042
5645
  };
7043
5646
  }
7044
5647
  function finalizeMessageRepresentation2(messages, resolvedProviderID, options) {
5648
+ const protectedMessages = options?.protectedThinkingMessages ?? new Set;
5649
+ const restoredMessages = new Set(messages.filter((message) => protectedMessages.has(message) && typeof message.info.id === "string" && options?.restoreThinkingMessageIds?.has(message.info.id)));
7045
5650
  let clearedParts = 0;
7046
5651
  if (modelAcceptsEmptyContent(resolvedProviderID)) {
7047
5652
  const prependedMessageCount = Math.min(messages.length, Math.max(0, options?.prependedMessageCount ?? 0));
@@ -7056,13 +5661,15 @@ function finalizeMessageRepresentation2(messages, resolvedProviderID, options) {
7056
5661
  }
7057
5662
  }
7058
5663
  if (targetedMessages.length > 0) {
7059
- clearedParts = stripClearedReasoning(targetedMessages);
5664
+ clearedParts = stripClearedReasoning(targetedMessages.filter((message) => !restoredMessages.has(message)));
7060
5665
  }
7061
5666
  }
7062
- const bindingRecoveryParts = options?.skipMergedReasoningStrip ? 0 : stripReasoningFromAssistantIds(messages, resolvedProviderID, options?.thinkingBindingRecoveryMessageIds ?? new Set);
5667
+ const bindingRecoveryParts = options?.skipMergedReasoningStrip ? 0 : stripReasoningFromAssistantIds(messages, resolvedProviderID, options?.frozenThinkingBindingMessageIds ?? new Set, restoredMessages);
7063
5668
  const mergedReasoningParts = bindingRecoveryParts + (options?.skipMergedReasoningStrip ? 0 : stripReasoningFromMergedAssistants(messages, resolvedProviderID, {
7064
5669
  mutationExemptMessage: options?.reasoningMutationExemptMessage,
7065
- frozenMessageIds: options?.mergedReasoningStrippedIds
5670
+ frozenMessageIds: options?.mergedReasoningStrippedIds,
5671
+ protectedMessages,
5672
+ restoreMessages: restoredMessages
7066
5673
  }));
7067
5674
  if (!options?.skipTrailingWhitespaceStrip && modelAcceptsEmptyContent(resolvedProviderID)) {
7068
5675
  applyFrozenTrailingBlankDecisions(messages, options?.trailingBlankDecisions ?? new Map);
@@ -7111,7 +5718,29 @@ async function runPostTransformPhase2(args) {
7111
5718
  const deferredMaterialize = args.canConsumeDeferredLate && deferredMaterializationWasPending;
7112
5719
  const materializationRequested = isExplicitFlush || deferredMaterialize;
7113
5720
  const m0M1EnabledForFold = args.m0M1 !== undefined && (!!args.m0M1.projectPath || !!args.m0M1.projectDirectory) && (args.fullFeatureMode || compactionOff);
7114
- const freezeM0M1 = args.freezeM0M1 === true;
5721
+ const frozenThinking = new Set(args.activeThinkingTurn ? getMergedReasoningStrippedIds(args.db, args.sessionId) : []);
5722
+ const bindingThinking = new Set([...frozenThinking].filter((id) => id.startsWith(THINKING_BINDING_RECOVERY_FROZEN_PREFIX)).map((id) => id.slice(THINKING_BINDING_RECOVERY_FROZEN_PREFIX.length)));
5723
+ const retainedThinking = args.activeThinkingTurn ? retainedActiveThinkingParts({
5724
+ messages: args.messages,
5725
+ providerID: args.resolvedProviderID,
5726
+ modelID: args.resolvedModelID,
5727
+ mergedIds: frozenThinking,
5728
+ bindingIds: bindingThinking,
5729
+ removedIds: getReasoningRemovalState(args.db, args.sessionId).messageIds
5730
+ }) : new Set;
5731
+ const activeThinkingTurn = retainedThinking.size > 0;
5732
+ const protectedThinkingMessages = new Set(args.messages.filter((message) => message.parts.some((part) => retainedThinking.has(part))));
5733
+ const newTargets = protectNewTagMutations(args.messages, args.targets, retainedThinking, args.thinkingBindingRecoveryEnabledForModel === true);
5734
+ let maxReasoningTag = 0;
5735
+ for (const tag of args.messageTagNumbers.values())
5736
+ maxReasoningTag = Math.max(maxReasoningTag, tag);
5737
+ let safeReasoningCutoff = maxReasoningTag;
5738
+ for (const message of protectedThinkingMessages) {
5739
+ const tag = args.messageTagNumbers.get(message);
5740
+ if (tag && tag > 0)
5741
+ safeReasoningCutoff = Math.min(safeReasoningCutoff, tag - 1);
5742
+ }
5743
+ const freezeM0M1 = args.freezeM0M1 === true || activeThinkingTurn && args.thinkingBindingRecoveryEnabledForModel === true;
7115
5744
  const foldDueDecision = m0M1EnabledForFold && args.m0M1 && !freezeM0M1 ? mustMaterialize({
7116
5745
  db: args.db,
7117
5746
  sessionId: args.sessionId,
@@ -7228,6 +5857,8 @@ async function runPostTransformPhase2(args) {
7228
5857
  const shouldReadPendingOps = !compactionOff && (materializationRequested || publishedM1RefreshedThisPass || args.schedulerDecision === "execute" || forceMaterialization || foldExecutedThisPass || firstRenderBust || compartmentRunning);
7229
5858
  const pendingOps = shouldReadPendingOps ? getPendingOps(args.db, args.sessionId) : [];
7230
5859
  const hasPendingUserOps = pendingOps.length > 0;
5860
+ if (args.schedulerDecision === "execute" && pendingOps.some((op) => newTargets.get(op.tagId)?.thinkingDropProtected))
5861
+ args.pendingMaterializationSessions.add(args.sessionId);
7231
5862
  const formatPendingOpsDepth = () => {
7232
5863
  const depth = getPendingOpsCount(args.db, args.sessionId);
7233
5864
  return depth === null ? "not loaded (deferred pass)" : String(depth);
@@ -7345,7 +5976,7 @@ async function runPostTransformPhase2(args) {
7345
5976
  const applyReason = isExplicitFlush ? "explicit_flush" : deferredMaterialize ? "deferred_materialization" : foldBustsServedPrefixThisPass && args.schedulerDecision !== "execute" ? `m0_hard_fold (drain folded into executed m[0] bust, scheduler=${args.schedulerDecision})` : `${reclaimRideLabel(rideSignals)} (scheduler=${args.schedulerDecision})`;
7346
5977
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${formatPendingOpsDepth()}, context=${args.contextUsage.percentage.toFixed(1)}%`);
7347
5978
  const tApply = performance.now();
7348
- pendingOpsDidMutate = applyPendingOperations(args.sessionId, args.db, args.targets, new Set([
5979
+ pendingOpsDidMutate = applyPendingOperations(args.sessionId, args.db, newTargets, new Set([
7349
5980
  ...args.contextUsage.percentage >= 95 ? [] : args.protectedTagIds,
7350
5981
  ...protectedToolTags
7351
5982
  ]), undefined, pendingOps, [], new Set, (reduction) => droppedTokenReductions.push(reduction));
@@ -7374,7 +6005,7 @@ async function runPostTransformPhase2(args) {
7374
6005
  clearEmergencyDropSample(args.db, args.sessionId);
7375
6006
  }
7376
6007
  let routineCleanupApplied = (emergencyDropEligible ? args.contextUsage.percentage >= 95 || getEmergencyInputSample(args.db, args.sessionId) === 0 : !args.fullFeatureMode || !routinePressureAlreadyApplied) || materializationRequested || emergencyRewriteAlreadyPriced;
7377
- let cleanup = applyHeuristicCleanup(args.sessionId, args.db, args.targets, args.messageTagNumbers, {
6008
+ let cleanup = applyHeuristicCleanup(args.sessionId, args.db, newTargets, args.messageTagNumbers, {
7378
6009
  protectedTagNumbers: args.protectedTagNumbers,
7379
6010
  protectedCutoff: args.protectedCutoff,
7380
6011
  protectedToolTags,
@@ -7388,7 +6019,7 @@ async function runPostTransformPhase2(args) {
7388
6019
  caveman: cavemanConfig
7389
6020
  }, heuristicTags);
7390
6021
  if (!routineCleanupApplied && cleanup.emergencyDroppedTools > 0) {
7391
- const ridingCleanup = applyHeuristicCleanup(args.sessionId, args.db, args.targets, args.messageTagNumbers, {
6022
+ const ridingCleanup = applyHeuristicCleanup(args.sessionId, args.db, newTargets, args.messageTagNumbers, {
7392
6023
  protectedTagNumbers: args.protectedTagNumbers,
7393
6024
  protectedCutoff: args.protectedCutoff,
7394
6025
  protectedToolTags,
@@ -7421,34 +6052,45 @@ async function runPostTransformPhase2(args) {
7421
6052
  droppedTokenReductions.push(...cleanup.droppedTokenReductions);
7422
6053
  const t7 = performance.now();
7423
6054
  const ageLaneAllowed = !prefixBoundModel;
7424
- const clearedReasoning = routineCleanupApplied && canUseEmptySentinels && ageLaneAllowed ? clearOldReasoning(args.messages, args.reasoningByMessage, args.messageTagNumbers, args.clearReasoningAge) : 0;
6055
+ const bindingStrippedIds = new Set;
6056
+ if (routineCleanupApplied && prefixBoundModel) {
6057
+ for (const id of getMergedReasoningStrippedIds(args.db, args.sessionId)) {
6058
+ if (id.startsWith(THINKING_BINDING_RECOVERY_FROZEN_PREFIX)) {
6059
+ bindingStrippedIds.add(id.slice(THINKING_BINDING_RECOVERY_FROZEN_PREFIX.length));
6060
+ }
6061
+ }
6062
+ }
6063
+ const budgetCutoff = routineCleanupApplied ? Math.min(safeReasoningCutoff, opencodeReasoningBudgetCutoff({
6064
+ messages: args.messages,
6065
+ messageTagNumbers: args.messageTagNumbers,
6066
+ budget: args.keepReasoningTokens ?? DEFAULT_KEEP_REASONING_TOKENS,
6067
+ alreadyRemoved: removedReasoningIds,
6068
+ alsoGone: bindingStrippedIds,
6069
+ countNeutralized: reasoningRemovalSelectable,
6070
+ proseRatio: sessionDecisionCalibration(args.db, args.sessionId).proseRatio,
6071
+ frozenMergedIds: getMergedReasoningStrippedIds(args.db, args.sessionId),
6072
+ anthropic: canUseEmptySentinels || prefixBoundModel || hasAnthropicReasoning(args.messages),
6073
+ textEstimateByMessageId: getReasoningTokenEstimatesByMessage(args.db, args.sessionId, sessionDecisionCalibration(args.db, args.sessionId).proseRatio)
6074
+ })) : 0;
6075
+ const clearedReasoning = routineCleanupApplied && canUseEmptySentinels && ageLaneAllowed ? clearOldReasoning(args.messages, args.reasoningByMessage, args.messageTagNumbers, budgetCutoff) : 0;
7425
6076
  if (routineCleanupApplied && canUseEmptySentinels) {
7426
6077
  stripClearedReasoning(args.messages);
7427
6078
  }
7428
- const strippedInline = routineCleanupApplied && ageLaneAllowed ? stripInlineThinking(args.messages, args.messageTagNumbers, args.clearReasoningAge) : 0;
6079
+ const strippedInline = routineCleanupApplied && ageLaneAllowed ? stripInlineThinking(args.messages, args.messageTagNumbers, budgetCutoff) : 0;
7429
6080
  const persistedInlineWatermark = args.sessionMeta?.clearedReasoningThroughTag ?? 0;
7430
6081
  if (routineCleanupApplied && persistedInlineWatermark > 0 && !compactionOff) {
7431
6082
  replayStrippedInlineThinking(args.messages, args.messageTagNumbers, persistedInlineWatermark);
7432
6083
  }
7433
6084
  let removedReasoningMessages = 0;
7434
6085
  if (routineCleanupApplied && reasoningRemovalSelectable && reasoningRemovalReadable) {
7435
- const bindingStrippedIds = new Set;
7436
- if (prefixBoundModel) {
7437
- for (const id of getMergedReasoningStrippedIds(args.db, args.sessionId)) {
7438
- if (!id.startsWith(THINKING_BINDING_RECOVERY_FROZEN_PREFIX))
7439
- continue;
7440
- const messageId = id.slice(THINKING_BINDING_RECOVERY_FROZEN_PREFIX.length);
7441
- if (messageId.length > 0)
7442
- bindingStrippedIds.add(messageId);
7443
- }
7444
- }
7445
6086
  const newIds = selectReasoningRemovals({
7446
6087
  messages: args.messages,
7447
6088
  messageTagNumbers: args.messageTagNumbers,
7448
- clearReasoningAge: args.clearReasoningAge,
6089
+ cutoff: budgetCutoff,
7449
6090
  alreadyRemoved: removedReasoningIds,
7450
6091
  prefixBound: prefixBoundModel,
7451
- alsoGone: bindingStrippedIds
6092
+ alsoGone: bindingStrippedIds,
6093
+ protectedMessages: protectedThinkingMessages
7452
6094
  });
7453
6095
  if (newIds.length > 0) {
7454
6096
  let persisted = false;
@@ -7478,12 +6120,7 @@ async function runPostTransformPhase2(args) {
7478
6120
  }
7479
6121
  }
7480
6122
  if (clearedReasoning > 0 || strippedInline > 0) {
7481
- let maxTag = 0;
7482
- for (const tag of args.messageTagNumbers.values()) {
7483
- if (tag > maxTag)
7484
- maxTag = tag;
7485
- }
7486
- const newWatermark = maxTag - args.clearReasoningAge;
6123
+ const newWatermark = budgetCutoff;
7487
6124
  const currentWatermark = args.sessionMeta?.clearedReasoningThroughTag ?? 0;
7488
6125
  if (newWatermark > currentWatermark) {
7489
6126
  updateSessionMeta(args.db, args.sessionId, {
@@ -7499,7 +6136,7 @@ async function runPostTransformPhase2(args) {
7499
6136
  heuristicOrReasoningDidMutate = heuristicMutationCount + clearedReasoning + strippedInline + removedReasoningMessages > 0;
7500
6137
  heuristicOrReasoningEditBesidesTrim = heuristicMutationCount + clearedReasoning + strippedInline > 0;
7501
6138
  droppedCount += clearedReasoning + strippedInline + removedReasoningMessages;
7502
- if (pendingMaterializationAtPassStart && !freezeM0M1) {
6139
+ if (pendingMaterializationAtPassStart && !freezeM0M1 && !pendingOps.some((op) => newTargets.get(op.tagId)?.thinkingDropProtected)) {
7503
6140
  args.pendingMaterializationSessions.delete(args.sessionId);
7504
6141
  }
7505
6142
  if (args.currentTurnId) {
@@ -7517,7 +6154,7 @@ async function runPostTransformPhase2(args) {
7517
6154
  const syntheticPendingOps = buildSyntheticToolReclaimOps({
7518
6155
  db: args.db,
7519
6156
  sessionId: args.sessionId,
7520
- targets: args.targets,
6157
+ targets: newTargets,
7521
6158
  watermark: args.sessionMeta.toolReclaimWatermark ?? 0,
7522
6159
  protectedToolTags,
7523
6160
  pendingOps
@@ -7529,7 +6166,7 @@ async function runPostTransformPhase2(args) {
7529
6166
  const supersessionOps = buildSupersessionReclaimOps({
7530
6167
  db: args.db,
7531
6168
  sessionId: args.sessionId,
7532
- targets: args.targets,
6169
+ targets: newTargets,
7533
6170
  pendingOps,
7534
6171
  recentMessageIds,
7535
6172
  protectedTagNumbers: args.protectedTagNumbers,
@@ -7544,7 +6181,7 @@ async function runPostTransformPhase2(args) {
7544
6181
  const editReclaim = buildEditSupersessionReclaim({
7545
6182
  db: args.db,
7546
6183
  sessionId: args.sessionId,
7547
- targets: args.targets,
6184
+ targets: newTargets,
7548
6185
  pendingOps,
7549
6186
  recentMessageIds,
7550
6187
  protectedTagNumbers: args.protectedTagNumbers,
@@ -7913,6 +6550,7 @@ async function runPostTransformPhase2(args) {
7913
6550
  }
7914
6551
  let autoSearchHintsBefore = 0;
7915
6552
  let autoSearchHintAppendedThisPass = false;
6553
+ let autoSearchCompletedThisPass = false;
7916
6554
  if (args.fullFeatureMode && args.autoSearch?.enabled && args.projectPath) {
7917
6555
  const tAutoSearch = performance.now();
7918
6556
  const visibleMemoryIds = getVisibleMemoryIds(args.db, args.sessionId) ?? undefined;
@@ -7933,6 +6571,7 @@ async function runPostTransformPhase2(args) {
7933
6571
  wordRules: args.autoSearch.wordRules
7934
6572
  }
7935
6573
  });
6574
+ autoSearchCompletedThisPass = autoSearchOutcome.ok;
7936
6575
  if (!autoSearchOutcome.ok) {
7937
6576
  args.passOutcome?.record(`auto-search-${autoSearchOutcome.kind}`);
7938
6577
  }
@@ -7940,7 +6579,7 @@ async function runPostTransformPhase2(args) {
7940
6579
  args.passOutcome?.record("auto-search-internal-failure");
7941
6580
  sessionLog(args.sessionId, "auto-search runner failed:", error);
7942
6581
  }
7943
- autoSearchHintAppendedThisPass = countAutoSearchHints(args.db, args.sessionId) !== autoSearchHintsBefore;
6582
+ autoSearchHintAppendedThisPass = autoSearchCompletedThisPass && countAutoSearchHints(args.db, args.sessionId) !== autoSearchHintsBefore;
7944
6583
  logTransformTiming(args.sessionId, "pp.autoSearchHint", tAutoSearch);
7945
6584
  }
7946
6585
  if (args.fullFeatureMode && isCacheBustingPass) {
@@ -7977,7 +6616,7 @@ async function runPostTransformPhase2(args) {
7977
6616
  }
7978
6617
  }
7979
6618
  const mergedReasoningStrippedIds = new Set(replaySnapshot?.mergedReasoningStrippedIds ?? []);
7980
- const thinkingBindingRecoveryMessageIds = new Set;
6619
+ const frozenThinkingBindingMessageIds = new Set;
7981
6620
  let thinkingBindingRecovery = null;
7982
6621
  if (!compactionOff) {
7983
6622
  try {
@@ -7985,17 +6624,18 @@ async function runPostTransformPhase2(args) {
7985
6624
  if (id.startsWith(THINKING_BINDING_RECOVERY_FROZEN_PREFIX)) {
7986
6625
  const messageId = id.slice(THINKING_BINDING_RECOVERY_FROZEN_PREFIX.length);
7987
6626
  if (messageId.length > 0)
7988
- thinkingBindingRecoveryMessageIds.add(messageId);
6627
+ frozenThinkingBindingMessageIds.add(messageId);
7989
6628
  }
7990
6629
  }
7991
- const flagTarget = args.thinkingBindingRecoveryEnabledForModel ? replaySnapshot?.thinkingBindingRecoveryTarget ?? null : null;
6630
+ const flagTarget = !activeThinkingTurn && args.thinkingBindingRecoveryEnabledForModel ? replaySnapshot?.thinkingBindingRecoveryTarget ?? null : null;
7992
6631
  if (flagTarget) {
7993
6632
  thinkingBindingRecovery = freezeAllReasoningForBindingRecovery({
7994
6633
  db: args.db,
7995
6634
  sessionId: args.sessionId,
7996
6635
  messages: args.messages,
7997
6636
  flagTarget,
7998
- recoveredMessageIds: thinkingBindingRecoveryMessageIds
6637
+ protectedMessages: args.protectedThinkingMessages,
6638
+ recoveredMessageIds: frozenThinkingBindingMessageIds
7999
6639
  });
8000
6640
  if (thinkingBindingRecovery) {
8001
6641
  for (const messageId of thinkingBindingRecovery.messageIds) {
@@ -8010,7 +6650,10 @@ async function runPostTransformPhase2(args) {
8010
6650
  }
8011
6651
  }
8012
6652
  if (canUseEmptySentinels && isCacheBustingPass) {
8013
- const candidates = findMergedReasoningStripDecisions(args.messages, args.resolvedProviderID, mergedReasoningStrippedIds, { mutationExemptMessage: reasoningMutationExemptMessage });
6653
+ const candidates = findMergedReasoningStripDecisions(args.messages, args.resolvedProviderID, mergedReasoningStrippedIds, {
6654
+ mutationExemptMessage: reasoningMutationExemptMessage,
6655
+ protectedMessages: protectedThinkingMessages
6656
+ });
8014
6657
  const newlyDetectedIds = candidates.filter((id) => !mergedReasoningStrippedIds.has(id));
8015
6658
  if (newlyDetectedIds.length > 0) {
8016
6659
  const persisted = addMergedReasoningStrippedIds(args.db, args.sessionId, newlyDetectedIds);
@@ -8116,11 +6759,14 @@ async function runPostTransformPhase2(args) {
8116
6759
  logTransformTiming(args.sessionId, "pp.frozenDecisions", tFrozenDecisions);
8117
6760
  bustedThisPass ||= firstApplicationEdits.any;
8118
6761
  const finalizeOptions = {
6762
+ modelID: args.resolvedModelID,
6763
+ protectedThinkingMessages: args.protectedThinkingMessages,
8119
6764
  prependedMessageCount,
8120
6765
  reasoningMutatedMessages,
8121
6766
  reasoningMutationExemptMessage,
8122
6767
  mergedReasoningStrippedIds,
8123
- thinkingBindingRecoveryMessageIds,
6768
+ frozenThinkingBindingMessageIds,
6769
+ restoreThinkingMessageIds: args.restoreThinkingMessageIds,
8124
6770
  trailingBlankDecisions,
8125
6771
  skipMergedReasoningStrip: compactionOff,
8126
6772
  skipTrailingWhitespaceStrip: compactionOff
@@ -8132,7 +6778,7 @@ async function runPostTransformPhase2(args) {
8132
6778
  parts: [...message.parts]
8133
6779
  }));
8134
6780
  stripClearedReasoning(remaining);
8135
- stripReasoningFromAssistantIds(remaining, args.resolvedProviderID, thinkingBindingRecoveryMessageIds);
6781
+ stripReasoningFromAssistantIds(remaining, args.resolvedProviderID, frozenThinkingBindingMessageIds);
8136
6782
  stripReasoningFromMergedAssistants(remaining, args.resolvedProviderID, {
8137
6783
  frozenMessageIds: mergedReasoningStrippedIds,
8138
6784
  mutationExemptMessage: remaining[args.messages.indexOf(reasoningMutationExemptMessage)]
@@ -8159,12 +6805,13 @@ async function runPostTransformPhase2(args) {
8159
6805
  db: args.db,
8160
6806
  sessionId: args.sessionId,
8161
6807
  messages: args.messages,
8162
- alreadyFrozen: thinkingBindingRecoveryMessageIds
6808
+ protectedMessages: protectedThinkingMessages,
6809
+ alreadyFrozen: frozenThinkingBindingMessageIds
8163
6810
  });
8164
6811
  if (outcome.strip) {
8165
6812
  proactiveThinkingStrip = outcome.strip;
8166
6813
  for (const messageId of outcome.strip.messageIds) {
8167
- thinkingBindingRecoveryMessageIds.add(messageId);
6814
+ frozenThinkingBindingMessageIds.add(messageId);
8168
6815
  mergedReasoningStrippedIds.add(thinkingBindingRecoveryFrozenId(messageId));
8169
6816
  }
8170
6817
  bustedThisPass = true;
@@ -8196,6 +6843,9 @@ async function runPostTransformPhase2(args) {
8196
6843
  injectTemporalMarkers(args.messages, temporalDecisions);
8197
6844
  const finalRepresentation = finalizeMessageRepresentation2(args.messages, args.resolvedProviderID, finalizeOptions);
8198
6845
  const removedReasoningParts = reasoningRemovalEnabled ? removeReasoningParts(args.messages, removedReasoningIds, args.resolvedProviderID) : 0;
6846
+ const reasoningStatusRatio = sessionDecisionCalibration(args.db, args.sessionId).proseRatio;
6847
+ captureOpencodeReasoningBudgetStatus(args.sessionId, args.messages, args.keepReasoningTokens ?? DEFAULT_KEEP_REASONING_TOKENS, prefixBoundModel, reasoningStatusRatio);
6848
+ args.restoreLatestTurnOriginals?.();
8199
6849
  sessionLog(args.sessionId, `final representation: clearedParts=${finalRepresentation.clearedParts} mergedReasoningParts=${finalRepresentation.mergedReasoningParts} removedReasoningParts=${removedReasoningParts} settledDroppedReasoning=${settledDroppedReasoning}`);
8200
6850
  logTransformTiming(args.sessionId, "finalizeMessageRepresentation", tFinalRepresentation, `clearedParts=${finalRepresentation.clearedParts} mergedReasoningParts=${finalRepresentation.mergedReasoningParts}`);
8201
6851
  const tTailBaseline = performance.now();
@@ -8327,4 +6977,4 @@ function checkM0MutationDriftAndSignal2(args) {
8327
6977
  return false;
8328
6978
  }
8329
6979
 
8330
- export { indexCommitsForProject, embedUnembeddedCommits, createTagger, createUnifiedSearchDiagnostics, parseIdShapedQuery, resolveMemoriesByIdsForSearch, unifiedSearch, formatSearchResults, cavemanWordRulesForLanguage, findSessionId, findLastUserMessageId, clearAutoSearchForSession, setCtxReduceRegisteredGlobally, resolveCtxReduceAvailabilityFromMessages, resolveCtxReduceAvailability, primeCtxReduceSpawnPermission, ctxReduceSpawnPermissionReadNeeded, spawnAgentFromMessages, spawnAgentFromOpenCodeDb, todowritePermissionDenied, cachedToolPermissionDenied, clearToolPermissionDenied, resolveTodowriteAvailabilityFromMessages, resolveTodowriteAvailability, canConsumeDeferredOnThisPass, hasReclaimRide, reclaimRideLabel, replayCavemanCompression, DegradedPassRefusalError, degradedPassError, ENGINE_RECONNECTING_USER_MESSAGE, contextRefusalError, protectedToolRefusal, outgoingContextRefusal, EmergencyFailClosedError, replayClearedReasoning, replayStrippedInlineThinking, stripClearedReasoning, snapshotTrailingBlankSourceDecisions, applyFlushedStatuses, stripStructuralNoise, logTransformTiming, hasRecentAssistantCommit, tagMessages, resetDegradedCacheCount2, isSyntheticHeadMessage2, applyTodoSynthesis2, freezeReasoningOnBustingPass2, firstServedDivergenceIndex2, rustModeServedKeyAfterPersistedStrips2, replayRustModeBindingMismatchStrips2, RUST_MARKER_LOCK_SKIP_LOG2, applyRustModeDeferredCompactionMarker2, clearRustModeBoundaryRecord2, runRustModePostprocess2, reconcileMarkerRepresentation2, clearPendingCompactionMarkerAfterSuccessfulDrain2, defaultCompactionMarkerStrategy2, abortSessionFailClosed2, evaluateEmergencyFailClosed2, finalizeMessageRepresentation2, runPostTransformPhase2, checkM0MutationDriftAndSignal2 };
6980
+ export { createTagger, cavemanWordRulesForLanguage, findSessionId, findLastUserMessageId, clearAutoSearchForSession, setCtxReduceRegisteredGlobally, resolveCtxReduceAvailabilityFromMessages, resolveCtxReduceAvailability, primeCtxReduceSpawnPermission, ctxReduceSpawnPermissionReadNeeded, spawnAgentFromMessages, spawnAgentFromOpenCodeDb, todowritePermissionDenied, cachedToolPermissionDenied, clearToolPermissionDenied, resolveTodowriteAvailabilityFromMessages, resolveTodowriteAvailability, canConsumeDeferredOnThisPass, hasReclaimRide, reclaimRideLabel, replayClearedReasoning, replayStrippedInlineThinking, stripClearedReasoning, snapshotTrailingBlankSourceDecisions, projectOpencodeReasoningBudgetCutoff, resolveKeepReasoningTokens, reasoningBudgetCutoff, captureOpencodeReasoningBudgetStatus, reasoningBudgetStatusLine, ENGINE_RECONNECTING_USER_MESSAGE, contextRefusalError, protectedToolRefusal, outgoingContextRefusal, EmergencyFailClosedError, latestAssistantTurnStart, latestAssistantTurnMessages, hasActiveAnthropicThinkingTurn, replayCavemanCompression, DegradedPassRefusalError, degradedPassError, applyFlushedStatuses, stripStructuralNoise, logTransformTiming, hasRecentAssistantCommit, tagMessages, resetDegradedCacheCount2, isSyntheticHeadMessage2, applyTodoSynthesis2, freezeReasoningOnBustingPass2, firstServedDivergenceIndex2, rustModeServedKeyAfterPersistedStrips2, replayRustModeBindingMismatchStrips2, RUST_MARKER_LOCK_SKIP_LOG2, applyRustModeDeferredCompactionMarker2, clearRustModeBoundaryRecord2, runRustModePostprocess2, reconcileMarkerRepresentation2, clearPendingCompactionMarkerAfterSuccessfulDrain2, defaultCompactionMarkerStrategy2, abortSessionFailClosed2, evaluateEmergencyFailClosed2, finalizeMessageRepresentation2, runPostTransformPhase2, checkM0MutationDriftAndSignal2 };