@groundnuty/macf-channel-server 0.2.36 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * comms-ledger-record — the LOUD-BUT-PROCEEDS policy slot for the
3
+ * comms-ledger (macf#473 piece 2; operator decision 2026-06-08).
4
+ *
5
+ * The library writer `appendEdge` (comms-ledger.ts) is deliberately
6
+ * FAIL-LOUD — it throws on any write failure (I/O error) or schema-parse
7
+ * rejection, because an authoritative DR-025 edge must never silently
8
+ * disappear at the WRITE layer. But the channel-server's three coordination
9
+ * EDGE SITES (inbound /notify recv, inbound A2A message/send recv, outbound
10
+ * notify_peer send) sit on the delivery hot path: a ledger write failure
11
+ * there must NOT abort the delivery the agent depends on. The observability
12
+ * layer can never be allowed to cause a coordination outage.
13
+ *
14
+ * `recordEdge` reconciles those two requirements. It is append-first, and on
15
+ * ANY failure it CATCHES, emits a LOUD signal on BOTH channels (a
16
+ * `logger.error('comms_ledger_write_failed', …)` line carrying the edge
17
+ * inline + a dedicated `comms_ledger_write_failed` metric carrying enough
18
+ * label dimensions to reconstruct the edge class), then RETURNS normally.
19
+ * It never re-throws and never silently swallows. The caller then proceeds
20
+ * with delivery regardless.
21
+ *
22
+ * The WHOLE loud-signal path is guarded so it can never make delivery fatal.
23
+ * The dominant cause of `appendEdge` throwing is a disk-full / read-only
24
+ * volume — but the ledger is a SIBLING of `channel.log`, and `logger.error`
25
+ * lands in `channel.log` via the same `appendFileSync`. A disk-full failure
26
+ * therefore makes the loud-signal emitter (`logger.error`) throw the SAME
27
+ * errno, which — without the outermost guard — would escape `recordEdge` and,
28
+ * at the recv sites (`try { record; await onNotify; 200 } catch { 500 }`),
29
+ * skip the delivery and 500 the sender. So the entire catch-block body is
30
+ * wrapped in an OUTERMOST try/catch. If even that fails, a best-effort
31
+ * `process.stderr.write` is the last channel left; once that path is
32
+ * exhausted the only correct action is to swallow — there is no louder
33
+ * channel remaining and delivery MUST proceed.
34
+ *
35
+ * This is the structural inverse of `appendEdge`: the WRITE is fail-loud
36
+ * (it must surface the failure to *someone*); the POLICY around it at the
37
+ * hot-path sites is loud-but-proceeds (it must surface AND keep going). The
38
+ * library stays pure; the policy lives here.
39
+ */
40
+ import type { Logger } from '@groundnuty/macf-core';
41
+ import type { CommsLedger, CommsLedgerEdge } from './comms-ledger.js';
42
+ /**
43
+ * Sink for the machine-readable half of the loud signal. Injected (not
44
+ * imported directly from `metrics.ts`) so `recordEdge` degrades gracefully
45
+ * when metrics are off — server.ts wires the real `getCommsLedgerWriteFailedCounter`
46
+ * increment; tests inject a spy; omitting it entirely still logs.
47
+ *
48
+ * Receives the edge that failed to write so the recorder can derive whatever
49
+ * label dimensions it wants (channel / direction / agent) from a single
50
+ * source — keeping the label set decided at the metrics layer, not here.
51
+ */
52
+ export type CommsLedgerWriteFailedRecorder = (failedEdge: CommsLedgerEdge) => void;
53
+ export interface RecordEdgeDeps {
54
+ readonly ledger: CommsLedger;
55
+ readonly logger: Logger;
56
+ /**
57
+ * Optional metric recorder for the write-failed signal. When absent,
58
+ * `recordEdge` still emits the `logger.error` loud signal — the metric is
59
+ * a complement, not a precondition (OTEL is opt-in; the log is always on).
60
+ */
61
+ readonly recordWriteFailed?: CommsLedgerWriteFailedRecorder;
62
+ }
63
+ /**
64
+ * Append `edge` to the comms-ledger under the loud-but-proceeds policy.
65
+ *
66
+ * Behavior (macf#473 operator decision):
67
+ * 1. append-first: call `ledger.appendEdge(edge)`.
68
+ * 2. on ANY throw (I/O failure OR the Zod schema-parse rejection inside
69
+ * appendEdge) → CATCH it.
70
+ * 3. emit the LOUD signal on both channels:
71
+ * - `logger.error('comms_ledger_write_failed', { edge, error })`
72
+ * — the edge is carried INLINE so the lost authoritative record is
73
+ * reconstructable from the log alone.
74
+ * - `recordWriteFailed(edge)` (if wired) — increments the dedicated
75
+ * metric carrying enough label dimensions to reconstruct the edge
76
+ * CLASS for alerting / dashboards.
77
+ * 4. RETURN normally. NEVER re-throw, NEVER silently swallow.
78
+ *
79
+ * The caller proceeds with delivery afterward regardless of outcome. This
80
+ * guarantees the observability layer can never cause a coordination outage,
81
+ * while never being silent about a lost edge.
82
+ *
83
+ * Note: the no-op ledger (no MACF_LOG_PATH) never throws, so this is a clean
84
+ * no-op there too — same as the rest of the observability surface when
85
+ * unconfigured.
86
+ */
87
+ export declare function recordEdge(deps: RecordEdgeDeps, edge: CommsLedgerEdge): void;
88
+ //# sourceMappingURL=comms-ledger-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comms-ledger-record.d.ts","sourceRoot":"","sources":["../src/comms-ledger-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEtE;;;;;;;;;GASG;AACH,MAAM,MAAM,8BAA8B,GAAG,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;AAEnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,8BAA8B,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CA4C5E"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Append `edge` to the comms-ledger under the loud-but-proceeds policy.
3
+ *
4
+ * Behavior (macf#473 operator decision):
5
+ * 1. append-first: call `ledger.appendEdge(edge)`.
6
+ * 2. on ANY throw (I/O failure OR the Zod schema-parse rejection inside
7
+ * appendEdge) → CATCH it.
8
+ * 3. emit the LOUD signal on both channels:
9
+ * - `logger.error('comms_ledger_write_failed', { edge, error })`
10
+ * — the edge is carried INLINE so the lost authoritative record is
11
+ * reconstructable from the log alone.
12
+ * - `recordWriteFailed(edge)` (if wired) — increments the dedicated
13
+ * metric carrying enough label dimensions to reconstruct the edge
14
+ * CLASS for alerting / dashboards.
15
+ * 4. RETURN normally. NEVER re-throw, NEVER silently swallow.
16
+ *
17
+ * The caller proceeds with delivery afterward regardless of outcome. This
18
+ * guarantees the observability layer can never cause a coordination outage,
19
+ * while never being silent about a lost edge.
20
+ *
21
+ * Note: the no-op ledger (no MACF_LOG_PATH) never throws, so this is a clean
22
+ * no-op there too — same as the rest of the observability surface when
23
+ * unconfigured.
24
+ */
25
+ export function recordEdge(deps, edge) {
26
+ try {
27
+ deps.ledger.appendEdge(edge);
28
+ }
29
+ catch (err) {
30
+ // OUTERMOST guard around the ENTIRE loud-signal path. The ledger is a
31
+ // sibling of channel.log, so the dominant failure cause (disk-full /
32
+ // read-only volume) makes `logger.error` throw the SAME errno. Without
33
+ // this guard that throw escapes recordEdge and 500s the sender at the
34
+ // recv sites. Nothing in here may escape — delivery must proceed.
35
+ try {
36
+ // LOUD signal — human-readable half (always on). The edge is carried
37
+ // inline so the lost authoritative record can be reconstructed from the
38
+ // log without the ledger file.
39
+ deps.logger.error('comms_ledger_write_failed', {
40
+ edge,
41
+ error: err instanceof Error ? err.message : String(err),
42
+ });
43
+ // LOUD signal — machine-readable half (when metrics are wired).
44
+ // Guard the recorder ITSELF so a misbehaving metric sink can't turn the
45
+ // loud-but-proceeds policy back into a fatal path. Last-ditch only.
46
+ if (deps.recordWriteFailed !== undefined) {
47
+ try {
48
+ deps.recordWriteFailed(edge);
49
+ }
50
+ catch (metricErr) {
51
+ deps.logger.error('comms_ledger_write_failed_metric_error', {
52
+ error: metricErr instanceof Error ? metricErr.message : String(metricErr),
53
+ });
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // The loud-signal path itself failed (e.g. logger.error threw the same
59
+ // disk-full errno that broke appendEdge — they share the log volume).
60
+ // process.stderr is the last channel that doesn't touch that volume;
61
+ // best-effort, guarded, then SWALLOW. This is the one place a true
62
+ // swallow is correct: no louder channel remains and delivery must
63
+ // proceed. recordEdge NEVER escapes.
64
+ try {
65
+ process.stderr.write('comms_ledger_write_failed (loud-signal emit also failed)\n');
66
+ }
67
+ catch {
68
+ /* nothing left to do */
69
+ }
70
+ }
71
+ // RETURN normally — the caller proceeds with delivery.
72
+ }
73
+ }
74
+ //# sourceMappingURL=comms-ledger-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comms-ledger-record.js","sourceRoot":"","sources":["../src/comms-ledger-record.ts"],"names":[],"mappings":"AAiEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,UAAU,CAAC,IAAoB,EAAE,IAAqB;IACpE,IAAI,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,sEAAsE;QACtE,qEAAqE;QACrE,uEAAuE;QACvE,sEAAsE;QACtE,kEAAkE;QAClE,IAAI,CAAC;YACH,qEAAqE;YACrE,wEAAwE;YACxE,+BAA+B;YAC/B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,EAAE;gBAC7C,IAAI;gBACJ,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;YACH,gEAAgE;YAChE,wEAAwE;YACxE,oEAAoE;YACpE,IAAI,IAAI,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;gBACzC,IAAI,CAAC;oBACH,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;gBAC/B,CAAC;gBAAC,OAAO,SAAS,EAAE,CAAC;oBACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE;wBAC1D,KAAK,EAAE,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;qBAC1E,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,mEAAmE;YACnE,kEAAkE;YAClE,qCAAqC;YACrC,IAAI,CAAC;gBACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;YACrF,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;QACD,uDAAuD;IACzD,CAAC;AACH,CAAC"}
@@ -0,0 +1,198 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * comms-ledger — the write-ahead, per-agent, authoritative record of every
4
+ * coordination edge the channel-server sends or receives.
5
+ *
6
+ * Implements DR-025 (observable coordination substrate). The invariant: any
7
+ * channel carrying agent-to-agent coordination MUST preserve a durable,
8
+ * graph-reconstructable, fleet-analyzable record. GitHub gives this for free;
9
+ * A2A / direct channels must earn it deliberately or the diagnose→redesign
10
+ * instrument (and the paper's evidence) goes blind. This is macf#444 one layer
11
+ * up; a Tempo-only design would reproduce silent-fallback Instance 8 (OTLP
12
+ * silent-drop) as the methodology's foundation.
13
+ *
14
+ * Resilience shape: a write-ahead log + a downstream rebuildable index. The
15
+ * JSONL ledger is AUTHORITATIVE (local disk, synchronous, fail-loud); the Tempo
16
+ * span is a DERIVED best-effort central index over the same data. The durable
17
+ * write happens BEFORE the lossy network hop, independent of Tempo's health.
18
+ *
19
+ * This module is the library layer (#473 piece 1): the schema, the unified
20
+ * event taxonomy, the fail-loud writer, the `processed` backfill join, the
21
+ * multi-host gather, and the rotation policy. Wiring it into the three edge
22
+ * sites (inbound `/notify`, inbound A2A `message/send`, outbound notify_peer)
23
+ * is #473 piece 2.
24
+ */
25
+ /**
26
+ * Unified coordination-event taxonomy (DR-025 §"Channel unification").
27
+ * One enum spanning the A2A events and the router events, so a ledger edge
28
+ * analyzes identically regardless of which channel carried it.
29
+ */
30
+ export declare const CommsEventSchema: z.ZodEnum<{
31
+ mention: "mention";
32
+ error: "error";
33
+ custom: "custom";
34
+ "session-end": "session-end";
35
+ "turn-complete": "turn-complete";
36
+ "issue-routed": "issue-routed";
37
+ "pr-review-state": "pr-review-state";
38
+ }>;
39
+ export type CommsEvent = z.infer<typeof CommsEventSchema>;
40
+ export declare const CommsChannelSchema: z.ZodEnum<{
41
+ a2a: "a2a";
42
+ "github-route": "github-route";
43
+ }>;
44
+ export type CommsChannel = z.infer<typeof CommsChannelSchema>;
45
+ export declare const CommsDirectionSchema: z.ZodEnum<{
46
+ send: "send";
47
+ recv: "recv";
48
+ }>;
49
+ export type CommsDirection = z.infer<typeof CommsDirectionSchema>;
50
+ /**
51
+ * One coordination edge — one JSONL line per exchange (DR-025 §"The edge schema").
52
+ *
53
+ * - `delivered` is known at edge-write (the byte sequence was accepted/pushed).
54
+ * - `processed` is the macf#444 distinction (delivery ≠ a turn actually happening).
55
+ * It is NULLABLE at edge-write — the peer hasn't necessarily taken a turn yet —
56
+ * and is backfilled later via the receipt join (`backfillProcessed`). The
57
+ * edge-write must never block on it.
58
+ * - `trace_id` cross-references the Tempo span. It is captured synchronously from
59
+ * `span.spanContext().traceId` (OTel api ≥1.9.1: synchronous, available pre-export)
60
+ * BEFORE this write; the span export (`span.end()`) happens after, async, best-effort.
61
+ * - `github_anchor` stitches an off-GitHub edge back to its GitHub object so the
62
+ * on-GitHub and off-GitHub graphs join into one; `null` for a pure nudge.
63
+ */
64
+ export declare const CommsLedgerEdgeSchema: z.ZodObject<{
65
+ ts: z.ZodString;
66
+ from: z.ZodString;
67
+ to: z.ZodString;
68
+ channel: z.ZodEnum<{
69
+ a2a: "a2a";
70
+ "github-route": "github-route";
71
+ }>;
72
+ event: z.ZodEnum<{
73
+ mention: "mention";
74
+ error: "error";
75
+ custom: "custom";
76
+ "session-end": "session-end";
77
+ "turn-complete": "turn-complete";
78
+ "issue-routed": "issue-routed";
79
+ "pr-review-state": "pr-review-state";
80
+ }>;
81
+ direction: z.ZodEnum<{
82
+ send: "send";
83
+ recv: "recv";
84
+ }>;
85
+ msg_id: z.ZodString;
86
+ intent_summary: z.ZodString;
87
+ github_anchor: z.ZodNullable<z.ZodString>;
88
+ delivered: z.ZodBoolean;
89
+ processed: z.ZodNullable<z.ZodBoolean>;
90
+ trace_id: z.ZodString;
91
+ }, z.core.$strip>;
92
+ export type CommsLedgerEdge = z.infer<typeof CommsLedgerEdgeSchema>;
93
+ /** Max length of the deterministic intent-summary clip. */
94
+ export declare const INTENT_SUMMARY_MAX = 120;
95
+ /**
96
+ * Cheap, deterministic intent summary: the first non-empty line of the message,
97
+ * trimmed and clipped to `max` chars.
98
+ *
99
+ * Explicitly NOT an LLM summarize (#473 AC): keep a model dependency and its
100
+ * latency out of the delivery hot path. This runs synchronously on every edge.
101
+ */
102
+ export declare function intentSummary(text: string | null | undefined, max?: number): string;
103
+ /** Canonical per-agent ledger filename, kept as a sibling of `channel.log`. */
104
+ export declare const COMMS_LEDGER_FILENAME = "comms-ledger.jsonl";
105
+ /**
106
+ * Derive the ledger path from the channel-server's `logPath` (`MACF_LOG_PATH`),
107
+ * as a sibling file in the same `.macf/logs/` directory. Returns `undefined`
108
+ * when no log path is configured (the ledger is then a no-op, mirroring how the
109
+ * `logger` is a no-op without `MACF_LOG_PATH` — the real fleet always sets it).
110
+ */
111
+ export declare function ledgerPathFromLog(logPath: string | undefined): string | undefined;
112
+ export interface CommsLedger {
113
+ /**
114
+ * Append one coordination edge — SYNCHRONOUS and FAIL-LOUD.
115
+ *
116
+ * Throws if the write fails: an authoritative edge would otherwise be lost,
117
+ * and DR-025 names this the one operation that must NOT silently degrade.
118
+ * This is the structural distinction from the best-effort `logger`
119
+ * (`channel.log`), whose `logger.info` must stay non-fatal. Callers append to
120
+ * the ledger BEFORE the (async, best-effort) Tempo span export, so a Tempo or
121
+ * network failure can never cost an edge.
122
+ */
123
+ readonly appendEdge: (edge: CommsLedgerEdge) => void;
124
+ /** The resolved ledger path, or `undefined` if the ledger is a no-op. */
125
+ readonly path: string | undefined;
126
+ }
127
+ /**
128
+ * Create the per-agent write-ahead comms-ledger writer.
129
+ *
130
+ * Pass the channel-server's `logPath`; the ledger is written to a sibling
131
+ * `comms-ledger.jsonl` in the same directory. A distinct writer from `logger`
132
+ * by design — fail-loud, authoritative — never the best-effort log.
133
+ *
134
+ * Durability note (research-confirmed: OTel api 1.9.1 / Node `fs`): `appendFileSync`
135
+ * is synchronous and throws on write error (the fail-loud guarantee), but does
136
+ * NOT `fsync`. The threat model is a Tempo/network failure with the edge already
137
+ * on local disk — not power-loss — so per-write `fsync` (latency on every
138
+ * exchange) is deliberately skipped.
139
+ */
140
+ export declare function createCommsLedger(opts: {
141
+ readonly logPath?: string | undefined;
142
+ /** Override the derived path (mainly for tests). */
143
+ readonly ledgerPath?: string | undefined;
144
+ }): CommsLedger;
145
+ /**
146
+ * Backfill `processed` on edges from a set of receipt keys (DR-025 / macf#444).
147
+ *
148
+ * Edges are written with `processed: null` (unknown-at-write); a receipt — proof
149
+ * the peer actually took a turn — resolves it. This is a PURE function: it does
150
+ * NOT mutate the append-only ledger, it produces a derived view (the same shape
151
+ * as `reconciler/reconcile.ts`, which joins delivered routes ⋈ turn receipts).
152
+ *
153
+ * Channel split for the `processed` (delivery ≠ turn) join:
154
+ * - **a2a edges** join on `msg_id` via THIS function — `keyOf` maps the edge
155
+ * to its `msg_id` and `receiptKeys` carries the observed receipts.
156
+ * - **github-route edges** are tracked SEPARATELY by the macf#444 reconciler,
157
+ * which is run_id-keyed off the prompt `[macf-route:RUN:AGENT]` marker. The
158
+ * github-route recv edge intentionally does NOT carry that run_id (it is
159
+ * absent from CommsLedgerEdge, NotifyPayload, and the `type:num:ts` msg_id),
160
+ * so the `(run_id, agent)` join is structurally impossible HERE. A
161
+ * github-route edge's `processed` therefore stays `null` in the ledger BY
162
+ * DESIGN; its delivery≠turn distinction lives in the reconciler's view, not
163
+ * this one. `backfillProcessed` is the a2a-side join.
164
+ *
165
+ * `keyOf` is left channel-agnostic on purpose (it just reads `msg_id` for the
166
+ * a2a join); edges whose `processed` is already non-null are left untouched
167
+ * (idempotent).
168
+ */
169
+ export declare function backfillProcessed(edges: readonly CommsLedgerEdge[], receiptKeys: ReadonlySet<string>, keyOf: (edge: CommsLedgerEdge) => string): CommsLedgerEdge[];
170
+ /**
171
+ * Gather + merge per-agent ledgers into one fleet view, ordered by `ts`.
172
+ *
173
+ * The per-agent ledger is the durable floor; Tempo is the central convenience
174
+ * index. When Tempo is down, fleet-graph analysis falls back to merging the
175
+ * per-agent ledgers. On the single-host substrate this is trivial — all the
176
+ * `comms-ledger.jsonl` files are local. For a MULTI-HOST fleet the gather step
177
+ * is explicit and operator-defined: collect each host's ledger (e.g.
178
+ * `rsync <agent-host>:.../comms-ledger.jsonl ./gathered/<agent>.jsonl`) into one
179
+ * place, then call this. There is deliberately NO central durable sink — that
180
+ * would reintroduce the single point of failure DR-025 exists to avoid.
181
+ *
182
+ * Malformed lines are skipped (a corrupt line must not blind the whole gather);
183
+ * this is the one place a parse error is tolerated, because gather is a derived
184
+ * read, not the authoritative write.
185
+ */
186
+ export declare function mergeLedgers(contents: readonly string[]): CommsLedgerEdge[];
187
+ /**
188
+ * Rotation/retention policy (DR-025 §"Costs", "no silent caps"):
189
+ *
190
+ * The ledger is the PERMANENT record, so rotation is deliberate and must NEVER
191
+ * silently truncate. The writer here only ever appends — it has no size cap and
192
+ * no truncation path by construction. If an operator rotates the file for size,
193
+ * the canonical action is archive-on-rotate (move the old file aside, e.g.
194
+ * `comms-ledger.jsonl.<date>`), never `> truncate` or head/tail clipping. Size
195
+ * management is an operator concern, intentionally outside this hot-path writer.
196
+ */
197
+ export declare const ROTATION_POLICY: "archive-on-rotate; never silent truncation";
198
+ //# sourceMappingURL=comms-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comms-ledger.d.ts","sourceRoot":"","sources":["../src/comms-ledger.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;EAU3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,kBAAkB;;;EAAkC,CAAC;AAClE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,oBAAoB;;;EAA2B,CAAC;AAC7D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAahC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,GAAG,GAAE,MAA2B,GAC/B,MAAM,CAUR;AAED,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IACrD,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C,GAAG,WAAW,CAkBd;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,EAChC,KAAK,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,MAAM,GACvC,eAAe,EAAE,CAInB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,EAAE,CAW3E;AAYD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAG,4CAAqD,CAAC"}
@@ -0,0 +1,224 @@
1
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { z } from 'zod';
4
+ /**
5
+ * comms-ledger — the write-ahead, per-agent, authoritative record of every
6
+ * coordination edge the channel-server sends or receives.
7
+ *
8
+ * Implements DR-025 (observable coordination substrate). The invariant: any
9
+ * channel carrying agent-to-agent coordination MUST preserve a durable,
10
+ * graph-reconstructable, fleet-analyzable record. GitHub gives this for free;
11
+ * A2A / direct channels must earn it deliberately or the diagnose→redesign
12
+ * instrument (and the paper's evidence) goes blind. This is macf#444 one layer
13
+ * up; a Tempo-only design would reproduce silent-fallback Instance 8 (OTLP
14
+ * silent-drop) as the methodology's foundation.
15
+ *
16
+ * Resilience shape: a write-ahead log + a downstream rebuildable index. The
17
+ * JSONL ledger is AUTHORITATIVE (local disk, synchronous, fail-loud); the Tempo
18
+ * span is a DERIVED best-effort central index over the same data. The durable
19
+ * write happens BEFORE the lossy network hop, independent of Tempo's health.
20
+ *
21
+ * This module is the library layer (#473 piece 1): the schema, the unified
22
+ * event taxonomy, the fail-loud writer, the `processed` backfill join, the
23
+ * multi-host gather, and the rotation policy. Wiring it into the three edge
24
+ * sites (inbound `/notify`, inbound A2A `message/send`, outbound notify_peer)
25
+ * is #473 piece 2.
26
+ */
27
+ /**
28
+ * Unified coordination-event taxonomy (DR-025 §"Channel unification").
29
+ * One enum spanning the A2A events and the router events, so a ledger edge
30
+ * analyzes identically regardless of which channel carried it.
31
+ */
32
+ export const CommsEventSchema = z.enum([
33
+ // A2A / peer_notification (notify_peer `event`, message/send)
34
+ 'turn-complete',
35
+ 'session-end',
36
+ 'error',
37
+ 'custom',
38
+ // GitHub router (the macf-actions route-by-* blocks)
39
+ 'issue-routed',
40
+ 'mention',
41
+ 'pr-review-state',
42
+ ]);
43
+ export const CommsChannelSchema = z.enum(['a2a', 'github-route']);
44
+ export const CommsDirectionSchema = z.enum(['send', 'recv']);
45
+ /**
46
+ * One coordination edge — one JSONL line per exchange (DR-025 §"The edge schema").
47
+ *
48
+ * - `delivered` is known at edge-write (the byte sequence was accepted/pushed).
49
+ * - `processed` is the macf#444 distinction (delivery ≠ a turn actually happening).
50
+ * It is NULLABLE at edge-write — the peer hasn't necessarily taken a turn yet —
51
+ * and is backfilled later via the receipt join (`backfillProcessed`). The
52
+ * edge-write must never block on it.
53
+ * - `trace_id` cross-references the Tempo span. It is captured synchronously from
54
+ * `span.spanContext().traceId` (OTel api ≥1.9.1: synchronous, available pre-export)
55
+ * BEFORE this write; the span export (`span.end()`) happens after, async, best-effort.
56
+ * - `github_anchor` stitches an off-GitHub edge back to its GitHub object so the
57
+ * on-GitHub and off-GitHub graphs join into one; `null` for a pure nudge.
58
+ */
59
+ export const CommsLedgerEdgeSchema = z.object({
60
+ ts: z.string(), // ISO 8601
61
+ from: z.string(),
62
+ to: z.string(),
63
+ channel: CommsChannelSchema,
64
+ event: CommsEventSchema,
65
+ direction: CommsDirectionSchema,
66
+ msg_id: z.string(),
67
+ intent_summary: z.string(),
68
+ github_anchor: z.string().nullable(),
69
+ delivered: z.boolean(),
70
+ processed: z.boolean().nullable(),
71
+ trace_id: z.string(),
72
+ });
73
+ /** Max length of the deterministic intent-summary clip. */
74
+ export const INTENT_SUMMARY_MAX = 120;
75
+ /**
76
+ * Cheap, deterministic intent summary: the first non-empty line of the message,
77
+ * trimmed and clipped to `max` chars.
78
+ *
79
+ * Explicitly NOT an LLM summarize (#473 AC): keep a model dependency and its
80
+ * latency out of the delivery hot path. This runs synchronously on every edge.
81
+ */
82
+ export function intentSummary(text, max = INTENT_SUMMARY_MAX) {
83
+ if (!text)
84
+ return '';
85
+ const firstLine = text
86
+ .split('\n')
87
+ .map((l) => l.trim())
88
+ .find((l) => l.length > 0) ?? '';
89
+ if (firstLine.length <= max)
90
+ return firstLine;
91
+ // Clip on a char boundary; the ellipsis signals truncation (never a silent cap).
92
+ return firstLine.slice(0, max - 1) + '…';
93
+ }
94
+ /** Canonical per-agent ledger filename, kept as a sibling of `channel.log`. */
95
+ export const COMMS_LEDGER_FILENAME = 'comms-ledger.jsonl';
96
+ /**
97
+ * Derive the ledger path from the channel-server's `logPath` (`MACF_LOG_PATH`),
98
+ * as a sibling file in the same `.macf/logs/` directory. Returns `undefined`
99
+ * when no log path is configured (the ledger is then a no-op, mirroring how the
100
+ * `logger` is a no-op without `MACF_LOG_PATH` — the real fleet always sets it).
101
+ */
102
+ export function ledgerPathFromLog(logPath) {
103
+ if (!logPath)
104
+ return undefined;
105
+ return join(dirname(logPath), COMMS_LEDGER_FILENAME);
106
+ }
107
+ const NOOP_LEDGER = {
108
+ appendEdge: () => {
109
+ /* no path configured → no-op (mirrors logger when MACF_LOG_PATH unset) */
110
+ },
111
+ path: undefined,
112
+ };
113
+ /**
114
+ * Create the per-agent write-ahead comms-ledger writer.
115
+ *
116
+ * Pass the channel-server's `logPath`; the ledger is written to a sibling
117
+ * `comms-ledger.jsonl` in the same directory. A distinct writer from `logger`
118
+ * by design — fail-loud, authoritative — never the best-effort log.
119
+ *
120
+ * Durability note (research-confirmed: OTel api 1.9.1 / Node `fs`): `appendFileSync`
121
+ * is synchronous and throws on write error (the fail-loud guarantee), but does
122
+ * NOT `fsync`. The threat model is a Tempo/network failure with the edge already
123
+ * on local disk — not power-loss — so per-write `fsync` (latency on every
124
+ * exchange) is deliberately skipped.
125
+ */
126
+ export function createCommsLedger(opts) {
127
+ const ledgerPath = opts.ledgerPath ?? ledgerPathFromLog(opts.logPath);
128
+ if (!ledgerPath)
129
+ return NOOP_LEDGER;
130
+ const dir = dirname(ledgerPath);
131
+ if (!existsSync(dir))
132
+ mkdirSync(dir, { recursive: true });
133
+ if (!existsSync(ledgerPath))
134
+ writeFileSync(ledgerPath, '');
135
+ return {
136
+ path: ledgerPath,
137
+ appendEdge: (edge) => {
138
+ // Validate the shape (cheap, catches a malformed edge at the source),
139
+ // then append exactly one JSONL line. appendFileSync throws on any write
140
+ // error → propagates to the caller (fail-loud, per DR-025).
141
+ const line = JSON.stringify(CommsLedgerEdgeSchema.parse(edge));
142
+ appendFileSync(ledgerPath, line + '\n');
143
+ },
144
+ };
145
+ }
146
+ /**
147
+ * Backfill `processed` on edges from a set of receipt keys (DR-025 / macf#444).
148
+ *
149
+ * Edges are written with `processed: null` (unknown-at-write); a receipt — proof
150
+ * the peer actually took a turn — resolves it. This is a PURE function: it does
151
+ * NOT mutate the append-only ledger, it produces a derived view (the same shape
152
+ * as `reconciler/reconcile.ts`, which joins delivered routes ⋈ turn receipts).
153
+ *
154
+ * Channel split for the `processed` (delivery ≠ turn) join:
155
+ * - **a2a edges** join on `msg_id` via THIS function — `keyOf` maps the edge
156
+ * to its `msg_id` and `receiptKeys` carries the observed receipts.
157
+ * - **github-route edges** are tracked SEPARATELY by the macf#444 reconciler,
158
+ * which is run_id-keyed off the prompt `[macf-route:RUN:AGENT]` marker. The
159
+ * github-route recv edge intentionally does NOT carry that run_id (it is
160
+ * absent from CommsLedgerEdge, NotifyPayload, and the `type:num:ts` msg_id),
161
+ * so the `(run_id, agent)` join is structurally impossible HERE. A
162
+ * github-route edge's `processed` therefore stays `null` in the ledger BY
163
+ * DESIGN; its delivery≠turn distinction lives in the reconciler's view, not
164
+ * this one. `backfillProcessed` is the a2a-side join.
165
+ *
166
+ * `keyOf` is left channel-agnostic on purpose (it just reads `msg_id` for the
167
+ * a2a join); edges whose `processed` is already non-null are left untouched
168
+ * (idempotent).
169
+ */
170
+ export function backfillProcessed(edges, receiptKeys, keyOf) {
171
+ return edges.map((e) => e.processed === null ? { ...e, processed: receiptKeys.has(keyOf(e)) } : e);
172
+ }
173
+ /**
174
+ * Gather + merge per-agent ledgers into one fleet view, ordered by `ts`.
175
+ *
176
+ * The per-agent ledger is the durable floor; Tempo is the central convenience
177
+ * index. When Tempo is down, fleet-graph analysis falls back to merging the
178
+ * per-agent ledgers. On the single-host substrate this is trivial — all the
179
+ * `comms-ledger.jsonl` files are local. For a MULTI-HOST fleet the gather step
180
+ * is explicit and operator-defined: collect each host's ledger (e.g.
181
+ * `rsync <agent-host>:.../comms-ledger.jsonl ./gathered/<agent>.jsonl`) into one
182
+ * place, then call this. There is deliberately NO central durable sink — that
183
+ * would reintroduce the single point of failure DR-025 exists to avoid.
184
+ *
185
+ * Malformed lines are skipped (a corrupt line must not blind the whole gather);
186
+ * this is the one place a parse error is tolerated, because gather is a derived
187
+ * read, not the authoritative write.
188
+ */
189
+ export function mergeLedgers(contents) {
190
+ const edges = [];
191
+ for (const content of contents) {
192
+ for (const raw of content.split('\n')) {
193
+ const line = raw.trim();
194
+ if (!line)
195
+ continue;
196
+ const parsed = CommsLedgerEdgeSchema.safeParse(JSON.parse(safeJson(line)));
197
+ if (parsed.success)
198
+ edges.push(parsed.data);
199
+ }
200
+ }
201
+ return edges.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
202
+ }
203
+ /** Parse-guard: return the line if it is JSON-parseable, else an empty object literal. */
204
+ function safeJson(line) {
205
+ try {
206
+ JSON.parse(line);
207
+ return line;
208
+ }
209
+ catch {
210
+ return '{}';
211
+ }
212
+ }
213
+ /**
214
+ * Rotation/retention policy (DR-025 §"Costs", "no silent caps"):
215
+ *
216
+ * The ledger is the PERMANENT record, so rotation is deliberate and must NEVER
217
+ * silently truncate. The writer here only ever appends — it has no size cap and
218
+ * no truncation path by construction. If an operator rotates the file for size,
219
+ * the canonical action is archive-on-rotate (move the old file aside, e.g.
220
+ * `comms-ledger.jsonl.<date>`), never `> truncate` or head/tail clipping. Size
221
+ * management is an operator concern, intentionally outside this hot-path writer.
222
+ */
223
+ export const ROTATION_POLICY = 'archive-on-rotate; never silent truncation';
224
+ //# sourceMappingURL=comms-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comms-ledger.js","sourceRoot":"","sources":["../src/comms-ledger.ts"],"names":[],"mappings":"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"}