mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
a0f410e161e60b38e7d7cb6ed1346f89ba3d1aea
34
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ff8482e76 |
feat(slack): report local relay attempts in flight (#2569)
rig slack status adds "Relay attempts awaiting response: N" for the local Slack relay, beside the retained inbound records. The daemon's inbound router counts each forward to another host while it's awaiting a response (incremented before the send, decremented when it settles, success or not), and the connector status reports that number. It's point-in-time and covers only the current connector instance: after the Slack connector is restarted, a relay the previous instance started can still be awaiting its response (up to the forwarder's 10 s timeout) while the new count reads 0. Zero doesn't prove quiescence across hosts or instances. Routing, retries, retained records and the forwarder are unchanged. |
||
|
|
a3e5e92112 |
feat: list Slack channels and people through the daemon (#2501)
A daemon client can now offer Slack channel and person choices, to fill in a channel map or register a person, without holding the Slack bot token. `GET /api/gateway/slack/channels` returns one page of the workspace's public, unarchived channels as `{ id, name, isMember }`, and `GET /api/gateway/slack/users` one page of people as `{ id, name, realName, displayName, isBot, deleted }`, without email. Both page with Slack's cursor (`nextCursor`, null on the last page) and `limit` (default 100, at most 200), and each request is one live Slack call with no cache or stored copy. Listing needs only a resolvable bot token and config, not inbound delivery. A missing token answers 503 `slack_not_configured`, a missing Slack scope 403 `slack_missing_scope` with the scope named, a rate limit 429 `slack_rate_limited` with Slack's `Retry-After`, a transport failure 502 `slack_unreachable` with no transport detail, and any other Slack error 502 `slack_error`. Access is the same as today's gateway reads. No new scope, private channels, direct messages or migration. Limits: proven against a fake Slack, not a live workspace; an app installed from an older manifest may lack `users:read`. Reported in #2487 (item 4).
|
||
|
|
74501bdf63 |
docs(slack): name the private-channel route the connector already accepts (#2494)
The Slack setup guide now says how to use a private channel. The generated manifest requests public-channel access only, and a new section gives the route: add groups:history and groups:read, subscribe to message.groups, reinstall the app and invite it to the channel, and optionally run rig slack setup --required-scopes so verify checks the added scopes. The channel-map paragraph says a private channel can be a target once the app has them. Direct messages stay unsupported, and the guide says OpenRig doesn't test private channels. Docs only; no behaviour change. Limits: the --required-scopes example repeats today's baseline scopes, so if the baseline gains a scope that command would need it added; a manifest option and verify mode for a private channel or direct message are not part of this change. Asked about in #313 by @ziadeh. |
||
|
|
bb007efabc |
fix(slack): mark original asks handled when their rows settle (#2472)
A Slack ask now shows that it was handled when its row settles anywhere, so a channel no longer fills with asks that look open but aren't. When a posted ask is answered in a pane, closed from the CLI, superseded, canceled, resolved in Mission Control, or a person's park moves to a queue blocker, the connector edits that ask's original primary post: a status line with the outcome, actor, time and closure note, the answer hints removed, and the buttons turned into historical option text. The rest of the post and its reconcile marker are kept, and supplemental posts and separate resolution notices are unchanged. Committed queue events drive the edits, with a catch-up at start from the retained notification receipts and channel stamps. A reopened direct ask reads "Open again"; it offers a thread answer only when it has no committed answer, and otherwise says the earlier answer is retained, since reopening doesn't accept a second answer. "Asked again" doesn't claim the later ask was already posted. One serialized worker uses the recovery budget (four calls per 15 seconds): each primary normally costs one GET read of the original and one JSON chat.update, a success is recorded only when Slack acknowledges the requested message at its current revision, Retry-After holds survive a restart, transient failures back off behind other work, and permanent Slack failures are reported as not done and excluded until the next connector start. Pending edits and the last failure show in rig slack status. Limits: private-channel reads may need groups:history; refused reads and unavailable originals never count as edited; catch-up needs the original receipt and stamp and a retained queue row; the first start after upgrading edits every retained historical ended ask at the edit rate, about an hour for 450 asks; reactions are not part of this change. Reported in #2468 by @ch-davidriott. |
||
|
|
522d635cdf |
docs(slack): correct missed-message recovery channel coverage (#2401)
docs/reference/slack-app-setup.md's channel-map section said missed-message recovery covers the default channel only. Since #1536 it recovers the default channel and every mapped channel, except a channel relayed to another machine, as the guide's reconnect-recovery sections already say. The historical 0.6.12 release note is unchanged. Docs +1/-1. Reported by @ziadeh. |
||
|
|
2f6263b7dc |
feat(slack): let reaction routing be turned off (#2241)
A person whose own script handles acknowledgements can set reactions off, so an emoji added to an ask no longer creates a row that costs the asking seat a turn. `rig slack setup --reactions route|off` saves an optional `reactions` key in `slack-connector.json` (validated on save; a missing key reads as route, so the default is unchanged), and the shared reaction attempt drops reactions when it's off, including one that was retained for retry before the switch, with the same ignored (reactions-off) disposition as a fresh one. Slack still sends the event and the manifest keeps it, so switching back needs no app change. The setup guide says this, and that the setting reaches a running connector when it next rewires (a daemon restart). No flow uses a reaction as an answer; a person answers by typing a reply or with the ask's buttons. 8 files: source (4 files, +20), tests (+35 -4) and docs (+9 -1). One save-time refusal for an invalid reactions value; no new error code. |
||
|
|
ba45f8ec61 |
feat(slack): show the asks still open after enable (#1679) (#2086)
`rig slack enable` treats the asks already open as seen, so none of them post: enabling never replays a backlog. `rig slack post-open-asks` now brings those asks into Slack through ordinary delivery, at the level each was asked at (quiet hours, digest and the posting floor apply), and says what became of each one: posted, already posted, queued awaiting its receipt, held, below the posting floor, no longer active, superseded, recipient changed, state unavailable, transport failed or uncertain. Running it again reuses each ask's one decision, so a confirmed post isn't repeated; one whose earlier attempt Slack couldn't confirm may post once more, marked as a possible repeat. State that can't be read is never taken as absent. An unreadable dispatch buffer, seen store or attempt record reads as state unavailable or uncertain, and an attempted re-shown ask whose Slack outcome is unconfirmed, or whose attempt log can't be read, stays in the buffer for its next replay instead of being recorded as not posted. Named limits are in the PR body. The need came from rakoort's report in #1679. |
||
|
|
16d2f01b15 |
docs(slack): one continuing conversation keeps earlier context, whoever writes (#2083)
The Slack setup guide now says that messages routed to the same continuing agent conversation can use that conversation's earlier context, whoever sent them. A queue row records who sent a message and which seat owns it; it doesn't start a separate agent conversation. If people need separate conversations, route them to separate seats (`--inbound` names the seat a channel's new messages reach). Separate channels help only when their routing also keeps the conversations separate. A conversation shared on purpose is fine. |
||
|
|
4e6896506a |
fix(slack): every gateway post names its sender first; a wake-rung escalation names the seat it's waiting on (#1960)
## What a user gets In a Slack channel where many agents post, the person sees who is asking before the ask. Every gateway post now starts with its sender, `From <seat> on <machine>`: - as the first block, above the headline; - as the first line of the fallback text a notification shows. Before, it was the last block and the last line. A wake-rung escalation (the queue's last rung, which posts a stuck row to a person) now says which seat the work is waiting on: `Waiting on <seat>: <the row's summary>`. Before, it showed only the row's summary, often with no seat named at all. ## The change - **`slack/message.ts`:** - the sender line moves to the front, in both the blocks and the fallback text, and now reads `From …`; - it's still named once, with the same caps and the same fallback budget; - at Slack's 50-block limit the repeat note is still the block left out. It's now found by identity, since it's no longer the second block. - **`operator-delivery-engine.ts`:** - the escalation's summary names the seat the row is waiting on; - its sender is unchanged: the row's own. So a reply in the thread still goes where the row's posts go; - a row with no sender is still signed with the queue's place. The stuck seat didn't send the escalation, so it isn't shown as the sender. - **Not in this change:** a separate "what you need to do" line. No row field holds a one-line ask (`humanIntent` is `decision` or `update`, and `humanDetail` is free-form), and this change adds no parsing. ## Tests - **New** (`slack-sender-first.test.ts`): - the sender is first, and named once, for a full session, for a session plus its machine, and with no seat; - the 50-block limit drops the repeat note, never the sender or the ask; - the escalation headline names the waiting seat, with and without a row sender, while the sender stays the row's own. - **Fail-first:** all 6 new tests fail on main. - **Existing tests that pinned the old order** (the sender last, the body at block 1, a lowercase `from`) are updated to the new order in five files. - **After the change:** all 80 Slack, gateway and delivery test files pass (1,186 tests), and daemon `tsc --noEmit` is clean. ## Docs `docs/reference/slack-app-setup.md` now says each post starts with its sender. |
||
|
|
1cb2ed90cd |
feat(gateway): the first attention subscription decides whether plain asks post to Slack (#1878)
Every ask to a person is on Steering and the TUI. Whether it also posts to Slack is the person's Slack subscription, the first attention subscription: a saved filter plus a destination on their gateway record (subscriptions: [{ destination: { connector: slack }, filter: { preset } }]).
- every-ask, the default: every ask posts to Slack, as on 0.7.0. Nothing changes on upgrade.
- urgent-only: only an ask marked --alert posts; the rest wait on Steering and the TUI (#1868's behaviour).
Set it with rig gateway human set <id> slack-subscription every-ask|urgent-only|unset; rig gateway human show prints it and whether it's authored or the default. --alert decides per item: a handoff doesn't inherit another ask's alert, and rig workflow route keeps a person's alert-worthy failed-step approval alert-worthy. The record's keys are closed and the preset must be a plain string, so a malformed entry is refused instead of silently quieting Slack. The skill, --alert help, the MCP text, --verify's not-alerted wording, the Feed and the Slack setup page use the subscription framing. A filter language, channel destinations and UI come later from the same record.
|
||
|
|
23469b94ce |
feat(queue): an ask to a person lands on Steering without a Slack alert unless it's marked alert-worthy (#1868)
An ordinary question, approval or request to a person lands on Steering and the TUI's Feed, where they can see and answer it, and nothing pings them on Slack. rig queue create --alert (and rig queue handoff --alert) marks an ask alert-worthy: something only they can do, or an approval whose wait costs now. That ask is the same item, still on Steering, and Slack posts it once. An answer closes the ask wherever it arrives. - Classification at create: human-required at RECORD for an ask on Steering, at ALERT with humanAlert: true. Nothing re-decides the level later. - Slack posts a human-required ask only at ALERT, whatever its posting dial says; the dial still governs updates and notices. - One attention membership for every reader (the Feed, ?attention=1 and the attention aggregate), independent of delivery, with rerouted asks leaving all of them. The Feed lists an ask's questions and answers, and its Slack line shows the actual receipt. - The TUI answers an ask the way its asker framed it: Approve for a plain ask; each question with its options, with no Approve while a question is open; or reply <text>. The answer is recorded once on the same row through the existing answer path and returned to the asking seat, as a Slack answer is. A completed answer set whose return was interrupted resumes that return; a retry in different words never closes on words that weren't returned. - The stuck sweep, wake ladder and waiting view treat a quiet ask as waiting on the person; a quiet handoff isn't escalated to Slack. rig queue create --verify reports not-alerted at once. - Callers that need an alert set it: host pairing, the health notify path, workflow exceptions routed to a person, and MCP rig_queue_create's optional alert. The messaging-the-human skill teaches both routes. Limits: per-person preferences beyond quiet hours aren't built, and where a mission-control bearer is configured that differs from the TUI's terminal token, the TUI's answers get the existing refusal. |
||
|
|
37b5f0242e |
fix(slack): preserve retained relays and report routing state (#1718)
With Slack forwarding between machines (`forwardChannels`), removing a forward entry could strand the relays a hub had retained, and status didn't show where a channel with no mapping goes. Now retained relays stay visible and keep retrying the host they came from after their forward entry is removed. Status names the configured local routing fallback and its destination. A disabled connector's status counts all three inbound stores and says retry is paused; an enabled machine that only receives relayed events still reads as awaiting retry. A repeated `rig slack enable` stays a no-op and now says configuration and tokens weren't reloaded, pointing to a daemon restart, which keeps unposted alerts to a person (disabling and re-enabling would mark them seen). `docs/reference/slack-app-setup.md` explains the pairing credential and says to let relays finish before editing forwarding entries. Zero retained records doesn't prove nothing is in flight. No quiesce step or channel-ownership mechanism is added. Refs #1711. Tests use real wire, router and store code with synthetic HTTP and sockets, not live Slack or two real machines. +102/-48 in 8 files: source +31/-38, tests +59/-8, docs +12/-2. Nothing that works today is refused. |
||
|
|
c4c1aa3d87 |
docs(gateway): say what quiet hours do now, in the help and the Slack guide (#1720)
`rig gateway human quiet` still said delivery didn't act on quiet hours, which has been wrong since #1606. It now says: while on, routine asks to this person are held, not posted; urgent ones (tagged escalation) still reach them; held asks post once, each on its own, when quiet ends. `docs/reference/slack-app-setup.md` gains "Quiet hours for a person (optional)", describing current behaviour only: what is held and what isn't (escalations, the operator rung, and the below-posting-level log-only case), that posts already confirmed or partly in Slack finish as themselves, and how held asks are released. Text only, no behaviour change: one help line and about 20 lines of docs. |
||
|
|
2d2132df8c |
fix(slack): persist invalid_blocks rejections per payload (#1703)
#47 reported a Slack notice silently failing with invalid_blocks when an evidence ref was a non-image https link. #89 (in 0.6.11) fixed that cause in the renderer. What remained, as the issue said, was the handling of an explicit rejection: Slack's invalid_blocks response was retried on every replay, failing the same way each time, with nothing telling the agent that sent it. An explicit invalid_blocks response (HTTP 200 with that error) now leaves a durable rejection for the exact rendered payload (channel, text, blocks and thread), so replays and restarts don't post it again. The queue item stays undelivered, and the source seat gets one notice saying the payload was refused and must change. A corrected payload is a new attempt, and a newer corrected attempt whose response was lost isn't hidden by the older rejection. Transient errors, 429s and outcomes that can't be confirmed keep their existing retry and reconciliation. In a multipart ask, parts already posted stay posted, and a partly rejected ask is never marked delivered. Thanks to @Aschenbath for the fix. |
||
|
|
df8238f5d3 |
fix(daemon): recover missed thread replies and mapped-channel messages (#1536)
## What changes - **Channels this machine owns:** - **Which ones:** the default channel and the channel map's channels. A channel the hub relays to another machine is left to that machine (the owner), so it isn't delivered twice. - **Coverage start:** a channel newly recovered starts at adoption, and older history reads as unknown. A landing or seen record isn't scanned coverage, so it never sets a floor that could skip an earlier hole. The default channel of the machine holding the Socket Mode connection keeps its existing seed. - **Replies in tracked threads:** - **Which threads:** threads whose root was posted, or which closed, within the last seven days. A root's age is its Slack timestamp, not the thread map's `opened_at`, which a map rebuild resets; so a rebuild doesn't make an old thread eligible again. An old thread that closed recently is scanned from its root, so this isn't a seven-day history cap. - **The cost:** missed replies in older open threads aren't caught up. Live delivery is unchanged. - **How they're scanned:** with `conversations.replies`, starting at the root, oldest first, 15 per call (Slack's cap for apps distributed outside the Marketplace). - **The checkpoint:** it moves past a reply once it landed, was already delivered, was kept as a dead letter, was refused because its sender isn't admitted, or was skipped as a kind inbound doesn't take. A restart resumes at the last page saved. It's kept in its own file, so a reader of channel checkpoints never meets it. - **A thread Slack can't find** (`thread_not_found`) stays incomplete, with no further call, until its mapping changes or the daemon restarts or rewires the connector. That's not taken as proof of deletion, and transient errors still retry. - **Late answers to decisions:** - **How it lands:** a recovered thread reply lands as an ordinary reply row in its thread, marked late with its original time. So does a broadcast reply recovered in a newly covered channel: a mapped channel, or a relayed machine's default channel. - **What it never does:** answer the decision, even one that's still open, and even after a dead-letter retry. The row says it wasn't applied, and the seat reads it and decides. - **Unchanged:** broadcast replies recovered in the Socket Mode machine's default channel, and every live reply, behave as before. A live envelope can't claim the mark. - **One budget per pass:** - **Limits:** four calls, 100 entries and 15 seconds per pass, as before. - **Sharing:** channels and threads take turns call by call, each kind in rotation. A kind with nothing to scan gives its turns to the other. - **Rate limits:** a `Retry-After` holds back only the method Slack limited. Every backoff saves whether it came from a 429 (`rateLimited: true` or `false`), and the method hold is rebuilt from the saved deadlines before any source runs, so it survives a restart. A future deadline with no marker, as an older daemon saved every backoff, is treated as a possible 429 and holds its method until that deadline, never extended. Channel history and thread replies stay independent. - **Machines without their own connection** (a shared app's other machines) recover a channel only when it's one they own and the hub has relayed an event for it. Its coverage starts at that event's local receipt, not the next timer pass. The adoption is saved per channel, so a restart needs no new event, and saved coverage wins over a new floor. A saved adoption never widens what the machine currently owns. A machine nothing is relayed to never reads its channels' history. - **`rig slack status`:** - **The lines:** one per additional channel, with a relayed machine's not-yet-adopted channels shown as waiting. A thread line gives the tracked, scanned and waiting counts, how far behind the oldest is, and the reasons for any thread that's backing off or incomplete. - **Counts:** accepted and dead-lettered counts cover the connector's lifetime, including threads that have since left the window. - **Docs:** `docs/reference/slack-app-setup.md` describes the new scope and its limits. ## Not changed - **Today's behavior:** the configured channel, its broadcast replies and live inbound behave as before. Relay authentication is unchanged. - **No new scope or setting:** `channels:history` already covers both methods, and there's no new setting, prompt or polling loop. - **Out of scope:** threads nobody tracks, older open threads, threads closed more than seven days ago, and history Slack no longer returns aren't recovered. - **Timing:** with many tracked threads, each one is scanned every few passes, and status says how far behind the oldest is. ## Compatibility Relay adoption (`relayAdoptedAt`) and the rate-limit marker (`rateLimited`) are optional fields in the existing channel coverage file; there's no new store. An older daemon reads the file, accepts the fields and keeps them when it saves, because its saves spread the existing record. If one is lost anyway, that channel waits for a fresh relayed event and keeps its coverage. Thread coverage (`slack-thread-coverage.json`) is new here and isn't pruned yet. **A downgrade seam, accepted as a cost:** after a downgrade, an older daemon's spread save keeps a `rateLimited: false` this version wrote. If that older daemon then gets a real 429, it replaces `nextRetryAt` but leaves the stale `false`. After upgrading again inside that `Retry-After` window, recovery trusts the `false` and doesn't restore the method-wide history hold. The affected channel still waits for its own deadline, but other channels can call `conversations.history` inside it. In an uninterrupted pass with a single recovery owner, the first of those calls that receives a 429 restores the shared hold: the steps run one at a time and share one method backoff, and a definite live 429 sets that hold before another source takes its step. That's not a promise of at most one extra call in every case; multiple recovery owners, repeated restarts, lost checkpoint writes or a response not observed as a 429 can each allow more. The pass budget (four calls, 100 entries, 15 seconds) is unchanged. This is a compatibility judgment from the source, with no new product guarantee and no measurement against Slack. ## Changed results and costs 1. **The hub's mapped channels** are recovered from their adoption. 2. **A relayed machine** recovers each configured channel the hub has relayed to it, from that channel's first relayed event's receipt, and across restarts. Main didn't recover on relayed machines. 3. **Thread replies** in threads whose Slack root, or whose close, is within seven days are recovered as late rows that never answer. This selects threads; it isn't a seven-day history cap, so a recently closed old root catches up older replies. **Cost:** missed replies in older open threads aren't caught up. Live delivery is unchanged. 4. **Recovered broadcast replies** in newly covered channels (the hub's mapped channels and every relayed-machine channel) land late and never answer. The Socket Mode machine's default channel and live replies are unchanged. 5. **The default channel's share:** when tracked threads need calls, channels get every other call and rotate among themselves. **Cost:** a default-channel backlog can drain at up to about half main's rate when both kinds are busy. That's an upper share, not a fixed slowdown, and there's no latency or global ordering promise. 6. **A history 429** on any channel holds `conversations.history` for every channel until its deadline, across a restart; a replies 429 does the same for threads. On main a 429 held only the default channel's own retry. **Cost:** an older daemon's unmarked deadline, or a `true` marker it kept after a downgrade, can represent a non-429 failure, so re-upgrading before that deadline can delay `conversations.history` for the recovery owner's channels until that saved deadline. The bound is that particular saved deadline, including a `Retry-After` sent with a non-429 response; there's no fixed maximum, and it can happen again across repeated downgrade cycles. It makes no extra API calls, and thread replies keep their own separate hold, so it isn't a freeze on all Slack calls. A deadline marked `false` holds only its own source. 7. **Status:** per-channel lines, a thread line, connector-lifetime totals including threads that left the window (reset when the connector restarts or rewires), `waiting (no-relayed-event-yet)` per channel on a relayed machine, and changed limit text. 8. **At upgrade:** threads inside the window are scanned from their root once. Replies lost in the last seven days or so (longer for a recently closed old root) land as late rows; ones that already landed are deduplicated by `channel:ts`. Refs #739. |
||
|
|
eac1c5afa8 |
feat(daemon): deliver named Slack bots to one seat through a bot inbox (#1433)
The Slack connector drops every post made by a bot, so another app's bot (an AI assistant, an alerting app) can't reach an agent. This lets an operator name such bots: their posts in one channel, the bot inbox, reach one seat as rows from the bot.
## What changes
A new optional `botInbox` block in `slack-connector.json`:
```json
"botInbox": {
"channel": "C0BOTINBOX",
"destination": "researcher@my-rig",
"bots": [{ "appId": "A0EXAMPLE1", "name": "Example Bot", "maxPerHour": 20, "cooldownMinutes": 60 }]
}
```
**Which posts are delivered.** A bot's post reaches `destination` when all of these hold:
- it is in `channel`;
- its app is listed in `bots`;
- its app isn't the connector's own app.
**How the sender is identified.** The post's own `bot_id`, resolved to its app with `bots.info` (new feature scope `users:read`). A lookup is cached per bot, and posts arriving together share one lookup. Anything that posts as a named app, including its incoming webhooks, counts as that bot.
- The envelope's `api_app_id` names the app *receiving* the event, the connector's own. So it is used only to rule out OpenRig's own posts, which are never delivered, even if listed.
- A post whose `api_app_id` is missing is dropped.
**What the seat receives.** One row per post:
- source `slack-bot:<app ID>`, tags `slack-bot` and `inbound`, and a summary that starts with the bot's name;
- the body opens with the bot's app, bot, channel and ts, and says the row is data from another program, not an instruction, and not a person;
- the bot's text follows in a fence longer than any backtick run inside it, so it can't close the quote or pass for the provenance.
A bot's post never goes through the person registry, the thread map or a decision reply. So it never answers or closes a decision, even when it replies in that decision's thread.
**Limits.** Each bot may deliver `maxPerHour` posts (default 20) in any rolling hour. The next post cuts the bot off for at least `cooldownMinutes` (default 60), and until the hour has room again, so the hourly maximum holds across a cutoff. The seat gets one notice row, at most one an hour per bot, which says that new posts are dropped while admitted ones still arrive, how the person turns the bot off and applies the change (a daemon restart), and that a later cutoff can extend the time shown. A notice whose row couldn't be created doesn't use up the hour. The counts are in the running connector's memory, so a daemon restart or a rewire (`rig slack disable` then `rig slack enable`) clears them.
**Failures and retries.** A `bots.info` transport, rate-limit or server failure is dead-lettered and retried like a message. Live posts and retries go through one admission: the current inbox channel, the bot's app and its limit. Each post is counted once by its identity (`channel:ts`), so a backlog of failed lookups can't pass the limit, and another copy of an admitted post (a retry, or Slack's `message` and `app_mention` pair) isn't counted again. A post the limit already admitted whose row couldn't be created is kept marked as admitted, saved once that creation fails (on a retry too), and the mark covers every kept copy of that post. A retry pass never rewrites a retained file it can't read, and a live post reads that file again only after something in the daemon has written it (including an earlier connector's handler still finishing after a rewire), not each time; while it can't be read, a live post is kept for retry. So it's delivered later without being counted again or dropped by a cutoff, even after its hour, a rewire or a restart; the marker can't come from Slack's payload. A retained post from a channel that is no longer the bot inbox is dropped and logged. Slack's own refusal (such as `missing_scope`) is logged and the post is not delivered.
**Config errors.** An invalid `botInbox` (for example a bot ID where the app ID belongs) turns only the bot inbox off. `rig slack status` shows a `bot-inbox` line saying why, and loading the config never fails because of it.
**With one app across machines.** When the bot inbox channel belongs to another machine, the hub relays the bot's post there untouched, like any event in that channel, and that machine's own bot inbox delivers it: the relayed envelope carries the receiving app, and the relayed post is checked as a fresh one, with any admission marker in its payload cleared. A relay retry whose file can't be read again is left for the next pass and logged, as the event and click retries are.
## Result changes against main
- **A named bot's post in the bot inbox:** main drops it (`bot_id`); now it lands on `destination`.
- **The app manifest** asks for one more feature scope, `users:read`.
- **Readiness:** `rig slack status` gains a `bot-inbox` line when a `botInbox` block is set.
- **Unchanged:**
- without `botInbox`, every bot post is dropped exactly as before;
- other bots, and named bots outside the inbox channel, are dropped as before;
- people's messages, including people writing in the bot inbox channel, take the existing path.
## Limits
- **Edits and deletions are not delivered.** A bot that writes its answer by editing one message delivers only that message's first version.
- **Attachments on a bot's post** are named in the row, not copied.
- **`rig slack verify`** checks neither `users:read` nor membership in the bot inbox channel.
- **Missed-message recovery** doesn't cover the bot inbox.
- **No reply route yet:** the seat can't answer the bot; the person asks it in Slack.
## Tests
`packages/daemon/test/slack-named-bots.test.ts`, committed before the change. CI is linked in the PR conversation.
- **Through `handleEnvelope` to the queue row:**
- a named bot delivered as the bot;
- a bot reply in a decision's thread doesn't call the decision resolver, while a person's same reply does;
- an unnamed bot dropped and its app ID logged;
- OpenRig's own app dropped even when listed;
- a missing receiving app dropped;
- `bot_profile` and the envelope's app ignored as identity;
- named bots outside the inbox dropped without a lookup;
- no `botInbox` means every bot is dropped;
- edits dropped;
- a `message` and `app_mention` pair landing once, with one lookup;
- a refused lookup not retried;
- a transport failure dead-lettered and landed on retry;
- the cut-off, its single notice and the return after the cooldown;
- an admitted post kept past its hour through a cutoff, and across a rewire with the new hour already used, then delivered;
- an admission granted on a retry that stops before the pass ends, and an unadmitted plus an admitted copy of one post across a rewire;
- an unreadable final re-read after the marked copy is saved, a live post while the retained file can't be read, and one file read for several live posts. These run on an in-memory file adapter, not a daemon kill or a real disk failure;
- a notice that couldn't be created leaving the hour's notice free;
- a person's message in the inbox channel staying on the person path.
- **Also:** the rolling hour, the config checks and readiness row, loading an invalid block, the `users:read` scope, and the `bots.info` request and its retry classification.
## Also changed against the shared-app relay
- A relay retry pass whose final re-read of its retained file fails now leaves that file untouched, through the same shared batch replacement the bot and event passes use, rather than replacing it from a lenient read. Envelopes already sent may be retried and rely on the receiver's dedupe: a possible duplicate, not a loss.
- The named-bot and relay suites are separate; no single test composes a named bot through a two-machine relay, and real Slack across two machines was not run.
|
||
|
|
f557667c51 |
feat(daemon): one Slack app across machines, routed by channel (#1436)
One OpenRig Slack app can now serve several machines, routed by channel. A message in a machine's channel reaches that machine's Kernel Operator; a message in a rig's channel reaches that rig's representative or a seat you name; and the answer and later updates come back to the same Slack thread, naming the machine, rig and agent. Refs #192 ## How it works A Slack app delivers each event to one of its connections, so one machine, the hub, holds the app's Socket Mode connection. Every machine keeps its own gateway: its human registry, thread map, decisions, dedupe and receipts stay local, and it posts its own replies with the app's bot token. **The hub relays.** - For a channel another machine owns (`forwardChannels: [{ channel, host }]` in `slack-connector.json`), the hub relays the event, reaction or button click untouched to that machine's daemon, over its http host entry (`rig host pair`), with the token that entry holds. - A failed relay is kept with its host and retried on the dead-letter cadence, after the hub's own kept events and clicks. After one failed send to a host, its other relays wait for the next pass. A relay that keeps failing is retried with no limit, since dropping it would lose a person's message. `rig slack status` counts it in the backlog. **Machines without the app-level token.** - They open no connection. Their connector builds its router anyway, and the new `POST /api/gateway/slack/forwarded` runs a relayed envelope through it. - That route checks the daemon's auth bearer when one is set, and never relays an envelope again. It trusts its caller for where an event came from: there's no Slack signature on a relayed event, so the sender lookup there isn't proof of a fresh act in Slack. - `rig slack status` reports `relayedInbound`: that the machine can take relayed events, and how many have arrived. - These machines drain their own retained events on the socket's five-minute cadence. **Answers return to the person's thread.** - A person's top-level message becomes the root of its answer: a thread mapping at its own channel and ts, marked as the person's, and stamped on their row so a rebuild restores it. The row is tagged `slack-thread-root`. A reaction on that message isn't a reaction to an ask. - When the seat it reached replies with `--reply-to` that row, the answer and later updates post under the person's message, in the channel they used, even if that seat's items otherwise post to another channel. A reply that fails the root's checks (another seat, another person, a closed root) keeps the replying seat's own channel. - Roots OpenRig posts keep the channel map's remap rule. - A button click finds its decision by its channel (from the payload or its container) and ts, and whether an update already shares a root is judged for that root: by its channel, or, for an older record that names no channel, by the root the update's post actually landed in (its channel and ts); when that isn't recorded, it counts as shared, as on `main`. **Channel routing.** - A person's new message in a rig's channel reaches the rig's representative, its one `delegates_to` root, at the root's current session. A new `inbound` field on a channel-map entry names another seat: `rig slack channel-map set my-rig C0... --inbound lead@my-rig`. - Everything else goes to `inboundDestination` as before. That covers the default channel (a rig entry there only sends that rig's posts there, unless it names `inbound`), a seat entry's channel, an ambiguous channel, and a rig whose representative can't be found. A seat entry alone doesn't make its channel a seat's inbox. **Every post names its machine:** `from <seat> on <host id>`, unless the sender's session already carries a host. ## Changed results against main 1. **A reply to a top-level message threads under it.** A person's top-level message now mints a thread root, so a `--reply-to` answer from the seat it reached posts in that thread instead of top-level (`root-missing`). An answer from another seat still posts top-level, now as `root-other-seat`. 2. **Rig channels route to the rig.** A new message in a channel mapped to a rig goes to the rig's representative when one is found, where main sent every new message to `inboundDestination`. 3. **Posts name the machine.** The sender line reads `from <seat> on <host id>` when the daemon has a host id. 4. **`inbound` is a known channel-map field.** It was an unsupported newer field, so the tests that used it as their example now use a hypothetical `policy` field. 5. **Thread roots are keyed by channel and ts.** Migration 115 rebuilds `thread_seat_map` with that key and an `origin` (posted or person); existing roots are kept as posted. Two roots with one ts in different channels no longer share a row. 6. **A fallback names the root's owner first.** When a `--reply-to` reference belongs to another seat or person and also sits in another channel, the recorded reason is `root-other-seat` or `root-other-human`, not `root-other-channel`. 7. **New, only when configured:** `forwardChannels` on a hub, a `forward-channels` readiness line, the relay route, and a connector running with only the bot token, which builds its router without a socket and reports `relayedInbound`. ## Upgrade note An existing channel-map entry for a rig, on that rig's own channel, now takes the channel's new messages for the rig's representative. A rig entry on the connector's default channel doesn't move that channel's new messages. ## Limits - **The bot token goes on every participating machine,** and the app-level token on the hub only. - **Hub coverage:** `rig slack verify` on the hub doesn't check membership in relayed channels, and missed-message recovery covers the hub's default channel only. - **Handoffs:** an answer from a seat other than the one the message reached still posts top-level. - **Relayed events** are trusted from the daemon caller, and a relay a machine keeps refusing is retried with no limit. - **An update posted through the delivery digest** records no thread part, so for an older record that names no channel its root counts as shared, as on `main`. - **The thread-part lookup** for those older records is an unindexed scan of `thread_part_map`, run only when a decision with its own open root posts. ## What the tests cover, and at what layer - `slack-shared-foundation-wire.test.ts` runs two machines' wires in one process with Slack faked. The hub's fetch stub hands a relayed envelope straight to the other machine's wire (`acceptForwarded`); it doesn't go through the HTTP route or its middleware, and both machines share one host id there. It covers: a machine channel reaching that machine's Kernel Operator, with the answer and a later update in the person's thread naming the machine; a rig channel reaching its configured seat, a changed seat taking the next message, and the answer returning to the person's channel; the hub's own channel staying on the hub; an unregistered sender still refused; and a machine without its own connection reporting relayed events. - `slack-shared-foundation.test.ts` covers the receiving route and its bearer separately, with a stub subsystem, plus the representative, resolver and router; minting; the relay, its retry, its loop guard, and messages, reactions and clicks; the forwarder over the registry; the config checks, the machine label and `channel-map set --inbound`; a rig entry on the default channel; and the retry order. - `slack-reply-to-thread.test.ts` and `slack-thread-routing.test.ts` cover roots per channel, a reaction on a person's own message, a failed reference keeping the replying seat's channel, the person's root stamp and its rebuild, a click that names its channel only in its container, the migration keeping existing roots, and an older record's shared root judged by where its update's post landed, including a retained replay into another channel and a post that never landed. The migration's upgrade test is synthetic (one closed root in an in-memory database). - Real Slack on two real machines was not run. |
||
|
|
0915430b0d |
docs(queue): say --summary is the row's subject and carry the queue rules in the shipped skills (#1354)
Teach `--summary` as every queue row's subject, and carry the queue rules in the shipped skills. - `rig queue create`, `handoff` and `handoff-and-complete` help, and the missing-summary warning, describe `--summary` as the subject shown on RECENT, the story and the needs-you view. - `queue-handoff` gains an "Every queue row" section: `--summary`, the `project:`/`mission:`/`slice:` tags, `handoff-and-complete`, closing what you hold. A handoff keeps the source row's tags unless `--tags` or `--gate` is passed, either of which replaces them, so repeat the scope tags in `--tags` with `--gate`. A repository test pins that rule for both verbs. - `mission-slice-sop` gains a "Records" section that points to `docs/reference/sdlc-conventions.md` and keeps one rule it lacks: editing judged evidence turns the judgment `unknown`. - `sdlc-conventions.md` A2 gains a paragraph on `stage:` and `verified:`. - Existing `rig queue` examples in skills, starter cultures, the factory craft file and the capability map gain `--summary`, and the scope tags for project work. Text only, apart from the new tests. |
||
|
|
eb6bc2574d |
docs(slack): clarify manifest YAML pasting and creation steps (#158) (#1207)
When Slack's create-from-manifest dialog opens from `rig slack manifest --url` without the manifest, the person can paste the plain `rig slack manifest` output into Slack's YAML tab instead. `rig slack manifest --help` now says so, and the Slack setup guide adds that fallback to step 1. Step 2 now says that if Create stays disabled, go Back and check that a development workspace is selected and the manifest step shows no error, which matches Slack's documented flow (workspace picked before Next) without claiming a cause. Help text and docs only; command output is unchanged. A test checks the new help line. Refs #158. |
||
|
|
dbc8abe607 |
fix(slack): recover broadcast replies after connection gaps (#1187)
Live inbound already accepted Slack thread replies sent with "Also send to channel" (thread_broadcast), but channel recovery skipped every threaded message and checkpointed past them, so a broadcast reply sent during a connection gap was never delivered. Recovery now admits broadcast replies from the configured default channel's history. They take the existing path: sender admission, bot and subtype rejection, the fixed duplicate identity, exact thread routing and the durable retry, with the usual late-delivery marker. A reply seen live and again in recovery lands once. Ordinary (non-broadcast) thread replies and other channels stay outside recovery. The setup reference and the recovery status text describe the new scope. Refs #739, which stays open for non-broadcast thread replies. |
||
|
|
9d0be982b0 |
fix(slack): tell the person in the thread when a decision click isn't recorded (#1067)
When a Slack decision click can't be recorded, the person gets a short reply in the thread saying so and what to do, instead of silence. Click handling is unchanged, and a replayed click confirms the stored answer once. |
||
|
|
a29dfbcf9e |
fix(slack): keep typed answers on structured decisions (#1045)
A typed Slack reply to a structured decision is kept as the person's own words. It's stored as an object marked as a typed reply, holding the text, how it was placed and how many questions were left, while a click stays a plain option id, so typed text can never read as a clicked option. The formatter labels typed answers honestly, and the write stays one transaction with no double fill on redelivery. Refs #1037 |
||
|
|
7d177fa12d |
fix(slack): report the durable inbound dead-letter backlog in status (#1036)
rig slack status reports the durable inbound dead-letter backlog: the inbound Slack events kept for retry after a processing failure. When the dead-letter file can't be read, the plain output says unknown, and --json and HTTP report a null count, so an unreadable file never reads as an empty backlog. Read-only; no delivery path changes. |
||
|
|
aa126c74d5 |
docs: bring the as-built and reference docs to the 0.6.8 cut (#1041)
The README, reference and as-built documents are refreshed to the 0.6.8 cut. |
||
|
|
1af1b2abd4 |
feat: an optional Slack channel map for outbound posts (#192, part 1) (#944)
An optional Slack channel map routes outbound posts per channel, with channel-map and verify commands; part 1 of #192 (outbound only). |
||
|
|
3adf7718d2 |
feat(daemon): a reaction on any part of a long ask reaches the seat that asked (#947)
A reaction on any part of a long ask reaches the seat that asked, and a one-message ask that already landed is acknowledged instead of refused. Refs #899 |
||
|
|
475ccabeca |
feat(daemon): deliver a Slack reaction on an ask to the seat that asked (#931)
A registered person's reaction on an ask's own Slack message reaches the seat that asked, once, without answering it. |
||
|
|
90cae3a570 |
docs: drop old team names and "queue row" from shipped text (#935)
CLI help examples and three reference pages use current team names and plain terms instead of retired names and "queue row". |
||
|
|
51bd931ed6 |
docs: bring the reference docs to the 0.6.6 cut (#903)
Brings the README and the reference docs to the 0.6.6 release commit
|
||
|
|
ca208a692f |
docs(reference): correct claims from the post-merge checks of #783 and #785 (#795)
Five corrections from after-merge source checks of the operations and process references, each re-read at
|
||
|
|
6ce7ec2089 |
docs(reference): refresh the operations references against main (#783)
Seven operations references re-verified against main for 0.6.6. Text only.
- browser-access: the web UI works over http on the daemon port at any accepted address, including the Tailscale name; OPENRIG_ALLOWED_ORIGINS is needed only for a UI served another way, such as https through your own domain.
- slack-app-setup: invite the bot before rig slack verify, and register a human with rig gateway human add before rig slack enable, which refuses without one; verify warns on missing feature scopes.
- health-diagnosis, host-resources, inventory-request-diagnostics, stream-classifier-worker, telemetry: fields, bounds and stop conditions corrected to match the code.
Commands and flags checked against the help inventory of a clean build of
|
||
|
|
6525f5f02f |
Recover Slack channel messages after connection gaps and report scan coverage (#742)
Slack messages posted while the connector was disconnected could stay in channel history without reaching OpenRig. On reconnect and on the existing periodic retry, the daemon now scans available top-level messages in the configured channel and passes them through the existing admission, routing, attachment and queue path. Recovered rows say they arrived late and carry the original posting time. A durable per-channel checkpoint keeps unfinished pagination across restarts. Live events cannot advance it; a page advances only after each message lands, is confirmed as a duplicate, receives a policy disposition or reaches dead-letter custody. An upgraded install starts once from the newest retained channel landing, with older history reported as unknown; without landing evidence, coverage starts at feature adoption. Slack plan limits qualify older history without stopping delivery of the available page, and that limitation stays visible in status after restart. New scan intervals end five seconds before the local clock so recent posts can become visible; transport and server failures become eligible for retry after five seconds, and an explicit Retry-After is honored. rig slack status keeps its local configuration output and adds bounded daemon observations: socket state, scanned and pending interval, recovery limits, retry time and per-connector counts, with ISO times for people and exact Slack timestamps in JSON. It calls no Slack API. No new dependency, migration, setup step or outbound message. Thread-reply catch-up and dead-connection detection remain open on the issue. Refs #739. |
||
|
|
7ca2724101 |
feat(slack): clickable questions on human decisions (#193) (#195)
* feat(slack): structured questions with clickable options on human decisions (#193) - `rig queue create --human-questions-file`: 1-4 questions, 2-4 options each (one may be recommended), validated at create; stored by migration 091. - Slack renders each question as a button row with a complete text fallback; a typed thread reply still answers ("Other"). - A block_actions click records that answer on the item; once every question is answered the answers are final, one reply row lands on the seat and the decision resolves like a typed reply. Failed hand-backs are dead-lettered and retried; each click is confirmed in the thread. - App manifest turns on interactivity (Socket Mode, no request URL); setup doc and messaging-the-human skill updated. * fix(slack): own-key answer lookup; require a human destination for questions(#193) * fix(slack): redact secrets in answer acknowledgements (#193) |
||
|
|
57380a250e |
Release0.6.0: works for strangers (#109)
* chore(release): prepare OpenRig 0.6.0 Add per-seat permission selection and typing protection, rig-wide Herdr entry, Node 22/24 support with SQLite 13, and first-use improvements. Label the optional Jev/Rig Stream classifier and Slack manifest setup assistance experimental. * docs: finalize 0.6.0 verification scope and release references Record the accepted Node 22 candidate install, retained Node 24 evidence, and unperformed native permission checks without broadening test claims. --------- Co-authored-by: dev-qa <[email protected]> |