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