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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user