hivecode-mcp 0.12.1 → 0.12.4

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/hive-receipts.js CHANGED
@@ -1,233 +1,233 @@
1
- // hive-receipts.js — did the message actually reach anybody, or did it only look like it did?
2
- //
3
- // THE DEFECT THIS EXISTS FOR, from this project's own history:
4
- //
5
- // "29 messages @-mentioned me today. hive_wait has surfaced a handful."
6
- // "Those messages land in HIVE_CHAT.md and look delivered -- they just may never reach the
7
- // recipient's context. The convention looks like it works, which is the worst property a
8
- // coordination protocol can have."
9
- //
10
- // `hive_say` returns "sent to @Builder: ..." the instant the message enters the LOCAL Yjs array.
11
- // It says that while the socket is down. It says that when @Builder crashed ten minutes ago. It
12
- // says that when @Builder is looping without ever calling hive_wait. Three different failures,
13
- // one cheerful word, and the sender has no way to tell them apart.
14
- //
15
- // WHAT THE EVIDENCE IS, HERE. Agent Relay verifies a delivery by watching the terminal echo the
16
- // keystrokes back (crates/broker/src/broker/delivery_verification.rs). There is no terminal here,
17
- // so copying the mechanism would be cargo cult. The equivalent evidence in Hivecode is the one
18
- // thing that only moves when a message is actually handed into an agent's context:
19
- //
20
- // THE READ CURSOR IS THE ECHO.
21
- //
22
- // A mailbox cursor advances only inside `box.take()`, which is what hands messages to the agent.
23
- // So "their cursor passed the slot my message sits in" means the message was in the payload that
24
- // agent received. Today that cursor is written to a LOCAL session file, where nobody else can see
25
- // it. Publishing it into the room is the whole change.
26
- //
27
- // FOUR STATES, AND THE FOURTH IS THE POINT. Agent Relay has three (Success / Unverified /
28
- // Failed) and the load-bearing one is Unverified -- an unconfirmed delivery must never be counted
29
- // as a confirmed one. Their comment: "Unverified neither speeds up nor backs off the throttle,
30
- // but breaks the consecutive-success streak so unverified deliveries never drive the delay down."
31
- //
32
- // The same rule here, phrased for this system:
33
- //
34
- // READ their cursor passed it. Evidence it landed.
35
- // UNREAD they publish cursors, and theirs is behind it. Evidence it did NOT.
36
- // UNKNOWN they publish no cursor at all. NO evidence either way -- and it must never be
37
- // counted as READ. A Telegram bridge, a human's browser and an older build all
38
- // land here, so silence from this state means nothing at all.
39
- // UNREACHABLE they are not in the room, or WE are offline. It is not going to arrive.
40
- //
41
- // UNKNOWN is the honest state that a two-state design (delivered / not) cannot express, and
42
- // collapsing it into either neighbour is how a receipt system starts lying. `countDelivered`
43
- // exists precisely so that no caller can accidentally sum UNKNOWN into the good number.
44
- //
45
- // NO STORED INDEX. A Yjs array index is not stable: two peers pushing concurrently converge in a
46
- // deterministic order that need not be the order either one observed, so a message's index can
47
- // move after it was recorded. Receipts are therefore DERIVED at query time by locating the
48
- // message in the converged array -- the same choice `unansweredFor` made in hive-mailbox.js, for
49
- // the same reason: nothing to migrate and nothing that can drift.
50
-
51
- // @-parsing is imported, never re-implemented. A second hand-written copy of "who was named in
52
- // this text" is exactly the shape Agent Relay wrote up in learnings/duplicated-wire-contract.md
53
- // and exactly the shape that already bit this repo once as a duplicated reconcile(): two
54
- // descriptions of one thing, only one of which is ever checked against reality.
55
- import { mentionedNames, mentionsAll } from './hive-mailbox.js'
56
-
57
- export const READ = 'READ'
58
- export const UNREAD = 'UNREAD'
59
- export const UNKNOWN = 'UNKNOWN'
60
- export const UNREACHABLE = 'UNREACHABLE'
61
-
62
- /** Only these count as delivered. Anything else is an absence of evidence, not evidence. */
63
- export const DELIVERED_STATES = [READ]
64
-
65
- const lc = (x) => String(x == null ? '' : x).toLowerCase()
66
-
67
- /**
68
- * Publish my read position into the room.
69
- *
70
- * `at` is a count of messages consumed, so it only ever moves forward. It is clamped against
71
- * going BACKWARDS because a rejoin can hand a session a stale persisted cursor, and a receipt
72
- * that un-reads a message somebody already answered is worse than no receipt at all.
73
- */
74
- export function publishCursor(cursors, name, at, now = Date.now) {
75
- if (!cursors || !name) return null
76
- const prev = cursors.get(String(name))
77
- const next = Math.max(0, Number(at) || 0)
78
- const highest = Math.max(next, (prev && Number(prev.at)) || 0)
79
- const rec = { at: highest, ts: now(), ver: 1 }
80
- // Writing an identical value would still be a CRDT update, waking every observer in the room
81
- // on every tool call for no information. Only a real advance is published.
82
- if (prev && prev.at === highest) return prev
83
- cursors.set(String(name), rec)
84
- return rec
85
- }
86
-
87
- /** Case-insensitive, because @Builder and @builder are one agent everywhere else in this system. */
88
- export function cursorOf(cursors, name) {
89
- if (!cursors) return null
90
- const direct = cursors.get(String(name))
91
- if (direct) return direct
92
- for (const [k, v] of cursors.entries()) if (lc(k) === lc(name)) return v
93
- return null
94
- }
95
-
96
- /**
97
- * Where a message sits in the converged chat array, or -1.
98
- *
99
- * Identity is (by, at, text) rather than an index: the triple survives reordering, and the index
100
- * does not. Searched from the end because a receipt is almost always asked about something said
101
- * recently, and the room's history can be thousands of lines.
102
- */
103
- export function findIndex(chat, msg) {
104
- if (!Array.isArray(chat) || !msg) return -1
105
- for (let i = chat.length - 1; i >= 0; i--) {
106
- const m = chat[i]
107
- if (m && m.by === msg.by && m.at === msg.at && m.text === msg.text) return i
108
- }
109
- return -1
110
- }
111
-
112
- /**
113
- * The state of ONE message for ONE recipient.
114
- *
115
- * Order matters. `connected === false` outranks everything, because if our own socket is down the
116
- * message has not left this process and nothing we believe about the other side is worth saying.
117
- * Absence outranks a stale cursor for the same reason it does in hive-busy.js: it is the more
118
- * actionable of the two.
119
- */
120
- export function stateFor({ cursors, name, index, present = true, connected = true }) {
121
- if (!connected) return { name, state: UNREACHABLE, why: 'this client is offline — the message is in your local copy only and has not left this machine' }
122
- if (!present) return { name, state: UNREACHABLE, why: `@${name} is not in the room, so nobody is going to read it` }
123
- if (index < 0) return { name, state: UNKNOWN, why: 'that message is not in the room log — it may not have synced' }
124
- const cur = cursorOf(cursors, name)
125
- if (!cur) return { name, state: UNKNOWN, why: `@${name} publishes no read position (an older build, a bridge, or a human client) — silence from them means nothing either way` }
126
- // A cursor of N means N messages consumed, i.e. indexes 0..N-1. So the message at `index` has
127
- // been handed over exactly when the cursor is strictly greater than it.
128
- if (Number(cur.at) > index) return { name, state: READ, at: cur.ts, why: `@${name} has read it` }
129
- return { name, state: UNREAD, at: cur.ts, behind: index + 1 - Number(cur.at), why: `@${name} is ${index + 1 - Number(cur.at)} message(s) behind and has NOT been handed this one` }
130
- }
131
-
132
- /**
133
- * The state of ONE message for ONE recipient, ASKED AS A DIFFERENT QUESTION.
134
- *
135
- * `stateFor` above answers "should I wait for a reply from them right now?", so absence outranks
136
- * a cursor: an agent that read your message and then left still cannot answer you, and blocking on
137
- * them is the failure that function exists to prevent. That is correct, it is deliberate, and
138
- * tests/receipts-test.mjs D1 pins it in capital letters.
139
- *
140
- * This one answers a different question: "did this message ever land?" — and there the cursor
141
- * outranks absence, because A READ IS A FACT ABOUT THE PAST AND LOGGING OFF CANNOT UNDO IT.
142
- *
143
- * WHY TWO FUNCTIONS AND NOT A FLAG. A boolean that silently changes what the returned STRING means
144
- * is how this class of bug is born: a caller that forgets it gets a confident answer to a question
145
- * it did not ask. Two names cannot be confused by omission.
146
- *
147
- * WHY THIS ORDER, AND IT IS THE WHOLE DESIGN:
148
- * - `!cur` is checked BEFORE `!present`. A human in a browser, a Telegram bridge and every older
149
- * build publish no cursor at all. Reaching `!present` first would render them UNREACHABLE with
150
- * the claim "they left without reading it" — a claim we cannot support, because we have no
151
- * evidence either way. That is precisely what UNKNOWN exists to express, and folding it into
152
- * UNREACHABLE would make every human in the room read as a delivery failure forever.
153
- * - `!present` may therefore only be reached when a cursor EXISTS and is behind the message.
154
- * Only then is "they left without reading it" a measurement rather than a guess.
155
- * - `connected` is absent on purpose. The cursors map is LOCAL. Our own socket being down right
156
- * now says nothing about whether a three-day-old message was read; that was recorded and is
157
- * already here. Gate this on connectivity and the whole transcript blanks to UNKNOWN during
158
- * every reconnect — exactly when a human is reading it to work out what happened.
159
- *
160
- * Built by DeepMind-Agent (Gemini, in its own container) in room `night`, 20 Sep 2026, against a
161
- * design argued in the room over four rounds. Tests are antigravity1's, written before the code
162
- * existed and verified red first: tests/transcript-receipts-test.mjs.
163
- */
164
- export function transcriptStateFor({ cursors, name, index, present = true }) {
165
- if (index < 0) return { name, state: UNKNOWN, why: 'that message is not in the room log — it may not have synced' }
166
- const cur = cursorOf(cursors, name)
167
- if (!cur) return { name, state: UNKNOWN, why: `@${name} publishes no read position (an older build, a bridge, or a human client) — silence from them means nothing either way` }
168
- // A cursor of N means N messages consumed, i.e. indexes 0..N-1. So the message at `index` has
169
- // been handed over exactly when the cursor is STRICTLY greater than it. A cursor sitting exactly
170
- // at `index` means NOT read; an off-by-one here is a receipt that lies quietly.
171
- if (Number(cur.at) > index) return { name, state: READ, at: cur.ts, why: `@${name} has read it` }
172
- // Only reachable when a cursor exists and is behind the message.
173
- if (!present) return { name, state: UNREACHABLE, at: cur.ts, behind: index + 1 - Number(cur.at), why: `@${name} is not in the room, and left with cursor behind this message` }
174
- return { name, state: UNREAD, at: cur.ts, behind: index + 1 - Number(cur.at), why: `@${name} is ${index + 1 - Number(cur.at)} message(s) behind and has NOT been handed this one` }
175
- }
176
-
177
- /** Only READ counts. Written as a function so no caller can quietly widen it to include UNKNOWN. */
178
- export const countDelivered = (states) => states.filter((s) => DELIVERED_STATES.includes(s.state)).length
179
-
180
- /**
181
- * Every message I addressed to somebody, with per-recipient state.
182
- *
183
- * Unaddressed messages are skipped on purpose: they land in the ambient digest, where agents are
184
- * instructed to skip them, so a receipt for one would be measuring something nobody promised.
185
- * `@all` is skipped for the same reason a broadcast creates no reply debt in hive-mailbox.js --
186
- * one message obliging eight receipts is the storm, not the signal.
187
- */
188
- export function receipts({ chat, cursors, me, present = () => true, connected = true, limit = 10 }) {
189
- const list = Array.isArray(chat) ? chat : []
190
- const out = []
191
- for (let i = list.length - 1; i >= 0 && out.length < limit; i--) {
192
- const m = list[i]
193
- if (!m || lc(m.by) !== lc(me)) continue
194
- if (mentionsAll(m.text)) continue
195
- const named = mentionedNames(m.text).filter((n) => lc(n) !== 'all' && lc(n) !== lc(me))
196
- if (!named.length) continue
197
- out.push({
198
- at: m.at,
199
- text: String(m.text || '').slice(0, 120),
200
- to: named.map((n) => stateFor({ cursors, name: n, index: i, present: present(n), connected })),
201
- })
202
- }
203
- return out.reverse()
204
- }
205
-
206
- const MARK = { [READ]: '✓', [UNREAD]: '…', [UNKNOWN]: '?', [UNREACHABLE]: '✗' }
207
-
208
- /** One line per message. The marks make a column a human can scan; the words carry the meaning. */
209
- export function describe(r) {
210
- return `${r.at} ${r.to.map((s) => `${MARK[s.state] || '?'} ${s.name}=${s.state}`).join(' ')} "${r.text}"`
211
- }
212
-
213
- /**
214
- * What hive_say should say instead of "sent".
215
- *
216
- * ONE FAILURE ONLY, AND THAT IS DELIBERATE. At send time delivery is not knowable -- the message
217
- * was created a millisecond ago -- so the single thing worth saying is the one that is already
218
- * decided and is currently reported as success: our own socket is down.
219
- *
220
- * It says nothing about an ABSENT recipient even though that is equally decidable, because
221
- * `describeRecipient` in hive-busy.js already says it ("@X is NOT in the room — nobody will read
222
- * that") and a second hand-written copy of that sentence is precisely the defect class Agent
223
- * Relay wrote up in learnings/duplicated-wire-contract.md: two descriptions of one fact, only one
224
- * of which anybody keeps current. One producer, one description.
225
- *
226
- * On success it returns the empty string. A reassuring word at send time IS the defect.
227
- */
228
- export function sendWarning({ connected }) {
229
- if (connected) return ''
230
- return 'NOT SENT — this client is OFFLINE. The message is in your local copy only; it has not ' +
231
- 'reached the relay and no one else can see it. It will go out by itself when the connection ' +
232
- 'heals (do NOT call hive_join, that tears down the session). Until then, treat it as unsent.'
233
- }
1
+ // hive-receipts.js — did the message actually reach anybody, or did it only look like it did?
2
+ //
3
+ // THE DEFECT THIS EXISTS FOR, from this project's own history:
4
+ //
5
+ // "29 messages @-mentioned me today. hive_wait has surfaced a handful."
6
+ // "Those messages land in HIVE_CHAT.md and look delivered -- they just may never reach the
7
+ // recipient's context. The convention looks like it works, which is the worst property a
8
+ // coordination protocol can have."
9
+ //
10
+ // `hive_say` returns "sent to @Builder: ..." the instant the message enters the LOCAL Yjs array.
11
+ // It says that while the socket is down. It says that when @Builder crashed ten minutes ago. It
12
+ // says that when @Builder is looping without ever calling hive_wait. Three different failures,
13
+ // one cheerful word, and the sender has no way to tell them apart.
14
+ //
15
+ // WHAT THE EVIDENCE IS, HERE. Agent Relay verifies a delivery by watching the terminal echo the
16
+ // keystrokes back (crates/broker/src/broker/delivery_verification.rs). There is no terminal here,
17
+ // so copying the mechanism would be cargo cult. The equivalent evidence in Hivecode is the one
18
+ // thing that only moves when a message is actually handed into an agent's context:
19
+ //
20
+ // THE READ CURSOR IS THE ECHO.
21
+ //
22
+ // A mailbox cursor advances only inside `box.take()`, which is what hands messages to the agent.
23
+ // So "their cursor passed the slot my message sits in" means the message was in the payload that
24
+ // agent received. Today that cursor is written to a LOCAL session file, where nobody else can see
25
+ // it. Publishing it into the room is the whole change.
26
+ //
27
+ // FOUR STATES, AND THE FOURTH IS THE POINT. Agent Relay has three (Success / Unverified /
28
+ // Failed) and the load-bearing one is Unverified -- an unconfirmed delivery must never be counted
29
+ // as a confirmed one. Their comment: "Unverified neither speeds up nor backs off the throttle,
30
+ // but breaks the consecutive-success streak so unverified deliveries never drive the delay down."
31
+ //
32
+ // The same rule here, phrased for this system:
33
+ //
34
+ // READ their cursor passed it. Evidence it landed.
35
+ // UNREAD they publish cursors, and theirs is behind it. Evidence it did NOT.
36
+ // UNKNOWN they publish no cursor at all. NO evidence either way -- and it must never be
37
+ // counted as READ. A Telegram bridge, a human's browser and an older build all
38
+ // land here, so silence from this state means nothing at all.
39
+ // UNREACHABLE they are not in the room, or WE are offline. It is not going to arrive.
40
+ //
41
+ // UNKNOWN is the honest state that a two-state design (delivered / not) cannot express, and
42
+ // collapsing it into either neighbour is how a receipt system starts lying. `countDelivered`
43
+ // exists precisely so that no caller can accidentally sum UNKNOWN into the good number.
44
+ //
45
+ // NO STORED INDEX. A Yjs array index is not stable: two peers pushing concurrently converge in a
46
+ // deterministic order that need not be the order either one observed, so a message's index can
47
+ // move after it was recorded. Receipts are therefore DERIVED at query time by locating the
48
+ // message in the converged array -- the same choice `unansweredFor` made in hive-mailbox.js, for
49
+ // the same reason: nothing to migrate and nothing that can drift.
50
+
51
+ // @-parsing is imported, never re-implemented. A second hand-written copy of "who was named in
52
+ // this text" is exactly the shape Agent Relay wrote up in learnings/duplicated-wire-contract.md
53
+ // and exactly the shape that already bit this repo once as a duplicated reconcile(): two
54
+ // descriptions of one thing, only one of which is ever checked against reality.
55
+ import { mentionedNames, mentionsAll } from './hive-mailbox.js'
56
+
57
+ export const READ = 'READ'
58
+ export const UNREAD = 'UNREAD'
59
+ export const UNKNOWN = 'UNKNOWN'
60
+ export const UNREACHABLE = 'UNREACHABLE'
61
+
62
+ /** Only these count as delivered. Anything else is an absence of evidence, not evidence. */
63
+ export const DELIVERED_STATES = [READ]
64
+
65
+ const lc = (x) => String(x == null ? '' : x).toLowerCase()
66
+
67
+ /**
68
+ * Publish my read position into the room.
69
+ *
70
+ * `at` is a count of messages consumed, so it only ever moves forward. It is clamped against
71
+ * going BACKWARDS because a rejoin can hand a session a stale persisted cursor, and a receipt
72
+ * that un-reads a message somebody already answered is worse than no receipt at all.
73
+ */
74
+ export function publishCursor(cursors, name, at, now = Date.now) {
75
+ if (!cursors || !name) return null
76
+ const prev = cursors.get(String(name))
77
+ const next = Math.max(0, Number(at) || 0)
78
+ const highest = Math.max(next, (prev && Number(prev.at)) || 0)
79
+ const rec = { at: highest, ts: now(), ver: 1 }
80
+ // Writing an identical value would still be a CRDT update, waking every observer in the room
81
+ // on every tool call for no information. Only a real advance is published.
82
+ if (prev && prev.at === highest) return prev
83
+ cursors.set(String(name), rec)
84
+ return rec
85
+ }
86
+
87
+ /** Case-insensitive, because @Builder and @builder are one agent everywhere else in this system. */
88
+ export function cursorOf(cursors, name) {
89
+ if (!cursors) return null
90
+ const direct = cursors.get(String(name))
91
+ if (direct) return direct
92
+ for (const [k, v] of cursors.entries()) if (lc(k) === lc(name)) return v
93
+ return null
94
+ }
95
+
96
+ /**
97
+ * Where a message sits in the converged chat array, or -1.
98
+ *
99
+ * Identity is (by, at, text) rather than an index: the triple survives reordering, and the index
100
+ * does not. Searched from the end because a receipt is almost always asked about something said
101
+ * recently, and the room's history can be thousands of lines.
102
+ */
103
+ export function findIndex(chat, msg) {
104
+ if (!Array.isArray(chat) || !msg) return -1
105
+ for (let i = chat.length - 1; i >= 0; i--) {
106
+ const m = chat[i]
107
+ if (m && m.by === msg.by && m.at === msg.at && m.text === msg.text) return i
108
+ }
109
+ return -1
110
+ }
111
+
112
+ /**
113
+ * The state of ONE message for ONE recipient.
114
+ *
115
+ * Order matters. `connected === false` outranks everything, because if our own socket is down the
116
+ * message has not left this process and nothing we believe about the other side is worth saying.
117
+ * Absence outranks a stale cursor for the same reason it does in hive-busy.js: it is the more
118
+ * actionable of the two.
119
+ */
120
+ export function stateFor({ cursors, name, index, present = true, connected = true }) {
121
+ if (!connected) return { name, state: UNREACHABLE, why: 'this client is offline — the message is in your local copy only and has not left this machine' }
122
+ if (!present) return { name, state: UNREACHABLE, why: `@${name} is not in the room, so nobody is going to read it` }
123
+ if (index < 0) return { name, state: UNKNOWN, why: 'that message is not in the room log — it may not have synced' }
124
+ const cur = cursorOf(cursors, name)
125
+ if (!cur) return { name, state: UNKNOWN, why: `@${name} publishes no read position (an older build, a bridge, or a human client) — silence from them means nothing either way` }
126
+ // A cursor of N means N messages consumed, i.e. indexes 0..N-1. So the message at `index` has
127
+ // been handed over exactly when the cursor is strictly greater than it.
128
+ if (Number(cur.at) > index) return { name, state: READ, at: cur.ts, why: `@${name} has read it` }
129
+ return { name, state: UNREAD, at: cur.ts, behind: index + 1 - Number(cur.at), why: `@${name} is ${index + 1 - Number(cur.at)} message(s) behind and has NOT been handed this one` }
130
+ }
131
+
132
+ /**
133
+ * The state of ONE message for ONE recipient, ASKED AS A DIFFERENT QUESTION.
134
+ *
135
+ * `stateFor` above answers "should I wait for a reply from them right now?", so absence outranks
136
+ * a cursor: an agent that read your message and then left still cannot answer you, and blocking on
137
+ * them is the failure that function exists to prevent. That is correct, it is deliberate, and
138
+ * tests/receipts-test.mjs D1 pins it in capital letters.
139
+ *
140
+ * This one answers a different question: "did this message ever land?" — and there the cursor
141
+ * outranks absence, because A READ IS A FACT ABOUT THE PAST AND LOGGING OFF CANNOT UNDO IT.
142
+ *
143
+ * WHY TWO FUNCTIONS AND NOT A FLAG. A boolean that silently changes what the returned STRING means
144
+ * is how this class of bug is born: a caller that forgets it gets a confident answer to a question
145
+ * it did not ask. Two names cannot be confused by omission.
146
+ *
147
+ * WHY THIS ORDER, AND IT IS THE WHOLE DESIGN:
148
+ * - `!cur` is checked BEFORE `!present`. A human in a browser, a Telegram bridge and every older
149
+ * build publish no cursor at all. Reaching `!present` first would render them UNREACHABLE with
150
+ * the claim "they left without reading it" — a claim we cannot support, because we have no
151
+ * evidence either way. That is precisely what UNKNOWN exists to express, and folding it into
152
+ * UNREACHABLE would make every human in the room read as a delivery failure forever.
153
+ * - `!present` may therefore only be reached when a cursor EXISTS and is behind the message.
154
+ * Only then is "they left without reading it" a measurement rather than a guess.
155
+ * - `connected` is absent on purpose. The cursors map is LOCAL. Our own socket being down right
156
+ * now says nothing about whether a three-day-old message was read; that was recorded and is
157
+ * already here. Gate this on connectivity and the whole transcript blanks to UNKNOWN during
158
+ * every reconnect — exactly when a human is reading it to work out what happened.
159
+ *
160
+ * Built by DeepMind-Agent (Gemini, in its own container) in room `night`, 20 Sep 2026, against a
161
+ * design argued in the room over four rounds. Tests are antigravity1's, written before the code
162
+ * existed and verified red first: tests/transcript-receipts-test.mjs.
163
+ */
164
+ export function transcriptStateFor({ cursors, name, index, present = true }) {
165
+ if (index < 0) return { name, state: UNKNOWN, why: 'that message is not in the room log — it may not have synced' }
166
+ const cur = cursorOf(cursors, name)
167
+ if (!cur) return { name, state: UNKNOWN, why: `@${name} publishes no read position (an older build, a bridge, or a human client) — silence from them means nothing either way` }
168
+ // A cursor of N means N messages consumed, i.e. indexes 0..N-1. So the message at `index` has
169
+ // been handed over exactly when the cursor is STRICTLY greater than it. A cursor sitting exactly
170
+ // at `index` means NOT read; an off-by-one here is a receipt that lies quietly.
171
+ if (Number(cur.at) > index) return { name, state: READ, at: cur.ts, why: `@${name} has read it` }
172
+ // Only reachable when a cursor exists and is behind the message.
173
+ if (!present) return { name, state: UNREACHABLE, at: cur.ts, behind: index + 1 - Number(cur.at), why: `@${name} is not in the room, and left with cursor behind this message` }
174
+ return { name, state: UNREAD, at: cur.ts, behind: index + 1 - Number(cur.at), why: `@${name} is ${index + 1 - Number(cur.at)} message(s) behind and has NOT been handed this one` }
175
+ }
176
+
177
+ /** Only READ counts. Written as a function so no caller can quietly widen it to include UNKNOWN. */
178
+ export const countDelivered = (states) => states.filter((s) => DELIVERED_STATES.includes(s.state)).length
179
+
180
+ /**
181
+ * Every message I addressed to somebody, with per-recipient state.
182
+ *
183
+ * Unaddressed messages are skipped on purpose: they land in the ambient digest, where agents are
184
+ * instructed to skip them, so a receipt for one would be measuring something nobody promised.
185
+ * `@all` is skipped for the same reason a broadcast creates no reply debt in hive-mailbox.js --
186
+ * one message obliging eight receipts is the storm, not the signal.
187
+ */
188
+ export function receipts({ chat, cursors, me, present = () => true, connected = true, limit = 10 }) {
189
+ const list = Array.isArray(chat) ? chat : []
190
+ const out = []
191
+ for (let i = list.length - 1; i >= 0 && out.length < limit; i--) {
192
+ const m = list[i]
193
+ if (!m || lc(m.by) !== lc(me)) continue
194
+ if (mentionsAll(m.text)) continue
195
+ const named = mentionedNames(m.text).filter((n) => lc(n) !== 'all' && lc(n) !== lc(me))
196
+ if (!named.length) continue
197
+ out.push({
198
+ at: m.at,
199
+ text: String(m.text || '').slice(0, 120),
200
+ to: named.map((n) => stateFor({ cursors, name: n, index: i, present: present(n), connected })),
201
+ })
202
+ }
203
+ return out.reverse()
204
+ }
205
+
206
+ const MARK = { [READ]: '✓', [UNREAD]: '…', [UNKNOWN]: '?', [UNREACHABLE]: '✗' }
207
+
208
+ /** One line per message. The marks make a column a human can scan; the words carry the meaning. */
209
+ export function describe(r) {
210
+ return `${r.at} ${r.to.map((s) => `${MARK[s.state] || '?'} ${s.name}=${s.state}`).join(' ')} "${r.text}"`
211
+ }
212
+
213
+ /**
214
+ * What hive_say should say instead of "sent".
215
+ *
216
+ * ONE FAILURE ONLY, AND THAT IS DELIBERATE. At send time delivery is not knowable -- the message
217
+ * was created a millisecond ago -- so the single thing worth saying is the one that is already
218
+ * decided and is currently reported as success: our own socket is down.
219
+ *
220
+ * It says nothing about an ABSENT recipient even though that is equally decidable, because
221
+ * `describeRecipient` in hive-busy.js already says it ("@X is NOT in the room — nobody will read
222
+ * that") and a second hand-written copy of that sentence is precisely the defect class Agent
223
+ * Relay wrote up in learnings/duplicated-wire-contract.md: two descriptions of one fact, only one
224
+ * of which anybody keeps current. One producer, one description.
225
+ *
226
+ * On success it returns the empty string. A reassuring word at send time IS the defect.
227
+ */
228
+ export function sendWarning({ connected }) {
229
+ if (connected) return ''
230
+ return 'NOT SENT — this client is OFFLINE. The message is in your local copy only; it has not ' +
231
+ 'reached the relay and no one else can see it. It will go out by itself when the connection ' +
232
+ 'heals (do NOT call hive_join, that tears down the session). Until then, treat it as unsent.'
233
+ }
@@ -212,9 +212,14 @@ export function statusFromError(e) {
212
212
  /**
213
213
  * Authoritative explanations for HTTP status codes returned by the relay on connection upgrade.
214
214
  */
215
- export function explainStatus(code, relay = '') {
215
+ export function explainStatus(code, relay = '', opts = {}) {
216
216
  const c = Number(code)
217
- if (c === 401) return 'the relay rejected your token. It may have expired -- ask the room owner for a new invite.'
217
+ if (c === 401) {
218
+ if (opts && opts.hasToken === false) {
219
+ return 'this room needs a token of your own. Ask the room owner to mint you one.'
220
+ }
221
+ return 'the relay rejected your token. It may have expired -- ask the room owner for a new invite.'
222
+ }
218
223
  if (c === 402) return 'this room is over its plan limit. Contact support@getgrm.tech and we will sort it out.'
219
224
  if (c === 403) return 'the relay refused this room or path for your token. You are outside the folders you were given.'
220
225
  if (c === 429) return 'this room is FULL, so you were not let in (Q431). A relay admits a limited number of connections per room -- 8 on the free tier -- and this room is at that ceiling. Note it counts CONNECTIONS, not people: one agent holding several files holds several. If you run the relay, raise it with HIVE_SEATS; otherwise ask whoever does, or contact support@getgrm.tech.'
@@ -0,0 +1,89 @@
1
+ // LIVE CREDENTIALS ARE NOT SENT TO THE ROOM (3 Oct 2026).
2
+ //
3
+ // A file in a room goes to the relay and to every participant's disk. ALWAYS_IGNORE keeps the
4
+ // files that are secrets by NAME at home (.env, *.pem, id_rsa, ...), but a secret inside an
5
+ // ordinary file went out with it: an agent that pastes an API key into config.js to try
6
+ // something shares that key with everyone in the room, and with whoever runs the relay.
7
+ //
8
+ // So a file whose content holds a credential the room does not already have is held on this
9
+ // machine (see sync.js, withheld) until the credential is moved out. Only formats a provider
10
+ // stamps on its keys are matched -- a fixed prefix plus enough random characters -- and private
11
+ // key blocks. Nothing here guesses from entropy alone: a false match holds back real work, and
12
+ // the people who hit it would be right to switch this off.
13
+ //
14
+ // The matched value never leaves this module's callers in any form: not in a log line, not in
15
+ // the room, not in status. Only its kind and its line number do.
16
+
17
+ const PATTERNS = [
18
+ { kind: 'private key', re: /-----BEGIN (?:[A-Z0-9]+ ){0,3}PRIVATE KEY(?: BLOCK)?-----/g },
19
+ { kind: 'AWS access key', re: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
20
+ { kind: 'GitHub token', re: /\b(?:gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{60,100})\b/g },
21
+ { kind: 'GitLab token', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
22
+ { kind: 'Slack token', re: /\bxox[abposr]-[0-9]{6,}-[0-9A-Za-z-]{10,}/g },
23
+ { kind: 'Slack webhook', re: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,}\/B[A-Z0-9]{6,}\/[A-Za-z0-9]{20,}/g },
24
+ { kind: 'Stripe live key', re: /\b[rs]k_live_[A-Za-z0-9]{20,}\b/g },
25
+ { kind: 'OpenAI API key', re: /\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{16,}T3BlbkFJ[A-Za-z0-9_-]{16,}/g },
26
+ { kind: 'Anthropic API key', re: /\bsk-ant-(?:api|admin|oat)\d{2}-[A-Za-z0-9_-]{60,}/g },
27
+ { kind: 'npm token', re: /\bnpm_[A-Za-z0-9]{36}\b/g },
28
+ { kind: 'Hugging Face token', re: /\bhf_[A-Za-z0-9]{30,40}\b/g },
29
+ { kind: 'Groq API key', re: /\bgsk_[A-Za-z0-9]{40,60}\b/g },
30
+ { kind: 'SendGrid API key', re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g },
31
+ { kind: 'DigitalOcean token', re: /\bdo[opr]_v1_[a-f0-9]{64}\b/g },
32
+ // .npmrc and friends: a registry password or token written out literally. `${NPM_TOKEN}` is the
33
+ // documented way to keep one out of the file, so a reference to the environment is not a secret.
34
+ { kind: 'registry credential', re: /(?:^|\n)[ \t]*(?:\/\/[^\s=]+:)?_(?:authToken|auth|password)[ \t]*=[ \t]*(?!\$\{)[^\s'"]{8,}/g },
35
+ ]
36
+
37
+ // Documentation and tests are full of keys that look real and are not: AWS's own example id,
38
+ // and placeholders like sk_live_XXXXXXXXXXXXXXXXXXXXXXXX. A provider's key is random, so a match
39
+ // with few distinct characters, or that says it is an example, is a placeholder.
40
+ const PLACEHOLDER = /EXAMPLE|example|XXXX|xxxx|0{8}|1234567890|your[_-]?(?:key|token|secret)/i
41
+ function placeholder(value, kind) {
42
+ if (kind === 'private key' || kind === 'registry credential') return false
43
+ if (PLACEHOLDER.test(value)) return true
44
+ return new Set(value).size < 10
45
+ }
46
+
47
+ // A SIGNED ACCESS TOKEN -- a JWT, which is what Hivecode's own room tokens are -- is a person: whoever
48
+ // holds it joins as them, with their access. It is kept out of the room's chat and tasks
49
+ // ({ tokens: true }). It is NOT a reason to hold a FILE back: auth code and its tests are full of
50
+ // sample tokens, most of them expired or fake, and holding each such file would stop real work.
51
+ export const TOKEN_PATTERNS = [
52
+ { kind: 'access token', re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}/g },
53
+ ]
54
+
55
+ /** Every credential in `text`: [{ kind, line, value }]. `value` is for comparing, never for showing.
56
+ * `tokens: true` adds signed access tokens, for what goes into the room's chat and tasks. */
57
+ export function findSecrets(text, { tokens = false } = {}) {
58
+ const s = String(text || '')
59
+ if (!s) return []
60
+ const out = []
61
+ for (const { kind, re } of tokens ? [...PATTERNS, ...TOKEN_PATTERNS] : PATTERNS) {
62
+ re.lastIndex = 0
63
+ let m
64
+ while ((m = re.exec(s)) !== null) {
65
+ const value = m[0].replace(/^\s+/, '')
66
+ if (!placeholder(value, kind)) {
67
+ const at = m.index + (m[0].length - m[0].replace(/^\s+/, '').length)
68
+ out.push({ kind, line: s.slice(0, at).split('\n').length, value })
69
+ }
70
+ if (m[0].length === 0) re.lastIndex++
71
+ }
72
+ }
73
+ return out.sort((a, b) => a.line - b.line)
74
+ }
75
+
76
+ /** Credentials in `local` that `shared` does not already contain: what sharing `local` would leak. */
77
+ export function newSecrets(local, shared) {
78
+ const found = findSecrets(local)
79
+ if (!found.length) return []
80
+ const room = String(shared || '')
81
+ return found.filter((f) => !room.includes(f.value))
82
+ }
83
+
84
+ /** "an OpenAI API key (line 12)" -- the only form a finding is ever shown in. */
85
+ export function describeSecret(f) {
86
+ if (!f) return ''
87
+ const a = /^(?:[aeiou]|npm\b)/i.test(f.kind) ? 'an' : 'a'
88
+ return `${a} ${f.kind} (line ${f.line})`
89
+ }