hivecode-mcp 0.11.2 → 0.11.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-coord.js +29 -2
- package/hive-mailbox.js +30 -0
- package/hive-mcp.js +19 -7
- package/package.json +3 -2
- package/sync.js +2021 -1982
package/hive-coord.js
CHANGED
|
@@ -31,8 +31,19 @@
|
|
|
31
31
|
|
|
32
32
|
export function makeCoordinator(shared, self, opts = {}) {
|
|
33
33
|
const ttl = opts.ttl != null ? opts.ttl : 5000
|
|
34
|
+
// A holder may RENEW its own claim, because a long edit should not lose the file mid-write.
|
|
35
|
+
// But renewal cannot be unbounded, or the lease stops being a lease. MEASURED on a 12-agent
|
|
36
|
+
// run: one agent re-claimed site/pricing.md eight times and legal/terms.md seven times and
|
|
37
|
+
// released twice, and since `claim` reset `at` on every call the 60s TTL never once elapsed.
|
|
38
|
+
// Six other agents sensed a live owner for the whole run; sixteen messages asked it to let go.
|
|
39
|
+
// `since` records the FIRST acquisition and survives renewal, so what caps a claim is total
|
|
40
|
+
// hold time rather than time since the holder last touched it.
|
|
41
|
+
const maxHold = opts.maxHold != null ? opts.maxHold : ttl * 3
|
|
34
42
|
const now = opts.now || (() => Date.now())
|
|
35
|
-
const
|
|
43
|
+
const held = (c) => now() - (c.since != null ? c.since : c.at) // total, across renewals
|
|
44
|
+
const cap = (c) => (c.maxHold != null ? c.maxHold : maxHold)
|
|
45
|
+
const overheld = (c) => !!c && held(c) >= cap(c)
|
|
46
|
+
const expired = (c) => !c || (now() - c.at) >= c.ttl || overheld(c)
|
|
36
47
|
|
|
37
48
|
// SENSE: the live, non-expired claim on a region (or null if open).
|
|
38
49
|
const sense = (region) => { const c = shared.get(region); return expired(c) ? null : c }
|
|
@@ -40,7 +51,16 @@ export function makeCoordinator(shared, self, opts = {}) {
|
|
|
40
51
|
// Is this region open to ME? (free, expired, or already mine)
|
|
41
52
|
const free = (region) => { const c = shared.get(region); return expired(c) || c.by === self }
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
// A renewal keeps the original `since`; a fresh acquisition starts a new one. Claims written
|
|
55
|
+
// before `since` existed fall back to `at`, so an older room stays readable.
|
|
56
|
+
const claim = (region, intent) => {
|
|
57
|
+
const prev = shared.get(region)
|
|
58
|
+
const renewing = !!prev && prev.by === self && !expired(prev)
|
|
59
|
+
shared.set(region, {
|
|
60
|
+
by: self, intent, at: now(), ttl, maxHold,
|
|
61
|
+
since: renewing ? (prev.since != null ? prev.since : prev.at) : now(),
|
|
62
|
+
})
|
|
63
|
+
}
|
|
44
64
|
const release = (region) => { const c = shared.get(region); if (c && c.by === self) shared.delete(region) }
|
|
45
65
|
|
|
46
66
|
// ACQUIRE one region: sense → claim → verify. Returns true iff I now hold it.
|
|
@@ -49,6 +69,13 @@ export function makeCoordinator(shared, self, opts = {}) {
|
|
|
49
69
|
const acquire = (region, intent) => {
|
|
50
70
|
const c = shared.get(region)
|
|
51
71
|
if (c && !expired(c) && c.by !== self) return false // sensed an owner → defer
|
|
72
|
+
// Once total hold is exhausted the region is open to everyone — INCLUDING the outgoing
|
|
73
|
+
// holder, which could then win the re-claim race indefinitely and starve the waiters that
|
|
74
|
+
// maxHold exists to admit. So the overrunning holder stands down for a full maxHold, which
|
|
75
|
+
// is a window every waiter is guaranteed to poll inside. A single-ttl cooldown was tried
|
|
76
|
+
// first and is NOT enough: the holder just waits it out and opens a fresh `since`.
|
|
77
|
+
// The bar lapses on its own, so a file nobody else wants returns to its owner.
|
|
78
|
+
if (c && c.by === self && overheld(c) && held(c) < 2 * cap(c)) return false
|
|
52
79
|
claim(region, intent)
|
|
53
80
|
const after = shared.get(region)
|
|
54
81
|
return !(after && after.by !== self) // verify: did my claim survive?
|
package/hive-mailbox.js
CHANGED
|
@@ -141,3 +141,33 @@ export function formatMessage(m) {
|
|
|
141
141
|
const times = m.repeated > 1 ? ` (×${m.repeated})` : ''
|
|
142
142
|
return `${m.at || ''} ${m.by}(${m.kind || 'ai'}): ${m.text}${times}`
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* AMBIENT DIGEST -- what to do with room chatter nobody addressed to you.
|
|
147
|
+
*
|
|
148
|
+
* Un-addressed messages were handed to every agent in full, on every wake. In a room of N
|
|
149
|
+
* agents that means each one pays for the other N-1's conversation every single turn: O(N^2)
|
|
150
|
+
* context, so TOKEN COST -- not the relay -- becomes the real ceiling on room size. Measured:
|
|
151
|
+
* the relay holds 8 clients per room, converging in 1.0s on 45 MB. What actually stops a big
|
|
152
|
+
* room is this block of text.
|
|
153
|
+
*
|
|
154
|
+
* The COUNT stays exact and the newest messages still arrive in full. Only the older bodies
|
|
155
|
+
* are withheld, and the agent is told the number and how to read them, so nothing is hidden --
|
|
156
|
+
* it just stops being paid for unasked. An agent that wants the whole log calls hive_read_chat.
|
|
157
|
+
*
|
|
158
|
+
* Returns '' when there is nothing, so a caller can drop it with a falsy check.
|
|
159
|
+
*/
|
|
160
|
+
export function ambientDigest(messages, { keep = 4 } = {}) {
|
|
161
|
+
const list = Array.isArray(messages) ? messages : []
|
|
162
|
+
if (!list.length) return ''
|
|
163
|
+
const shown = keep > 0 ? list.slice(-keep) : []
|
|
164
|
+
const hidden = list.length - shown.length
|
|
165
|
+
const head = 'room activity (' + list.length +
|
|
166
|
+
(hidden ? ', newest ' + shown.length + ' shown' : '') + '):'
|
|
167
|
+
const tail = hidden
|
|
168
|
+
? '\n... ' + hidden + ' earlier message(s) withheld to keep your context small'
|
|
169
|
+
+ ' -- hive_read_chat for the full log'
|
|
170
|
+
: ''
|
|
171
|
+
if (!shown.length) return head + tail
|
|
172
|
+
return head + '\n' + shown.map(formatMessage).join('\n') + tail
|
|
173
|
+
}
|
package/hive-mcp.js
CHANGED
|
@@ -34,7 +34,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprot
|
|
|
34
34
|
if (!process.env.HIVE_PROVENANCE) process.env.HIVE_PROVENANCE = 'on'
|
|
35
35
|
if (!process.env.HIVE_FORK_GATE) process.env.HIVE_FORK_GATE = 'on'
|
|
36
36
|
import { startSync, parseLink } from './sync.js'
|
|
37
|
-
import { createMailbox, formatMessage, mentionsName, priorityOf } from './hive-mailbox.js'
|
|
37
|
+
import { createMailbox, formatMessage, mentionsName, mentionedNames, priorityOf, ambientDigest } from './hive-mailbox.js'
|
|
38
38
|
|
|
39
39
|
const DEFAULT_RELAY = process.env.HIVE_RELAY || 'wss://livecode-xoss.onrender.com'
|
|
40
40
|
let session = null // { hive, room, relay, dir, name, mailbox }
|
|
@@ -457,7 +457,18 @@ async function dispatch(req) {
|
|
|
457
457
|
// a human reading the log, not just to the recipient's tool output.
|
|
458
458
|
const body = args.urgent && !/^\s*\[!\]/.test(raw) ? `[!] ${raw}` : raw
|
|
459
459
|
requireSession().hive.say(body)
|
|
460
|
-
|
|
460
|
+
// ADDRESSING IS THE PROTOCOL, and a rule an agent merely read is a rule it forgets once
|
|
461
|
+
// its context fills. So the tool itself answers back: a message that named nobody is
|
|
462
|
+
// reported as landing in everyone's ambient digest, where it is meant to be skipped.
|
|
463
|
+
// Immediate, per-call feedback, at the moment the mistake is made.
|
|
464
|
+
const named = mentionedNames(body)
|
|
465
|
+
if (!named.length) {
|
|
466
|
+
return text(`sent: ${body}\n\nNOBODY WAS ADDRESSED. This went to every agent's "room ` +
|
|
467
|
+
`activity", where they are told to skip it — so assume no one acts on it. Name the ` +
|
|
468
|
+
`agents you actually need with @Name (hive_members lists who is here). Use an ` +
|
|
469
|
+
`unaddressed message only for a genuine announcement to the whole room.`)
|
|
470
|
+
}
|
|
471
|
+
return text(`sent to ${named.map((n) => '@' + n).join(', ')}: ${body}`)
|
|
461
472
|
}
|
|
462
473
|
case 'hive_read_chat': {
|
|
463
474
|
const me = requireSession().name
|
|
@@ -515,7 +526,8 @@ async function dispatch(req) {
|
|
|
515
526
|
parts.push(got.mentions.map(formatMessage).join('\n'))
|
|
516
527
|
}
|
|
517
528
|
const ambient = got.messages.filter((m) => !mentionsName(m.text, s.name))
|
|
518
|
-
|
|
529
|
+
const dig = ambientDigest(ambient)
|
|
530
|
+
if (dig) parts.push(dig)
|
|
519
531
|
if (tasks.length) parts.push(`approved work queued for you:\n${tasks.map(fmtTask).join('\n')}`)
|
|
520
532
|
return text(parts.join('\n\n'))
|
|
521
533
|
}
|
|
@@ -552,10 +564,10 @@ async function dispatch(req) {
|
|
|
552
564
|
const mentionBlock = (r) => (r.mentions && r.mentions.length)
|
|
553
565
|
? `${r.priority === 'stop' ? '🛑 STOP — handle this BEFORE anything else' : '📣 ADDRESSED TO YOU'}:\n${r.mentions.map(formatMessage).join('\n')}`
|
|
554
566
|
: ''
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
567
|
+
// Both delivery paths go through the SAME digest. They used to format ambient traffic
|
|
568
|
+
// separately, which is how two copies of a rule drift apart -- the exact hazard that
|
|
569
|
+
// already bit the duplicated reconcile() and the two HIVE_RULES copies.
|
|
570
|
+
const ambientBlock = (r) => ambientDigest((r.messages || []).filter((m) => !mentionsName(m.text, me)))
|
|
559
571
|
|
|
560
572
|
if (r && r.paused) {
|
|
561
573
|
const parts = [mentionBlock(r), ambientBlock(r)].filter(Boolean)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hivecode-mcp",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "MCP server that lets any AI coding agent join a Hivecode room \u2014 humans and multiple AI agents edit one live codebase with folder-scoped permissions and approval gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"demo:panel": "node demo/record-panel.mjs",
|
|
89
89
|
"demo:all": "node demo/record.mjs && node demo/record-panel.mjs",
|
|
90
90
|
"check:files": "node scripts/check-files.mjs",
|
|
91
|
-
"prepublishOnly": "node scripts/check-files.mjs"
|
|
91
|
+
"prepublishOnly": "node scripts/check-files.mjs",
|
|
92
|
+
"demo:cast": "node demo/cast.mjs --dry-run"
|
|
92
93
|
},
|
|
93
94
|
"engines": {
|
|
94
95
|
"node": ">=18"
|