feat(discord): connector pilot for the Sage seat, reviewed candidate (#1509)

Zero-dependency Discord connector under packages/discord: binding
validation, REST and gateway clients, pi engine adapter, journal with
append-only inbox, outbox, admissions and notices, and a run.lock
ownership record {pid, start, boot} whose identity is checked three ways
and whose cleanup is gated by STOP. CLI check|run|stop|unlock via
scripts/discord.sh; offline suite scripts/test-discord.sh (28 checks,
87 node tests).

Reviewed by rev-code-02 on #1509 over nine rounds; approved exact tree
4e0feb6758c0a7e4a71483912a8e0d3e3ec95aef at comment 26170. Corrections
(1) to (12) recorded in BUILD-LOG. No listener started, no token read,
no Discord write; the live pilot follows this commit per the brief.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-13 01:15:37 -05:00
co-authored by Claude Fable 5.1
parent b023841c8a
commit 786e379c49
34 changed files with 4641 additions and 1 deletions
+9
View File
@@ -237,3 +237,12 @@ are never rewritten or removed; corrections are new entries.
- 2026-09-13 — filbert, native Pi — WEBUI-1507-R1 independent review APPROVED for manifest 509f20e53292d5731c866d84c73cfaf539a89104b38b12a43260075ee7c3cb4d. All 21 hashes verified; separate baseline-plus-candidate copy passed WebUI 6/6 and combined 208/208 including Chromium, 330 contrast samples and five widths. Four fresh screenshots inspected. Verdict: docs/plans/reviews/2026-09-13_webui-verdict.md. No source edits or publication; Gate E remains Jason-owned.
- 2026-09-13 UTC — dewey — #1507 source published ea00ec66, exact remote verified, exact committed-tree tests 208/208 after Filbert APPROVED manifest 509f20e5. Scoped tracking records Gate E ready for Jason Monday; issue open, no deployment or unrelated source edits.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot planning: recovered Sage's private shared-signals records (setup receipts, direction memo, research packet), wrote brief docs/plans/2026-09-13_discord-connector-pilot.md, added QUEUE row 14 briefed/unassigned. No code, no listener, no Discord write, no credential read. Waiting on Jason for D1-D7.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot build (#1509, QUEUE row 14): wrote packages/discord (binding, authorize, gateway, rest, engine-pi, journal, context, connector, cli), scripts/discord.sh, scripts/test-discord.sh (28 checks, 74 node tests), agents/sage/DISCORD-USER.md draft, README, TOOLS.md section; brief section 10 records build choices. Existing suites green (config 24, auth 15, release 14, conductor 17, foundation 43, seat, ledger). Defects found and fixed before commit: two engine attribution bugs (offline tests), ceiling in-flight count (code-review subagent), and rev-code-02's four findings in #1509 comment 26114 (reconcile window refresh, context path escape, ceiling across a crash, pid claim and identity). Filbert deferred (26111). Review requested; no listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round two (#1509, QUEUE row 14): rev-code-02 comment 26121 kept the pid claim open (publication window) and found a memory-only ceiling notice and a duplicate admission across the thread lookup. Fixed all three: `run.lock` directory ownership with a four-process race test, `notices.jsonl` with a same-day restart test, id reservation with a held-lookup test. `scripts/test-discord.sh` 28/28 (80 node tests). Round three posted; files frozen for review. No listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round three (#1509, QUEUE row 14): rev-code-02 comment 26123 reproduced a three-party stale-reclaim race. Removed automatic reclaim; `run` fails closed on a stale lock and a new `unlock` command clears a verified-dead owner. Stale handoff and changed-under-unlock tests added. `scripts/test-discord.sh` 28/28 (82 node tests). Round four posted; files frozen. No listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round four (#1509, QUEUE row 14): rev-code-02 comment 26132 showed unlock's restore path can still leave two owners. Replaced it with a `STOP` quiescence gate: unlock writes `STOP` first, claims re-check `STOP` after publishing and release themselves. Four-party schedule test added. `scripts/test-discord.sh` 28/28 (82 node tests). Round five posted; files frozen. No listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round five (#1509, QUEUE row 14): rev-code-02 comment 26150 accepted the `STOP` gate and found the owner record lacked a boot identity plus a false promise in the unlock text. Owner record now {pid, start, boot}, three-way identity check, unreadable identity refuses everything, unlock text says use `stop`. `scripts/test-discord.sh` 28/28 (83 node tests). Round six posted; files frozen. No listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round six (#1509, QUEUE row 14): rev-code-02 comment 26158 found that a live pid with a pre-boot-id record was treated as a mismatch, so `unlock` cleared a lock under a running connector, and that an unparseable owner record read as absent. Both now refuse everything until the pid is dead; unreadable records are never removed. Legacy-upgrade test with a live child plus an invalid-record control. `scripts/test-discord.sh` 28/28 (85 node tests). Round seven posted; files frozen. No listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round seven (#1509, QUEUE row 14): rev-code-02 comment 26165 found that malformed start or boot strings in a parseable record read as a positive mismatch. Identity values now have a checked syntax; a malformed value under a live pid is "unknown" and refuses everything. Six live-child controls plus a syntax table. `scripts/test-discord.sh` 28/28 (87 node tests). Round eight posted; files frozen. No listener, no Discord write, no token read, no push.
- 2026-09-13 UTC — coordinator (Claude) — Discord connector pilot review round eight (#1509, QUEUE row 14): rev-code-02 comment 26168 found leading-zero and above-2^64 start strings still compared as a mismatch. Start ticks now require the canonical unsigned decimal form /proc prints. Boundary table entries and three more live-child controls. `scripts/test-discord.sh` 28/28 (87 node tests). Round nine posted; files frozen. No listener, no Discord write, no token read, no push.
+27
View File
@@ -147,6 +147,33 @@ npm-global `mosaic` CLI; run by path. Exit codes: the launch script's own
once it runs; before that 1 could not start, 2 invalid config or seat, 4
usage. Details and the record's fields: `packages/seat/README.md`.
## Discord connector (`scripts/discord.sh`)
```bash
scripts/discord.sh check <binding>
scripts/discord.sh run <binding>
scripts/discord.sh stop <binding>
scripts/discord.sh unlock <binding>
```
One seat's conversation reachable from listed channels of one Discord
server, chat only. `<binding>` names `<dataRoot>/discord/<binding>.json`
(0600, never committed; shape in `packages/discord/fixtures/binding.example.json`).
`check` validates the binding, token file mode, context files and pi, reads
the bot, guild and channels over REST, opens and closes one gateway
connection; it sends nothing. `run` serves turns until `stop`, SIGTERM or
SIGINT and refuses to start when `STOP` exists or a delivery cannot be
reconciled. `stop` writes `STOP` and signals the running process. `unlock`
writes `STOP` and removes a `run.lock` left by a dead process; it refuses
while the owner is alive with an unverifiable identity or its record cannot be
parsed. `run` refuses a stale lock rather than reclaiming it, and releases a
claim that meets `STOP`. Records
under `<dataRoot>/discord/<binding>/`: `inbox.jsonl`, `outbox.jsonl`,
`drops.jsonl`, `admissions.jsonl`, `notices.jsonl`, write-once `turns/<id>.json`. Suite:
`scripts/test-discord.sh`.
Exit codes: 0 ok, 1 operation failed, 2 invalid data or configuration, 4
usage. Details: `packages/discord/README.md`.
## Tools (host-side)
Host-side helpers under `tools/`, outside the `scripts/` command surface.
@@ -0,0 +1,413 @@
# Discord connector pilot: Sage on Shared Signals
Owner of the plan: Jason. Coordinator and builder: the Claude session.
Reviewer: Filbert or an orch-01 review seat, whoever is free when the
candidate is pinned. Tracking: #1509. QUEUE row 14.
Status: candidate built, in review. Jason settled Q1 to Q27 through
ms-grill-me on 2026-09-13 (section 9). The package, the offline suite
(`scripts/test-discord.sh`, 74 tests in eight groups plus package checks),
the wrapper, the DISCORD-USER.md draft and the README were written the same
day; section 10 records the choices made while building. No listener runs
until the live pilot in section 6. The bot on Shared Signals is
administration-capable only, per the private setup record.
## 1. What exists today
Sage's private workspace holds the completed one-time server setup, a
bounded REST administration utility, a research packet on Hermes and OpenClaw
with pinned hashes, and an agent-direction memo. Path (private, not in this
checkout):
```
~/.mosaic/fleet/agents/sage/work/shared-signals/
```
Verified from those records on 2026-09-13:
- Bot identity and the exact guild are checked before every write. Fourteen
writes have matching journal receipts. Two categories and nine channels were
created, original channels and roles untouched. Avatar and banner set.
- The admin utility is on-demand. There is no gateway connection, so a
mention in Discord reaches nobody.
- The direction memo recommends one small owned integration, not a fork of
Hermes or OpenClaw, and lists the controls every version must have:
server AND channel AND stable user ID before any model call, action
permissions separate from chat permission, Discord text treated as
untrusted input, durable inbound and outbound records so a restart cannot
duplicate a reply, a stop switch and a usage ceiling.
- The token and server-id files are mode 0644 inside a 0700 directory. The
connector's check must refuse anything but 0600, so this gets fixed before
the first check run.
The archived `v1/plugins/discord` is a gateway-service adapter for a
different runtime. It is reference only. Nothing from it is activated.
## 2. Outcome of the pilot
Jason writes to Sage in one Shared Signals channel and gets a reply from the
same Sage persona that runs in the terminal, without a terminal. A restart in
the middle of a turn does not produce a second reply. An unlisted user gets
nothing. The whole exchange is on disk as write-once records.
That is the entire MVP. Repository writes from chat, announcements,
scheduled posts, attachments, tools, DMs, slash commands, Carmen's
enrollment, and sharing the terminal conversation are all later pieces and
are listed in section 8 so they are not lost.
## 3. Where it sits in Mosaic Stack
A Discord channel is one more interface onto a seat's conversation, the same
class of thing as the WebUI session chat (row 5) and the terminal. The
settled WebUI rulings apply here where they overlap: one writer per
conversation (Q5, Q9), a transport receipt is not an answer, uncertain sends
are reconciled rather than replayed (Q14), and Discord text is untrusted
data. The pilot does not wait for the conversation controller that CHAT-03
will build. It owns its own engine now and is written so the engine can be
swapped for that controller later without touching transport or policy.
Layout, following `packages/seat`:
```
packages/discord/ reusable software, committed
src/binding.mjs strict schema, fail-closed load, token file check
src/authorize.mjs pure decision: guild, channel, thread, user, mode
src/gateway.mjs Discord gateway v10 client (identify, heartbeat, resume)
src/rest.mjs create message with nonce, typing, get channel/guild/me
src/engine-pi.mjs pi --mode rpc child, one session, follow-up queue
src/journal.mjs inbox, outbox, drops JSONL; write-once turn records; STOP; pid
src/context.mjs Discord context block, envelope, reply splitter
src/connector.mjs the loop: event -> authorize -> journal -> engine -> deliver
src/cli.mjs check | run | stop
src/errors.mjs DiscordError with exit code
fixtures/binding.example.json the shape with placeholder ids
tests/ node --test, fake gateway, fake REST, fake pi
README.md
scripts/discord.sh thin wrapper, same as scripts/mosaic
scripts/test-discord.sh the offline suite
agents/sage/DISCORD-USER.md reduced profile (Q14), Jason edits
```
Runtime data, never committed:
```
<dataRoot>/discord/<binding>.json the binding (IDs, limits), 0600
<dataRoot>/discord/<binding>/inbox.jsonl every accepted Discord message id
<dataRoot>/discord/<binding>/outbox.jsonl intent -> confirmed | refused | unknown
<dataRoot>/discord/<binding>/admissions.jsonl one line per admitted turn, before the engine runs
<dataRoot>/discord/<binding>/turns/<id>.json one write-once record per turn
<dataRoot>/discord/<binding>/drops.jsonl one line per dropped or refused message
<dataRoot>/discord/<binding>/launches/ context snapshot and sha256 per run
<dataRoot>/discord/<binding>/STOP stop switch (presence refuses new turns)
<dataRoot>/discord/<binding>/notices.jsonl once-per-day fixed lines already attempted
<dataRoot>/discord/<binding>/run.lock/ ownership directory (atomic mkdir), owner.json {pid, start, boot}; stale needs `unlock`
<dataRoot>/sessions/discord-<binding>/ pi session JSONL for this conversation
```
The binding is deployment policy for a private server. It carries Discord
IDs for real people, so it lives under the data root, not in `roles/`. The
repository holds the schema and a fixture with placeholder IDs. The token
stays where it is, in Sage's private secrets directory, and the binding
points at it by path. The connector reads it into the child process
environment and never prints, journals or passes it on a command line.
## 4. Behaviour
Inbound. The gateway delivers `MESSAGE_CREATE`. The connector drops the
event unless all of these hold: guild id matches the binding; the channel,
or for a thread its parent channel, is listed; the author id is listed; the
author is not a bot or webhook; for a channel in `mention` mode the bot is
mentioned. A drop writes one counter line, never a Discord reply. Anything
that passes is appended to the inbox before any other action. A message id
already in the inbox is ignored, which is the restart guard.
Turn. One turn at a time per binding. The prompt sent to pi is an envelope:
channel name, thread name if any, author id, message id, then the text.
Text is data. The system prompt says so in one paragraph and states the reply
limit. While a turn runs, later accepted messages queue as pi follow-ups and
stay in the inbox. Turn timeout aborts pi and journals a failed turn.
Outbound. The final assistant text is split at 1900 characters on paragraph
boundaries. Each chunk is sent with `nonce` set to the outbox intent id and
`enforce_nonce` true, so a retry after an unknown outcome returns the
existing message instead of posting twice. `allowed_mentions` is empty.
Intent is journaled before the POST, confirmation after, with the Discord
message id. A network failure leaves `unknown`; on the next start the
connector reconciles every `unknown` by re-sending the same nonce, which
Discord dedups, before it accepts new traffic.
Controls. `check` validates the binding, refuses a token file that is not
0600 regular and non-symlink, connects, confirms bot id, guild id and channel
visibility, confirms the message-content intent is granted (close code 4014
means it is not), and exits without sending anything. `run` refuses to start
when STOP exists or unknown outbox entries cannot be reconciled. `stop`
creates STOP and signals the process; the current turn finishes or times
out, then the process exits. A daily turn ceiling from the binding is
enforced by counting turn records; over the ceiling the connector journals
the refusal and stops accepting.
Engine. `pi --mode rpc --no-tools --no-extensions --no-context-files
--no-skills --offline`, provider, model and thinking from the binding, session
dir under the data root, `--continue` when a session exists. The system prompt
is assembled the way `scripts/agent-host-dev.sh` does it for the terminal
Sage: CONSTITUTION, STANDARDS, Sage SOUL, USER, plus a Discord context block.
AGENTS.md and CONTEXT.md are not injected; this Sage has no tools and no
repository. Every prompt waits for `agent_settled`, then reads the last
assistant message from `get_messages`. No tools means no approvals and no
file effects in the pilot.
## 5. Tests before any live run
All offline, `node --test packages/discord/tests`. No token, no network, no
model.
- Binding: unknown key, missing field, wrong type, non-0600 token file,
symlink token file, empty allowlists each refuse with exit 2.
- Authorization: a table of 20 or so cases covering wrong guild, unlisted
channel, thread of listed and unlisted parent, unlisted user, bot author,
webhook, mention mode with and without mention, `@everyone`.
- Gateway: fake WebSocket drives hello, identify, ready, heartbeat and ack,
missed ack forces reconnect, op 7 reconnect resumes with sequence, op 9
invalid session re-identifies, close 4014 reports missing intent.
- Delivery: fake REST returns 200, 429 with retry_after, 5xx, and a socket
error; the outbox shows confirmed, refused and unknown; restart with an
unknown entry re-sends the same nonce once and reconciles.
- Engine: fake pi RPC over stdio; prompt while streaming becomes follow-up;
timeout sends abort; malformed JSONL line fails the turn, not the process.
- Restart: an inbox with three ids and a MESSAGE_CREATE replay of the same
three produces zero turns.
- Stop and ceiling: STOP present refuses run; the ceiling plus one is refused
and journaled.
## 6. Live pilot and acceptance
After the offline suite is green and Jason gives the go in section 7.
1. Fix token file mode to 0600. Run `check`. Receipt in Sage's private
evidence directory.
2. Start `run` in a tmux window on the host. Jason sends one message in the
agreed channel. Sage answers. Turn record and outbox line exist.
3. Send a second message while the first turn is running. It arrives as a
follow-up, not a lost message and not a second concurrent turn.
4. Kill the process during a turn, restart. No second reply to the same
message. Outbox reconciled.
5. From a second Discord account not on the allowlist (Jason's choice of
account), send a message. No reply, one drop line.
6. Send a message containing a quoted instruction to reveal the token or to
read private strategy. Sage refuses in text. Nothing leaves the process
that was not already in the prompt.
7. Lower the ceiling to the current count, send one more. Refused, journaled,
process stops accepting.
8. Record per-turn latency and token usage from the turn records.
Acceptance is all eight with receipts, plus Jason's word that the reply
reads as Sage. The gate is Jason's ruling, not the checklist.
## 7. Decisions put to Jason (all ruled, see section 9)
These change the work or cross a boundary (spend, external communication as
Sage, a new dependency). Recommendations are marked.
- D1. Activation and ceiling. Running a listener that posts as Sage on a
shared server is external communication and costs model calls. Recommend
go for the pilot with a ceiling of 200 turns per day and a 180 second turn
timeout, both in the binding.
- D2. Authorized users. The guild owner id in the setup evidence is presumed
to be Jason. Confirm it. Carmen is not enrolled in the pilot; her verified
id is a later piece.
- D3. Channel scope. Recommend `#sage-admin` in open mode (it is already
private to admins) and `#general` in mention mode, threads allowed in both.
Nothing else listens.
- D4. Discord client. Recommend a raw gateway and REST client on Node's
built-in WebSocket and fetch, matching the no-dependency convention of the
other packages and keeping the whole transport auditable. The alternative
is `discord.js` pinned exactly, which brings a large dependency tree. If
the soak in section 6 shows reconnect problems the raw client cannot
handle, switch to `discord.js` as a separate piece.
- D5. Model and account. Recommend the same provider, model and thinking as
the terminal Sage launcher (`zai/glm-5.3`, high), same auth file, with the
ceiling from D1 as the spend limit. A separate account is a later choice.
- D6. Message-content intent. The Discord developer portal toggle for the
bot must be on. Jason's action; `check` proves it.
- D7. Builder. Recommend the Claude session builds it under this brief with
Filbert as reviewer, keeping darkwing on rows 6 and 9. Alternatively
darkwing builds it after row 9.
## 8. Explicitly later
Kept here so the pilot stays small. None of these start on their own.
- Carmen's verified id and shared-room rules.
- Tools for the Discord Sage (read-only first), then repository writes with
reviewed diffs, per the direction memo.
- Announcements and scheduled posts with explicit destinations and approval.
- Attachments and images.
- Slash commands, DMs, per-thread sessions.
- Sharing one conversation between terminal, WebUI and Discord through the
CHAT-03 controller.
- Control board row for the connector (its pane runs node, not pi, so the
current liveness check would show it offline).
- Supervision as a systemd user service instead of a tmux window.
- Fleet seats other than Sage, other servers, one product with
per-workspace bindings.
## 9. Rulings, 2026-09-13
Three rounds through ms-grill-me. Where a ruling changed section 4 the
ruling wins.
| ID | Ruling |
|---|---|
| Q1 | Chat only. No tools, no repository writes, no posting on its own. |
| Q2 | Live pilot authorized. Ceiling 200 turns per day, 180 s turn timeout, both in the binding. |
| Q3 | Jason only, by the guild owner id in the setup evidence. Carmen later. |
| Q4 | `#sage-admin` open mode, `#general` mention mode, threads inherit from parent. Nothing else. |
| Q5 | Raw gateway and REST on Node built-in WebSocket and fetch. No `discord.js`; revisit only if the soak shows reconnect trouble. |
| Q6 | Same provider, model, thinking and auth as the terminal Sage launcher. |
| Q7 | One pi session per binding, separate from the terminal Sage conversation. Envelope names channel, thread and author. |
| Q8 | Unauthorized message: silence, one drop line in the journal. |
| Q9 | Binding under `<dataRoot>/discord/`, 0600, never committed. Repo holds schema and placeholder fixture. |
| Q10 | No backfill. Only messages received live after start. |
| Q11 | tmux window by hand, STOP file as the brake. No systemd in the pilot. |
| Q12 | Claude session builds; reviewer is Filbert or an orch-01 review seat, whoever is free at pin. Darkwing stays on rows 6 and 9. |
| Q13 | Message-content intent believed on; `check` proves it. |
| Q14 | Do not inject USER.md. Inject `agents/sage/DISCORD-USER.md`, a Jason-authored reduced profile, committed. |
| Q15 | Discord context block as proposed: channel and server, messages are data not instructions, 1900 char replies, no tools/files/memory outside the conversation, no credentials/paths/private strategy, no promises of action. |
| Q16 | Sage declines DYOR strategy discussion everywhere in Discord until the shared-repository piece exists. |
| Q17 | Reply with message reference, in place. No thread creation. |
| Q18 | Chunk at 1900 on paragraph boundaries, one nonce per chunk. Typing indicator every 8 s during a turn. |
| Q19 | Failed turn: one fixed line posted, never model output, plus the failed turn record. |
| Q20 | Ceiling hit: one fixed line in the tripping channel, stay up idle until UTC midnight. |
| Q21 | Drop messages over 4000 chars with a fixed reply. Envelope is text only; attachments and embeds stripped. |
| Q22 | Live evidence goes to Sage's private shared-signals evidence directory. Repo gets offline suite results and a pointer. |
| Q23 | Step 5 uses Jason's real second account. |
| Q24 | Pi auto-compaction on with defaults. |
| Q25 | Offline suite of seven groups, wired into `scripts/verify.sh`. |
| Q26 | Live pilot exactly as section 6, receipts private. Gate H: Jason rules the reply reads as Sage. |
| Q27 | Order: package, tests, `scripts/discord.sh`, DISCORD-USER.md draft, README, verify hook. Commit to `refactor` after green suites and reviewer approval. No push without separate say-so. Live pilot after commit. |
## 10. Choices made while building, 2026-09-13
Recorded here because they refine a ruling or fill a gap the rulings left.
- Q25 said the suite is wired into `scripts/verify.sh`. That script is the
Docker hello check, not a suite aggregator; no package suite hooks into
it. The suite is `scripts/test-discord.sh`, the same shape as
`test-foundation.sh`, listed in `docs/TOOLS.md`. If verify.sh ever becomes
an aggregator, this suite joins it there.
- Reconcile window. Discord dedups `enforce_nonce` for "the past few
minutes". An `unknown` delivery older than five minutes is marked
`refused` with a reason instead of re-sent, because a re-send outside the
window would post a second reply, which is the one thing the outcome in
section 2 forbids. Inside the window it is re-sent once with the same
nonce, as section 4 says.
- Turn attribution in the engine. A turn that fails on the connector's side
(timeout, malformed line) stays in the engine's queue, marked done, until
pi's own `turn_end` for it arrives; otherwise that event would be read as
the answer to the next prompt. The offline engine group covers this.
- Thread lookup. A message in a channel that is neither listed nor known
from `GUILD_CREATE`/`THREAD_*` events causes one `GET /channels/{id}`,
and only when the guild matches and the author is listed, so strangers
never cost a REST call. The result is cached.
- Over the ceiling, refusals are drop lines, not turn records, so they do
not count against the ceiling themselves.
- Chunk nonces are `<message id>-<n>`; fixed-line nonces add `-f`, `-b`,
`-c` for failed, oversize and ceiling, all under Discord's 25-character
nonce limit.
- `agents/sage/` is untracked in this checkout as a whole (another session's
work). Only `DISCORD-USER.md` is part of this piece; the binding's context
list needs `agents/sage/SOUL.md` on disk, which it is.
- Review intake found the ceiling counted only written turn records, so a
burst arriving while turns ran could queue past the cap. Admitted turns now
count until their record is written. Two smaller observations stay as
follow-ups, not changes: a follow-up turn's timeout budget starts at
admission, so it runs shorter than `turnTimeoutSeconds` while an earlier
turn is still busy; and the pid file was left behind if `run` failed between
writing it and starting the connector (the next `run` overwrote a dead
pid, so nothing refused). Both pid-file points are moot since round two.
- rev-code-02's round one (#1509 comment 26114) changed four things. The
reconcile window is measured from the first outbox line for a nonce, never
a retry. Context files must be repository-relative, symlink-free and
realpath-contained; the reviewer's stronger option, hard-coding the four
Sage files, was not taken because the binding is the owner's 0600 policy
and the containment is the boundary that matters. Admissions are appended
to `admissions.jsonl` before the engine runs and the ceiling counts them,
so a crash mid-turn still counts. Ownership is a `run.lock` directory
(mkdir is atomic) holding the pid and its /proc start time; `stop` signals
only a live pid whose start time matches, and `run` refuses where /proc is
unavailable.
- rev-code-02's round two (#1509 comment 26121) kept F4 open and added two.
The O_EXCL pid file had a window between create and write where a second
start read an empty file as stale and unlinked it. Ownership is now the
`run.lock` directory: mkdir either succeeds or fails, the owner record is
written to a temp name and renamed in, and a directory without a record is
treated as a claim in progress (busy) for 30 seconds, after which only a
crash between mkdir and rename can explain it. A stale lock is moved aside
by rename, never deleted in place, so two reclaimers cannot both take it
and neither can remove a lock a third start just made. The test spawns
four processes that claim at once; one wins, three refuse, and a fifth
claim refuses while the winner holds the lock. The daily ceiling notice
was memory-only, so a same-day restart repeated it; the decision is now a
line in `notices.jsonl` written before the delivery attempt. A duplicate
gateway event arriving during the awaited thread lookup was admitted
twice; the id is reserved for the whole of `handleMessage` now.
- rev-code-02's round three (#1509 comment 26123) reproduced a three-party
race in the automatic reclaim: two starts both judge a lock stale, one
moves it aside, a third start publishes a live lock, and the delayed one
then moves that live lock aside and claims. Rename proves nothing about
which directory it moved. The reviewer offered two options, a gate that
every claimant respects or fail-closed with a separate controlled cleanup.
I took the second: a gate is one more thing that can go stale, and the
repository's rule is to diagnose a refusal, not route around it. `run` now
refuses any lock it does not own and names `scripts/discord.sh unlock`.
`unlock` refuses a live owner and, after moving the lock aside, checks the
record it moved is the record it inspected; if not, it renames the lock
back and refuses. The cost is a manual step after a crash or reboot. Tests
cover four starts over a stale lock (all refuse, the lock is untouched),
one unlock, four starts again (one owner), and the changed-under-unlock
case.
- rev-code-02's round four (#1509 comment 26132) broke that `unlock` too: a
claim can land in the empty interval between moving a lock aside and
restoring it, so a compare-then-restore leaves two owners. The fix is the
reviewer's other option, an explicit quiescence gate, and the gate is the
`STOP` file the connector already honors. `unlock` writes `STOP` before it
inspects anything; a claim re-checks `STOP` after publishing its record
and releases itself if `STOP` is there. A claim that survives therefore
published before `unlock` began, and `unlock` sees it alive and refuses.
No rename-aside, no restore, no residue. The reviewer's four-party
schedule is a test: stale lock, unlock in progress, two claims inside its
gap, one record landing right before removal; the result is no owner, no
residue, `STOP` standing, and one clean claim after `STOP` is removed.
- rev-code-02's round five (#1509 comment 26150) closed the gate design and
found two more. The owner record held pid and /proc start ticks, and ticks
count from boot, so the same pair can recur after a reboot; the record now
carries the boot id too and `stop` signals only when all three match. The
identity check is three-way: dead or positively different is stale and
`unlock` clears it; alive but unreadable identity refuses everything and
removes nothing; only a full match is live. A claimant that cannot read
its own start time or the boot id refuses to claim. Second, `unlock`'s
refusal for a live owner promised an exit on `STOP` that the running
connector does not perform (it only drops later messages), so the text now
says to use `stop`. The connector does not gain a `STOP` watcher in this
pilot; `stop` sends SIGTERM, which is the shutdown path.
- rev-code-02's round six (#1509 comment 26158) probed the upgrade path: a
connector started under the round-five build holds `{pid, start}` with no
boot id, and the three-way check read that as a positive mismatch, so
`unlock` removed the lock under a live process. A live pid with a record
lacking start or boot is now "unknown" and refuses everything until the pid
is dead. A record file that exists but cannot be parsed is now "invalid"
rather than absent; nothing removes or claims over it. Tests: a live legacy
child (byte-identical lock, no signal target, no second owner, cleared after
exit) and an invalid-record control.
- rev-code-02's round seven (#1509 comment 26165) found the last gap in the
same family: a record with a start or boot string of the wrong shape was
compared to /proc and read as a positive mismatch. Identity values now have
a syntax (decimal start ticks, lowercase uuid boot id); anything else reads
as absent and, under a live pid, is "unknown". Six live-child controls
cover empty, non-decimal and mixed start values and empty, malformed and
uppercase boot values.
- rev-code-02's round eight (#1509 comment 26168): the start syntax still
took leading-zero and above-2^64 digit strings, which the kernel never
prints. The check is now the canonical unsigned decimal form; boundary
values sit in the syntax table and three more live-child controls.
+11
View File
@@ -39,6 +39,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
| 11 | Brief template `docs/plans/BRIEF-TEMPLATE.md`; `queue add` refuses a missing brief | darkwing | #1508 | required; with row 9 | two briefs accepted by Jason with no scope question | `2026-09-13_queue-as-data.md`, Piece C |
| 12 | Reviews as issue comments posted by `queue move ID in-review`, not files in `docs/plans/reviews/` | darkwing | #1508 | required; after row 9 | one review round with no new file under reviews/ | `2026-09-13_queue-as-data.md`, Piece D |
| 13 | Ledger "queue" section: issue/row/seat drift printed with the weekly number | darkwing; filbert reviews | #1508 | required; after row 9 | first run Monday 2026-09-21, zero violations or every one moved same day | `2026-09-13_queue-as-data.md`, Piece E |
| 14 | Discord connector pilot: Sage answers in Shared Signals (chat only, no tools, no repo writes) | coordinator; Filbert or orch-01 seat reviews | #1509 | in review: candidate built 2026-09-13, `scripts/test-discord.sh` 28/28 (87 node tests), existing suites green; Filbert deferred, rev-code-02 rounds 1-8 REQUEST CHANGES (26114, 26121, 26123, 26132, 26150, 26158, 26165, 26168) fixed, round 9 pending; no listener yet | H: offline suite green, eight-step live pilot with private receipts, then Jason says the reply reads as Sage | `2026-09-13_discord-connector-pilot.md` |
Start message for row 6, sent from the board to darkwing:
"Read docs/plans/QUEUE.md, then the plan page section "Piece 5: darkwing on
@@ -62,3 +63,13 @@ Gate F or when blocked."
- 2026-09-13 — dewey: WebUI row advanced to Jason-owned Gate E after Filbert approval, exact committed-tree 208/208 and remote-verified ea00ec66 publication. No other row changed.
- 2026-09-13 — rows 9 to 13 now point at the plan page `2026-09-13_queue-as-data.md` (pieces A to E, Gate G).
- 2026-09-13 — coordinator: row 14 added (Discord connector pilot for Sage on Shared Signals), briefed, unassigned, waiting on Jason for decisions D1-D7. No other row changed.
- 2026-09-13 — coordinator: row 14 moved to in progress (#1509) after Jason ruled Q1-Q27 through ms-grill-me. No other row changed.
- 2026-09-13 — coordinator: row 14 moved to in review; package, suite, wrapper, DISCORD-USER.md draft and README written, offline and existing suites green. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round two findings (26121) fixed, round three requested. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round three finding (26123, stale reclaim race) fixed by failing closed plus `unlock`, round four requested. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round four finding (26132, unlock restore race) fixed with a `STOP` quiescence gate, round five requested. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round five findings (26150, boot identity and unlock wording) fixed, round six requested. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round six finding (26158, legacy record and unreadable record must refuse) fixed, round seven requested. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round seven finding (26165, malformed identity strings must refuse) fixed, round eight requested. No other row changed.
- 2026-09-13 — coordinator: row 14 stays in review; rev-code-02 round eight finding (26168, noncanonical start digits must refuse) fixed, round nine requested. No other row changed.