@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,173 @@
1
+ import type { Broker, BrokerOptions } from "../broker/broker.js";
2
+ import { ProtocolError } from "../broker/protocol.js";
3
+ import { type PagerConfig } from "../configuration-defaults.js";
4
+ import { type SnapshotBrokerPort } from "../broker/snapshot.js";
5
+ import { ImplementationRegistry } from "../implementations/registry.js";
6
+ import { type McpBrokerPort } from "../messaging/mcp.js";
7
+ import { type PushDeliveryBroker } from "../messaging/push-delivery.js";
8
+ import { type OperatorCommandBrokerPort } from "../operator/commands.js";
9
+ import { type OperatorTransportSchedulerPort } from "../operator/transport.js";
10
+ import { type OperatorUpdateSourcePort } from "../operator/updates.js";
11
+ import { type BrokerLockOwner } from "./broker-lock.js";
12
+ export { systemWorkspaceBranch } from "./workspace-branch.js";
13
+ /** Schedules the service's timers, none of which may keep the process alive. */
14
+ export interface BrokerServiceSchedulerPort extends OperatorTransportSchedulerPort {
15
+ /**
16
+ * Schedule one callback.
17
+ * @param callback - Work invoked once the delay has elapsed.
18
+ * @param delayMs - Non-negative delay in milliseconds, of any size.
19
+ * @returns A callback that cancels the invocation if it has not yet run.
20
+ */
21
+ once(callback: () => void, delayMs: number): () => void;
22
+ }
23
+ /** Filesystem operations the service performs on its database and the files beside it. */
24
+ export type BrokerServiceFilesystemPort = {
25
+ /**
26
+ * Report whether a path exists.
27
+ * @param path - Absolute path to test.
28
+ * @returns - True when the path exists.
29
+ */
30
+ exists(path: string): boolean;
31
+ /**
32
+ * Report a file's size.
33
+ * @param path - Existing file.
34
+ * @returns - Size in bytes.
35
+ */
36
+ sizeOf(path: string): number;
37
+ /**
38
+ * Delete a file.
39
+ * @param path - Existing file.
40
+ */
41
+ remove(path: string): void;
42
+ /**
43
+ * Create a directory and any missing parents.
44
+ * @param path - Directory that must exist afterwards.
45
+ */
46
+ createDirectory(path: string): void;
47
+ };
48
+ /** Broker operations the service, and every endpoint it composes, reach. */
49
+ export type BrokerServiceBrokerPort = Pick<Broker, "epoch" | "reapExpiredReservations" | "subscribeCommits" | "nextSweepDelayMs" | "reapTerminalAgents" | "dueRedeliveryWakes" | "recordRedeliveryAttempt" | "close"> & PushDeliveryBroker & McpBrokerPort & SnapshotBrokerPort & OperatorCommandBrokerPort & OperatorUpdateSourcePort;
50
+ /** Reports that a configured loopback endpoint could not take its port. */
51
+ export declare class EndpointPortUnavailableError extends ProtocolError {
52
+ /** Names the endpoint whose port is unavailable. */
53
+ readonly endpoint: string;
54
+ /** Names the interface the endpoint attempted to bind. */
55
+ readonly host: string;
56
+ /** Records the requested TCP port. */
57
+ readonly port: number;
58
+ /**
59
+ * Constructs a startup failure naming the endpoint that could not bind.
60
+ * @param endpoint - Human-facing name of the endpoint being started.
61
+ * @param host - Interface the endpoint attempted to bind.
62
+ * @param port - Requested TCP port.
63
+ * @param cause - Operating-system error code that rejected the bind.
64
+ */
65
+ constructor(
66
+ /** Names the endpoint whose port is unavailable. */
67
+ endpoint: string,
68
+ /** Names the interface the endpoint attempted to bind. */
69
+ host: string,
70
+ /** Records the requested TCP port. */
71
+ port: number, cause: string);
72
+ }
73
+ /** Supplies validated configuration and path metadata to a broker service. */
74
+ export type BrokerServiceOptions = {
75
+ /**
76
+ * Supplies the one clock this service process runs on.
77
+ *
78
+ * Every component the service composes stamps work that is read against rows the others write;
79
+ * two clocks between them produce an ordering none of them agreed to. Required of the caller
80
+ * rather than defaulted here, so that "one clock" is a property the composition root states and a test can substitute.
81
+ */
82
+ now: () => number;
83
+ /**
84
+ * Receives the diagnostics this service and the components it composes report, including each
85
+ * failing step of `stop()`, the first of which it also rejects with.
86
+ *
87
+ * One sink, so an operator reading it does not have to know which component spoke. The broker's
88
+ * schema preparation is the exception: it writes to standard error itself.
89
+ * @param message - Operator-facing diagnostic, never model-facing and never a process binding.
90
+ */
91
+ report: (message: string) => void;
92
+ /** Owns every timer the service arms. */
93
+ scheduler: BrokerServiceSchedulerPort;
94
+ /** Watches for the database's removal and tidies the files beside it. */
95
+ filesystem: BrokerServiceFilesystemPort;
96
+ /**
97
+ * Constructs the broker this service owns for the life of its process.
98
+ *
99
+ * A dependency built fresh per session from a value resolved late (the database path and the
100
+ * config `start()` resolves), so it takes a factory rather than a constructed instance.
101
+ */
102
+ openBroker: (databasePath: string, options: BrokerOptions) => BrokerServiceBrokerPort;
103
+ /**
104
+ * Holds the exclusive claim on the database.
105
+ *
106
+ * Must own the lock at `brokerLockPath` of the database path: that is where every adopting caller
107
+ * reads the endpoints this service publishes. The service reports the path the lock names.
108
+ */
109
+ lock: BrokerLockOwner;
110
+ /** Supplies validated runtime policies and deterministic server endpoints. */
111
+ config?: PagerConfig;
112
+ /** Names the resolved user configuration file for diagnostics. */
113
+ configPath?: string | null;
114
+ /** Names the user store directory when configuration is supplied. */
115
+ storeDirectory?: string;
116
+ };
117
+ /** Describes the active loopback endpoints published by the singleton broker service. */
118
+ export type BrokerServiceStatus = {
119
+ /** Identifies the process serving the endpoints. */
120
+ pid: number;
121
+ /** Identifies the current broker generation. */
122
+ epoch: string;
123
+ /** Names the durable broker database. */
124
+ databasePath: string;
125
+ /** Names the exclusive service lock. */
126
+ lockPath: string;
127
+ /** Locates the shared MCP endpoint. */
128
+ mcpUrl: string;
129
+ /** Locates the shared snapshot origin. */
130
+ snapshotUrl: string;
131
+ /** Locates the operator update stream, or reports that only this endpoint is unavailable. */
132
+ operatorUrl: string | null;
133
+ };
134
+ /** Runs one durable broker and its loopback endpoints under an exclusive process lock. */
135
+ export declare class BrokerService {
136
+ #private;
137
+ /**
138
+ * Constructs a service with the durable database and adjacent lock-file locations.
139
+ * @param databasePath - Durable SQLite file owned by the service.
140
+ * @param implementations - Registry supplying implementation notification capabilities.
141
+ * @param options - Validated runtime settings and the ports the service runs on.
142
+ */
143
+ constructor(databasePath: string, implementations: ImplementationRegistry, options: BrokerServiceOptions);
144
+ /**
145
+ * Starts the exclusive broker service and returns its published loopback endpoints.
146
+ * @returns - Process ownership and endpoint details for the running service.
147
+ */
148
+ start(): Promise<BrokerServiceStatus>;
149
+ /**
150
+ * Returns the live service status after successful startup.
151
+ * @returns - Process ownership and endpoint details for the running service.
152
+ */
153
+ status(): BrokerServiceStatus;
154
+ /**
155
+ * Register what to do once this service finds its database gone.
156
+ *
157
+ * A handler registered before abandonment runs before the stop that follows it; one registered after
158
+ * runs at once, while that stop may still be in flight. Stopping releases the endpoints but cannot
159
+ * end a process parked waiting for a signal, so the owner of that wait decides how the process ends.
160
+ * @param handler - Invoked once the database this service serves has gone.
161
+ */
162
+ onAbandoned(handler: () => void): void;
163
+ /**
164
+ * Stops endpoints, closes the database, and releases this process's lock.
165
+ *
166
+ * A call made while a stop is in flight joins it, so any caller's settled stop means teardown has
167
+ * finished. Every close is attempted and the lock is released whatever the others do. Each failing
168
+ * step reaches the report sink, and the first one rejects. Sidecar cleanup runs only once every
169
+ * connection to the database has closed, is best effort, and reports nothing.
170
+ * @returns - Settles once teardown has finished.
171
+ */
172
+ stop(): Promise<void>;
173
+ }
@@ -0,0 +1,492 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { dirname } from "node:path";
3
+ import { ProtocolError } from "../broker/protocol.js";
4
+ import { LOOPBACK_HOST } from "../configuration-defaults.js";
5
+ import { initializePagerStore } from "../configuration.js";
6
+ import { publicEpoch, startSnapshotServer, } from "../broker/snapshot.js";
7
+ import { implementationId, ImplementationRegistry } from "../implementations/registry.js";
8
+ import { startMcpServer } from "../messaging/mcp.js";
9
+ import { NotificationHub, NotificationScheduler } from "../messaging/notifications.js";
10
+ import { PushDeliveryService } from "../messaging/push-delivery.js";
11
+ import { OperatorCommandService } from "../operator/commands.js";
12
+ import { createSystemRepositoryProjectionDependencies } from "../operator/repository-system.js";
13
+ import { issueOperatorClientId, startOperatorTransport, } from "../operator/transport.js";
14
+ import { SqliteOperatorEventStore } from "../operator/update-store.js";
15
+ import { OperatorUpdateService } from "../operator/updates.js";
16
+ import { currentBrokerServiceIdentity } from "./broker-lock.js";
17
+ import { systemWorkspaceBranch } from "./workspace-branch.js";
18
+ export { systemWorkspaceBranch } from "./workspace-branch.js";
19
+ /** Tables whose rows can move the moment a sweep next has work to do. */
20
+ const SWEEP_DEADLINE_TABLES = ["agents", "sessions", "messages", "wakes"];
21
+ /** Sets how often the watchdog checks that the database this service serves still exists. */
22
+ const ABANDONMENT_WATCHDOG_INTERVAL_MS = 1_000;
23
+ /** Sets how often the operator transport tells a connected client it is still alive. */
24
+ const OPERATOR_HEARTBEAT_INTERVAL_MS = 5_000;
25
+ /** Bounds how long the operator transport waits for a client before treating it as gone. */
26
+ const OPERATOR_LIVENESS_TIMEOUT_MS = 15_000;
27
+ /** Sets the entropy of an operator control id, in bytes. */
28
+ const CONTROL_ID_BYTES = 12;
29
+ /** Delays the retry after a sweep failed, so a persistent failure cannot spin on a due deadline. */
30
+ const SWEEP_FAILURE_RETRY_MS = 1_000;
31
+ /** Reports that a configured loopback endpoint could not take its port. */
32
+ export class EndpointPortUnavailableError extends ProtocolError {
33
+ endpoint;
34
+ host;
35
+ port;
36
+ /**
37
+ * Constructs a startup failure naming the endpoint that could not bind.
38
+ * @param endpoint - Human-facing name of the endpoint being started.
39
+ * @param host - Interface the endpoint attempted to bind.
40
+ * @param port - Requested TCP port.
41
+ * @param cause - Operating-system error code that rejected the bind.
42
+ */
43
+ constructor(
44
+ /** Names the endpoint whose port is unavailable. */
45
+ endpoint,
46
+ /** Names the interface the endpoint attempted to bind. */
47
+ host,
48
+ /** Records the requested TCP port. */
49
+ port, cause) {
50
+ super("conflict", `${endpoint} endpoint cannot start: ${host}:${String(port)} is unavailable (${cause}). ` +
51
+ `Stop the process holding it, or set a different port in the Pager configuration.`);
52
+ this.endpoint = endpoint;
53
+ this.host = host;
54
+ this.port = port;
55
+ }
56
+ }
57
+ /**
58
+ * Start one loopback endpoint, reporting a port clash as a named startup failure.
59
+ * @param endpoint - Human-facing name used in the failure message.
60
+ * @param port - Requested port, where zero and undefined accept any free port.
61
+ * @param host - Requested interface.
62
+ * @param start - Operation that binds the endpoint.
63
+ * @returns - Started endpoint handle.
64
+ */
65
+ async function bindEndpoint(endpoint, port, host, start) {
66
+ try {
67
+ return await start();
68
+ }
69
+ catch (error) {
70
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
71
+ if (code !== "EADDRINUSE" && code !== "EACCES")
72
+ throw error;
73
+ throw new EndpointPortUnavailableError(endpoint, host, port ?? 0, code);
74
+ }
75
+ }
76
+ /** Runs one durable broker and its loopback endpoints under an exclusive process lock. */
77
+ export class BrokerService {
78
+ #now;
79
+ #report;
80
+ #scheduler;
81
+ #filesystem;
82
+ #databasePath;
83
+ #lockPath;
84
+ #lock;
85
+ #implementations;
86
+ #options;
87
+ #broker;
88
+ #mcp;
89
+ #snapshot;
90
+ #operatorScheduler;
91
+ #pushDelivery;
92
+ #operatorUpdates;
93
+ #operatorTransport;
94
+ #cancelSweep;
95
+ #unsubscribeCommits;
96
+ #sweepFloorMs = 0;
97
+ #abandoned = false;
98
+ #cancelWatchdog;
99
+ #onAbandoned;
100
+ #stopping;
101
+ /**
102
+ * Constructs a service with the durable database and adjacent lock-file locations.
103
+ * @param databasePath - Durable SQLite file owned by the service.
104
+ * @param implementations - Registry supplying implementation notification capabilities.
105
+ * @param options - Validated runtime settings and the ports the service runs on.
106
+ */
107
+ constructor(databasePath, implementations, options) {
108
+ this.#databasePath = databasePath;
109
+ this.#implementations = implementations;
110
+ this.#options = options;
111
+ this.#now = options.now;
112
+ this.#report = options.report;
113
+ this.#scheduler = options.scheduler;
114
+ this.#filesystem = options.filesystem;
115
+ this.#lockPath = options.lock.path;
116
+ this.#lock = options.lock;
117
+ }
118
+ /**
119
+ * Starts the exclusive broker service and returns its published loopback endpoints.
120
+ * @returns - Process ownership and endpoint details for the running service.
121
+ */
122
+ async start() {
123
+ this.#lock.acquire();
124
+ try {
125
+ if (this.#options.config && this.#options.storeDirectory) {
126
+ initializePagerStore({
127
+ config: this.#options.config,
128
+ filePath: this.#options.configPath ?? null,
129
+ storeDirectory: this.#options.storeDirectory,
130
+ });
131
+ }
132
+ else {
133
+ this.#filesystem.createDirectory(dirname(this.#databasePath));
134
+ }
135
+ const config = this.#options.config;
136
+ const now = this.#now;
137
+ this.#broker = this.#options.openBroker(this.#databasePath, {
138
+ now,
139
+ workspaceBranch: systemWorkspaceBranch,
140
+ report: this.#report,
141
+ ...(config
142
+ ? {
143
+ visibilityTimeoutMs: config.visibilityTimeoutMs,
144
+ redeliveryBaseDelayMs: config.redeliveryBaseDelayMs,
145
+ redeliveryGiveUpMs: config.redeliveryGiveUpMs,
146
+ reservationTtlMs: config.reservationTtlMs,
147
+ leaseTtlMs: config.leaseTtlMs,
148
+ perPairRateLimit: config.perPairRateLimit,
149
+ perPairRateWindowMs: config.perPairRateWindowMs,
150
+ maxInboxMessages: config.maxInboxMessages,
151
+ }
152
+ : {}),
153
+ });
154
+ const broker = this.#broker;
155
+ const notifications = new NotificationHub(broker.epoch(), {
156
+ onSubscriberError: (failure) => {
157
+ this.#report(`pager notification: a subscriber for ${failure.agentId} failed on event ${String(failure.sequence)}`);
158
+ },
159
+ });
160
+ const scheduler = new NotificationScheduler(broker, notifications, {
161
+ now,
162
+ ...(config ? { wakeBudget: config.wakeBudget, wakeBudgetWindowMs: config.wakeBudgetWindowMs } : {}),
163
+ });
164
+ this.#operatorScheduler = scheduler;
165
+ const implementations = this.#implementations;
166
+ const pushDelivery = new PushDeliveryService({
167
+ broker,
168
+ hub: notifications,
169
+ scheduler,
170
+ now,
171
+ capabilityFor: (id) => {
172
+ let capability;
173
+ try {
174
+ capability = implementations.resolve(implementationId(id)).notification;
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ return capability.supported ? capability : undefined;
180
+ },
181
+ report: this.#report,
182
+ });
183
+ this.#pushDelivery = pushDelivery;
184
+ broker.reapExpiredReservations();
185
+ const bindHost = config?.bindHost ?? LOOPBACK_HOST;
186
+ this.#mcp = await bindEndpoint("MCP", config?.mcpPort, bindHost, () => startMcpServer({
187
+ broker,
188
+ port: config?.mcpPort,
189
+ notifications: {
190
+ notifyMessage: scheduler.notifyMessage.bind(scheduler),
191
+ registerDeliveryAddress: (agentId, raw) => {
192
+ pushDelivery.registerAddress(agentId, raw);
193
+ },
194
+ forgetDeliveryAddress: (agentId) => {
195
+ pushDelivery.forget(agentId);
196
+ },
197
+ },
198
+ host: bindHost,
199
+ healthIdentity: currentBrokerServiceIdentity(),
200
+ }));
201
+ this.#snapshot = await bindEndpoint("snapshot", config?.snapshotPort, bindHost, () => startSnapshotServer(broker, config?.snapshotPort, bindHost));
202
+ await this.#startOperatorEndpoint();
203
+ const status = this.status();
204
+ this.#lock.publish({
205
+ ...currentBrokerServiceIdentity(),
206
+ pid: status.pid,
207
+ epoch: status.epoch,
208
+ mcpUrl: status.mcpUrl,
209
+ snapshotUrl: status.snapshotUrl,
210
+ ...(status.operatorUrl ? { operatorUrl: status.operatorUrl } : {}),
211
+ });
212
+ this.#unsubscribeCommits = broker.subscribeCommits((commit) => {
213
+ if (commit.tables === undefined || SWEEP_DEADLINE_TABLES.some((table) => commit.tables?.has(table))) {
214
+ this.#scheduleSweep();
215
+ }
216
+ });
217
+ this.#scheduleSweep();
218
+ this.#armAbandonmentWatchdog();
219
+ return status;
220
+ }
221
+ catch (error) {
222
+ // Teardown failures reach the report sink; the startup failure is the one the caller must see.
223
+ await this.stop().catch(() => undefined);
224
+ throw error;
225
+ }
226
+ }
227
+ /**
228
+ * Arm one sweep for the moment the broker says work could next exist.
229
+ *
230
+ * Rearmed after every sweep and after any commit that could move a deadline, so an idle broker
231
+ * holds no armed timer at all.
232
+ */
233
+ #scheduleSweep() {
234
+ this.#cancelSweep?.();
235
+ this.#cancelSweep = undefined;
236
+ const delay = this.#broker?.nextSweepDelayMs();
237
+ if (delay === undefined || delay === null) {
238
+ this.#sweepFloorMs = 0;
239
+ return;
240
+ }
241
+ this.#cancelSweep = this.#scheduler.once(() => {
242
+ // Both the sweep and the rearm read the database, and a timer callback has no caller to
243
+ // catch either. An unguarded throw here would take down the process.
244
+ try {
245
+ // Ahead of reapTerminalAgents: its own ephemeral-lapsed reap covers the same never-attached
246
+ // rows and would silently win the race, deleting the reservation before this call could
247
+ // notify the parent.
248
+ this.#broker?.reapExpiredReservations();
249
+ this.#broker?.reapTerminalAgents();
250
+ this.#redeliverDueInboxes();
251
+ this.#sweepFloorMs = 0;
252
+ this.#scheduleSweep();
253
+ }
254
+ catch (error) {
255
+ this.#report(`pager sweep: ${error instanceof Error ? error.message : String(error)}`);
256
+ this.#sweepFloorMs = SWEEP_FAILURE_RETRY_MS;
257
+ this.#retrySweepAfterFailure();
258
+ }
259
+ }, Math.max(delay, this.#sweepFloorMs));
260
+ }
261
+ /**
262
+ * Wake every recipient the broker's redelivery schedule now owes another notification.
263
+ *
264
+ * The broker owns the schedule and the scheduler owns the wake, so this is the only place the two
265
+ * meet: neither can reach the other.
266
+ */
267
+ #redeliverDueInboxes() {
268
+ const broker = this.#broker;
269
+ const scheduler = this.#operatorScheduler;
270
+ if (!broker || !scheduler)
271
+ return;
272
+ for (const agentId of broker.dueRedeliveryWakes()) {
273
+ // The schedule has already been advanced for every recipient in this list, so one that throws
274
+ // must not take the wakes owed to the others with it.
275
+ try {
276
+ if (scheduler.retryUnclaimed(agentId) !== null)
277
+ broker.recordRedeliveryAttempt(agentId);
278
+ }
279
+ catch (error) {
280
+ this.#report(`pager redelivery: ${error instanceof Error ? error.message : String(error)}`);
281
+ }
282
+ }
283
+ }
284
+ /**
285
+ * Rearm after a failed sweep without reading the database again.
286
+ *
287
+ * The failure may have been the read itself, so the retry is scheduled from the floor rather than
288
+ * from a deadline this service cannot currently obtain.
289
+ */
290
+ #retrySweepAfterFailure() {
291
+ this.#cancelSweep = this.#scheduler.once(() => {
292
+ this.#scheduleSweep();
293
+ }, SWEEP_FAILURE_RETRY_MS);
294
+ }
295
+ /**
296
+ * Returns the live service status after successful startup.
297
+ * @returns - Process ownership and endpoint details for the running service.
298
+ */
299
+ status() {
300
+ if (!this.#broker || !this.#mcp || !this.#snapshot)
301
+ throw new Error("Broker service is not running");
302
+ return {
303
+ pid: process.pid,
304
+ epoch: this.#broker.epoch(),
305
+ databasePath: this.#databasePath,
306
+ lockPath: this.#lockPath,
307
+ mcpUrl: this.#mcp.url,
308
+ snapshotUrl: this.#snapshot.url,
309
+ operatorUrl: this.#operatorTransport?.url ?? null,
310
+ };
311
+ }
312
+ /**
313
+ * Watch for the database disappearing from under this service.
314
+ *
315
+ * Armed last in `start()`, once every component it builds is assigned, so the stop a tick begins
316
+ * closes all of them; a start that fails before then stops what it bound itself. The owner is told
317
+ * before the stop rather than after it, so it can end the process whether or not the stop finishes.
318
+ */
319
+ #armAbandonmentWatchdog() {
320
+ if (this.#cancelWatchdog)
321
+ return;
322
+ this.#cancelWatchdog = this.#scheduler.recurring(() => {
323
+ if (this.#filesystem.exists(this.#databasePath))
324
+ return;
325
+ this.#cancelWatchdog?.();
326
+ this.#cancelWatchdog = undefined;
327
+ this.#abandoned = true;
328
+ this.#onAbandoned?.();
329
+ // Stop reports its own failures.
330
+ void this.stop().catch(() => undefined);
331
+ }, ABANDONMENT_WATCHDOG_INTERVAL_MS);
332
+ }
333
+ /**
334
+ * Register what to do once this service finds its database gone.
335
+ *
336
+ * A handler registered before abandonment runs before the stop that follows it; one registered after
337
+ * runs at once, while that stop may still be in flight. Stopping releases the endpoints but cannot
338
+ * end a process parked waiting for a signal, so the owner of that wait decides how the process ends.
339
+ * @param handler - Invoked once the database this service serves has gone.
340
+ */
341
+ onAbandoned(handler) {
342
+ this.#onAbandoned = handler;
343
+ // A handler can be registered after start() resolves, so abandonment can precede this
344
+ // registration. Without the latch the owner of the wait is never told and the process, having
345
+ // stopped serving, stays resident until it is killed.
346
+ if (this.#abandoned)
347
+ handler();
348
+ }
349
+ /**
350
+ * Stops endpoints, closes the database, and releases this process's lock.
351
+ *
352
+ * A call made while a stop is in flight joins it, so any caller's settled stop means teardown has
353
+ * finished. Every close is attempted and the lock is released whatever the others do. Each failing
354
+ * step reaches the report sink, and the first one rejects. Sidecar cleanup runs only once every
355
+ * connection to the database has closed, is best effort, and reports nothing.
356
+ * @returns - Settles once teardown has finished.
357
+ */
358
+ stop() {
359
+ this.#stopping ??= this.#stopOnce().finally(() => {
360
+ this.#stopping = undefined;
361
+ });
362
+ return this.#stopping;
363
+ }
364
+ async #stopOnce() {
365
+ const snapshot = this.#snapshot;
366
+ const mcp = this.#mcp;
367
+ const operatorTransport = this.#operatorTransport;
368
+ const operatorUpdates = this.#operatorUpdates;
369
+ const pushDelivery = this.#pushDelivery;
370
+ const broker = this.#broker;
371
+ const unsubscribeCommits = this.#unsubscribeCommits;
372
+ const cancelSweep = this.#cancelSweep;
373
+ const cancelWatchdog = this.#cancelWatchdog;
374
+ this.#snapshot = undefined;
375
+ this.#mcp = undefined;
376
+ this.#operatorTransport = undefined;
377
+ this.#operatorUpdates = undefined;
378
+ this.#pushDelivery = undefined;
379
+ this.#broker = undefined;
380
+ this.#unsubscribeCommits = undefined;
381
+ this.#cancelSweep = undefined;
382
+ this.#cancelWatchdog = undefined;
383
+ this.#operatorScheduler = undefined;
384
+ const failures = [];
385
+ const attempt = async (step) => {
386
+ try {
387
+ await step();
388
+ return true;
389
+ }
390
+ catch (error) {
391
+ failures.push(error);
392
+ this.#report(`pager stop: ${error instanceof Error ? error.message : String(error)}`);
393
+ return false;
394
+ }
395
+ };
396
+ await attempt(() => unsubscribeCommits?.());
397
+ await attempt(() => cancelSweep?.());
398
+ await attempt(() => cancelWatchdog?.());
399
+ await attempt(() => operatorTransport?.close());
400
+ const eventStoreClosed = await attempt(() => operatorUpdates?.close());
401
+ await attempt(() => snapshot?.close());
402
+ await attempt(() => mcp?.close());
403
+ await attempt(() => pushDelivery?.close());
404
+ // The sidecars are safe to remove only once every connection to the database has closed.
405
+ const brokerClosed = await attempt(() => broker?.close());
406
+ if (eventStoreClosed && brokerClosed)
407
+ removeCheckpointedSqliteSidecars(this.#databasePath, this.#filesystem);
408
+ await attempt(() => {
409
+ this.#lock.release();
410
+ });
411
+ if (failures.length > 0)
412
+ throw failures[0];
413
+ }
414
+ async #startOperatorEndpoint() {
415
+ const broker = this.#broker;
416
+ const scheduler = this.#operatorScheduler;
417
+ if (!broker || !scheduler)
418
+ return;
419
+ let store;
420
+ let updates;
421
+ try {
422
+ store = new SqliteOperatorEventStore(this.#databasePath, publicEpoch(broker.epoch()));
423
+ const descriptors = new Map();
424
+ for (const descriptor of this.#implementations.descriptors())
425
+ descriptors.set(descriptor.id, descriptor);
426
+ const operatorUpdates = new OperatorUpdateService({
427
+ source: broker,
428
+ store,
429
+ now: this.#now,
430
+ projection: {
431
+ ...createSystemRepositoryProjectionDependencies(),
432
+ implementations: {
433
+ find: (implementationId) => {
434
+ const descriptor = descriptors.get(implementationId);
435
+ return descriptor
436
+ ? {
437
+ displayName: descriptor.displayName,
438
+ metadata: { capabilities: [...descriptor.capabilities] },
439
+ }
440
+ : null;
441
+ },
442
+ },
443
+ },
444
+ });
445
+ updates = operatorUpdates;
446
+ const operatorPort = this.#options.config?.operatorPort ?? 0;
447
+ const operatorBindHost = this.#options.config?.bindHost ?? LOOPBACK_HOST;
448
+ this.#operatorTransport = await bindEndpoint("operator", operatorPort, operatorBindHost, () => startOperatorTransport(operatorUpdates, {
449
+ host: operatorBindHost,
450
+ port: operatorPort,
451
+ now: this.#now,
452
+ issueClientId: issueOperatorClientId,
453
+ scheduler: this.#scheduler,
454
+ commands: new OperatorCommandService({
455
+ broker,
456
+ scheduler,
457
+ issueControlId: () => randomBytes(CONTROL_ID_BYTES).toString("hex"),
458
+ }),
459
+ heartbeatIntervalMs: OPERATOR_HEARTBEAT_INTERVAL_MS,
460
+ livenessTimeoutMs: OPERATOR_LIVENESS_TIMEOUT_MS,
461
+ }));
462
+ this.#operatorUpdates = operatorUpdates;
463
+ }
464
+ catch (error) {
465
+ updates?.close();
466
+ if (!updates)
467
+ store?.close();
468
+ this.#operatorTransport = undefined;
469
+ this.#operatorUpdates = undefined;
470
+ // A port clash is an operator mistake with a fix; every other startup failure leaves the
471
+ // endpoint absent rather than taking durable messaging down with it.
472
+ if (error instanceof EndpointPortUnavailableError)
473
+ throw error;
474
+ this.#report(`pager operator endpoint unavailable: ${error instanceof Error ? error.message : String(error)}`);
475
+ }
476
+ }
477
+ }
478
+ function removeCheckpointedSqliteSidecars(databasePath, filesystem) {
479
+ const walPath = `${databasePath}-wal`;
480
+ try {
481
+ if (filesystem.exists(walPath) && filesystem.sizeOf(walPath) !== 0)
482
+ return;
483
+ if (filesystem.exists(walPath))
484
+ filesystem.remove(walPath);
485
+ const sharedMemoryPath = `${databasePath}-shm`;
486
+ if (filesystem.exists(sharedMemoryPath))
487
+ filesystem.remove(sharedMemoryPath);
488
+ }
489
+ catch {
490
+ // Cleanup is opportunistic.
491
+ }
492
+ }