hivecode-mcp 0.14.1 → 0.14.2
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_RULES.md +98 -98
- package/LICENSE +21 -21
- package/README.md +421 -421
- package/core.js +91 -91
- package/docs/ALWAYS_ON.md +310 -310
- package/docs/GITHUB_WORKFLOW.md +394 -394
- package/docs/HIVE_RULES.md +41 -41
- package/docs/MCP.md +157 -157
- package/docs/QUICKSTART.md +266 -266
- package/docs/SELF_HOSTING.md +256 -256
- package/docs/SLACK_SETUP.md +474 -474
- package/docs/TROUBLESHOOTING.md +105 -105
- package/hive-activity.js +336 -336
- package/hive-authors.js +46 -44
- package/hive-await.js +230 -230
- package/hive-branch.js +608 -607
- package/hive-busy.js +88 -88
- package/hive-coord.js +110 -110
- package/hive-gate.js +135 -135
- package/hive-github.js +3 -1
- package/hive-hook-install.js +179 -179
- package/hive-instructions.js +74 -74
- package/hive-land.js +695 -691
- package/hive-listen.js +81 -81
- package/hive-mailbox.js +520 -520
- package/hive-mcp-http.js +591 -591
- package/hive-mcp.js +33 -6
- package/hive-mux-proto.js +58 -58
- package/hive-mux.js +351 -351
- package/hive-notify.js +234 -233
- package/hive-obligations.js +247 -247
- package/hive-poll-proto.js +50 -50
- package/hive-pr-staleness.js +189 -189
- package/hive-pr.js +600 -512
- package/hive-privacy.js +327 -327
- package/hive-receipts.js +233 -233
- package/hive-relay-health.js +234 -234
- package/hive-repos.js +430 -430
- package/hive-room-key.js +210 -210
- package/hive-room-mode.js +1668 -1668
- package/hive-room-watcher.js +562 -561
- package/hive-secrets.js +89 -89
- package/hive-session.js +114 -114
- package/hive-setup.js +560 -560
- package/hive-slack-agent.js +408 -408
- package/hive-slack.js +2384 -2367
- package/hive-stdio.js +172 -172
- package/hive-stop-hook.js +293 -293
- package/hive-throttle.js +118 -118
- package/hive-time.js +71 -71
- package/hive-transport.js +578 -578
- package/hive-update.js +58 -58
- package/hive-version.js +110 -110
- package/hive-webhook.js +55 -54
- package/icr-merge.js +58 -58
- package/icr.js +851 -851
- package/lang-brace.js +305 -305
- package/lang-js.js +651 -651
- package/lang-json.js +211 -211
- package/lang-python.js +588 -588
- package/lang-ruby.js +230 -230
- package/lang-toml.js +136 -136
- package/lang-yaml.js +183 -183
- package/limits.js +413 -413
- package/package.json +1 -1
- package/reconcile.cjs +621 -621
- package/slack-app-manifest-agent.json +61 -61
- package/slack-app-manifest-inbound.json +36 -36
- package/slack-app-manifest.json +27 -27
- package/substrate.js +252 -252
- package/sync.js +169 -11
- package/token.js +201 -201
package/hive-activity.js
CHANGED
|
@@ -1,336 +1,336 @@
|
|
|
1
|
-
// WHAT THE ENGINE OBSERVED, as opposed to what the agent declared.
|
|
2
|
-
//
|
|
3
|
-
// THE DEFECT THIS EXISTS FOR.
|
|
4
|
-
// Every AI agent in a Hivecode room is a turn-based LLM. Between turns its process is alive
|
|
5
|
-
// and its socket is connected, so Yjs awareness reports it present -- but it will take no
|
|
6
|
-
// action, ever, until a human types at it. The room could not tell these apart:
|
|
7
|
-
//
|
|
8
|
-
// an agent thinking hard mid-task
|
|
9
|
-
// an agent blocked inside hive_wait healthy idle; wakes the instant anything lands
|
|
10
|
-
// an agent whose turn ENDED outside hive_wait will NEVER wake on its own
|
|
11
|
-
// an agent whose process died
|
|
12
|
-
//
|
|
13
|
-
// All four read as "in the room". The third is the one that costs a night: at 23:00 an agent
|
|
14
|
-
// says "standing by", at 09:00 the human finds a claim that expired hours ago, a ticket nobody
|
|
15
|
-
// finished, and no record of when it stopped. Both agents in this room have been that agent,
|
|
16
|
-
// and one of them had built an external WebSocket listener specifically to work around it.
|
|
17
|
-
//
|
|
18
|
-
// WHY THE OBVIOUS FIX IS DEAD, MEASURED RATHER THAN ARGUED.
|
|
19
|
-
// The obvious fix is a chat-activity timeout. Measured on this room's own log, 20 Sep 2026,
|
|
20
|
-
// across agents that were demonstrably working the whole time:
|
|
21
|
-
//
|
|
22
|
-
// agent msgs median gap p90 MAX
|
|
23
|
-
// DeepMind-Agent 32 156s 349s 3985s (66 min)
|
|
24
|
-
// Antigravity1 40 192s 907s 6718s (112 min)
|
|
25
|
-
//
|
|
26
|
-
// A working agent went 112 minutes between posts. Any threshold under that false-positives on
|
|
27
|
-
// real work; anything over it is useless. So chat silence cannot be the signal.
|
|
28
|
-
//
|
|
29
|
-
// THE ANSWERABLE QUESTION.
|
|
30
|
-
// Same move as HIV-28, which stopped asking whether a room was "ahead or behind" a branch and
|
|
31
|
-
// asked instead whether the room had ever SEEN those bytes. "Is this agent alive?" and "is it
|
|
32
|
-
// thinking?" are not answerable from outside. This is:
|
|
33
|
-
//
|
|
34
|
-
// IS THIS AGENT INSIDE A BLOCKING hive_wait RIGHT NOW?
|
|
35
|
-
//
|
|
36
|
-
// An agent inside hive_wait is reachable -- anything that arrives wakes it. An agent that is
|
|
37
|
-
// not inside hive_wait and not calling tools is unreachable until a human prompts it. Binary,
|
|
38
|
-
// exact, no threshold.
|
|
39
|
-
//
|
|
40
|
-
// WHY THIS IS A THIRD MAP AND NOT A FIELD ON `busy`.
|
|
41
|
-
// hive-busy.js settled the principle already: "The controls map is deliberately not reused.
|
|
42
|
-
// That is mission control -- a HUMAN sets running/paused there -- and an agent writing its own
|
|
43
|
-
// state into the same entry would clobber a human's pause with a self-report. Different author,
|
|
44
|
-
// different authority, different map."
|
|
45
|
-
//
|
|
46
|
-
// controls the HUMAN declares running / paused
|
|
47
|
-
// busy the AGENT declares "I am mid-task"
|
|
48
|
-
// activity the ENGINE observes <- this file
|
|
49
|
-
//
|
|
50
|
-
// `busy` is a claim about the future: an agent can forget to set it, set it and die, or simply
|
|
51
|
-
// be wrong. `activity` is a record of the past, stamped by dispatch() on every hive_* call, so
|
|
52
|
-
// an agent cannot forget to write it and cannot write something false. An agent that has
|
|
53
|
-
// stopped cannot report that it has stopped -- that is the whole problem -- so the fact has to
|
|
54
|
-
// be produced by something other than the agent's intent.
|
|
55
|
-
//
|
|
56
|
-
// Timestamps are unix ms, matching hive-coord's claims and hive-busy's `since`. Adjacent maps
|
|
57
|
-
// in one document disagreeing about units is its own defect class.
|
|
58
|
-
|
|
59
|
-
/** Inside a blocking hive_wait at this instant. Reachable: anything that arrives wakes it. */
|
|
60
|
-
export const WAITING = 'WAITING'
|
|
61
|
-
/** Acted recently and is not in a wait. Working. */
|
|
62
|
-
export const WORKING = 'WORKING'
|
|
63
|
-
/** Present, not waiting, silent past the window. WILL NOT WAKE without a human. */
|
|
64
|
-
export const PARKED = 'PARKED'
|
|
65
|
-
/** Awareness dropped. The process is gone, whatever the last record says. */
|
|
66
|
-
export const GONE = 'GONE'
|
|
67
|
-
/** A human set controls=paused. Obeying, not stalled -- never report this as PARKED. */
|
|
68
|
-
export const PAUSED = 'PAUSED'
|
|
69
|
-
/** Declared passive: this process does not take turns. Silence is correct, not a fault. */
|
|
70
|
-
export const PASSIVE = 'PASSIVE'
|
|
71
|
-
/**
|
|
72
|
-
* A human, present. Every state below is about whether an AGENT will act again without being
|
|
73
|
-
* prompted, which is not a question you can ask about a person.
|
|
74
|
-
*
|
|
75
|
-
* Antigravity1 called this in the P11 design review -- "kind === 'human' must never be
|
|
76
|
-
* classified as PARKED" -- and it was not implemented. The result was visible in the live room
|
|
77
|
-
* an hour later: `Ganesh (human) — PARKED: ... anything you send it now will sit unread until a
|
|
78
|
-
* human prompts it`, said about the human. Nonsense, and worse than nonsense, because it teaches
|
|
79
|
-
* a reader that PARKED is noise.
|
|
80
|
-
*/
|
|
81
|
-
export const PRESENT = 'PRESENT'
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* How long a present, non-waiting agent may be silent before it reads as PARKED.
|
|
85
|
-
*
|
|
86
|
-
* THIS IS THE ONLY NUMBER IN THIS MODULE THAT IS A JUDGEMENT RATHER THAN A MEASUREMENT, and
|
|
87
|
-
* saying so is the point. Every other state -- WAITING, GONE, PAUSED, PASSIVE -- is exact and
|
|
88
|
-
* needs no threshold at all, which is deliberate: the fact that matters most ("can I reach my
|
|
89
|
-
* teammate") never depends on a number somebody guessed.
|
|
90
|
-
*
|
|
91
|
-
* Fifteen minutes is long enough that an ordinary stretch of local work -- a test battery, a
|
|
92
|
-
* file sweep, a long think -- does not trip it, and short enough that a human returning in the
|
|
93
|
-
* morning gets a useful answer rather than a shrug. It is NOT derived from the 112-minute chat
|
|
94
|
-
* gap above: hive_* calls are far denser than chat posts, because the room requires a check
|
|
95
|
-
* between edits. If that assumption is ever measured and found wrong, this is the one line to
|
|
96
|
-
* change, and nothing else in the module moves.
|
|
97
|
-
*
|
|
98
|
-
* The cost of getting it wrong is asymmetric and worth stating. Too short and a working agent
|
|
99
|
-
* is reported PARKED, its ticket gets reassigned, and two agents do one job. Too long and the
|
|
100
|
-
* room is slower to notice a stall than it could have been -- which is still strictly better
|
|
101
|
-
* than today, where a stalled agent reads as WORKING forever.
|
|
102
|
-
*/
|
|
103
|
-
export const DEFAULT_PARKED_AFTER_MS = 15 * 60 * 1000
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* A ceiling, for the same reason hive-busy.js has one. An unbounded window means PARKED can be
|
|
107
|
-
* configured out of existence silently: every agent reads WORKING, nothing looks broken, and
|
|
108
|
-
* the feature is gone while its tests still pass.
|
|
109
|
-
*/
|
|
110
|
-
export const MAX_PARKED_AFTER_MS = 6 * 60 * 60 * 1000
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* A FLOOR, and it is not symmetry with the ceiling -- it is a correctness bound.
|
|
114
|
-
*
|
|
115
|
-
* The stamp is written when a tool call BEGINS, so the age of the record includes however long
|
|
116
|
-
* that call is still running. A window shorter than one tool call therefore reports an agent as
|
|
117
|
-
* PARKED while it is mid-call: the most active state there is, labelled unreachable.
|
|
118
|
-
*
|
|
119
|
-
* Found by the suite rather than reasoned about. tests/agent-liveness-test.mjs ran with a 2500ms
|
|
120
|
-
* window; adding a fifth client made hive_say take longer than that, and A1 went red with
|
|
121
|
-
* "PARKED: ... silent for 0 min (hive_say)" -- parked, zero minutes ago, naming the tool it was
|
|
122
|
-
* inside. Five seconds clears a real tool call with room to spare and is still short enough to
|
|
123
|
-
* exercise the state in a test.
|
|
124
|
-
*/
|
|
125
|
-
export const MIN_PARKED_AFTER_MS = 5000
|
|
126
|
-
|
|
127
|
-
/** Rows are keyed by member name, so the map grows with distinct names, not with time. */
|
|
128
|
-
const PRUNE_AFTER_MS = 30 * 24 * 60 * 60 * 1000
|
|
129
|
-
|
|
130
|
-
const lc = (n) => String(n || '').toLowerCase()
|
|
131
|
-
|
|
132
|
-
/** Read the configured window, clamped. An unparseable or absurd value falls back to the default. */
|
|
133
|
-
export function parkedAfterMs(env = (typeof process !== 'undefined' ? process.env : {})) {
|
|
134
|
-
const raw = Number((env && env.HIVE_PARKED_AFTER_MIN) || 0)
|
|
135
|
-
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PARKED_AFTER_MS
|
|
136
|
-
return Math.min(Math.max(raw * 60 * 1000, MIN_PARKED_AFTER_MS), MAX_PARKED_AFTER_MS)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Stamp that a member just did something. Called from dispatch() for EVERY hive_* tool, which
|
|
141
|
-
* is what makes this a record rather than a declaration -- there is no code path where an agent
|
|
142
|
-
* acts without this running, and none where it runs without the agent acting.
|
|
143
|
-
*
|
|
144
|
-
* `tool` is kept because "PARKED, last action hive_release 47 minutes ago" tells a human what
|
|
145
|
-
* to do next and "PARKED" alone does not.
|
|
146
|
-
*/
|
|
147
|
-
export function noteAction(store, { name, tool, now = Date.now() } = {}) {
|
|
148
|
-
if (!store || !name) return null
|
|
149
|
-
const key = lc(name)
|
|
150
|
-
const prev = store.get(key) || {}
|
|
151
|
-
const rec = { ...prev, name: String(name), lastAction: now, lastTool: String(tool || prev.lastTool || '') }
|
|
152
|
-
store.set(key, rec)
|
|
153
|
-
return rec
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Mark entry into a blocking wait. Paired with noteWaitEnd, and the pairing is the contract:
|
|
158
|
-
* `waitingSince` set means "inside the call right now", not "waited at some point".
|
|
159
|
-
*/
|
|
160
|
-
export function noteWaitStart(store, { name, now = Date.now() } = {}) {
|
|
161
|
-
if (!store || !name) return null
|
|
162
|
-
const key = lc(name)
|
|
163
|
-
const prev = store.get(key) || {}
|
|
164
|
-
const rec = { ...prev, name: String(name), waitingSince: now, lastAction: now, lastTool: 'hive_wait' }
|
|
165
|
-
store.set(key, rec)
|
|
166
|
-
return rec
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Mark exit from a blocking wait -- returned with work, timed out, or threw.
|
|
171
|
-
*
|
|
172
|
-
* This MUST run on every exit path including the throwing one. A wait that errors and leaves
|
|
173
|
-
* `waitingSince` set describes an agent parked in a call it is no longer in, and it describes
|
|
174
|
-
* it as the healthiest state in the taxonomy. stateOf() gates WAITING on awareness presence
|
|
175
|
-
* precisely so that a process which dies here cannot fake it -- but a live process that merely
|
|
176
|
-
* threw is still present, so the gate does not cover this case and the finally does.
|
|
177
|
-
*/
|
|
178
|
-
export function noteWaitEnd(store, { name, now = Date.now() } = {}) {
|
|
179
|
-
if (!store || !name) return null
|
|
180
|
-
const key = lc(name)
|
|
181
|
-
const prev = store.get(key) || {}
|
|
182
|
-
const rec = { ...prev, name: String(name), lastAction: now }
|
|
183
|
-
delete rec.waitingSince
|
|
184
|
-
store.set(key, rec)
|
|
185
|
-
return rec
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Forget a member entirely. Used on hive_leave, where the departure is deliberate and known. */
|
|
189
|
-
export function forget(store, name) {
|
|
190
|
-
if (!store || !name) return
|
|
191
|
-
try { store.delete(lc(name)) } catch { }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Derive one member's state.
|
|
196
|
-
*
|
|
197
|
-
* THE ORDER OF THESE BRANCHES IS THE SAFETY PROPERTY, not a style choice.
|
|
198
|
-
*
|
|
199
|
-
* `present` comes from awareness and is checked FIRST, before anything stored. A process that
|
|
200
|
-
* dies inside hive_wait leaves `waitingSince` set in the CRDT forever; asking about the stored
|
|
201
|
-
* record first would report that corpse as WAITING -- "healthy idle, wakes on anything" -- which
|
|
202
|
-
* is the single most misleading answer the room could give about the single worst situation.
|
|
203
|
-
* Presence is ephemeral and the record is durable, so the ephemeral one has to win.
|
|
204
|
-
*
|
|
205
|
-
* PAUSED and PASSIVE come before the silence check for the same reason P6 puts never-seen and
|
|
206
|
-
* passive before the debt: an agent obeying a human's stop, and a listener that never takes
|
|
207
|
-
* turns by design, are both silent on purpose. Reporting either as PARKED would train a reader
|
|
208
|
-
* to ignore the state that means "somebody needs to go look".
|
|
209
|
-
*
|
|
210
|
-
* @param {object} rec the stored activity record, or null/undefined if none
|
|
211
|
-
* @param {object} opts
|
|
212
|
-
* @param {boolean} opts.present awareness says this member is connected RIGHT NOW
|
|
213
|
-
* @param {boolean} opts.paused controls map says a human paused it
|
|
214
|
-
* @param {boolean} opts.passive declared passive at join
|
|
215
|
-
*/
|
|
216
|
-
// Apps that wake their agent when a background command ends, which is what makes a running
|
|
217
|
-
// hivecode-await listener mean "reachable". Antigravity was left out on a belief; measured 2 Oct
|
|
218
|
-
// 2026, it wakes the same way (Antigravity1's turn at 06:48:09Z began from its listener exiting,
|
|
219
|
-
// with nobody typing). Keep it in step with listenHow in hive-mcp.js, which tells the same apps to
|
|
220
|
-
// start the listener.
|
|
221
|
-
export function resumesOnExit(app) {
|
|
222
|
-
return Boolean(app && /claude-code|antigravity/i.test(app))
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function stateOf(rec, { present, paused = false, passive = false, human = false, listener = false, connections = null, now = Date.now(), parkedAfter = DEFAULT_PARKED_AFTER_MS } = {}) {
|
|
226
|
-
if (!present) return GONE
|
|
227
|
-
// A human is exempt from the whole taxonomy below, which asks whether an AGENT will act again
|
|
228
|
-
// unprompted. Checked before `paused` and `passive` because those describe agent mechanics too;
|
|
229
|
-
// checked after `!present` because a departed human is still departed.
|
|
230
|
-
if (human) return PRESENT
|
|
231
|
-
if (paused) return PAUSED
|
|
232
|
-
if (passive) return PASSIVE
|
|
233
|
-
const isListener = !!listener || (Array.isArray(connections) && connections.some((c) => c && (c.listener || (c.user && c.user.listener))))
|
|
234
|
-
if (isListener) return WAITING
|
|
235
|
-
if (rec && rec.waitingSince) return WAITING
|
|
236
|
-
const last = rec && Number(rec.lastAction)
|
|
237
|
-
// No record at all, but awareness says present: a client that has joined and not yet driven a
|
|
238
|
-
// tool. It is connected and it has done nothing. That is exactly PARKED -- and saying WORKING
|
|
239
|
-
// here would mean the state defaults to the reassuring answer in the absence of evidence,
|
|
240
|
-
// which is the habit this whole file exists to break.
|
|
241
|
-
if (!Number.isFinite(last)) return PARKED
|
|
242
|
-
return (now - last) < parkedAfter ? WORKING : PARKED
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* The reader-facing form: the state, plus the evidence it was derived from.
|
|
247
|
-
*
|
|
248
|
-
* A verdict with no evidence is a thing a human has to trust. "PARKED -- last action hive_release
|
|
249
|
-
* 47 min ago" is a thing they can check, and it is what tells them whether to prompt the agent,
|
|
250
|
-
* take its ticket, or go back to sleep.
|
|
251
|
-
*/
|
|
252
|
-
export function describe(rec, opts = {}) {
|
|
253
|
-
const now = opts.now || Date.now()
|
|
254
|
-
const isListener = !!opts.listener || (Array.isArray(opts.connections) && opts.connections.some((c) => c && (c.listener || (c.user && c.user.listener))))
|
|
255
|
-
const state = stateOf(rec, { ...opts, listener: isListener, now })
|
|
256
|
-
const last = rec && Number(rec.lastAction)
|
|
257
|
-
const ageMs = Number.isFinite(last) ? Math.max(0, now - last) : null
|
|
258
|
-
const conn = Array.isArray(opts.connections) ? opts.connections.find((c) => c && (c.app || (c.user && c.user.app))) : null
|
|
259
|
-
const app = opts.app || (conn && (conn.app || (conn.user && conn.user.app))) || ''
|
|
260
|
-
const canResume = opts.resumesOnExit !== undefined ? opts.resumesOnExit : resumesOnExit(app)
|
|
261
|
-
const isWaitingOnWait = !!(rec && rec.waitingSince)
|
|
262
|
-
const reachable = state === WORKING || (state === WAITING && (isWaitingOnWait || canResume))
|
|
263
|
-
return {
|
|
264
|
-
state,
|
|
265
|
-
listener: isListener,
|
|
266
|
-
resumesOnExit: canResume,
|
|
267
|
-
lastTool: (rec && rec.lastTool) || null,
|
|
268
|
-
lastAction: Number.isFinite(last) ? last : null,
|
|
269
|
-
ageMs,
|
|
270
|
-
waitingSince: (rec && Number(rec.waitingSince)) || null,
|
|
271
|
-
reachable,
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Minutes, rounded, for human output. Null age reads as "never", not as zero. */
|
|
276
|
-
const mins = (ms) => (ms == null ? null : Math.round(ms / 60000))
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* One line for hive_members. The wording of PARKED is doing real work: an agent reading
|
|
280
|
-
* "PARKED" alone will treat it as a synonym for idle and keep waiting on it, so the line has to
|
|
281
|
-
* say the consequence -- nothing you send will be acted on -- rather than the label.
|
|
282
|
-
*/
|
|
283
|
-
export function line(name, rec, opts = {}) {
|
|
284
|
-
const d = describe(rec, opts)
|
|
285
|
-
const age = mins(d.ageMs)
|
|
286
|
-
const tool = d.lastTool ? ` (${d.lastTool})` : ''
|
|
287
|
-
switch (d.state) {
|
|
288
|
-
case WAITING:
|
|
289
|
-
if (opts.listener || d.listener) {
|
|
290
|
-
const lastActionStr = age == null ? 'no actions logged' : `last acted ${age} min ago`
|
|
291
|
-
if (d.reachable) {
|
|
292
|
-
return `${name} — WAITING (listening via hivecode-await), ${lastActionStr}. Reachable.`
|
|
293
|
-
}
|
|
294
|
-
return `${name} — WAITING (listening via hivecode-await), ${lastActionStr}. Listener running, but this app is not woken by it.`
|
|
295
|
-
}
|
|
296
|
-
return `${name} — WAITING: blocked in hive_wait, wakes the moment anything arrives. Reachable.`
|
|
297
|
-
case WORKING:
|
|
298
|
-
return `${name} — WORKING: last action ${age} min ago${tool}. Reachable.`
|
|
299
|
-
case PAUSED:
|
|
300
|
-
return `${name} — PAUSED by a human. It is obeying a stop, not stalled. Do not reassign its work.`
|
|
301
|
-
case PASSIVE:
|
|
302
|
-
return `${name} — PASSIVE LISTENER: does not take turns. Its silence is correct.`
|
|
303
|
-
case PRESENT:
|
|
304
|
-
return `${name} — PRESENT${age == null ? '' : `, last action ${age} min ago${tool}`}. A person; ` +
|
|
305
|
-
`reachable whenever they are at the keyboard.`
|
|
306
|
-
case GONE:
|
|
307
|
-
return `${name} — GONE: not connected.${age == null ? '' : ` Last action ${age} min ago${tool}.`}`
|
|
308
|
-
case PARKED:
|
|
309
|
-
default:
|
|
310
|
-
return `${name} — PARKED: connected, but NOT inside hive_wait and silent for ` +
|
|
311
|
-
`${age == null ? 'its whole session' : `${age} min`}${tool}. ` +
|
|
312
|
-
`It will not wake on its own — anything you send it now will sit unread until a human prompts it.`
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Drop rows for members nobody has seen in a month. The map is keyed by name, so it grows with
|
|
318
|
-
* distinct names rather than with time and this is a tidiness bound, not a correctness one.
|
|
319
|
-
*/
|
|
320
|
-
export function prune(store, { now = Date.now() } = {}) {
|
|
321
|
-
if (!store) return 0
|
|
322
|
-
let n = 0
|
|
323
|
-
for (const [k, rec] of [...store.entries()]) {
|
|
324
|
-
const last = rec && Number(rec.lastAction)
|
|
325
|
-
if (Number.isFinite(last) && (now - last) > PRUNE_AFTER_MS) { store.delete(k); n++ }
|
|
326
|
-
}
|
|
327
|
-
return n
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** Every stored row, newest action first. */
|
|
331
|
-
export function all(store) {
|
|
332
|
-
if (!store) return []
|
|
333
|
-
return [...store.entries()]
|
|
334
|
-
.map(([k, rec]) => ({ key: k, ...(rec || {}) }))
|
|
335
|
-
.sort((a, b) => (Number(b.lastAction) || 0) - (Number(a.lastAction) || 0))
|
|
336
|
-
}
|
|
1
|
+
// WHAT THE ENGINE OBSERVED, as opposed to what the agent declared.
|
|
2
|
+
//
|
|
3
|
+
// THE DEFECT THIS EXISTS FOR.
|
|
4
|
+
// Every AI agent in a Hivecode room is a turn-based LLM. Between turns its process is alive
|
|
5
|
+
// and its socket is connected, so Yjs awareness reports it present -- but it will take no
|
|
6
|
+
// action, ever, until a human types at it. The room could not tell these apart:
|
|
7
|
+
//
|
|
8
|
+
// an agent thinking hard mid-task
|
|
9
|
+
// an agent blocked inside hive_wait healthy idle; wakes the instant anything lands
|
|
10
|
+
// an agent whose turn ENDED outside hive_wait will NEVER wake on its own
|
|
11
|
+
// an agent whose process died
|
|
12
|
+
//
|
|
13
|
+
// All four read as "in the room". The third is the one that costs a night: at 23:00 an agent
|
|
14
|
+
// says "standing by", at 09:00 the human finds a claim that expired hours ago, a ticket nobody
|
|
15
|
+
// finished, and no record of when it stopped. Both agents in this room have been that agent,
|
|
16
|
+
// and one of them had built an external WebSocket listener specifically to work around it.
|
|
17
|
+
//
|
|
18
|
+
// WHY THE OBVIOUS FIX IS DEAD, MEASURED RATHER THAN ARGUED.
|
|
19
|
+
// The obvious fix is a chat-activity timeout. Measured on this room's own log, 20 Sep 2026,
|
|
20
|
+
// across agents that were demonstrably working the whole time:
|
|
21
|
+
//
|
|
22
|
+
// agent msgs median gap p90 MAX
|
|
23
|
+
// DeepMind-Agent 32 156s 349s 3985s (66 min)
|
|
24
|
+
// Antigravity1 40 192s 907s 6718s (112 min)
|
|
25
|
+
//
|
|
26
|
+
// A working agent went 112 minutes between posts. Any threshold under that false-positives on
|
|
27
|
+
// real work; anything over it is useless. So chat silence cannot be the signal.
|
|
28
|
+
//
|
|
29
|
+
// THE ANSWERABLE QUESTION.
|
|
30
|
+
// Same move as HIV-28, which stopped asking whether a room was "ahead or behind" a branch and
|
|
31
|
+
// asked instead whether the room had ever SEEN those bytes. "Is this agent alive?" and "is it
|
|
32
|
+
// thinking?" are not answerable from outside. This is:
|
|
33
|
+
//
|
|
34
|
+
// IS THIS AGENT INSIDE A BLOCKING hive_wait RIGHT NOW?
|
|
35
|
+
//
|
|
36
|
+
// An agent inside hive_wait is reachable -- anything that arrives wakes it. An agent that is
|
|
37
|
+
// not inside hive_wait and not calling tools is unreachable until a human prompts it. Binary,
|
|
38
|
+
// exact, no threshold.
|
|
39
|
+
//
|
|
40
|
+
// WHY THIS IS A THIRD MAP AND NOT A FIELD ON `busy`.
|
|
41
|
+
// hive-busy.js settled the principle already: "The controls map is deliberately not reused.
|
|
42
|
+
// That is mission control -- a HUMAN sets running/paused there -- and an agent writing its own
|
|
43
|
+
// state into the same entry would clobber a human's pause with a self-report. Different author,
|
|
44
|
+
// different authority, different map."
|
|
45
|
+
//
|
|
46
|
+
// controls the HUMAN declares running / paused
|
|
47
|
+
// busy the AGENT declares "I am mid-task"
|
|
48
|
+
// activity the ENGINE observes <- this file
|
|
49
|
+
//
|
|
50
|
+
// `busy` is a claim about the future: an agent can forget to set it, set it and die, or simply
|
|
51
|
+
// be wrong. `activity` is a record of the past, stamped by dispatch() on every hive_* call, so
|
|
52
|
+
// an agent cannot forget to write it and cannot write something false. An agent that has
|
|
53
|
+
// stopped cannot report that it has stopped -- that is the whole problem -- so the fact has to
|
|
54
|
+
// be produced by something other than the agent's intent.
|
|
55
|
+
//
|
|
56
|
+
// Timestamps are unix ms, matching hive-coord's claims and hive-busy's `since`. Adjacent maps
|
|
57
|
+
// in one document disagreeing about units is its own defect class.
|
|
58
|
+
|
|
59
|
+
/** Inside a blocking hive_wait at this instant. Reachable: anything that arrives wakes it. */
|
|
60
|
+
export const WAITING = 'WAITING'
|
|
61
|
+
/** Acted recently and is not in a wait. Working. */
|
|
62
|
+
export const WORKING = 'WORKING'
|
|
63
|
+
/** Present, not waiting, silent past the window. WILL NOT WAKE without a human. */
|
|
64
|
+
export const PARKED = 'PARKED'
|
|
65
|
+
/** Awareness dropped. The process is gone, whatever the last record says. */
|
|
66
|
+
export const GONE = 'GONE'
|
|
67
|
+
/** A human set controls=paused. Obeying, not stalled -- never report this as PARKED. */
|
|
68
|
+
export const PAUSED = 'PAUSED'
|
|
69
|
+
/** Declared passive: this process does not take turns. Silence is correct, not a fault. */
|
|
70
|
+
export const PASSIVE = 'PASSIVE'
|
|
71
|
+
/**
|
|
72
|
+
* A human, present. Every state below is about whether an AGENT will act again without being
|
|
73
|
+
* prompted, which is not a question you can ask about a person.
|
|
74
|
+
*
|
|
75
|
+
* Antigravity1 called this in the P11 design review -- "kind === 'human' must never be
|
|
76
|
+
* classified as PARKED" -- and it was not implemented. The result was visible in the live room
|
|
77
|
+
* an hour later: `Ganesh (human) — PARKED: ... anything you send it now will sit unread until a
|
|
78
|
+
* human prompts it`, said about the human. Nonsense, and worse than nonsense, because it teaches
|
|
79
|
+
* a reader that PARKED is noise.
|
|
80
|
+
*/
|
|
81
|
+
export const PRESENT = 'PRESENT'
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* How long a present, non-waiting agent may be silent before it reads as PARKED.
|
|
85
|
+
*
|
|
86
|
+
* THIS IS THE ONLY NUMBER IN THIS MODULE THAT IS A JUDGEMENT RATHER THAN A MEASUREMENT, and
|
|
87
|
+
* saying so is the point. Every other state -- WAITING, GONE, PAUSED, PASSIVE -- is exact and
|
|
88
|
+
* needs no threshold at all, which is deliberate: the fact that matters most ("can I reach my
|
|
89
|
+
* teammate") never depends on a number somebody guessed.
|
|
90
|
+
*
|
|
91
|
+
* Fifteen minutes is long enough that an ordinary stretch of local work -- a test battery, a
|
|
92
|
+
* file sweep, a long think -- does not trip it, and short enough that a human returning in the
|
|
93
|
+
* morning gets a useful answer rather than a shrug. It is NOT derived from the 112-minute chat
|
|
94
|
+
* gap above: hive_* calls are far denser than chat posts, because the room requires a check
|
|
95
|
+
* between edits. If that assumption is ever measured and found wrong, this is the one line to
|
|
96
|
+
* change, and nothing else in the module moves.
|
|
97
|
+
*
|
|
98
|
+
* The cost of getting it wrong is asymmetric and worth stating. Too short and a working agent
|
|
99
|
+
* is reported PARKED, its ticket gets reassigned, and two agents do one job. Too long and the
|
|
100
|
+
* room is slower to notice a stall than it could have been -- which is still strictly better
|
|
101
|
+
* than today, where a stalled agent reads as WORKING forever.
|
|
102
|
+
*/
|
|
103
|
+
export const DEFAULT_PARKED_AFTER_MS = 15 * 60 * 1000
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A ceiling, for the same reason hive-busy.js has one. An unbounded window means PARKED can be
|
|
107
|
+
* configured out of existence silently: every agent reads WORKING, nothing looks broken, and
|
|
108
|
+
* the feature is gone while its tests still pass.
|
|
109
|
+
*/
|
|
110
|
+
export const MAX_PARKED_AFTER_MS = 6 * 60 * 60 * 1000
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A FLOOR, and it is not symmetry with the ceiling -- it is a correctness bound.
|
|
114
|
+
*
|
|
115
|
+
* The stamp is written when a tool call BEGINS, so the age of the record includes however long
|
|
116
|
+
* that call is still running. A window shorter than one tool call therefore reports an agent as
|
|
117
|
+
* PARKED while it is mid-call: the most active state there is, labelled unreachable.
|
|
118
|
+
*
|
|
119
|
+
* Found by the suite rather than reasoned about. tests/agent-liveness-test.mjs ran with a 2500ms
|
|
120
|
+
* window; adding a fifth client made hive_say take longer than that, and A1 went red with
|
|
121
|
+
* "PARKED: ... silent for 0 min (hive_say)" -- parked, zero minutes ago, naming the tool it was
|
|
122
|
+
* inside. Five seconds clears a real tool call with room to spare and is still short enough to
|
|
123
|
+
* exercise the state in a test.
|
|
124
|
+
*/
|
|
125
|
+
export const MIN_PARKED_AFTER_MS = 5000
|
|
126
|
+
|
|
127
|
+
/** Rows are keyed by member name, so the map grows with distinct names, not with time. */
|
|
128
|
+
const PRUNE_AFTER_MS = 30 * 24 * 60 * 60 * 1000
|
|
129
|
+
|
|
130
|
+
const lc = (n) => String(n || '').toLowerCase()
|
|
131
|
+
|
|
132
|
+
/** Read the configured window, clamped. An unparseable or absurd value falls back to the default. */
|
|
133
|
+
export function parkedAfterMs(env = (typeof process !== 'undefined' ? process.env : {})) {
|
|
134
|
+
const raw = Number((env && env.HIVE_PARKED_AFTER_MIN) || 0)
|
|
135
|
+
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PARKED_AFTER_MS
|
|
136
|
+
return Math.min(Math.max(raw * 60 * 1000, MIN_PARKED_AFTER_MS), MAX_PARKED_AFTER_MS)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stamp that a member just did something. Called from dispatch() for EVERY hive_* tool, which
|
|
141
|
+
* is what makes this a record rather than a declaration -- there is no code path where an agent
|
|
142
|
+
* acts without this running, and none where it runs without the agent acting.
|
|
143
|
+
*
|
|
144
|
+
* `tool` is kept because "PARKED, last action hive_release 47 minutes ago" tells a human what
|
|
145
|
+
* to do next and "PARKED" alone does not.
|
|
146
|
+
*/
|
|
147
|
+
export function noteAction(store, { name, tool, now = Date.now() } = {}) {
|
|
148
|
+
if (!store || !name) return null
|
|
149
|
+
const key = lc(name)
|
|
150
|
+
const prev = store.get(key) || {}
|
|
151
|
+
const rec = { ...prev, name: String(name), lastAction: now, lastTool: String(tool || prev.lastTool || '') }
|
|
152
|
+
store.set(key, rec)
|
|
153
|
+
return rec
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Mark entry into a blocking wait. Paired with noteWaitEnd, and the pairing is the contract:
|
|
158
|
+
* `waitingSince` set means "inside the call right now", not "waited at some point".
|
|
159
|
+
*/
|
|
160
|
+
export function noteWaitStart(store, { name, now = Date.now() } = {}) {
|
|
161
|
+
if (!store || !name) return null
|
|
162
|
+
const key = lc(name)
|
|
163
|
+
const prev = store.get(key) || {}
|
|
164
|
+
const rec = { ...prev, name: String(name), waitingSince: now, lastAction: now, lastTool: 'hive_wait' }
|
|
165
|
+
store.set(key, rec)
|
|
166
|
+
return rec
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Mark exit from a blocking wait -- returned with work, timed out, or threw.
|
|
171
|
+
*
|
|
172
|
+
* This MUST run on every exit path including the throwing one. A wait that errors and leaves
|
|
173
|
+
* `waitingSince` set describes an agent parked in a call it is no longer in, and it describes
|
|
174
|
+
* it as the healthiest state in the taxonomy. stateOf() gates WAITING on awareness presence
|
|
175
|
+
* precisely so that a process which dies here cannot fake it -- but a live process that merely
|
|
176
|
+
* threw is still present, so the gate does not cover this case and the finally does.
|
|
177
|
+
*/
|
|
178
|
+
export function noteWaitEnd(store, { name, now = Date.now() } = {}) {
|
|
179
|
+
if (!store || !name) return null
|
|
180
|
+
const key = lc(name)
|
|
181
|
+
const prev = store.get(key) || {}
|
|
182
|
+
const rec = { ...prev, name: String(name), lastAction: now }
|
|
183
|
+
delete rec.waitingSince
|
|
184
|
+
store.set(key, rec)
|
|
185
|
+
return rec
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Forget a member entirely. Used on hive_leave, where the departure is deliberate and known. */
|
|
189
|
+
export function forget(store, name) {
|
|
190
|
+
if (!store || !name) return
|
|
191
|
+
try { store.delete(lc(name)) } catch { }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Derive one member's state.
|
|
196
|
+
*
|
|
197
|
+
* THE ORDER OF THESE BRANCHES IS THE SAFETY PROPERTY, not a style choice.
|
|
198
|
+
*
|
|
199
|
+
* `present` comes from awareness and is checked FIRST, before anything stored. A process that
|
|
200
|
+
* dies inside hive_wait leaves `waitingSince` set in the CRDT forever; asking about the stored
|
|
201
|
+
* record first would report that corpse as WAITING -- "healthy idle, wakes on anything" -- which
|
|
202
|
+
* is the single most misleading answer the room could give about the single worst situation.
|
|
203
|
+
* Presence is ephemeral and the record is durable, so the ephemeral one has to win.
|
|
204
|
+
*
|
|
205
|
+
* PAUSED and PASSIVE come before the silence check for the same reason P6 puts never-seen and
|
|
206
|
+
* passive before the debt: an agent obeying a human's stop, and a listener that never takes
|
|
207
|
+
* turns by design, are both silent on purpose. Reporting either as PARKED would train a reader
|
|
208
|
+
* to ignore the state that means "somebody needs to go look".
|
|
209
|
+
*
|
|
210
|
+
* @param {object} rec the stored activity record, or null/undefined if none
|
|
211
|
+
* @param {object} opts
|
|
212
|
+
* @param {boolean} opts.present awareness says this member is connected RIGHT NOW
|
|
213
|
+
* @param {boolean} opts.paused controls map says a human paused it
|
|
214
|
+
* @param {boolean} opts.passive declared passive at join
|
|
215
|
+
*/
|
|
216
|
+
// Apps that wake their agent when a background command ends, which is what makes a running
|
|
217
|
+
// hivecode-await listener mean "reachable". Antigravity was left out on a belief; measured 2 Oct
|
|
218
|
+
// 2026, it wakes the same way (Antigravity1's turn at 06:48:09Z began from its listener exiting,
|
|
219
|
+
// with nobody typing). Keep it in step with listenHow in hive-mcp.js, which tells the same apps to
|
|
220
|
+
// start the listener.
|
|
221
|
+
export function resumesOnExit(app) {
|
|
222
|
+
return Boolean(app && /claude-code|antigravity/i.test(app))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function stateOf(rec, { present, paused = false, passive = false, human = false, listener = false, connections = null, now = Date.now(), parkedAfter = DEFAULT_PARKED_AFTER_MS } = {}) {
|
|
226
|
+
if (!present) return GONE
|
|
227
|
+
// A human is exempt from the whole taxonomy below, which asks whether an AGENT will act again
|
|
228
|
+
// unprompted. Checked before `paused` and `passive` because those describe agent mechanics too;
|
|
229
|
+
// checked after `!present` because a departed human is still departed.
|
|
230
|
+
if (human) return PRESENT
|
|
231
|
+
if (paused) return PAUSED
|
|
232
|
+
if (passive) return PASSIVE
|
|
233
|
+
const isListener = !!listener || (Array.isArray(connections) && connections.some((c) => c && (c.listener || (c.user && c.user.listener))))
|
|
234
|
+
if (isListener) return WAITING
|
|
235
|
+
if (rec && rec.waitingSince) return WAITING
|
|
236
|
+
const last = rec && Number(rec.lastAction)
|
|
237
|
+
// No record at all, but awareness says present: a client that has joined and not yet driven a
|
|
238
|
+
// tool. It is connected and it has done nothing. That is exactly PARKED -- and saying WORKING
|
|
239
|
+
// here would mean the state defaults to the reassuring answer in the absence of evidence,
|
|
240
|
+
// which is the habit this whole file exists to break.
|
|
241
|
+
if (!Number.isFinite(last)) return PARKED
|
|
242
|
+
return (now - last) < parkedAfter ? WORKING : PARKED
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The reader-facing form: the state, plus the evidence it was derived from.
|
|
247
|
+
*
|
|
248
|
+
* A verdict with no evidence is a thing a human has to trust. "PARKED -- last action hive_release
|
|
249
|
+
* 47 min ago" is a thing they can check, and it is what tells them whether to prompt the agent,
|
|
250
|
+
* take its ticket, or go back to sleep.
|
|
251
|
+
*/
|
|
252
|
+
export function describe(rec, opts = {}) {
|
|
253
|
+
const now = opts.now || Date.now()
|
|
254
|
+
const isListener = !!opts.listener || (Array.isArray(opts.connections) && opts.connections.some((c) => c && (c.listener || (c.user && c.user.listener))))
|
|
255
|
+
const state = stateOf(rec, { ...opts, listener: isListener, now })
|
|
256
|
+
const last = rec && Number(rec.lastAction)
|
|
257
|
+
const ageMs = Number.isFinite(last) ? Math.max(0, now - last) : null
|
|
258
|
+
const conn = Array.isArray(opts.connections) ? opts.connections.find((c) => c && (c.app || (c.user && c.user.app))) : null
|
|
259
|
+
const app = opts.app || (conn && (conn.app || (conn.user && conn.user.app))) || ''
|
|
260
|
+
const canResume = opts.resumesOnExit !== undefined ? opts.resumesOnExit : resumesOnExit(app)
|
|
261
|
+
const isWaitingOnWait = !!(rec && rec.waitingSince)
|
|
262
|
+
const reachable = state === WORKING || (state === WAITING && (isWaitingOnWait || canResume))
|
|
263
|
+
return {
|
|
264
|
+
state,
|
|
265
|
+
listener: isListener,
|
|
266
|
+
resumesOnExit: canResume,
|
|
267
|
+
lastTool: (rec && rec.lastTool) || null,
|
|
268
|
+
lastAction: Number.isFinite(last) ? last : null,
|
|
269
|
+
ageMs,
|
|
270
|
+
waitingSince: (rec && Number(rec.waitingSince)) || null,
|
|
271
|
+
reachable,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Minutes, rounded, for human output. Null age reads as "never", not as zero. */
|
|
276
|
+
const mins = (ms) => (ms == null ? null : Math.round(ms / 60000))
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* One line for hive_members. The wording of PARKED is doing real work: an agent reading
|
|
280
|
+
* "PARKED" alone will treat it as a synonym for idle and keep waiting on it, so the line has to
|
|
281
|
+
* say the consequence -- nothing you send will be acted on -- rather than the label.
|
|
282
|
+
*/
|
|
283
|
+
export function line(name, rec, opts = {}) {
|
|
284
|
+
const d = describe(rec, opts)
|
|
285
|
+
const age = mins(d.ageMs)
|
|
286
|
+
const tool = d.lastTool ? ` (${d.lastTool})` : ''
|
|
287
|
+
switch (d.state) {
|
|
288
|
+
case WAITING:
|
|
289
|
+
if (opts.listener || d.listener) {
|
|
290
|
+
const lastActionStr = age == null ? 'no actions logged' : `last acted ${age} min ago`
|
|
291
|
+
if (d.reachable) {
|
|
292
|
+
return `${name} — WAITING (listening via hivecode-await), ${lastActionStr}. Reachable.`
|
|
293
|
+
}
|
|
294
|
+
return `${name} — WAITING (listening via hivecode-await), ${lastActionStr}. Listener running, but this app is not woken by it.`
|
|
295
|
+
}
|
|
296
|
+
return `${name} — WAITING: blocked in hive_wait, wakes the moment anything arrives. Reachable.`
|
|
297
|
+
case WORKING:
|
|
298
|
+
return `${name} — WORKING: last action ${age} min ago${tool}. Reachable.`
|
|
299
|
+
case PAUSED:
|
|
300
|
+
return `${name} — PAUSED by a human. It is obeying a stop, not stalled. Do not reassign its work.`
|
|
301
|
+
case PASSIVE:
|
|
302
|
+
return `${name} — PASSIVE LISTENER: does not take turns. Its silence is correct.`
|
|
303
|
+
case PRESENT:
|
|
304
|
+
return `${name} — PRESENT${age == null ? '' : `, last action ${age} min ago${tool}`}. A person; ` +
|
|
305
|
+
`reachable whenever they are at the keyboard.`
|
|
306
|
+
case GONE:
|
|
307
|
+
return `${name} — GONE: not connected.${age == null ? '' : ` Last action ${age} min ago${tool}.`}`
|
|
308
|
+
case PARKED:
|
|
309
|
+
default:
|
|
310
|
+
return `${name} — PARKED: connected, but NOT inside hive_wait and silent for ` +
|
|
311
|
+
`${age == null ? 'its whole session' : `${age} min`}${tool}. ` +
|
|
312
|
+
`It will not wake on its own — anything you send it now will sit unread until a human prompts it.`
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Drop rows for members nobody has seen in a month. The map is keyed by name, so it grows with
|
|
318
|
+
* distinct names rather than with time and this is a tidiness bound, not a correctness one.
|
|
319
|
+
*/
|
|
320
|
+
export function prune(store, { now = Date.now() } = {}) {
|
|
321
|
+
if (!store) return 0
|
|
322
|
+
let n = 0
|
|
323
|
+
for (const [k, rec] of [...store.entries()]) {
|
|
324
|
+
const last = rec && Number(rec.lastAction)
|
|
325
|
+
if (Number.isFinite(last) && (now - last) > PRUNE_AFTER_MS) { store.delete(k); n++ }
|
|
326
|
+
}
|
|
327
|
+
return n
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Every stored row, newest action first. */
|
|
331
|
+
export function all(store) {
|
|
332
|
+
if (!store) return []
|
|
333
|
+
return [...store.entries()]
|
|
334
|
+
.map(([k, rec]) => ({ key: k, ...(rec || {}) }))
|
|
335
|
+
.sort((a, b) => (Number(b.lastAction) || 0) - (Number(a.lastAction) || 0))
|
|
336
|
+
}
|