@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,435 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { ProtocolError } from "../broker/protocol.js";
7
+ import { resolveBunExecutable } from "./runtime.js";
8
+ import { brokerLockHasCurrentIdentity, brokerLockIsLive, brokerLockPath, isCurrentBrokerServiceIdentity, processIsAlive, processStartedAt, readBrokerLock, startTimeStillMatches, readBrokerOwner, readBrokerOwnerPid, } from "./broker-lock.js";
9
+ /** Bounds one loopback endpoint probe, which either answers promptly or is treated as unreachable. */
10
+ export const BROKER_PROBE_TIMEOUT_MS = 2_000;
11
+ /**
12
+ * Marks a service whose standard error is a file the starting caller reads, and deletes once the
13
+ * service has started.
14
+ */
15
+ export const STARTUP_CAPTURE_ENVIRONMENT = "PAGER_STARTUP_CAPTURE";
16
+ /**
17
+ * Stop writing to a standard error that only the starting caller read, once startup has settled.
18
+ *
19
+ * The caller deletes the capture file, but the service keeps its descriptor for life, so any later
20
+ * write would fill a file nothing can read. `console.error` and `console.warn` are silenced too:
21
+ * under Bun 1.4.2 neither goes through `stderr.write`, so leaving them alone would still reach the
22
+ * deleted file's descriptor.
23
+ * @param environment - The service's environment, which says whether its standard error is a capture.
24
+ * @param stderr - The stream whose writes are dropped when it is.
25
+ * @param consoleLike - Console whose `error` and `warn` are dropped alongside `stderr`; defaults to
26
+ * the global console.
27
+ */
28
+ export function endStartupCapture(environment, stderr, consoleLike = console) {
29
+ if (environment[STARTUP_CAPTURE_ENVIRONMENT] === undefined)
30
+ return;
31
+ stderr.write = () => true;
32
+ consoleLike.error = () => undefined;
33
+ consoleLike.warn = () => undefined;
34
+ }
35
+ /** Delays the first retry while waiting for a starting service to answer. */
36
+ const ADOPTION_RETRY_DELAY_MS = 25;
37
+ /** Caps the growing delay between adoption probes, so a slow owner is not probed at full rate. */
38
+ const ADOPTION_RETRY_MAX_DELAY_MS = 1_000;
39
+ /** Named failure raised when no service publishes usable endpoints within the configured budget. */
40
+ export class ServiceStartupTimeoutError extends ProtocolError {
41
+ /**
42
+ * Construct the timeout reported to callers.
43
+ * @param output - What the still-running child wrote to standard error, appended when not blank.
44
+ */
45
+ constructor(output = "") {
46
+ const detail = startupFailureDetail(output);
47
+ super("validation_failed", `on-demand service startup timed out before endpoints were published${detail ? `: ${detail}` : ""}`);
48
+ this.name = "ServiceStartupTimeoutError";
49
+ }
50
+ }
51
+ /** Named failure raised when the spawned service ends before it publishes endpoints. */
52
+ export class ServiceStartupFailedError extends ProtocolError {
53
+ /**
54
+ * Construct a failure carrying the reason the service gave for ending.
55
+ * @param status - How the child ended.
56
+ * @param output - What the child wrote to standard error before it ended.
57
+ */
58
+ constructor(status, output) {
59
+ const ended = status.signal ? `was ended by ${status.signal}` : `exited with code ${String(status.code)}`;
60
+ const detail = startupFailureDetail(output);
61
+ super("validation_failed", `broker service ${ended} before publishing endpoints${detail ? `: ${detail}` : ""}`);
62
+ this.name = "ServiceStartupFailedError";
63
+ }
64
+ }
65
+ /** Named failure raised when the current-build service owns the lock but will not answer. */
66
+ export class BrokerServiceUnreachableError extends ProtocolError {
67
+ pid;
68
+ probes;
69
+ windowMs;
70
+ outcome;
71
+ /**
72
+ * Construct a failure that reports what the probes observed and nothing beyond it.
73
+ *
74
+ * A busy owner and a broken one produce the same probe result, so the message states the
75
+ * observation and leaves the operator a retry before any action that would stop a working service.
76
+ * @param pid - Process id recorded by the current-identity lock, still running when probing ended.
77
+ * @param probes - Number of readiness probes that were made and failed.
78
+ * @param windowMs - Elapsed time those probes covered, in milliseconds.
79
+ * @param outcome - Whether the probes went unanswered or were answered with a not-ready status.
80
+ */
81
+ constructor(
82
+ /** Identifies the running process that owns the database. */
83
+ pid,
84
+ /** Records how many probes failed before the budget ran out. */
85
+ probes,
86
+ /** Records the elapsed time those probes covered. */
87
+ windowMs,
88
+ /** Records which kind of failure the last probe saw. */
89
+ outcome) {
90
+ super("conflict", `broker service pid ${String(pid)} owns the database and is still running, but ${String(probes)} readiness ` +
91
+ `${probes === 1 ? "probe" : "probes"} over ${(windowMs / 1_000).toFixed(1)}s ` +
92
+ `${outcome === "unhealthy" ? "were answered with a not-ready status" : "got no answer from its loopback endpoints"}. ` +
93
+ `An owner that is merely busy fails these probes the same way a broken one does, so retry this command first. ` +
94
+ `If it keeps failing, inspect pid ${String(pid)} and confirm it is not serving other clients before stopping it.`);
95
+ this.pid = pid;
96
+ this.probes = probes;
97
+ this.windowMs = windowMs;
98
+ this.outcome = outcome;
99
+ this.name = "BrokerServiceUnreachableError";
100
+ }
101
+ }
102
+ /** Named failure raised when a signalled broker service is still running once the exit budget is spent. */
103
+ export class BrokerServiceExitTimeoutError extends ProtocolError {
104
+ /**
105
+ * Construct the failure reported when the owner ignores or outlasts its termination signal.
106
+ * @param pid - Process id of the owner that was signalled.
107
+ */
108
+ constructor(pid) {
109
+ super("validation_failed", `broker service pid ${String(pid)} was signalled to exit and is still running`);
110
+ this.name = "BrokerServiceExitTimeoutError";
111
+ }
112
+ }
113
+ /** Named failure raised when an upgrade encounters a live pre-identity service. */
114
+ export class LegacyBrokerServiceError extends ProtocolError {
115
+ /**
116
+ * Construct an actionable upgrade failure without terminating an arbitrary process.
117
+ * @param pid - Process id recorded by the incompatible lock.
118
+ */
119
+ constructor(pid) {
120
+ super("validation_failed", `legacy or incompatible broker service pid ${String(pid)} owns the database; run pager exit to stop it before retrying`);
121
+ this.name = "LegacyBrokerServiceError";
122
+ }
123
+ }
124
+ /**
125
+ * Return live endpoints from a lock only when its owning process still exists.
126
+ * @param lock - Candidate lock read beside the configured database.
127
+ * @param isAlive - Operating-system liveness boundary.
128
+ * @param startedAt - Reads a process start time, used to reject a recycled process id. Omit it to skip
129
+ * that check, which costs a subprocess on macOS.
130
+ * @returns - Complete service endpoints when the lock is live and fully published.
131
+ */
132
+ export function liveBrokerEndpoints(lock, isAlive, startedAt) {
133
+ if (!brokerLockIsLive(lock, isAlive) || !lock.mcpUrl || !lock.snapshotUrl)
134
+ return undefined;
135
+ if (!ownerStartTimeMatches(lock, startedAt))
136
+ return undefined;
137
+ return {
138
+ pid: lock.pid,
139
+ mcpUrl: lock.mcpUrl,
140
+ snapshotUrl: lock.snapshotUrl,
141
+ ...(lock.operatorUrl ? { operatorUrl: lock.operatorUrl } : {}),
142
+ };
143
+ }
144
+ /**
145
+ * Adopt a running broker service or start one detached and wait for its endpoints.
146
+ * @param environment - Resolved configuration shared by the caller and service process.
147
+ * @param overrides - Optional process and timing seams for isolated verification.
148
+ * @returns - Live loopback endpoints owned by the singleton service process.
149
+ */
150
+ export async function ensureBrokerService(environment, overrides = {}) {
151
+ const dependencies = { ...defaultDependencies(), ...overrides };
152
+ const lockPath = brokerLockPath(environment.dbPath);
153
+ const ownerPid = readBrokerOwnerPid(lockPath);
154
+ const candidate = readBrokerLock(lockPath);
155
+ if (ownerPid && dependencies.processIsAlive(ownerPid) && !brokerLockHasCurrentIdentity(candidate)) {
156
+ throw new LegacyBrokerServiceError(ownerPid);
157
+ }
158
+ const existing = liveBrokerEndpoints(candidate, dependencies.processIsAlive, dependencies.processStartedAt);
159
+ if (existing) {
160
+ // The lock already proved this is the current build, so a probe that misses is a slow or
161
+ // stalled service, never an incompatible one. Retry it before giving up on an owner that
162
+ // nothing else may replace.
163
+ const probeStart = dependencies.now();
164
+ const probeDeadline = probeStart + environment.config.serviceStartupTimeoutMs;
165
+ let probes = 0;
166
+ for (;;) {
167
+ const outcome = await dependencies.endpointsReady(existing);
168
+ probes += 1;
169
+ if (outcome === "ready")
170
+ return existing;
171
+ if (outcome === "mismatch")
172
+ throw new LegacyBrokerServiceError(existing.pid);
173
+ // The owner was running when its lock was read. An owner that has exited since is not an
174
+ // unreachable service to report, it is an absent one to replace.
175
+ if (!dependencies.processIsAlive(existing.pid))
176
+ break;
177
+ if (dependencies.now() > probeDeadline) {
178
+ throw new BrokerServiceUnreachableError(existing.pid, probes, dependencies.now() - probeStart, outcome);
179
+ }
180
+ await dependencies.wait(adoptionRetryDelayMs(probes));
181
+ }
182
+ }
183
+ const serviceCommand = dependencies.serviceCommand(environment);
184
+ const child = dependencies.spawnService(serviceCommand.command, serviceCommand.args, environment.processEnv);
185
+ child.unref();
186
+ const startedWaiting = dependencies.now();
187
+ const deadline = startedWaiting + environment.config.serviceStartupTimeoutMs;
188
+ let probes = 0;
189
+ let unanswered;
190
+ try {
191
+ while (dependencies.now() <= deadline) {
192
+ const loopOwnerPid = readBrokerOwnerPid(lockPath);
193
+ const loopCandidate = readBrokerLock(lockPath);
194
+ if (loopOwnerPid && dependencies.processIsAlive(loopOwnerPid) && !brokerLockHasCurrentIdentity(loopCandidate)) {
195
+ throw new LegacyBrokerServiceError(loopOwnerPid);
196
+ }
197
+ const endpoints = liveBrokerEndpoints(loopCandidate, dependencies.processIsAlive, dependencies.processStartedAt);
198
+ unanswered = undefined;
199
+ if (endpoints) {
200
+ const outcome = await dependencies.endpointsReady(endpoints);
201
+ probes += 1;
202
+ if (outcome === "ready") {
203
+ // Only the child's own service is described by what the child wrote. Another service that
204
+ // answered won the race to own the database, and the child's report of losing it describes
205
+ // a failure the caller did not suffer.
206
+ const output = child.startupOutput();
207
+ if (endpoints.pid === child.pid && output.trim())
208
+ dependencies.report(output);
209
+ return endpoints;
210
+ }
211
+ if (outcome === "mismatch")
212
+ throw new LegacyBrokerServiceError(endpoints.pid);
213
+ unanswered = { pid: endpoints.pid, outcome };
214
+ }
215
+ const status = child.exitStatus();
216
+ // A child that ended while another current-build service holds the lock lost the race to own
217
+ // the database. That service has not answered yet, and waiting for it is adoption; only a
218
+ // database with no live owner left is a failed start. The owner counts only when it started at
219
+ // the time its lock records, so a process that inherited a dead service's pid does not.
220
+ const peerIsStarting = loopOwnerPid !== undefined &&
221
+ loopOwnerPid !== child.pid &&
222
+ dependencies.processIsAlive(loopOwnerPid) &&
223
+ loopCandidate !== undefined &&
224
+ brokerLockHasCurrentIdentity(loopCandidate) &&
225
+ ownerStartTimeMatches(loopCandidate, dependencies.processStartedAt);
226
+ if (status && !peerIsStarting)
227
+ throw new ServiceStartupFailedError(status, child.startupOutput());
228
+ await dependencies.wait(ADOPTION_RETRY_DELAY_MS);
229
+ }
230
+ // A child that ended while a peer's published endpoints kept failing their probes is not a
231
+ // startup timeout of its own; the peer is the finding, as it is when it is found before spawning.
232
+ if (unanswered && child.exitStatus()) {
233
+ throw new BrokerServiceUnreachableError(unanswered.pid, probes, dependencies.now() - startedWaiting, unanswered.outcome);
234
+ }
235
+ throw new ServiceStartupTimeoutError(child.startupOutput());
236
+ }
237
+ catch (error) {
238
+ if (child.pid && dependencies.processIsAlive(child.pid))
239
+ child.kill("SIGTERM");
240
+ throw error;
241
+ }
242
+ finally {
243
+ child.releaseOutput();
244
+ }
245
+ }
246
+ /**
247
+ * Bounds waiting for a signalled owner to exit. It must exceed `SHUTDOWN_GRACE_MS` in `cli.ts`, the
248
+ * time a stopping service allows its endpoints to close before it exits on its own.
249
+ */
250
+ const BROKER_EXIT_TIMEOUT_MS = 10_000;
251
+ /** Spaces the liveness polls made while a signalled owner shuts down. */
252
+ const BROKER_EXIT_POLL_MS = 50;
253
+ /**
254
+ * Stop the broker service that owns the configured database, whichever build started it.
255
+ *
256
+ * A lock whose process id now belongs to a process started at a different time names no owner, so
257
+ * that process is left alone rather than signalled.
258
+ * @param environment - Resolved configuration locating the database whose owner is stopped.
259
+ * @param overrides - Optional process and timing seams for isolated verification.
260
+ * @returns - The owner that exited after being signalled, or `not_running` when there was none.
261
+ */
262
+ export async function stopBrokerService(environment, overrides = {}) {
263
+ const dependencies = {
264
+ processIsAlive,
265
+ processStartedAt,
266
+ signal: (pid, signal) => {
267
+ process.kill(pid, signal);
268
+ },
269
+ wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
270
+ now: Date.now,
271
+ ...overrides,
272
+ };
273
+ const owner = readBrokerOwner(brokerLockPath(environment.dbPath));
274
+ if (owner === undefined || !dependencies.processIsAlive(owner.pid))
275
+ return { state: "not_running" };
276
+ if (owner.startedAt !== undefined &&
277
+ !startTimeStillMatches(owner.startedAt, dependencies.processStartedAt(owner.pid)))
278
+ return { state: "not_running" };
279
+ try {
280
+ dependencies.signal(owner.pid, "SIGTERM");
281
+ }
282
+ catch (error) {
283
+ if (!dependencies.processIsAlive(owner.pid))
284
+ return { state: "stopped", pid: owner.pid };
285
+ throw error;
286
+ }
287
+ const deadline = dependencies.now() + BROKER_EXIT_TIMEOUT_MS;
288
+ while (dependencies.processIsAlive(owner.pid)) {
289
+ if (dependencies.now() > deadline)
290
+ throw new BrokerServiceExitTimeoutError(owner.pid);
291
+ await dependencies.wait(BROKER_EXIT_POLL_MS);
292
+ }
293
+ return { state: "stopped", pid: owner.pid };
294
+ }
295
+ /**
296
+ * Extract the reason from what a service wrote to standard error.
297
+ *
298
+ * `pager serve` reports a failed start as one JSON line carrying `error.message`; any other text is
299
+ * kept as written.
300
+ * @param output - Captured standard error of the service.
301
+ * @returns - The failure reason, or an empty string when nothing was written.
302
+ */
303
+ function startupFailureDetail(output) {
304
+ const lines = output
305
+ .split("\n")
306
+ .map((line) => line.trim())
307
+ .filter((line) => line !== "");
308
+ for (const line of lines.toReversed()) {
309
+ try {
310
+ const parsed = JSON.parse(line);
311
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
312
+ const { error } = parsed;
313
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string")
314
+ return error.message;
315
+ }
316
+ }
317
+ catch {
318
+ continue;
319
+ }
320
+ }
321
+ return lines.join(" ");
322
+ }
323
+ /**
324
+ * Start the detached service with its standard error captured in a file.
325
+ *
326
+ * A file rather than a pipe: the service outlives this process, and a pipe closed after startup
327
+ * would fail the service's later writes to standard error. The service stops writing to it once it
328
+ * has started, per {@link STARTUP_CAPTURE_ENVIRONMENT}, so the file removed afterwards does not grow.
329
+ * @param command - Executable to run.
330
+ * @param args - Arguments for the executable.
331
+ * @param environment - Environment the service runs with.
332
+ * @returns - The started child with its captured output.
333
+ */
334
+ function spawnCapturingStderr(command, args, environment) {
335
+ const directory = mkdtempSync(join(tmpdir(), "pager-start-"));
336
+ const path = join(directory, "stderr");
337
+ const descriptor = openSync(path, "a");
338
+ let child;
339
+ try {
340
+ child = spawn(command, args, {
341
+ detached: true,
342
+ stdio: ["ignore", "ignore", descriptor],
343
+ env: { ...environment, [STARTUP_CAPTURE_ENVIRONMENT]: "1" },
344
+ });
345
+ }
346
+ catch (error) {
347
+ rmSync(directory, { recursive: true, force: true });
348
+ throw error;
349
+ }
350
+ finally {
351
+ closeSync(descriptor);
352
+ }
353
+ return {
354
+ get pid() {
355
+ return child.pid;
356
+ },
357
+ unref: () => {
358
+ child.unref();
359
+ },
360
+ kill: (signal) => {
361
+ child.kill(signal);
362
+ },
363
+ exitStatus: () => child.exitCode === null && child.signalCode === null
364
+ ? undefined
365
+ : { code: child.exitCode, signal: child.signalCode },
366
+ startupOutput: () => {
367
+ try {
368
+ return readFileSync(path, "utf8");
369
+ }
370
+ catch {
371
+ return "";
372
+ }
373
+ },
374
+ releaseOutput: () => {
375
+ rmSync(directory, { recursive: true, force: true });
376
+ },
377
+ };
378
+ }
379
+ function defaultDependencies() {
380
+ return {
381
+ spawnService: spawnCapturingStderr,
382
+ report: (message) => {
383
+ process.stderr.write(message.endsWith("\n") ? message : `${message}\n`);
384
+ },
385
+ serviceCommand: (environment) => ({
386
+ command: resolveBunExecutable(environment.processEnv),
387
+ args: [fileURLToPath(new URL("./cli.js", import.meta.url)), "serve"],
388
+ }),
389
+ processIsAlive,
390
+ processStartedAt,
391
+ endpointsReady: async ({ mcpUrl, snapshotUrl }) => {
392
+ let health;
393
+ let snapshot;
394
+ try {
395
+ ;
396
+ [health, snapshot] = await Promise.all([
397
+ fetch(new URL("/health", mcpUrl), { signal: AbortSignal.timeout(BROKER_PROBE_TIMEOUT_MS) }),
398
+ fetch(new URL("/snapshot/health", snapshotUrl), { signal: AbortSignal.timeout(BROKER_PROBE_TIMEOUT_MS) }),
399
+ ]);
400
+ }
401
+ catch {
402
+ // A refused connection, a dropped socket and an expired probe budget arrive here alike, and
403
+ // none of them says whether the owner is broken or busy.
404
+ return "unreachable";
405
+ }
406
+ if (!health.ok || !snapshot.ok)
407
+ return "unhealthy";
408
+ let body;
409
+ try {
410
+ body = await health.text();
411
+ }
412
+ catch {
413
+ // The status line arrived and the body did not, which is the socket dropping mid-response.
414
+ // Reporting that as a not-ready answer would describe the owner as something it never said.
415
+ return "unreachable";
416
+ }
417
+ try {
418
+ return isCurrentBrokerServiceIdentity(JSON.parse(body)) ? "ready" : "mismatch";
419
+ }
420
+ catch {
421
+ return "unhealthy";
422
+ }
423
+ },
424
+ wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
425
+ now: Date.now,
426
+ };
427
+ }
428
+ function adoptionRetryDelayMs(probes) {
429
+ return Math.min(ADOPTION_RETRY_DELAY_MS * 2 ** (probes - 1), ADOPTION_RETRY_MAX_DELAY_MS);
430
+ }
431
+ function ownerStartTimeMatches(lock, startedAt) {
432
+ if (startedAt === undefined || lock.ownerStartedAt === undefined)
433
+ return true;
434
+ return startTimeStillMatches(lock.ownerStartedAt, startedAt(lock.pid));
435
+ }
@@ -0,0 +1,88 @@
1
+ import { Broker, type BrokerOptions } from "../broker/broker.js";
2
+ import { type ImplementationRegistry } from "../implementations/registry.js";
3
+ import { LaunchService, type LaunchBrokerPort, type TerminalProvider, type TerminalProviderModule } from "./launch.js";
4
+ import type { RuntimeEnvironment } from "./environment.js";
5
+ export { readRuntimeEnvironment, type RuntimeEnvironment } from "./environment.js";
6
+ /**
7
+ * Construct the broker from resolved runtime configuration.
8
+ * @param environment - Resolved Bun runtime configuration.
9
+ * @returns - Handle that reads the current broker epoch without rotating it, so it does not detach live agents.
10
+ */
11
+ export declare function createBroker(environment: RuntimeEnvironment): Broker;
12
+ /**
13
+ * Construct the broker `BrokerService` owns for the life of its process.
14
+ * @param databasePath - Durable SQLite file the broker owns.
15
+ * @param options - Broker construction options, including the root's clock and report sink.
16
+ * @returns - Broker instance satisfying the service's declared port.
17
+ */
18
+ export declare function openBroker(databasePath: string, options: BrokerOptions): Broker;
19
+ /**
20
+ * Discover every built-in implementation manifest without a shared product list.
21
+ * @returns - Registry of validated discovered implementations.
22
+ */
23
+ export declare function createImplementationRegistry(): Promise<ImplementationRegistry>;
24
+ /**
25
+ * Construct launch orchestration from discovered implementation and terminal providers.
26
+ * @param broker - Durable broker used by launch orchestration.
27
+ * @param terminalProviderId - Open terminal-provider identifier to resolve.
28
+ * @param environment - Resolved Bun runtime configuration.
29
+ * @param implementations - Registry resolving agent implementations.
30
+ * @returns - Launch service offering only the requested terminal provider.
31
+ */
32
+ export declare function createLaunchService(broker: LaunchBrokerPort, terminalProviderId: string, environment: RuntimeEnvironment, implementations: ImplementationRegistry): Promise<LaunchService>;
33
+ /**
34
+ * Compose an interactive launch service from conventionally discovered terminal providers.
35
+ * @param broker - Durable broker used by launch orchestration.
36
+ * @param environment - Resolved Bun runtime configuration.
37
+ * @param implementations - Registry resolving agent implementations.
38
+ * @param report - Receives each discovered terminal provider that was skipped, and why.
39
+ * @returns - Launch service offering only the selected provider, and that provider's name.
40
+ */
41
+ export declare function createInteractiveLaunchService(broker: LaunchBrokerPort, environment: RuntimeEnvironment, implementations: ImplementationRegistry, report: (message: string) => void): Promise<{
42
+ service: LaunchService;
43
+ terminalProvider: string;
44
+ }>;
45
+ /**
46
+ * Select the highest-priority available interactive provider or the inherited-terminal fallback.
47
+ * @param candidates - Conventionally discovered optional terminal providers.
48
+ * @param fallback - Foreground provider used when no optional provider is available.
49
+ * @returns - The available candidate with the highest priority, or the fallback when none is available.
50
+ */
51
+ export declare function selectInteractiveTerminalProvider(candidates: readonly TerminalProvider[], fallback: TerminalProvider): Promise<TerminalProvider>;
52
+ /** One `terminal.js` module discovery found, and whether importing it succeeded. */
53
+ export type DiscoveredTerminalModule = {
54
+ /** Path the module was imported from. */
55
+ readonly modulePath: string;
56
+ /** Import succeeded. */
57
+ readonly state: "loaded";
58
+ /** Namespace the import produced. */
59
+ readonly namespace: unknown;
60
+ } | {
61
+ /** Path the module was imported from. */
62
+ readonly modulePath: string;
63
+ /** Import threw. */
64
+ readonly state: "unloadable";
65
+ /** Value the import threw. */
66
+ readonly error: unknown;
67
+ };
68
+ /**
69
+ * Build the provider each discovered terminal module offers, skipping any that cannot build one.
70
+ *
71
+ * An interactive launch always has the foreground fallback, so one broken module is named and left
72
+ * out rather than failing every launch.
73
+ * @param modules - Discovered `terminal.js` modules, in discovery order.
74
+ * @param environment - Process environment every provider is built from.
75
+ * @param report - Receives one line per skipped module, naming it and the reason.
76
+ * @returns - Providers from every module whose factory built one, in module order.
77
+ */
78
+ export declare function adoptTerminalProviders(modules: readonly DiscoveredTerminalModule[], environment: NodeJS.ProcessEnv, report: (message: string) => void): TerminalProvider[];
79
+ /**
80
+ * Read a loaded `terminal.js` module as the port discovery expects, checking what its factory builds.
81
+ *
82
+ * The factory's parameter cannot be checked at runtime, so the provider it returns is, on every call.
83
+ * @param loaded - Module namespace a dynamic import produced.
84
+ * @param modulePath - Module path named in a rejection.
85
+ * @returns - The module's factory, or undefined when it exports none.
86
+ * @throws {Error} When the factory throws or builds something that is not a terminal provider, naming the module.
87
+ */
88
+ export declare function terminalProviderModule(loaded: unknown, modulePath: string): TerminalProviderModule | undefined;