@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,249 @@
1
+ import { uniquePathNames } from "../broker/address.js";
2
+ import { operatorDiagnostic } from "./diagnostics.js";
3
+ import { projectRepositoryLocation } from "./repository.js";
4
+ import { OPERATOR_ARRAY_LENGTH_LIMIT, validateOperatorAgent, validateOperatorRepository } from "./validation.js";
5
+ /** Reports an invalid or unbounded projection source. */
6
+ export class OperatorAgentProjectionError extends Error {
7
+ }
8
+ /**
9
+ * Project broker snapshot repositories and agents without changing broker identities.
10
+ * @param snapshot - Authoritative broker state containing durable identity and attachment facts.
11
+ * @param dependencies - Explicit repository and implementation collaborators.
12
+ * @returns Bounded repositories and agents suitable for an operator snapshot.
13
+ */
14
+ export function projectOperatorAgents(snapshot, dependencies) {
15
+ assertBounded(snapshot.workspaces.length, "workspace projection");
16
+ assertBounded(snapshot.agents.length, "agent projection");
17
+ const connections = snapshot.connections;
18
+ const locationsByWorkspace = new Map();
19
+ const rootPathsById = new Map();
20
+ const admitted = [];
21
+ for (const [index, workspace] of snapshot.workspaces.entries()) {
22
+ if (locationsByWorkspace.has(workspace.id)) {
23
+ throw new OperatorAgentProjectionError(`duplicate broker workspace identity: ${workspace.id}`);
24
+ }
25
+ const location = projectRepositoryLocation(workspace.path, dependencies);
26
+ const existing = rootPathsById.get(location.repository.id);
27
+ if (existing !== undefined && existing !== location.repository.rootPath) {
28
+ throw new OperatorAgentProjectionError(`repository identity collision: ${location.repository.id}`);
29
+ }
30
+ rootPathsById.set(location.repository.id, location.repository.rootPath);
31
+ admitted.push({ index, id: location.repository.id });
32
+ locationsByWorkspace.set(workspace.id, location);
33
+ }
34
+ const repositoryNames = uniquePathNames([...rootPathsById].map(([id, path]) => ({ id, path })));
35
+ const repositoriesById = new Map();
36
+ for (const { index, id } of admitted) {
37
+ try {
38
+ repositoriesById.set(id, validateOperatorRepository({
39
+ id,
40
+ rootPath: rootPathsById.get(id) ?? "",
41
+ name: repositoryNames.get(id) ?? id,
42
+ }));
43
+ }
44
+ catch (error) {
45
+ reportSkipped(dependencies, "repository", index, error);
46
+ }
47
+ }
48
+ const workspaceNames = uniquePathNames(snapshot.workspaces);
49
+ const connectionsByAgent = new Map(connections.map((connection) => [connection.agentId, connection]));
50
+ const agents = [];
51
+ for (const [index, agent] of snapshot.agents.entries()) {
52
+ const location = locationsByWorkspace.get(agent.workspaceId);
53
+ if (!location) {
54
+ reportSkipped(dependencies, "agent", index, new Error("agent references an unprojected broker workspace"));
55
+ continue;
56
+ }
57
+ try {
58
+ agents.push(validateOperatorAgent({
59
+ id: agent.id,
60
+ alias: bareAlias(agent.alias),
61
+ previousAliases: agent.previousAliases.map(bareAlias),
62
+ registeredAt: agent.createdAt,
63
+ lifecycle: agent.state,
64
+ archivedAt: agent.archivedAt,
65
+ workspaceAvailable: location.available,
66
+ workspace: workspaceNames.get(agent.workspaceId) ?? agent.workspaceId,
67
+ brokerIdentity: { teamId: agent.teamId, workspaceId: agent.workspaceId },
68
+ repository: { repositoryId: location.repository.id, worktree: location.worktree },
69
+ adapter: adapterFromConnection(connectionsByAgent.get(agent.id)),
70
+ implementation: projectImplementation(agent, dependencies.implementations),
71
+ }));
72
+ }
73
+ catch (error) {
74
+ reportSkipped(dependencies, "agent", index, error);
75
+ }
76
+ }
77
+ agents.sort((left, right) => left.registeredAt - right.registeredAt || compareStableText(left.id, right.id));
78
+ const repositories = [...repositoriesById.values()].sort((left, right) => compareStableText(left.rootPath, right.rootPath) || compareStableText(left.id, right.id));
79
+ assertBounded(repositories.length, "repository projection");
80
+ return { repositories, agents };
81
+ }
82
+ /**
83
+ * Strip the at-sign a broker alias is displayed with.
84
+ *
85
+ * The at-sign is a presentation pattern, so the operator contract carries the name without it.
86
+ * @param alias - Broker alias, which may carry a leading at-sign.
87
+ * @returns The alias without its at-sign.
88
+ */
89
+ function bareAlias(alias) {
90
+ return alias.replace(/^@/u, "");
91
+ }
92
+ /**
93
+ * Recover the three-state adapter contract from the snapshot's single connection encoding.
94
+ *
95
+ * `connections[].state` alone cannot distinguish a session that expired from one that was closed
96
+ * or never existed; `lastSeenAt` (the creation time of the open session the connection state is
97
+ * read from: the newest unexpired one, else the newest open one) does.
98
+ * @param connection - This agent's entry in the snapshot's connections list, if any.
99
+ * @returns Adapter state and last observation for the projected agent.
100
+ */
101
+ function adapterFromConnection(connection) {
102
+ if (!connection)
103
+ return { state: "disconnected", lastSeenAt: null };
104
+ // A connected connection always came from a session row, so its lastSeenAt is never null.
105
+ if (connection.state === "connected" && connection.lastSeenAt !== null)
106
+ return { state: "connected", lastSeenAt: connection.lastSeenAt };
107
+ if (connection.lastSeenAt !== null)
108
+ return { state: "expired", lastSeenAt: connection.lastSeenAt };
109
+ return { state: "disconnected", lastSeenAt: null };
110
+ }
111
+ function reportSkipped(dependencies, scope, index, error) {
112
+ dependencies.diagnostics?.report(operatorDiagnostic("broker", "warning", `${scope} projection`, `row ${String(index)} skipped: ${error instanceof Error ? error.message : "unknown projection failure"}`, (dependencies.now ?? Date.now)()));
113
+ }
114
+ /**
115
+ * Report why an agent belongs to the operator archive rather than the active list.
116
+ *
117
+ * Archive membership is not lifecycle alone: an agent whose workspace directory has been removed
118
+ * can no longer be worked in, so it archives while the broker still holds it active and routable.
119
+ * An operator-archived agent whose workspace is still there did not lose its workspace, so a surface
120
+ * naming the cause must be told which one it is. The causes can hold together, and the first that holds
121
+ * is reported, in this order: the operator's archive, then lifecycle, then a removed workspace. The
122
+ * operator's own action comes first because it is why they see the agent archived.
123
+ * @param agent - Projected agent whose archive cause is requested.
124
+ * @returns The first cause that holds, or null when the agent is still active.
125
+ */
126
+ export function operatorArchiveReason(agent) {
127
+ if (agent.archivedAt !== null)
128
+ return "operator";
129
+ if (agent.lifecycle !== "active")
130
+ return "lifecycle";
131
+ if (!agent.workspaceAvailable)
132
+ return "workspace_removed";
133
+ return null;
134
+ }
135
+ /**
136
+ * Decide whether an agent belongs to the operator archive rather than the active list.
137
+ * @param agent - Projected agent whose archive membership is requested.
138
+ * @returns True when the agent belongs in a repository's archived section.
139
+ */
140
+ export function isOperatorAgentArchived(agent) {
141
+ return operatorArchiveReason(agent) !== null;
142
+ }
143
+ /**
144
+ * Select the repositories whose every conversation is archived.
145
+ *
146
+ * A repository holding no agents at all is not archived: there is nothing in it that ended.
147
+ * @param agents - Projected agents across every repository.
148
+ * @returns - Identities of the repositories that are archived in their entirety.
149
+ */
150
+ export function archivedOperatorRepositoryIds(agents) {
151
+ const active = new Set();
152
+ const seen = new Set();
153
+ for (const agent of agents) {
154
+ seen.add(agent.repository.repositoryId);
155
+ if (!isOperatorAgentArchived(agent))
156
+ active.add(agent.repository.repositoryId);
157
+ }
158
+ return new Set([...seen].filter((id) => !active.has(id)));
159
+ }
160
+ /**
161
+ * Select one exhaustive health classification from authoritative agent and traffic facts.
162
+ * @param agent - Projected agent whose health is requested.
163
+ * @param traffic - Complete operator traffic collection for the snapshot.
164
+ * @returns The first classification that applies: archived, then disconnected (an expired connection
165
+ * counts), then the state of the agent's newest accepted delivery, where an expired delivery is neutral.
166
+ */
167
+ export function selectOperatorAgentHealth(agent, traffic) {
168
+ if (isOperatorAgentArchived(agent))
169
+ return { kind: "archived" };
170
+ switch (agent.adapter.state) {
171
+ case "disconnected":
172
+ case "expired":
173
+ return { kind: "connection_disconnected" };
174
+ case "connected":
175
+ break;
176
+ }
177
+ const acceptedSequences = new Set();
178
+ let newest;
179
+ for (const entry of traffic) {
180
+ if (entry.kind !== "accepted_delivery")
181
+ continue;
182
+ if (acceptedSequences.has(entry.acceptedSequence)) {
183
+ throw new OperatorAgentProjectionError(`duplicate accepted traffic sequence: ${String(entry.acceptedSequence)}`);
184
+ }
185
+ acceptedSequences.add(entry.acceptedSequence);
186
+ if (entry.fromAgentId !== agent.id && entry.toAgentId !== agent.id)
187
+ continue;
188
+ if (!newest || entry.acceptedSequence > newest.acceptedSequence)
189
+ newest = entry;
190
+ }
191
+ if (!newest)
192
+ return { kind: "neutral" };
193
+ switch (newest.state) {
194
+ case "queued":
195
+ return { kind: "delivery_queued" };
196
+ case "claimed":
197
+ return { kind: "delivery_claimed" };
198
+ case "failed":
199
+ case "dead_lettered":
200
+ return { kind: "delivery_failed" };
201
+ case "acknowledged":
202
+ return { kind: "delivery_acknowledged" };
203
+ case "expired":
204
+ return { kind: "neutral" };
205
+ }
206
+ }
207
+ function projectImplementation(agent, catalog) {
208
+ if (agent.implementation.id === null)
209
+ return { available: false, id: null, version: { known: false } };
210
+ const version = agent.implementation.version;
211
+ const descriptor = catalog.find(agent.implementation.id);
212
+ if (!descriptor)
213
+ return { available: false, id: agent.implementation.id, version };
214
+ return {
215
+ available: true,
216
+ id: agent.implementation.id,
217
+ displayName: descriptor.displayName,
218
+ version,
219
+ metadata: descriptor.metadata,
220
+ };
221
+ }
222
+ function assertBounded(length, label) {
223
+ if (length > OPERATOR_ARRAY_LENGTH_LIMIT) {
224
+ throw new OperatorAgentProjectionError(`${label} exceeds maximum array length of ${String(OPERATOR_ARRAY_LENGTH_LIMIT)}`);
225
+ }
226
+ }
227
+ function compareStableText(left, right) {
228
+ return left < right ? -1 : left > right ? 1 : 0;
229
+ }
230
+ /**
231
+ * Report whether the broker would still accept an expiry for one delivery.
232
+ *
233
+ * Expiry moves a message to a terminal state, so a delivery already in one is refused with a conflict.
234
+ * Offering the action anyway would put the operator's only feedback in the diagnostics log.
235
+ * @param message - Delivery the operator is being offered expiry for.
236
+ * @returns Whether expiry can still change this delivery's state.
237
+ */
238
+ export function isTuiExpirable(message) {
239
+ switch (message.state) {
240
+ case "queued":
241
+ case "claimed":
242
+ return true;
243
+ case "acknowledged":
244
+ case "expired":
245
+ case "dead_lettered":
246
+ case "failed":
247
+ return false;
248
+ }
249
+ }
@@ -0,0 +1,50 @@
1
+ import type { BrokerOperatorCommandAcceptance, BrokerOperatorCommandRecord } from "../broker/broker.js";
2
+ import { type AgentId } from "../broker/protocol.js";
3
+ import type { OperatorPingAdmission } from "../messaging/notifications.js";
4
+ import { type OperatorCommandResult } from "./protocol.js";
5
+ /** Supplies transactional command persistence without exposing broker storage to the operator boundary. */
6
+ export interface OperatorCommandBrokerPort {
7
+ /** Return a prior durable acceptance for command-id idempotency. */
8
+ operatorCommand(commandId: string): BrokerOperatorCommandRecord | null;
9
+ /** Persist ping acceptance and audit before a wake becomes observable. */
10
+ acceptOperatorPing(commandId: string, targetAgentId: AgentId, controlId: string): BrokerOperatorCommandAcceptance;
11
+ /** Expire a message and persist command acceptance in one transaction. */
12
+ acceptOperatorExpiry(commandId: string, messageId: string): BrokerOperatorCommandAcceptance;
13
+ /** Mark an agent archived and persist command acceptance in one transaction, ending its attachments but leaving its inbox and addressability intact. */
14
+ acceptOperatorArchive(commandId: string, targetAgentId: AgentId): BrokerOperatorCommandAcceptance;
15
+ }
16
+ /** Supplies non-reentrant notification admission around durable ping acceptance. */
17
+ export interface OperatorPingSchedulerPort {
18
+ /**
19
+ * Preflight a control wake, run durable acceptance, then publish or retain it.
20
+ * @param agentId - Broker target of the body-free control.
21
+ * @param controlId - Opaque durable correlation.
22
+ * @param commitAcceptance - Synchronous transaction callback classifying creation or an exact durable retry.
23
+ * @returns Scheduler-boundary admission without an adapter-delivery claim.
24
+ */
25
+ notifyOperatorPing(agentId: AgentId, controlId: string, commitAcceptance: () => "created" | "existing", resolveExisting: () => "absent" | "existing"): OperatorPingAdmission;
26
+ }
27
+ /** Explicit collaborators used by the renderer-neutral operator command boundary. */
28
+ export type OperatorCommandDependencies = {
29
+ /** Owns durable command and traffic state. */
30
+ broker: OperatorCommandBrokerPort;
31
+ /** Owns body-free notification admission. */
32
+ scheduler: OperatorPingSchedulerPort;
33
+ /** Issues bounded opaque control correlations. */
34
+ issueControlId: () => string;
35
+ };
36
+ /** Admits validated operator commands without treating their submissions as authoritative state. */
37
+ export declare class OperatorCommandService {
38
+ #private;
39
+ /**
40
+ * Construct a command boundary from explicit durable, scheduler, and correlation collaborators.
41
+ * @param dependencies - Collaborators selected by the production composition root.
42
+ */
43
+ constructor(dependencies: OperatorCommandDependencies);
44
+ /**
45
+ * Validate and admit one untrusted operator command envelope.
46
+ * @param value - Candidate request received at the operator boundary.
47
+ * @returns Correlated acceptance or stable pre-acceptance rejection.
48
+ */
49
+ execute(value: unknown): OperatorCommandResult;
50
+ }
@@ -0,0 +1,199 @@
1
+ import { ProtocolError } from "../broker/protocol.js";
2
+ import { OPERATOR_PROTOCOL_VERSION, } from "./protocol.js";
3
+ import { validateOperatorCommand, validateOperatorCommandResult, validateOperatorControlId } from "./validation.js";
4
+ /** Admits validated operator commands without treating their submissions as authoritative state. */
5
+ export class OperatorCommandService {
6
+ #broker;
7
+ #scheduler;
8
+ #issueControlId;
9
+ /**
10
+ * Construct a command boundary from explicit durable, scheduler, and correlation collaborators.
11
+ * @param dependencies - Collaborators selected by the production composition root.
12
+ */
13
+ constructor(dependencies) {
14
+ this.#broker = dependencies.broker;
15
+ this.#scheduler = dependencies.scheduler;
16
+ this.#issueControlId = dependencies.issueControlId;
17
+ }
18
+ /**
19
+ * Validate and admit one untrusted operator command envelope.
20
+ * @param value - Candidate request received at the operator boundary.
21
+ * @returns Correlated acceptance or stable pre-acceptance rejection.
22
+ */
23
+ execute(value) {
24
+ const command = validateOperatorCommand(value);
25
+ try {
26
+ const prior = this.#broker.operatorCommand(command.commandId);
27
+ if (prior)
28
+ return this.#priorResult(command, prior);
29
+ }
30
+ catch (error) {
31
+ return rejected(command.commandId, rejectionReason(error));
32
+ }
33
+ switch (command.command.type) {
34
+ case "ping_agent":
35
+ return this.#ping({ ...command, command: command.command });
36
+ case "expire_message":
37
+ return this.#expire({ ...command, command: command.command });
38
+ case "archive_agent":
39
+ return this.#archive({ ...command, command: command.command });
40
+ }
41
+ }
42
+ #ping(command) {
43
+ const targetAgentId = command.command.targetAgentId;
44
+ let acceptedResult;
45
+ try {
46
+ const controlId = validateOperatorControlId(this.#issueControlId());
47
+ const admission = this.#scheduler.notifyOperatorPing(targetAgentId, controlId, () => {
48
+ const acceptance = this.#broker.acceptOperatorPing(command.commandId, targetAgentId, controlId);
49
+ assertPingAcceptance(acceptance, command.commandId, targetAgentId, controlId);
50
+ const result = pingAccepted(command.commandId, acceptance.record);
51
+ acceptedResult = result;
52
+ return acceptance.status;
53
+ }, () => {
54
+ const prior = this.#broker.operatorCommand(command.commandId);
55
+ if (!prior)
56
+ return "absent";
57
+ const result = this.#priorResult(command, prior);
58
+ acceptedResult = result;
59
+ return "existing";
60
+ });
61
+ if (acceptedResult)
62
+ return acceptedResult;
63
+ if (admission.status === "budget_rejected")
64
+ return rejected(command.commandId, "wake_budget");
65
+ if (admission.status === "correlation_rejected")
66
+ return rejected(command.commandId, admission.reason);
67
+ throw new Error("Scheduler accepted a ping without durable command acceptance");
68
+ }
69
+ catch (error) {
70
+ if (acceptedResult)
71
+ return acceptedResult;
72
+ return rejected(command.commandId, rejectionReason(error));
73
+ }
74
+ }
75
+ #expire(command) {
76
+ try {
77
+ const acceptance = this.#broker.acceptOperatorExpiry(command.commandId, command.command.messageId);
78
+ assertExpiryAcceptance(acceptance, command.commandId, command.command.messageId);
79
+ const accepted = acceptance.record;
80
+ return validateOperatorCommandResult({
81
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
82
+ type: "expiry_accepted",
83
+ status: "accepted",
84
+ commandId: command.commandId,
85
+ acceptedAt: accepted.acceptedAt,
86
+ });
87
+ }
88
+ catch (error) {
89
+ return rejected(command.commandId, rejectionReason(error));
90
+ }
91
+ }
92
+ #archive(command) {
93
+ try {
94
+ const acceptance = this.#broker.acceptOperatorArchive(command.commandId, command.command.targetAgentId);
95
+ const accepted = acceptance.record;
96
+ if (accepted.type !== "archive_agent" || accepted.commandId !== command.commandId) {
97
+ throw new Error("Broker returned a mismatched archive acceptance");
98
+ }
99
+ return validateOperatorCommandResult({
100
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
101
+ type: "archive_accepted",
102
+ status: "accepted",
103
+ commandId: command.commandId,
104
+ acceptedAt: accepted.acceptedAt,
105
+ });
106
+ }
107
+ catch (error) {
108
+ return rejected(command.commandId, rejectionReason(error));
109
+ }
110
+ }
111
+ #priorResult(command, prior) {
112
+ const expectedTarget = command.command.type === "expire_message" ? command.command.messageId : command.command.targetAgentId;
113
+ if (prior.commandId !== command.commandId)
114
+ throw new Error("Broker returned a mismatched command lookup");
115
+ if (prior.type !== command.command.type || prior.targetId !== expectedTarget) {
116
+ return rejected(command.commandId, "command_id_conflict");
117
+ }
118
+ if (prior.type === "ping_agent")
119
+ validateOperatorControlId(prior.controlId);
120
+ if (prior.type === "ping_agent")
121
+ return validateOperatorCommandResult({
122
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
123
+ type: "ping_accepted",
124
+ status: "accepted",
125
+ commandId: prior.commandId,
126
+ acceptedAt: prior.acceptedAt,
127
+ controlId: prior.controlId,
128
+ });
129
+ return validateOperatorCommandResult({
130
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
131
+ type: prior.type === "expire_message" ? "expiry_accepted" : "archive_accepted",
132
+ status: "accepted",
133
+ commandId: prior.commandId,
134
+ acceptedAt: prior.acceptedAt,
135
+ });
136
+ }
137
+ }
138
+ function pingAccepted(commandId, accepted) {
139
+ if (accepted.type !== "ping_agent" || accepted.commandId !== commandId)
140
+ throw new Error("Broker returned the wrong ping command acceptance");
141
+ return validateOperatorCommandResult({
142
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
143
+ type: "ping_accepted",
144
+ status: "accepted",
145
+ commandId,
146
+ acceptedAt: accepted.acceptedAt,
147
+ controlId: accepted.controlId,
148
+ });
149
+ }
150
+ function assertPingAcceptance(acceptance, commandId, targetAgentId, controlId) {
151
+ const { record, status } = acceptance;
152
+ assertAcceptanceStatus(status);
153
+ if (record.type !== "ping_agent" ||
154
+ record.commandId !== commandId ||
155
+ record.targetId !== targetAgentId ||
156
+ (status === "created" && record.controlId !== controlId)) {
157
+ throw new Error("Broker returned a mismatched ping command acceptance");
158
+ }
159
+ validateOperatorControlId(record.controlId);
160
+ }
161
+ function assertExpiryAcceptance(acceptance, commandId, messageId) {
162
+ const { record, status } = acceptance;
163
+ assertAcceptanceStatus(status);
164
+ if (record.type !== "expire_message" || record.commandId !== commandId || record.targetId !== messageId) {
165
+ throw new Error("Broker returned a mismatched expiry command acceptance");
166
+ }
167
+ }
168
+ function assertAcceptanceStatus(value) {
169
+ if (value !== "created" && value !== "existing") {
170
+ throw new Error("Broker returned an invalid command acceptance status");
171
+ }
172
+ }
173
+ function rejected(commandId, reason) {
174
+ return validateOperatorCommandResult({
175
+ protocolVersion: OPERATOR_PROTOCOL_VERSION,
176
+ type: "command_rejected",
177
+ status: "rejected",
178
+ commandId,
179
+ reason,
180
+ });
181
+ }
182
+ function rejectionReason(error) {
183
+ if (error instanceof ProtocolError) {
184
+ switch (error.code) {
185
+ case "validation_failed":
186
+ case "expired":
187
+ return "target_unavailable";
188
+ case "conflict":
189
+ return "conflict";
190
+ case "unsupported_version":
191
+ case "oversized_content":
192
+ case "unknown_recipient":
193
+ case "recipient_alias_changed":
194
+ case "alias_conflict":
195
+ return "rejected";
196
+ }
197
+ }
198
+ return "internal_error";
199
+ }
@@ -0,0 +1,21 @@
1
+ import { type OperatorDiagnostic, type OperatorDiagnosticSeverity } from "./protocol.js";
2
+ /** Receives operator-visible conditions without deciding how they are presented or transported. */
3
+ export interface OperatorDiagnosticSink {
4
+ /**
5
+ * Record one condition.
6
+ * @param diagnostic - Content-free description of what was observed.
7
+ */
8
+ report(diagnostic: OperatorDiagnostic): void;
9
+ }
10
+ /** Discards every diagnostic, for callers that do not surface them. */
11
+ export declare const SILENT_OPERATOR_DIAGNOSTICS: OperatorDiagnosticSink;
12
+ /**
13
+ * Build one diagnostic whose text cannot exceed the bounded contract length.
14
+ * @param origin - Whether the broker reported the condition or the client observed it.
15
+ * @param severity - Attention the condition asks of an operator.
16
+ * @param scope - Subsystem the condition arose in.
17
+ * @param summary - Operator-facing statement carrying no message or identity content.
18
+ * @param at - Epoch milliseconds the condition was observed.
19
+ * @returns A diagnostic ready for the operator contract.
20
+ */
21
+ export declare function operatorDiagnostic(origin: OperatorDiagnostic["origin"], severity: OperatorDiagnosticSeverity, scope: string, summary: string, at: number): OperatorDiagnostic;
@@ -0,0 +1,22 @@
1
+ import { OPERATOR_DIAGNOSTIC_TEXT_LIMIT } from "./protocol.js";
2
+ /** Discards every diagnostic, for callers that do not surface them. */
3
+ export const SILENT_OPERATOR_DIAGNOSTICS = { report: () => undefined };
4
+ /**
5
+ * Build one diagnostic whose text cannot exceed the bounded contract length.
6
+ * @param origin - Whether the broker reported the condition or the client observed it.
7
+ * @param severity - Attention the condition asks of an operator.
8
+ * @param scope - Subsystem the condition arose in.
9
+ * @param summary - Operator-facing statement carrying no message or identity content.
10
+ * @param at - Epoch milliseconds the condition was observed.
11
+ * @returns A diagnostic ready for the operator contract.
12
+ */
13
+ export function operatorDiagnostic(origin, severity, scope, summary, at) {
14
+ return { at, origin, severity, scope: clamp(scope), summary: clamp(summary) };
15
+ }
16
+ function clamp(text) {
17
+ const collapsed = text.replaceAll(/\s+/gu, " ").trim();
18
+ const bounded = collapsed.length > OPERATOR_DIAGNOSTIC_TEXT_LIMIT
19
+ ? `${collapsed.slice(0, OPERATOR_DIAGNOSTIC_TEXT_LIMIT - 1)}…`
20
+ : collapsed;
21
+ return bounded === "" ? "unspecified" : bounded;
22
+ }