@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.
- package/README.md +204 -14
- package/dist/src/admission/admit.d.ts +18 -0
- package/dist/src/admission/admit.d.ts.map +1 -1
- package/dist/src/admission/admit.js +16 -3
- package/dist/src/admission/admit.js.map +1 -1
- package/dist/src/agent.d.ts +185 -78
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +279 -102
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +50 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -20
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +24 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/config-checks.d.ts +7 -0
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +14 -0
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/context-manager-errors.d.ts +25 -0
- package/dist/src/context-manager-errors.d.ts.map +1 -0
- package/dist/src/context-manager-errors.js +31 -0
- package/dist/src/context-manager-errors.js.map +1 -0
- package/dist/src/context-options.d.ts +67 -0
- package/dist/src/context-options.d.ts.map +1 -0
- package/dist/src/context-options.js +85 -0
- package/dist/src/context-options.js.map +1 -0
- package/dist/src/deferred-import.d.ts +93 -0
- package/dist/src/deferred-import.d.ts.map +1 -0
- package/dist/src/deferred-import.js +177 -0
- package/dist/src/deferred-import.js.map +1 -0
- package/dist/src/dendrite/audience.d.ts +223 -0
- package/dist/src/dendrite/audience.d.ts.map +1 -0
- package/dist/src/dendrite/audience.js +202 -0
- package/dist/src/dendrite/audience.js.map +1 -0
- package/dist/src/dendrite/index.d.ts +5 -3
- package/dist/src/dendrite/index.d.ts.map +1 -1
- package/dist/src/dendrite/index.js +2 -1
- package/dist/src/dendrite/index.js.map +1 -1
- package/dist/src/dendrite/presets.d.ts +10 -5
- package/dist/src/dendrite/presets.d.ts.map +1 -1
- package/dist/src/dendrite/presets.js +17 -5
- package/dist/src/dendrite/presets.js.map +1 -1
- package/dist/src/dendrite/registry.d.ts +62 -7
- package/dist/src/dendrite/registry.d.ts.map +1 -1
- package/dist/src/dendrite/registry.js +179 -10
- package/dist/src/dendrite/registry.js.map +1 -1
- package/dist/src/dendrite/types.d.ts +91 -8
- package/dist/src/dendrite/types.d.ts.map +1 -1
- package/dist/src/foreign-rounds.d.ts +69 -0
- package/dist/src/foreign-rounds.d.ts.map +1 -0
- package/dist/src/foreign-rounds.js +152 -0
- package/dist/src/foreign-rounds.js.map +1 -0
- package/dist/src/framework.d.ts +1977 -820
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +7571 -3457
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +85 -15
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +421 -47
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/rule-scope.d.ts +30 -0
- package/dist/src/gate/rule-scope.d.ts.map +1 -0
- package/dist/src/gate/rule-scope.js +41 -0
- package/dist/src/gate/rule-scope.js.map +1 -0
- package/dist/src/gate/suppress-script-worker.d.ts +2 -0
- package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
- package/dist/src/gate/suppress-script-worker.js +103 -0
- package/dist/src/gate/suppress-script-worker.js.map +1 -0
- package/dist/src/gate/suppress-scripts.d.ts +105 -0
- package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
- package/dist/src/gate/suppress-scripts.js +403 -0
- package/dist/src/gate/suppress-scripts.js.map +1 -0
- package/dist/src/gate/suppression-review.d.ts +143 -0
- package/dist/src/gate/suppression-review.d.ts.map +1 -0
- package/dist/src/gate/suppression-review.js +463 -0
- package/dist/src/gate/suppression-review.js.map +1 -0
- package/dist/src/gate/suppression-stats.d.ts +125 -0
- package/dist/src/gate/suppression-stats.d.ts.map +1 -0
- package/dist/src/gate/suppression-stats.js +214 -0
- package/dist/src/gate/suppression-stats.js.map +1 -0
- package/dist/src/gate/types.d.ts +135 -7
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/gate/wake-onboarding.d.ts +119 -0
- package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
- package/dist/src/gate/wake-onboarding.js +277 -0
- package/dist/src/gate/wake-onboarding.js.map +1 -0
- package/dist/src/held-writes.d.ts +31 -23
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +44 -59
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc-envelope.d.ts +69 -0
- package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
- package/dist/src/jsonrpc-envelope.js +66 -0
- package/dist/src/jsonrpc-envelope.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +131 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +290 -46
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +2 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +6 -6
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -22
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +105 -15
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +9 -7
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +13 -10
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +134 -56
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +8 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +24 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -18
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +45 -8
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +237 -86
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +59 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +432 -67
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +6 -5
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +15 -22
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +1 -1
- package/dist/src/modules/workspace/index.d.ts +28 -7
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +76 -22
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/notice-tags.d.ts +10 -1
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +10 -1
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/operator-log.d.ts +36 -3
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js +2 -2
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/presentation.d.ts +746 -0
- package/dist/src/presentation.d.ts.map +1 -0
- package/dist/src/presentation.js +1435 -0
- package/dist/src/presentation.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +15 -11
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +16 -12
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/read-marks.d.ts +192 -0
- package/dist/src/read-marks.d.ts.map +1 -0
- package/dist/src/read-marks.js +242 -0
- package/dist/src/read-marks.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +91 -6
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +273 -29
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +22 -3
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +5 -9
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +40 -7
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/stored-injections.d.ts +2 -2
- package/dist/src/stored-injections.js +2 -2
- package/dist/src/stored-removal.d.ts +17 -4
- package/dist/src/stored-removal.d.ts.map +1 -1
- package/dist/src/stored-removal.js +29 -5
- package/dist/src/stored-removal.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +12 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +42 -8
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +1159 -79
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +3709 -363
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +114 -17
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +87 -27
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/tune-out/window-anchor.d.ts +143 -0
- package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
- package/dist/src/tune-out/window-anchor.js +202 -0
- package/dist/src/tune-out/window-anchor.js.map +1 -0
- package/dist/src/types/agent.d.ts +86 -31
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +2 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -21
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +180 -69
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +510 -36
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +32 -3
- package/src/agent.ts +323 -120
- package/src/api/mcp-server-core.ts +54 -17
- package/src/api/server.ts +4 -23
- package/src/code-execution/isolate/scripts.ts +28 -3
- package/src/config-checks.ts +14 -0
- package/src/context-manager-errors.ts +37 -0
- package/src/context-options.ts +96 -0
- package/src/deferred-import.ts +239 -0
- package/src/dendrite/audience.ts +315 -0
- package/src/dendrite/index.ts +14 -2
- package/src/dendrite/presets.ts +18 -6
- package/src/dendrite/registry.ts +209 -12
- package/src/dendrite/types.ts +92 -8
- package/src/foreign-rounds.ts +199 -0
- package/src/framework.ts +7814 -3585
- package/src/gate/event-gate.ts +428 -65
- package/src/gate/index.ts +3 -0
- package/src/gate/rule-scope.ts +45 -0
- package/src/gate/suppress-script-worker.ts +89 -0
- package/src/gate/suppress-scripts.ts +414 -0
- package/src/gate/suppression-review.ts +517 -0
- package/src/gate/suppression-stats.ts +314 -0
- package/src/gate/types.ts +141 -7
- package/src/gate/wake-onboarding.ts +309 -0
- package/src/held-writes.ts +49 -60
- package/src/index.ts +9 -2
- package/src/jsonrpc-envelope.ts +81 -0
- package/src/mcpl/channel-registry.ts +353 -85
- package/src/mcpl/checkpoint-manager.ts +3 -2
- package/src/mcpl/feature-set-manager.ts +2 -1
- package/src/mcpl/inbound-source.ts +2 -1
- package/src/mcpl/inference-router.ts +7 -4
- package/src/mcpl/push-coalescer.ts +193 -34
- package/src/mcpl/push-handler.ts +10 -5
- package/src/mcpl/server-connection.ts +141 -60
- package/src/mcpl/tool-classes.ts +3 -0
- package/src/mcpl/types.ts +8 -2
- package/src/module-registry.ts +73 -17
- package/src/modules/discord/index.ts +229 -81
- package/src/modules/history/index.ts +439 -70
- package/src/modules/history/semantic.ts +15 -21
- package/src/modules/workspace/disk-agreement.ts +1 -1
- package/src/modules/workspace/index.ts +85 -23
- package/src/notice-tags.ts +10 -1
- package/src/operator-log.ts +36 -3
- package/src/presentation.ts +1676 -0
- package/src/prose-drafts.ts +16 -12
- package/src/read-marks.ts +328 -0
- package/src/recovery/offline-branch.ts +346 -32
- package/src/recovery/recover-cli.ts +21 -3
- package/src/send-tools.ts +40 -7
- package/src/stored-injections.ts +2 -2
- package/src/stored-removal.ts +31 -7
- package/src/tool-result-guard.ts +41 -7
- package/src/tune-out/coordinator.ts +4089 -457
- package/src/tune-out/tools.ts +160 -43
- package/src/tune-out/window-anchor.ts +277 -0
- package/src/types/agent.ts +90 -31
- package/src/types/events.ts +2 -0
- package/src/types/framework.ts +43 -21
- package/src/types/module.ts +181 -68
- package/src/types/trace.ts +542 -40
- package/dist/src/reasoning-loss-notice.d.ts +0 -23
- package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
- package/dist/src/reasoning-loss-notice.js +0 -52
- package/dist/src/reasoning-loss-notice.js.map +0 -1
- package/dist/src/reasoning-loss.d.ts +0 -24
- package/dist/src/reasoning-loss.d.ts.map +0 -1
- package/dist/src/reasoning-loss.js +0 -102
- package/dist/src/reasoning-loss.js.map +0 -1
- package/src/reasoning-loss-notice.ts +0 -64
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
82
|
-
*
|
|
83
|
-
* the
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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 !==
|
|
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 -
|
|
161
|
-
removedCount =
|
|
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
|
-
|
|
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:
|
|
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.
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
808
|
+
lastKeptIndex: number,
|
|
505
809
|
rawMessageIds: string[],
|
|
506
810
|
rawRanges: Array<{ fromMessageId: string; toMessageId: string }>,
|
|
507
|
-
|
|
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 <=
|
|
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,
|
|
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,
|