@haywoodmarx/agent-pager 0.1.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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/src/broker/address.d.ts +49 -0
  4. package/dist/src/broker/address.js +110 -0
  5. package/dist/src/broker/broker.d.ts +665 -0
  6. package/dist/src/broker/broker.js +2270 -0
  7. package/dist/src/broker/database.d.ts +81 -0
  8. package/dist/src/broker/database.js +69 -0
  9. package/dist/src/broker/operator-projection.d.ts +291 -0
  10. package/dist/src/broker/operator-projection.js +722 -0
  11. package/dist/src/broker/protocol.d.ts +237 -0
  12. package/dist/src/broker/protocol.js +311 -0
  13. package/dist/src/broker/schema.d.ts +94 -0
  14. package/dist/src/broker/schema.js +454 -0
  15. package/dist/src/broker/snapshot.d.ts +171 -0
  16. package/dist/src/broker/snapshot.js +78 -0
  17. package/dist/src/configuration-defaults.d.ts +64 -0
  18. package/dist/src/configuration-defaults.js +48 -0
  19. package/dist/src/configuration.d.ts +76 -0
  20. package/dist/src/configuration.js +156 -0
  21. package/dist/src/implementations/claude/environment.d.ts +8 -0
  22. package/dist/src/implementations/claude/environment.js +9 -0
  23. package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
  24. package/dist/src/implementations/claude/gateway-cli.js +136 -0
  25. package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
  26. package/dist/src/implementations/claude/gateway-identity.js +103 -0
  27. package/dist/src/implementations/claude/gateway.d.ts +148 -0
  28. package/dist/src/implementations/claude/gateway.js +236 -0
  29. package/dist/src/implementations/claude/instructions.d.ts +33 -0
  30. package/dist/src/implementations/claude/instructions.js +64 -0
  31. package/dist/src/implementations/claude/manifest.d.ts +33 -0
  32. package/dist/src/implementations/claude/manifest.js +108 -0
  33. package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
  34. package/dist/src/implementations/claude/plugin-directory.js +137 -0
  35. package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
  36. package/dist/src/implementations/claude/session-continuation.js +125 -0
  37. package/dist/src/implementations/claude/session-identity.d.ts +25 -0
  38. package/dist/src/implementations/claude/session-identity.js +31 -0
  39. package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
  40. package/dist/src/implementations/claude/session-start-cli.js +16 -0
  41. package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
  42. package/dist/src/implementations/claude/socket-delivery.js +159 -0
  43. package/dist/src/implementations/model.d.ts +162 -0
  44. package/dist/src/implementations/model.js +1 -0
  45. package/dist/src/implementations/opencode/adapter.d.ts +71 -0
  46. package/dist/src/implementations/opencode/adapter.js +94 -0
  47. package/dist/src/implementations/opencode/environment.d.ts +8 -0
  48. package/dist/src/implementations/opencode/environment.js +9 -0
  49. package/dist/src/implementations/opencode/manifest.d.ts +30 -0
  50. package/dist/src/implementations/opencode/manifest.js +93 -0
  51. package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
  52. package/dist/src/implementations/opencode/plugin-api.js +1 -0
  53. package/dist/src/implementations/opencode/plugin.d.ts +38 -0
  54. package/dist/src/implementations/opencode/plugin.js +303 -0
  55. package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
  56. package/dist/src/implementations/opencode/self-close-tool.js +31 -0
  57. package/dist/src/implementations/orca/terminal.d.ts +48 -0
  58. package/dist/src/implementations/orca/terminal.js +154 -0
  59. package/dist/src/implementations/pi/broker-client.d.ts +56 -0
  60. package/dist/src/implementations/pi/broker-client.js +156 -0
  61. package/dist/src/implementations/pi/environment.d.ts +8 -0
  62. package/dist/src/implementations/pi/environment.js +9 -0
  63. package/dist/src/implementations/pi/extension.d.ts +57 -0
  64. package/dist/src/implementations/pi/extension.js +253 -0
  65. package/dist/src/implementations/pi/host-slots.d.ts +18 -0
  66. package/dist/src/implementations/pi/host-slots.js +27 -0
  67. package/dist/src/implementations/pi/manifest.d.ts +11 -0
  68. package/dist/src/implementations/pi/manifest.js +61 -0
  69. package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
  70. package/dist/src/implementations/pi/self-close-tool.js +27 -0
  71. package/dist/src/implementations/registry.d.ts +47 -0
  72. package/dist/src/implementations/registry.js +157 -0
  73. package/dist/src/implementations/session-key.d.ts +13 -0
  74. package/dist/src/implementations/session-key.js +19 -0
  75. package/dist/src/index.d.ts +27 -0
  76. package/dist/src/index.js +26 -0
  77. package/dist/src/launch/broker-lock.d.ts +257 -0
  78. package/dist/src/launch/broker-lock.js +644 -0
  79. package/dist/src/launch/cli.d.ts +37 -0
  80. package/dist/src/launch/cli.js +317 -0
  81. package/dist/src/launch/environment.d.ts +45 -0
  82. package/dist/src/launch/environment.js +48 -0
  83. package/dist/src/launch/launch.d.ts +235 -0
  84. package/dist/src/launch/launch.js +223 -0
  85. package/dist/src/launch/on-demand.d.ts +202 -0
  86. package/dist/src/launch/on-demand.js +435 -0
  87. package/dist/src/launch/runtime-composition.d.ts +88 -0
  88. package/dist/src/launch/runtime-composition.js +253 -0
  89. package/dist/src/launch/runtime.d.ts +79 -0
  90. package/dist/src/launch/runtime.js +196 -0
  91. package/dist/src/launch/service-system.d.ts +11 -0
  92. package/dist/src/launch/service-system.js +39 -0
  93. package/dist/src/launch/service.d.ts +173 -0
  94. package/dist/src/launch/service.js +492 -0
  95. package/dist/src/launch/skill.d.ts +57 -0
  96. package/dist/src/launch/skill.js +81 -0
  97. package/dist/src/launch/terminal.d.ts +26 -0
  98. package/dist/src/launch/terminal.js +43 -0
  99. package/dist/src/launch/workspace-branch.d.ts +10 -0
  100. package/dist/src/launch/workspace-branch.js +29 -0
  101. package/dist/src/messaging/delivery-client.d.ts +144 -0
  102. package/dist/src/messaging/delivery-client.js +261 -0
  103. package/dist/src/messaging/delivery.d.ts +75 -0
  104. package/dist/src/messaging/delivery.js +22 -0
  105. package/dist/src/messaging/inbox-socket.d.ts +98 -0
  106. package/dist/src/messaging/inbox-socket.js +278 -0
  107. package/dist/src/messaging/mcp.d.ts +53 -0
  108. package/dist/src/messaging/mcp.js +501 -0
  109. package/dist/src/messaging/notifications.d.ts +231 -0
  110. package/dist/src/messaging/notifications.js +489 -0
  111. package/dist/src/messaging/push-delivery.d.ts +77 -0
  112. package/dist/src/messaging/push-delivery.js +187 -0
  113. package/dist/src/messaging/tools.d.ts +11 -0
  114. package/dist/src/messaging/tools.js +102 -0
  115. package/dist/src/messaging/wake.d.ts +23 -0
  116. package/dist/src/messaging/wake.js +39 -0
  117. package/dist/src/operator/agent-projection.d.ts +102 -0
  118. package/dist/src/operator/agent-projection.js +249 -0
  119. package/dist/src/operator/commands.d.ts +50 -0
  120. package/dist/src/operator/commands.js +199 -0
  121. package/dist/src/operator/diagnostics.d.ts +21 -0
  122. package/dist/src/operator/diagnostics.js +22 -0
  123. package/dist/src/operator/protocol.d.ts +511 -0
  124. package/dist/src/operator/protocol.js +49 -0
  125. package/dist/src/operator/repository-system.d.ts +14 -0
  126. package/dist/src/operator/repository-system.js +150 -0
  127. package/dist/src/operator/repository.d.ts +79 -0
  128. package/dist/src/operator/repository.js +72 -0
  129. package/dist/src/operator/traffic-projection.d.ts +19 -0
  130. package/dist/src/operator/traffic-projection.js +229 -0
  131. package/dist/src/operator/transport.d.ts +87 -0
  132. package/dist/src/operator/transport.js +549 -0
  133. package/dist/src/operator/update-store.d.ts +53 -0
  134. package/dist/src/operator/update-store.js +162 -0
  135. package/dist/src/operator/updates.d.ts +94 -0
  136. package/dist/src/operator/updates.js +280 -0
  137. package/dist/src/operator/validation.d.ts +114 -0
  138. package/dist/src/operator/validation.js +916 -0
  139. package/dist/src/shared/guards.d.ts +59 -0
  140. package/dist/src/shared/guards.js +119 -0
  141. package/dist/src/shared/http.d.ts +30 -0
  142. package/dist/src/shared/http.js +50 -0
  143. package/dist/src/shared/row.d.ts +2 -0
  144. package/dist/src/shared/row.js +1 -0
  145. package/dist/src/shared/shell.d.ts +6 -0
  146. package/dist/src/shared/shell.js +8 -0
  147. package/dist/src/tui/actions.d.ts +166 -0
  148. package/dist/src/tui/actions.js +253 -0
  149. package/dist/src/tui/agent-tree.d.ts +125 -0
  150. package/dist/src/tui/agent-tree.js +663 -0
  151. package/dist/src/tui/app.d.ts +38 -0
  152. package/dist/src/tui/app.js +378 -0
  153. package/dist/src/tui/chrome.d.ts +86 -0
  154. package/dist/src/tui/chrome.js +85 -0
  155. package/dist/src/tui/client.d.ts +41 -0
  156. package/dist/src/tui/client.js +279 -0
  157. package/dist/src/tui/composition.d.ts +41 -0
  158. package/dist/src/tui/composition.js +146 -0
  159. package/dist/src/tui/message-views.d.ts +94 -0
  160. package/dist/src/tui/message-views.js +515 -0
  161. package/dist/src/tui/model.d.ts +414 -0
  162. package/dist/src/tui/model.js +758 -0
  163. package/dist/src/tui/presentation.d.ts +350 -0
  164. package/dist/src/tui/presentation.js +541 -0
  165. package/dist/src/tui/production.d.ts +79 -0
  166. package/dist/src/tui/production.js +253 -0
  167. package/dist/src/tui/run-terminal.d.ts +32 -0
  168. package/dist/src/tui/run-terminal.js +54 -0
  169. package/dist/src/tui/runtime.d.ts +113 -0
  170. package/dist/src/tui/runtime.js +189 -0
  171. package/dist/src/tui/theme.d.ts +49 -0
  172. package/dist/src/tui/theme.js +55 -0
  173. package/dist/src/tui/transport.d.ts +31 -0
  174. package/dist/src/tui/transport.js +255 -0
  175. package/dist/src/version.d.ts +11 -0
  176. package/dist/src/version.js +52 -0
  177. package/package.json +142 -0
  178. package/skills/pager/SKILL.md +112 -0
@@ -0,0 +1,162 @@
1
+ import { openBrokerDatabase } from "../broker/database.js";
2
+ import { OPERATOR_PROTOCOL_VERSION, } from "./protocol.js";
3
+ import { validateOperatorEventEnvelope } from "./validation.js";
4
+ /** Reports corrupt or incompatible durable operator-stream state. */
5
+ class OperatorEventStoreError extends Error {
6
+ }
7
+ /** SQLite-backed bounded replay storage owned by one broker epoch. */
8
+ export class SqliteOperatorEventStore {
9
+ #database;
10
+ #epoch;
11
+ #replayLimit;
12
+ /**
13
+ * Open replay storage adjacent to the broker's durable state.
14
+ * @param databasePath - Broker SQLite path containing the operator stream tables.
15
+ * @param epoch - Current broker generation that owns new event sequence values.
16
+ * @param replayLimit - Maximum number of event envelopes retained globally.
17
+ * @param openDatabase - Opens the connection this store owns and closes.
18
+ */
19
+ constructor(databasePath, epoch, replayLimit = 1_024, openDatabase = openBrokerDatabase) {
20
+ if (!epoch)
21
+ throw new OperatorEventStoreError("operator stream epoch must be non-empty");
22
+ if (!Number.isSafeInteger(replayLimit) || replayLimit < 1) {
23
+ throw new OperatorEventStoreError("operator replay limit must be a positive safe integer");
24
+ }
25
+ this.#database = openDatabase(databasePath);
26
+ this.#epoch = epoch;
27
+ this.#replayLimit = replayLimit;
28
+ // A constructor that throws hands its caller nothing to close, so the connection is closed here.
29
+ try {
30
+ this.#initializeEpoch();
31
+ }
32
+ catch (error) {
33
+ this.#database.close();
34
+ throw error;
35
+ }
36
+ }
37
+ /** @inheritdoc */
38
+ currentCursor() {
39
+ const row = this.#state();
40
+ return {
41
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
42
+ epoch: this.#epoch,
43
+ sequence: readSequence(row.sequence),
44
+ };
45
+ }
46
+ /** @inheritdoc */
47
+ append(events) {
48
+ if (events.length === 0)
49
+ return [];
50
+ const current = this.currentCursor().sequence;
51
+ const envelopes = events.map((event, index) => validateOperatorEventEnvelope({
52
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
53
+ epoch: this.#epoch,
54
+ sequence: current + index + 1,
55
+ event,
56
+ }));
57
+ this.#database.exec("begin immediate");
58
+ try {
59
+ const state = this.#state();
60
+ if (readSequence(state.sequence) !== current) {
61
+ throw new OperatorEventStoreError("operator stream cursor changed during batch persistence");
62
+ }
63
+ for (const envelope of envelopes) {
64
+ this.#database
65
+ .prepare("insert into operator_stream_events(epoch, sequence, payload) values (?, ?, ?)")
66
+ .run(this.#epoch, envelope.sequence, JSON.stringify(envelope));
67
+ }
68
+ const next = envelopes.at(-1)?.sequence;
69
+ if (next === undefined)
70
+ throw new OperatorEventStoreError("operator event batch is unexpectedly empty");
71
+ this.#database.prepare("update operator_stream_state set sequence = ? where singleton = 1").run(next);
72
+ const oldestRetained = Math.max(1, next - this.#replayLimit + 1);
73
+ this.#database
74
+ .prepare("delete from operator_stream_events where epoch = ? and sequence < ?")
75
+ .run(this.#epoch, oldestRetained);
76
+ this.#database.exec("commit");
77
+ return envelopes;
78
+ }
79
+ catch (error) {
80
+ this.#database.exec("rollback");
81
+ throw error;
82
+ }
83
+ }
84
+ /** @inheritdoc */
85
+ replay(cursor) {
86
+ if (cursor.epoch !== this.#epoch)
87
+ return { type: "epoch_changed" };
88
+ const current = this.currentCursor().sequence;
89
+ if (cursor.sequence > current)
90
+ return { type: "cursor_gap" };
91
+ if (cursor.sequence === current)
92
+ return { type: "events", events: [] };
93
+ const rows = this.#database
94
+ .prepare("select sequence, payload from operator_stream_events where epoch = ? and sequence > ? order by sequence")
95
+ .all(this.#epoch, cursor.sequence);
96
+ if (rows.length === 0 || Number(rows[0]?.sequence) !== cursor.sequence + 1)
97
+ return { type: "cursor_gap" };
98
+ const events = rows.map((row, index) => {
99
+ if (Number(row.sequence) !== cursor.sequence + index + 1 || typeof row.payload !== "string") {
100
+ throw new OperatorEventStoreError("operator replay is not strictly contiguous");
101
+ }
102
+ let value;
103
+ try {
104
+ value = JSON.parse(row.payload);
105
+ }
106
+ catch {
107
+ throw new OperatorEventStoreError("operator replay contains invalid JSON");
108
+ }
109
+ return validateOperatorEventEnvelope(value);
110
+ });
111
+ if (events.at(-1)?.sequence !== current) {
112
+ throw new OperatorEventStoreError("operator replay does not reach the durable cursor");
113
+ }
114
+ return { type: "events", events };
115
+ }
116
+ /** @inheritdoc */
117
+ close() {
118
+ try {
119
+ this.#database.exec("pragma wal_checkpoint(TRUNCATE)");
120
+ }
121
+ finally {
122
+ this.#database.close();
123
+ }
124
+ }
125
+ #initializeEpoch() {
126
+ this.#database.exec("begin immediate");
127
+ try {
128
+ const existing = this.#database
129
+ .prepare("select epoch, sequence from operator_stream_state where singleton = 1")
130
+ .get();
131
+ if (!existing || existing.epoch !== this.#epoch) {
132
+ this.#database.exec("delete from operator_stream_events; delete from operator_stream_state;");
133
+ this.#database
134
+ .prepare("insert into operator_stream_state(singleton, epoch, sequence) values (1, ?, 0)")
135
+ .run(this.#epoch);
136
+ }
137
+ else {
138
+ readSequence(existing.sequence);
139
+ }
140
+ this.#database.exec("commit");
141
+ }
142
+ catch (error) {
143
+ this.#database.exec("rollback");
144
+ throw error;
145
+ }
146
+ }
147
+ #state() {
148
+ const row = this.#database
149
+ .prepare("select epoch, sequence from operator_stream_state where singleton = 1")
150
+ .get();
151
+ if (!row || row.epoch !== this.#epoch)
152
+ throw new OperatorEventStoreError("operator stream epoch state is unavailable");
153
+ return row;
154
+ }
155
+ }
156
+ function readSequence(value) {
157
+ const sequence = Number(value);
158
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
159
+ throw new OperatorEventStoreError("operator stream sequence is invalid");
160
+ }
161
+ return sequence;
162
+ }
@@ -0,0 +1,94 @@
1
+ import type { BrokerCommit, BrokerTrafficReadResult } from "../broker/broker.js";
2
+ import type { BrokerSnapshot } from "../broker/snapshot.js";
3
+ import { type OperatorAgentProjectionDependencies } from "./agent-projection.js";
4
+ import { type OperatorCursor, type OperatorDiagnostic, type OperatorEventEnvelope, type OperatorSnapshot } from "./protocol.js";
5
+ import type { OperatorEventStorePort } from "./update-store.js";
6
+ /** Supplies durable broker state and successful-commit observation to the update projection. */
7
+ export interface OperatorUpdateSourcePort {
8
+ /**
9
+ * Return the agent-derived broker state used by repository and agent projection.
10
+ * @returns Workspaces, agents, and their connection states, with no deliveries.
11
+ */
12
+ operatorAgentSource(): Pick<BrokerSnapshot, "workspaces" | "agents" | "connections">;
13
+ /**
14
+ * Return bounded durable traffic without claiming message bodies.
15
+ * @returns Durable traffic rows from the broker's bounded window, each carrying a traffic position, and
16
+ * the rows skipped while reading it.
17
+ */
18
+ operatorTrafficRecords(): BrokerTrafficReadResult;
19
+ /**
20
+ * Observe commits only after the domain transaction succeeds.
21
+ * @param listener - Isolated post-commit projection callback receiving what the commit changed.
22
+ * @returns A callback that detaches the observer.
23
+ */
24
+ subscribeCommits(listener: (commit: BrokerCommit) => void): () => void;
25
+ }
26
+ /** Configures deterministic projection, durability, and timestamp collaborators. */
27
+ export type OperatorUpdateDependencies = {
28
+ /** Supplies durable broker source state. */ source: OperatorUpdateSourcePort;
29
+ /** Supplies repository discovery and public provider metadata. */ projection: OperatorAgentProjectionDependencies;
30
+ /** Persists complete epoch-scoped replay batches. */ store: OperatorEventStorePort;
31
+ /** Supplies broker-clock timestamps for snapshots and retention metadata. */ now: () => number;
32
+ };
33
+ /** Reports a bounded failure without retaining thrown values or projected content. */
34
+ export type OperatorUpdateDiagnostic = {
35
+ /** Identifies the terminal stream condition. */ type: "stream_unavailable";
36
+ /** Identifies the boundary that could not preserve authoritative ordering. */ stage: "projection" | "journal";
37
+ /** Records the last cursor that was durably safe to publish. */ cursor: OperatorCursor;
38
+ /** Records when the boundary became unavailable. */ at: number;
39
+ };
40
+ /** Reports the initial frames needed before a subscriber can consume live events. */
41
+ export type OperatorOpenResult = {
42
+ type: "initial";
43
+ snapshot: OperatorSnapshot;
44
+ } | {
45
+ type: "resumed";
46
+ events: OperatorEventEnvelope[];
47
+ } | {
48
+ /** Requires complete replacement before live events. */ type: "replacement";
49
+ /** Identifies why exact replay was impossible. */ reason: "cursor_gap" | "epoch_changed";
50
+ /** Supplies the complete authoritative replacement. */ snapshot: OperatorSnapshot;
51
+ } | {
52
+ type: "unavailable";
53
+ };
54
+ /** Carries a post-open publication or terminal integrity signal. */
55
+ export type OperatorPublication = {
56
+ type: "event";
57
+ envelope: OperatorEventEnvelope;
58
+ } | {
59
+ /** Reports one operator-visible condition beside the ordered stream. */ type: "diagnostic";
60
+ diagnostic: OperatorDiagnostic;
61
+ } | {
62
+ type: "unavailable";
63
+ };
64
+ /** Produces complete snapshots and durable strictly ordered projection changes. */
65
+ export declare class OperatorUpdateService {
66
+ #private;
67
+ /**
68
+ * Construct an update service from explicit durable and projection boundaries.
69
+ * @param dependencies - Source, projection, journal, and clock collaborators.
70
+ */
71
+ constructor(dependencies: OperatorUpdateDependencies);
72
+ /**
73
+ * Atomically establish replay state and subsequent publication for one consumer.
74
+ * @param cursor - Last incorporated position, or null for a cold snapshot.
75
+ * @param subscriber - Isolated consumer of future durable publications.
76
+ * @returns Initial hydration or recovery result plus an unsubscribe callback.
77
+ */
78
+ open(cursor: OperatorCursor | null, subscriber: (publication: OperatorPublication) => void): {
79
+ result: OperatorOpenResult;
80
+ unsubscribe: () => void;
81
+ };
82
+ /**
83
+ * Return the latest client-visible cursor without exposing mutable state.
84
+ * @returns Last complete batch position committed to the replay journal.
85
+ */
86
+ cursor(): OperatorCursor;
87
+ /**
88
+ * Return bounded stream-integrity diagnostics.
89
+ * @returns Defensive copies containing no projected content or thrown values.
90
+ */
91
+ diagnostics(): OperatorUpdateDiagnostic[];
92
+ /** Stop commit observation and close replay storage. */
93
+ close(): void;
94
+ }
@@ -0,0 +1,280 @@
1
+ import { projectOperatorAgents } from "./agent-projection.js";
2
+ import { OPERATOR_PROTOCOL_VERSION, } from "./protocol.js";
3
+ import { operatorDiagnostic } from "./diagnostics.js";
4
+ import { projectOperatorTraffic } from "./traffic-projection.js";
5
+ /** Bounds retained stream-integrity diagnostics. */
6
+ const UPDATE_DIAGNOSTIC_LIMIT = 64;
7
+ /** Broker tables whose rows feed the repository and agent halves of an operator snapshot. */
8
+ const AGENT_SOURCE_TABLES = ["agents", "sessions", "workspaces", "alias_history"];
9
+ /** Broker tables whose rows feed the traffic half of an operator snapshot. */
10
+ const TRAFFIC_SOURCE_TABLES = ["messages", "message_events", "claims", "traffic_positions"];
11
+ /** Produces complete snapshots and durable strictly ordered projection changes. */
12
+ export class OperatorUpdateService {
13
+ #source;
14
+ #projection;
15
+ #store;
16
+ #now;
17
+ #subscribers = new Set();
18
+ #diagnostics = [];
19
+ #unsubscribeCommits;
20
+ #snapshot;
21
+ #unavailable = false;
22
+ #refreshPending = false;
23
+ #pendingTables = new Set();
24
+ #refreshing = false;
25
+ /**
26
+ * Construct an update service from explicit durable and projection boundaries.
27
+ * @param dependencies - Source, projection, journal, and clock collaborators.
28
+ */
29
+ constructor(dependencies) {
30
+ this.#source = dependencies.source;
31
+ this.#store = dependencies.store;
32
+ this.#now = dependencies.now;
33
+ this.#projection = {
34
+ ...dependencies.projection,
35
+ diagnostics: {
36
+ report: (diagnostic) => {
37
+ this.#publish({ type: "diagnostic", diagnostic });
38
+ },
39
+ },
40
+ now: dependencies.now,
41
+ };
42
+ this.#snapshot = this.#project(this.#store.currentCursor(), undefined);
43
+ this.#unsubscribeCommits = this.#source.subscribeCommits((commit) => {
44
+ this.#refreshAfterCommit(commit);
45
+ });
46
+ }
47
+ /**
48
+ * Atomically establish replay state and subsequent publication for one consumer.
49
+ * @param cursor - Last incorporated position, or null for a cold snapshot.
50
+ * @param subscriber - Isolated consumer of future durable publications.
51
+ * @returns Initial hydration or recovery result plus an unsubscribe callback.
52
+ */
53
+ open(cursor, subscriber) {
54
+ if (this.#unavailable)
55
+ return { result: { type: "unavailable" }, unsubscribe: () => undefined };
56
+ let result;
57
+ if (cursor === null) {
58
+ result = { type: "initial", snapshot: cloneSnapshot(this.#snapshot) };
59
+ }
60
+ else {
61
+ let replay;
62
+ try {
63
+ replay = this.#store.replay(cursor);
64
+ }
65
+ catch {
66
+ this.#fail("journal");
67
+ return { result: { type: "unavailable" }, unsubscribe: () => undefined };
68
+ }
69
+ result =
70
+ replay.type === "events"
71
+ ? { type: "resumed", events: replay.events.map(cloneEnvelope) }
72
+ : {
73
+ type: "replacement",
74
+ reason: replay.type,
75
+ snapshot: cloneSnapshot(this.#snapshot),
76
+ };
77
+ }
78
+ this.#subscribers.add(subscriber);
79
+ return { result, unsubscribe: () => this.#subscribers.delete(subscriber) };
80
+ }
81
+ /**
82
+ * Return the latest client-visible cursor without exposing mutable state.
83
+ * @returns Last complete batch position committed to the replay journal.
84
+ */
85
+ cursor() {
86
+ return { ...this.#snapshot.cursor };
87
+ }
88
+ /**
89
+ * Return bounded stream-integrity diagnostics.
90
+ * @returns Defensive copies containing no projected content or thrown values.
91
+ */
92
+ diagnostics() {
93
+ return this.#diagnostics.map((entry) => ({ ...entry, cursor: { ...entry.cursor } }));
94
+ }
95
+ /** Stop commit observation and close replay storage. */
96
+ close() {
97
+ this.#unsubscribeCommits();
98
+ this.#subscribers.clear();
99
+ this.#store.close();
100
+ }
101
+ #refreshAfterCommit(commit) {
102
+ if (this.#unavailable)
103
+ return;
104
+ // Coalesced refreshes cover every table any of them named, so a merged run must re-derive the
105
+ // union rather than only what the last commit touched.
106
+ if (commit.tables === undefined)
107
+ this.#pendingTables = undefined;
108
+ else if (this.#pendingTables !== undefined)
109
+ for (const table of commit.tables)
110
+ this.#pendingTables.add(table);
111
+ this.#refreshPending = true;
112
+ if (this.#refreshing)
113
+ return;
114
+ this.#refreshing = true;
115
+ try {
116
+ while (this.#refreshPending) {
117
+ this.#refreshPending = false;
118
+ const tables = this.#pendingTables;
119
+ this.#pendingTables = new Set();
120
+ this.#refreshOnce(tables);
121
+ }
122
+ }
123
+ finally {
124
+ this.#refreshing = false;
125
+ }
126
+ }
127
+ #refreshOnce(tables) {
128
+ let next;
129
+ try {
130
+ next = this.#project(this.#snapshot.cursor, this.#snapshot, tables);
131
+ }
132
+ catch {
133
+ this.#fail("projection");
134
+ return;
135
+ }
136
+ const changes = diffSnapshots(this.#snapshot, next);
137
+ if (changes.length === 0)
138
+ return;
139
+ let envelopes;
140
+ try {
141
+ envelopes = verifyDurableBatch(this.#snapshot.cursor, changes, this.#store.append(changes));
142
+ }
143
+ catch {
144
+ this.#fail("journal");
145
+ return;
146
+ }
147
+ const cursor = envelopes.at(-1);
148
+ if (!cursor) {
149
+ this.#fail("journal");
150
+ return;
151
+ }
152
+ this.#snapshot = {
153
+ ...next,
154
+ cursor: {
155
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
156
+ epoch: cursor.epoch,
157
+ sequence: cursor.sequence,
158
+ },
159
+ epoch: cursor.epoch,
160
+ };
161
+ for (const envelope of envelopes)
162
+ this.#publish({ type: "event", envelope });
163
+ }
164
+ /**
165
+ * Re-derive the operator snapshot, skipping either half whose source tables did not change.
166
+ *
167
+ * A commit that names its tables cannot have altered a half none of them feed, so that half is
168
+ * carried across by reference and the diff skips it outright. An unnamed table set means the
169
+ * changeset was incomplete, and both halves are re-derived.
170
+ * @param cursor - Cursor to stamp on the projected snapshot.
171
+ * @param previous - Snapshot whose unchanged halves are carried across, or undefined before the first projection.
172
+ * @param tables - Tables the commits behind this refresh changed, or undefined to re-derive all.
173
+ * @returns Projected snapshot at the supplied cursor.
174
+ */
175
+ #project(cursor, previous, tables) {
176
+ const agentsStale = previous === undefined || tables === undefined || intersects(tables, AGENT_SOURCE_TABLES);
177
+ const trafficStale = previous === undefined || tables === undefined || intersects(tables, TRAFFIC_SOURCE_TABLES);
178
+ const projected = agentsStale
179
+ ? projectOperatorAgents(this.#source.operatorAgentSource(), this.#projection)
180
+ : { repositories: previous.repositories, agents: previous.agents };
181
+ return {
182
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
183
+ epoch: cursor.epoch,
184
+ cursor,
185
+ generatedAt: this.#now(),
186
+ repositories: projected.repositories,
187
+ agents: projected.agents,
188
+ traffic: trafficStale
189
+ ? projectOperatorTraffic(this.#source, {
190
+ report: (diagnostic) => {
191
+ this.#publish({ type: "diagnostic", diagnostic });
192
+ },
193
+ }, this.#now)
194
+ : previous.traffic,
195
+ };
196
+ }
197
+ #fail(stage) {
198
+ if (this.#unavailable)
199
+ return;
200
+ this.#unavailable = true;
201
+ const diagnostic = {
202
+ type: "stream_unavailable",
203
+ stage,
204
+ cursor: { ...this.#snapshot.cursor },
205
+ at: this.#now(),
206
+ };
207
+ this.#diagnostics.push(diagnostic);
208
+ if (this.#diagnostics.length > UPDATE_DIAGNOSTIC_LIMIT)
209
+ this.#diagnostics.shift();
210
+ this.#publish({
211
+ type: "diagnostic",
212
+ diagnostic: operatorDiagnostic("broker", "error", "operator stream", `unavailable at ${stage}`, this.#now()),
213
+ });
214
+ this.#publish({ type: "unavailable" });
215
+ this.#subscribers.clear();
216
+ }
217
+ #publish(publication) {
218
+ for (const subscriber of [...this.#subscribers]) {
219
+ try {
220
+ subscriber(publication);
221
+ }
222
+ catch {
223
+ this.#subscribers.delete(subscriber);
224
+ }
225
+ }
226
+ }
227
+ }
228
+ function diffSnapshots(previous, next) {
229
+ const repositoryChanges = diffMap(previous.repositories, next.repositories, (value) => value.id, (repository) => ({ type: "repository_upserted", repository }), (repositoryId) => ({ type: "repository_removed", repositoryId }));
230
+ const agentChanges = diffMap(previous.agents, next.agents, (value) => value.id, (agent) => ({ type: "agent_upserted", agent }), (agentId) => ({ type: "agent_removed", agentId }));
231
+ const trafficChanges = diffMap(previous.traffic, next.traffic, (value) => value.trafficId, (traffic) => ({ type: "traffic_upserted", traffic }), (trafficId) => ({ type: "traffic_removed", trafficId }));
232
+ return [
233
+ ...repositoryChanges.upserts,
234
+ ...agentChanges.upserts,
235
+ ...trafficChanges.upserts,
236
+ ...trafficChanges.removals,
237
+ ...agentChanges.removals,
238
+ ...repositoryChanges.removals,
239
+ ];
240
+ }
241
+ function intersects(tables, candidates) {
242
+ return candidates.some((table) => tables.has(table));
243
+ }
244
+ function diffMap(previous, next, key, upsert, removal) {
245
+ // A projection half the refresh skipped is carried across by reference, and identity is the only
246
+ // check that can tell that apart from a rebuild that happened to produce equal values.
247
+ if (previous === next)
248
+ return { upserts: [], removals: [] };
249
+ const before = new Map(previous.map((value) => [key(value), value]));
250
+ const after = new Map(next.map((value) => [key(value), value]));
251
+ const upserts = [];
252
+ for (const value of next) {
253
+ const old = before.get(key(value));
254
+ if (old === undefined || JSON.stringify(old) !== JSON.stringify(value))
255
+ upserts.push(upsert(value));
256
+ }
257
+ const removals = [];
258
+ for (const value of previous)
259
+ if (!after.has(key(value)))
260
+ removals.push(removal(key(value)));
261
+ return { upserts, removals };
262
+ }
263
+ function cloneSnapshot(snapshot) {
264
+ return structuredClone(snapshot);
265
+ }
266
+ function cloneEnvelope(envelope) {
267
+ return structuredClone(envelope);
268
+ }
269
+ function verifyDurableBatch(cursor, changes, envelopes) {
270
+ if (envelopes.length !== changes.length)
271
+ throw new Error("operator journal returned an incomplete event batch");
272
+ return envelopes.map((envelope, index) => {
273
+ if (envelope.epoch !== cursor.epoch ||
274
+ envelope.sequence !== cursor.sequence + index + 1 ||
275
+ JSON.stringify(envelope.event) !== JSON.stringify(changes[index])) {
276
+ throw new Error("operator journal returned a mismatched event batch");
277
+ }
278
+ return cloneEnvelope(envelope);
279
+ });
280
+ }
@@ -0,0 +1,114 @@
1
+ import { type OperatorAgent, type OperatorCommandEnvelope, type OperatorCommandResult, type OperatorCorrelatedCommandResult, type OperatorCursor, type OperatorCursorResult, type OperatorDiagnostic, type OperatorEventEnvelope, type OperatorMessageBody, type OperatorRepository, type OperatorSnapshot, type OperatorTraffic, type OperatorTransportRequest } from "./protocol.js";
2
+ /**
3
+ * Bounds how many rows a projection admits into one operator snapshot array.
4
+ *
5
+ * The projections enforce it, not the validators in this file. Derived from the broker's traffic
6
+ * bound: an array smaller than what the broker projects would reject a snapshot the broker
7
+ * considers valid, and one larger admits rows nothing can produce.
8
+ */
9
+ export declare const OPERATOR_ARRAY_LENGTH_LIMIT = 4096;
10
+ /** Reports that an untrusted value does not satisfy the operator protocol. */
11
+ export declare class OperatorValidationError extends Error {
12
+ /** Identifies the rejected field. */
13
+ readonly path: string;
14
+ /**
15
+ * Constructs a boundary error at one public field path.
16
+ * @param path - Field path at which validation failed.
17
+ * @param reason - Safe explanation suitable for operator diagnostics.
18
+ */
19
+ constructor(
20
+ /** Identifies the rejected field. */
21
+ path: string, reason: string);
22
+ }
23
+ /**
24
+ * Validate an untrusted operator cursor.
25
+ * @param value - Candidate cursor received from an external boundary.
26
+ * @returns A fully validated epoch and monotonic sequence position.
27
+ */
28
+ export declare function validateOperatorCursor(value: unknown): OperatorCursor;
29
+ /**
30
+ * Validate an untrusted repository projection.
31
+ * @param value - Candidate presentation grouping received from an external boundary.
32
+ * @returns A repository projection that carries no broker authority.
33
+ */
34
+ export declare function validateOperatorRepository(value: unknown): OperatorRepository;
35
+ /**
36
+ * Validate an untrusted operator agent projection.
37
+ * @param value - Candidate agent received from an external boundary.
38
+ * @returns A fully typed agent with separated broker and repository identities.
39
+ */
40
+ export declare function validateOperatorAgent(value: unknown): OperatorAgent;
41
+ /**
42
+ * Validate an untrusted traffic projection.
43
+ * @param value - Candidate accepted delivery or audit received from an external boundary.
44
+ * @returns One member of the closed operator traffic union.
45
+ */
46
+ export declare function validateOperatorTraffic(value: unknown): OperatorTraffic;
47
+ /**
48
+ * Validate one public operator body.
49
+ * @param value - Candidate retained public body.
50
+ * @returns A bounded body representation whose metadata matches its public raw copy.
51
+ */
52
+ export declare function validateOperatorMessageBody(value: unknown): OperatorMessageBody;
53
+ /**
54
+ * Validate an untrusted operator diagnostic.
55
+ * @param value - Candidate diagnostic received from an external boundary.
56
+ * @returns A diagnostic with a bounded scope and summary.
57
+ */
58
+ export declare function validateOperatorDiagnostic(value: unknown): OperatorDiagnostic;
59
+ /**
60
+ * Validate an untrusted complete operator projection.
61
+ * @param value - Candidate snapshot received from an external boundary.
62
+ * @returns A typed snapshot whose cross-record identities and sequences are consistent.
63
+ */
64
+ export declare function validateOperatorSnapshot(value: unknown): OperatorSnapshot;
65
+ /**
66
+ * Validate the serialized size shared by snapshot producers and the transport.
67
+ * @param byteLength - UTF-8 JSON byte count for one complete snapshot.
68
+ * @returns The accepted byte count for subsequent bounded transfer.
69
+ */
70
+ export declare function validateOperatorSnapshotByteLength(byteLength: number): number;
71
+ /**
72
+ * Validate an untrusted sequenced state event.
73
+ * @param value - Candidate event envelope received from an external boundary.
74
+ * @returns A fully typed authoritative state event.
75
+ */
76
+ export declare function validateOperatorEventEnvelope(value: unknown): OperatorEventEnvelope;
77
+ /**
78
+ * Classify an untrusted event against the current cursor.
79
+ * @param cursorValue - Candidate last authoritative event position, validated before classification.
80
+ * @param value - Candidate event envelope received from the stream.
81
+ * @returns A distinct apply, duplicate, gap, stale-epoch, or incompatible-version result.
82
+ */
83
+ export declare function classifyOperatorEvent(cursorValue: unknown, value: unknown): OperatorCursorResult;
84
+ /**
85
+ * Validate one untrusted client-to-server transport frame.
86
+ * @param value - Candidate newline-delimited request received from a loopback client.
87
+ * @returns The validated request, as whichever member of the transport request union it names.
88
+ */
89
+ export declare function validateOperatorTransportRequest(value: unknown): OperatorTransportRequest;
90
+ /**
91
+ * Validate one generated operator control correlation before it reaches scheduling or durability.
92
+ * @param value - Candidate correlation returned by an injected generator.
93
+ * @returns Bounded public-safe opaque control identity.
94
+ */
95
+ export declare function validateOperatorControlId(value: unknown): string;
96
+ /**
97
+ * Validate an untrusted operator command without granting repository projection authority.
98
+ * @param value - Candidate versioned command received from an external boundary.
99
+ * @returns A typed operator command envelope.
100
+ */
101
+ export declare function validateOperatorCommand(value: unknown): OperatorCommandEnvelope;
102
+ /**
103
+ * Validate an untrusted command admission result.
104
+ * @param value - Candidate result received from the command endpoint.
105
+ * @returns A typed admission outcome that contains no authoritative state event.
106
+ */
107
+ export declare function validateOperatorCommandResult(value: unknown): OperatorCommandResult;
108
+ /**
109
+ * Validate and correlate a command result with the originating command kind and identity.
110
+ * @param commandValue - Candidate originating command received at the operator boundary.
111
+ * @param resultValue - Candidate result returned for that command.
112
+ * @returns A narrowed command/result pair whose accepted members cannot cross command kinds.
113
+ */
114
+ export declare function validateOperatorCommandResultForCommand(commandValue: unknown, resultValue: unknown): OperatorCorrelatedCommandResult;