@animalabs/agent-framework 0.32.0 → 0.34.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 (368) hide show
  1. package/README.md +340 -22
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +208 -80
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +345 -113
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/append-all.d.ts +5 -0
  17. package/dist/src/append-all.d.ts.map +1 -0
  18. package/dist/src/append-all.js +8 -0
  19. package/dist/src/append-all.js.map +1 -0
  20. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  21. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  22. package/dist/src/code-execution/isolate/scripts.js +24 -0
  23. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  24. package/dist/src/config-checks.d.ts +7 -0
  25. package/dist/src/config-checks.d.ts.map +1 -1
  26. package/dist/src/config-checks.js +14 -0
  27. package/dist/src/config-checks.js.map +1 -1
  28. package/dist/src/context-manager-errors.d.ts +25 -0
  29. package/dist/src/context-manager-errors.d.ts.map +1 -0
  30. package/dist/src/context-manager-errors.js +31 -0
  31. package/dist/src/context-manager-errors.js.map +1 -0
  32. package/dist/src/context-options.d.ts +70 -0
  33. package/dist/src/context-options.d.ts.map +1 -0
  34. package/dist/src/context-options.js +90 -0
  35. package/dist/src/context-options.js.map +1 -0
  36. package/dist/src/cut-off-round.js +4 -4
  37. package/dist/src/deferred-import.d.ts +93 -0
  38. package/dist/src/deferred-import.d.ts.map +1 -0
  39. package/dist/src/deferred-import.js +177 -0
  40. package/dist/src/deferred-import.js.map +1 -0
  41. package/dist/src/dendrite/audience.d.ts +223 -0
  42. package/dist/src/dendrite/audience.d.ts.map +1 -0
  43. package/dist/src/dendrite/audience.js +202 -0
  44. package/dist/src/dendrite/audience.js.map +1 -0
  45. package/dist/src/dendrite/index.d.ts +5 -3
  46. package/dist/src/dendrite/index.d.ts.map +1 -1
  47. package/dist/src/dendrite/index.js +2 -1
  48. package/dist/src/dendrite/index.js.map +1 -1
  49. package/dist/src/dendrite/presets.d.ts +10 -5
  50. package/dist/src/dendrite/presets.d.ts.map +1 -1
  51. package/dist/src/dendrite/presets.js +17 -5
  52. package/dist/src/dendrite/presets.js.map +1 -1
  53. package/dist/src/dendrite/registry.d.ts +62 -7
  54. package/dist/src/dendrite/registry.d.ts.map +1 -1
  55. package/dist/src/dendrite/registry.js +183 -12
  56. package/dist/src/dendrite/registry.js.map +1 -1
  57. package/dist/src/dendrite/types.d.ts +91 -8
  58. package/dist/src/dendrite/types.d.ts.map +1 -1
  59. package/dist/src/failure-text.d.ts +36 -0
  60. package/dist/src/failure-text.d.ts.map +1 -0
  61. package/dist/src/failure-text.js +89 -0
  62. package/dist/src/failure-text.js.map +1 -0
  63. package/dist/src/foreign-rounds.d.ts +69 -0
  64. package/dist/src/foreign-rounds.d.ts.map +1 -0
  65. package/dist/src/foreign-rounds.js +152 -0
  66. package/dist/src/foreign-rounds.js.map +1 -0
  67. package/dist/src/framework.d.ts +2064 -842
  68. package/dist/src/framework.d.ts.map +1 -1
  69. package/dist/src/framework.js +8586 -3739
  70. package/dist/src/framework.js.map +1 -1
  71. package/dist/src/gate/event-gate.d.ts +125 -21
  72. package/dist/src/gate/event-gate.d.ts.map +1 -1
  73. package/dist/src/gate/event-gate.js +487 -72
  74. package/dist/src/gate/event-gate.js.map +1 -1
  75. package/dist/src/gate/index.d.ts +1 -1
  76. package/dist/src/gate/index.d.ts.map +1 -1
  77. package/dist/src/gate/index.js.map +1 -1
  78. package/dist/src/gate/reply-judge.d.ts +4 -0
  79. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  80. package/dist/src/gate/reply-judge.js +12 -3
  81. package/dist/src/gate/reply-judge.js.map +1 -1
  82. package/dist/src/gate/rule-scope.d.ts +30 -0
  83. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  84. package/dist/src/gate/rule-scope.js +41 -0
  85. package/dist/src/gate/rule-scope.js.map +1 -0
  86. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  87. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  88. package/dist/src/gate/suppress-script-worker.js +103 -0
  89. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  90. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  91. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  92. package/dist/src/gate/suppress-scripts.js +403 -0
  93. package/dist/src/gate/suppress-scripts.js.map +1 -0
  94. package/dist/src/gate/suppression-review.d.ts +143 -0
  95. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  96. package/dist/src/gate/suppression-review.js +463 -0
  97. package/dist/src/gate/suppression-review.js.map +1 -0
  98. package/dist/src/gate/suppression-stats.d.ts +125 -0
  99. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  100. package/dist/src/gate/suppression-stats.js +214 -0
  101. package/dist/src/gate/suppression-stats.js.map +1 -0
  102. package/dist/src/gate/types.d.ts +142 -7
  103. package/dist/src/gate/types.d.ts.map +1 -1
  104. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  105. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  106. package/dist/src/gate/wake-onboarding.js +277 -0
  107. package/dist/src/gate/wake-onboarding.js.map +1 -0
  108. package/dist/src/heartbeat/heartbeat.js +1 -1
  109. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  110. package/dist/src/held-writes.d.ts +31 -23
  111. package/dist/src/held-writes.d.ts.map +1 -1
  112. package/dist/src/held-writes.js +44 -59
  113. package/dist/src/held-writes.js.map +1 -1
  114. package/dist/src/index.d.ts +6 -2
  115. package/dist/src/index.d.ts.map +1 -1
  116. package/dist/src/index.js +4 -0
  117. package/dist/src/index.js.map +1 -1
  118. package/dist/src/inference-bounds.d.ts +2 -1
  119. package/dist/src/inference-bounds.d.ts.map +1 -1
  120. package/dist/src/inference-bounds.js +9 -6
  121. package/dist/src/inference-bounds.js.map +1 -1
  122. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  123. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  124. package/dist/src/jsonrpc-envelope.js +66 -0
  125. package/dist/src/jsonrpc-envelope.js.map +1 -0
  126. package/dist/src/mcpl/channel-registry.d.ts +188 -28
  127. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  128. package/dist/src/mcpl/channel-registry.js +426 -70
  129. package/dist/src/mcpl/channel-registry.js.map +1 -1
  130. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  131. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  132. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  133. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  134. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  135. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  136. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  137. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  138. package/dist/src/mcpl/inbound-source.js +2 -1
  139. package/dist/src/mcpl/inbound-source.js.map +1 -1
  140. package/dist/src/mcpl/inference-router.d.ts +6 -6
  141. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  142. package/dist/src/mcpl/inference-router.js.map +1 -1
  143. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  144. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  145. package/dist/src/mcpl/modern-connection.js +593 -86
  146. package/dist/src/mcpl/modern-connection.js.map +1 -1
  147. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  148. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  149. package/dist/src/mcpl/protocol-family.js +15 -9
  150. package/dist/src/mcpl/protocol-family.js.map +1 -1
  151. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  152. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  153. package/dist/src/mcpl/push-coalescer.js +105 -15
  154. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  155. package/dist/src/mcpl/push-handler.d.ts +9 -7
  156. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  157. package/dist/src/mcpl/push-handler.js.map +1 -1
  158. package/dist/src/mcpl/server-connection.d.ts +50 -28
  159. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  160. package/dist/src/mcpl/server-connection.js +165 -72
  161. package/dist/src/mcpl/server-connection.js.map +1 -1
  162. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  163. package/dist/src/mcpl/tool-classes.js +3 -0
  164. package/dist/src/mcpl/tool-classes.js.map +1 -1
  165. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  166. package/dist/src/mcpl/tool-presence.js +10 -6
  167. package/dist/src/mcpl/tool-presence.js.map +1 -1
  168. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  169. package/dist/src/mcpl/toolset-host.js +7 -6
  170. package/dist/src/mcpl/toolset-host.js.map +1 -1
  171. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  172. package/dist/src/mcpl/toolsets-module.js +2 -1
  173. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  174. package/dist/src/mcpl/types.d.ts +33 -6
  175. package/dist/src/mcpl/types.d.ts.map +1 -1
  176. package/dist/src/mcpl/types.js.map +1 -1
  177. package/dist/src/module-registry.d.ts +24 -1
  178. package/dist/src/module-registry.d.ts.map +1 -1
  179. package/dist/src/module-registry.js +68 -18
  180. package/dist/src/module-registry.js.map +1 -1
  181. package/dist/src/modules/discord/index.d.ts +45 -8
  182. package/dist/src/modules/discord/index.d.ts.map +1 -1
  183. package/dist/src/modules/discord/index.js +238 -87
  184. package/dist/src/modules/discord/index.js.map +1 -1
  185. package/dist/src/modules/health/index.js +2 -2
  186. package/dist/src/modules/health/index.js.map +1 -1
  187. package/dist/src/modules/history/index.d.ts +59 -0
  188. package/dist/src/modules/history/index.d.ts.map +1 -1
  189. package/dist/src/modules/history/index.js +432 -67
  190. package/dist/src/modules/history/index.js.map +1 -1
  191. package/dist/src/modules/history/semantic.d.ts +6 -5
  192. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  193. package/dist/src/modules/history/semantic.js +18 -25
  194. package/dist/src/modules/history/semantic.js.map +1 -1
  195. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  196. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  197. package/dist/src/modules/workspace/index.d.ts +28 -7
  198. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  199. package/dist/src/modules/workspace/index.js +80 -25
  200. package/dist/src/modules/workspace/index.js.map +1 -1
  201. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  202. package/dist/src/modules/workspace/observe.js +2 -1
  203. package/dist/src/modules/workspace/observe.js.map +1 -1
  204. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/reconcile.js +3 -2
  206. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  207. package/dist/src/notice-tags.d.ts +20 -9
  208. package/dist/src/notice-tags.d.ts.map +1 -1
  209. package/dist/src/notice-tags.js +20 -9
  210. package/dist/src/notice-tags.js.map +1 -1
  211. package/dist/src/operator-log.d.ts +36 -3
  212. package/dist/src/operator-log.d.ts.map +1 -1
  213. package/dist/src/operator-log.js +2 -2
  214. package/dist/src/operator-log.js.map +1 -1
  215. package/dist/src/presentation.d.ts +747 -0
  216. package/dist/src/presentation.d.ts.map +1 -0
  217. package/dist/src/presentation.js +1437 -0
  218. package/dist/src/presentation.js.map +1 -0
  219. package/dist/src/prose-drafts.d.ts +15 -11
  220. package/dist/src/prose-drafts.d.ts.map +1 -1
  221. package/dist/src/prose-drafts.js +16 -12
  222. package/dist/src/prose-drafts.js.map +1 -1
  223. package/dist/src/read-marks.d.ts +192 -0
  224. package/dist/src/read-marks.d.ts.map +1 -0
  225. package/dist/src/read-marks.js +242 -0
  226. package/dist/src/read-marks.js.map +1 -0
  227. package/dist/src/reasoning-loss.d.ts +5 -0
  228. package/dist/src/reasoning-loss.d.ts.map +1 -0
  229. package/dist/src/reasoning-loss.js +8 -0
  230. package/dist/src/reasoning-loss.js.map +1 -0
  231. package/dist/src/recovery/offline-branch.d.ts +91 -6
  232. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  233. package/dist/src/recovery/offline-branch.js +277 -32
  234. package/dist/src/recovery/offline-branch.js.map +1 -1
  235. package/dist/src/recovery/recover-cli.js +24 -4
  236. package/dist/src/recovery/recover-cli.js.map +1 -1
  237. package/dist/src/send-tools.d.ts +5 -9
  238. package/dist/src/send-tools.d.ts.map +1 -1
  239. package/dist/src/send-tools.js +40 -7
  240. package/dist/src/send-tools.js.map +1 -1
  241. package/dist/src/sent-rows.js +1 -1
  242. package/dist/src/stored-injections.d.ts +2 -2
  243. package/dist/src/stored-injections.js +2 -2
  244. package/dist/src/stored-removal.d.ts +17 -4
  245. package/dist/src/stored-removal.d.ts.map +1 -1
  246. package/dist/src/stored-removal.js +29 -5
  247. package/dist/src/stored-removal.js.map +1 -1
  248. package/dist/src/supplied-text.d.ts +1 -1
  249. package/dist/src/supplied-text.js +3 -3
  250. package/dist/src/supplied-text.js.map +1 -1
  251. package/dist/src/thinking-drops.d.ts +2 -0
  252. package/dist/src/thinking-drops.d.ts.map +1 -1
  253. package/dist/src/thinking-drops.js.map +1 -1
  254. package/dist/src/tool-presentation.d.ts.map +1 -1
  255. package/dist/src/tool-presentation.js +38 -9
  256. package/dist/src/tool-presentation.js.map +1 -1
  257. package/dist/src/tool-result-guard.d.ts +12 -1
  258. package/dist/src/tool-result-guard.d.ts.map +1 -1
  259. package/dist/src/tool-result-guard.js +42 -8
  260. package/dist/src/tool-result-guard.js.map +1 -1
  261. package/dist/src/tune-out/coordinator.d.ts +1170 -81
  262. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  263. package/dist/src/tune-out/coordinator.js +3732 -369
  264. package/dist/src/tune-out/coordinator.js.map +1 -1
  265. package/dist/src/tune-out/tools.d.ts +114 -17
  266. package/dist/src/tune-out/tools.d.ts.map +1 -1
  267. package/dist/src/tune-out/tools.js +89 -28
  268. package/dist/src/tune-out/tools.js.map +1 -1
  269. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  270. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  271. package/dist/src/tune-out/window-anchor.js +202 -0
  272. package/dist/src/tune-out/window-anchor.js.map +1 -0
  273. package/dist/src/types/agent.d.ts +121 -25
  274. package/dist/src/types/agent.d.ts.map +1 -1
  275. package/dist/src/types/events.d.ts +2 -0
  276. package/dist/src/types/events.d.ts.map +1 -1
  277. package/dist/src/types/framework.d.ts +72 -17
  278. package/dist/src/types/framework.d.ts.map +1 -1
  279. package/dist/src/types/inference-log.d.ts +1 -0
  280. package/dist/src/types/inference-log.d.ts.map +1 -1
  281. package/dist/src/types/module.d.ts +180 -69
  282. package/dist/src/types/module.d.ts.map +1 -1
  283. package/dist/src/types/trace.d.ts +573 -39
  284. package/dist/src/types/trace.d.ts.map +1 -1
  285. package/package.json +4 -4
  286. package/src/admission/admit.ts +32 -3
  287. package/src/agent.ts +392 -132
  288. package/src/api/mcp-server-core.ts +54 -17
  289. package/src/api/server.ts +4 -23
  290. package/src/append-all.ts +6 -0
  291. package/src/code-execution/isolate/scripts.ts +28 -3
  292. package/src/config-checks.ts +14 -0
  293. package/src/context-manager-errors.ts +37 -0
  294. package/src/context-options.ts +106 -0
  295. package/src/cut-off-round.ts +4 -4
  296. package/src/deferred-import.ts +239 -0
  297. package/src/dendrite/audience.ts +315 -0
  298. package/src/dendrite/index.ts +14 -2
  299. package/src/dendrite/presets.ts +18 -6
  300. package/src/dendrite/registry.ts +213 -14
  301. package/src/dendrite/types.ts +92 -8
  302. package/src/failure-text.ts +88 -0
  303. package/src/foreign-rounds.ts +199 -0
  304. package/src/framework.ts +8796 -3813
  305. package/src/gate/event-gate.ts +518 -91
  306. package/src/gate/index.ts +3 -0
  307. package/src/gate/reply-judge.ts +12 -3
  308. package/src/gate/rule-scope.ts +45 -0
  309. package/src/gate/suppress-script-worker.ts +89 -0
  310. package/src/gate/suppress-scripts.ts +414 -0
  311. package/src/gate/suppression-review.ts +517 -0
  312. package/src/gate/suppression-stats.ts +314 -0
  313. package/src/gate/types.ts +148 -7
  314. package/src/gate/wake-onboarding.ts +309 -0
  315. package/src/heartbeat/heartbeat.ts +1 -1
  316. package/src/held-writes.ts +49 -60
  317. package/src/index.ts +10 -2
  318. package/src/inference-bounds.ts +11 -6
  319. package/src/jsonrpc-envelope.ts +81 -0
  320. package/src/mcpl/channel-registry.ts +513 -116
  321. package/src/mcpl/checkpoint-manager.ts +3 -2
  322. package/src/mcpl/feature-set-manager.ts +2 -1
  323. package/src/mcpl/inbound-source.ts +2 -1
  324. package/src/mcpl/inference-router.ts +7 -4
  325. package/src/mcpl/modern-connection.ts +629 -91
  326. package/src/mcpl/protocol-family.ts +15 -9
  327. package/src/mcpl/push-coalescer.ts +193 -34
  328. package/src/mcpl/push-handler.ts +10 -5
  329. package/src/mcpl/server-connection.ts +169 -75
  330. package/src/mcpl/tool-classes.ts +3 -0
  331. package/src/mcpl/tool-presence.ts +10 -5
  332. package/src/mcpl/toolset-host.ts +7 -6
  333. package/src/mcpl/toolsets-module.ts +2 -1
  334. package/src/mcpl/types.ts +33 -6
  335. package/src/module-registry.ts +73 -17
  336. package/src/modules/discord/index.ts +230 -82
  337. package/src/modules/health/index.ts +2 -2
  338. package/src/modules/history/index.ts +439 -70
  339. package/src/modules/history/semantic.ts +18 -24
  340. package/src/modules/workspace/disk-agreement.ts +1 -1
  341. package/src/modules/workspace/index.ts +89 -26
  342. package/src/modules/workspace/observe.ts +2 -1
  343. package/src/modules/workspace/reconcile.ts +3 -2
  344. package/src/notice-tags.ts +21 -9
  345. package/src/operator-log.ts +36 -3
  346. package/src/presentation.ts +1678 -0
  347. package/src/prose-drafts.ts +16 -12
  348. package/src/read-marks.ts +328 -0
  349. package/src/reasoning-loss.ts +7 -0
  350. package/src/recovery/offline-branch.ts +350 -35
  351. package/src/recovery/recover-cli.ts +23 -4
  352. package/src/send-tools.ts +40 -7
  353. package/src/sent-rows.ts +1 -1
  354. package/src/stored-injections.ts +2 -2
  355. package/src/stored-removal.ts +31 -7
  356. package/src/supplied-text.ts +3 -3
  357. package/src/thinking-drops.ts +2 -0
  358. package/src/tool-presentation.ts +34 -9
  359. package/src/tool-result-guard.ts +41 -7
  360. package/src/tune-out/coordinator.ts +4121 -472
  361. package/src/tune-out/tools.ts +162 -44
  362. package/src/tune-out/window-anchor.ts +277 -0
  363. package/src/types/agent.ts +122 -25
  364. package/src/types/events.ts +2 -0
  365. package/src/types/framework.ts +73 -17
  366. package/src/types/inference-log.ts +1 -0
  367. package/src/types/module.ts +181 -68
  368. package/src/types/trace.ts +607 -43
@@ -1,3 +1,4 @@
1
+ import { suppressionRecordOf } from '../gate/suppression-stats.js';
1
2
  import { JsStore } from '@animalabs/chronicle';
2
3
  import type { SurgeryMarkerReceipt } from '../operator-log.js';
3
4
  import { ContextManager, continuesBody, type BodyShard } from '@animalabs/context-manager';
@@ -15,16 +16,50 @@ import {
15
16
  type DiscordAwarenessRef,
16
17
  type DiscordMessageMetadataCarrier,
17
18
  } from './discord-awareness-outbox.js';
19
+ import { appendAll } from '../append-all.js';
18
20
 
19
21
  export interface OfflineRecoveryBranchOptions {
20
22
  storePath: string;
21
23
  agentName: string;
22
- /** Preferred: Discord message that remains as the last visible anchor. */
24
+ /**
25
+ * Preferred: Discord message that remains as the last visible anchor. On
26
+ * an agent's window that was presented (#291) the branch is cut at the
27
+ * write that presented the anchor, so rows stored after the anchor and
28
+ * before that write stay on the branch: those the window took after the
29
+ * anchor enter it again at the agent's next presentation point, after the
30
+ * anchor, and the rows that one write presented together with the anchor
31
+ * stay after it in the window. Only rows stored after the cut are removed
32
+ * and counted. A
33
+ * suppression may name any row the branch keeps that has a Discord
34
+ * message id, one the cut keeps after the anchor too; one without (an
35
+ * agent's round, a tool row) takes the live suppressMessages, by its row
36
+ * id. A recovery on that window that would change nothing (no
37
+ * row removed, none an arrival again, no suppression) is refused, as the
38
+ * live rollback refuses it.
39
+ */
23
40
  messageId?: string;
24
- /** Exact internal ContextManager message ID that remains as the anchor. */
41
+ /** Exact internal ContextManager message ID that remains as the anchor (the cut as for `messageId`). */
25
42
  contextId?: string;
26
- /** Backward-compatible alternative: remove the newest N context entries. */
43
+ /**
44
+ * Backward-compatible alternative: remove the newest N context entries,
45
+ * counted in the agent's window once it was presented (#291), over the
46
+ * rows of its own slot, and in the record before that; the anchor is the
47
+ * entry before them, cut as for `messageId`.
48
+ */
27
49
  messages?: number;
50
+ /**
51
+ * Remove every row a suppress rule hid (`metadata.suppressed`) on the new
52
+ * branch, which needs no anchor: the branch then starts from the newest row.
53
+ * For rolling back to a release that doesn't read the stamp, where a row
54
+ * hidden between messages a summary covers makes that summary non-contiguous,
55
+ * and the store is refused at open. The source branch keeps them. Once this
56
+ * release has made a window write of two or more rows (the first
57
+ * write-down of a window that holds a row, as at the first boot on a store
58
+ * with history, or a presentation of more than one row: MANIFEST version
59
+ * 2), no earlier release opens the store at all, and only a copy taken
60
+ * before the upgrade goes back.
61
+ */
62
+ removeSuppressed?: boolean;
28
63
  /** Exact Discord messages to suppress on the new branch. */
29
64
  suppressMessageIds?: string[];
30
65
  /** Inclusive context-order ranges between Discord message endpoints. */
@@ -52,6 +87,20 @@ export interface OfflineRecoveryBranchResult {
52
87
  targetBranch: string;
53
88
  messagesRemoved: number;
54
89
  messagesSuppressed: number;
90
+ /**
91
+ * On a presented window (#291): the rows after the anchor in the agent's
92
+ * window that the cut keeps in the record (stored before the write that
93
+ * presented the anchor, presented after it), as the live rollback counts
94
+ * them, of the agent's own slot: the recovery reads that slot alone, so a
95
+ * row of a slot the agent observes (the residents' rows in a persistent
96
+ * subconscious's window), which the live rollback counts, isn't counted
97
+ * here, and the check of a cut that would change nothing doesn't see it. They leave the window as arrived and not presented, and enter it
98
+ * again at the agent's next presentation point, after the anchor. Not
99
+ * removed, so not counted in `messagesRemoved`, and not marked; a row the
100
+ * same recovery suppresses is counted there instead. 0 for a run with no
101
+ * anchor; absent on a window that is still the record.
102
+ */
103
+ returnedToArrivals?: number;
55
104
  /** Removed messages that carry a Discord address. */
56
105
  discordAddressable: number;
57
106
  /** The publication scope recorded for this recovery. */
@@ -78,9 +127,12 @@ export interface OfflineRecoveryBranchResult {
78
127
  /**
79
128
  * Create a safe Chronicle branch while the normal agent host is stopped.
80
129
  *
81
- * History is scanned in bounded windows and blobs are not re-inlined. The
82
- * affected messages are never compiled and their content is never written to
83
- * the recovery outbox; only Discord addressing metadata leaves Chronicle.
130
+ * History is scanned in bounded windows and blobs are not re-inlined, but a
131
+ * presented window (#291) is read whole where its order counts: for a
132
+ * `messages` count, and for the rows after the anchor that the cut returns
133
+ * to the arrivals or keeps (windowAfterCut). The affected messages are never
134
+ * compiled and their content is never written to the recovery outbox; only
135
+ * Discord addressing metadata leaves Chronicle.
84
136
  */
85
137
  export async function createOfflineRecoveryBranch(
86
138
  options: OfflineRecoveryBranchOptions,
@@ -88,7 +140,7 @@ export async function createOfflineRecoveryBranch(
88
140
  const anchorCount = Number(!!options.messageId)
89
141
  + Number(!!options.contextId)
90
142
  + Number(options.messages !== undefined);
91
- if (anchorCount !== 1) {
143
+ if (anchorCount !== 1 && !(anchorCount === 0 && options.removeSuppressed === true)) {
92
144
  throw new Error('Specify exactly one of messageId, contextId, or messages');
93
145
  }
94
146
  const count = options.messages === undefined ? undefined : Math.floor(options.messages);
@@ -119,6 +171,16 @@ export async function createOfflineRecoveryBranch(
119
171
  let targetIndex: number;
120
172
  let removedCount: number;
121
173
  let carriers: DiscordMessageMetadataCarrier[];
174
+ // Once the agent's window was presented (#291), the branch cut is the
175
+ // write that presented the anchor (branchSequenceOf), not the anchor's
176
+ // own place in the record: the rows stored after the anchor and before
177
+ // that write stay on the branch, and those the window took after the
178
+ // anchor are arrivals again there. Only the rows stored after the cut
179
+ // are removed, and only they are counted, carried and checked.
180
+ const presented = contextManager.windowMode() === 'presented';
181
+ const firstRemovedAfter = (index: number, anchor: StoredWindowMessage): number => presented
182
+ ? firstIndexAfterCut(contextManager!, index, total, contextManager!.branchSequenceOf(anchor.id))
183
+ : index + 1;
122
184
 
123
185
  if (options.messageId) {
124
186
  // Search backward in bounded windows: recovery anchors are normally near
@@ -134,8 +196,9 @@ export async function createOfflineRecoveryBranch(
134
196
  }
135
197
  target = located.message;
136
198
  targetIndex = located.index;
137
- removedCount = total - located.index - 1;
138
- carriers = collectDiscordCarriers(contextManager, located.index + 1, total);
199
+ const firstRemoved = firstRemovedAfter(targetIndex, target);
200
+ removedCount = total - firstRemoved;
201
+ carriers = collectDiscordCarriers(contextManager, firstRemoved, total);
139
202
  } else if (options.contextId) {
140
203
  const located = findInternalMessageFromTail(contextManager, total, options.contextId.trim());
141
204
  if (!located) {
@@ -143,39 +206,82 @@ export async function createOfflineRecoveryBranch(
143
206
  }
144
207
  target = located.message;
145
208
  targetIndex = located.index;
146
- removedCount = total - located.index - 1;
147
- carriers = collectDiscordCarriers(contextManager, located.index + 1, total);
209
+ const firstRemoved = firstRemovedAfter(targetIndex, target);
210
+ removedCount = total - firstRemoved;
211
+ carriers = collectDiscordCarriers(contextManager, firstRemoved, total);
212
+ } else if (presented && count !== undefined) {
213
+ // Counted back in the agent's window, whose order isn't the record's:
214
+ // the anchor is the window's row `count` before its newest. Only the
215
+ // rows of the context's own slot count, as the live undo counts them:
216
+ // the anchor is looked for in that slot, and a window index can name
217
+ // a row of a slot the agent observed (the residents' rows in a
218
+ // persistent subconscious's window). The one context opened here
219
+ // merges no other slot, so its listing holds none of those today; the
220
+ // count doesn't lean on that.
221
+ const window = contextManager.getPresentedMessages().filter((row) => contextManager!.getMessage(row.id) !== null);
222
+ if (count! >= window.length) {
223
+ throw new Error(
224
+ `Cannot remove ${count} message(s) — history has ${window.length}; at least one must remain.`,
225
+ );
226
+ }
227
+ const anchor = window[window.length - 1 - count!]!;
228
+ const located = findInternalMessageFromTail(contextManager, total, String(anchor.id));
229
+ if (!located) {
230
+ throw new Error(`The recovery anchor ${String(anchor.id)}, ${count} back in the window, is not in this context's record.`);
231
+ }
232
+ target = located.message;
233
+ targetIndex = located.index;
234
+ const firstRemoved = firstRemovedAfter(targetIndex, target);
235
+ removedCount = total - firstRemoved;
236
+ carriers = collectDiscordCarriers(contextManager, firstRemoved, total);
148
237
  } else {
149
238
  // Count mode remains for compatibility. Read only the target plus suffix,
150
239
  // avoiding full-history materialization and attachment blob inflation.
151
- const window = contextManager.getMessageWindow(total - count! - 1, count! + 1, {
240
+ // With no anchor (removeSuppressed alone), the target is the newest row,
241
+ // on a presented window too: nothing is cut, the branch starting at the
242
+ // head.
243
+ const dropping = count ?? 0;
244
+ const window = contextManager.getMessageWindow(total - dropping - 1, dropping + 1, {
152
245
  resolveBlobs: false,
153
246
  }).messages;
154
247
  const countTarget = window[0];
155
248
  const discarded = window.slice(1);
156
- if (!countTarget || discarded.length !== count) {
249
+ if (!countTarget || discarded.length !== dropping) {
157
250
  throw new Error('Could not read the requested recovery window from Chronicle');
158
251
  }
159
252
  target = countTarget;
160
- targetIndex = total - count! - 1;
161
- removedCount = count!;
253
+ targetIndex = total - dropping - 1;
254
+ removedCount = dropping;
162
255
  carriers = discarded.map(addressingOnly);
163
256
  }
164
257
 
165
258
  validateAnchorBoundary(contextManager, targetIndex, total, target);
166
259
 
260
+ // The rows the branch keeps are the record's first `total -
261
+ // removedCount`: a suppression may name any of them, those stored after
262
+ // the anchor and kept by a presented window's cut too (in the record
263
+ // before a presentation, the last kept row is the anchor).
167
264
  const suppression = buildSuppressionPlan(
168
265
  contextManager,
169
- targetIndex,
266
+ total - removedCount - 1,
170
267
  options.suppressMessageIds ?? [],
171
268
  options.suppressRanges ?? [],
269
+ options.removeSuppressed === true,
172
270
  );
271
+ // A run with no anchor cuts nothing, so there is no anchor to name and
272
+ // nothing returns: one with nothing to suppress makes a branch at the
273
+ // head, as on a legacy window.
274
+ const afterCut = presented && anchorCount > 0 ? windowAfterCut(contextManager, target) : undefined;
275
+ const returnedToArrivals = afterCut?.returned.filter((id) => !suppression.ids.has(String(id))).length;
276
+ if (afterCut && removedCount === 0 && suppression.intervals.length === 0 && afterCut.returned.length === 0) {
277
+ refuseIneffectiveCut(target, afterCut.kept, options.agentName, contextManager.getPresentedMessages() as StoredWindowMessage[]);
278
+ }
173
279
  validateRemovalIntegrity(
174
280
  contextManager,
175
281
  total,
176
282
  [
177
283
  ...(removedCount > 0 ? [{
178
- start: targetIndex + 1,
284
+ start: total - removedCount,
179
285
  end: total - 1,
180
286
  fromId: '',
181
287
  toId: '',
@@ -208,6 +314,7 @@ export async function createOfflineRecoveryBranch(
208
314
  targetBranch,
209
315
  messagesRemoved: removedCount,
210
316
  messagesSuppressed: suppression.messageCount,
317
+ ...(presented ? { returnedToArrivals: returnedToArrivals ?? 0 } : {}),
211
318
  discordAddressable: addressable,
212
319
  marksScope: marks === 'none' ? 'none' : marks.scope,
213
320
  discordMarkersQueued: refs.length,
@@ -241,11 +348,15 @@ export async function createOfflineRecoveryBranch(
241
348
 
242
349
  let createdBranch: string | undefined;
243
350
  try {
244
- // branchAt uses the target message's origin sequence. No message
245
- // content is compiled or submitted to Membrane during this operation.
246
- // Creating the branch is part of the body: a failure here settles the
247
- // batch like a failed switch or redaction.
248
- createdBranch = contextManager.branchAt(target.id, targetBranch);
351
+ // branchAt uses the target message's origin sequence. With no anchor
352
+ // nothing is rewound, so the branch starts at the head instead: what
353
+ // was written after the newest message, a removal or a summary, stays.
354
+ // No message content is compiled or submitted to Membrane during this
355
+ // operation. Creating the branch is part of the body: a failure here
356
+ // settles the batch like a failed switch or redaction.
357
+ createdBranch = anchorCount === 0
358
+ ? store.createBranchAt(targetBranch, sourceBranch, store.currentSequence()).name
359
+ : contextManager.branchAt(target.id, targetBranch);
249
360
  await contextManager.switchBranch(createdBranch);
250
361
  // Each interval is removed by its messages' ids, so the order doesn't
251
362
  // change the result. removeStoredInterval takes a single message outside
@@ -428,6 +539,188 @@ function findInternalMessageFromTail(
428
539
  return null;
429
540
  }
430
541
 
542
+ /**
543
+ * The first slot index after `targetIndex` whose row was stored after
544
+ * `cut`, or `total` when none was: the rows a branch cut at `cut` leaves
545
+ * behind, the slot being in store order, start there. Read in bounded,
546
+ * blob-free windows.
547
+ */
548
+ function firstIndexAfterCut(
549
+ contextManager: ContextManager,
550
+ targetIndex: number,
551
+ total: number,
552
+ cut: number,
553
+ ): number {
554
+ for (let offset = targetIndex + 1; offset < total; offset += RECOVERY_SCAN_WINDOW) {
555
+ const messages = contextManager.getMessageWindow(
556
+ offset,
557
+ Math.min(RECOVERY_SCAN_WINDOW, total - offset),
558
+ { resolveBlobs: false },
559
+ ).messages;
560
+ const at = messages.findIndex((message) => message.sequence > cut);
561
+ if (at >= 0) return offset + at;
562
+ }
563
+ return total;
564
+ }
565
+
566
+ /**
567
+ * The rows after `target` in a presented window, as the live rollback reads
568
+ * them (windowAfterCut): those the write that presented the target
569
+ * presented too (`kept`, which no branch cut separates from it), and those
570
+ * presented after that write but stored at or before it (`returned`, which
571
+ * the cut keeps in the record and makes arrivals again). Read from the
572
+ * window's newest row back to the target or the first row presented before
573
+ * the cut (every row's cut grows in window order).
574
+ */
575
+ function windowAfterCut(
576
+ contextManager: ContextManager,
577
+ target: StoredWindowMessage,
578
+ ): { returned: StoredWindowMessage['id'][]; kept: StoredWindowMessage[] } {
579
+ const cut = contextManager.branchSequenceOf(target.id);
580
+ const window = contextManager.getPresentedMessages();
581
+ const returned: StoredWindowMessage['id'][] = [];
582
+ const kept: StoredWindowMessage[] = [];
583
+ for (let i = window.length - 1; i >= 0; i--) {
584
+ const row = window[i]!;
585
+ if (row.id === target.id) break;
586
+ const at = contextManager.branchSequenceOf(row.id);
587
+ if (at < cut) break;
588
+ if (at === cut) kept.unshift(row as StoredWindowMessage);
589
+ else if (row.sequence <= cut) returned.unshift(row.id);
590
+ }
591
+ return { returned, kept };
592
+ }
593
+
594
+ /**
595
+ * Refuses a recovery on a presented window that would change nothing, as
596
+ * the live rollback refuses one: the cut at the write that presented the
597
+ * anchor removes no row, and no row the window took after that write
598
+ * becomes an arrival again (windowAfterCut). The rows that write presented
599
+ * after the anchor (`kept`) name the way to take them out. A row holding a
600
+ * result of a call at or before the anchor (`window`, the agent's window:
601
+ * a tool boundary presents a round's reply and its results in one write)
602
+ * isn't to be suppressed, which would leave that call without its result:
603
+ * the advice is to anchor before the call. The others are suppressed: in
604
+ * the same recovery a row by its Discord message id, each one named, which
605
+ * takes every copy of that message; and by its row id with the live
606
+ * `suppressMessages` a row with none (an agent's round, a tool row) or one
607
+ * sharing the anchor's Discord id (a retried write's copy), which the
608
+ * offline suppression would take with the anchor. No range is offered: it
609
+ * takes every record row between its ends, any agent's, rows the window
610
+ * never held among them. Each refusal says the recovery reads the agent's
611
+ * own slot alone: a row of a slot it observes, which the live rollback
612
+ * weighs, isn't weighed here.
613
+ */
614
+ function refuseIneffectiveCut(
615
+ target: StoredWindowMessage,
616
+ kept: readonly StoredWindowMessage[],
617
+ agentName: string,
618
+ window: readonly StoredWindowMessage[],
619
+ ): never {
620
+ const anchorDiscordId = discordIdOf(target);
621
+ const anchor = `${String(target.id)}${anchorDiscordId ? ` (Discord message ${anchorDiscordId})` : ''}`;
622
+ const ownSlotOnly = `. Offline, only ${agentName}'s own slot is read: a row of a slot it observes, which the live rollback weighs, isn't weighed here.`;
623
+ if (kept.length > 0) {
624
+ const n = kept.length;
625
+ const first = kept[0]!;
626
+ const firstDiscordId = discordIdOf(first);
627
+ const named = `${String(first.id)}${firstDiscordId ? `, Discord message ${firstDiscordId}` : ''}`;
628
+ const split = resultsOfAnchoredCalls(window, target.id, kept);
629
+ const suppressible = split ? kept.filter((row) => !split.rows.includes(row)) : kept;
630
+ const copies = suppressible.filter((row) => anchorDiscordId !== undefined && discordIdOf(row) === anchorDiscordId);
631
+ // By row id, live: every one, so following the advice leaves none of them in place.
632
+ const unaddressed = suppressible.filter((row) => discordIdOf(row) === undefined || copies.includes(row));
633
+ const addressed = suppressible.filter((row) => !unaddressed.includes(row));
634
+ const offline = `in the same recovery (--suppress / suppressMessageIds: ${addressed.map((row) => discordIdOf(row)!).join(', ')})`;
635
+ const live = `by row id with the live suppressMessages (${unaddressed.map((row) => String(row.id)).join(', ')}) once the host runs, ` +
636
+ 'since the offline recovery suppresses only by Discord message id' +
637
+ (copies.length > 0 ? `, which takes every copy of a message, the anchor's own too` : '');
638
+ const m = suppressible.length;
639
+ const them = m === n ? (n === 1 ? 'it' : 'them') : 'the rest';
640
+ const how = m === 0 ? ''
641
+ : unaddressed.length === 0 ? `suppress ${them} ${offline}`
642
+ : addressed.length === 0 ? `suppress ${them} ${live}` +
643
+ (copies.length > 0 ? '' : ` and ${m === 1 ? 'it has none' : 'none of them has one'}`)
644
+ : `suppress the ${addressed.length} with a Discord message id ${offline}, and the ${unaddressed.length} ` +
645
+ `${copies.length > 0 ? `other${unaddressed.length === 1 ? '' : 's'}` : 'without one'} ${live}`;
646
+ const before = split ? anchorBefore(split, 'anchor the recovery', 'at') : '';
647
+ throw new Error(
648
+ `Recovery anchor ${anchor} was presented to ${agentName} in one write with the ${n} message${n === 1 ? '' : 's'} ` +
649
+ `after it in its window (the first: ${named}), and a branch cut can't separate the rows one write presented, ` +
650
+ `so a recovery anchored there would remove nothing; to take ${n === 1 ? 'that message' : 'those messages'} out, ` +
651
+ (before && how ? `${before}; or, to keep that call and its results, ${how}` : before || how) + ownSlotOnly,
652
+ );
653
+ }
654
+ throw new Error(
655
+ `Recovery anchor ${anchor} is already the tail of ${agentName}'s window: every message stored after it was ` +
656
+ 'stored before the write that presented it, where the branch is cut, and none entered the window after it, so ' +
657
+ 'a recovery anchored there would change nothing' + ownSlotOnly,
658
+ );
659
+ }
660
+
661
+ /**
662
+ * Of the rows a no-op cut keeps after its anchor (`kept`), those holding a
663
+ * result of a call in a row of the agent's window at or before the anchor,
664
+ * which a suppression would leave without its result: a tool boundary
665
+ * presents a round's reply and its results in one write, and so does a
666
+ * write-down for rows stored together. With them, the earliest such call's
667
+ * row and the window row before it, where a cut keeps the call and its
668
+ * results together. Undefined where no kept row answers such a call, or the
669
+ * anchor isn't in `window`. The live rollback's refusal reads it too.
670
+ */
671
+ export function resultsOfAnchoredCalls<R extends { id: unknown; content: readonly unknown[] }>(
672
+ window: readonly R[],
673
+ anchorId: unknown,
674
+ kept: readonly R[],
675
+ ): { rows: R[]; call: R; before?: R } | undefined {
676
+ const anchorAt = window.findIndex((row) => row.id === anchorId);
677
+ if (anchorAt < 0) return undefined;
678
+ const callAt = new Map<string, number>();
679
+ for (let i = 0; i <= anchorAt; i++) {
680
+ for (const raw of window[i]!.content) {
681
+ const block = raw as { type?: unknown; id?: unknown };
682
+ if (block.type === 'tool_use' && typeof block.id === 'string') callAt.set(block.id, i);
683
+ }
684
+ }
685
+ let earliest: number | undefined;
686
+ const rows = kept.filter((row) => {
687
+ let answers = false;
688
+ for (const raw of row.content) {
689
+ const block = raw as { type?: unknown; toolUseId?: unknown };
690
+ const at = block.type === 'tool_result' && typeof block.toolUseId === 'string' ? callAt.get(block.toolUseId) : undefined;
691
+ if (at === undefined) continue;
692
+ answers = true;
693
+ earliest = earliest === undefined ? at : Math.min(earliest, at);
694
+ }
695
+ return answers;
696
+ });
697
+ if (rows.length === 0 || earliest === undefined) return undefined;
698
+ return { rows, call: window[earliest]!, ...(earliest > 0 ? { before: window[earliest - 1]! } : {}) };
699
+ }
700
+
701
+ /**
702
+ * A refusal's advice for the kept rows that answer a call at or before the
703
+ * anchor (resultsOfAnchoredCalls): `verb` (anchor the recovery, roll back)
704
+ * before the call's row, naming the row before it (`to`: at, to), and why
705
+ * they aren't to be suppressed.
706
+ */
707
+ export function anchorBefore(
708
+ split: { rows: ReadonlyArray<{ id: unknown }>; call: { id: unknown }; before?: { id: unknown } },
709
+ verb: string,
710
+ to: string,
711
+ ): string {
712
+ const ids = split.rows.map((row) => String(row.id)).join(', ');
713
+ const one = split.rows.length === 1;
714
+ return `${verb} before ${String(split.call.id)}, which holds the call ${one ? 'it answers' : 'they answer'}, instead` +
715
+ `${split.before ? ` (${to} ${String(split.before.id)}, the message before it in the window)` : ''}: ${ids} ${one ? 'holds a result' : 'hold results'} ` +
716
+ `of a call at or before the anchor, and suppressing ${one ? 'it' : 'them'} would leave that call without its result`;
717
+ }
718
+
719
+ function discordIdOf(message: { metadata?: Record<string, unknown> }): string | undefined {
720
+ const id = message.metadata?.messageId;
721
+ return id === undefined || id === null || id === '' ? undefined : String(id);
722
+ }
723
+
431
724
  function validateAnchorBoundary(
432
725
  contextManager: ContextManager,
433
726
  targetIndex: number,
@@ -453,8 +746,20 @@ function validateRemovalIntegrity(
453
746
  intervals: SuppressionInterval[],
454
747
  ): void {
455
748
  if (intervals.length === 0) return;
456
- const removed = (index: number) => intervals.some((interval) =>
457
- index >= interval.start && index <= interval.end);
749
+ // Sorted starts with each one's running furthest end, searched per row: a
750
+ // store can hold a removal for every third row (--remove-suppressed).
751
+ const sorted = [...intervals].sort((a, b) => a.start - b.start);
752
+ const reach: number[] = [];
753
+ for (const interval of sorted) reach.push(Math.max(interval.end, reach.at(-1) ?? -1));
754
+ const removed = (index: number) => {
755
+ let lo = 0;
756
+ let hi = sorted.length - 1;
757
+ while (lo < hi) {
758
+ const mid = Math.ceil((lo + hi) / 2);
759
+ if (sorted[mid]!.start <= index) lo = mid; else hi = mid - 1;
760
+ }
761
+ return sorted[lo]!.start <= index && index <= reach[lo]!;
762
+ };
458
763
  const toolUses = new Map<string, boolean>();
459
764
  const toolResults = new Map<string, boolean>();
460
765
  // The message before, in store order: a selection splits a body when a
@@ -501,10 +806,11 @@ function validateRemovalIntegrity(
501
806
 
502
807
  function buildSuppressionPlan(
503
808
  contextManager: ContextManager,
504
- targetIndex: number,
809
+ lastKeptIndex: number,
505
810
  rawMessageIds: string[],
506
811
  rawRanges: Array<{ fromMessageId: string; toMessageId: string }>,
507
- ): { intervals: SuppressionInterval[]; carriers: DiscordMessageMetadataCarrier[]; messageCount: number } {
812
+ removeSuppressed = false,
813
+ ): { intervals: SuppressionInterval[]; carriers: DiscordMessageMetadataCarrier[]; messageCount: number; ids: Set<string> } {
508
814
  const messageIds = rawMessageIds.map(normalizeDiscordMessageId);
509
815
  const ranges = rawRanges.map((range) => ({
510
816
  fromMessageId: normalizeDiscordMessageId(range.fromMessageId),
@@ -514,16 +820,20 @@ function buildSuppressionPlan(
514
820
  ...messageIds,
515
821
  ...ranges.flatMap((range) => [range.fromMessageId, range.toMessageId]),
516
822
  ]);
517
- if (wanted.size === 0) return { intervals: [], carriers: [], messageCount: 0 };
823
+ if (wanted.size === 0 && !removeSuppressed) return { intervals: [], carriers: [], messageCount: 0, ids: new Set() };
824
+ const stamped: SuppressionInterval[] = [];
518
825
 
519
826
  const locations = new Map<string, MessageLocation>();
520
- for (let offset = 0; offset <= targetIndex; offset += RECOVERY_SCAN_WINDOW) {
827
+ for (let offset = 0; offset <= lastKeptIndex; offset += RECOVERY_SCAN_WINDOW) {
521
828
  const messages = contextManager.getMessageWindow(
522
829
  offset,
523
- Math.min(RECOVERY_SCAN_WINDOW, targetIndex - offset + 1),
830
+ Math.min(RECOVERY_SCAN_WINDOW, lastKeptIndex - offset + 1),
524
831
  { resolveBlobs: false },
525
832
  ).messages;
526
833
  for (let i = 0; i < messages.length; i++) {
834
+ if (removeSuppressed && suppressionRecordOf(messages[i].metadata as Record<string, unknown> | undefined)) {
835
+ stamped.push({ start: offset + i, end: offset + i, fromId: messages[i].id, toId: messages[i].id });
836
+ }
527
837
  const externalId = String(messages[i].metadata?.messageId ?? '');
528
838
  if (!wanted.has(externalId)) continue;
529
839
  const index = offset + i;
@@ -565,10 +875,10 @@ function buildSuppressionPlan(
565
875
  }
566
876
  }
567
877
 
568
- const intervals: SuppressionInterval[] = [];
878
+ const intervals: SuppressionInterval[] = [...stamped];
569
879
  for (const id of messageIds) {
570
880
  const location = locations.get(id)!;
571
- intervals.push(...location.runs.map((run) => ({ ...run })));
881
+ appendAll(intervals, location.runs.map((run) => ({ ...run })));
572
882
  }
573
883
  for (const range of ranges) {
574
884
  const from = locations.get(range.fromMessageId)!;
@@ -587,7 +897,7 @@ function buildSuppressionPlan(
587
897
  fromId: earlier.firstId,
588
898
  toId: later.firstId,
589
899
  });
590
- for (const end of [from, to]) intervals.push(...end.runs.map((run) => ({ ...run })));
900
+ for (const end of [from, to]) appendAll(intervals, end.runs.map((run) => ({ ...run })));
591
901
  }
592
902
 
593
903
  intervals.sort((a, b) => a.start - b.start || a.end - b.end);
@@ -605,13 +915,15 @@ function buildSuppressionPlan(
605
915
  }
606
916
 
607
917
  const carriers: DiscordMessageMetadataCarrier[] = [];
918
+ const ids = new Set<string>();
608
919
  for (const interval of merged) {
609
- carriers.push(...collectDiscordCarriers(contextManager, interval.start, interval.end + 1));
920
+ appendAll(carriers, collectDiscordCarriers(contextManager, interval.start, interval.end + 1, ids));
610
921
  }
611
922
  return {
612
923
  intervals: merged,
613
924
  carriers,
614
925
  messageCount: merged.reduce((sum, interval) => sum + interval.end - interval.start + 1, 0),
926
+ ids,
615
927
  };
616
928
  }
617
929
 
@@ -619,12 +931,14 @@ function buildSuppressionPlan(
619
931
  * The addressing metadata of removed messages, read in bounded windows: only
620
932
  * the Discord address and the MCPL tags (for `addressed` selection) are kept,
621
933
  * never content. A sharded message may repeat its metadata across adjacent
622
- * records; selection dedupes by address.
934
+ * records; selection dedupes by address. Each row's id is added to `ids`
935
+ * where given.
623
936
  */
624
937
  function collectDiscordCarriers(
625
938
  contextManager: ContextManager,
626
939
  start: number,
627
940
  end: number,
941
+ ids?: Set<string>,
628
942
  ): DiscordMessageMetadataCarrier[] {
629
943
  const carriers: DiscordMessageMetadataCarrier[] = [];
630
944
  for (let offset = start; offset < end; offset += RECOVERY_SCAN_WINDOW) {
@@ -633,7 +947,8 @@ function collectDiscordCarriers(
633
947
  Math.min(RECOVERY_SCAN_WINDOW, end - offset),
634
948
  { resolveBlobs: false },
635
949
  ).messages;
636
- carriers.push(...messages.map(addressingOnly));
950
+ appendAll(carriers, messages.map(addressingOnly));
951
+ if (ids) for (const message of messages) ids.add(String(message.id));
637
952
  }
638
953
  return carriers;
639
954
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { createOfflineRecoveryBranch } from './offline-branch.js';
4
4
  import type { DiscordAwarenessMarks } from './discord-awareness-outbox.js';
5
+ import { appendAll } from '../append-all.js';
5
6
 
6
7
  interface CliOptions {
7
8
  storePath?: string;
@@ -11,6 +12,7 @@ interface CliOptions {
11
12
  messages?: number;
12
13
  suppressMessageIds: string[];
13
14
  suppressRanges: Array<{ fromMessageId: string; toMessageId: string }>;
15
+ removeSuppressed: boolean;
14
16
  branchName?: string;
15
17
  namespace?: string;
16
18
  discordServerId?: string;
@@ -35,8 +37,23 @@ Options:
35
37
  --message-id <id|link> Last Discord message the agent should still see
36
38
  --context-id <internal-id> Exact internal context message to retain as anchor
37
39
  --messages <count> Legacy alternative: remove N context entries
40
+ On a presented window the rows one write
41
+ presented with the anchor stay after it, a
42
+ count is over the agent's window, and a
43
+ recovery that would remove nothing is refused
38
44
  --suppress <id|link> Suppress one Discord message; repeat or comma-separate
39
45
  --suppress-range <a>..<b> Suppress an inclusive context range; repeatable
46
+ --remove-suppressed Remove every row a suppress rule hid, needing no
47
+ anchor. Run it before rolling back to a release
48
+ without suppress rules, which can refuse the
49
+ store at open otherwise; the source branch keeps
50
+ the rows. Once this release has made a window
51
+ write of two or more rows (the first write-down
52
+ of a window that holds a row, as at the first
53
+ boot on a store with history, or a presentation
54
+ of more than one row: MANIFEST version 2) no
55
+ earlier release opens the store, and only a copy
56
+ taken before the upgrade goes back
40
57
  --branch <name> Recovery branch name (generated by default)
41
58
  --namespace <namespace> Context namespace (default: agents/<agent>)
42
59
  --discord-server <id> Override/inject the Discord MCPL server id
@@ -53,7 +70,7 @@ The agent host must be stopped before running this command.`;
53
70
  }
54
71
 
55
72
  function parseArgs(args: string[]): CliOptions {
56
- const options: CliOptions = { dryRun: false, suppressMessageIds: [], suppressRanges: [], marks: 'none' };
73
+ const options: CliOptions = { dryRun: false, suppressMessageIds: [], suppressRanges: [], removeSuppressed: false, marks: 'none' };
57
74
  for (let i = 0; i < args.length; i++) {
58
75
  const arg = args[i];
59
76
  const value = () => {
@@ -70,7 +87,7 @@ function parseArgs(args: string[]): CliOptions {
70
87
  case '--messages': options.messages = Number(value()); break;
71
88
  case '--suppress':
72
89
  case '--suppress-message':
73
- options.suppressMessageIds.push(...value().split(',').map((item) => item.trim()).filter(Boolean));
90
+ appendAll(options.suppressMessageIds, value().split(',').map((item) => item.trim()).filter(Boolean));
74
91
  break;
75
92
  case '--suppress-range': {
76
93
  const raw = value();
@@ -97,6 +114,7 @@ function parseArgs(args: string[]): CliOptions {
97
114
  options.marks = marks;
98
115
  break;
99
116
  }
117
+ case '--remove-suppressed': options.removeSuppressed = true; break;
100
118
  case '--dry-run': options.dryRun = true; break;
101
119
  case '--help':
102
120
  case '-h':
@@ -113,9 +131,9 @@ async function main(): Promise<void> {
113
131
  if (
114
132
  !options.storePath
115
133
  || !options.agentName
116
- || (!options.messageId && !options.contextId && options.messages === undefined)
134
+ || (!options.messageId && !options.contextId && options.messages === undefined && !options.removeSuppressed)
117
135
  ) {
118
- throw new Error(`--store, --agent, and one anchor (--message-id, --context-id, or --messages) are required\n\n${usage()}`);
136
+ throw new Error(`--store, --agent, and one anchor (--message-id, --context-id, or --messages) or --remove-suppressed are required\n\n${usage()}`);
119
137
  }
120
138
 
121
139
  // A CLI choice and its application are the same moment (the host is
@@ -129,6 +147,7 @@ async function main(): Promise<void> {
129
147
  messages: options.messages,
130
148
  suppressMessageIds: options.suppressMessageIds,
131
149
  suppressRanges: options.suppressRanges,
150
+ removeSuppressed: options.removeSuppressed,
132
151
  branchName: options.branchName,
133
152
  namespace: options.namespace,
134
153
  discordServerId: options.discordServerId,