feat(comms): P1 presence — minimal Synapse + fleet presence room + mosaic.presence heartbeat + liveness
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Implements RFC-001 P1 (the first shippable slice): deterministic Matrix presence/liveness on a single-instance dev Synapse. - infra/matrix/: DEV Synapse (RFC-002 Mode B, federation OFF, self-signed TLS, enable_registration:false, appservice registration wired). Rendered from parameterized templates — zero hardcoded topology. .data is gitignored. - packages/comms/: minimal MACP presence SDK — set presence, run the mosaic.presence heartbeat (seq + interval per RFC-001 §4.5), and a deterministic liveness reader (online/away/offline from heartbeat age, NOT native-presence-timeout). Liveness core written RED-FIRST. 19 vitest tests. - tools/matrix-presence-harness/: minimal provisioner (registers >=3 agent MXIDs, creates the fleet presence room, joins them — reuses the existing @mosaicstack/appservice intent lib) + an E2E validation harness proving A2/A3/A4 against a real Synapse. - eslint.config.mjs: register packages/comms/vitest.config.ts with the type-aware project service (same as other packages' vitest configs). DEV-compose validated only; production deploy is a separate coordinated step (deploy-holds respected). Part of the comms-evolution program (RFC-001 P1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
70
tools/matrix-presence-harness/agent-proc.ts
Normal file
70
tools/matrix-presence-harness/agent-proc.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* tools/matrix-presence-harness/agent-proc.ts
|
||||
*
|
||||
* *** DEV harness — LOCAL SANDBOX ONLY. ***
|
||||
*
|
||||
* Runs ONE presence agent as its own OS process: joins the fleet presence
|
||||
* room and heartbeats `mosaic.presence` via @mosaicstack/comms (RFC-001 P1).
|
||||
* Kept as a standalone process so the validation harness can `kill -9` it to
|
||||
* prove A3 (hard-kill -> offline within dark_threshold) against a real crash,
|
||||
* not a graceful shutdown.
|
||||
*
|
||||
* Auth in DEV is Application-Service masquerade: the process presents the
|
||||
* as_token and acts as its own @agent-<slug> MXID. (Per-agent minted tokens
|
||||
* are P2, RFC-001 §2.2/§8.)
|
||||
*
|
||||
* ESM / NodeNext: .js import extensions.
|
||||
*/
|
||||
import { MinimalMatrixClient, PresenceAgent, type LivenessPolicy } from '@mosaicstack/comms';
|
||||
|
||||
const env = (k: string, fallback?: string): string => {
|
||||
const v = process.env[k] ?? fallback;
|
||||
if (v === undefined) throw new Error(`missing env ${k}`);
|
||||
return v;
|
||||
};
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const homeserverUrl = env('MATRIX_CS_URL');
|
||||
const asToken = env('MOSAIC_AS_TOKEN');
|
||||
const serverName = env('MATRIX_SERVER_NAME');
|
||||
const slug = env('AGENT_SLUG');
|
||||
const roomId = env('FLEET_ROOM_ID');
|
||||
const intervalMs = Number(env('HEARTBEAT_INTERVAL_MS', '1000'));
|
||||
|
||||
const mxid = `@agent-${slug}:${serverName}`;
|
||||
const policy: LivenessPolicy = {
|
||||
heartbeatIntervalMs: intervalMs,
|
||||
missTolerance: Number(env('MISS_TOLERANCE', '2')),
|
||||
darkThresholdMs: Number(env('DARK_THRESHOLD_MS', '6000')),
|
||||
};
|
||||
|
||||
const client = new MinimalMatrixClient({
|
||||
homeserverUrl,
|
||||
accessToken: asToken,
|
||||
actAsUserId: mxid,
|
||||
});
|
||||
const agent = new PresenceAgent({
|
||||
client,
|
||||
agent: { mxid, slug, harness: 'claude-code' },
|
||||
roomId,
|
||||
intervalMs,
|
||||
policy,
|
||||
onError: (err) => console.error(`[${slug}] heartbeat error:`, (err as Error).message),
|
||||
});
|
||||
|
||||
await agent.connect();
|
||||
agent.start();
|
||||
console.log(`[${slug}] pid=${process.pid} mxid=${mxid} heartbeating every ${intervalMs}ms`);
|
||||
|
||||
// Graceful stop only on SIGTERM; the A3 test uses SIGKILL (no cleanup runs).
|
||||
process.on('SIGTERM', () => {
|
||||
void agent.stop().finally(() => process.exit(0));
|
||||
});
|
||||
// Keep the event loop alive indefinitely.
|
||||
setInterval(() => {}, 1 << 30);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('agent-proc fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user