@animalabs/agent-framework 0.23.1 → 0.24.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 +212 -42
- package/dist/src/agent.d.ts +60 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +92 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +14 -12
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/engine.d.ts +177 -0
- package/dist/src/code-execution/engine.d.ts.map +1 -0
- package/dist/src/code-execution/engine.js +494 -0
- package/dist/src/code-execution/engine.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +5 -10
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js +5 -9
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/isolate/calls.d.ts +114 -0
- package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/calls.js +145 -0
- package/dist/src/code-execution/isolate/calls.js.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.js +5 -0
- package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
- package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
- package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-main.js +132 -0
- package/dist/src/code-execution/isolate/child-main.js.map +1 -0
- package/dist/src/code-execution/isolate/child.d.ts +44 -0
- package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child.js +315 -0
- package/dist/src/code-execution/isolate/child.js.map +1 -0
- package/dist/src/code-execution/isolate/frames.d.ts +80 -0
- package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/frames.js +210 -0
- package/dist/src/code-execution/isolate/frames.js.map +1 -0
- package/dist/src/code-execution/isolate/output.d.ts +68 -0
- package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/output.js +143 -0
- package/dist/src/code-execution/isolate/output.js.map +1 -0
- package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
- package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/prelude.js +401 -0
- package/dist/src/code-execution/isolate/prelude.js.map +1 -0
- package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
- package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/protocol.js +150 -0
- package/dist/src/code-execution/isolate/protocol.js.map +1 -0
- package/dist/src/code-execution/isolate/render.d.ts +57 -0
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/render.js +214 -0
- package/dist/src/code-execution/isolate/render.js.map +1 -0
- package/dist/src/code-execution/isolate/runner.d.ts +250 -0
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/runner.js +854 -0
- package/dist/src/code-execution/isolate/runner.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/scripts.js +357 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -0
- package/dist/src/code-execution/quickjs.wasm +0 -0
- package/dist/src/code-execution/tool-definition.d.ts +93 -43
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +176 -142
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/deadline-timer.d.ts +3 -0
- package/dist/src/deadline-timer.d.ts.map +1 -1
- package/dist/src/deadline-timer.js +1 -1
- package/dist/src/deadline-timer.js.map +1 -1
- package/dist/src/framework.d.ts +1036 -440
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4131 -2228
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +28 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +88 -10
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -3
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +16 -3
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +471 -75
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1673 -337
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +12 -7
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +12 -7
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +9 -5
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +50 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +130 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -2
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +6 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +8 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +15 -6
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +168 -53
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -5
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +4 -5
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +97 -10
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +173 -16
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -14
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +227 -50
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +6 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +4 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +4 -4
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +50 -21
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +158 -74
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +33 -0
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +55 -7
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +163 -34
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +22 -6
- 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 +4 -2
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +100 -13
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +8 -13
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +27 -4
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +15 -3
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +27 -5
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +4 -1
- package/dist/src/modules/unknown-tool.d.ts.map +1 -1
- package/dist/src/modules/unknown-tool.js +4 -1
- package/dist/src/modules/unknown-tool.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +3 -1
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
- package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-plan.js +794 -0
- package/dist/src/modules/workspace/edit-plan.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +117 -0
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-text.js +584 -0
- package/dist/src/modules/workspace/edit-text.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +5 -4
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +78 -25
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +484 -61
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2173 -645
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +213 -38
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +416 -36
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +15 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/text.d.ts +32 -0
- package/dist/src/modules/workspace/text.d.ts.map +1 -0
- package/dist/src/modules/workspace/text.js +67 -0
- package/dist/src/modules/workspace/text.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +210 -6
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +3 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/operator-log.d.ts +52 -24
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +26 -1
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +164 -39
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +23 -4
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +11 -0
- package/dist/src/send-tools.d.ts.map +1 -0
- package/dist/src/send-tools.js +11 -0
- package/dist/src/send-tools.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +4 -3
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +5 -4
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +1 -1
- package/dist/src/tool-result-history.js +1 -1
- package/dist/src/tune-out/coordinator.d.ts +21 -0
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +139 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +4 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +62 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +62 -58
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +40 -8
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +111 -10
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/types.d.ts +16 -0
- package/dist/src/usage/types.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +14 -1
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +81 -41
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +6 -5
- package/src/agent.ts +93 -12
- package/src/api/server.ts +14 -12
- package/src/code-execution/engine.ts +595 -0
- package/src/code-execution/index.ts +5 -17
- package/src/code-execution/isolate/calls.ts +199 -0
- package/src/code-execution/isolate/child-entry.ts +5 -0
- package/src/code-execution/isolate/child-main.ts +132 -0
- package/src/code-execution/isolate/child.ts +327 -0
- package/src/code-execution/isolate/frames.ts +209 -0
- package/src/code-execution/isolate/output.ts +161 -0
- package/src/code-execution/isolate/prelude.ts +409 -0
- package/src/code-execution/isolate/protocol.ts +240 -0
- package/src/code-execution/isolate/render.ts +230 -0
- package/src/code-execution/isolate/runner.ts +911 -0
- package/src/code-execution/isolate/scripts.ts +479 -0
- package/src/code-execution/quickjs.wasm +0 -0
- package/src/code-execution/quickjs.wasm.LICENSES +149 -0
- package/src/code-execution/tool-definition.ts +236 -167
- package/src/deadline-timer.ts +1 -1
- package/src/framework.ts +4507 -2406
- package/src/gate/event-gate.ts +78 -11
- package/src/held-writes.ts +17 -3
- package/src/index.ts +30 -22
- package/src/mcpl/channel-registry.ts +1825 -368
- package/src/mcpl/conversation-router.ts +12 -7
- package/src/mcpl/convert-block.ts +9 -5
- package/src/mcpl/inbound-source.ts +131 -0
- package/src/mcpl/index.ts +4 -2
- package/src/mcpl/log-value.ts +9 -0
- package/src/mcpl/modern-connection.ts +162 -53
- package/src/mcpl/prose-grammar.ts +4 -5
- package/src/mcpl/protocol-family.ts +199 -21
- package/src/mcpl/push-coalescer.ts +306 -62
- package/src/mcpl/push-handler.ts +10 -2
- package/src/mcpl/reference-fetcher.ts +4 -4
- package/src/mcpl/server-connection.ts +180 -79
- package/src/mcpl/tags.ts +36 -0
- package/src/mcpl/tool-result-normalize.ts +2 -1
- package/src/mcpl/transport.ts +162 -33
- package/src/mcpl/types.ts +23 -6
- package/src/module-registry.ts +92 -13
- package/src/modules/discord/index.ts +8 -13
- package/src/modules/history/index.ts +25 -3
- package/src/modules/history/semantic.ts +29 -6
- package/src/modules/index.ts +1 -0
- package/src/modules/unknown-tool.ts +4 -1
- package/src/modules/workspace/branch-intent.ts +3 -1
- package/src/modules/workspace/edit-plan.ts +954 -0
- package/src/modules/workspace/edit-text.ts +618 -0
- package/src/modules/workspace/effects.ts +70 -19
- package/src/modules/workspace/index.ts +2247 -642
- package/src/modules/workspace/observe.ts +465 -56
- package/src/modules/workspace/reconcile.ts +14 -7
- package/src/modules/workspace/text.ts +68 -0
- package/src/modules/workspace/types.ts +211 -6
- package/src/modules/workspace/watcher.ts +3 -2
- package/src/operator-log.ts +47 -19
- package/src/recovery/discord-awareness-outbox.ts +1500 -356
- package/src/recovery/offline-branch.ts +189 -55
- package/src/recovery/recover-cli.ts +25 -4
- package/src/send-tools.ts +11 -0
- package/src/tool-result-guard.ts +6 -5
- package/src/tool-result-history.ts +1 -1
- package/src/tune-out/coordinator.ts +163 -42
- package/src/types/agent.ts +4 -0
- package/src/types/events.ts +62 -9
- package/src/types/framework.ts +62 -58
- package/src/types/module.ts +40 -8
- package/src/types/trace.ts +121 -15
- package/src/usage/types.ts +16 -0
- package/src/usage/usage-tracker.ts +86 -43
- package/dist/src/code-execution/py-runner.d.ts +0 -191
- package/dist/src/code-execution/py-runner.d.ts.map +0 -1
- package/dist/src/code-execution/py-runner.js +0 -655
- package/dist/src/code-execution/py-runner.js.map +0 -1
- package/dist/src/code-execution/runtime-py.d.ts +0 -50
- package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
- package/dist/src/code-execution/runtime-py.js +0 -677
- package/dist/src/code-execution/runtime-py.js.map +0 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
- package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
- package/dist/src/mcpl/prose-stream-router.js +0 -291
- package/dist/src/mcpl/prose-stream-router.js.map +0 -1
- package/src/code-execution/py-runner.ts +0 -791
- package/src/code-execution/runtime-py.ts +0 -677
- package/src/mcpl/prose-stream-router.ts +0 -268
|
@@ -96,6 +96,13 @@ export interface CoalescedOccurrence<E = unknown> {
|
|
|
96
96
|
/** Set by the coalescer: deliver into this agent only (§3.2 audience may
|
|
97
97
|
* narrow, never widen — a replacement or notice follows the prior delivery). */
|
|
98
98
|
deliverTo?: string;
|
|
99
|
+
/** Set by the coalescer when the delivery is the agent's first view of
|
|
100
|
+
* the subject: a plain occurrence that replaces one that was the agent's
|
|
101
|
+
* first view (its `initial` birth, or a replacement so marked) and that
|
|
102
|
+
* the agent may never have been shown (CoalescerHost.isUnshown), or the
|
|
103
|
+
* first delivery of a batch that opened on a first view (its render, or
|
|
104
|
+
* a plain occurrence that drops it). No other path sets it. */
|
|
105
|
+
firstView?: boolean;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
export interface DeferredNotice { eventId: string; timestamp: string; data?: unknown }
|
|
@@ -117,6 +124,13 @@ interface DeferredBatch<E> {
|
|
|
117
124
|
dropped: number;
|
|
118
125
|
/** Set on a batch restored from a snapshot whose render may have started. */
|
|
119
126
|
noRender?: boolean;
|
|
127
|
+
/** The batch opened on the agent's first view of its subject: a fresh
|
|
128
|
+
* `initial` birth, or a first-view occupant it displaced that nothing had
|
|
129
|
+
* shown. Its first delivery is a first view too, while nothing of the
|
|
130
|
+
* subject has been read since and no other version of it is placed. It is
|
|
131
|
+
* cleared when the subject's first view ends while the batch is pending
|
|
132
|
+
* or rendering (occupantLeaves). */
|
|
133
|
+
firstView?: true;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
interface Rendering<E> {
|
|
@@ -139,25 +153,65 @@ export interface AssemblyResult {
|
|
|
139
153
|
* read: empty or blank render (§5.2), cancelled, revoked before the render
|
|
140
154
|
* or at its response, a render-start that could not be made durable (the
|
|
141
155
|
* batch then waits for its recovery wake), a batch whose audience doesn't
|
|
142
|
-
* include the agent, a render placed in
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* by another
|
|
156
|
+
* include the agent, a render placed in a deferred queue (another
|
|
157
|
+
* agent's, or this agent's own behind a write held there), a render
|
|
158
|
+
* another assembly froze (its compile may show it, as that request's), or
|
|
159
|
+
* a cause whose batch was withdrawn, displaced or consumed by another
|
|
160
|
+
* request. */
|
|
146
161
|
materialized: Set<string>;
|
|
147
162
|
}
|
|
148
163
|
|
|
164
|
+
/** What holds a subject's single slot (§3.2). A render describes what changed
|
|
165
|
+
* since the server's previous render (§5.2), so the renders before it that
|
|
166
|
+
* are still unread stay where they were placed, and the newest carries them
|
|
167
|
+
* as `before` (§3.3, vector 49). A plain occurrence displaces the run and a
|
|
168
|
+
* retraction removes it (§6), newest first (removeOccupant). */
|
|
169
|
+
interface Occupant {
|
|
170
|
+
eventId: string;
|
|
171
|
+
placement: CoalescingPlacement;
|
|
172
|
+
/** A materialized batch (its result or its fallback), which later renders build on. */
|
|
173
|
+
rendered?: true;
|
|
174
|
+
/** This occupant was the agent's first view of the subject when it was
|
|
175
|
+
* placed, as `firstView` defines it. It hands that view on only while
|
|
176
|
+
* nothing of the subject has been read and nothing of its run may have
|
|
177
|
+
* been shown (occupantLeaves). Unknown counts as not one. A plain
|
|
178
|
+
* occurrence can carry it, and of a run's renders only the oldest, since a
|
|
179
|
+
* render that joins a run finds a version placed. In memory only: a
|
|
180
|
+
* snapshot's occupant comes back as read. */
|
|
181
|
+
firstView?: true;
|
|
182
|
+
/** The run's earlier renders, oldest first, each placed ahead of this one. */
|
|
183
|
+
before?: Array<Omit<Occupant, 'before'>>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The occupant holding a run's renders, given oldest first: the newest,
|
|
187
|
+
* carrying the rest as `before`. */
|
|
188
|
+
function runOf(members: Array<Omit<Occupant, 'before'>>): Occupant | undefined {
|
|
189
|
+
if (members.length === 0) return undefined;
|
|
190
|
+
const newest = members[members.length - 1];
|
|
191
|
+
return members.length > 1 ? { ...newest, before: members.slice(0, -1) } : newest;
|
|
192
|
+
}
|
|
193
|
+
|
|
149
194
|
interface SubjectState<E> {
|
|
150
195
|
history: 'none' | 'some' | 'unknown';
|
|
151
|
-
occupant?:
|
|
196
|
+
occupant?: Occupant;
|
|
152
197
|
batch?: DeferredBatch<E>;
|
|
153
198
|
rendering?: Rendering<E>;
|
|
154
199
|
consumedEventId?: string;
|
|
155
200
|
identity?: CoalescedOccurrence['identity'];
|
|
156
201
|
/** Agent that received the last delivery; replacements and notices follow it. */
|
|
157
202
|
audienceAgent?: string;
|
|
203
|
+
/** The agent's first view of the subject has ended: an occupant that may
|
|
204
|
+
* have been shown left it without handing a first view on
|
|
205
|
+
* (occupantLeaves, endFirstView). No later batch or birth of the subject
|
|
206
|
+
* is a first view while the coalescer keeps this record. An idle
|
|
207
|
+
* subject's record can be evicted (prune), and an `initial` occurrence
|
|
208
|
+
* after that starts the subject afresh. */
|
|
209
|
+
firstViewEnded?: true;
|
|
158
210
|
touchedAt: number;
|
|
159
211
|
}
|
|
160
212
|
|
|
213
|
+
interface ReceiptEntry { result: PushEventResult; at: number; committed: boolean; firstView?: true }
|
|
214
|
+
|
|
161
215
|
export interface CoalescingSnapshot {
|
|
162
216
|
version: 1;
|
|
163
217
|
receipts: Array<[string, { result: PushEventResult; at: number }]>;
|
|
@@ -168,7 +222,17 @@ export interface CoalescingSnapshot {
|
|
|
168
222
|
identity?: CoalescedOccurrence['identity'];
|
|
169
223
|
occupant?: { eventId: string; placement: CoalescingPlacement };
|
|
170
224
|
audienceAgent?: string;
|
|
171
|
-
|
|
225
|
+
/** The subject's first view has ended (SubjectState.firstViewEnded). A
|
|
226
|
+
* snapshot written before this field existed has none and reads as not
|
|
227
|
+
* ended. So after an upgrade, a subject whose first view had ended
|
|
228
|
+
* without a read is granted a first view again. Until the agent is shown
|
|
229
|
+
* or reads a version of it, each delivery that would mark a first view (a
|
|
230
|
+
* render, or a plain occurrence that replaces an unread version or a
|
|
231
|
+
* pending notice) carries a spurious `firstView`. */
|
|
232
|
+
firstViewEnded?: true;
|
|
233
|
+
/** `firstView` only on a batch that wasn't rendering: one that was may
|
|
234
|
+
* have been shown. */
|
|
235
|
+
batch?: { latest: CoalescedOccurrence<unknown>; notices: DeferredNotice[]; dropped: number; rendering?: boolean; firstView?: true };
|
|
172
236
|
}>;
|
|
173
237
|
}
|
|
174
238
|
/** One receipt, written synchronously at acceptance (bridges the snapshot throttle). */
|
|
@@ -179,6 +243,11 @@ export interface CoalescingReceiptRecord {
|
|
|
179
243
|
subject: string;
|
|
180
244
|
/** History the subject was created with, for a subject first seen by this receipt. */
|
|
181
245
|
born?: SubjectState<unknown>['history'];
|
|
246
|
+
/** The subject's first view had ended by this acceptance
|
|
247
|
+
* (SubjectState.firstViewEnded), so a crash before the snapshot flush
|
|
248
|
+
* can't forget the end. A record written before this field existed has
|
|
249
|
+
* none and reads as not ended, as a snapshot does. */
|
|
250
|
+
firstViewEnded?: true;
|
|
182
251
|
/** The subject's pending batch AFTER this acceptance: the accepted work
|
|
183
252
|
* itself (so a crash before the snapshot flush cannot lose it while keeping
|
|
184
253
|
* the receipt that suppresses its retry), or `null` when the acceptance
|
|
@@ -188,8 +257,20 @@ export interface CoalescingReceiptRecord {
|
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
export interface CoalescerHost<E> {
|
|
191
|
-
/** True iff the placement still exists and
|
|
260
|
+
/** True iff the placement still exists and, as far as the host can tell,
|
|
261
|
+
* no model request has included it. The host can't always tell: a
|
|
262
|
+
* message injected into a stream that didn't say whether it carries
|
|
263
|
+
* injections, as no membrane 0.5.x stream does, counts as unread though
|
|
264
|
+
* a request may have included it. */
|
|
192
265
|
isUnread(placement: CoalescingPlacement): boolean;
|
|
266
|
+
/** True iff nothing may have shown the placement to the agent: it is still
|
|
267
|
+
* in the deferred queue and known not to be stored already, or it is
|
|
268
|
+
* stored where no committing compile has begun to assemble it and no tool
|
|
269
|
+
* boundary has injected past it. Unlike
|
|
270
|
+
* isUnread, a compile counts from its start, and a boundary counts
|
|
271
|
+
* whether or not its stream says it carries injected messages: a doubt
|
|
272
|
+
* counts as shown. */
|
|
273
|
+
isUnshown(placement: CoalescingPlacement): boolean;
|
|
193
274
|
/** False when the unread occurrence could not be removed (it may still be read). */
|
|
194
275
|
remove(placement: CoalescingPlacement): boolean;
|
|
195
276
|
/** Ordinary delivery of the event; undefined when routing delivered nowhere.
|
|
@@ -200,9 +281,9 @@ export interface CoalescerHost<E> {
|
|
|
200
281
|
wakeForBatch(occurrence: CoalescedOccurrence<E>): Promise<void>;
|
|
201
282
|
/** Queue an ordinary push wake for `agent`, under the batch's own wake
|
|
202
283
|
* policy, to read a render of the batch placed in its deferred queue that
|
|
203
|
-
* no request consumed
|
|
204
|
-
* and later withdrawals of them, leave it. */
|
|
205
|
-
wakeToRead(occurrence: CoalescedOccurrence<E>, agent: string): Promise<void>;
|
|
284
|
+
* no request consumed, at `deferredId`. It carries no subject, so the
|
|
285
|
+
* subject's other wakes, and later withdrawals of them, leave it. */
|
|
286
|
+
wakeToRead(occurrence: CoalescedOccurrence<E>, agent: string, deferredId?: string): Promise<void>;
|
|
206
287
|
/** Withdraw unstarted wakes whose sole cause is this subject. */
|
|
207
288
|
cancelWake(subject: string): void;
|
|
208
289
|
/** The batch's audience still holds the authority it was admitted under. */
|
|
@@ -326,7 +407,7 @@ function showsAsItself(b: Record<string, unknown>): boolean {
|
|
|
326
407
|
if (b.type === 'resource') return classifyBlock(b).kind === 'reference';
|
|
327
408
|
if (b.type !== 'image' && b.type !== 'audio') return false;
|
|
328
409
|
if (isInlineContradiction(b)) return false;
|
|
329
|
-
if (isFilledString(b.uri) && b.disposition) return classifyBlock(b).kind === 'reference';
|
|
410
|
+
if (isFilledString(b.uri) && b.disposition !== undefined) return classifyBlock(b).kind === 'reference';
|
|
330
411
|
if (isFilledString(b.data) && isFilledString(b.mimeType)) return true;
|
|
331
412
|
return b.type === 'image' && isFilledString(b.uri);
|
|
332
413
|
}
|
|
@@ -367,8 +448,17 @@ export class PushCoalescer<E = unknown> {
|
|
|
367
448
|
private readonly subjects = new Map<string, SubjectState<E>>();
|
|
368
449
|
/** `committed`: the durability barrier succeeded after this receipt was
|
|
369
450
|
* written. An uncommitted receipt deduplicates effects but is not an
|
|
370
|
-
* acknowledgement until a commit succeeds.
|
|
371
|
-
|
|
451
|
+
* acknowledgement until a commit succeeds. `firstView`: the admission it
|
|
452
|
+
* records handed its occurrence to the host's delivery marked as the
|
|
453
|
+
* agent's first view (`firstView`), whether or not the host placed
|
|
454
|
+
* it, so a retry answered from it says so too. Kept in memory only: a
|
|
455
|
+
* receipt restored from a snapshot or the crash-window bridge reads as no
|
|
456
|
+
* first view. */
|
|
457
|
+
private readonly receipts = new Map<string, ReceiptEntry>();
|
|
458
|
+
/** Set by a delivery that carries `firstView`. admit() clears it before
|
|
459
|
+
* an admission's effects and reads it after them; every delivery runs
|
|
460
|
+
* under the lock, so between the two it is that admission's alone. */
|
|
461
|
+
private deliveredFirstView = false;
|
|
372
462
|
private readonly retryWindowMs: number;
|
|
373
463
|
private readonly maxSubjects: number;
|
|
374
464
|
private readonly maxNotices: number;
|
|
@@ -424,12 +514,26 @@ export class PushCoalescer<E = unknown> {
|
|
|
424
514
|
return (this.rendersInFlight.get(serverId) ?? 0) > 0;
|
|
425
515
|
}
|
|
426
516
|
|
|
517
|
+
/** The host's delivery, noting one that carries `firstView` for the
|
|
518
|
+
* receipt of the admission it belongs to (deliveredFirstView). */
|
|
519
|
+
private deliver(...args: Parameters<CoalescerHost<E>['deliver']>): ReturnType<CoalescerHost<E>['deliver']> {
|
|
520
|
+
if (args[0].firstView) this.deliveredFirstView = true;
|
|
521
|
+
return this.host.deliver(...args);
|
|
522
|
+
}
|
|
523
|
+
|
|
427
524
|
receipt(serverId: string, binding: string, eventId: string): PushEventResult | undefined {
|
|
428
525
|
const entry = this.receiptEntry(coalescingReceiptKey(serverId, binding, eventId));
|
|
429
526
|
return entry && structuredClone(entry.result);
|
|
430
527
|
}
|
|
431
528
|
|
|
432
|
-
|
|
529
|
+
/** Whether the admission this receipt records delivered its occurrence as
|
|
530
|
+
* the agent's first view: for the host's own bookkeeping, such as the
|
|
531
|
+
* channel registry's reply anchor, and never part of the wire result. */
|
|
532
|
+
receiptFirstView(serverId: string, binding: string, eventId: string): boolean {
|
|
533
|
+
return this.receiptEntry(coalescingReceiptKey(serverId, binding, eventId))?.firstView === true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private receiptEntry(key: string): ReceiptEntry | undefined {
|
|
433
537
|
const entry = this.receipts.get(key);
|
|
434
538
|
if (!entry) return undefined;
|
|
435
539
|
if (this.now() - entry.at > this.retryWindowMs) { this.receipts.delete(key); return undefined; }
|
|
@@ -439,7 +543,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
439
543
|
/** Acknowledge a receipt: durable first. A receipt whose commit failed is
|
|
440
544
|
* committed now (no effects are repeated); if that fails again, so does
|
|
441
545
|
* the acknowledgement, and the producer retries once more. */
|
|
442
|
-
private async acknowledge(key: string, entry:
|
|
546
|
+
private async acknowledge(key: string, entry: ReceiptEntry, subject: string, eventId: string): Promise<PushEventResult> {
|
|
443
547
|
if (!entry.committed) {
|
|
444
548
|
try {
|
|
445
549
|
await this.host.commit?.();
|
|
@@ -480,10 +584,14 @@ export class PushCoalescer<E = unknown> {
|
|
|
480
584
|
const born = this.subjects.has(subject) ? undefined : (occurrence.initial && !occurrence.retract ? 'none' as const : 'unknown' as const);
|
|
481
585
|
const state = this.subjectFor(subject, occurrence);
|
|
482
586
|
const occupantUnread = this.refreshOccupant(state);
|
|
483
|
-
|
|
587
|
+
// A notice leaves an unread render in place (acceptDeferred): neither
|
|
588
|
+
// displaced nor found consumed, it isn't the prior (§3.3).
|
|
589
|
+
const occupantPrior = occurrence.deferred && state.occupant?.rendered ? undefined : state.occupant?.eventId;
|
|
590
|
+
const priorEventId = occupantPrior ?? state.batch?.latest.eventId ?? state.rendering?.batch.latest.eventId ?? state.consumedEventId;
|
|
484
591
|
this.host.audit({ kind: 'received', subject, eventId: occurrence.eventId, retract: occurrence.retract, deferred: occurrence.deferred });
|
|
485
592
|
|
|
486
593
|
let outcome: CoalesceOutcome;
|
|
594
|
+
this.deliveredFirstView = false;
|
|
487
595
|
if (occurrence.retract) {
|
|
488
596
|
outcome = await this.retract(subject, state, occurrence, occupantUnread);
|
|
489
597
|
} else if (occurrence.deferred) {
|
|
@@ -497,10 +605,11 @@ export class PushCoalescer<E = unknown> {
|
|
|
497
605
|
state.touchedAt = this.now();
|
|
498
606
|
const result: PushEventResult = { accepted: true, coalesce: { outcome, ...(priorEventId ? { priorEventId } : {}) } };
|
|
499
607
|
const receiptKey = coalescingReceiptKey(occurrence.serverId, occurrence.binding, occurrence.eventId);
|
|
500
|
-
const entry = { result: structuredClone(result), at: this.now(), committed: false };
|
|
608
|
+
const entry: ReceiptEntry = { result: structuredClone(result), at: this.now(), committed: false, ...(this.deliveredFirstView ? { firstView: true as const } : {}) };
|
|
501
609
|
try {
|
|
502
610
|
this.host.recordReceipt?.({
|
|
503
611
|
key: receiptKey, result: entry.result, at: entry.at, subject, ...(born ? { born } : {}),
|
|
612
|
+
...(state.firstViewEnded ? { firstViewEnded: true as const } : {}),
|
|
504
613
|
batch: state.batch ? structuredClone({ latest: state.batch.latest as CoalescedOccurrence<unknown>, notices: state.batch.notices, dropped: state.batch.dropped }) : null,
|
|
505
614
|
});
|
|
506
615
|
} catch (error) {
|
|
@@ -534,16 +643,80 @@ export class PushCoalescer<E = unknown> {
|
|
|
534
643
|
return state;
|
|
535
644
|
}
|
|
536
645
|
|
|
537
|
-
/**
|
|
646
|
+
/**
|
|
647
|
+
* Re-check the occupant against the host, member by member: a member the
|
|
648
|
+
* host counts as read, or that is gone (isUnread is false either way),
|
|
649
|
+
* becomes history, and the run keeps the members still unread. Returns
|
|
650
|
+
* whether any are.
|
|
651
|
+
*/
|
|
538
652
|
private refreshOccupant(state: SubjectState<E>): boolean {
|
|
539
|
-
|
|
540
|
-
if (
|
|
653
|
+
const occupant = state.occupant;
|
|
654
|
+
if (!occupant) return false;
|
|
655
|
+
const { before = [], ...newest } = occupant;
|
|
656
|
+
const members = [...before, newest];
|
|
657
|
+
const unread = members.filter((member) => this.host.isUnread(member.placement));
|
|
658
|
+
if (unread.length === members.length) return true;
|
|
541
659
|
state.history = 'some';
|
|
542
|
-
state.consumedEventId =
|
|
543
|
-
state.occupant =
|
|
660
|
+
state.consumedEventId = members.filter((member) => !unread.includes(member)).at(-1)!.eventId;
|
|
661
|
+
state.occupant = runOf(unread);
|
|
662
|
+
return unread.length > 0;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Remove the occupant and the run it builds on, newest first, auditing each
|
|
667
|
+
* as `kind`. Every member is unread: refreshOccupant has just run, under the
|
|
668
|
+
* lock. The host's removal may still refuse one, such as a held write the
|
|
669
|
+
* host can't tell isn't already stored. A member the host
|
|
670
|
+
* can't remove may be read, so the walk stops there, leaving it and the
|
|
671
|
+
* renders it builds on, and the history becomes `unknown`.
|
|
672
|
+
* Returns false then. It doesn't touch `state.occupant`, which still names
|
|
673
|
+
* the members it removed: each caller clears it, so what the walk left is
|
|
674
|
+
* history from then on, as an occupant whose removal failed always was.
|
|
675
|
+
*/
|
|
676
|
+
private removeOccupant(subject: string, state: SubjectState<E>, kind: 'removed' | 'displaced'): boolean {
|
|
677
|
+
const occupant = state.occupant!;
|
|
678
|
+
for (const member of [occupant, ...[...(occupant.before ?? [])].reverse()]) {
|
|
679
|
+
this.host.audit({ kind, subject, eventId: member.eventId });
|
|
680
|
+
if (!this.host.remove(member.placement)) {
|
|
681
|
+
// Still in context and possibly about to be read.
|
|
682
|
+
this.host.audit({ kind: 'remove-failed', subject, eventId: member.eventId });
|
|
683
|
+
state.history = 'unknown';
|
|
684
|
+
state.consumedEventId = member.eventId;
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* An unread occupant leaves the subject: a notice or a plain occurrence
|
|
693
|
+
* displaces it, or a retraction withdraws it, with any run of renders it
|
|
694
|
+
* carries. Returns whether it was a first view that nothing may have shown
|
|
695
|
+
* (isUnshown), which a replacement carries on: for a run, its oldest render
|
|
696
|
+
* carries the mark, and no render of it may have been shown. Nor may
|
|
697
|
+
* anything of the subject have been read: a member a request has read, or
|
|
698
|
+
* that is gone, leaves the run before this (refreshOccupant), and only the
|
|
699
|
+
* subject's history still says so. Any other occupant may have been seen,
|
|
700
|
+
* and this is the last point where the host can say so, since the removal
|
|
701
|
+
* takes its placements: the subject's first view ends here (endFirstView).
|
|
702
|
+
*/
|
|
703
|
+
private occupantLeaves(state: SubjectState<E>, occupant: Occupant): boolean {
|
|
704
|
+
const { before = [], ...newest } = occupant;
|
|
705
|
+
const members = [...before, newest];
|
|
706
|
+
if (state.history === 'none' && members[0].firstView === true && members.every((member) => this.host.isUnshown(member.placement))) return true;
|
|
707
|
+
this.endFirstView(state);
|
|
544
708
|
return false;
|
|
545
709
|
}
|
|
546
710
|
|
|
711
|
+
/** The subject's first view ends: for a batch pending or rendering, whose
|
|
712
|
+
* first delivery would come after it, and for every later batch and
|
|
713
|
+
* birth (firstViewEnded). */
|
|
714
|
+
private endFirstView(state: SubjectState<E>): void {
|
|
715
|
+
state.firstViewEnded = true;
|
|
716
|
+
if (state.batch) delete state.batch.firstView;
|
|
717
|
+
if (state.rendering) delete state.rendering.batch.firstView;
|
|
718
|
+
}
|
|
719
|
+
|
|
547
720
|
private scheduleRecovery(subject: string, state: SubjectState<E>): void {
|
|
548
721
|
const prior = this.recovery.get(subject);
|
|
549
722
|
if (prior?.timer) clearTimeout(prior.timer);
|
|
@@ -593,12 +766,10 @@ export class PushCoalescer<E = unknown> {
|
|
|
593
766
|
|
|
594
767
|
private async retract(subject: string, state: SubjectState<E>, occurrence: CoalescedOccurrence<E>, occupantUnread: boolean): Promise<CoalesceOutcome> {
|
|
595
768
|
if (occupantUnread && state.occupant) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
state.history = 'unknown';
|
|
601
|
-
}
|
|
769
|
+
// Asked of the whole run before the walk takes its placements.
|
|
770
|
+
this.occupantLeaves(state, state.occupant);
|
|
771
|
+
// Whatever the walk leaves may be read, and the history it sets says so (§6).
|
|
772
|
+
this.removeOccupant(subject, state, 'removed');
|
|
602
773
|
}
|
|
603
774
|
state.occupant = undefined;
|
|
604
775
|
this.dropBatches(subject, state);
|
|
@@ -611,7 +782,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
611
782
|
// The notice is an ordinary occurrence: it rides the normal delivery path
|
|
612
783
|
// (tags → gate policy → wake) and is consumed like any message — in the
|
|
613
784
|
// context that read a version, never a new one (§3.2).
|
|
614
|
-
const placement = await this.
|
|
785
|
+
const placement = await this.deliver({ ...occurrence, deliverTo: state.audienceAgent });
|
|
615
786
|
if (placement) {
|
|
616
787
|
state.occupant = { eventId: occurrence.eventId, placement };
|
|
617
788
|
state.audienceAgent = placement.agent;
|
|
@@ -620,42 +791,68 @@ export class PushCoalescer<E = unknown> {
|
|
|
620
791
|
}
|
|
621
792
|
|
|
622
793
|
private async acceptPlain(subject: string, state: SubjectState<E>, occurrence: CoalescedOccurrence<E>, occupantUnread: boolean): Promise<CoalesceOutcome> {
|
|
794
|
+
// A batch this occurrence drops delivered nothing, so when it opened on
|
|
795
|
+
// the agent's first view, this occurrence takes that view.
|
|
796
|
+
const batchFirstView = (state.batch ?? state.rendering?.batch)?.firstView === true;
|
|
623
797
|
const hadBatch = this.dropBatches(subject, state);
|
|
624
798
|
if (occupantUnread && state.occupant) {
|
|
625
799
|
const prior = state.occupant;
|
|
626
|
-
|
|
627
|
-
|
|
800
|
+
// The replacement is the agent's first view only when the run it
|
|
801
|
+
// displaces held one and nothing may have shown any of it to the agent.
|
|
802
|
+
// Otherwise the subject's first view ends here (occupantLeaves).
|
|
803
|
+
const firstView = this.occupantLeaves(state, prior);
|
|
804
|
+
// A plain occurrence is a complete snapshot (rule 6): it displaces the run.
|
|
805
|
+
if (this.removeOccupant(subject, state, 'displaced')) {
|
|
628
806
|
this.host.cancelWake(subject);
|
|
629
807
|
// The replacement follows the prior delivery's audience (§3.2).
|
|
630
|
-
const placement = await this.
|
|
631
|
-
|
|
808
|
+
const placement = await this.deliver({
|
|
809
|
+
...occurrence, deliverTo: prior.placement.agent,
|
|
810
|
+
...(firstView ? { firstView: true } : {}),
|
|
811
|
+
});
|
|
812
|
+
state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}) } : undefined;
|
|
632
813
|
if (placement) state.audienceAgent = placement.agent;
|
|
633
814
|
return 'replaced';
|
|
634
815
|
}
|
|
635
|
-
//
|
|
636
|
-
this.host.audit({ kind: 'remove-failed', subject, eventId: prior.eventId });
|
|
637
|
-
state.history = 'unknown';
|
|
638
|
-
state.consumedEventId = prior.eventId;
|
|
816
|
+
// The walk stopped: what it left may be read. Append, as for consumed.
|
|
639
817
|
state.occupant = undefined;
|
|
640
818
|
}
|
|
641
819
|
if (hadBatch) this.host.cancelWake(subject);
|
|
642
|
-
const
|
|
643
|
-
|
|
820
|
+
const outcome: CoalesceOutcome = hadBatch ? 'replaced' : state.consumedEventId || state.history !== 'none' ? 'appended' : 'first';
|
|
821
|
+
// The subject's `initial` birth is the agent's first view of it unless
|
|
822
|
+
// that view has ended, and so is an occurrence that takes a first-view
|
|
823
|
+
// batch's place (a batch's own first view ends with the subject's).
|
|
824
|
+
const firstView = hadBatch ? batchFirstView && state.history === 'none' : outcome === 'first' && !state.firstViewEnded;
|
|
825
|
+
const placement = await this.deliver(hadBatch && firstView ? { ...occurrence, firstView: true } : occurrence);
|
|
826
|
+
state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}) } : undefined;
|
|
644
827
|
if (placement) state.audienceAgent = placement.agent;
|
|
645
|
-
|
|
646
|
-
return state.consumedEventId || state.history !== 'none' ? 'appended' : 'first';
|
|
828
|
+
return outcome;
|
|
647
829
|
}
|
|
648
830
|
|
|
649
831
|
private async acceptDeferred(subject: string, state: SubjectState<E>, occurrence: CoalescedOccurrence<E>, occupantUnread: boolean): Promise<CoalesceOutcome> {
|
|
650
832
|
let outcome: CoalesceOutcome = 'first';
|
|
651
|
-
|
|
833
|
+
// Whether a batch this notice opens is the agent's first view of the
|
|
834
|
+
// subject: nothing of it read, its first view not ended, no run of
|
|
835
|
+
// renders placed (the run holds the first view, and the batch's render
|
|
836
|
+
// joins it), and a first-view occupant this notice displaces that nothing
|
|
837
|
+
// has shown (occupantLeaves, which otherwise ends the first view for the
|
|
838
|
+
// batches already open too). A render still under way is weighed when a
|
|
839
|
+
// batch's first delivery is.
|
|
840
|
+
let firstView = state.history === 'none' && !state.firstViewEnded;
|
|
841
|
+
if (occupantUnread && state.occupant?.rendered) {
|
|
842
|
+
// An unread render is a delta the next render builds on (§5.2), not a
|
|
843
|
+
// plain occurrence: it stays, and the batch follows its agent (§3.2), so
|
|
844
|
+
// the next render reaches the context that holds it (§3.3, vector 49).
|
|
845
|
+
occurrence = { ...occurrence, deliverTo: state.occupant.placement.agent };
|
|
846
|
+
firstView = false;
|
|
847
|
+
} else if (occupantUnread && state.occupant) {
|
|
652
848
|
// §5.1: a notice displaces an unread plain occurrence and opens a batch
|
|
653
849
|
// — in the audience that held the plain occurrence (§3.2).
|
|
850
|
+
if (!this.occupantLeaves(state, state.occupant)) firstView = false;
|
|
654
851
|
this.host.audit({ kind: 'displaced', subject, eventId: state.occupant.eventId });
|
|
655
852
|
if (this.host.remove(state.occupant.placement)) {
|
|
656
853
|
outcome = 'replaced';
|
|
657
854
|
occurrence = { ...occurrence, deliverTo: state.occupant.placement.agent };
|
|
658
|
-
} else { this.host.audit({ kind: 'remove-failed', subject, eventId: state.occupant.eventId }); state.history = 'unknown'; }
|
|
855
|
+
} else { this.host.audit({ kind: 'remove-failed', subject, eventId: state.occupant.eventId }); state.history = 'unknown'; firstView = false; }
|
|
659
856
|
this.host.cancelWake(subject);
|
|
660
857
|
state.occupant = undefined;
|
|
661
858
|
} else if (state.batch?.latest.deliverTo) {
|
|
@@ -670,7 +867,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
670
867
|
outcome = 'replaced';
|
|
671
868
|
} else {
|
|
672
869
|
// Rule 1: a notice during a render opens a NEW batch ("first").
|
|
673
|
-
state.batch = { latest: occurrence, notices: [notice], dropped: 0 };
|
|
870
|
+
state.batch = { latest: occurrence, notices: [notice], dropped: 0, ...(firstView ? { firstView: true as const } : {}) };
|
|
674
871
|
}
|
|
675
872
|
await this.host.wakeForBatch(occurrence);
|
|
676
873
|
return outcome;
|
|
@@ -855,21 +1052,46 @@ export class PushCoalescer<E = unknown> {
|
|
|
855
1052
|
const empty = isVisiblyEmptyContent(content);
|
|
856
1053
|
this.host.audit({ kind: 'rendered', subject, eventId: occurrence.eventId, source, empty });
|
|
857
1054
|
if (empty) return; // §5.2: nothing happened (blank text included)
|
|
858
|
-
|
|
1055
|
+
// A render describes what changed since the server's previous render
|
|
1056
|
+
// (§5.2), so the renders before it stay where they are, and this one
|
|
1057
|
+
// joins their run after them (§3.3, vector 49). Any a request has
|
|
1058
|
+
// read by then, refreshOccupant drops at its next look. The batch's
|
|
1059
|
+
// first delivery is the agent's first view while nothing of the
|
|
1060
|
+
// subject has been read and no other version of it is placed, so only
|
|
1061
|
+
// a run's oldest render can be one.
|
|
1062
|
+
const run = state.occupant?.rendered ? state.occupant : undefined;
|
|
1063
|
+
const firstView = batch.firstView === true && state.history === 'none' && !state.occupant;
|
|
1064
|
+
const placement = await this.deliver({ ...occurrence, timestamp, content, ...(firstView ? { firstView: true } : {}) }, content, assemblingFor);
|
|
859
1065
|
if (placement?.deferredId) {
|
|
860
|
-
// Landed in another agent's
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
// has
|
|
867
|
-
|
|
1066
|
+
// Landed in a deferred queue: another agent's, whose turn is alive,
|
|
1067
|
+
// or the assembling agent's own, behind a write held there (the
|
|
1068
|
+
// host's bypassDeferralFor), or after its assembly ended, as any
|
|
1069
|
+
// write during its turn. Still unread
|
|
1070
|
+
// there, so it stays replaceable and withdrawable. The turn being
|
|
1071
|
+
// assembled doesn't read it, so it isn't materialized for that turn,
|
|
1072
|
+
// and no request has consumed it. The freeze withdrew the queue's
|
|
1073
|
+
// agent's wake for this batch; give it an ordinary wake, as any
|
|
1074
|
+
// delivery during its turn has, beside any wakes a newer batch has,
|
|
1075
|
+
// so that its next turn reads the content (the wake starts no turn
|
|
1076
|
+
// once the agent has read it, or it's removed).
|
|
1077
|
+
// The run's renders keep their own records, whatever else an
|
|
1078
|
+
// occupant carries, a first view's mark included.
|
|
1079
|
+
let before: Occupant['before'];
|
|
1080
|
+
if (run) { const { before: earlier, ...member } = run; before = [...(earlier ?? []), member]; }
|
|
1081
|
+
state.occupant = { eventId: occurrence.eventId, placement, rendered: true, ...(firstView ? { firstView: true as const } : {}), ...(before ? { before } : {}) };
|
|
1082
|
+
// A batch opened while this render was under way (§5.4 rule 1)
|
|
1083
|
+
// follows the run's agent from here, wakes included, as a later
|
|
1084
|
+
// notice's does (acceptDeferred), so the run stays in one context.
|
|
1085
|
+
if (state.batch && !state.batch.latest.deliverTo) {
|
|
1086
|
+
state.batch.latest = { ...state.batch.latest, deliverTo: placement.agent };
|
|
1087
|
+
await this.host.wakeForBatch(state.batch.latest);
|
|
1088
|
+
}
|
|
868
1089
|
state.audienceAgent = placement.agent;
|
|
869
|
-
await this.host.wakeToRead(occurrence, placement.agent);
|
|
1090
|
+
await this.host.wakeToRead(occurrence, placement.agent, placement.deferredId);
|
|
870
1091
|
return;
|
|
871
1092
|
}
|
|
872
|
-
// The materialized occurrence is consumed by the request being
|
|
1093
|
+
// The materialized occurrence is consumed by the request being
|
|
1094
|
+
// assembled, and becomes history with the run placed ahead of it.
|
|
873
1095
|
rendering.materialized = true;
|
|
874
1096
|
state.history = 'some';
|
|
875
1097
|
state.consumedEventId = occurrence.eventId;
|
|
@@ -899,13 +1121,17 @@ export class PushCoalescer<E = unknown> {
|
|
|
899
1121
|
consumedEventId: s.occupant?.eventId ?? s.consumedEventId,
|
|
900
1122
|
identity: s.identity,
|
|
901
1123
|
audienceAgent: s.audienceAgent ?? s.occupant?.placement.agent,
|
|
1124
|
+
...(s.firstViewEnded ? { firstViewEnded: true as const } : {}),
|
|
902
1125
|
touchedAt: now,
|
|
903
1126
|
};
|
|
904
1127
|
if (s.batch) {
|
|
905
1128
|
// An interrupted render is not replayed (§3.2): a batch that was
|
|
906
1129
|
// RENDERING keeps its fallback; a batch that was only pending renders
|
|
907
1130
|
// normally at the next assembly.
|
|
908
|
-
state.batch = {
|
|
1131
|
+
state.batch = {
|
|
1132
|
+
latest: s.batch.latest as CoalescedOccurrence<E>, notices: s.batch.notices, dropped: s.batch.dropped,
|
|
1133
|
+
...(s.batch.rendering ? { noRender: true } : s.batch.firstView ? { firstView: true as const } : {}),
|
|
1134
|
+
};
|
|
909
1135
|
}
|
|
910
1136
|
this.subjects.set(s.subject, state);
|
|
911
1137
|
}
|
|
@@ -935,9 +1161,24 @@ export class PushCoalescer<E = unknown> {
|
|
|
935
1161
|
restoreReceipts(records: CoalescingReceiptRecord[]): void {
|
|
936
1162
|
const now = this.now();
|
|
937
1163
|
for (const r of records) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1164
|
+
const fresh = now - r.at <= this.retryWindowMs;
|
|
1165
|
+
if (fresh) {
|
|
1166
|
+
this.receipts.set(r.key, { result: r.result, at: r.at, committed: true });
|
|
1167
|
+
if (r.born && !this.subjects.has(r.subject)) this.subjects.set(r.subject, { history: r.born, touchedAt: now });
|
|
1168
|
+
}
|
|
1169
|
+
// A first view's end is a fact about the subject, which holds while the
|
|
1170
|
+
// coalescer keeps it, not about the receipt, whose retry window says
|
|
1171
|
+
// nothing about it. So a record that carries it ends the subject's
|
|
1172
|
+
// first view whatever its age, for a batch the snapshot brought back
|
|
1173
|
+
// too. A subject this restore doesn't hold yet existed when the record
|
|
1174
|
+
// was written and may have been shown, so it comes back with that end
|
|
1175
|
+
// and an unknown history.
|
|
1176
|
+
if (r.firstViewEnded) {
|
|
1177
|
+
const ended = this.subjects.get(r.subject);
|
|
1178
|
+
if (ended) this.endFirstView(ended);
|
|
1179
|
+
else this.subjects.set(r.subject, { history: 'unknown', touchedAt: now, firstViewEnded: true });
|
|
1180
|
+
}
|
|
1181
|
+
if (!fresh) continue;
|
|
941
1182
|
// A birth fact is not proof the subject is still unread: the plain
|
|
942
1183
|
// occurrence this record acknowledged is in context (durable before the
|
|
943
1184
|
// record) and counts as read from recovery on, like a snapshot occupant.
|
|
@@ -967,12 +1208,15 @@ export class PushCoalescer<E = unknown> {
|
|
|
967
1208
|
snapshot(): CoalescingSnapshot {
|
|
968
1209
|
return {
|
|
969
1210
|
version: 1,
|
|
970
|
-
receipts: [...this.receipts],
|
|
1211
|
+
receipts: [...this.receipts].map(([key, { result, at }]): [string, { result: PushEventResult; at: number }] => [key, { result, at }]),
|
|
971
1212
|
subjects: [...this.subjects].map(([subject, s]) => ({
|
|
972
1213
|
subject, history: s.history, consumedEventId: s.consumedEventId, identity: s.identity,
|
|
973
1214
|
...(s.audienceAgent ? { audienceAgent: s.audienceAgent } : {}),
|
|
974
|
-
...(s.
|
|
975
|
-
|
|
1215
|
+
...(s.firstViewEnded ? { firstViewEnded: true as const } : {}),
|
|
1216
|
+
// restore() reads only these. After a restart nothing of the subject
|
|
1217
|
+
// is tracked as unread, a run included: its history counts as read.
|
|
1218
|
+
...(s.occupant ? { occupant: { eventId: s.occupant.eventId, placement: s.occupant.placement } } : {}),
|
|
1219
|
+
...(s.batch ? { batch: { latest: s.batch.latest as CoalescedOccurrence<unknown>, notices: s.batch.notices, dropped: s.batch.dropped, ...(s.batch.noRender ? { rendering: true } : s.batch.firstView ? { firstView: true as const } : {}) } }
|
|
976
1220
|
: s.rendering ? { batch: { latest: s.rendering.batch.latest as CoalescedOccurrence<unknown>, notices: s.rendering.batch.notices, dropped: s.rendering.batch.dropped, rendering: true } }
|
|
977
1221
|
: {}),
|
|
978
1222
|
})),
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -48,8 +48,13 @@ export interface McplPushEvent {
|
|
|
48
48
|
targetAgents?: string[];
|
|
49
49
|
/** RFC-006: set when the coalescer delivers; rides the wake it queues. */
|
|
50
50
|
coalescingSubject?: string;
|
|
51
|
-
/** RFC-006 assembly: materialized for this agent's turn
|
|
51
|
+
/** RFC-006 assembly: materialized for this agent's turn, stored directly
|
|
52
|
+
* while its assembly runs, after what is queued for it (the framework's
|
|
53
|
+
* addMessage, bypassDeferralFor). */
|
|
52
54
|
assemblingFor?: string;
|
|
55
|
+
/** The coalescer's mark: the agent's first view of the subject (see
|
|
56
|
+
* CoalescedOccurrence.firstView). */
|
|
57
|
+
firstView?: boolean;
|
|
53
58
|
/** Host acceptance time (epoch ms), stamped where the push is admitted. */
|
|
54
59
|
acceptedAt?: number;
|
|
55
60
|
/** Framework-owned source envelope, frozen at a coalesced occurrence's
|
|
@@ -224,12 +229,15 @@ export class PushHandler {
|
|
|
224
229
|
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
225
230
|
.map((b) => b.text)
|
|
226
231
|
.join('\n');
|
|
232
|
+
// The host's own fields come after the server's origin, as a channel
|
|
233
|
+
// message's do, so an origin can't give the gate another server's id,
|
|
234
|
+
// feature set, event id or event type.
|
|
227
235
|
const metadata: Record<string, unknown> = {
|
|
236
|
+
...(params.origin ?? {}),
|
|
228
237
|
serverId,
|
|
229
238
|
featureSet: params.featureSet,
|
|
230
239
|
eventId: params.eventId,
|
|
231
240
|
eventType: 'mcpl:push-event',
|
|
232
|
-
...(params.origin ?? {}),
|
|
233
241
|
...(params.tags ? { tags: params.tags } : {}),
|
|
234
242
|
};
|
|
235
243
|
triggerInference = this.shouldTriggerInference(textContent, metadata);
|
|
@@ -224,7 +224,7 @@ export class ReferenceFetcher {
|
|
|
224
224
|
constructor(
|
|
225
225
|
private readonly resolveServer: (serverId: string) => ServerResolution | undefined,
|
|
226
226
|
/** Persist the payload; returns a mount-prefixed path or null when no
|
|
227
|
-
* writable
|
|
227
|
+
* writable mount without an include exists. File name is host-generated (§7.3). */
|
|
228
228
|
private readonly saveFile: (fileName: string, data: Buffer, mimeType: string) => Promise<string | null>,
|
|
229
229
|
) {}
|
|
230
230
|
|
|
@@ -463,9 +463,9 @@ export class ReferenceFetcher {
|
|
|
463
463
|
const observedMime = sniffMime(data) ?? headerMime ?? claimedMime ?? 'application/octet-stream';
|
|
464
464
|
const ext = EXT_BY_MIME.get(observedMime) ?? EXT_BY_MIME.get(headerMime ?? '') ?? EXT_BY_MIME.get(claimedMime ?? '') ?? 'bin';
|
|
465
465
|
// §7.3: host-generated storage name; the server's `name` is display-only.
|
|
466
|
-
// A null from the save hook covers no workspace, no writable mount
|
|
467
|
-
// a failed write, and the hook logs which. A
|
|
468
|
-
// too, not the fetch's, so it's logged here.
|
|
466
|
+
// A null from the save hook covers no workspace, no writable mount
|
|
467
|
+
// without an include and a failed write, and the hook logs which. A
|
|
468
|
+
// throw is the save's failure too, not the fetch's, so it's logged here.
|
|
469
469
|
const fileName = `${record.refId}.${ext}`;
|
|
470
470
|
let path: string | null;
|
|
471
471
|
try {
|