Files
stack/packages/plugins/src/channel.mjs
T
jason.woltjeandClaude Opus 5.5 008746b6ed plugins: channel kind for the CLI and Console chat paths (row 164, #1661)
Adds plugin kind `channel` (interfaceVersion 1, dispatch(request, {call}))
with cli and webui modules. Project and thread surfaces and thread
talk/follow in the CLI and the Console chat go through the selected
channel. An empty or absent channel selection is off: the CLI exits 3 and
the Console answers 503, each naming `mosaic plugins` and `mosaic plugins
set` with the business selector. Management plugins and plugins.set stay
on the core transport. The loader gains a synchronous snapshot(kind)
getter; loaded(), modules() and last-good handling are unchanged. No lock
is held across dispatch. serve.mjs, public/ and the bus are untouched.

Owner Rocko. Reviewer Dewey: r1 changes (comment 28980, B1: the recovery
sentence was never asserted), r2 approve (comment 28984) on candidate
manifest ae2fcfda...1fc8, the B1 assert killing mutant D1. Design accepted
by Mos (mos-0768, mos-0773). Fixture rewrite at project.test.mjs:47 acked
by Mos (mos-0789) on Dewey's line-by-line check: lines 48-51 identical,
one added assert. Sage gate on e5b10d275: 25 steps green, 2081 package tests, 156 queue node tests, 422 script selftests, 0 failed.

Live stays as pinned. The re-pin that takes this live puts `mosaic
plugins set` in Jason's window right after activate (mos-0768).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 14:24:42 -05:00

29 lines
1.6 KiB
JavaScript

// Core selection only: modules receive a business-bound call, never a proof or writer.
import { createLoader } from "./loader.mjs";
import { NAME, PluginError } from "./kinds.mjs";
export const CHANNEL_RECOVERY = 'Run mosaic plugins to read the revision, then mosaic plugins set --file PATCH with plugins.channel set to ["cli","webui"].';
const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
const report = entry => process.stderr.write(`channel: ${JSON.stringify({
event: entry.event, kind: entry.kind, name: entry.name, code: entry.code, kept: entry.kept,
})}\n`);
export function createChannelCaller({ name, call, log = report }) {
if (typeof name !== "string" || !NAME.test(name)) throw new PluginError("bad-name", "invalid channel name");
if (typeof call !== "function") throw new TypeError("channel needs a core caller");
const loader = createLoader({ log });
return async (verb, args = {}) => {
const settings = await call("plugins", {});
if (!record(settings) || !record(settings.plugins)) {
throw new PluginError("channel-config", "invalid channel settings response");
}
const selection = Object.hasOwn(settings.plugins, "channel") ? { channel: settings.plugins.channel } : {};
await loader.load(selection);
const snapshot = loader.snapshot("channel");
const entry = snapshot.find(item => item.name === name);
if (!entry) throw new PluginError("channel-disabled", CHANNEL_RECOVERY, name);
// No lock spans dispatch. Concurrent reads can proceed while a send awaits.
return entry.module.dispatch({ verb, args }, { call });
};
}