hivecode-mcp 0.13.1 → 0.14.1

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.
@@ -236,6 +236,30 @@ Paste the contents of [`slack-app-manifest-inbound.json`](../slack-app-manifest-
236
236
  3. Enter `socket-mode` as the **Token Name** (required by Slack), click **Add Scope**, select `connections:write`, and click **Generate**.
237
237
  4. Copy the resulting `xapp-…` token and set it as `SLACK_APP_TOKEN`.
238
238
 
239
+ ### Networks That Refuse WebSockets (Cloud VMs): `--inbound`
240
+ Socket Mode is a WebSocket. Some networks refuse every WebSocket upgrade. One example is Antigravity's cloud VM, whose proxy answers `403 WebSocket upgrades are not allowed`. On such a network the bridge reads Slack over plain HTTPS instead: `conversations.history` for the channel and `conversations.replies` for the threads it tracks.
241
+
242
+ | `--inbound` (or `HIVE_SLACK_INBOUND`) | What it does |
243
+ | --- | --- |
244
+ | `auto` (default) | Socket Mode when `SLACK_APP_TOKEN` is set. If the network refuses the upgrade with an HTTP status, it switches to HTTPS polling and prints `[hivecode] Slack replies are read over HTTPS every 3 s (...)`. Without `SLACK_APP_TOKEN`, it is outbound only, as before. |
245
+ | `poll` | HTTPS polling only. **Needs no `SLACK_APP_TOKEN`**, just a bot token from [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json) (`channels:history` or `groups:history`, plus `users:read`). |
246
+ | `socket` | Socket Mode only. Never polls. |
247
+
248
+ ```bash
249
+ SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack check --channel C0123456789 --inbound poll
250
+ SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack start "<relay|room>" --channel C0123456789 --allow U123 --inbound poll
251
+ ```
252
+
253
+ What to know about polling:
254
+ - **Speed:** a reply reaches the room within about `HIVE_SLACK_POLL_MS` (default 3000 ms). Socket Mode is near-instant.
255
+ - **Restarts:** where it stopped reading is kept in the room and on disk, so a restarted bridge neither replays old messages nor skips new ones. Two bridges on one room claim each message before acting, so a `stop` is acted on once.
256
+ - **Downtime:** after the bridge has been offline, messages older than `HIVE_SLACK_CATCHUP_MS` (default 30 minutes) are **not acted on**. A two-hour-old `resume` should not fire. The bridge posts once in the room's thread with the offline window and the count, so nobody assumes they were handled. Send it again if it still matters.
257
+ - **Rate limits:** two reads per poll, more while tracked threads are active. A `429` is honoured with Slack's `Retry-After`.
258
+ - `--agent` (the native stop button) needs Socket Mode and cannot poll.
259
+
260
+ ### Strict or Secured Rooms: the Bridge Needs a Human Token
261
+ `stop` and `resume` from Slack change the room's controls, and the relay lets only a human change controls: an AI's write is reverted. The bridge acts for a named Slack person, so it joins as a human. On an open room this works with no token. On a **strict or secured room**, start the bridge with a human (or admin/maintainer) token in the link or `HIVE_TOKEN`. Otherwise the relay reverts the stop, so check the agents' state in the room after your first `stop`.
262
+
239
263
  ### Who May Act (`--allow` / `--slack-allow`) & No Impersonation
240
264
  - **STOP is fail-safe**: Any full member of your Slack workspace may type `stop`, `stop all`, or `stop <AgentName>` in a thread to pause agents (or stop an active GitHub issue run) immediately. If Slack's `users.info` endpoint fails during an outage, `stop` still succeeds and notes `(stopped; I could not confirm who you are)`.
241
265
  - **RESUME, RETRY, and thread replies require the allowlist (`--allow U123,U456` on `slack start`, or `--slack-allow U123,U456` on `github start`)**: Only Slack user IDs explicitly listed in the allowlist (find yours in Slack under **Profile → `⋯` → Copy member ID**) may resume agents (`resume`, `resume all`, `resume <AgentName>`), retry a GitHub issue run (`retry`), or send replies into the room. If the allowlist is omitted, inbound is **stop-only**.
package/hive-await.js CHANGED
@@ -165,16 +165,34 @@ if (process.env.HIVE_AWAIT_KEEP_ALIVE !== '1' && PARENT > 1 && !parentGone()) {
165
165
  }, Number(process.env.HIVE_AWAIT_PARENT_CHECK_MS) || 15000).unref()
166
166
  }
167
167
 
168
+ // A REBUILT ROOM MUST NOT LEAVE THE LISTENER DEAF (7 Nov 2026, found by antigravity-2). A relay
169
+ // that restarts without its rooms (every deploy on Render's free plan) starts chat again from zero,
170
+ // but the cursor on disk still said, say, 300: scanning from 300 of a 3-message chat found nothing,
171
+ // so the agent showed LISTENING and no mention woke it until the idle timeout, which then moved the
172
+ // cursor past everything it had missed. So the cursor records which room it was counted in (its
173
+ // first message) and is ignored when that differs, or when it points past the end -- the rule the
174
+ // room's cursor above and the MCP's catch-up already follow. Chat is never trimmed, so within one
175
+ // room the first message never changes. An older cursor file without `head` is only clamped.
176
+ // (tests/await-rebuilt-room-test.mjs)
168
177
  const cursorFile = path.join(dir, `.await-cursor-${room}.json`)
169
- const readCursor = () => { try { return Number(JSON.parse(fs.readFileSync(cursorFile, 'utf8')).at) || 0 } catch { return 0 } }
170
- const writeCursor = (n) => { try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(cursorFile, JSON.stringify({ at: n }), 'utf8') } catch { /* a cursor we cannot persist must not break the wait */ } }
178
+ const headOf = (msgs) => { const m = msgs[0]; return m ? `${m.by}|${m.at}|${String(m.text || '').slice(0, 80)}` : '' }
179
+ const readCursor = (msgs) => {
180
+ try {
181
+ const c = JSON.parse(fs.readFileSync(cursorFile, 'utf8'))
182
+ const at = Number(c.at) || 0
183
+ if (at > msgs.length) return 0
184
+ if (typeof c.head === 'string' && c.head && c.head !== headOf(msgs)) return 0
185
+ return at
186
+ } catch { return 0 }
187
+ }
188
+ const writeCursor = (n, msgs) => { try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(cursorFile, JSON.stringify({ at: n, head: headOf(msgs) }), 'utf8') } catch { /* a cursor we cannot persist must not break the wait */ } }
171
189
 
172
190
  function check() {
173
191
  if (superseded()) return // the newer listener delivers it; consuming it here would hide it from that one
174
192
  const msgs = chat.toArray()
175
193
  let lastMine = -1
176
194
  for (let i = msgs.length - 1; i >= 0; i--) { if (msgs[i].by === NAME) { lastMine = i; break } }
177
- const from = Math.max(readCursor(), lastMine + 1, roomCursor(msgs.length))
195
+ const from = Math.max(readCursor(msgs), lastMine + 1, roomCursor(msgs.length))
178
196
  const hits = []
179
197
  for (let i = from; i < msgs.length; i++) {
180
198
  const m = msgs[i]
@@ -183,7 +201,7 @@ function check() {
183
201
  // Advance past everything READ, not just everything matched — an unaddressed message I have
184
202
  // already seen must not be re-scanned on the next call either.
185
203
  if (hits.length) {
186
- writeCursor(msgs.length)
204
+ writeCursor(msgs.length, msgs)
187
205
  publish(msgs.length)
188
206
  // THE ORDER TO START AGAIN RIDES ON EVERY WAKE. It used to be said once, on the agent's first
189
207
  // quiet hive_wait, and an agent had to remember it after every wake; agents forget (4 Oct 2026).
@@ -205,7 +223,7 @@ hive.provider.on('sync', (s) => {
205
223
  // An idle timeout also advances the cursor. Without this, a quiet window followed by a loud one
206
224
  // would re-deliver everything that arrived during the quiet window too.
207
225
  setTimeout(() => {
208
- try { writeCursor(chat.length) } catch {}
226
+ try { writeCursor(chat.length, chat.toArray()) } catch {}
209
227
  publish(chat.length) // nothing in the window named us, so nothing in it is mail the MCP should raise
210
228
  finish('idle', { note: `nothing addressed to ${NAME} in ${IDLE_MIN} min. Run this same command again in the background to keep listening: ${AGAIN}` }, 1500)
211
229
  }, Number(IDLE_MIN) * 60 * 1000)