@superblocksteam/gateway 2.0.155-next.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/LICENSE.txt +87 -0
- package/README.md +170 -0
- package/dist/agents/resolve-orchestrator-url.d.ts +22 -0
- package/dist/agents/resolve-orchestrator-url.js +38 -0
- package/dist/agents/resolve-orchestrator-url.js.map +1 -0
- package/dist/capabilities/import-prompt.d.ts +11 -0
- package/dist/capabilities/import-prompt.js +96 -0
- package/dist/capabilities/import-prompt.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +73 -0
- package/dist/capabilities/lifecycle.js +2531 -0
- package/dist/capabilities/lifecycle.js.map +1 -0
- package/dist/capabilities/persisted-progress.d.ts +46 -0
- package/dist/capabilities/persisted-progress.js +246 -0
- package/dist/capabilities/persisted-progress.js.map +1 -0
- package/dist/capabilities/types.d.ts +446 -0
- package/dist/capabilities/types.js +47 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capture/browser-contract.d.ts +36 -0
- package/dist/capture/browser-contract.js +24 -0
- package/dist/capture/browser-contract.js.map +1 -0
- package/dist/capture/browser-instructions.d.ts +9 -0
- package/dist/capture/browser-instructions.js +17 -0
- package/dist/capture/browser-instructions.js.map +1 -0
- package/dist/capture/capture-library-probe.d.ts +55 -0
- package/dist/capture/capture-library-probe.js +177 -0
- package/dist/capture/capture-library-probe.js.map +1 -0
- package/dist/capture/capture-library.d.ts +52 -0
- package/dist/capture/capture-library.js +249 -0
- package/dist/capture/capture-library.js.map +1 -0
- package/dist/capture/classify-library-paint.d.ts +21 -0
- package/dist/capture/classify-library-paint.js +13 -0
- package/dist/capture/classify-library-paint.js.map +1 -0
- package/dist/capture/host-mcp-scan.d.ts +16 -0
- package/dist/capture/host-mcp-scan.js +64 -0
- package/dist/capture/host-mcp-scan.js.map +1 -0
- package/dist/capture/library-parent-script.d.ts +34 -0
- package/dist/capture/library-parent-script.js +82 -0
- package/dist/capture/library-parent-script.js.map +1 -0
- package/dist/capture/mode.d.ts +22 -0
- package/dist/capture/mode.js +45 -0
- package/dist/capture/mode.js.map +1 -0
- package/dist/config.d.ts +141 -0
- package/dist/config.js +109 -0
- package/dist/config.js.map +1 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +9 -0
- package/dist/debug.js.map +1 -0
- package/dist/deps.d.ts +36 -0
- package/dist/deps.js +2 -0
- package/dist/deps.js.map +1 -0
- package/dist/events/cursor.d.ts +43 -0
- package/dist/events/cursor.js +78 -0
- package/dist/events/cursor.js.map +1 -0
- package/dist/events/memory-event-store.d.ts +34 -0
- package/dist/events/memory-event-store.js +110 -0
- package/dist/events/memory-event-store.js.map +1 -0
- package/dist/events/merge.d.ts +23 -0
- package/dist/events/merge.js +97 -0
- package/dist/events/merge.js.map +1 -0
- package/dist/events/normalized-collector.d.ts +62 -0
- package/dist/events/normalized-collector.js +156 -0
- package/dist/events/normalized-collector.js.map +1 -0
- package/dist/events/schema.d.ts +9 -0
- package/dist/events/schema.js +93 -0
- package/dist/events/schema.js.map +1 -0
- package/dist/events/snapshot.d.ts +32 -0
- package/dist/events/snapshot.js +57 -0
- package/dist/events/snapshot.js.map +1 -0
- package/dist/events/stream-key.d.ts +2 -0
- package/dist/events/stream-key.js +31 -0
- package/dist/events/stream-key.js.map +1 -0
- package/dist/events/types.d.ts +179 -0
- package/dist/events/types.js +66 -0
- package/dist/events/types.js.map +1 -0
- package/dist/health/readiness.d.ts +99 -0
- package/dist/health/readiness.js +202 -0
- package/dist/health/readiness.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/filters.d.ts +8 -0
- package/dist/integrations/filters.js +40 -0
- package/dist/integrations/filters.js.map +1 -0
- package/dist/integrations/map.d.ts +26 -0
- package/dist/integrations/map.js +43 -0
- package/dist/integrations/map.js.map +1 -0
- package/dist/integrations/profile-key.d.ts +19 -0
- package/dist/integrations/profile-key.js +35 -0
- package/dist/integrations/profile-key.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -0
- package/dist/orchestrator/client.d.ts +62 -0
- package/dist/orchestrator/client.js +160 -0
- package/dist/orchestrator/client.js.map +1 -0
- package/dist/playwright/ensure-chromium.d.ts +26 -0
- package/dist/playwright/ensure-chromium.js +60 -0
- package/dist/playwright/ensure-chromium.js.map +1 -0
- package/dist/preview/capture-screenshot.d.ts +52 -0
- package/dist/preview/capture-screenshot.js +158 -0
- package/dist/preview/capture-screenshot.js.map +1 -0
- package/dist/preview/viewer-url.d.ts +82 -0
- package/dist/preview/viewer-url.js +128 -0
- package/dist/preview/viewer-url.js.map +1 -0
- package/dist/process/fault-barrier.d.ts +55 -0
- package/dist/process/fault-barrier.js +113 -0
- package/dist/process/fault-barrier.js.map +1 -0
- package/dist/resume/memory-progress-store.d.ts +39 -0
- package/dist/resume/memory-progress-store.js +82 -0
- package/dist/resume/memory-progress-store.js.map +1 -0
- package/dist/resume/memory-recent-app-store.d.ts +14 -0
- package/dist/resume/memory-recent-app-store.js +27 -0
- package/dist/resume/memory-recent-app-store.js.map +1 -0
- package/dist/resume/memory-turn-store.d.ts +18 -0
- package/dist/resume/memory-turn-store.js +73 -0
- package/dist/resume/memory-turn-store.js.map +1 -0
- package/dist/resume/progress-key.d.ts +21 -0
- package/dist/resume/progress-key.js +58 -0
- package/dist/resume/progress-key.js.map +1 -0
- package/dist/resume/stores.d.ts +14 -0
- package/dist/resume/stores.js +18 -0
- package/dist/resume/stores.js.map +1 -0
- package/dist/resume/types.d.ts +124 -0
- package/dist/resume/types.js +13 -0
- package/dist/resume/types.js.map +1 -0
- package/dist/sabs/agent-facing-text.d.ts +33 -0
- package/dist/sabs/agent-facing-text.js +125 -0
- package/dist/sabs/agent-facing-text.js.map +1 -0
- package/dist/sabs/browser-context.d.ts +53 -0
- package/dist/sabs/browser-context.js +91 -0
- package/dist/sabs/browser-context.js.map +1 -0
- package/dist/sabs/editor-client-methods.d.ts +147 -0
- package/dist/sabs/editor-client-methods.js +715 -0
- package/dist/sabs/editor-client-methods.js.map +1 -0
- package/dist/sabs/recent-tool-runs.d.ts +14 -0
- package/dist/sabs/recent-tool-runs.js +61 -0
- package/dist/sabs/recent-tool-runs.js.map +1 -0
- package/dist/sabs/session-peer.d.ts +351 -0
- package/dist/sabs/session-peer.js +2 -0
- package/dist/sabs/session-peer.js.map +1 -0
- package/dist/sabs/turn-collector.d.ts +152 -0
- package/dist/sabs/turn-collector.js +573 -0
- package/dist/sabs/turn-collector.js.map +1 -0
- package/dist/sabs/websocket-session-peer.d.ts +514 -0
- package/dist/sabs/websocket-session-peer.js +2487 -0
- package/dist/sabs/websocket-session-peer.js.map +1 -0
- package/dist/security/constant-time.d.ts +9 -0
- package/dist/security/constant-time.js +13 -0
- package/dist/security/constant-time.js.map +1 -0
- package/dist/server/client.d.ts +371 -0
- package/dist/server/client.js +572 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/identity-reads.d.ts +76 -0
- package/dist/server/identity-reads.js +209 -0
- package/dist/server/identity-reads.js.map +1 -0
- package/dist/start.d.ts +14 -0
- package/dist/start.js +75 -0
- package/dist/start.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +32 -0
- package/dist/telemetry/metrics.js +57 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/transports/mcp/admin-tools.d.ts +47 -0
- package/dist/transports/mcp/admin-tools.js +164 -0
- package/dist/transports/mcp/admin-tools.js.map +1 -0
- package/dist/transports/mcp/app-status-html.d.ts +15 -0
- package/dist/transports/mcp/app-status-html.js +563 -0
- package/dist/transports/mcp/app-status-html.js.map +1 -0
- package/dist/transports/mcp/decision-elicitation.d.ts +74 -0
- package/dist/transports/mcp/decision-elicitation.js +233 -0
- package/dist/transports/mcp/decision-elicitation.js.map +1 -0
- package/dist/transports/mcp/dispatch-probe.d.ts +24 -0
- package/dist/transports/mcp/dispatch-probe.js +48 -0
- package/dist/transports/mcp/dispatch-probe.js.map +1 -0
- package/dist/transports/mcp/format-tool-content.d.ts +25 -0
- package/dist/transports/mcp/format-tool-content.js +48 -0
- package/dist/transports/mcp/format-tool-content.js.map +1 -0
- package/dist/transports/mcp/mount.d.ts +9 -0
- package/dist/transports/mcp/mount.js +469 -0
- package/dist/transports/mcp/mount.js.map +1 -0
- package/dist/transports/mcp/progress-notifier.d.ts +39 -0
- package/dist/transports/mcp/progress-notifier.js +52 -0
- package/dist/transports/mcp/progress-notifier.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { advanceEventCursor, cursorPosition, } from "./cursor.js";
|
|
2
|
+
import { PRODUCERS_IN_ORDER, sortEvents } from "./merge.js";
|
|
3
|
+
import { EVENT_SNAPSHOT_TAIL } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Assembles a snapshot from a store's three reads.
|
|
6
|
+
*
|
|
7
|
+
* Shared rather than written twice so the memory and Redis stores cannot drift
|
|
8
|
+
* into disagreeing about what resuming means — the same reason both build
|
|
9
|
+
* their keys from one builder. Each store supplies only the reads; the cursor
|
|
10
|
+
* arithmetic and the staleness rule live here.
|
|
11
|
+
*/
|
|
12
|
+
export async function buildSnapshot(input, reads) {
|
|
13
|
+
const collected = [];
|
|
14
|
+
const tail = [];
|
|
15
|
+
const lost = [];
|
|
16
|
+
for (const producer of PRODUCERS_IN_ORDER) {
|
|
17
|
+
const resumedAfterSequence = cursorPosition(input.cursor, producer);
|
|
18
|
+
const afterEvents = await reads.readAfter({
|
|
19
|
+
afterSequence: resumedAfterSequence,
|
|
20
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
21
|
+
producer,
|
|
22
|
+
});
|
|
23
|
+
collected.push(...afterEvents);
|
|
24
|
+
tail.push(...(await reads.readTail({ count: EVENT_SNAPSHOT_TAIL, producer })));
|
|
25
|
+
const firstRetainedSequence = await reads.firstRetainedSequence(producer);
|
|
26
|
+
// readAfter and firstRetainedSequence are two separate reads, not one
|
|
27
|
+
// atomic one, so retention can advance between them: firstRetainedSequence
|
|
28
|
+
// can then report a boundary past events readAfter had already captured
|
|
29
|
+
// moments earlier. A delivered sequence below that boundary proves the
|
|
30
|
+
// read beat the eviction, so what looks like a gap here never actually
|
|
31
|
+
// reached the caller as one.
|
|
32
|
+
const minDelivered = afterEvents.length > 0
|
|
33
|
+
? Math.min(...afterEvents.map((event) => event.sequence))
|
|
34
|
+
: undefined;
|
|
35
|
+
const provenByDelivery = firstRetainedSequence !== undefined &&
|
|
36
|
+
minDelivered !== undefined &&
|
|
37
|
+
minDelivered < firstRetainedSequence;
|
|
38
|
+
// A gap only exists when the stream starts *past* the next sequence the
|
|
39
|
+
// cursor expects. An empty stream is not a gap: nothing was produced yet.
|
|
40
|
+
if (firstRetainedSequence !== undefined &&
|
|
41
|
+
firstRetainedSequence > resumedAfterSequence + 1 &&
|
|
42
|
+
!provenByDelivery) {
|
|
43
|
+
lost.push({ firstRetainedSequence, producer, resumedAfterSequence });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const events = sortEvents(collected);
|
|
47
|
+
const limited = input.limit === undefined ? events : events.slice(0, input.limit);
|
|
48
|
+
return {
|
|
49
|
+
// Advanced past what is actually being handed over, never past what was
|
|
50
|
+
// read and then cut by the limit — otherwise the remainder is skipped.
|
|
51
|
+
cursor: advanceEventCursor(input.cursor, limited),
|
|
52
|
+
events: limited,
|
|
53
|
+
lost,
|
|
54
|
+
tail: sortEvents(tail),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/events/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,cAAc,GAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,mBAAmB,EAAuB,MAAM,YAAY,CAAC;AAqBtE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAA+C,EAC/C,KAAoB;IAEpB,MAAM,SAAS,GAAoB,EAAE,CAAC;IACtC,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAqB,EAAE,CAAC;IAElC,KAAK,MAAM,QAAQ,IAAI,kBAAkB,EAAE,CAAC;QAC1C,MAAM,oBAAoB,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACpE,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC;YACxC,aAAa,EAAE,oBAAoB;YACnC,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;YAC5D,QAAQ;SACT,CAAC,CAAC;QACH,SAAS,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,CACP,GAAG,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,CAAC,CAAC,CACpE,CAAC;QAEF,MAAM,qBAAqB,GAAG,MAAM,KAAK,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC;QAC1E,sEAAsE;QACtE,2EAA2E;QAC3E,wEAAwE;QACxE,uEAAuE;QACvE,uEAAuE;QACvE,6BAA6B;QAC7B,MAAM,YAAY,GAChB,WAAW,CAAC,MAAM,GAAG,CAAC;YACpB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACzD,CAAC,CAAC,SAAS,CAAC;QAChB,MAAM,gBAAgB,GACpB,qBAAqB,KAAK,SAAS;YACnC,YAAY,KAAK,SAAS;YAC1B,YAAY,GAAG,qBAAqB,CAAC;QAEvC,wEAAwE;QACxE,0EAA0E;QAC1E,IACE,qBAAqB,KAAK,SAAS;YACnC,qBAAqB,GAAG,oBAAoB,GAAG,CAAC;YAChD,CAAC,gBAAgB,EACjB,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,EAAE,qBAAqB,EAAE,QAAQ,EAAE,oBAAoB,EAAE,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,OAAO,GACX,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACpE,OAAO;QACL,wEAAwE;QACxE,uEAAuE;QACvE,MAAM,EAAE,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC;QACjD,MAAM,EAAE,OAAO;QACf,IAAI;QACJ,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC;KACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the stream address shared by every event store, so the two
|
|
4
|
+
* implementations cannot drift into addressing streams differently.
|
|
5
|
+
*
|
|
6
|
+
* Tenant identifiers (organization, application, live edit) are length-
|
|
7
|
+
* prefixed and SHA-256-digested before they reach Redis. Length-prefixing
|
|
8
|
+
* keeps `a:b`+`c` and `a`+`b:c` from colliding; digesting keeps the
|
|
9
|
+
* identifiers themselves out of key listings — ProgressStore already does
|
|
10
|
+
* the same for caller/org/app. The producer stays in the clear: it is a
|
|
11
|
+
* closed enum (`dev_server` | `sabs`), cannot identify a tenant, and naming
|
|
12
|
+
* the stream at a glance is worth more than the nothing it discloses.
|
|
13
|
+
*
|
|
14
|
+
* v0 has not shipped this key format, so there is no dual-read. Existing
|
|
15
|
+
* local Redis event keys from earlier builds are orphaned after deploy.
|
|
16
|
+
*/
|
|
17
|
+
function digest(parts) {
|
|
18
|
+
const hash = createHash("sha256");
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
hash.update(`${part.length}:${part}`);
|
|
21
|
+
}
|
|
22
|
+
return hash.digest("hex");
|
|
23
|
+
}
|
|
24
|
+
export function streamSuffix(scope, producer) {
|
|
25
|
+
return `${digest([
|
|
26
|
+
scope.organizationId,
|
|
27
|
+
scope.applicationId,
|
|
28
|
+
scope.liveEditId,
|
|
29
|
+
])}:${producer}`;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=stream-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-key.js","sourceRoot":"","sources":["../../src/events/stream-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC;;;;;;;;;;;;;;GAcG;AACH,SAAS,MAAM,CAAC,KAAwB;IACtC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,KAAiB,EACjB,QAAuB;IAEvB,OAAO,GAAG,MAAM,CAAC;QACf,KAAK,CAAC,cAAc;QACpB,KAAK,CAAC,aAAa;QACnB,KAAK,CAAC,UAAU;KACjB,CAAC,IAAI,QAAQ,EAAE,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { EventCursor } from "./cursor.js";
|
|
2
|
+
/**
|
|
3
|
+
* The versioned event contract SABS pushes over the direct authenticated
|
|
4
|
+
* callback. This mirrors `sabs/internal/gateway/event.go`; the two are kept in
|
|
5
|
+
* step by hand because the channel is deliberately not proto, and the bounds
|
|
6
|
+
* below are the only thing standing between a producer bug and unbounded
|
|
7
|
+
* Redis growth.
|
|
8
|
+
*/
|
|
9
|
+
export declare const EVENT_CONTRACT_VERSION = 1;
|
|
10
|
+
export declare const MAX_IDENTIFIER_LENGTH = 128;
|
|
11
|
+
export declare const MAX_LABEL_LENGTH = 512;
|
|
12
|
+
export declare const MAX_TEXT_LENGTH = 8192;
|
|
13
|
+
/**
|
|
14
|
+
* Ceiling on an inbound sequence number. Every consumer of `sequence`
|
|
15
|
+
* (cursor comparisons, gap detection) does so with ordinary JS number
|
|
16
|
+
* arithmetic, which stops being exact past this point; a producer sequence
|
|
17
|
+
* that reached it would silently corrupt ordering rather than fail loudly.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_SEQUENCE: number;
|
|
20
|
+
/** Kinds of fact a live-edit session reports. */
|
|
21
|
+
export declare const EVENT_KINDS: readonly ["auth_required", "chat_message", "generation_state", "progress", "session"];
|
|
22
|
+
export type LiveEditEventKind = (typeof EVENT_KINDS)[number];
|
|
23
|
+
/**
|
|
24
|
+
* Which stream a sequence number belongs to. The SABS control plane and the
|
|
25
|
+
* dev server both push about the same live edit and neither can see the
|
|
26
|
+
* other's counter, so ordering is defined per producer and Gateway keeps the
|
|
27
|
+
* two streams apart.
|
|
28
|
+
*/
|
|
29
|
+
export declare const EVENT_PRODUCERS: readonly ["dev_server", "sabs"];
|
|
30
|
+
export type EventProducer = (typeof EVENT_PRODUCERS)[number];
|
|
31
|
+
export declare const GENERATION_STATES: readonly ["awaiting_user_input", "cancelled", "completed", "debugging", "generating", "idle", "summarizing"];
|
|
32
|
+
export declare const CHAT_ROLES: readonly ["assistant", "user"];
|
|
33
|
+
export declare const SESSION_STATUSES: readonly ["created", "terminated"];
|
|
34
|
+
export type LiveEditEventPayload = {
|
|
35
|
+
kind: "auth_required";
|
|
36
|
+
reason?: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "chat_message";
|
|
39
|
+
messageId?: string;
|
|
40
|
+
messageType?: string;
|
|
41
|
+
role: (typeof CHAT_ROLES)[number];
|
|
42
|
+
text: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "generation_state";
|
|
45
|
+
hasError?: boolean;
|
|
46
|
+
hasSuggestions?: boolean;
|
|
47
|
+
state: (typeof GENERATION_STATES)[number];
|
|
48
|
+
} | {
|
|
49
|
+
kind: "progress";
|
|
50
|
+
label: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "session";
|
|
53
|
+
status: (typeof SESSION_STATUSES)[number];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* One accepted event. `occurredAtMs` replaces the wire's ISO string so
|
|
57
|
+
* ordering never depends on re-parsing, and `payload` is narrowed by `kind`.
|
|
58
|
+
*/
|
|
59
|
+
export type LiveEditEvent = {
|
|
60
|
+
applicationId: string;
|
|
61
|
+
eventId: string;
|
|
62
|
+
gatewayTurnId?: string;
|
|
63
|
+
kind: LiveEditEventKind;
|
|
64
|
+
liveEditId: string;
|
|
65
|
+
occurredAtMs: number;
|
|
66
|
+
organizationId: string;
|
|
67
|
+
payload: LiveEditEventPayload;
|
|
68
|
+
producer: EventProducer;
|
|
69
|
+
sequence: number;
|
|
70
|
+
version: typeof EVENT_CONTRACT_VERSION;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Why an event was refused. These values reach a metric label, so the set is
|
|
74
|
+
* closed and carries nothing derived from the event's content.
|
|
75
|
+
*/
|
|
76
|
+
export type EventRejectionReason = "malformed_event" | "unsupported_version";
|
|
77
|
+
export type ParseResult = {
|
|
78
|
+
ok: true;
|
|
79
|
+
event: LiveEditEvent;
|
|
80
|
+
} | {
|
|
81
|
+
ok: false;
|
|
82
|
+
reason: EventRejectionReason;
|
|
83
|
+
};
|
|
84
|
+
/** Outcome of persisting one event. */
|
|
85
|
+
export type AppendOutcome = "accepted" | "duplicate";
|
|
86
|
+
/**
|
|
87
|
+
* The whole tenant path to one live-edit stream.
|
|
88
|
+
*
|
|
89
|
+
* A live-edit id alone would be enough to address a stream, and that is
|
|
90
|
+
* exactly the problem: it makes the store's isolation depend entirely on
|
|
91
|
+
* every caller having validated ownership first. Carrying the tenant in the
|
|
92
|
+
* key means a read cannot reach another tenant's history even if something
|
|
93
|
+
* upstream let the wrong id through.
|
|
94
|
+
*/
|
|
95
|
+
export type EventScope = {
|
|
96
|
+
applicationId: string;
|
|
97
|
+
liveEditId: string;
|
|
98
|
+
organizationId: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* A producer whose retention boundary overtook a caller's cursor.
|
|
102
|
+
*
|
|
103
|
+
* Reported rather than hidden: the caller asked to continue from a position
|
|
104
|
+
* that no longer exists, and answering with only the events that survived
|
|
105
|
+
* would present a stream with a hole in it as a complete one. Naming the
|
|
106
|
+
* producer and both boundaries lets a caller say exactly what it lost.
|
|
107
|
+
*/
|
|
108
|
+
export type LostEventRange = {
|
|
109
|
+
/** Lowest sequence this producer's stream still holds. */
|
|
110
|
+
firstRetainedSequence: number;
|
|
111
|
+
producer: EventProducer;
|
|
112
|
+
/** Last sequence the caller's cursor had consumed. */
|
|
113
|
+
resumedAfterSequence: number;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* How many events at the end of each producer's stream a snapshot carries.
|
|
117
|
+
*
|
|
118
|
+
* Current state has to be read from the stream's end rather than from the
|
|
119
|
+
* window after the cursor: a caller that already consumed the terminal event
|
|
120
|
+
* would otherwise be told nothing is known and would poll a build that ended.
|
|
121
|
+
* A tail bounds that read so it does not grow with the length of the session.
|
|
122
|
+
*/
|
|
123
|
+
export declare const EVENT_SNAPSHOT_TAIL = 64;
|
|
124
|
+
/**
|
|
125
|
+
* What one caller sees when it resumes reading a live edit.
|
|
126
|
+
*
|
|
127
|
+
* `events` is what is new for this caller and `tail` is where the session
|
|
128
|
+
* currently stands; the two overlap freely and a reader is expected to take
|
|
129
|
+
* activity from the first and state from the second.
|
|
130
|
+
*/
|
|
131
|
+
export type EventSnapshot = {
|
|
132
|
+
/** Advanced past every event in {@link EventSnapshot.events}. */
|
|
133
|
+
cursor: EventCursor;
|
|
134
|
+
/** Strictly after the incoming cursor, ordered deterministically. */
|
|
135
|
+
events: LiveEditEvent[];
|
|
136
|
+
/** Empty when the cursor was still inside every stream's retention. */
|
|
137
|
+
lost: LostEventRange[];
|
|
138
|
+
/** Newest {@link EVENT_SNAPSHOT_TAIL} events per producer, cursor aside. */
|
|
139
|
+
tail: LiveEditEvent[];
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Durable, bounded record of what happened in a live-edit session.
|
|
143
|
+
*
|
|
144
|
+
* Retention is bounded twice over: a stream holds at most
|
|
145
|
+
* {@link MAX_EVENTS_PER_STREAM} events and expires after
|
|
146
|
+
* {@link EVENT_STREAM_TTL_MS} of silence. This is the source a resumed caller
|
|
147
|
+
* reads. Live turns hear Clark over the editor websocket.
|
|
148
|
+
*/
|
|
149
|
+
export type EventStore = {
|
|
150
|
+
/** The event carries its own scope, so an append cannot be misfiled. */
|
|
151
|
+
append(event: LiveEditEvent): Promise<AppendOutcome>;
|
|
152
|
+
/** Events for a live edit, ordered deterministically across producers. */
|
|
153
|
+
list(input: EventScope & {
|
|
154
|
+
afterSequence?: number;
|
|
155
|
+
limit?: number;
|
|
156
|
+
producer?: EventProducer;
|
|
157
|
+
}): Promise<LiveEditEvent[]>;
|
|
158
|
+
/**
|
|
159
|
+
* Everything one caller needs to continue from where it left off, read
|
|
160
|
+
* without consuming anything: two callers resuming the same live edit each
|
|
161
|
+
* get their own view of it.
|
|
162
|
+
*/
|
|
163
|
+
snapshot(input: EventScope & {
|
|
164
|
+
cursor?: EventCursor;
|
|
165
|
+
limit?: number;
|
|
166
|
+
}): Promise<EventSnapshot>;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Per-(live edit, producer) retention bound. A runaway producer overwrites its
|
|
170
|
+
* own oldest events rather than growing Redis without limit. The trade-off is
|
|
171
|
+
* explicit: an event evicted by count loses its de-duplication record, so a
|
|
172
|
+
* redelivery arriving after eviction is accepted again. Bounded memory is
|
|
173
|
+
* worth more than perfect de-duplication of an event nobody can still read.
|
|
174
|
+
*/
|
|
175
|
+
export declare const MAX_EVENTS_PER_STREAM = 500;
|
|
176
|
+
/** How long a silent stream is kept before Redis reclaims it. */
|
|
177
|
+
export declare const EVENT_STREAM_TTL_MS = 3600000;
|
|
178
|
+
/** Cap on one inbound batch, matching the SABS publish bound. */
|
|
179
|
+
export declare const MAX_EVENTS_PER_REQUEST = 64;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The versioned event contract SABS pushes over the direct authenticated
|
|
3
|
+
* callback. This mirrors `sabs/internal/gateway/event.go`; the two are kept in
|
|
4
|
+
* step by hand because the channel is deliberately not proto, and the bounds
|
|
5
|
+
* below are the only thing standing between a producer bug and unbounded
|
|
6
|
+
* Redis growth.
|
|
7
|
+
*/
|
|
8
|
+
export const EVENT_CONTRACT_VERSION = 1;
|
|
9
|
+
export const MAX_IDENTIFIER_LENGTH = 128;
|
|
10
|
+
export const MAX_LABEL_LENGTH = 512;
|
|
11
|
+
export const MAX_TEXT_LENGTH = 8192;
|
|
12
|
+
/**
|
|
13
|
+
* Ceiling on an inbound sequence number. Every consumer of `sequence`
|
|
14
|
+
* (cursor comparisons, gap detection) does so with ordinary JS number
|
|
15
|
+
* arithmetic, which stops being exact past this point; a producer sequence
|
|
16
|
+
* that reached it would silently corrupt ordering rather than fail loudly.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_SEQUENCE = Number.MAX_SAFE_INTEGER;
|
|
19
|
+
/** Kinds of fact a live-edit session reports. */
|
|
20
|
+
export const EVENT_KINDS = [
|
|
21
|
+
"auth_required",
|
|
22
|
+
"chat_message",
|
|
23
|
+
"generation_state",
|
|
24
|
+
"progress",
|
|
25
|
+
"session",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Which stream a sequence number belongs to. The SABS control plane and the
|
|
29
|
+
* dev server both push about the same live edit and neither can see the
|
|
30
|
+
* other's counter, so ordering is defined per producer and Gateway keeps the
|
|
31
|
+
* two streams apart.
|
|
32
|
+
*/
|
|
33
|
+
export const EVENT_PRODUCERS = ["dev_server", "sabs"];
|
|
34
|
+
export const GENERATION_STATES = [
|
|
35
|
+
"awaiting_user_input",
|
|
36
|
+
"cancelled",
|
|
37
|
+
"completed",
|
|
38
|
+
"debugging",
|
|
39
|
+
"generating",
|
|
40
|
+
"idle",
|
|
41
|
+
"summarizing",
|
|
42
|
+
];
|
|
43
|
+
export const CHAT_ROLES = ["assistant", "user"];
|
|
44
|
+
export const SESSION_STATUSES = ["created", "terminated"];
|
|
45
|
+
/**
|
|
46
|
+
* How many events at the end of each producer's stream a snapshot carries.
|
|
47
|
+
*
|
|
48
|
+
* Current state has to be read from the stream's end rather than from the
|
|
49
|
+
* window after the cursor: a caller that already consumed the terminal event
|
|
50
|
+
* would otherwise be told nothing is known and would poll a build that ended.
|
|
51
|
+
* A tail bounds that read so it does not grow with the length of the session.
|
|
52
|
+
*/
|
|
53
|
+
export const EVENT_SNAPSHOT_TAIL = 64;
|
|
54
|
+
/**
|
|
55
|
+
* Per-(live edit, producer) retention bound. A runaway producer overwrites its
|
|
56
|
+
* own oldest events rather than growing Redis without limit. The trade-off is
|
|
57
|
+
* explicit: an event evicted by count loses its de-duplication record, so a
|
|
58
|
+
* redelivery arriving after eviction is accepted again. Bounded memory is
|
|
59
|
+
* worth more than perfect de-duplication of an event nobody can still read.
|
|
60
|
+
*/
|
|
61
|
+
export const MAX_EVENTS_PER_STREAM = 500;
|
|
62
|
+
/** How long a silent stream is kept before Redis reclaims it. */
|
|
63
|
+
export const EVENT_STREAM_TTL_MS = 3_600_000;
|
|
64
|
+
/** Cap on one inbound batch, matching the SABS publish bound. */
|
|
65
|
+
export const MAX_EVENTS_PER_REQUEST = 64;
|
|
66
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/events/types.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AACpC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,gBAAgB,CAAC;AAEpD,iDAAiD;AACjD,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,eAAe;IACf,cAAc;IACd,kBAAkB;IAClB,UAAU;IACV,SAAS;CACD,CAAC;AAGX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,YAAY,EAAE,MAAM,CAAU,CAAC;AAG/D,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,qBAAqB;IACrB,WAAW;IACX,WAAW;IACX,WAAW;IACX,YAAY;IACZ,MAAM;IACN,aAAa;CACL,CAAC;AAEX,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,WAAW,EAAE,MAAM,CAAU,CAAC;AAEzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,YAAY,CAAU,CAAC;AAkFnE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAiDtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,iEAAiE;AACjE,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAE7C,iEAAiE;AACjE,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { GatewayFault } from "../process/fault-barrier.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the gateway's own request path is doing.
|
|
4
|
+
*
|
|
5
|
+
* `unmounted` is not a degenerate case: `createApp` builds the HTTP surface
|
|
6
|
+
* before the MCP transport is mounted onto it, and a gateway in that window has
|
|
7
|
+
* no way to serve a tool call. Reporting it as healthy is the ENG-5809 bug in
|
|
8
|
+
* miniature.
|
|
9
|
+
*/
|
|
10
|
+
export type DispatchState = "faulted" | "ok" | "unmounted" | "wedged";
|
|
11
|
+
export type UpstreamState = "reachable" | "unknown" | "unreachable";
|
|
12
|
+
export type ReadinessVerdict = {
|
|
13
|
+
dispatch: DispatchState;
|
|
14
|
+
/** Human-readable cause, present only when something is wrong. */
|
|
15
|
+
detail?: string;
|
|
16
|
+
/** Whether callers should be routed here. */
|
|
17
|
+
ready: boolean;
|
|
18
|
+
upstream: UpstreamState;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Proves the gateway can still take a request all the way through its own
|
|
22
|
+
* dispatch path. Resolving means it can; throwing or hanging means it cannot.
|
|
23
|
+
*/
|
|
24
|
+
export type DispatchProbe = () => Promise<void>;
|
|
25
|
+
export type UpstreamProbe = () => Promise<UpstreamState>;
|
|
26
|
+
export type GatewayReadinessOptions = {
|
|
27
|
+
cacheMs?: number;
|
|
28
|
+
dispatchDeadlineMs?: number;
|
|
29
|
+
/** Where the cause behind a bounded `detail` goes. */
|
|
30
|
+
log?: (message: string, cause: unknown) => void;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
/** The Superblocks origin this gateway fronts, named in the verdict. */
|
|
33
|
+
serverUrl: string;
|
|
34
|
+
upstreamProbe?: UpstreamProbe;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Probes an origin for reachability rather than for authorization.
|
|
38
|
+
*
|
|
39
|
+
* Any HTTP status is a pass, including 401: a server that refuses an
|
|
40
|
+
* unauthenticated probe has still proved it is answering, which is the only
|
|
41
|
+
* question a health check can ask without a caller's credential. Only a
|
|
42
|
+
* transport failure or a timeout means unreachable.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createUpstreamProbe(input: {
|
|
45
|
+
fetchImpl?: typeof fetch;
|
|
46
|
+
serverUrl: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}): UpstreamProbe;
|
|
49
|
+
/**
|
|
50
|
+
* The gateway's honest answer to "are you working".
|
|
51
|
+
*
|
|
52
|
+
* Two questions, deliberately kept apart, because during the ENG-5809 outage
|
|
53
|
+
* nothing on the wire told the difference:
|
|
54
|
+
*
|
|
55
|
+
* - {@link liveness} asks whether *this process* can still serve. It fails on a
|
|
56
|
+
* broken or wedged dispatch path and on a fault it decided not to survive.
|
|
57
|
+
* An unreachable platform never fails it, because restarting this gateway
|
|
58
|
+
* cannot fix the platform, and a supervisor acting on liveness would restart
|
|
59
|
+
* a working process every few seconds until the platform came back.
|
|
60
|
+
* - {@link readiness} asks whether tool calls should be routed here, so an
|
|
61
|
+
* unreachable platform does fail it: nothing can be served through it.
|
|
62
|
+
*
|
|
63
|
+
* Both report `dispatch` and `upstream` as separate fields either way, so a
|
|
64
|
+
* caller reads which side to act on instead of inferring it from a bare "ok".
|
|
65
|
+
*/
|
|
66
|
+
export declare class GatewayReadiness {
|
|
67
|
+
private cached;
|
|
68
|
+
private dispatchProbe;
|
|
69
|
+
private fatal;
|
|
70
|
+
private inFlight;
|
|
71
|
+
private readonly options;
|
|
72
|
+
constructor(options: GatewayReadinessOptions);
|
|
73
|
+
/**
|
|
74
|
+
* Hands over the real dispatch path, once there is one. Called by the MCP
|
|
75
|
+
* transport as it mounts; until then the gateway reports `unmounted`.
|
|
76
|
+
*/
|
|
77
|
+
useDispatchProbe(probe: DispatchProbe): void;
|
|
78
|
+
/**
|
|
79
|
+
* Records what the process-level barrier caught.
|
|
80
|
+
*
|
|
81
|
+
* Only a fatal fault changes the verdict. A rejection the barrier absorbed
|
|
82
|
+
* cost one caller their call; failing liveness for it would hand a working
|
|
83
|
+
* process to a restart loop, which is the outage the barrier prevents.
|
|
84
|
+
*/
|
|
85
|
+
recordFault(fault: GatewayFault): void;
|
|
86
|
+
liveness(): Promise<ReadinessVerdict>;
|
|
87
|
+
readiness(): Promise<ReadinessVerdict>;
|
|
88
|
+
private verdict;
|
|
89
|
+
/**
|
|
90
|
+
* One probe run per cache window, shared by however many pollers arrive
|
|
91
|
+
* while it is running. Eight concurrent agents polling health must not stack
|
|
92
|
+
* eight MCP servers onto a gateway that is already struggling.
|
|
93
|
+
*/
|
|
94
|
+
private probe;
|
|
95
|
+
private runProbe;
|
|
96
|
+
private probeDispatch;
|
|
97
|
+
private log;
|
|
98
|
+
private probeUpstream;
|
|
99
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { recordReadinessVerdict } from "../telemetry/metrics.js";
|
|
2
|
+
/** How long the dispatch path gets to answer before it counts as wedged. */
|
|
3
|
+
const DEFAULT_DISPATCH_DEADLINE_MS = 5_000;
|
|
4
|
+
/** How long the upstream origin gets to answer before it counts as down. */
|
|
5
|
+
const DEFAULT_UPSTREAM_DEADLINE_MS = 3_000;
|
|
6
|
+
/** How long one probe run answers for. Health endpoints get polled hard. */
|
|
7
|
+
const DEFAULT_CACHE_MS = 2_000;
|
|
8
|
+
async function withDeadline(work, deadlineMs, onDeadline) {
|
|
9
|
+
let timer;
|
|
10
|
+
try {
|
|
11
|
+
return await Promise.race([
|
|
12
|
+
work,
|
|
13
|
+
new Promise((resolve) => {
|
|
14
|
+
timer = setTimeout(() => resolve(onDeadline()), deadlineMs);
|
|
15
|
+
timer.unref?.();
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (timer) {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Probes an origin for reachability rather than for authorization.
|
|
27
|
+
*
|
|
28
|
+
* Any HTTP status is a pass, including 401: a server that refuses an
|
|
29
|
+
* unauthenticated probe has still proved it is answering, which is the only
|
|
30
|
+
* question a health check can ask without a caller's credential. Only a
|
|
31
|
+
* transport failure or a timeout means unreachable.
|
|
32
|
+
*/
|
|
33
|
+
export function createUpstreamProbe(input) {
|
|
34
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
35
|
+
return async () => {
|
|
36
|
+
try {
|
|
37
|
+
await fetchImpl(input.serverUrl, {
|
|
38
|
+
method: "GET",
|
|
39
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? DEFAULT_UPSTREAM_DEADLINE_MS),
|
|
40
|
+
});
|
|
41
|
+
return "reachable";
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return "unreachable";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The gateway's honest answer to "are you working".
|
|
50
|
+
*
|
|
51
|
+
* Two questions, deliberately kept apart, because during the ENG-5809 outage
|
|
52
|
+
* nothing on the wire told the difference:
|
|
53
|
+
*
|
|
54
|
+
* - {@link liveness} asks whether *this process* can still serve. It fails on a
|
|
55
|
+
* broken or wedged dispatch path and on a fault it decided not to survive.
|
|
56
|
+
* An unreachable platform never fails it, because restarting this gateway
|
|
57
|
+
* cannot fix the platform, and a supervisor acting on liveness would restart
|
|
58
|
+
* a working process every few seconds until the platform came back.
|
|
59
|
+
* - {@link readiness} asks whether tool calls should be routed here, so an
|
|
60
|
+
* unreachable platform does fail it: nothing can be served through it.
|
|
61
|
+
*
|
|
62
|
+
* Both report `dispatch` and `upstream` as separate fields either way, so a
|
|
63
|
+
* caller reads which side to act on instead of inferring it from a bare "ok".
|
|
64
|
+
*/
|
|
65
|
+
export class GatewayReadiness {
|
|
66
|
+
cached;
|
|
67
|
+
dispatchProbe;
|
|
68
|
+
fatal;
|
|
69
|
+
inFlight;
|
|
70
|
+
options;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.options = options;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Hands over the real dispatch path, once there is one. Called by the MCP
|
|
76
|
+
* transport as it mounts; until then the gateway reports `unmounted`.
|
|
77
|
+
*/
|
|
78
|
+
useDispatchProbe(probe) {
|
|
79
|
+
this.dispatchProbe = probe;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Records what the process-level barrier caught.
|
|
83
|
+
*
|
|
84
|
+
* Only a fatal fault changes the verdict. A rejection the barrier absorbed
|
|
85
|
+
* cost one caller their call; failing liveness for it would hand a working
|
|
86
|
+
* process to a restart loop, which is the outage the barrier prevents.
|
|
87
|
+
*/
|
|
88
|
+
recordFault(fault) {
|
|
89
|
+
if (fault.fatal) {
|
|
90
|
+
this.fatal = fault;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async liveness() {
|
|
94
|
+
return this.verdict("liveness");
|
|
95
|
+
}
|
|
96
|
+
async readiness() {
|
|
97
|
+
return this.verdict("readiness");
|
|
98
|
+
}
|
|
99
|
+
async verdict(probe) {
|
|
100
|
+
const probed = await this.probe();
|
|
101
|
+
const upstreamCounts = probe === "readiness";
|
|
102
|
+
const upstreamDown = upstreamCounts && probed.upstream === "unreachable";
|
|
103
|
+
const verdict = {
|
|
104
|
+
dispatch: probed.dispatch,
|
|
105
|
+
ready: probed.dispatch === "ok" && !upstreamDown,
|
|
106
|
+
upstream: probed.upstream,
|
|
107
|
+
...detailOf(probed, upstreamDown, this.options.serverUrl),
|
|
108
|
+
};
|
|
109
|
+
recordReadinessVerdict({
|
|
110
|
+
dispatch: verdict.dispatch,
|
|
111
|
+
probe,
|
|
112
|
+
ready: verdict.ready,
|
|
113
|
+
upstream: verdict.upstream,
|
|
114
|
+
});
|
|
115
|
+
return verdict;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One probe run per cache window, shared by however many pollers arrive
|
|
119
|
+
* while it is running. Eight concurrent agents polling health must not stack
|
|
120
|
+
* eight MCP servers onto a gateway that is already struggling.
|
|
121
|
+
*/
|
|
122
|
+
async probe() {
|
|
123
|
+
const now = (this.options.now ?? Date.now)();
|
|
124
|
+
const cacheMs = this.options.cacheMs ?? DEFAULT_CACHE_MS;
|
|
125
|
+
if (this.cached && now - this.cached.at < cacheMs) {
|
|
126
|
+
return this.cached;
|
|
127
|
+
}
|
|
128
|
+
this.inFlight ??= this.runProbe(now).finally(() => {
|
|
129
|
+
this.inFlight = undefined;
|
|
130
|
+
});
|
|
131
|
+
return this.inFlight;
|
|
132
|
+
}
|
|
133
|
+
async runProbe(at) {
|
|
134
|
+
const [dispatch, upstream] = await Promise.all([
|
|
135
|
+
this.probeDispatch(),
|
|
136
|
+
this.probeUpstream(),
|
|
137
|
+
]);
|
|
138
|
+
const probed = { at, upstream, ...dispatch };
|
|
139
|
+
this.cached = probed;
|
|
140
|
+
return probed;
|
|
141
|
+
}
|
|
142
|
+
async probeDispatch() {
|
|
143
|
+
// A fault the process did not survive outranks anything a probe could say:
|
|
144
|
+
// the heap it left behind is what the probe would be running on.
|
|
145
|
+
if (this.fatal) {
|
|
146
|
+
// The kind and not the message. Both probes are unauthenticated, and an
|
|
147
|
+
// exception message can carry an internal route, a query, or an id that
|
|
148
|
+
// a caller cannot act on anyway. The message went to the log when the
|
|
149
|
+
// barrier caught it.
|
|
150
|
+
return {
|
|
151
|
+
dispatch: "faulted",
|
|
152
|
+
dispatchDetail: `The gateway recorded a fatal ${this.fatal.kind} and is waiting to be restarted.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const probe = this.dispatchProbe;
|
|
156
|
+
if (!probe) {
|
|
157
|
+
return {
|
|
158
|
+
dispatch: "unmounted",
|
|
159
|
+
dispatchDetail: "No MCP transport is mounted on this gateway yet.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const deadlineMs = this.options.dispatchDeadlineMs ?? DEFAULT_DISPATCH_DEADLINE_MS;
|
|
163
|
+
try {
|
|
164
|
+
return await withDeadline(probe().then(() => ({ dispatch: "ok" })), deadlineMs, () => ({
|
|
165
|
+
dispatch: "wedged",
|
|
166
|
+
dispatchDetail: `The gateway's own dispatch path did not answer within ${deadlineMs}ms.`,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
// Same reason as the fatal branch: the cause is for the operator reading
|
|
171
|
+
// logs, not for an unauthenticated caller reading a health endpoint.
|
|
172
|
+
this.log("gateway dispatch probe failed", error);
|
|
173
|
+
return {
|
|
174
|
+
dispatch: "wedged",
|
|
175
|
+
dispatchDetail: "The gateway's own dispatch path failed.",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
log(message, cause) {
|
|
180
|
+
(this.options.log ?? ((text, err) => console.error(text, err)))(message, cause);
|
|
181
|
+
}
|
|
182
|
+
async probeUpstream() {
|
|
183
|
+
const probe = this.options.upstreamProbe;
|
|
184
|
+
if (!probe) {
|
|
185
|
+
return "unknown";
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
return await probe();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return "unreachable";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function detailOf(probed, upstreamDown, serverUrl) {
|
|
196
|
+
const reasons = [
|
|
197
|
+
probed.dispatchDetail,
|
|
198
|
+
upstreamDown ? `${serverUrl} did not answer.` : undefined,
|
|
199
|
+
].filter((reason) => reason !== undefined);
|
|
200
|
+
return reasons.length > 0 ? { detail: reasons.join(" ") } : {};
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=readiness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readiness.js","sourceRoot":"","sources":["../../src/health/readiness.ts"],"names":[],"mappings":"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"}
|