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,31 @@
|
||||
{
|
||||
"bindingVersion": 1,
|
||||
"name": "example-seat",
|
||||
"seat": "sage",
|
||||
"guildId": "100000000000000001",
|
||||
"guildName": "Example Server",
|
||||
"botUserId": "100000000000000002",
|
||||
"tokenFile": "/home/example/secrets/discord-example.token",
|
||||
"channels": [
|
||||
{ "id": "100000000000000010", "name": "seat-admin", "mode": "open" },
|
||||
{ "id": "100000000000000011", "name": "general", "mode": "mention" }
|
||||
],
|
||||
"users": [
|
||||
{ "id": "100000000000000100", "name": "owner" }
|
||||
],
|
||||
"engine": { "provider": "zai", "model": "glm-5.3", "thinking": "high" },
|
||||
"limits": {
|
||||
"turnsPerDay": 200,
|
||||
"turnTimeoutSeconds": 180,
|
||||
"replyChunkChars": 1900,
|
||||
"inboundMaxChars": 4000
|
||||
},
|
||||
"context": {
|
||||
"files": [
|
||||
"contracts/CONSTITUTION.md",
|
||||
"contracts/STANDARDS.md",
|
||||
"agents/sage/SOUL.md",
|
||||
"agents/sage/DISCORD-USER.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Race participant for the lock tests: waits for the go file, then claims the
|
||||
// binding once. Prints "claimed" or "refused" on stdout and its pid on
|
||||
// stderr. A winner stays alive (holding the lock) until the done file
|
||||
// appears, then releases it.
|
||||
import { existsSync } from "node:fs";
|
||||
import { writePid, clearPid } from "../src/journal.mjs";
|
||||
|
||||
const [dir, go, done] = process.argv.slice(2);
|
||||
function waitFor(path) {
|
||||
const deadline = Date.now() + 10_000;
|
||||
while (!existsSync(path)) {
|
||||
if (Date.now() > deadline) { process.stderr.write(" timeout"); process.exit(2); }
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2);
|
||||
}
|
||||
}
|
||||
waitFor(go);
|
||||
process.stderr.write(String(process.pid));
|
||||
let claimed = false;
|
||||
try {
|
||||
writePid(dir, process.pid);
|
||||
claimed = true;
|
||||
process.stdout.write("claimed\n");
|
||||
} catch (err) {
|
||||
if (!/another connector is running|without an owner record|which is gone|cannot be verified|cannot be read|STOP is present/.test(err.message)) { process.stderr.write(" " + err.message); process.exit(1); }
|
||||
process.stdout.write(/STOP is present/.test(err.message) ? "stopped\n" : "refused\n");
|
||||
}
|
||||
if (claimed) {
|
||||
waitFor(done);
|
||||
clearPid(dir, process.pid);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Test participant: publishes an owner record and stays alive until the done
|
||||
// file appears, without ever clearing the lock. By default it writes the
|
||||
// round-five shape, {pid, start, at} with no boot id, as an already-running
|
||||
// connector from before the upgrade would have. Optional third and fourth
|
||||
// arguments override the recorded start and boot ("-" omits the field, "real"
|
||||
// records the genuine value), so tests can publish corrupt identity metadata
|
||||
// under a live pid. Prints "legacy-published" on stdout.
|
||||
import { existsSync, mkdirSync, writeFileSync, renameSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { processStart, bootId } from "../src/journal.mjs";
|
||||
|
||||
const [dir, done, startArg = "real", bootArg = "-"] = process.argv.slice(2);
|
||||
const rec = { pid: process.pid, at: new Date().toISOString() };
|
||||
if (startArg !== "-") rec.start = startArg === "real" ? processStart(process.pid) : startArg;
|
||||
if (bootArg !== "-") rec.boot = bootArg === "real" ? bootId() : bootArg;
|
||||
const lock = join(dir, "run.lock");
|
||||
mkdirSync(lock, { mode: 0o700 });
|
||||
const tmp = join(lock, "owner.json.tmp");
|
||||
writeFileSync(tmp, JSON.stringify(rec) + "\n", { mode: 0o600 });
|
||||
renameSync(tmp, join(lock, "owner.json"));
|
||||
process.stdout.write("legacy-published\n");
|
||||
const deadline = Date.now() + 10_000;
|
||||
while (!existsSync(done)) {
|
||||
if (Date.now() > deadline) process.exit(2);
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2);
|
||||
}
|
||||
Reference in New Issue
Block a user