@groundnuty/macf-channel-server 0.2.36 → 0.2.38

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.
@@ -45,7 +45,8 @@ export type CollisionResult = {
45
45
  * squatter that motivated this), so a versioned incoming takes it over.
46
46
  *
47
47
  * Quadrant (incoming × existing-alive), assuming the existing peer answers
48
- * `/health` (dead takeover regardless):
48
+ * `/health` on every re-confirm ping (dead, OR alive-then-dead flicker per the
49
+ * just-killed-port race groundnuty/macf#553 → takeover regardless):
49
50
  *
50
51
  * incoming versioned, existing unversioned → takeover (takeover_unversioned_existing)
51
52
  * incoming versioned, existing older → takeover (takeover_newer_version)
@@ -1 +1 @@
1
- {"version":3,"file":"collision.d.ts","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAiB,MAAM,uBAAuB,CAAC;AAKjE,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAQrD;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,IAAI;CAYpD;AAID;;0EAE0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAkFD,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE;IACT,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,EACD,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CA4E1B"}
1
+ {"version":3,"file":"collision.d.ts","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAiB,MAAM,uBAAuB,CAAC;AAKjE,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAQrD;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,IAAI;CAYpD;AA2BD;;0EAE0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AA2GD,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE;IACT,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,EACD,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAgF1B"}
package/dist/collision.js CHANGED
@@ -30,6 +30,26 @@ export class RegisterRaceError extends MacfError {
30
30
  }
31
31
  }
32
32
  const HEALTH_PING_TIMEOUT_MS = 5000;
33
+ /**
34
+ * Liveness re-confirmation against the just-killed-port race (groundnuty/macf#553).
35
+ *
36
+ * A killed channel server's listening socket can momentarily still accept a
37
+ * TCP/TLS connection and answer ONE `/health` 2xx during the narrow window
38
+ * before the OS finishes releasing the port. Trusting that single 2xx
39
+ * mis-classifies a just-killed prior instance as `alive`, and the #424 version
40
+ * quadrant then aborts the restart against a doomed peer — stranding the slot
41
+ * until a manual registry delete.
42
+ *
43
+ * To be robust we require HEALTH_CONFIRM_ATTEMPTS *consecutive* 2xx answers
44
+ * (HEALTH_CONFIRM_DELAY_MS apart). Any failed attempt short-circuits to dead →
45
+ * the caller takes over the slot. A genuinely-live peer answers every attempt →
46
+ * still alive → still aborts (no groundnuty/macf#424 regression). Bounded: at
47
+ * most HEALTH_CONFIRM_ATTEMPTS pings and (HEALTH_CONFIRM_ATTEMPTS - 1) delays of
48
+ * added startup latency, paid only when the first ping is alive.
49
+ */
50
+ const HEALTH_CONFIRM_ATTEMPTS = 2;
51
+ const HEALTH_CONFIRM_DELAY_MS = 300;
52
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
33
53
  /**
34
54
  * Ping an agent's /health endpoint via mTLS.
35
55
  * Returns `{ alive, version }`: alive iff the agent responds 2xx; version is
@@ -100,6 +120,26 @@ function pingHealth(host, port, caCertPath, agentCertPath, agentKeyPath, timeout
100
120
  req.end();
101
121
  });
102
122
  }
123
+ /**
124
+ * Classify the existing peer's liveness, robust to the just-killed-port race
125
+ * (groundnuty/macf#553). Pings `/health` up to HEALTH_CONFIRM_ATTEMPTS times,
126
+ * waiting HEALTH_CONFIRM_DELAY_MS between attempts. The FIRST non-alive answer
127
+ * short-circuits to dead (`{ alive:false, version:null }`), so a flickering
128
+ * just-killed peer is treated as dead → the caller takes over the slot. Only a
129
+ * peer that answers 2xx on EVERY attempt is classified alive; the returned
130
+ * version is that of the final (still-answering) ping, feeding the #424 quadrant.
131
+ */
132
+ async function confirmLiveness(host, port, caCertPath, agentCertPath, agentKeyPath) {
133
+ let result = { alive: false, version: null };
134
+ for (let attempt = 0; attempt < HEALTH_CONFIRM_ATTEMPTS; attempt += 1) {
135
+ if (attempt > 0)
136
+ await sleep(HEALTH_CONFIRM_DELAY_MS);
137
+ result = await pingHealth(host, port, caCertPath, agentCertPath, agentKeyPath);
138
+ if (!result.alive)
139
+ return { alive: false, version: null };
140
+ }
141
+ return result;
142
+ }
103
143
  /**
104
144
  * Check if an agent is already registered and alive.
105
145
  *
@@ -114,7 +154,8 @@ function pingHealth(host, port, caCertPath, agentCertPath, agentKeyPath, timeout
114
154
  * squatter that motivated this), so a versioned incoming takes it over.
115
155
  *
116
156
  * Quadrant (incoming × existing-alive), assuming the existing peer answers
117
- * `/health` (dead takeover regardless):
157
+ * `/health` on every re-confirm ping (dead, OR alive-then-dead flicker per the
158
+ * just-killed-port race groundnuty/macf#553 → takeover regardless):
118
159
  *
119
160
  * incoming versioned, existing unversioned → takeover (takeover_unversioned_existing)
120
161
  * incoming versioned, existing older → takeover (takeover_newer_version)
@@ -147,7 +188,11 @@ export async function checkCollision(name, registry, certPaths, incomingVersion,
147
188
  port: existing.port,
148
189
  instance_id: existing.instance_id,
149
190
  });
150
- const { alive, version: existingVersion } = await pingHealth(existing.host, existing.port, certPaths.caCertPath, certPaths.agentCertPath, certPaths.agentKeyPath);
191
+ // Liveness is re-confirmed across HEALTH_CONFIRM_ATTEMPTS pings to defeat the
192
+ // just-killed-port race (groundnuty/macf#553): a single momentary 2xx from a
193
+ // dying prior instance must NOT count as alive. A confirmed-live peer then
194
+ // flows into the unchanged #424 version quadrant below.
195
+ const { alive, version: existingVersion } = await confirmLiveness(existing.host, existing.port, certPaths.caCertPath, certPaths.agentCertPath, certPaths.agentKeyPath);
151
196
  if (alive) {
152
197
  const versionTakeoverDisabled = process.env['MACF_NO_VERSION_TAKEOVER'] === '1';
153
198
  const incomingVersioned = VERSION_PATTERN.test(incomingVersion);
@@ -1 +1 @@
1
- {"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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"}
@@ -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"}