@opengeni/events 0.4.12 → 0.4.15-canary.0
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.
- package/dist/coalesce.d.ts +6 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +27 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/coalesce.ts +25 -3
- package/src/index.ts +30 -15
package/dist/coalesce.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type SessionEvent } from "@opengeni/contracts";
|
|
2
2
|
/** Flush long runs incrementally before concatenation can become unbounded. */
|
|
3
3
|
export declare const SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES: number;
|
|
4
|
+
export type CoalescedSessionEventPage = {
|
|
5
|
+
events: SessionEvent[];
|
|
6
|
+
/** Durable raw sequence covered by each returned synthetic event sequence. */
|
|
7
|
+
coveredThroughBySequence: ReadonlyMap<number, number>;
|
|
8
|
+
};
|
|
4
9
|
export declare function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[];
|
|
10
|
+
export declare function coalesceSessionEventDeltasWithCoverage(events: SessionEvent[]): CoalescedSessionEventPage;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type EventBusOptions = {
|
|
|
10
10
|
/** Test/host transport seam; production defaults to the nats.js connector. */
|
|
11
11
|
connect?: typeof connect;
|
|
12
12
|
};
|
|
13
|
-
export { SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES, coalesceSessionEventDeltas } from "./coalesce.js";
|
|
13
|
+
export { SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES, coalesceSessionEventDeltas, coalesceSessionEventDeltasWithCoverage, type CoalescedSessionEventPage, } from "./coalesce.js";
|
|
14
14
|
/** Comfortably below NATS Core's common 1 MiB max_payload default. */
|
|
15
15
|
export declare const SESSION_EVENT_NATS_MESSAGE_MAX_BYTES: number;
|
|
16
16
|
/** Payload is <=64 KiB; the larger envelope leaves deterministic wire headroom. */
|
|
@@ -259,13 +259,13 @@ export declare function appendAndPublishTurnEventsFenced(db: Database, bus: Even
|
|
|
259
259
|
export declare function formatSse<T extends {
|
|
260
260
|
sequence: number;
|
|
261
261
|
type: string;
|
|
262
|
-
}>(event: T): string;
|
|
262
|
+
}>(event: T, idSequence?: number): string;
|
|
263
263
|
/** Canonical one-event NATS payload with an exact broker byte assertion. */
|
|
264
264
|
export declare function workspaceControlEventNatsPayload(event: WorkspaceControlEvent): Uint8Array;
|
|
265
265
|
/** Defensively bounds current and historical workspace invalidations per frame. */
|
|
266
266
|
export declare function formatWorkspaceControlEventSse(event: WorkspaceControlEvent): string;
|
|
267
267
|
/** Defensively bounds historical rows before they become one SSE frame. */
|
|
268
|
-
export declare function formatSessionEventSse(event: SessionEvent): string;
|
|
268
|
+
export declare function formatSessionEventSse(event: SessionEvent, coveredThrough?: number): string;
|
|
269
269
|
/**
|
|
270
270
|
* Split an already-durable batch by exact encoded NATS bytes. Each event is
|
|
271
271
|
* defensively normalized first so historical oversized rows cannot exceed the
|
|
@@ -278,6 +278,8 @@ export declare function boundSessionEventHttpPage(events: readonly SessionEvent[
|
|
|
278
278
|
maxBytes?: number;
|
|
279
279
|
/** Exact mode is restricted to already-canonical forensic REST rows. */
|
|
280
280
|
eventProjection?: "bounded" | "exact";
|
|
281
|
+
/** Out-of-band raw coverage for events synthesized by trusted coalescing. */
|
|
282
|
+
coveredThroughBySequence?: ReadonlyMap<number, number>;
|
|
281
283
|
}): {
|
|
282
284
|
events: SessionEvent[];
|
|
283
285
|
truncated: boolean;
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
boundSessionEventPayload as boundSessionEventPayload2,
|
|
5
5
|
boundWorkspaceControlEvent,
|
|
6
6
|
sessionEventJsonBytes,
|
|
7
|
-
sessionEventPayloadTruncation
|
|
7
|
+
sessionEventPayloadTruncation as sessionEventPayloadTruncation2
|
|
8
8
|
} from "@opengeni/contracts";
|
|
9
9
|
import {
|
|
10
10
|
appendSessionEvents,
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
} from "nats";
|
|
18
18
|
|
|
19
19
|
// src/coalesce.ts
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
boundSessionEventPayload,
|
|
22
|
+
sessionEventPayloadTruncation
|
|
23
|
+
} from "@opengeni/contracts";
|
|
21
24
|
var COALESCIBLE_DELTA_TYPES = /* @__PURE__ */ new Set([
|
|
22
25
|
"agent.message.delta",
|
|
23
26
|
"agent.reasoning.delta",
|
|
@@ -26,7 +29,11 @@ var COALESCIBLE_DELTA_TYPES = /* @__PURE__ */ new Set([
|
|
|
26
29
|
var SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES = 48 * 1024;
|
|
27
30
|
var encoder = new TextEncoder();
|
|
28
31
|
function coalesceSessionEventDeltas(events) {
|
|
32
|
+
return coalesceSessionEventDeltasWithCoverage(events).events;
|
|
33
|
+
}
|
|
34
|
+
function coalesceSessionEventDeltasWithCoverage(events) {
|
|
29
35
|
const coalesced = [];
|
|
36
|
+
const coveredThroughBySequence = /* @__PURE__ */ new Map();
|
|
30
37
|
let run = null;
|
|
31
38
|
const flush = () => {
|
|
32
39
|
if (!run) {
|
|
@@ -48,16 +55,19 @@ function coalesceSessionEventDeltas(events) {
|
|
|
48
55
|
};
|
|
49
56
|
coalesced.push({
|
|
50
57
|
...run.first,
|
|
58
|
+
coveredThrough: run.lastSequence,
|
|
51
59
|
payload: boundSessionEventPayload(payload, {
|
|
52
60
|
surface: "http_projection"
|
|
53
61
|
})
|
|
54
62
|
});
|
|
63
|
+
coveredThroughBySequence.set(run.first.sequence, run.lastSequence);
|
|
55
64
|
run = null;
|
|
56
65
|
};
|
|
57
66
|
for (const event of events) {
|
|
58
67
|
if (!isCoalescibleDelta(event)) {
|
|
59
68
|
flush();
|
|
60
69
|
coalesced.push(event);
|
|
70
|
+
coveredThroughBySequence.set(event.sequence, event.sequence);
|
|
61
71
|
continue;
|
|
62
72
|
}
|
|
63
73
|
const isSandbox = event.type === "sandbox.command.output.delta";
|
|
@@ -88,10 +98,10 @@ function coalesceSessionEventDeltas(events) {
|
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
100
|
flush();
|
|
91
|
-
return coalesced;
|
|
101
|
+
return { events: coalesced, coveredThroughBySequence };
|
|
92
102
|
}
|
|
93
103
|
function isCoalescibleDelta(event) {
|
|
94
|
-
return COALESCIBLE_DELTA_TYPES.has(event.type);
|
|
104
|
+
return COALESCIBLE_DELTA_TYPES.has(event.type) && sessionEventPayloadTruncation(event.payload) === null;
|
|
95
105
|
}
|
|
96
106
|
function sameDeltaRun(first, next, firstSandboxName, nextSandboxName) {
|
|
97
107
|
if (first.type !== next.type) {
|
|
@@ -691,9 +701,10 @@ function subscribeAgentEvents(nc, subject, handler) {
|
|
|
691
701
|
sub.unsubscribe();
|
|
692
702
|
};
|
|
693
703
|
}
|
|
694
|
-
function formatSse(event) {
|
|
704
|
+
function formatSse(event, idSequence = event.sequence) {
|
|
705
|
+
const trustedId = Number.isSafeInteger(idSequence) && idSequence >= event.sequence ? idSequence : event.sequence;
|
|
695
706
|
return [
|
|
696
|
-
`id: ${
|
|
707
|
+
`id: ${trustedId}`,
|
|
697
708
|
`event: ${event.type}`,
|
|
698
709
|
`data: ${JSON.stringify(event)}`,
|
|
699
710
|
"",
|
|
@@ -725,9 +736,9 @@ function formatWorkspaceControlEventSse(event) {
|
|
|
725
736
|
}
|
|
726
737
|
return formatted;
|
|
727
738
|
}
|
|
728
|
-
function formatSessionEventSse(event) {
|
|
739
|
+
function formatSessionEventSse(event, coveredThrough = event.sequence) {
|
|
729
740
|
const bounded = boundSessionEventForSurface(event, "sse_legacy_guard");
|
|
730
|
-
const formatted = formatSse(bounded);
|
|
741
|
+
const formatted = formatSse(bounded, coveredThrough);
|
|
731
742
|
if (new TextEncoder().encode(formatted).byteLength > SESSION_EVENT_SSE_FRAME_MAX_BYTES) {
|
|
732
743
|
const minimal = {
|
|
733
744
|
...bounded,
|
|
@@ -743,7 +754,7 @@ function formatSessionEventSse(event) {
|
|
|
743
754
|
{ surface: "sse_legacy_guard", maxBytes: 4096 }
|
|
744
755
|
)
|
|
745
756
|
};
|
|
746
|
-
return formatSse(minimal);
|
|
757
|
+
return formatSse(minimal, coveredThrough);
|
|
747
758
|
}
|
|
748
759
|
return formatted;
|
|
749
760
|
}
|
|
@@ -804,7 +815,10 @@ function boundSessionEventHttpPage(events, options) {
|
|
|
804
815
|
return {
|
|
805
816
|
events: selected,
|
|
806
817
|
truncated,
|
|
807
|
-
nextSequence: edge === void 0 ? null : options.direction === "after" ?
|
|
818
|
+
nextSequence: edge === void 0 ? null : options.direction === "after" ? Math.max(
|
|
819
|
+
edge.sequence,
|
|
820
|
+
options.coveredThroughBySequence?.get(edge.sequence) ?? edge.sequence
|
|
821
|
+
) : edge.sequence,
|
|
808
822
|
bytes
|
|
809
823
|
};
|
|
810
824
|
}
|
|
@@ -834,21 +848,14 @@ function boundWorkspaceControlHttpPage(events, maxBytes = WORKSPACE_CONTROL_HTTP
|
|
|
834
848
|
};
|
|
835
849
|
}
|
|
836
850
|
function sessionEventResumeSequence(event) {
|
|
837
|
-
|
|
838
|
-
return event.sequence;
|
|
839
|
-
}
|
|
840
|
-
const coalescedUntil = Number(event.payload.coalescedUntil);
|
|
841
|
-
return Math.max(
|
|
842
|
-
event.sequence,
|
|
843
|
-
Number.isFinite(coalescedUntil) ? Math.floor(coalescedUntil) : event.sequence
|
|
844
|
-
);
|
|
851
|
+
return typeof event.coveredThrough === "number" && Number.isSafeInteger(event.coveredThrough) && event.coveredThrough >= event.sequence ? event.coveredThrough : event.sequence;
|
|
845
852
|
}
|
|
846
853
|
function boundSessionEventForSurface(event, surface) {
|
|
847
854
|
return boundSessionEvent(event, { surface });
|
|
848
855
|
}
|
|
849
856
|
function observeEventBoundaries(events, logger) {
|
|
850
857
|
for (const event of events) {
|
|
851
|
-
const boundary =
|
|
858
|
+
const boundary = sessionEventPayloadTruncation2(event.payload);
|
|
852
859
|
if (!boundary) continue;
|
|
853
860
|
(logger?.debug ?? silentLogger.debug)("Session event payload is a bounded audit preview", {
|
|
854
861
|
eventType: event.type,
|
|
@@ -879,6 +886,7 @@ export {
|
|
|
879
886
|
boundSessionEventHttpPage,
|
|
880
887
|
boundWorkspaceControlHttpPage,
|
|
881
888
|
coalesceSessionEventDeltas,
|
|
889
|
+
coalesceSessionEventDeltasWithCoverage,
|
|
882
890
|
connect2 as connect,
|
|
883
891
|
createNatsEventBus,
|
|
884
892
|
createResponderConnection,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/coalesce.ts","../src/nats-jwt.ts"],"sourcesContent":["import {\n boundSessionEvent,\n boundSessionEventPayload,\n boundWorkspaceControlEvent,\n sessionEventJsonBytes,\n sessionEventPayloadTruncation,\n type SessionBusMessage,\n type SessionEvent,\n type SessionEventBoundarySurface,\n type WorkspaceControlEvent,\n} from \"@opengeni/contracts\";\nimport {\n appendSessionEvents,\n appendSessionEventsForTurnAttempt,\n sessionSubject,\n type AppendEventInput,\n type CanonicalTurnStartupMilestoneReceipt,\n type Database,\n type SessionEventAppendObserver,\n} from \"@opengeni/db\";\nimport {\n connect,\n JSONCodec,\n type ConnectionOptions,\n type Msg,\n type NatsConnection,\n type Subscription,\n} from \"nats\";\n\nconst codec = JSONCodec<SessionBusMessage | SessionEvent | WorkspaceControlEvent>();\n\nexport type EventLogger = {\n debug?: (message: string, attributes?: Record<string, unknown>) => void;\n warn?: (message: string, attributes?: Record<string, unknown>) => void;\n};\n\nexport type EventBusOptions = {\n logger?: EventLogger;\n /** Test/host transport seam; production defaults to the nats.js connector. */\n connect?: typeof connect;\n};\n\nconst silentLogger: Required<EventLogger> = {\n debug: () => {},\n warn: () => {},\n};\n\nexport { SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES, coalesceSessionEventDeltas } from \"./coalesce\";\n\n/**\n * Reconnect + keepalive defaults applied to EVERY long-lived NATS connection\n * this package opens (the event bus AND the standalone auth-callout responder).\n *\n * The production outage these guard against: an in-cluster NATS broker pod\n * restart. nats.js's stock policy gives up after ~10 attempts (~20s) and the\n * client goes permanently CONNECTION_CLOSED — which takes the whole control\n * plane down with it: every session-create publishes events to NATS, and the\n * API-hosted auth-callout responder dies so BYO agents get \"authorization\n * violation\". Recovery then required a MANUAL api+worker restart. With these\n * options the client retries forever and auto-recovers the moment the broker\n * returns. Factored into one source of truth so the call sites never drift.\n *\n * - `reconnect` + `maxReconnectAttempts: -1` — never give up (infinite retry).\n * - `reconnectTimeWait` (2s base) + `reconnectJitter`/`reconnectJitterTLS`\n * (up to 1s) — a fleet of api/worker pods doesn't thundering-herd the broker\n * on recovery.\n * - `waitOnFirstConnect` — a broker briefly unavailable at boot must not\n * hard-fail the process; the client keeps trying instead of throwing.\n * - `pingInterval`/`maxPingOut` — promptly detect a silently-dead socket so the\n * reconnect machinery actually engages instead of hanging on a zombie.\n */\nconst RECONNECT_OPTIONS = {\n reconnect: true,\n maxReconnectAttempts: -1,\n reconnectTimeWait: 2_000,\n reconnectJitter: 1_000,\n reconnectJitterTLS: 1_000,\n waitOnFirstConnect: true,\n pingInterval: 20_000,\n maxPingOut: 3,\n} satisfies ConnectionOptions;\n\n/**\n * The single source of truth for a long-lived connection's resilience: merge the\n * reconnect/keepalive defaults UNDER the caller's connection options (servers +\n * optional auth/name). Every long-lived `connect()` in this package goes through\n * here so the two call sites can never diverge.\n */\nfunction withReconnectDefaults(options: ConnectionOptions): ConnectionOptions {\n return { ...RECONNECT_OPTIONS, ...options };\n}\n\n/** How long a best-effort publish waits on `flush()` before giving up (see `publish`). */\nconst PUBLISH_FLUSH_TIMEOUT_MS = 2_000;\n\n/** Comfortably below NATS Core's common 1 MiB max_payload default. */\nexport const SESSION_EVENT_NATS_MESSAGE_MAX_BYTES = 512 * 1024;\n/** Payload is <=64 KiB; the larger envelope leaves deterministic wire headroom. */\nexport const SESSION_EVENT_SSE_FRAME_MAX_BYTES = 96 * 1024;\n/** Independent count+byte envelope for one durable HTTP replay response. */\nexport const SESSION_EVENT_HTTP_PAGE_MAX_BYTES = 1024 * 1024;\n/** Workspace invalidations are one compact event, never a broker evidence blob. */\nexport const WORKSPACE_CONTROL_NATS_MESSAGE_MAX_BYTES = 32 * 1024;\n/** Count+byte envelope for one workspace-control REST replay page. */\nexport const WORKSPACE_CONTROL_HTTP_PAGE_MAX_BYTES = 1024 * 1024;\n\n/**\n * Await `nc.flush()` but never longer than `timeoutMs`. With infinite reconnect a\n * `flush()` issued while the broker is down does NOT reject — it pends until the\n * broker returns, which can be minutes. Racing it against a timer keeps a long\n * outage from stalling an in-flight turn; the published message stays buffered\n * and is delivered on reconnect regardless. A flush rejection (connection fully\n * CLOSED) is swallowed here so the timeout race never leaks an unhandled\n * rejection — the caller's publish path is what logs the drop.\n */\nasync function flushWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n });\n try {\n await Promise.race([nc.flush().catch(() => undefined), timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Flush a durable outbox publication and reject unless the NATS server confirms\n * the write within the bounded wait. A timeout can still leave the original\n * bytes buffered for reconnect, so callers must remain duplicate-safe when they\n * retry the durable obligation.\n */\nasync function flushConfirmedWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error(`NATS publish confirmation timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n });\n try {\n await Promise.race([nc.flush(), timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Drain a long-lived connection's status async-iterator to the log so a future\n * broker outage is OBSERVABLE (disconnect → reconnecting → reconnect → update).\n * Fire-and-forget for the connection's lifetime; the loop ends when the\n * connection closes. `label` distinguishes the event-bus connection from the\n * auth-callout responder in the logs.\n */\nfunction logConnectionStatus(\n nc: NatsConnection,\n label: string,\n logger: EventLogger = silentLogger,\n onStatus?: (type: string) => void,\n): void {\n void (async () => {\n try {\n for await (const status of nc.status()) {\n onStatus?.(status.type);\n const attributes = { label, status: status.type, data: status.data };\n if (isWarnNatsStatus(status.type)) {\n (logger.warn ?? silentLogger.warn)(\"NATS connection status\", attributes);\n } else {\n (logger.debug ?? silentLogger.debug)(\"NATS connection status\", attributes);\n }\n }\n } catch {\n // The status iterator simply ends when the connection closes; never let it\n // throw out of this background loop.\n }\n })();\n}\n\nfunction isWarnNatsStatus(type: string): boolean {\n return type === \"disconnect\" || type === \"error\" || type === \"staleConnection\";\n}\n\nexport {\n decodeAuthRequest,\n mintAuthResponse,\n mintUserJwt,\n parseAgentConnectionName,\n workspaceAgentPermissions,\n type DecodedAuthRequest,\n type MintAuthResponseInput,\n type MintUserJwtInput,\n type NatsPermission,\n type NatsPermissions,\n} from \"./nats-jwt\";\n\n// Re-export the raw NATS primitives a consumer needs to open a direct connection or\n// generate nkeys (the auth-callout responder's standalone connection, the\n// agent-simulating integration tests). This keeps `nats` an internal dependency of\n// this leaf — callers in the bun workspace reach it through @opengeni/events rather\n// than depending on `nats` directly.\nexport { connect, nkeys, type NatsConnection } from \"nats\";\n\n/**\n * A raw request/reply reply — just the response bytes. Mirrors the subset of the\n * NATS `Msg` shape a binary request/reply caller needs (`NatsControlRpc` consumes\n * exactly this). Kept minimal so the events package does not leak the `nats` `Msg`\n * type into the agent-loop-free runtime leaf.\n */\nexport type RequestReply = { data: Uint8Array };\n\n/**\n * The minimal request/reply connection the selfhosted control plane consumes\n * (structurally identical to `@opengeni/runtime`'s `NatsRequestConnection`). The\n * API/worker hand this accessor to `NatsControlRpc` so the control transport rides\n * the SAME managed NATS connection the event bus already owns — a NATS connection\n * natively supports both pub/sub and request/reply, so there is NEVER a second\n * connection.\n */\nexport interface RequestConnection {\n request(subject: string, payload: Uint8Array, opts: { timeout: number }): Promise<RequestReply>;\n}\n\n/**\n * The raw subscribe/publish surface the selfhosted OP-STREAM transport consumes\n * (structurally identical to `@opengeni/runtime`'s `NatsOpStreamConnection`):\n * a plain subscription for the runner's fire-and-forget op frames\n * (the exact process generation's `.op.<op_id>`) and a plain publish for acks\n * (that same generation's `.ack`). Same managed connection as everything else — a NATS\n * connection natively supports all of it; there is NEVER a second connection.\n */\nexport interface OpStreamConnection {\n subscribe(subject: string): AsyncIterable<{ data: Uint8Array }> & { unsubscribe(): void };\n publish(subject: string, payload: Uint8Array): void;\n /** Subscription/publish barrier used before reading durable authority. */\n flush?(): Promise<void>;\n}\n\n/**\n * A handler answering a request/reply on a subscribed subject: given the request\n * bytes (+ the concrete subject the message landed on, for exact-process RPC\n * style wildcard routing), return the response bytes to reply with. A thrown error\n * leaves the request unanswered (the caller's request times out / sees no\n * responder), which the control plane maps to `agent_offline` / reconnecting.\n */\nexport type RequestHandler = (\n request: Uint8Array,\n subject: string,\n) => Promise<Uint8Array> | Uint8Array;\n\n/**\n * Versioned recovery contract for already-durable session-event fanout.\n *\n * A publisher acknowledgement alone cannot prove that every API subscriber was\n * connected for the live message. Supported broker-backed buses must therefore\n * notify consumers after transport subscriptions have been restored so an\n * already-open SSE stream can run one bounded Postgres catch-up. A bus that can\n * never disconnect may implement this as a listener registry that never fires.\n */\nexport const SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION = 1 as const;\n\nexport type SessionEventDurableFanoutCapability = {\n version: typeof SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION;\n subscribeRecovery: (onRecovery: (generation: number) => void) => () => void;\n};\n\nexport function requireSessionEventDurableFanoutCapability(\n bus: unknown,\n): SessionEventDurableFanoutCapability {\n const capability = (bus as { sessionEventDurableFanout?: unknown } | null)\n ?.sessionEventDurableFanout as\n | { version?: unknown; subscribeRecovery?: unknown }\n | null\n | undefined;\n if (\n capability?.version !== SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION ||\n typeof capability.subscribeRecovery !== \"function\"\n ) {\n throw new Error(\n \"EventBus must provide sessionEventDurableFanout v1 so accepted durable publications reconcile after subscriber reconnect\",\n );\n }\n return capability as SessionEventDurableFanoutCapability;\n}\n\nexport type EventBus = {\n /**\n * Mandatory paired recovery contract for durable session-event publication.\n * API and worker instances sharing one broker must expose the same semantics.\n */\n sessionEventDurableFanout: SessionEventDurableFanoutCapability;\n /**\n * Publish a session-event batch. Embedding implementations without the\n * optional publishConfirmed capability must resolve only after their\n * transport has accepted the batch and reject transport failures so durable\n * outbox callers can retry safely. Ordinary live-fanout callers remain\n * best-effort because they catch failures around this contract.\n */\n publish: (workspaceId: string, sessionId: string, events: SessionEvent[]) => Promise<void>;\n /**\n * Publish an already-durable batch and reject unless the transport confirms\n * acceptance with a stronger provider-specific acknowledgement. Optional for\n * embedding-host buses; durable fanout reconcilers fall back to the required\n * publish promise when this capability is unavailable.\n */\n publishConfirmed?: (\n workspaceId: string,\n sessionId: string,\n events: SessionEvent[],\n ) => Promise<void>;\n subscribe: (\n workspaceId: string,\n sessionId: string,\n onEvents: (events: SessionEvent[]) => void | Promise<void>,\n ) => Promise<() => void>;\n /** Best-effort live invalidation; the event is already durable in Postgres. */\n publishWorkspaceControl: (workspaceId: string, event: WorkspaceControlEvent) => Promise<void>;\n /** One workspace subscription fans a control change to every open descendant view. */\n subscribeWorkspaceControl: (\n workspaceId: string,\n onEvent: (event: WorkspaceControlEvent) => void | Promise<void>,\n ) => Promise<() => void>;\n /**\n * Issue a binary request/reply on a subject over the bus's NATS connection\n * (the selfhosted control plane's exact claimed process subject). A new usage of what was\n * a one-way bus — same connection, native NATS request/reply. Rejects on a\n * no-responder (NATS 503) or a request timeout; the caller (`NatsControlRpc`)\n * maps those to `agent_offline` / `agent_reconnecting`, never a NotFound.\n */\n request: (\n subject: string,\n payload: Uint8Array,\n opts: { timeoutMs: number },\n ) => Promise<RequestReply>;\n /**\n * Subscribe-and-reply on a subject (the responder side — the enrolled agent, or\n * a test stand-in for it): for every request on `subject`, call `handler` and\n * `respond` with its bytes over the SAME connection. Returns an unsubscribe fn.\n * A subject may be a NATS wildcard (e.g. `agent.*.*.connection.*.rpc`).\n */\n subscribeRequests: (subject: string, handler: RequestHandler) => () => void;\n /**\n * Subscribe to the agent EVENT plane (the one-way fire-and-forget heartbeats +\n * going-offline the agent PUBLISHES on its exact process `.events`, NOT a\n * request/reply). The M10 metrics-ingestion consumer subscribes the wildcard\n * `agent.*.*.connection.*.events` and gets each raw payload plus its concrete\n * subject (so it can extract `<ws>`/`<id>`/`<instance>`). Returns an\n * unsubscribe fn. Decoding the AgentEvent is the caller's concern (this leaf\n * does not depend on `@opengeni/agent-proto`).\n */\n subscribeAgentEvents: (\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n ) => () => void;\n /**\n * The `RequestConnection` accessor the selfhosted `NatsControlRpc` consumes —\n * the SAME managed connection (pub/sub + request/reply share it). The control\n * plane injects this so the transport never opens a second connection.\n */\n getRequestConnection: () => RequestConnection;\n /**\n * The `OpStreamConnection` accessor the selfhosted op-stream transport\n * consumes (`NatsOpStreamTransport`) — the same managed connection again.\n * Optional so bus test doubles that never exercise op-stream stay valid.\n */\n getOpStreamConnection?: () => OpStreamConnection;\n isConnected?: () => boolean;\n close: () => Promise<void>;\n};\n\n/**\n * Connect the event bus + control-plane request/reply over ONE managed NATS\n * connection. `auth` is the PRIVILEGED control-plane login (M-AUTH): when the\n * server runs with auth_callout, the api/worker authenticates as a static account\n * user permitted to request exact generation-fenced agent RPC subjects + receive\n * its inbox replies. When `auth`\n * is omitted the connection is anonymous (local dev / a NATS without auth_callout)\n * — the existing behavior, unchanged.\n */\nexport async function createNatsEventBus(\n natsUrl: string,\n auth?: { user: string; pass: string },\n options: EventBusOptions = {},\n): Promise<EventBus> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (auth) {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n }\n const nc = await (options.connect ?? connect)(withReconnectDefaults(connectOptions));\n let connected = true;\n let reconnectGeneration = 0;\n const reconnectSubscribers = new Set<(generation: number) => void>();\n logConnectionStatus(nc, \"event-bus\", options.logger, (type) => {\n if (\n type === \"disconnect\" ||\n type === \"reconnecting\" ||\n type === \"staleConnection\" ||\n type === \"error\"\n ) {\n connected = false;\n } else if (type === \"connect\" || type === \"reconnect\") {\n connected = true;\n }\n if (type === \"reconnect\") {\n reconnectGeneration += 1;\n for (const subscriber of reconnectSubscribers) {\n try {\n subscriber(reconnectGeneration);\n } catch (error) {\n (options.logger?.warn ?? silentLogger.warn)(\"NATS reconnect observer failed\", {\n label: \"event-bus\",\n reconnectGeneration,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n });\n const requestConnection: RequestConnection = {\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeout),\n };\n const opStreamConnection: OpStreamConnection = {\n subscribe: (subject) => nc.subscribe(subject),\n publish: (subject, payload) => {\n nc.publish(subject, payload);\n },\n flush: () => nc.flush(),\n };\n const publishSessionEvents = (\n workspaceId: string,\n sessionId: string,\n events: SessionEvent[],\n ): void => {\n const batches = sessionEventBatchesByBytes(workspaceId, sessionId, events);\n for (const batch of batches) {\n nc.publish(\n sessionSubject(workspaceId, sessionId),\n codec.encode({ workspaceId, sessionId, events: batch }),\n );\n }\n if (batches.length > 1) {\n (options.logger?.debug ?? silentLogger.debug)(\"NATS session event batch chunked\", {\n workspaceId,\n sessionId,\n eventCount: events.length,\n batchCount: batches.length,\n maxMessageBytes: SESSION_EVENT_NATS_MESSAGE_MAX_BYTES,\n });\n }\n observeEventBoundaries(batches.flat(), options.logger);\n };\n return {\n sessionEventDurableFanout: {\n version: SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION,\n subscribeRecovery: (onRecovery) => {\n reconnectSubscribers.add(onRecovery);\n return () => reconnectSubscribers.delete(onRecovery);\n },\n },\n publish: async (workspaceId, sessionId, events) => {\n if (events.length === 0) {\n return;\n }\n // Best-effort LIVE fan-out. These events are ALREADY durably appended to\n // the DB before we get here (they carry a DB-assigned `sequence`), and\n // every consumer reconciles from that durable log — the server SSE stream\n // replays + gap-backfills via `listSessionEvents`, and the SDK client\n // reconnects and replays from the durable events endpoint. So a publish\n // that fails during a broker blip only delays LIVE delivery (healed by the\n // next successful publish's gap-backfill, or a stream reconnect); it must\n // never throw the in-flight turn to death.\n try {\n publishSessionEvents(workspaceId, sessionId, events);\n } catch (error) {\n // `publish()` throws synchronously only when the connection is fully\n // CLOSED (with infinite reconnect, effectively never outside shutdown).\n (options.logger?.warn ?? silentLogger.warn)(\n \"NATS live publish dropped; events are durable in the DB and reconcile on stream replay\",\n {\n workspaceId,\n sessionId,\n error: error instanceof Error ? error.message : String(error),\n },\n );\n return;\n }\n await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n publishConfirmed: async (workspaceId, sessionId, events) => {\n if (events.length === 0) {\n return;\n }\n publishSessionEvents(workspaceId, sessionId, events);\n await flushConfirmedWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n subscribe: async (workspaceId, sessionId, onEvents) =>\n subscribeSession(nc, workspaceId, sessionId, onEvents),\n publishWorkspaceControl: async (workspaceId, event) => {\n try {\n const encoded = workspaceControlEventNatsPayload(event);\n nc.publish(workspaceControlSubject(workspaceId), encoded);\n } catch (error) {\n (options.logger?.warn ?? silentLogger.warn)(\n \"NATS workspace-control invalidation dropped; clients reconcile from Postgres\",\n {\n workspaceId,\n revision: event.revision,\n error: error instanceof Error ? error.message : String(error),\n },\n );\n return;\n }\n await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n subscribeWorkspaceControl: async (workspaceId, onEvent) => {\n const sub = nc.subscribe(workspaceControlSubject(workspaceId));\n void (async () => {\n for await (const msg of sub) {\n await onEvent(\n boundWorkspaceControlEvent(codec.decode(msg.data) as WorkspaceControlEvent, {\n surface: \"nats_legacy_guard\",\n }),\n );\n }\n })();\n return () => sub.unsubscribe();\n },\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeoutMs),\n subscribeRequests: (subject, handler) => subscribeRequests(nc, subject, handler),\n subscribeAgentEvents: (subject, handler) => subscribeAgentEvents(nc, subject, handler),\n getRequestConnection: () => requestConnection,\n getOpStreamConnection: () => opStreamConnection,\n isConnected: () => connected && !nc.isClosed() && !nc.isDraining(),\n close: async () => {\n reconnectSubscribers.clear();\n await nc.drain();\n },\n };\n}\n\n/**\n * A standalone NATS connection answering request/reply on ONE subject — the\n * transport primitive the auth-callout responder uses. It is DELIBERATELY a\n * SEPARATE connection from the event bus: the callout responder authenticates as\n * the callout account's `auth_users` user (a username/password or token in the\n * `AUTH` account), which is a DIFFERENT identity from the control-plane's\n * privileged account that the event bus + `NatsControlRpc` ride. One connection\n * per identity; never multiplex the two.\n *\n * `request`/`reply` here is the RAW NATS request/reply (`$SYS.REQ.USER.AUTH`): the\n * server publishes an authorization request with a reply inbox; the handler returns\n * the signed authorization-response bytes which we `respond` on that inbox.\n */\nexport interface ResponderConnection {\n /** Subscribe-and-reply on `subject`; returns an async close that drains. */\n close: () => Promise<void>;\n}\n\n/** Connection auth for a standalone NATS connection (the callout responder). */\nexport type NatsConnectAuth =\n | { kind: \"user-password\"; user: string; pass: string }\n | { kind: \"token\"; token: string }\n | { kind: \"anonymous\" };\n\n/**\n * Open a standalone NATS connection and subscribe `subject`, replying to every\n * request with `handler(requestBytes, subject)`. Used by the auth-callout\n * responder to serve `$SYS.REQ.USER.AUTH` as the callout auth user. Returns a\n * handle whose `close()` drains the connection. A handler that throws leaves the\n * request UNANSWERED — for auth-callout that means the server denies the\n * connection on its own timeout, which is the correct fail-closed behavior (a\n * responder bug must never accidentally grant access).\n */\nexport async function createResponderConnection(\n natsUrl: string,\n auth: NatsConnectAuth,\n subject: string,\n handler: RequestHandler,\n options: {\n name?: string;\n logger?: EventLogger;\n connect?: typeof connect;\n } = {},\n): Promise<ResponderConnection> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (options.name) {\n connectOptions.name = options.name;\n }\n if (auth.kind === \"user-password\") {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n } else if (auth.kind === \"token\") {\n connectOptions.token = auth.token;\n }\n const nc = await (options.connect ?? connect)(withReconnectDefaults(connectOptions));\n logConnectionStatus(\n nc,\n options.name ? `auth-callout:${options.name}` : \"auth-callout\",\n options.logger,\n );\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave UNANSWERED — fail-closed. The server denies the connect attempt\n // on its callout timeout; a responder error never grants access.\n }\n }\n })();\n return {\n close: async () => {\n sub.unsubscribe();\n await nc.drain();\n },\n };\n}\n\n/**\n * Optional timing seam for {@link appendAndPublishEvents}: `onAppend` fires after\n * the durable DB write, `onPublish` after the best-effort live fan-out (on both\n * success AND failure of the publish, so a broker blip still records its latency).\n * Kept as a plain callback so the events package takes no dependency on the\n * observability package; the worker wires it to Prometheus histograms.\n */\nexport type AppendPublishObserver = {\n onAppend?: (info: { durationSeconds: number; count: number }) => void;\n onAppendPhase?: SessionEventAppendObserver[\"onPhase\"];\n onPublish?: (info: { durationSeconds: number; count: number }) => void;\n};\n\nexport type AppendPublishOptions = AppendPublishObserver & {\n /** Test/host persistence seam; production uses the database implementation. */\n appendSessionEvents?: typeof appendSessionEvents;\n};\n\n/**\n * Invoke a phase-timing callback with the elapsed seconds since `startedAt` and the\n * event count, swallowing any throw so a metrics sink can never break the\n * append/publish path. Exported for direct unit testing: the wider test suite\n * installs a process-global `mock.module(\"@opengeni/events\")` that stubs\n * `appendAndPublishEvents` (spreading the real module for everything else), so the\n * observer wiring can only be exercised through a helper that survives that mock.\n */\nexport function observeSince(\n fn: ((info: { durationSeconds: number; count: number }) => void) | undefined,\n startedAt: number,\n count: number,\n): void {\n if (!fn) {\n return;\n }\n try {\n fn({\n durationSeconds: Math.max(0, (performance.now() - startedAt) / 1000),\n count,\n });\n } catch {\n // Metrics emission must never affect the append/publish path.\n }\n}\n\nexport async function appendAndPublishEvents(\n db: Database,\n bus: EventBus,\n workspaceId: string,\n sessionId: string,\n events: AppendEventInput[],\n options: AppendPublishOptions = {},\n): Promise<SessionEvent[]> {\n const appendStartedAt = performance.now();\n const appended = await (options.appendSessionEvents ?? appendSessionEvents)(\n db,\n workspaceId,\n sessionId,\n events,\n );\n observeSince(options.onAppend, appendStartedAt, appended.length);\n await publishDurableSessionEvents(bus, workspaceId, sessionId, appended, options);\n return appended;\n}\n\n/**\n * Best-effort live fanout for events another DB helper already committed in\n * the same transaction as related durable state. This must never append again.\n */\nexport async function publishDurableSessionEvents(\n bus: EventBus,\n workspaceId: string,\n sessionId: string,\n appended: SessionEvent[],\n observe?: AppendPublishObserver,\n): Promise<void> {\n if (appended.length === 0) {\n return;\n }\n // The committed DB events are the durable system of record; this publish is only a\n // best-effort LIVE fan-out. Guard it so NO EventBus implementation can throw an\n // in-flight agent turn to death on a transient NATS disconnect — consumers\n // reconcile any missed live events from the durable log via the events/stream\n // endpoint (DB replay + gap-backfill). The managed `createNatsEventBus` bus\n // already swallows internally, so this catch is the belt-and-suspenders guard\n // for any other bus impl (and a fully CLOSED connection during shutdown).\n const publishStartedAt = performance.now();\n try {\n await bus.publish(workspaceId, sessionId, appended);\n } catch {\n console.warn(\"[events] live publish failed; durable events reconcile on stream replay\", {\n errorClass: \"EventPublishOperationError\",\n errorCode: \"session_event_live_publish_failed\",\n origin: \"events\",\n eventCount: appended.length,\n });\n }\n observeSince(observe?.onPublish, publishStartedAt, appended.length);\n}\n\n/** Best-effort fanout for a workspace-control event already committed in PostgreSQL. */\nexport async function publishDurableWorkspaceControlEvent(\n bus: EventBus,\n workspaceId: string,\n event: WorkspaceControlEvent,\n): Promise<void> {\n try {\n await bus.publishWorkspaceControl(workspaceId, event);\n } catch {\n console.warn(\n \"[events] workspace-control live publish failed; durable event reconciles on stream replay\",\n {\n errorClass: \"EventPublishOperationError\",\n errorCode: \"workspace_control_live_publish_failed\",\n origin: \"events\",\n },\n );\n }\n}\n\nexport async function appendAndPublishTurnEventsFenced(\n db: Database,\n bus: EventBus,\n workspaceId: string,\n sessionId: string,\n turnId: string,\n executionGeneration: number,\n attemptId: string,\n events: AppendEventInput[],\n observe?: AppendPublishObserver,\n): Promise<{\n events: SessionEvent[];\n accepted: boolean;\n canonicalStartupMilestones: CanonicalTurnStartupMilestoneReceipt[];\n}> {\n const appendStartedAt = performance.now();\n const result = await appendSessionEventsForTurnAttempt(\n db,\n workspaceId,\n sessionId,\n turnId,\n executionGeneration,\n attemptId,\n events,\n observe?.onAppendPhase ? { onPhase: observe.onAppendPhase } : undefined,\n );\n observeSince(observe?.onAppend, appendStartedAt, result.events.length);\n if (result.events.length === 0) return result;\n const publishStartedAt = performance.now();\n try {\n await bus.publish(workspaceId, sessionId, result.events);\n } catch {\n console.warn(\"[events] live fenced publish failed; events remain durable\", {\n errorClass: \"EventPublishOperationError\",\n errorCode: \"fenced_event_live_publish_failed\",\n origin: \"events\",\n eventCount: result.events.length,\n });\n }\n observeSince(observe?.onPublish, publishStartedAt, result.events.length);\n return result;\n}\n\nfunction subscribeSession(\n nc: NatsConnection,\n workspaceId: string,\n sessionId: string,\n onEvents: (events: SessionEvent[]) => void | Promise<void>,\n): () => void {\n const sub: Subscription = nc.subscribe(sessionSubject(workspaceId, sessionId));\n void (async () => {\n for await (const msg of sub) {\n const decoded = codec.decode(msg.data) as SessionBusMessage | SessionEvent;\n const events = (\"events\" in decoded ? decoded.events : [decoded]).map((event) =>\n boundSessionEventForSurface(event, \"nats_legacy_guard\"),\n );\n await onEvents(events);\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * A binary request/reply over the managed connection. Returns ONLY the reply\n * bytes (the `RequestReply` shape) — the request/reply error semantics (a\n * no-responder NATS 503, a request timeout) propagate as the rejected promise so\n * the caller owns the mapping. The reply is delivered via the connection's\n * built-in mux inbox; no extra subscription is created here.\n */\nasync function requestReply(\n nc: NatsConnection,\n subject: string,\n payload: Uint8Array,\n timeout: number,\n): Promise<RequestReply> {\n const msg: Msg = await nc.request(subject, payload, { timeout });\n return { data: msg.data };\n}\n\n/**\n * Subscribe to `subject` and reply to every request with the handler's bytes,\n * over the SAME connection. The responder side of request/reply: each delivered\n * `Msg` carries a `reply` inbox; `msg.respond(bytes)` publishes the answer there.\n * A handler that throws (or a message with no `reply` subject) is left unanswered\n * — the requester then sees a timeout, never a malformed reply.\n */\nfunction subscribeRequests(\n nc: NatsConnection,\n subject: string,\n handler: RequestHandler,\n): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n // A request always carries a reply inbox; a plain publish to this subject\n // (no reply) is ignored — request/reply is the only contract here.\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave the request unanswered: the requester's request times out, which\n // the selfhosted control plane reads as a transient blip (reconnecting),\n // never a malformed reply. The responder stays subscribed for the next op.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * Subscribe to the one-way agent event plane: deliver each published payload (the\n * agent's `AgentEvent` heartbeat / going-offline, NOT a request/reply) to the\n * handler with its concrete subject. A plain `nc.subscribe` (no reply); a handler\n * that throws is swallowed so one bad event never tears down the subscription\n * (ingestion is best-effort — a metrics gap is never fatal).\n */\nfunction subscribeAgentEvents(\n nc: NatsConnection,\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n try {\n await handler(msg.data, msg.subject);\n } catch {\n // Swallow: best-effort ingestion. The subscription stays live for the\n // next event.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\nexport function formatSse<T extends { sequence: number; type: string }>(event: T): string {\n return [\n `id: ${event.sequence}`,\n `event: ${event.type}`,\n `data: ${JSON.stringify(event)}`,\n \"\",\n \"\",\n ].join(\"\\n\");\n}\n\n/** Canonical one-event NATS payload with an exact broker byte assertion. */\nexport function workspaceControlEventNatsPayload(event: WorkspaceControlEvent): Uint8Array {\n const bounded = boundWorkspaceControlEvent(event, {\n surface: \"nats_legacy_guard\",\n });\n const encoded = codec.encode(bounded);\n if (encoded.byteLength > WORKSPACE_CONTROL_NATS_MESSAGE_MAX_BYTES) {\n throw new RangeError(\n `Workspace-control event cannot fit in the NATS envelope (${encoded.byteLength} > ${WORKSPACE_CONTROL_NATS_MESSAGE_MAX_BYTES} bytes)`,\n );\n }\n return encoded;\n}\n\n/** Defensively bounds current and historical workspace invalidations per frame. */\nexport function formatWorkspaceControlEventSse(event: WorkspaceControlEvent): string {\n const bounded = boundWorkspaceControlEvent(event, {\n surface: \"sse_legacy_guard\",\n });\n const formatted = formatSse(bounded);\n const bytes = new TextEncoder().encode(formatted).byteLength;\n if (bytes > SESSION_EVENT_SSE_FRAME_MAX_BYTES) {\n throw new RangeError(\n `Bounded workspace-control SSE frame exceeds its envelope (${bytes} > ${SESSION_EVENT_SSE_FRAME_MAX_BYTES} bytes)`,\n );\n }\n return formatted;\n}\n\n/** Defensively bounds historical rows before they become one SSE frame. */\nexport function formatSessionEventSse(event: SessionEvent): string {\n const bounded = boundSessionEventForSurface(event, \"sse_legacy_guard\");\n const formatted = formatSse(bounded);\n if (new TextEncoder().encode(formatted).byteLength > SESSION_EVENT_SSE_FRAME_MAX_BYTES) {\n // The payload normalizer targets 60 KiB, so this fallback is reachable only\n // for a malformed legacy event with oversized non-payload envelope fields.\n const minimal: SessionEvent = {\n ...bounded,\n type: bounded.type.slice(0, 256) as SessionEvent[\"type\"],\n payload: boundSessionEventPayload(\n {\n preview: \"[legacy event envelope omitted at SSE frame boundary]\",\n // The complete event has already crossed the non-invoking bounded\n // projection above. Do not re-read an untrusted source accessor merely\n // to populate optional diagnostic accounting in this last-resort path.\n originalPayloadBytes: null,\n },\n { surface: \"sse_legacy_guard\", maxBytes: 4096 },\n ),\n };\n return formatSse(minimal);\n }\n return formatted;\n}\n\n/**\n * Split an already-durable batch by exact encoded NATS bytes. Each event is\n * defensively normalized first so historical oversized rows cannot exceed the\n * broker envelope. Sequence and ordering are unchanged across chunks.\n */\nexport function sessionEventBatchesByBytes(\n workspaceId: string,\n sessionId: string,\n events: readonly SessionEvent[],\n maxBytes = SESSION_EVENT_NATS_MESSAGE_MAX_BYTES,\n): SessionEvent[][] {\n const bounded = events.map((event) => boundSessionEventForSurface(event, \"nats_legacy_guard\"));\n const batches: SessionEvent[][] = [];\n let current: SessionEvent[] = [];\n for (const event of bounded) {\n const candidate = [...current, event];\n const encodedBytes = codec.encode({\n workspaceId,\n sessionId,\n events: candidate,\n }).byteLength;\n if (current.length > 0 && encodedBytes > maxBytes) {\n batches.push(current);\n current = [event];\n } else {\n current = candidate;\n }\n }\n if (current.length > 0) batches.push(current);\n for (const batch of batches) {\n const encodedBytes = codec.encode({\n workspaceId,\n sessionId,\n events: batch,\n }).byteLength;\n if (encodedBytes > maxBytes) {\n throw new RangeError(\n `Session event cannot fit in the configured NATS envelope (${encodedBytes} > ${maxBytes} bytes)`,\n );\n }\n }\n return batches;\n}\n\n/** Return one count+byte-bounded HTTP page and truthful continuation facts. */\nexport function boundSessionEventHttpPage(\n events: readonly SessionEvent[],\n options: {\n direction: \"after\" | \"before\";\n maxBytes?: number;\n /** Exact mode is restricted to already-canonical forensic REST rows. */\n eventProjection?: \"bounded\" | \"exact\";\n },\n): {\n events: SessionEvent[];\n truncated: boolean;\n nextSequence: number | null;\n bytes: number;\n} {\n const maxBytes = options.maxBytes ?? SESSION_EVENT_HTTP_PAGE_MAX_BYTES;\n const selected: SessionEvent[] = [];\n let bytes = 2; // []\n const projected =\n options.eventProjection === \"exact\"\n ? [...events]\n : events.map((event) => boundSessionEventForSurface(event, \"http_projection\"));\n const candidates = options.direction === \"after\" ? projected : [...projected].reverse();\n for (const event of candidates) {\n const eventBytes = sessionEventJsonBytes(event);\n const separator = selected.length === 0 ? 0 : 1;\n if (bytes + separator + eventBytes > maxBytes) break;\n selected.push(event);\n bytes += separator + eventBytes;\n }\n if (options.direction === \"before\") selected.reverse();\n if (projected.length > 0 && selected.length === 0) {\n throw new RangeError(\n `A bounded session event cannot fit in the configured HTTP page envelope (${maxBytes} bytes)`,\n );\n }\n const truncated = selected.length < projected.length;\n const edge = options.direction === \"after\" ? selected.at(-1) : selected[0];\n return {\n events: selected,\n truncated,\n nextSequence:\n edge === undefined\n ? null\n : options.direction === \"after\"\n ? sessionEventResumeSequence(edge)\n : edge.sequence,\n bytes,\n };\n}\n\n/** Return one count+byte-bounded workspace-control page and resume cursor. */\nexport function boundWorkspaceControlHttpPage(\n events: readonly WorkspaceControlEvent[],\n maxBytes = WORKSPACE_CONTROL_HTTP_PAGE_MAX_BYTES,\n): {\n events: WorkspaceControlEvent[];\n truncated: boolean;\n nextSequence: number | null;\n bytes: number;\n} {\n const projected = events.map((event) =>\n boundWorkspaceControlEvent(event, { surface: \"http_projection\" }),\n );\n const selected: WorkspaceControlEvent[] = [];\n let bytes = 2; // []\n for (const event of projected) {\n const eventBytes = sessionEventJsonBytes(event);\n const separator = selected.length === 0 ? 0 : 1;\n if (bytes + separator + eventBytes > maxBytes) break;\n selected.push(event);\n bytes += separator + eventBytes;\n }\n if (projected.length > 0 && selected.length === 0) {\n throw new RangeError(\n `A bounded workspace-control event cannot fit in the HTTP page envelope (${maxBytes} bytes)`,\n );\n }\n return {\n events: selected,\n truncated: selected.length < projected.length,\n nextSequence: selected.at(-1)?.sequence ?? null,\n bytes,\n };\n}\n\n/** Raw durable cursor covered by a possibly coalesced compact event. */\nexport function sessionEventResumeSequence(event: SessionEvent): number {\n if (!event.payload || typeof event.payload !== \"object\" || Array.isArray(event.payload)) {\n return event.sequence;\n }\n const coalescedUntil = Number((event.payload as Record<string, unknown>).coalescedUntil);\n return Math.max(\n event.sequence,\n Number.isFinite(coalescedUntil) ? Math.floor(coalescedUntil) : event.sequence,\n );\n}\n\nfunction boundSessionEventForSurface(\n event: SessionEvent,\n surface: SessionEventBoundarySurface,\n): SessionEvent {\n return boundSessionEvent(event, { surface });\n}\n\nfunction observeEventBoundaries(events: readonly SessionEvent[], logger?: EventLogger): void {\n for (const event of events) {\n const boundary = sessionEventPayloadTruncation(event.payload);\n if (!boundary) continue;\n (logger?.debug ?? silentLogger.debug)(\"Session event payload is a bounded audit preview\", {\n eventType: event.type,\n surface: boundary.surface,\n reason: boundary.reason,\n originalBytes: boundary.originalBytes,\n deliveredBytes: boundary.deliveredBytes,\n estimatedOriginalTokens: boundary.estimatedOriginalTokens,\n estimatedDeliveredTokens: boundary.estimatedDeliveredTokens,\n fullEvidenceAvailable: boundary.fullEvidence.available,\n retainedOutputKind: boundary.fullEvidence.available ? boundary.fullEvidence.kind : null,\n });\n }\n}\n\nfunction workspaceControlSubject(workspaceId: string): string {\n return `workspaces.${workspaceId}.control`;\n}\n","import { boundSessionEventPayload, type SessionEvent } from \"@opengeni/contracts\";\n\nconst COALESCIBLE_DELTA_TYPES = new Set([\n \"agent.message.delta\",\n \"agent.reasoning.delta\",\n \"sandbox.command.output.delta\",\n]);\n\n/** Flush long runs incrementally before concatenation can become unbounded. */\nexport const SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES = 48 * 1024;\nconst encoder = new TextEncoder();\n\ntype DeltaRun = {\n first: SessionEvent;\n lastSequence: number;\n text: string;\n textBytes: number;\n sandboxName: string | undefined;\n sandboxStream: string | undefined;\n sandboxCommandId: string | undefined;\n};\n\nexport function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[] {\n const coalesced: SessionEvent[] = [];\n let run: DeltaRun | null = null;\n\n const flush = () => {\n if (!run) {\n return;\n }\n const payload =\n run.first.type === \"sandbox.command.output.delta\"\n ? // Sandbox output keeps its CANONICAL field (`chunk` — the terminal and\n // projection read it) plus the stream/commandId identity of the run.\n {\n chunk: run.text,\n coalescedUntil: run.lastSequence,\n ...(run.sandboxStream !== undefined ? { stream: run.sandboxStream } : {}),\n ...(run.sandboxCommandId !== undefined ? { commandId: run.sandboxCommandId } : {}),\n ...(run.sandboxName !== undefined ? { name: run.sandboxName } : {}),\n }\n : {\n text: run.text,\n coalescedUntil: run.lastSequence,\n };\n coalesced.push({\n ...run.first,\n payload: boundSessionEventPayload(payload, {\n surface: \"http_projection\",\n }),\n });\n run = null;\n };\n\n for (const event of events) {\n if (!isCoalescibleDelta(event)) {\n flush();\n coalesced.push(event);\n continue;\n }\n\n const isSandbox = event.type === \"sandbox.command.output.delta\";\n const sandboxName = isSandbox ? sandboxDeltaName(event.payload) : undefined;\n const sandboxStream = isSandbox ? sandboxDeltaString(event.payload, \"stream\") : undefined;\n const sandboxCommandId = isSandbox ? sandboxDeltaString(event.payload, \"commandId\") : undefined;\n const text = deltaText(event);\n if (\n run &&\n sameDeltaRun(run.first, event, run.sandboxName, sandboxName) &&\n run.sandboxStream === sandboxStream &&\n run.sandboxCommandId === sandboxCommandId\n ) {\n const textBytes = encoder.encode(text).byteLength;\n if (\n (run.textBytes === 0 && textBytes <= SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES) ||\n run.textBytes + textBytes <= SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES\n ) {\n run.text += text;\n run.textBytes += textBytes;\n run.lastSequence = event.sequence;\n continue;\n }\n // The current segment is already useful and bounded. Flush before adding\n // the next raw delta rather than building the full run and truncating it\n // only after a multi-megabyte intermediate allocation.\n flush();\n } else {\n flush();\n }\n\n run = {\n first: event,\n lastSequence: event.sequence,\n text,\n textBytes: encoder.encode(text).byteLength,\n sandboxName,\n sandboxStream,\n sandboxCommandId,\n };\n }\n\n flush();\n return coalesced;\n}\n\nfunction isCoalescibleDelta(event: SessionEvent): boolean {\n return COALESCIBLE_DELTA_TYPES.has(event.type);\n}\n\nfunction sameDeltaRun(\n first: SessionEvent,\n next: SessionEvent,\n firstSandboxName: string | undefined,\n nextSandboxName: string | undefined,\n): boolean {\n if (first.type !== next.type) {\n return false;\n }\n if ((first.turnId ?? null) !== (next.turnId ?? null)) {\n return false;\n }\n return first.type !== \"sandbox.command.output.delta\" || firstSandboxName === nextSandboxName;\n}\n\nfunction deltaText(event: SessionEvent): string {\n if (event.type === \"agent.reasoning.delta\") {\n return reasoningText(event.payload);\n }\n const payload = asRecord(event.payload);\n if (event.type === \"sandbox.command.output.delta\") {\n // `chunk` is the canonical wire field (contracts SandboxCommandOutputDeltaPayload);\n // text/output are tolerated legacy shapes.\n for (const key of [\"chunk\", \"text\", \"output\"] as const) {\n if (typeof payload[key] === \"string\") {\n return payload[key] as string;\n }\n }\n return \"\";\n }\n return typeof payload.text === \"string\" ? payload.text : \"\";\n}\n\nfunction reasoningText(payload: unknown): string {\n const record = asRecord(payload);\n if (typeof record.text === \"string\") {\n return record.text;\n }\n const content = asRecord(asRecord(record.item).rawItem).content;\n if (!Array.isArray(content)) {\n return \"\";\n }\n return content\n .map((part) => {\n const text = asRecord(part).text;\n return typeof text === \"string\" ? text : \"\";\n })\n .join(\"\");\n}\n\nfunction sandboxDeltaName(payload: unknown): string | undefined {\n const name = asRecord(payload).name;\n return typeof name === \"string\" ? name : undefined;\n}\n\nfunction sandboxDeltaString(payload: unknown, key: \"stream\" | \"commandId\"): string | undefined {\n const value = asRecord(payload)[key];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : {};\n}\n","// packages/events/src/nats-jwt.ts — NATS JWT v2 signing for the auth-callout\n// responder (bring-your-own-compute M-AUTH; NATS Accounts per\n// workspace + §17 the isolation smoke).\n//\n// This is the cryptographic core of the auth-callout tenancy boundary. When an\n// external agent connects to NATS presenting its `oge_` enrollment bearer as the\n// connect auth-token, nats-server (configured with `auth_callout`) issues an\n// authorization request on `$SYS.REQ.USER.AUTH`. Our responder (auth-callout.ts)\n// validates the bearer and answers with a SIGNED authorization-response JWT that\n// embeds a SIGNED user JWT scoping the connection to publish/subscribe ONLY its\n// generation-fenced process subtree\n// `agent.<workspaceId>.<agentId>.connection.<instanceId>.>` (+ reply\n// `_INBOX.>`). That exact scope prevents both cross-workspace access and a stale\n// process sharing credentials with its live successor.\n//\n// WHY HAND-ROLL THE JWT ENCODING (vs a dep): the NATS JWT v2 wire format is small,\n// stable, and fully specified (ADR-26 + nats-io/jwt): a base64url header\n// `{\"typ\":\"JWT\",\"alg\":\"ed25519-nkey\"}`, base64url JSON claims whose `jti` is the\n// base32(SHA-512/256(claims-with-blank-jti)), and an ed25519 nkey signature over\n// `header.payload`. nkeys (re-exported by the `nats` package we already depend on)\n// gives us the ed25519 sign primitive; Node `crypto` gives SHA-512/256. So we own\n// the encoding in a few well-tested functions rather than pull an alpha\n// `@nats-io/jwt` (0.0.x) whose nkeys-version compat is uncertain. No `xkey`\n// encryption is used (the bearer is already an authenticated identity claim and\n// the wire is TLS — encryption is an optional ADR-26 hardening, off here).\n//\n// SECURITY: the account SIGNING SEED never leaves this process and is NEVER logged.\n// Callers pass it as a `string` seed; we `fromSeed` it once per sign. The bearer\n// the responder validates is HMAC-verified elsewhere (verifyEnrollmentBearer); this\n// module only mints the scoped NATS credential once identity is proven.\n\nimport { createHash } from \"node:crypto\";\nimport { nkeys } from \"nats\";\n\n/** The NATS JWT v2 header — constant for every token we mint (ADR-26 / nats-io/jwt:\n * `TokenTypeJwt=\"JWT\"`, `AlgorithmNkey=\"ed25519-nkey\"`). */\nconst JWT_HEADER = { typ: \"JWT\", alg: \"ed25519-nkey\" } as const;\n\n/** NATS user-claim `nats.type` discriminator + `nats.version` for v2 claims. */\nconst USER_CLAIM_TYPE = \"user\";\nconst AUTH_RESPONSE_CLAIM_TYPE = \"authorization_response\";\nconst NATS_CLAIM_VERSION = 2;\n\n/** A NATS permission set: subject allow/deny lists (ADR-26 `pub`/`sub` →\n * `allow`/`deny`). An empty/undefined list means \"no explicit grant\" — combined\n * with the agent scope below, the connection can ONLY reach what `allow` lists. */\nexport interface NatsPermission {\n allow?: string[];\n deny?: string[];\n}\n\n/** The pub/sub permissions embedded in a user JWT. */\nexport interface NatsPermissions {\n pub: NatsPermission;\n sub: NatsPermission;\n}\n\n/**\n * The minimal nkey keypair surface this module needs — exactly what\n * `nkeys.fromSeed(seed)` returns. Declared structurally so the module does not\n * leak the `nats` nkeys type through its public signature.\n */\ninterface NkeyPair {\n getPublicKey(): string;\n sign(input: Uint8Array): Uint8Array;\n}\n\n/** base64url (RawURLEncoding — no padding), matching nats-io/jwt's `serialize`. */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64url\");\n}\n\n/** RFC 4648 base32 (standard alphabet, NO padding) — the encoding nats-io/jwt\n * uses for the `jti` hash. Node has no built-in base32, so a tiny encoder. */\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\nfunction base32NoPadding(bytes: Uint8Array): string {\n let bits = 0;\n let value = 0;\n let out = \"\";\n for (const byte of bytes) {\n value = (value << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n out += BASE32_ALPHABET[(value >>> bits) & 31];\n }\n }\n if (bits > 0) {\n out += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n }\n return out;\n}\n\n/**\n * Compute the canonical NATS `jti`: base32(NoPadding, std-alphabet) of the\n * SHA-512/256 of the claims object SERIALIZED WITH AN EMPTY `jti` (nats-io/jwt's\n * `hash`). nats-server recomputes + verifies this on decode, so it must match\n * byte-for-byte. We serialize the SAME object we will sign, only with `jti:\"\"`.\n */\nfunction computeJti(claimsWithBlankJti: object): string {\n const json = JSON.stringify(claimsWithBlankJti);\n const digest = createHash(\"sha512-256\").update(json, \"utf8\").digest();\n return base32NoPadding(digest);\n}\n\n/**\n * Encode + sign a NATS v2 JWT. The `claims` MUST already carry `iss`/`sub`/`iat`\n * (+ optional `aud`/`exp`) and a `nats` block; this function fills `jti` (the\n * canonical hash), serializes `header.payload`, signs that with `signingKey`, and\n * appends the base64url signature. Returns the compact `header.payload.signature`.\n */\nfunction encodeJwt(claims: Record<string, unknown>, signingKey: NkeyPair): string {\n // jti is the hash of the claims with jti blanked — set it blank, hash, then set.\n const withBlankJti = { ...claims, jti: \"\" };\n const jti = computeJti(withBlankJti);\n const finalClaims = { ...claims, jti };\n\n const header = base64UrlEncode(Buffer.from(JSON.stringify(JWT_HEADER), \"utf8\"));\n const payload = base64UrlEncode(Buffer.from(JSON.stringify(finalClaims), \"utf8\"));\n const signingInput = `${header}.${payload}`;\n const signature = signingKey.sign(Buffer.from(signingInput, \"utf8\"));\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\n/**\n * Input to mint a workspace-scoped NATS user JWT for an enrolled agent.\n * - `userPublicKey` — the `user_nkey` from the authorization request; it MUST be\n * the `sub` of the user JWT (nats-server rejects a mismatch).\n * - `accountSeed` — the callout account SIGNING seed (`SA...`); both the user JWT\n * `iss` (its public key) and the signature come from it. NEVER logged.\n * - `name` — a human label for the user (the agent id), for server logs.\n * - `permissions` — the pub/sub allow/deny lists (the workspace scope).\n * - `expiresAtSeconds` — optional absolute `exp` (unix seconds). When set the\n * server will expire the connection's credential; we tie it to the bearer's\n * remaining life so a revoked/expired enrollment cannot outlive its bearer.\n */\nexport interface MintUserJwtInput {\n userPublicKey: string;\n accountSeed: string;\n name: string;\n permissions: NatsPermissions;\n /** The target account NAME (the `auth_callout.account`) the user binds to; the\n * embedded user JWT's `aud` in server-config mode. */\n audienceAccount: string;\n expiresAtSeconds?: number;\n}\n\n/**\n * Mint a signed NATS user JWT scoped by `permissions`. In auth-callout SERVER\n * mode the user JWT is signed by the callout ISSUER ACCOUNT key, and its `iss` is\n * that account's public key. The returned JWT is embedded as `nats.jwt` in the\n * authorization response.\n */\nexport function mintUserJwt(input: MintUserJwtInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: USER_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n pub: input.permissions.pub,\n sub: input.permissions.sub,\n // Unlimited subscriptions / data / payload (the workspace subject scope, NOT\n // a connection-resource quota, is the boundary here).\n subs: -1,\n data: -1,\n payload: -1,\n };\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n name: input.name,\n sub: input.userPublicKey,\n // SERVER-config-mode placement: nats-server reads the embedded user JWT's `aud`\n // as the target account NAME (the configured `auth_callout.account`). This is\n // how the authenticated user binds to that account; the workspace isolation is\n // then carried by the pub/sub permissions below.\n aud: input.audienceAccount,\n nats: natsBlock,\n };\n if (typeof input.expiresAtSeconds === \"number\") {\n claims.exp = input.expiresAtSeconds;\n }\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * Input to mint the authorization RESPONSE JWT the responder publishes back on the\n * request's reply subject (ADR-26 §3).\n * - `userPublicKey` — the request's `user_nkey`; the response `sub`.\n * - `serverId` — the request's `nats.server_id.id` (the server's public key); the\n * response `aud`.\n * - `accountSeed` — the callout account signing seed; signs the response and is\n * its `iss` (public key). NEVER logged.\n * - `userJwt` — the embedded signed user JWT (omit on a denial).\n * - `error` — a human-readable denial message (omit on success). When present the\n * server denies the connection.\n */\nexport interface MintAuthResponseInput {\n userPublicKey: string;\n serverId: string;\n accountSeed: string;\n userJwt?: string;\n error?: string;\n}\n\n/**\n * Mint the signed authorization-response JWT. On success it carries the embedded\n * user JWT (`nats.jwt`); on denial it carries `nats.error` and NO user JWT, which\n * makes nats-server refuse the connection. Signed by the callout account key (its\n * public key is `iss`); `sub` is the user_nkey, `aud` is the server id.\n */\nexport function mintAuthResponse(input: MintAuthResponseInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: AUTH_RESPONSE_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n };\n if (input.userJwt) {\n natsBlock.jwt = input.userJwt;\n }\n if (input.error) {\n natsBlock.error = input.error;\n }\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n // The response `aud` MUST be the SERVER public key in server-config mode\n // (nats-server validates \"Audience must be a server public key\"). The\n // authenticated user is placed into the configured `auth_callout.account` (the\n // SAME account the responder + the privileged control plane connect into), so\n // exact generation-fenced agent request/reply routes; workspace isolation is carried\n // entirely by the user JWT's pub/sub subject permissions (NOT by cross-account\n // placement, which server-config-mode nats does not support — nats-io#4335).\n aud: input.serverId,\n sub: input.userPublicKey,\n nats: natsBlock,\n };\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * The fields the responder needs out of the authorization REQUEST JWT (ADR-26 §2).\n * The request is itself a NATS JWT (`header.payload.signature`) the server signs;\n * we only DECODE it (the server proves its own identity by the connection, and the\n * embedded `auth_token` is independently HMAC-verified), so we read the payload\n * without re-verifying the server signature.\n */\nexport interface DecodedAuthRequest {\n /** The public user nkey the response user JWT MUST be `sub`-scoped to. */\n userNkey: string;\n /** The server's public id — the response `aud`. */\n serverId: string;\n /** The connect `auth_token` the client presented (our `oge_` bearer), if any. */\n authToken: string | undefined;\n /** The connect username, if any (unused today; present for completeness). */\n user: string | undefined;\n /** Client-reported process identity. OpenGeni agents use a strict\n * `opengeni-agent/connection/<uuid>` shape; auth-callout rejects anything\n * else before granting machine subjects. */\n name: string | undefined;\n}\n\n/**\n * Decode the authorization-request JWT payload (the middle base64url segment). The\n * request shape (ADR-26 §2): `nats.user_nkey`, `nats.server_id.id`, and the\n * presented connect options under `nats.connect_opts` (`auth_token` / `user`).\n * Returns null on a malformed token so the caller can deny cleanly.\n */\nexport function decodeAuthRequest(token: string): DecodedAuthRequest | null {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"));\n } catch {\n return null;\n }\n if (typeof payload !== \"object\" || payload === null) {\n return null;\n }\n const nats = (payload as { nats?: unknown }).nats;\n if (typeof nats !== \"object\" || nats === null) {\n return null;\n }\n const natsObj = nats as {\n user_nkey?: unknown;\n server_id?: { id?: unknown } | unknown;\n connect_opts?: { auth_token?: unknown; user?: unknown } | unknown;\n };\n const userNkey = typeof natsObj.user_nkey === \"string\" ? natsObj.user_nkey : null;\n if (!userNkey) {\n return null;\n }\n const serverIdRaw =\n typeof natsObj.server_id === \"object\" && natsObj.server_id !== null\n ? (natsObj.server_id as { id?: unknown }).id\n : undefined;\n const serverId = typeof serverIdRaw === \"string\" ? serverIdRaw : \"\";\n const connectOpts =\n typeof natsObj.connect_opts === \"object\" && natsObj.connect_opts !== null\n ? (natsObj.connect_opts as { auth_token?: unknown; user?: unknown; name?: unknown })\n : {};\n const authToken = typeof connectOpts.auth_token === \"string\" ? connectOpts.auth_token : undefined;\n const user = typeof connectOpts.user === \"string\" ? connectOpts.user : undefined;\n const name = typeof connectOpts.name === \"string\" ? connectOpts.name : undefined;\n return { userNkey, serverId, authToken, user, name };\n}\n\nconst AGENT_CONNECTION_NAME_PREFIX = \"opengeni-agent/connection/\";\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Parse the exact process instance carried by the NATS CONNECT name. The value\n * is authority material used in subjects, so arbitrary tokens/dots are never\n * accepted. */\nexport function parseAgentConnectionName(name: string | undefined): string | null {\n if (!name?.startsWith(AGENT_CONNECTION_NAME_PREFIX)) return null;\n const instanceId = name.slice(AGENT_CONNECTION_NAME_PREFIX.length);\n return UUID_PATTERN.test(instanceId) ? instanceId.toLowerCase() : null;\n}\n\n/**\n * Build the exact process-scoped permission set for an authenticated agent. In\n * production agentId + connectionInstanceId are mandatory, restricting both\n * directions to that claimed daemon's RPC/event/hello/op subtree. It may publish\n * to `_INBOX.>` only to answer control-plane requests; it never needs to read\n * another connection's reply inbox.\n * The workspace-only fallback exists solely for legacy isolated callers/tests.\n *\n * THE isolation assertion (§17): with workspace A, agent B, instance C, the\n * production allow lists name only `agent.A.B.connection.C.>` and `_INBOX.>`.\n * NATS rejects every other workspace, agent, or process generation.\n */\nexport function workspaceAgentPermissions(\n workspaceId: string,\n agentId?: string,\n connectionInstanceId?: string,\n): NatsPermissions {\n const agentScope =\n agentId && connectionInstanceId\n ? `agent.${workspaceId}.${agentId}.connection.${connectionInstanceId}.>`\n : `agent.${workspaceId}.>`;\n // The reply-inbox subtree must be reachable for request/reply (the control plane\n // requests on the exact process RPC subject with a reply inbox; the agent responds there).\n const inboxScope = \"_INBOX.>\";\n return {\n pub: { allow: [agentScope, inboxScope] },\n sub: { allow: [agentScope] },\n };\n}\n"],"mappings":"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","names":["boundSessionEventPayload","connect","nkeys","boundSessionEventPayload"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/coalesce.ts","../src/nats-jwt.ts"],"sourcesContent":["import {\n boundSessionEvent,\n boundSessionEventPayload,\n boundWorkspaceControlEvent,\n sessionEventJsonBytes,\n sessionEventPayloadTruncation,\n type SessionBusMessage,\n type SessionEvent,\n type SessionEventBoundarySurface,\n type WorkspaceControlEvent,\n} from \"@opengeni/contracts\";\nimport {\n appendSessionEvents,\n appendSessionEventsForTurnAttempt,\n sessionSubject,\n type AppendEventInput,\n type CanonicalTurnStartupMilestoneReceipt,\n type Database,\n type SessionEventAppendObserver,\n} from \"@opengeni/db\";\nimport {\n connect,\n JSONCodec,\n type ConnectionOptions,\n type Msg,\n type NatsConnection,\n type Subscription,\n} from \"nats\";\n\nconst codec = JSONCodec<SessionBusMessage | SessionEvent | WorkspaceControlEvent>();\n\nexport type EventLogger = {\n debug?: (message: string, attributes?: Record<string, unknown>) => void;\n warn?: (message: string, attributes?: Record<string, unknown>) => void;\n};\n\nexport type EventBusOptions = {\n logger?: EventLogger;\n /** Test/host transport seam; production defaults to the nats.js connector. */\n connect?: typeof connect;\n};\n\nconst silentLogger: Required<EventLogger> = {\n debug: () => {},\n warn: () => {},\n};\n\nexport {\n SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES,\n coalesceSessionEventDeltas,\n coalesceSessionEventDeltasWithCoverage,\n type CoalescedSessionEventPage,\n} from \"./coalesce\";\n\n/**\n * Reconnect + keepalive defaults applied to EVERY long-lived NATS connection\n * this package opens (the event bus AND the standalone auth-callout responder).\n *\n * The production outage these guard against: an in-cluster NATS broker pod\n * restart. nats.js's stock policy gives up after ~10 attempts (~20s) and the\n * client goes permanently CONNECTION_CLOSED — which takes the whole control\n * plane down with it: every session-create publishes events to NATS, and the\n * API-hosted auth-callout responder dies so BYO agents get \"authorization\n * violation\". Recovery then required a MANUAL api+worker restart. With these\n * options the client retries forever and auto-recovers the moment the broker\n * returns. Factored into one source of truth so the call sites never drift.\n *\n * - `reconnect` + `maxReconnectAttempts: -1` — never give up (infinite retry).\n * - `reconnectTimeWait` (2s base) + `reconnectJitter`/`reconnectJitterTLS`\n * (up to 1s) — a fleet of api/worker pods doesn't thundering-herd the broker\n * on recovery.\n * - `waitOnFirstConnect` — a broker briefly unavailable at boot must not\n * hard-fail the process; the client keeps trying instead of throwing.\n * - `pingInterval`/`maxPingOut` — promptly detect a silently-dead socket so the\n * reconnect machinery actually engages instead of hanging on a zombie.\n */\nconst RECONNECT_OPTIONS = {\n reconnect: true,\n maxReconnectAttempts: -1,\n reconnectTimeWait: 2_000,\n reconnectJitter: 1_000,\n reconnectJitterTLS: 1_000,\n waitOnFirstConnect: true,\n pingInterval: 20_000,\n maxPingOut: 3,\n} satisfies ConnectionOptions;\n\n/**\n * The single source of truth for a long-lived connection's resilience: merge the\n * reconnect/keepalive defaults UNDER the caller's connection options (servers +\n * optional auth/name). Every long-lived `connect()` in this package goes through\n * here so the two call sites can never diverge.\n */\nfunction withReconnectDefaults(options: ConnectionOptions): ConnectionOptions {\n return { ...RECONNECT_OPTIONS, ...options };\n}\n\n/** How long a best-effort publish waits on `flush()` before giving up (see `publish`). */\nconst PUBLISH_FLUSH_TIMEOUT_MS = 2_000;\n\n/** Comfortably below NATS Core's common 1 MiB max_payload default. */\nexport const SESSION_EVENT_NATS_MESSAGE_MAX_BYTES = 512 * 1024;\n/** Payload is <=64 KiB; the larger envelope leaves deterministic wire headroom. */\nexport const SESSION_EVENT_SSE_FRAME_MAX_BYTES = 96 * 1024;\n/** Independent count+byte envelope for one durable HTTP replay response. */\nexport const SESSION_EVENT_HTTP_PAGE_MAX_BYTES = 1024 * 1024;\n/** Workspace invalidations are one compact event, never a broker evidence blob. */\nexport const WORKSPACE_CONTROL_NATS_MESSAGE_MAX_BYTES = 32 * 1024;\n/** Count+byte envelope for one workspace-control REST replay page. */\nexport const WORKSPACE_CONTROL_HTTP_PAGE_MAX_BYTES = 1024 * 1024;\n\n/**\n * Await `nc.flush()` but never longer than `timeoutMs`. With infinite reconnect a\n * `flush()` issued while the broker is down does NOT reject — it pends until the\n * broker returns, which can be minutes. Racing it against a timer keeps a long\n * outage from stalling an in-flight turn; the published message stays buffered\n * and is delivered on reconnect regardless. A flush rejection (connection fully\n * CLOSED) is swallowed here so the timeout race never leaks an unhandled\n * rejection — the caller's publish path is what logs the drop.\n */\nasync function flushWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n });\n try {\n await Promise.race([nc.flush().catch(() => undefined), timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Flush a durable outbox publication and reject unless the NATS server confirms\n * the write within the bounded wait. A timeout can still leave the original\n * bytes buffered for reconnect, so callers must remain duplicate-safe when they\n * retry the durable obligation.\n */\nasync function flushConfirmedWithTimeout(nc: NatsConnection, timeoutMs: number): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error(`NATS publish confirmation timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n });\n try {\n await Promise.race([nc.flush(), timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Drain a long-lived connection's status async-iterator to the log so a future\n * broker outage is OBSERVABLE (disconnect → reconnecting → reconnect → update).\n * Fire-and-forget for the connection's lifetime; the loop ends when the\n * connection closes. `label` distinguishes the event-bus connection from the\n * auth-callout responder in the logs.\n */\nfunction logConnectionStatus(\n nc: NatsConnection,\n label: string,\n logger: EventLogger = silentLogger,\n onStatus?: (type: string) => void,\n): void {\n void (async () => {\n try {\n for await (const status of nc.status()) {\n onStatus?.(status.type);\n const attributes = { label, status: status.type, data: status.data };\n if (isWarnNatsStatus(status.type)) {\n (logger.warn ?? silentLogger.warn)(\"NATS connection status\", attributes);\n } else {\n (logger.debug ?? silentLogger.debug)(\"NATS connection status\", attributes);\n }\n }\n } catch {\n // The status iterator simply ends when the connection closes; never let it\n // throw out of this background loop.\n }\n })();\n}\n\nfunction isWarnNatsStatus(type: string): boolean {\n return type === \"disconnect\" || type === \"error\" || type === \"staleConnection\";\n}\n\nexport {\n decodeAuthRequest,\n mintAuthResponse,\n mintUserJwt,\n parseAgentConnectionName,\n workspaceAgentPermissions,\n type DecodedAuthRequest,\n type MintAuthResponseInput,\n type MintUserJwtInput,\n type NatsPermission,\n type NatsPermissions,\n} from \"./nats-jwt\";\n\n// Re-export the raw NATS primitives a consumer needs to open a direct connection or\n// generate nkeys (the auth-callout responder's standalone connection, the\n// agent-simulating integration tests). This keeps `nats` an internal dependency of\n// this leaf — callers in the bun workspace reach it through @opengeni/events rather\n// than depending on `nats` directly.\nexport { connect, nkeys, type NatsConnection } from \"nats\";\n\n/**\n * A raw request/reply reply — just the response bytes. Mirrors the subset of the\n * NATS `Msg` shape a binary request/reply caller needs (`NatsControlRpc` consumes\n * exactly this). Kept minimal so the events package does not leak the `nats` `Msg`\n * type into the agent-loop-free runtime leaf.\n */\nexport type RequestReply = { data: Uint8Array };\n\n/**\n * The minimal request/reply connection the selfhosted control plane consumes\n * (structurally identical to `@opengeni/runtime`'s `NatsRequestConnection`). The\n * API/worker hand this accessor to `NatsControlRpc` so the control transport rides\n * the SAME managed NATS connection the event bus already owns — a NATS connection\n * natively supports both pub/sub and request/reply, so there is NEVER a second\n * connection.\n */\nexport interface RequestConnection {\n request(subject: string, payload: Uint8Array, opts: { timeout: number }): Promise<RequestReply>;\n}\n\n/**\n * The raw subscribe/publish surface the selfhosted OP-STREAM transport consumes\n * (structurally identical to `@opengeni/runtime`'s `NatsOpStreamConnection`):\n * a plain subscription for the runner's fire-and-forget op frames\n * (the exact process generation's `.op.<op_id>`) and a plain publish for acks\n * (that same generation's `.ack`). Same managed connection as everything else — a NATS\n * connection natively supports all of it; there is NEVER a second connection.\n */\nexport interface OpStreamConnection {\n subscribe(subject: string): AsyncIterable<{ data: Uint8Array }> & { unsubscribe(): void };\n publish(subject: string, payload: Uint8Array): void;\n /** Subscription/publish barrier used before reading durable authority. */\n flush?(): Promise<void>;\n}\n\n/**\n * A handler answering a request/reply on a subscribed subject: given the request\n * bytes (+ the concrete subject the message landed on, for exact-process RPC\n * style wildcard routing), return the response bytes to reply with. A thrown error\n * leaves the request unanswered (the caller's request times out / sees no\n * responder), which the control plane maps to `agent_offline` / reconnecting.\n */\nexport type RequestHandler = (\n request: Uint8Array,\n subject: string,\n) => Promise<Uint8Array> | Uint8Array;\n\n/**\n * Versioned recovery contract for already-durable session-event fanout.\n *\n * A publisher acknowledgement alone cannot prove that every API subscriber was\n * connected for the live message. Supported broker-backed buses must therefore\n * notify consumers after transport subscriptions have been restored so an\n * already-open SSE stream can run one bounded Postgres catch-up. A bus that can\n * never disconnect may implement this as a listener registry that never fires.\n */\nexport const SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION = 1 as const;\n\nexport type SessionEventDurableFanoutCapability = {\n version: typeof SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION;\n subscribeRecovery: (onRecovery: (generation: number) => void) => () => void;\n};\n\nexport function requireSessionEventDurableFanoutCapability(\n bus: unknown,\n): SessionEventDurableFanoutCapability {\n const capability = (bus as { sessionEventDurableFanout?: unknown } | null)\n ?.sessionEventDurableFanout as\n | { version?: unknown; subscribeRecovery?: unknown }\n | null\n | undefined;\n if (\n capability?.version !== SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION ||\n typeof capability.subscribeRecovery !== \"function\"\n ) {\n throw new Error(\n \"EventBus must provide sessionEventDurableFanout v1 so accepted durable publications reconcile after subscriber reconnect\",\n );\n }\n return capability as SessionEventDurableFanoutCapability;\n}\n\nexport type EventBus = {\n /**\n * Mandatory paired recovery contract for durable session-event publication.\n * API and worker instances sharing one broker must expose the same semantics.\n */\n sessionEventDurableFanout: SessionEventDurableFanoutCapability;\n /**\n * Publish a session-event batch. Embedding implementations without the\n * optional publishConfirmed capability must resolve only after their\n * transport has accepted the batch and reject transport failures so durable\n * outbox callers can retry safely. Ordinary live-fanout callers remain\n * best-effort because they catch failures around this contract.\n */\n publish: (workspaceId: string, sessionId: string, events: SessionEvent[]) => Promise<void>;\n /**\n * Publish an already-durable batch and reject unless the transport confirms\n * acceptance with a stronger provider-specific acknowledgement. Optional for\n * embedding-host buses; durable fanout reconcilers fall back to the required\n * publish promise when this capability is unavailable.\n */\n publishConfirmed?: (\n workspaceId: string,\n sessionId: string,\n events: SessionEvent[],\n ) => Promise<void>;\n subscribe: (\n workspaceId: string,\n sessionId: string,\n onEvents: (events: SessionEvent[]) => void | Promise<void>,\n ) => Promise<() => void>;\n /** Best-effort live invalidation; the event is already durable in Postgres. */\n publishWorkspaceControl: (workspaceId: string, event: WorkspaceControlEvent) => Promise<void>;\n /** One workspace subscription fans a control change to every open descendant view. */\n subscribeWorkspaceControl: (\n workspaceId: string,\n onEvent: (event: WorkspaceControlEvent) => void | Promise<void>,\n ) => Promise<() => void>;\n /**\n * Issue a binary request/reply on a subject over the bus's NATS connection\n * (the selfhosted control plane's exact claimed process subject). A new usage of what was\n * a one-way bus — same connection, native NATS request/reply. Rejects on a\n * no-responder (NATS 503) or a request timeout; the caller (`NatsControlRpc`)\n * maps those to `agent_offline` / `agent_reconnecting`, never a NotFound.\n */\n request: (\n subject: string,\n payload: Uint8Array,\n opts: { timeoutMs: number },\n ) => Promise<RequestReply>;\n /**\n * Subscribe-and-reply on a subject (the responder side — the enrolled agent, or\n * a test stand-in for it): for every request on `subject`, call `handler` and\n * `respond` with its bytes over the SAME connection. Returns an unsubscribe fn.\n * A subject may be a NATS wildcard (e.g. `agent.*.*.connection.*.rpc`).\n */\n subscribeRequests: (subject: string, handler: RequestHandler) => () => void;\n /**\n * Subscribe to the agent EVENT plane (the one-way fire-and-forget heartbeats +\n * going-offline the agent PUBLISHES on its exact process `.events`, NOT a\n * request/reply). The M10 metrics-ingestion consumer subscribes the wildcard\n * `agent.*.*.connection.*.events` and gets each raw payload plus its concrete\n * subject (so it can extract `<ws>`/`<id>`/`<instance>`). Returns an\n * unsubscribe fn. Decoding the AgentEvent is the caller's concern (this leaf\n * does not depend on `@opengeni/agent-proto`).\n */\n subscribeAgentEvents: (\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n ) => () => void;\n /**\n * The `RequestConnection` accessor the selfhosted `NatsControlRpc` consumes —\n * the SAME managed connection (pub/sub + request/reply share it). The control\n * plane injects this so the transport never opens a second connection.\n */\n getRequestConnection: () => RequestConnection;\n /**\n * The `OpStreamConnection` accessor the selfhosted op-stream transport\n * consumes (`NatsOpStreamTransport`) — the same managed connection again.\n * Optional so bus test doubles that never exercise op-stream stay valid.\n */\n getOpStreamConnection?: () => OpStreamConnection;\n isConnected?: () => boolean;\n close: () => Promise<void>;\n};\n\n/**\n * Connect the event bus + control-plane request/reply over ONE managed NATS\n * connection. `auth` is the PRIVILEGED control-plane login (M-AUTH): when the\n * server runs with auth_callout, the api/worker authenticates as a static account\n * user permitted to request exact generation-fenced agent RPC subjects + receive\n * its inbox replies. When `auth`\n * is omitted the connection is anonymous (local dev / a NATS without auth_callout)\n * — the existing behavior, unchanged.\n */\nexport async function createNatsEventBus(\n natsUrl: string,\n auth?: { user: string; pass: string },\n options: EventBusOptions = {},\n): Promise<EventBus> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (auth) {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n }\n const nc = await (options.connect ?? connect)(withReconnectDefaults(connectOptions));\n let connected = true;\n let reconnectGeneration = 0;\n const reconnectSubscribers = new Set<(generation: number) => void>();\n logConnectionStatus(nc, \"event-bus\", options.logger, (type) => {\n if (\n type === \"disconnect\" ||\n type === \"reconnecting\" ||\n type === \"staleConnection\" ||\n type === \"error\"\n ) {\n connected = false;\n } else if (type === \"connect\" || type === \"reconnect\") {\n connected = true;\n }\n if (type === \"reconnect\") {\n reconnectGeneration += 1;\n for (const subscriber of reconnectSubscribers) {\n try {\n subscriber(reconnectGeneration);\n } catch (error) {\n (options.logger?.warn ?? silentLogger.warn)(\"NATS reconnect observer failed\", {\n label: \"event-bus\",\n reconnectGeneration,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n });\n const requestConnection: RequestConnection = {\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeout),\n };\n const opStreamConnection: OpStreamConnection = {\n subscribe: (subject) => nc.subscribe(subject),\n publish: (subject, payload) => {\n nc.publish(subject, payload);\n },\n flush: () => nc.flush(),\n };\n const publishSessionEvents = (\n workspaceId: string,\n sessionId: string,\n events: SessionEvent[],\n ): void => {\n const batches = sessionEventBatchesByBytes(workspaceId, sessionId, events);\n for (const batch of batches) {\n nc.publish(\n sessionSubject(workspaceId, sessionId),\n codec.encode({ workspaceId, sessionId, events: batch }),\n );\n }\n if (batches.length > 1) {\n (options.logger?.debug ?? silentLogger.debug)(\"NATS session event batch chunked\", {\n workspaceId,\n sessionId,\n eventCount: events.length,\n batchCount: batches.length,\n maxMessageBytes: SESSION_EVENT_NATS_MESSAGE_MAX_BYTES,\n });\n }\n observeEventBoundaries(batches.flat(), options.logger);\n };\n return {\n sessionEventDurableFanout: {\n version: SESSION_EVENT_DURABLE_FANOUT_CAPABILITY_VERSION,\n subscribeRecovery: (onRecovery) => {\n reconnectSubscribers.add(onRecovery);\n return () => reconnectSubscribers.delete(onRecovery);\n },\n },\n publish: async (workspaceId, sessionId, events) => {\n if (events.length === 0) {\n return;\n }\n // Best-effort LIVE fan-out. These events are ALREADY durably appended to\n // the DB before we get here (they carry a DB-assigned `sequence`), and\n // every consumer reconciles from that durable log — the server SSE stream\n // replays + gap-backfills via `listSessionEvents`, and the SDK client\n // reconnects and replays from the durable events endpoint. So a publish\n // that fails during a broker blip only delays LIVE delivery (healed by the\n // next successful publish's gap-backfill, or a stream reconnect); it must\n // never throw the in-flight turn to death.\n try {\n publishSessionEvents(workspaceId, sessionId, events);\n } catch (error) {\n // `publish()` throws synchronously only when the connection is fully\n // CLOSED (with infinite reconnect, effectively never outside shutdown).\n (options.logger?.warn ?? silentLogger.warn)(\n \"NATS live publish dropped; events are durable in the DB and reconcile on stream replay\",\n {\n workspaceId,\n sessionId,\n error: error instanceof Error ? error.message : String(error),\n },\n );\n return;\n }\n await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n publishConfirmed: async (workspaceId, sessionId, events) => {\n if (events.length === 0) {\n return;\n }\n publishSessionEvents(workspaceId, sessionId, events);\n await flushConfirmedWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n subscribe: async (workspaceId, sessionId, onEvents) =>\n subscribeSession(nc, workspaceId, sessionId, onEvents),\n publishWorkspaceControl: async (workspaceId, event) => {\n try {\n const encoded = workspaceControlEventNatsPayload(event);\n nc.publish(workspaceControlSubject(workspaceId), encoded);\n } catch (error) {\n (options.logger?.warn ?? silentLogger.warn)(\n \"NATS workspace-control invalidation dropped; clients reconcile from Postgres\",\n {\n workspaceId,\n revision: event.revision,\n error: error instanceof Error ? error.message : String(error),\n },\n );\n return;\n }\n await flushWithTimeout(nc, PUBLISH_FLUSH_TIMEOUT_MS);\n },\n subscribeWorkspaceControl: async (workspaceId, onEvent) => {\n const sub = nc.subscribe(workspaceControlSubject(workspaceId));\n void (async () => {\n for await (const msg of sub) {\n await onEvent(\n boundWorkspaceControlEvent(codec.decode(msg.data) as WorkspaceControlEvent, {\n surface: \"nats_legacy_guard\",\n }),\n );\n }\n })();\n return () => sub.unsubscribe();\n },\n request: async (subject, payload, opts) => requestReply(nc, subject, payload, opts.timeoutMs),\n subscribeRequests: (subject, handler) => subscribeRequests(nc, subject, handler),\n subscribeAgentEvents: (subject, handler) => subscribeAgentEvents(nc, subject, handler),\n getRequestConnection: () => requestConnection,\n getOpStreamConnection: () => opStreamConnection,\n isConnected: () => connected && !nc.isClosed() && !nc.isDraining(),\n close: async () => {\n reconnectSubscribers.clear();\n await nc.drain();\n },\n };\n}\n\n/**\n * A standalone NATS connection answering request/reply on ONE subject — the\n * transport primitive the auth-callout responder uses. It is DELIBERATELY a\n * SEPARATE connection from the event bus: the callout responder authenticates as\n * the callout account's `auth_users` user (a username/password or token in the\n * `AUTH` account), which is a DIFFERENT identity from the control-plane's\n * privileged account that the event bus + `NatsControlRpc` ride. One connection\n * per identity; never multiplex the two.\n *\n * `request`/`reply` here is the RAW NATS request/reply (`$SYS.REQ.USER.AUTH`): the\n * server publishes an authorization request with a reply inbox; the handler returns\n * the signed authorization-response bytes which we `respond` on that inbox.\n */\nexport interface ResponderConnection {\n /** Subscribe-and-reply on `subject`; returns an async close that drains. */\n close: () => Promise<void>;\n}\n\n/** Connection auth for a standalone NATS connection (the callout responder). */\nexport type NatsConnectAuth =\n | { kind: \"user-password\"; user: string; pass: string }\n | { kind: \"token\"; token: string }\n | { kind: \"anonymous\" };\n\n/**\n * Open a standalone NATS connection and subscribe `subject`, replying to every\n * request with `handler(requestBytes, subject)`. Used by the auth-callout\n * responder to serve `$SYS.REQ.USER.AUTH` as the callout auth user. Returns a\n * handle whose `close()` drains the connection. A handler that throws leaves the\n * request UNANSWERED — for auth-callout that means the server denies the\n * connection on its own timeout, which is the correct fail-closed behavior (a\n * responder bug must never accidentally grant access).\n */\nexport async function createResponderConnection(\n natsUrl: string,\n auth: NatsConnectAuth,\n subject: string,\n handler: RequestHandler,\n options: {\n name?: string;\n logger?: EventLogger;\n connect?: typeof connect;\n } = {},\n): Promise<ResponderConnection> {\n const connectOptions: ConnectionOptions = { servers: natsUrl };\n if (options.name) {\n connectOptions.name = options.name;\n }\n if (auth.kind === \"user-password\") {\n connectOptions.user = auth.user;\n connectOptions.pass = auth.pass;\n } else if (auth.kind === \"token\") {\n connectOptions.token = auth.token;\n }\n const nc = await (options.connect ?? connect)(withReconnectDefaults(connectOptions));\n logConnectionStatus(\n nc,\n options.name ? `auth-callout:${options.name}` : \"auth-callout\",\n options.logger,\n );\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave UNANSWERED — fail-closed. The server denies the connect attempt\n // on its callout timeout; a responder error never grants access.\n }\n }\n })();\n return {\n close: async () => {\n sub.unsubscribe();\n await nc.drain();\n },\n };\n}\n\n/**\n * Optional timing seam for {@link appendAndPublishEvents}: `onAppend` fires after\n * the durable DB write, `onPublish` after the best-effort live fan-out (on both\n * success AND failure of the publish, so a broker blip still records its latency).\n * Kept as a plain callback so the events package takes no dependency on the\n * observability package; the worker wires it to Prometheus histograms.\n */\nexport type AppendPublishObserver = {\n onAppend?: (info: { durationSeconds: number; count: number }) => void;\n onAppendPhase?: SessionEventAppendObserver[\"onPhase\"];\n onPublish?: (info: { durationSeconds: number; count: number }) => void;\n};\n\nexport type AppendPublishOptions = AppendPublishObserver & {\n /** Test/host persistence seam; production uses the database implementation. */\n appendSessionEvents?: typeof appendSessionEvents;\n};\n\n/**\n * Invoke a phase-timing callback with the elapsed seconds since `startedAt` and the\n * event count, swallowing any throw so a metrics sink can never break the\n * append/publish path. Exported for direct unit testing: the wider test suite\n * installs a process-global `mock.module(\"@opengeni/events\")` that stubs\n * `appendAndPublishEvents` (spreading the real module for everything else), so the\n * observer wiring can only be exercised through a helper that survives that mock.\n */\nexport function observeSince(\n fn: ((info: { durationSeconds: number; count: number }) => void) | undefined,\n startedAt: number,\n count: number,\n): void {\n if (!fn) {\n return;\n }\n try {\n fn({\n durationSeconds: Math.max(0, (performance.now() - startedAt) / 1000),\n count,\n });\n } catch {\n // Metrics emission must never affect the append/publish path.\n }\n}\n\nexport async function appendAndPublishEvents(\n db: Database,\n bus: EventBus,\n workspaceId: string,\n sessionId: string,\n events: AppendEventInput[],\n options: AppendPublishOptions = {},\n): Promise<SessionEvent[]> {\n const appendStartedAt = performance.now();\n const appended = await (options.appendSessionEvents ?? appendSessionEvents)(\n db,\n workspaceId,\n sessionId,\n events,\n );\n observeSince(options.onAppend, appendStartedAt, appended.length);\n await publishDurableSessionEvents(bus, workspaceId, sessionId, appended, options);\n return appended;\n}\n\n/**\n * Best-effort live fanout for events another DB helper already committed in\n * the same transaction as related durable state. This must never append again.\n */\nexport async function publishDurableSessionEvents(\n bus: EventBus,\n workspaceId: string,\n sessionId: string,\n appended: SessionEvent[],\n observe?: AppendPublishObserver,\n): Promise<void> {\n if (appended.length === 0) {\n return;\n }\n // The committed DB events are the durable system of record; this publish is only a\n // best-effort LIVE fan-out. Guard it so NO EventBus implementation can throw an\n // in-flight agent turn to death on a transient NATS disconnect — consumers\n // reconcile any missed live events from the durable log via the events/stream\n // endpoint (DB replay + gap-backfill). The managed `createNatsEventBus` bus\n // already swallows internally, so this catch is the belt-and-suspenders guard\n // for any other bus impl (and a fully CLOSED connection during shutdown).\n const publishStartedAt = performance.now();\n try {\n await bus.publish(workspaceId, sessionId, appended);\n } catch {\n console.warn(\"[events] live publish failed; durable events reconcile on stream replay\", {\n errorClass: \"EventPublishOperationError\",\n errorCode: \"session_event_live_publish_failed\",\n origin: \"events\",\n eventCount: appended.length,\n });\n }\n observeSince(observe?.onPublish, publishStartedAt, appended.length);\n}\n\n/** Best-effort fanout for a workspace-control event already committed in PostgreSQL. */\nexport async function publishDurableWorkspaceControlEvent(\n bus: EventBus,\n workspaceId: string,\n event: WorkspaceControlEvent,\n): Promise<void> {\n try {\n await bus.publishWorkspaceControl(workspaceId, event);\n } catch {\n console.warn(\n \"[events] workspace-control live publish failed; durable event reconciles on stream replay\",\n {\n errorClass: \"EventPublishOperationError\",\n errorCode: \"workspace_control_live_publish_failed\",\n origin: \"events\",\n },\n );\n }\n}\n\nexport async function appendAndPublishTurnEventsFenced(\n db: Database,\n bus: EventBus,\n workspaceId: string,\n sessionId: string,\n turnId: string,\n executionGeneration: number,\n attemptId: string,\n events: AppendEventInput[],\n observe?: AppendPublishObserver,\n): Promise<{\n events: SessionEvent[];\n accepted: boolean;\n canonicalStartupMilestones: CanonicalTurnStartupMilestoneReceipt[];\n}> {\n const appendStartedAt = performance.now();\n const result = await appendSessionEventsForTurnAttempt(\n db,\n workspaceId,\n sessionId,\n turnId,\n executionGeneration,\n attemptId,\n events,\n observe?.onAppendPhase ? { onPhase: observe.onAppendPhase } : undefined,\n );\n observeSince(observe?.onAppend, appendStartedAt, result.events.length);\n if (result.events.length === 0) return result;\n const publishStartedAt = performance.now();\n try {\n await bus.publish(workspaceId, sessionId, result.events);\n } catch {\n console.warn(\"[events] live fenced publish failed; events remain durable\", {\n errorClass: \"EventPublishOperationError\",\n errorCode: \"fenced_event_live_publish_failed\",\n origin: \"events\",\n eventCount: result.events.length,\n });\n }\n observeSince(observe?.onPublish, publishStartedAt, result.events.length);\n return result;\n}\n\nfunction subscribeSession(\n nc: NatsConnection,\n workspaceId: string,\n sessionId: string,\n onEvents: (events: SessionEvent[]) => void | Promise<void>,\n): () => void {\n const sub: Subscription = nc.subscribe(sessionSubject(workspaceId, sessionId));\n void (async () => {\n for await (const msg of sub) {\n const decoded = codec.decode(msg.data) as SessionBusMessage | SessionEvent;\n const events = (\"events\" in decoded ? decoded.events : [decoded]).map((event) =>\n boundSessionEventForSurface(event, \"nats_legacy_guard\"),\n );\n await onEvents(events);\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * A binary request/reply over the managed connection. Returns ONLY the reply\n * bytes (the `RequestReply` shape) — the request/reply error semantics (a\n * no-responder NATS 503, a request timeout) propagate as the rejected promise so\n * the caller owns the mapping. The reply is delivered via the connection's\n * built-in mux inbox; no extra subscription is created here.\n */\nasync function requestReply(\n nc: NatsConnection,\n subject: string,\n payload: Uint8Array,\n timeout: number,\n): Promise<RequestReply> {\n const msg: Msg = await nc.request(subject, payload, { timeout });\n return { data: msg.data };\n}\n\n/**\n * Subscribe to `subject` and reply to every request with the handler's bytes,\n * over the SAME connection. The responder side of request/reply: each delivered\n * `Msg` carries a `reply` inbox; `msg.respond(bytes)` publishes the answer there.\n * A handler that throws (or a message with no `reply` subject) is left unanswered\n * — the requester then sees a timeout, never a malformed reply.\n */\nfunction subscribeRequests(\n nc: NatsConnection,\n subject: string,\n handler: RequestHandler,\n): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n // A request always carries a reply inbox; a plain publish to this subject\n // (no reply) is ignored — request/reply is the only contract here.\n if (!msg.reply) {\n continue;\n }\n try {\n const reply = await handler(msg.data, msg.subject);\n msg.respond(reply);\n } catch {\n // Leave the request unanswered: the requester's request times out, which\n // the selfhosted control plane reads as a transient blip (reconnecting),\n // never a malformed reply. The responder stays subscribed for the next op.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\n/**\n * Subscribe to the one-way agent event plane: deliver each published payload (the\n * agent's `AgentEvent` heartbeat / going-offline, NOT a request/reply) to the\n * handler with its concrete subject. A plain `nc.subscribe` (no reply); a handler\n * that throws is swallowed so one bad event never tears down the subscription\n * (ingestion is best-effort — a metrics gap is never fatal).\n */\nfunction subscribeAgentEvents(\n nc: NatsConnection,\n subject: string,\n handler: (payload: Uint8Array, subject: string) => void | Promise<void>,\n): () => void {\n const sub: Subscription = nc.subscribe(subject);\n void (async () => {\n for await (const msg of sub) {\n try {\n await handler(msg.data, msg.subject);\n } catch {\n // Swallow: best-effort ingestion. The subscription stays live for the\n // next event.\n }\n }\n })();\n return () => {\n sub.unsubscribe();\n };\n}\n\nexport function formatSse<T extends { sequence: number; type: string }>(\n event: T,\n idSequence = event.sequence,\n): string {\n const trustedId =\n Number.isSafeInteger(idSequence) && idSequence >= event.sequence ? idSequence : event.sequence;\n return [\n `id: ${trustedId}`,\n `event: ${event.type}`,\n `data: ${JSON.stringify(event)}`,\n \"\",\n \"\",\n ].join(\"\\n\");\n}\n\n/** Canonical one-event NATS payload with an exact broker byte assertion. */\nexport function workspaceControlEventNatsPayload(event: WorkspaceControlEvent): Uint8Array {\n const bounded = boundWorkspaceControlEvent(event, {\n surface: \"nats_legacy_guard\",\n });\n const encoded = codec.encode(bounded);\n if (encoded.byteLength > WORKSPACE_CONTROL_NATS_MESSAGE_MAX_BYTES) {\n throw new RangeError(\n `Workspace-control event cannot fit in the NATS envelope (${encoded.byteLength} > ${WORKSPACE_CONTROL_NATS_MESSAGE_MAX_BYTES} bytes)`,\n );\n }\n return encoded;\n}\n\n/** Defensively bounds current and historical workspace invalidations per frame. */\nexport function formatWorkspaceControlEventSse(event: WorkspaceControlEvent): string {\n const bounded = boundWorkspaceControlEvent(event, {\n surface: \"sse_legacy_guard\",\n });\n const formatted = formatSse(bounded);\n const bytes = new TextEncoder().encode(formatted).byteLength;\n if (bytes > SESSION_EVENT_SSE_FRAME_MAX_BYTES) {\n throw new RangeError(\n `Bounded workspace-control SSE frame exceeds its envelope (${bytes} > ${SESSION_EVENT_SSE_FRAME_MAX_BYTES} bytes)`,\n );\n }\n return formatted;\n}\n\n/** Defensively bounds historical rows before they become one SSE frame. */\nexport function formatSessionEventSse(\n event: SessionEvent,\n coveredThrough = event.sequence,\n): string {\n const bounded = boundSessionEventForSurface(event, \"sse_legacy_guard\");\n const formatted = formatSse(bounded, coveredThrough);\n if (new TextEncoder().encode(formatted).byteLength > SESSION_EVENT_SSE_FRAME_MAX_BYTES) {\n // The payload normalizer targets 60 KiB, so this fallback is reachable only\n // for a malformed legacy event with oversized non-payload envelope fields.\n const minimal: SessionEvent = {\n ...bounded,\n type: bounded.type.slice(0, 256) as SessionEvent[\"type\"],\n payload: boundSessionEventPayload(\n {\n preview: \"[legacy event envelope omitted at SSE frame boundary]\",\n // The complete event has already crossed the non-invoking bounded\n // projection above. Do not re-read an untrusted source accessor merely\n // to populate optional diagnostic accounting in this last-resort path.\n originalPayloadBytes: null,\n },\n { surface: \"sse_legacy_guard\", maxBytes: 4096 },\n ),\n };\n return formatSse(minimal, coveredThrough);\n }\n return formatted;\n}\n\n/**\n * Split an already-durable batch by exact encoded NATS bytes. Each event is\n * defensively normalized first so historical oversized rows cannot exceed the\n * broker envelope. Sequence and ordering are unchanged across chunks.\n */\nexport function sessionEventBatchesByBytes(\n workspaceId: string,\n sessionId: string,\n events: readonly SessionEvent[],\n maxBytes = SESSION_EVENT_NATS_MESSAGE_MAX_BYTES,\n): SessionEvent[][] {\n const bounded = events.map((event) => boundSessionEventForSurface(event, \"nats_legacy_guard\"));\n const batches: SessionEvent[][] = [];\n let current: SessionEvent[] = [];\n for (const event of bounded) {\n const candidate = [...current, event];\n const encodedBytes = codec.encode({\n workspaceId,\n sessionId,\n events: candidate,\n }).byteLength;\n if (current.length > 0 && encodedBytes > maxBytes) {\n batches.push(current);\n current = [event];\n } else {\n current = candidate;\n }\n }\n if (current.length > 0) batches.push(current);\n for (const batch of batches) {\n const encodedBytes = codec.encode({\n workspaceId,\n sessionId,\n events: batch,\n }).byteLength;\n if (encodedBytes > maxBytes) {\n throw new RangeError(\n `Session event cannot fit in the configured NATS envelope (${encodedBytes} > ${maxBytes} bytes)`,\n );\n }\n }\n return batches;\n}\n\n/** Return one count+byte-bounded HTTP page and truthful continuation facts. */\nexport function boundSessionEventHttpPage(\n events: readonly SessionEvent[],\n options: {\n direction: \"after\" | \"before\";\n maxBytes?: number;\n /** Exact mode is restricted to already-canonical forensic REST rows. */\n eventProjection?: \"bounded\" | \"exact\";\n /** Out-of-band raw coverage for events synthesized by trusted coalescing. */\n coveredThroughBySequence?: ReadonlyMap<number, number>;\n },\n): {\n events: SessionEvent[];\n truncated: boolean;\n nextSequence: number | null;\n bytes: number;\n} {\n const maxBytes = options.maxBytes ?? SESSION_EVENT_HTTP_PAGE_MAX_BYTES;\n const selected: SessionEvent[] = [];\n let bytes = 2; // []\n const projected =\n options.eventProjection === \"exact\"\n ? [...events]\n : events.map((event) => boundSessionEventForSurface(event, \"http_projection\"));\n const candidates = options.direction === \"after\" ? projected : [...projected].reverse();\n for (const event of candidates) {\n const eventBytes = sessionEventJsonBytes(event);\n const separator = selected.length === 0 ? 0 : 1;\n if (bytes + separator + eventBytes > maxBytes) break;\n selected.push(event);\n bytes += separator + eventBytes;\n }\n if (options.direction === \"before\") selected.reverse();\n if (projected.length > 0 && selected.length === 0) {\n throw new RangeError(\n `A bounded session event cannot fit in the configured HTTP page envelope (${maxBytes} bytes)`,\n );\n }\n const truncated = selected.length < projected.length;\n const edge = options.direction === \"after\" ? selected.at(-1) : selected[0];\n return {\n events: selected,\n truncated,\n nextSequence:\n edge === undefined\n ? null\n : options.direction === \"after\"\n ? Math.max(\n edge.sequence,\n options.coveredThroughBySequence?.get(edge.sequence) ?? edge.sequence,\n )\n : edge.sequence,\n bytes,\n };\n}\n\n/** Return one count+byte-bounded workspace-control page and resume cursor. */\nexport function boundWorkspaceControlHttpPage(\n events: readonly WorkspaceControlEvent[],\n maxBytes = WORKSPACE_CONTROL_HTTP_PAGE_MAX_BYTES,\n): {\n events: WorkspaceControlEvent[];\n truncated: boolean;\n nextSequence: number | null;\n bytes: number;\n} {\n const projected = events.map((event) =>\n boundWorkspaceControlEvent(event, { surface: \"http_projection\" }),\n );\n const selected: WorkspaceControlEvent[] = [];\n let bytes = 2; // []\n for (const event of projected) {\n const eventBytes = sessionEventJsonBytes(event);\n const separator = selected.length === 0 ? 0 : 1;\n if (bytes + separator + eventBytes > maxBytes) break;\n selected.push(event);\n bytes += separator + eventBytes;\n }\n if (projected.length > 0 && selected.length === 0) {\n throw new RangeError(\n `A bounded workspace-control event cannot fit in the HTTP page envelope (${maxBytes} bytes)`,\n );\n }\n return {\n events: selected,\n truncated: selected.length < projected.length,\n nextSequence: selected.at(-1)?.sequence ?? null,\n bytes,\n };\n}\n\n/** Raw durable cursor covered by a possibly coalesced compact event. */\nexport function sessionEventResumeSequence(event: SessionEvent): number {\n return typeof event.coveredThrough === \"number\" &&\n Number.isSafeInteger(event.coveredThrough) &&\n event.coveredThrough >= event.sequence\n ? event.coveredThrough\n : event.sequence;\n}\n\nfunction boundSessionEventForSurface(\n event: SessionEvent,\n surface: SessionEventBoundarySurface,\n): SessionEvent {\n return boundSessionEvent(event, { surface });\n}\n\nfunction observeEventBoundaries(events: readonly SessionEvent[], logger?: EventLogger): void {\n for (const event of events) {\n const boundary = sessionEventPayloadTruncation(event.payload);\n if (!boundary) continue;\n (logger?.debug ?? silentLogger.debug)(\"Session event payload is a bounded audit preview\", {\n eventType: event.type,\n surface: boundary.surface,\n reason: boundary.reason,\n originalBytes: boundary.originalBytes,\n deliveredBytes: boundary.deliveredBytes,\n estimatedOriginalTokens: boundary.estimatedOriginalTokens,\n estimatedDeliveredTokens: boundary.estimatedDeliveredTokens,\n fullEvidenceAvailable: boundary.fullEvidence.available,\n retainedOutputKind: boundary.fullEvidence.available ? boundary.fullEvidence.kind : null,\n });\n }\n}\n\nfunction workspaceControlSubject(workspaceId: string): string {\n return `workspaces.${workspaceId}.control`;\n}\n","import {\n boundSessionEventPayload,\n sessionEventPayloadTruncation,\n type SessionEvent,\n} from \"@opengeni/contracts\";\n\nconst COALESCIBLE_DELTA_TYPES = new Set([\n \"agent.message.delta\",\n \"agent.reasoning.delta\",\n \"sandbox.command.output.delta\",\n]);\n\n/** Flush long runs incrementally before concatenation can become unbounded. */\nexport const SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES = 48 * 1024;\nconst encoder = new TextEncoder();\n\nexport type CoalescedSessionEventPage = {\n events: SessionEvent[];\n /** Durable raw sequence covered by each returned synthetic event sequence. */\n coveredThroughBySequence: ReadonlyMap<number, number>;\n};\n\ntype DeltaRun = {\n first: SessionEvent;\n lastSequence: number;\n text: string;\n textBytes: number;\n sandboxName: string | undefined;\n sandboxStream: string | undefined;\n sandboxCommandId: string | undefined;\n};\n\nexport function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[] {\n return coalesceSessionEventDeltasWithCoverage(events).events;\n}\n\nexport function coalesceSessionEventDeltasWithCoverage(\n events: SessionEvent[],\n): CoalescedSessionEventPage {\n const coalesced: SessionEvent[] = [];\n const coveredThroughBySequence = new Map<number, number>();\n let run: DeltaRun | null = null;\n\n const flush = () => {\n if (!run) {\n return;\n }\n const payload =\n run.first.type === \"sandbox.command.output.delta\"\n ? // Sandbox output keeps its CANONICAL field (`chunk` — the terminal and\n // projection read it) plus the stream/commandId identity of the run.\n {\n chunk: run.text,\n coalescedUntil: run.lastSequence,\n ...(run.sandboxStream !== undefined ? { stream: run.sandboxStream } : {}),\n ...(run.sandboxCommandId !== undefined ? { commandId: run.sandboxCommandId } : {}),\n ...(run.sandboxName !== undefined ? { name: run.sandboxName } : {}),\n }\n : {\n text: run.text,\n coalescedUntil: run.lastSequence,\n };\n coalesced.push({\n ...run.first,\n coveredThrough: run.lastSequence,\n payload: boundSessionEventPayload(payload, {\n surface: \"http_projection\",\n }),\n });\n coveredThroughBySequence.set(run.first.sequence, run.lastSequence);\n run = null;\n };\n\n for (const event of events) {\n if (!isCoalescibleDelta(event)) {\n flush();\n coalesced.push(event);\n coveredThroughBySequence.set(event.sequence, event.sequence);\n continue;\n }\n\n const isSandbox = event.type === \"sandbox.command.output.delta\";\n const sandboxName = isSandbox ? sandboxDeltaName(event.payload) : undefined;\n const sandboxStream = isSandbox ? sandboxDeltaString(event.payload, \"stream\") : undefined;\n const sandboxCommandId = isSandbox ? sandboxDeltaString(event.payload, \"commandId\") : undefined;\n const text = deltaText(event);\n if (\n run &&\n sameDeltaRun(run.first, event, run.sandboxName, sandboxName) &&\n run.sandboxStream === sandboxStream &&\n run.sandboxCommandId === sandboxCommandId\n ) {\n const textBytes = encoder.encode(text).byteLength;\n if (\n (run.textBytes === 0 && textBytes <= SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES) ||\n run.textBytes + textBytes <= SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES\n ) {\n run.text += text;\n run.textBytes += textBytes;\n run.lastSequence = event.sequence;\n continue;\n }\n // The current segment is already useful and bounded. Flush before adding\n // the next raw delta rather than building the full run and truncating it\n // only after a multi-megabyte intermediate allocation.\n flush();\n } else {\n flush();\n }\n\n run = {\n first: event,\n lastSequence: event.sequence,\n text,\n textBytes: encoder.encode(text).byteLength,\n sandboxName,\n sandboxStream,\n sandboxCommandId,\n };\n }\n\n flush();\n return { events: coalesced, coveredThroughBySequence };\n}\n\nfunction isCoalescibleDelta(event: SessionEvent): boolean {\n return (\n COALESCIBLE_DELTA_TYPES.has(event.type) && sessionEventPayloadTruncation(event.payload) === null\n );\n}\n\nfunction sameDeltaRun(\n first: SessionEvent,\n next: SessionEvent,\n firstSandboxName: string | undefined,\n nextSandboxName: string | undefined,\n): boolean {\n if (first.type !== next.type) {\n return false;\n }\n if ((first.turnId ?? null) !== (next.turnId ?? null)) {\n return false;\n }\n return first.type !== \"sandbox.command.output.delta\" || firstSandboxName === nextSandboxName;\n}\n\nfunction deltaText(event: SessionEvent): string {\n if (event.type === \"agent.reasoning.delta\") {\n return reasoningText(event.payload);\n }\n const payload = asRecord(event.payload);\n if (event.type === \"sandbox.command.output.delta\") {\n // `chunk` is the canonical wire field (contracts SandboxCommandOutputDeltaPayload);\n // text/output are tolerated legacy shapes.\n for (const key of [\"chunk\", \"text\", \"output\"] as const) {\n if (typeof payload[key] === \"string\") {\n return payload[key] as string;\n }\n }\n return \"\";\n }\n return typeof payload.text === \"string\" ? payload.text : \"\";\n}\n\nfunction reasoningText(payload: unknown): string {\n const record = asRecord(payload);\n if (typeof record.text === \"string\") {\n return record.text;\n }\n const content = asRecord(asRecord(record.item).rawItem).content;\n if (!Array.isArray(content)) {\n return \"\";\n }\n return content\n .map((part) => {\n const text = asRecord(part).text;\n return typeof text === \"string\" ? text : \"\";\n })\n .join(\"\");\n}\n\nfunction sandboxDeltaName(payload: unknown): string | undefined {\n const name = asRecord(payload).name;\n return typeof name === \"string\" ? name : undefined;\n}\n\nfunction sandboxDeltaString(payload: unknown, key: \"stream\" | \"commandId\"): string | undefined {\n const value = asRecord(payload)[key];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : {};\n}\n","// packages/events/src/nats-jwt.ts — NATS JWT v2 signing for the auth-callout\n// responder (bring-your-own-compute M-AUTH; NATS Accounts per\n// workspace + §17 the isolation smoke).\n//\n// This is the cryptographic core of the auth-callout tenancy boundary. When an\n// external agent connects to NATS presenting its `oge_` enrollment bearer as the\n// connect auth-token, nats-server (configured with `auth_callout`) issues an\n// authorization request on `$SYS.REQ.USER.AUTH`. Our responder (auth-callout.ts)\n// validates the bearer and answers with a SIGNED authorization-response JWT that\n// embeds a SIGNED user JWT scoping the connection to publish/subscribe ONLY its\n// generation-fenced process subtree\n// `agent.<workspaceId>.<agentId>.connection.<instanceId>.>` (+ reply\n// `_INBOX.>`). That exact scope prevents both cross-workspace access and a stale\n// process sharing credentials with its live successor.\n//\n// WHY HAND-ROLL THE JWT ENCODING (vs a dep): the NATS JWT v2 wire format is small,\n// stable, and fully specified (ADR-26 + nats-io/jwt): a base64url header\n// `{\"typ\":\"JWT\",\"alg\":\"ed25519-nkey\"}`, base64url JSON claims whose `jti` is the\n// base32(SHA-512/256(claims-with-blank-jti)), and an ed25519 nkey signature over\n// `header.payload`. nkeys (re-exported by the `nats` package we already depend on)\n// gives us the ed25519 sign primitive; Node `crypto` gives SHA-512/256. So we own\n// the encoding in a few well-tested functions rather than pull an alpha\n// `@nats-io/jwt` (0.0.x) whose nkeys-version compat is uncertain. No `xkey`\n// encryption is used (the bearer is already an authenticated identity claim and\n// the wire is TLS — encryption is an optional ADR-26 hardening, off here).\n//\n// SECURITY: the account SIGNING SEED never leaves this process and is NEVER logged.\n// Callers pass it as a `string` seed; we `fromSeed` it once per sign. The bearer\n// the responder validates is HMAC-verified elsewhere (verifyEnrollmentBearer); this\n// module only mints the scoped NATS credential once identity is proven.\n\nimport { createHash } from \"node:crypto\";\nimport { nkeys } from \"nats\";\n\n/** The NATS JWT v2 header — constant for every token we mint (ADR-26 / nats-io/jwt:\n * `TokenTypeJwt=\"JWT\"`, `AlgorithmNkey=\"ed25519-nkey\"`). */\nconst JWT_HEADER = { typ: \"JWT\", alg: \"ed25519-nkey\" } as const;\n\n/** NATS user-claim `nats.type` discriminator + `nats.version` for v2 claims. */\nconst USER_CLAIM_TYPE = \"user\";\nconst AUTH_RESPONSE_CLAIM_TYPE = \"authorization_response\";\nconst NATS_CLAIM_VERSION = 2;\n\n/** A NATS permission set: subject allow/deny lists (ADR-26 `pub`/`sub` →\n * `allow`/`deny`). An empty/undefined list means \"no explicit grant\" — combined\n * with the agent scope below, the connection can ONLY reach what `allow` lists. */\nexport interface NatsPermission {\n allow?: string[];\n deny?: string[];\n}\n\n/** The pub/sub permissions embedded in a user JWT. */\nexport interface NatsPermissions {\n pub: NatsPermission;\n sub: NatsPermission;\n}\n\n/**\n * The minimal nkey keypair surface this module needs — exactly what\n * `nkeys.fromSeed(seed)` returns. Declared structurally so the module does not\n * leak the `nats` nkeys type through its public signature.\n */\ninterface NkeyPair {\n getPublicKey(): string;\n sign(input: Uint8Array): Uint8Array;\n}\n\n/** base64url (RawURLEncoding — no padding), matching nats-io/jwt's `serialize`. */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64url\");\n}\n\n/** RFC 4648 base32 (standard alphabet, NO padding) — the encoding nats-io/jwt\n * uses for the `jti` hash. Node has no built-in base32, so a tiny encoder. */\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\nfunction base32NoPadding(bytes: Uint8Array): string {\n let bits = 0;\n let value = 0;\n let out = \"\";\n for (const byte of bytes) {\n value = (value << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n out += BASE32_ALPHABET[(value >>> bits) & 31];\n }\n }\n if (bits > 0) {\n out += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n }\n return out;\n}\n\n/**\n * Compute the canonical NATS `jti`: base32(NoPadding, std-alphabet) of the\n * SHA-512/256 of the claims object SERIALIZED WITH AN EMPTY `jti` (nats-io/jwt's\n * `hash`). nats-server recomputes + verifies this on decode, so it must match\n * byte-for-byte. We serialize the SAME object we will sign, only with `jti:\"\"`.\n */\nfunction computeJti(claimsWithBlankJti: object): string {\n const json = JSON.stringify(claimsWithBlankJti);\n const digest = createHash(\"sha512-256\").update(json, \"utf8\").digest();\n return base32NoPadding(digest);\n}\n\n/**\n * Encode + sign a NATS v2 JWT. The `claims` MUST already carry `iss`/`sub`/`iat`\n * (+ optional `aud`/`exp`) and a `nats` block; this function fills `jti` (the\n * canonical hash), serializes `header.payload`, signs that with `signingKey`, and\n * appends the base64url signature. Returns the compact `header.payload.signature`.\n */\nfunction encodeJwt(claims: Record<string, unknown>, signingKey: NkeyPair): string {\n // jti is the hash of the claims with jti blanked — set it blank, hash, then set.\n const withBlankJti = { ...claims, jti: \"\" };\n const jti = computeJti(withBlankJti);\n const finalClaims = { ...claims, jti };\n\n const header = base64UrlEncode(Buffer.from(JSON.stringify(JWT_HEADER), \"utf8\"));\n const payload = base64UrlEncode(Buffer.from(JSON.stringify(finalClaims), \"utf8\"));\n const signingInput = `${header}.${payload}`;\n const signature = signingKey.sign(Buffer.from(signingInput, \"utf8\"));\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\n/**\n * Input to mint a workspace-scoped NATS user JWT for an enrolled agent.\n * - `userPublicKey` — the `user_nkey` from the authorization request; it MUST be\n * the `sub` of the user JWT (nats-server rejects a mismatch).\n * - `accountSeed` — the callout account SIGNING seed (`SA...`); both the user JWT\n * `iss` (its public key) and the signature come from it. NEVER logged.\n * - `name` — a human label for the user (the agent id), for server logs.\n * - `permissions` — the pub/sub allow/deny lists (the workspace scope).\n * - `expiresAtSeconds` — optional absolute `exp` (unix seconds). When set the\n * server will expire the connection's credential; we tie it to the bearer's\n * remaining life so a revoked/expired enrollment cannot outlive its bearer.\n */\nexport interface MintUserJwtInput {\n userPublicKey: string;\n accountSeed: string;\n name: string;\n permissions: NatsPermissions;\n /** The target account NAME (the `auth_callout.account`) the user binds to; the\n * embedded user JWT's `aud` in server-config mode. */\n audienceAccount: string;\n expiresAtSeconds?: number;\n}\n\n/**\n * Mint a signed NATS user JWT scoped by `permissions`. In auth-callout SERVER\n * mode the user JWT is signed by the callout ISSUER ACCOUNT key, and its `iss` is\n * that account's public key. The returned JWT is embedded as `nats.jwt` in the\n * authorization response.\n */\nexport function mintUserJwt(input: MintUserJwtInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: USER_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n pub: input.permissions.pub,\n sub: input.permissions.sub,\n // Unlimited subscriptions / data / payload (the workspace subject scope, NOT\n // a connection-resource quota, is the boundary here).\n subs: -1,\n data: -1,\n payload: -1,\n };\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n name: input.name,\n sub: input.userPublicKey,\n // SERVER-config-mode placement: nats-server reads the embedded user JWT's `aud`\n // as the target account NAME (the configured `auth_callout.account`). This is\n // how the authenticated user binds to that account; the workspace isolation is\n // then carried by the pub/sub permissions below.\n aud: input.audienceAccount,\n nats: natsBlock,\n };\n if (typeof input.expiresAtSeconds === \"number\") {\n claims.exp = input.expiresAtSeconds;\n }\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * Input to mint the authorization RESPONSE JWT the responder publishes back on the\n * request's reply subject (ADR-26 §3).\n * - `userPublicKey` — the request's `user_nkey`; the response `sub`.\n * - `serverId` — the request's `nats.server_id.id` (the server's public key); the\n * response `aud`.\n * - `accountSeed` — the callout account signing seed; signs the response and is\n * its `iss` (public key). NEVER logged.\n * - `userJwt` — the embedded signed user JWT (omit on a denial).\n * - `error` — a human-readable denial message (omit on success). When present the\n * server denies the connection.\n */\nexport interface MintAuthResponseInput {\n userPublicKey: string;\n serverId: string;\n accountSeed: string;\n userJwt?: string;\n error?: string;\n}\n\n/**\n * Mint the signed authorization-response JWT. On success it carries the embedded\n * user JWT (`nats.jwt`); on denial it carries `nats.error` and NO user JWT, which\n * makes nats-server refuse the connection. Signed by the callout account key (its\n * public key is `iss`); `sub` is the user_nkey, `aud` is the server id.\n */\nexport function mintAuthResponse(input: MintAuthResponseInput): string {\n const accountKey = nkeys.fromSeed(Buffer.from(input.accountSeed)) as unknown as NkeyPair;\n const accountPublicKey = accountKey.getPublicKey();\n const nowSeconds = Math.floor(Date.now() / 1000);\n\n const natsBlock: Record<string, unknown> = {\n type: AUTH_RESPONSE_CLAIM_TYPE,\n version: NATS_CLAIM_VERSION,\n };\n if (input.userJwt) {\n natsBlock.jwt = input.userJwt;\n }\n if (input.error) {\n natsBlock.error = input.error;\n }\n\n const claims: Record<string, unknown> = {\n jti: \"\",\n iat: nowSeconds,\n iss: accountPublicKey,\n // The response `aud` MUST be the SERVER public key in server-config mode\n // (nats-server validates \"Audience must be a server public key\"). The\n // authenticated user is placed into the configured `auth_callout.account` (the\n // SAME account the responder + the privileged control plane connect into), so\n // exact generation-fenced agent request/reply routes; workspace isolation is carried\n // entirely by the user JWT's pub/sub subject permissions (NOT by cross-account\n // placement, which server-config-mode nats does not support — nats-io#4335).\n aud: input.serverId,\n sub: input.userPublicKey,\n nats: natsBlock,\n };\n return encodeJwt(claims, accountKey);\n}\n\n/**\n * The fields the responder needs out of the authorization REQUEST JWT (ADR-26 §2).\n * The request is itself a NATS JWT (`header.payload.signature`) the server signs;\n * we only DECODE it (the server proves its own identity by the connection, and the\n * embedded `auth_token` is independently HMAC-verified), so we read the payload\n * without re-verifying the server signature.\n */\nexport interface DecodedAuthRequest {\n /** The public user nkey the response user JWT MUST be `sub`-scoped to. */\n userNkey: string;\n /** The server's public id — the response `aud`. */\n serverId: string;\n /** The connect `auth_token` the client presented (our `oge_` bearer), if any. */\n authToken: string | undefined;\n /** The connect username, if any (unused today; present for completeness). */\n user: string | undefined;\n /** Client-reported process identity. OpenGeni agents use a strict\n * `opengeni-agent/connection/<uuid>` shape; auth-callout rejects anything\n * else before granting machine subjects. */\n name: string | undefined;\n}\n\n/**\n * Decode the authorization-request JWT payload (the middle base64url segment). The\n * request shape (ADR-26 §2): `nats.user_nkey`, `nats.server_id.id`, and the\n * presented connect options under `nats.connect_opts` (`auth_token` / `user`).\n * Returns null on a malformed token so the caller can deny cleanly.\n */\nexport function decodeAuthRequest(token: string): DecodedAuthRequest | null {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"));\n } catch {\n return null;\n }\n if (typeof payload !== \"object\" || payload === null) {\n return null;\n }\n const nats = (payload as { nats?: unknown }).nats;\n if (typeof nats !== \"object\" || nats === null) {\n return null;\n }\n const natsObj = nats as {\n user_nkey?: unknown;\n server_id?: { id?: unknown } | unknown;\n connect_opts?: { auth_token?: unknown; user?: unknown } | unknown;\n };\n const userNkey = typeof natsObj.user_nkey === \"string\" ? natsObj.user_nkey : null;\n if (!userNkey) {\n return null;\n }\n const serverIdRaw =\n typeof natsObj.server_id === \"object\" && natsObj.server_id !== null\n ? (natsObj.server_id as { id?: unknown }).id\n : undefined;\n const serverId = typeof serverIdRaw === \"string\" ? serverIdRaw : \"\";\n const connectOpts =\n typeof natsObj.connect_opts === \"object\" && natsObj.connect_opts !== null\n ? (natsObj.connect_opts as { auth_token?: unknown; user?: unknown; name?: unknown })\n : {};\n const authToken = typeof connectOpts.auth_token === \"string\" ? connectOpts.auth_token : undefined;\n const user = typeof connectOpts.user === \"string\" ? connectOpts.user : undefined;\n const name = typeof connectOpts.name === \"string\" ? connectOpts.name : undefined;\n return { userNkey, serverId, authToken, user, name };\n}\n\nconst AGENT_CONNECTION_NAME_PREFIX = \"opengeni-agent/connection/\";\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Parse the exact process instance carried by the NATS CONNECT name. The value\n * is authority material used in subjects, so arbitrary tokens/dots are never\n * accepted. */\nexport function parseAgentConnectionName(name: string | undefined): string | null {\n if (!name?.startsWith(AGENT_CONNECTION_NAME_PREFIX)) return null;\n const instanceId = name.slice(AGENT_CONNECTION_NAME_PREFIX.length);\n return UUID_PATTERN.test(instanceId) ? instanceId.toLowerCase() : null;\n}\n\n/**\n * Build the exact process-scoped permission set for an authenticated agent. In\n * production agentId + connectionInstanceId are mandatory, restricting both\n * directions to that claimed daemon's RPC/event/hello/op subtree. It may publish\n * to `_INBOX.>` only to answer control-plane requests; it never needs to read\n * another connection's reply inbox.\n * The workspace-only fallback exists solely for legacy isolated callers/tests.\n *\n * THE isolation assertion (§17): with workspace A, agent B, instance C, the\n * production allow lists name only `agent.A.B.connection.C.>` and `_INBOX.>`.\n * NATS rejects every other workspace, agent, or process generation.\n */\nexport function workspaceAgentPermissions(\n workspaceId: string,\n agentId?: string,\n connectionInstanceId?: string,\n): NatsPermissions {\n const agentScope =\n agentId && connectionInstanceId\n ? `agent.${workspaceId}.${agentId}.connection.${connectionInstanceId}.>`\n : `agent.${workspaceId}.>`;\n // The reply-inbox subtree must be reachable for request/reply (the control plane\n // requests on the exact process RPC subject with a reply inbox; the agent responds there).\n const inboxScope = \"_INBOX.>\";\n return {\n pub: { allow: [agentScope, inboxScope] },\n sub: { allow: [agentScope] },\n };\n}\n"],"mappings":"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","names":["boundSessionEventPayload","sessionEventPayloadTruncation","connect","nkeys","boundSessionEventPayload","sessionEventPayloadTruncation"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/events",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15-canary.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@opengeni/contracts": "^2.
|
|
35
|
-
"@opengeni/db": "^3.
|
|
34
|
+
"@opengeni/contracts": "^2.11.1-canary.0",
|
|
35
|
+
"@opengeni/db": "^3.8.2-canary.0",
|
|
36
36
|
"nats": "^2.29.3"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/coalesce.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
boundSessionEventPayload,
|
|
3
|
+
sessionEventPayloadTruncation,
|
|
4
|
+
type SessionEvent,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
2
6
|
|
|
3
7
|
const COALESCIBLE_DELTA_TYPES = new Set([
|
|
4
8
|
"agent.message.delta",
|
|
@@ -10,6 +14,12 @@ const COALESCIBLE_DELTA_TYPES = new Set([
|
|
|
10
14
|
export const SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES = 48 * 1024;
|
|
11
15
|
const encoder = new TextEncoder();
|
|
12
16
|
|
|
17
|
+
export type CoalescedSessionEventPage = {
|
|
18
|
+
events: SessionEvent[];
|
|
19
|
+
/** Durable raw sequence covered by each returned synthetic event sequence. */
|
|
20
|
+
coveredThroughBySequence: ReadonlyMap<number, number>;
|
|
21
|
+
};
|
|
22
|
+
|
|
13
23
|
type DeltaRun = {
|
|
14
24
|
first: SessionEvent;
|
|
15
25
|
lastSequence: number;
|
|
@@ -21,7 +31,14 @@ type DeltaRun = {
|
|
|
21
31
|
};
|
|
22
32
|
|
|
23
33
|
export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent[] {
|
|
34
|
+
return coalesceSessionEventDeltasWithCoverage(events).events;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function coalesceSessionEventDeltasWithCoverage(
|
|
38
|
+
events: SessionEvent[],
|
|
39
|
+
): CoalescedSessionEventPage {
|
|
24
40
|
const coalesced: SessionEvent[] = [];
|
|
41
|
+
const coveredThroughBySequence = new Map<number, number>();
|
|
25
42
|
let run: DeltaRun | null = null;
|
|
26
43
|
|
|
27
44
|
const flush = () => {
|
|
@@ -45,10 +62,12 @@ export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent
|
|
|
45
62
|
};
|
|
46
63
|
coalesced.push({
|
|
47
64
|
...run.first,
|
|
65
|
+
coveredThrough: run.lastSequence,
|
|
48
66
|
payload: boundSessionEventPayload(payload, {
|
|
49
67
|
surface: "http_projection",
|
|
50
68
|
}),
|
|
51
69
|
});
|
|
70
|
+
coveredThroughBySequence.set(run.first.sequence, run.lastSequence);
|
|
52
71
|
run = null;
|
|
53
72
|
};
|
|
54
73
|
|
|
@@ -56,6 +75,7 @@ export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent
|
|
|
56
75
|
if (!isCoalescibleDelta(event)) {
|
|
57
76
|
flush();
|
|
58
77
|
coalesced.push(event);
|
|
78
|
+
coveredThroughBySequence.set(event.sequence, event.sequence);
|
|
59
79
|
continue;
|
|
60
80
|
}
|
|
61
81
|
|
|
@@ -100,11 +120,13 @@ export function coalesceSessionEventDeltas(events: SessionEvent[]): SessionEvent
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
flush();
|
|
103
|
-
return coalesced;
|
|
123
|
+
return { events: coalesced, coveredThroughBySequence };
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
function isCoalescibleDelta(event: SessionEvent): boolean {
|
|
107
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
COALESCIBLE_DELTA_TYPES.has(event.type) && sessionEventPayloadTruncation(event.payload) === null
|
|
129
|
+
);
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
function sameDeltaRun(
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,12 @@ const silentLogger: Required<EventLogger> = {
|
|
|
45
45
|
warn: () => {},
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
SESSION_EVENT_COALESCED_TEXT_TARGET_BYTES,
|
|
50
|
+
coalesceSessionEventDeltas,
|
|
51
|
+
coalesceSessionEventDeltasWithCoverage,
|
|
52
|
+
type CoalescedSessionEventPage,
|
|
53
|
+
} from "./coalesce";
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
56
|
* Reconnect + keepalive defaults applied to EVERY long-lived NATS connection
|
|
@@ -889,9 +894,14 @@ function subscribeAgentEvents(
|
|
|
889
894
|
};
|
|
890
895
|
}
|
|
891
896
|
|
|
892
|
-
export function formatSse<T extends { sequence: number; type: string }>(
|
|
897
|
+
export function formatSse<T extends { sequence: number; type: string }>(
|
|
898
|
+
event: T,
|
|
899
|
+
idSequence = event.sequence,
|
|
900
|
+
): string {
|
|
901
|
+
const trustedId =
|
|
902
|
+
Number.isSafeInteger(idSequence) && idSequence >= event.sequence ? idSequence : event.sequence;
|
|
893
903
|
return [
|
|
894
|
-
`id: ${
|
|
904
|
+
`id: ${trustedId}`,
|
|
895
905
|
`event: ${event.type}`,
|
|
896
906
|
`data: ${JSON.stringify(event)}`,
|
|
897
907
|
"",
|
|
@@ -929,9 +939,12 @@ export function formatWorkspaceControlEventSse(event: WorkspaceControlEvent): st
|
|
|
929
939
|
}
|
|
930
940
|
|
|
931
941
|
/** Defensively bounds historical rows before they become one SSE frame. */
|
|
932
|
-
export function formatSessionEventSse(
|
|
942
|
+
export function formatSessionEventSse(
|
|
943
|
+
event: SessionEvent,
|
|
944
|
+
coveredThrough = event.sequence,
|
|
945
|
+
): string {
|
|
933
946
|
const bounded = boundSessionEventForSurface(event, "sse_legacy_guard");
|
|
934
|
-
const formatted = formatSse(bounded);
|
|
947
|
+
const formatted = formatSse(bounded, coveredThrough);
|
|
935
948
|
if (new TextEncoder().encode(formatted).byteLength > SESSION_EVENT_SSE_FRAME_MAX_BYTES) {
|
|
936
949
|
// The payload normalizer targets 60 KiB, so this fallback is reachable only
|
|
937
950
|
// for a malformed legacy event with oversized non-payload envelope fields.
|
|
@@ -949,7 +962,7 @@ export function formatSessionEventSse(event: SessionEvent): string {
|
|
|
949
962
|
{ surface: "sse_legacy_guard", maxBytes: 4096 },
|
|
950
963
|
),
|
|
951
964
|
};
|
|
952
|
-
return formatSse(minimal);
|
|
965
|
+
return formatSse(minimal, coveredThrough);
|
|
953
966
|
}
|
|
954
967
|
return formatted;
|
|
955
968
|
}
|
|
@@ -1006,6 +1019,8 @@ export function boundSessionEventHttpPage(
|
|
|
1006
1019
|
maxBytes?: number;
|
|
1007
1020
|
/** Exact mode is restricted to already-canonical forensic REST rows. */
|
|
1008
1021
|
eventProjection?: "bounded" | "exact";
|
|
1022
|
+
/** Out-of-band raw coverage for events synthesized by trusted coalescing. */
|
|
1023
|
+
coveredThroughBySequence?: ReadonlyMap<number, number>;
|
|
1009
1024
|
},
|
|
1010
1025
|
): {
|
|
1011
1026
|
events: SessionEvent[];
|
|
@@ -1043,7 +1058,10 @@ export function boundSessionEventHttpPage(
|
|
|
1043
1058
|
edge === undefined
|
|
1044
1059
|
? null
|
|
1045
1060
|
: options.direction === "after"
|
|
1046
|
-
?
|
|
1061
|
+
? Math.max(
|
|
1062
|
+
edge.sequence,
|
|
1063
|
+
options.coveredThroughBySequence?.get(edge.sequence) ?? edge.sequence,
|
|
1064
|
+
)
|
|
1047
1065
|
: edge.sequence,
|
|
1048
1066
|
bytes,
|
|
1049
1067
|
};
|
|
@@ -1086,14 +1104,11 @@ export function boundWorkspaceControlHttpPage(
|
|
|
1086
1104
|
|
|
1087
1105
|
/** Raw durable cursor covered by a possibly coalesced compact event. */
|
|
1088
1106
|
export function sessionEventResumeSequence(event: SessionEvent): number {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
event.sequence,
|
|
1095
|
-
Number.isFinite(coalescedUntil) ? Math.floor(coalescedUntil) : event.sequence,
|
|
1096
|
-
);
|
|
1107
|
+
return typeof event.coveredThrough === "number" &&
|
|
1108
|
+
Number.isSafeInteger(event.coveredThrough) &&
|
|
1109
|
+
event.coveredThrough >= event.sequence
|
|
1110
|
+
? event.coveredThrough
|
|
1111
|
+
: event.sequence;
|
|
1097
1112
|
}
|
|
1098
1113
|
|
|
1099
1114
|
function boundSessionEventForSurface(
|