@north-light/crouter 0.3.185 → 0.3.186

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.
@@ -1,143 +0,0 @@
1
- // message-ledger.ts — broker-owned message identity across pi's id-less queues.
2
- //
3
- // pi's steering/follow-up queues and user messages carry TEXT only: `steer()`
4
- // pushes bare strings, `queue_update` relays bare string arrays, and retirement
5
- // (on user `message_start`) is a first-index-of-text splice. Every downstream
6
- // consumer was therefore forced into order/text heuristics to match its own
7
- // optimistic send to the wake that echoed it — wrong under duplicates,
8
- // cross-surface sends, and reconnect replay.
9
- //
10
- // The broker is the ONLY writer to the engine (every prompt/steer/follow_up/
11
- // deliver funnels through driveEngine) and observes pi's event stream
12
- // synchronously in-process, so it can own identity at the wire boundary: mint
13
- // (or adopt the sender's) id per accepted frame, mirror pi's queue state by
14
- // reconciling pi's own `queue_update` events, and attach the retired entry's id
15
- // to the relayed user `message_start`. Same deterministic algorithm over the
16
- // same data in the same process as pi's own bookkeeping — it cannot drift.
17
- //
18
- // pi ordering facts this relies on (verified at pi 0.83.0, agent-session.js):
19
- // - `_queueSteer`/`_queueFollowUp` push the text and emit `queue_update`
20
- // synchronously inside the steer/followUp call.
21
- // - On a queued message's user `message_start`, `_handleAgentEvent` splices
22
- // the queue and emits `queue_update` BEFORE emitting the `message_start`,
23
- // so a retirement's queue_update always precedes its message and two
24
- // removals never interleave.
25
- // - An idle `prompt()` never touches a queue — its user message matches a
26
- // pending dispatch intent by text instead.
27
- import { randomUUID } from 'node:crypto';
28
- /** Dispatch intents not yet observed anywhere: bounded FIFO so an intent whose
29
- * text pi transformed (and so never matches) cannot accumulate forever. */
30
- const MAX_PENDING_INTENTS = 32;
31
- /** Recent id-bearing user-message dispatches carried in `welcome` so a
32
- * reattaching follower can key its snapshot's trailing user wakes. */
33
- const MAX_RECENT = 8;
34
- export class MessageIdLedger {
35
- steering = [];
36
- followUp = [];
37
- pendingIntents = [];
38
- stagedRetirementId = null;
39
- recent = [];
40
- /** Record an accepted client frame's identity before its engine call runs.
41
- * `id` is the sender's `message_id` when it minted one, else broker-minted. */
42
- noteDispatch(id, text) {
43
- this.pendingIntents.push({ id, text });
44
- if (this.pendingIntents.length > MAX_PENDING_INTENTS)
45
- this.pendingIntents.shift();
46
- }
47
- /** Remove a noted intent whose engine call rejected — the message will never
48
- * appear in a queue or as a user message. No-op if already adopted. */
49
- dropIntent(id) {
50
- const idx = this.pendingIntents.findIndex((e) => e.id === id);
51
- if (idx !== -1)
52
- this.pendingIntents.splice(idx, 1);
53
- }
54
- /**
55
- * Fold one pi `queue_update` into the ledger and return the id arrays
56
- * parallel to its text arrays (total: every position gets an id).
57
- *
58
- * Per-queue positional diff against the mirrored entries: a text matching an
59
- * old entry at-or-after the cursor keeps its id (everything skipped is
60
- * removed); an unmatched text ADOPTS the first pending intent with equal
61
- * text, else mints a fresh id. Exactly ONE removal across both queues is
62
- * pi's per-message retirement (its queue_update precedes the user
63
- * `message_start` it belongs to) and is staged for `takeUserMessageId`;
64
- * 0 or >1 removals (an enqueue, clearQueue/dequeue, abort) stage nothing.
65
- */
66
- reconcileQueueUpdate(steering, followUp) {
67
- const removed = [];
68
- const next = (old, texts) => {
69
- const out = [];
70
- let cursor = 0;
71
- for (const text of texts) {
72
- let match = -1;
73
- for (let j = cursor; j < old.length; j++) {
74
- if (old[j].text === text) {
75
- match = j;
76
- break;
77
- }
78
- }
79
- if (match !== -1) {
80
- for (let j = cursor; j < match; j++)
81
- removed.push(old[j].id);
82
- out.push(old[match]);
83
- cursor = match + 1;
84
- continue;
85
- }
86
- const intentIdx = this.pendingIntents.findIndex((e) => e.text === text);
87
- const id = intentIdx !== -1 ? this.pendingIntents.splice(intentIdx, 1)[0].id : randomUUID();
88
- out.push({ id, text });
89
- }
90
- for (let j = cursor; j < old.length; j++)
91
- removed.push(old[j].id);
92
- return out;
93
- };
94
- this.steering = next(this.steering, steering);
95
- this.followUp = next(this.followUp, followUp);
96
- this.stagedRetirementId = removed.length === 1 ? removed[0] : null;
97
- return {
98
- steeringIds: this.steering.map((e) => e.id),
99
- followUpIds: this.followUp.map((e) => e.id),
100
- };
101
- }
102
- /**
103
- * The id for a relayed user `message_start`, or undefined when this message
104
- * carries none (an engine-command expansion, a text pi transformed, or a
105
- * pre-ledger replay). A staged retirement wins — pi emitted its queue_update
106
- * synchronously right before this message; otherwise the first pending
107
- * intent with equal text (the idle-prompt case, which never touches a
108
- * queue). Id-bearing results are remembered for `recentUserMessages`.
109
- */
110
- takeUserMessageId(messageText) {
111
- const staged = this.stagedRetirementId;
112
- if (staged !== null) {
113
- this.stagedRetirementId = null;
114
- this.remember(staged, messageText);
115
- return staged;
116
- }
117
- const idx = this.pendingIntents.findIndex((e) => e.text === messageText);
118
- if (idx === -1)
119
- return undefined;
120
- const intent = this.pendingIntents.splice(idx, 1)[0];
121
- this.remember(intent.id, messageText);
122
- return intent.id;
123
- }
124
- /** The last {@link MAX_RECENT} id-bearing user-message dispatches, oldest
125
- * first — the `welcome.recentUserMessages` payload. */
126
- recentUserMessages() {
127
- return this.recent.map((e) => ({ ...e }));
128
- }
129
- /** Session rebind/replacement: the mirrored queues, staged retirement, and
130
- * recent list all described the OLD session — drop everything. */
131
- reset() {
132
- this.steering = [];
133
- this.followUp = [];
134
- this.pendingIntents = [];
135
- this.stagedRetirementId = null;
136
- this.recent = [];
137
- }
138
- remember(id, text) {
139
- this.recent.push({ id, text });
140
- if (this.recent.length > MAX_RECENT)
141
- this.recent.shift();
142
- }
143
- }