packages/cli adds mosaic inbox, decide, tasks, agents and trail over the human-cli transport, and mosaic bus start, stop and status as the trusted host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots packages/bus/src/process.mjs, passes config.trackers from the tracker.* variables (lead decision 70), and runs a notifier child. The notifier DMs each open blocking decision once and sends an 08:00 America/Chicago digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord ids. A torn journal tail is copied aside and truncated; a malformed line, a directory looser than 0700 or a symlinked journal refuses (lead decision 71). packages/discord gains dmRecipient, createDm and notify.mjs. Candidate agents/rocko/work/slice1-s4, baseb9b6cf00, build.patch b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on #1521 (comment 26855), Filbert approved round 2 (comment 26856). The packet's mutant table lists M28 as killed; it survived, and BUILD-LOG records the correction. Integration gate in a worktree on2557e29dwith the patch applied: bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78, mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail on the base; S4 doesn't touch the package. Every scripts/test-*.sh is green, with test-release 14/14 and test-task 98/98 on the existing gate2 compose network. A scratch test, not in this commit, booted the real host with trackers against S3's fake Vikunja: the adapter went ready and a task.close on a missing task answered task-not-found after a Vikunja read. Co-Authored-By: Claude Opus 5.5 <[email protected]>
53 lines
2.0 KiB
JavaScript
53 lines
2.0 KiB
JavaScript
// The notifier as a child of the bus host. Started with fork(); the reader
|
|
// capability arrives over IPC in `{op: 'start'}` and never touches argv,
|
|
// stdout or the environment. Replies `{ok: true}` once the binding, the
|
|
// token file and the journal check out, or `{ok: false, error}` and exits
|
|
// 3. `{op: 'stop'}`, SIGTERM or the host going away stop it after the poll
|
|
// in flight.
|
|
import { Client } from "../../bus/src/client.mjs";
|
|
import { openDirect } from "../../discord/src/notify.mjs";
|
|
import { createNotifier, runLoop } from "./notifier.mjs";
|
|
|
|
const log = (line) => process.stderr.write(`mosaic-notify: ${line}\n`);
|
|
let loop = null;
|
|
let started = false;
|
|
let stopping = false;
|
|
|
|
async function stop(code) {
|
|
if (stopping) return;
|
|
stopping = true;
|
|
try {
|
|
await loop?.stop();
|
|
} catch {
|
|
code = 1;
|
|
}
|
|
process.exitCode = code;
|
|
if (process.connected) process.disconnect();
|
|
}
|
|
|
|
if (!process.send) {
|
|
process.stderr.write("trusted-host-required\n");
|
|
process.exitCode = 2;
|
|
} else {
|
|
const timer = setTimeout(() => stop(2), 10000);
|
|
process.on("message", (m) => {
|
|
if (m?.op === "stop") return stop(0);
|
|
if (m?.op !== "start" || started) return;
|
|
started = true;
|
|
clearTimeout(timer);
|
|
try {
|
|
const client = new Client({ path: m.path, cap: m.cap });
|
|
const direct = openDirect({ dataRoot: m.dataRoot, name: m.binding, ...(m.base ? { base: m.base } : {}), log });
|
|
const notifier = createNotifier({ business: m.business, dataRoot: m.dataRoot, inbox: () => client.call("inbox"), direct, log });
|
|
loop = runLoop(notifier, { pollMs: m.pollMs ?? undefined, log });
|
|
process.send({ ok: true });
|
|
} catch (e) {
|
|
// DiscordError and CliError messages name files, never a token or id.
|
|
process.send({ ok: false, error: e?.message ?? "notifier-refused" }, () => stop(3));
|
|
}
|
|
});
|
|
process.on("disconnect", () => stop(stopping ? process.exitCode : 2));
|
|
process.on("SIGTERM", () => stop(0));
|
|
process.on("SIGINT", () => {});
|
|
}
|