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]>
47 lines
2.2 KiB
JavaScript
47 lines
2.2 KiB
JavaScript
// The outbound DM path for the bus notifier (packages/cli, lead decision 70).
|
|
// It uses the connector's bot token and REST client but not its gateway
|
|
// process: one binding, one recipient (`dmRecipient`, which must be a listed
|
|
// user), one DM channel opened on first use and kept in memory.
|
|
//
|
|
// The caller sees outcomes only: `{messageId}` on a confirmed send, or a
|
|
// RestOutcome (refused | unknown) it journals and retries. The recipient id,
|
|
// the channel id and the token never leave this module: not in a return
|
|
// value, an error message or a log line.
|
|
|
|
import { DiscordError } from "./errors.mjs";
|
|
import { bindingPath, loadBinding, readToken } from "./binding.mjs";
|
|
import { createRest, API_BASE, RestOutcome } from "./rest.mjs";
|
|
|
|
export function openDirect({ dataRoot, name, fetch = globalThis.fetch, base = API_BASE, sleep, log = () => {} }) {
|
|
const binding = loadBinding(bindingPath(dataRoot, name));
|
|
if (binding.dmRecipient === null) throw new DiscordError(`binding ${name}: no dmRecipient; the notifier has nobody to DM`, 2);
|
|
const rest = createRest({ token: readToken(binding), fetch, base, sleep, log });
|
|
const recipient = binding.dmRecipient;
|
|
let channel = null;
|
|
return Object.freeze({
|
|
binding: binding.name,
|
|
// Resolves {messageId}. Throws RestOutcome (refused | unknown). The
|
|
// nonce makes Discord return the earlier message when an unknown send
|
|
// is retried within its dedupe window.
|
|
async send({ content, nonce }) {
|
|
if (channel === null) {
|
|
try {
|
|
channel = await rest.createDm(recipient);
|
|
} catch (err) {
|
|
if (err instanceof RestOutcome) throw new RestOutcome(err.kind, `dm channel: ${err.kind}`, { status: err.details.status });
|
|
throw err;
|
|
}
|
|
}
|
|
try {
|
|
const { messageId } = await rest.createMessage(channel, { content, nonce });
|
|
return { messageId };
|
|
} catch (err) {
|
|
if (!(err instanceof RestOutcome)) throw err;
|
|
// A refusal can mean the channel went away; open it again next time.
|
|
if (err.kind === "refused") channel = null;
|
|
throw new RestOutcome(err.kind, `dm: ${err.kind}`, { status: err.details.status });
|
|
}
|
|
},
|
|
});
|
|
}
|