@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/src/broker/address.d.ts +49 -0
- package/dist/src/broker/address.js +110 -0
- package/dist/src/broker/broker.d.ts +665 -0
- package/dist/src/broker/broker.js +2270 -0
- package/dist/src/broker/database.d.ts +81 -0
- package/dist/src/broker/database.js +69 -0
- package/dist/src/broker/operator-projection.d.ts +291 -0
- package/dist/src/broker/operator-projection.js +722 -0
- package/dist/src/broker/protocol.d.ts +237 -0
- package/dist/src/broker/protocol.js +311 -0
- package/dist/src/broker/schema.d.ts +94 -0
- package/dist/src/broker/schema.js +454 -0
- package/dist/src/broker/snapshot.d.ts +171 -0
- package/dist/src/broker/snapshot.js +78 -0
- package/dist/src/configuration-defaults.d.ts +64 -0
- package/dist/src/configuration-defaults.js +48 -0
- package/dist/src/configuration.d.ts +76 -0
- package/dist/src/configuration.js +156 -0
- package/dist/src/implementations/claude/environment.d.ts +8 -0
- package/dist/src/implementations/claude/environment.js +9 -0
- package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
- package/dist/src/implementations/claude/gateway-cli.js +136 -0
- package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
- package/dist/src/implementations/claude/gateway-identity.js +103 -0
- package/dist/src/implementations/claude/gateway.d.ts +148 -0
- package/dist/src/implementations/claude/gateway.js +236 -0
- package/dist/src/implementations/claude/instructions.d.ts +33 -0
- package/dist/src/implementations/claude/instructions.js +64 -0
- package/dist/src/implementations/claude/manifest.d.ts +33 -0
- package/dist/src/implementations/claude/manifest.js +108 -0
- package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
- package/dist/src/implementations/claude/plugin-directory.js +137 -0
- package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
- package/dist/src/implementations/claude/session-continuation.js +125 -0
- package/dist/src/implementations/claude/session-identity.d.ts +25 -0
- package/dist/src/implementations/claude/session-identity.js +31 -0
- package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
- package/dist/src/implementations/claude/session-start-cli.js +16 -0
- package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
- package/dist/src/implementations/claude/socket-delivery.js +159 -0
- package/dist/src/implementations/model.d.ts +162 -0
- package/dist/src/implementations/model.js +1 -0
- package/dist/src/implementations/opencode/adapter.d.ts +71 -0
- package/dist/src/implementations/opencode/adapter.js +94 -0
- package/dist/src/implementations/opencode/environment.d.ts +8 -0
- package/dist/src/implementations/opencode/environment.js +9 -0
- package/dist/src/implementations/opencode/manifest.d.ts +30 -0
- package/dist/src/implementations/opencode/manifest.js +93 -0
- package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
- package/dist/src/implementations/opencode/plugin-api.js +1 -0
- package/dist/src/implementations/opencode/plugin.d.ts +38 -0
- package/dist/src/implementations/opencode/plugin.js +303 -0
- package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
- package/dist/src/implementations/opencode/self-close-tool.js +31 -0
- package/dist/src/implementations/orca/terminal.d.ts +48 -0
- package/dist/src/implementations/orca/terminal.js +154 -0
- package/dist/src/implementations/pi/broker-client.d.ts +56 -0
- package/dist/src/implementations/pi/broker-client.js +156 -0
- package/dist/src/implementations/pi/environment.d.ts +8 -0
- package/dist/src/implementations/pi/environment.js +9 -0
- package/dist/src/implementations/pi/extension.d.ts +57 -0
- package/dist/src/implementations/pi/extension.js +253 -0
- package/dist/src/implementations/pi/host-slots.d.ts +18 -0
- package/dist/src/implementations/pi/host-slots.js +27 -0
- package/dist/src/implementations/pi/manifest.d.ts +11 -0
- package/dist/src/implementations/pi/manifest.js +61 -0
- package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
- package/dist/src/implementations/pi/self-close-tool.js +27 -0
- package/dist/src/implementations/registry.d.ts +47 -0
- package/dist/src/implementations/registry.js +157 -0
- package/dist/src/implementations/session-key.d.ts +13 -0
- package/dist/src/implementations/session-key.js +19 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +26 -0
- package/dist/src/launch/broker-lock.d.ts +257 -0
- package/dist/src/launch/broker-lock.js +644 -0
- package/dist/src/launch/cli.d.ts +37 -0
- package/dist/src/launch/cli.js +317 -0
- package/dist/src/launch/environment.d.ts +45 -0
- package/dist/src/launch/environment.js +48 -0
- package/dist/src/launch/launch.d.ts +235 -0
- package/dist/src/launch/launch.js +223 -0
- package/dist/src/launch/on-demand.d.ts +202 -0
- package/dist/src/launch/on-demand.js +435 -0
- package/dist/src/launch/runtime-composition.d.ts +88 -0
- package/dist/src/launch/runtime-composition.js +253 -0
- package/dist/src/launch/runtime.d.ts +79 -0
- package/dist/src/launch/runtime.js +196 -0
- package/dist/src/launch/service-system.d.ts +11 -0
- package/dist/src/launch/service-system.js +39 -0
- package/dist/src/launch/service.d.ts +173 -0
- package/dist/src/launch/service.js +492 -0
- package/dist/src/launch/skill.d.ts +57 -0
- package/dist/src/launch/skill.js +81 -0
- package/dist/src/launch/terminal.d.ts +26 -0
- package/dist/src/launch/terminal.js +43 -0
- package/dist/src/launch/workspace-branch.d.ts +10 -0
- package/dist/src/launch/workspace-branch.js +29 -0
- package/dist/src/messaging/delivery-client.d.ts +144 -0
- package/dist/src/messaging/delivery-client.js +261 -0
- package/dist/src/messaging/delivery.d.ts +75 -0
- package/dist/src/messaging/delivery.js +22 -0
- package/dist/src/messaging/inbox-socket.d.ts +98 -0
- package/dist/src/messaging/inbox-socket.js +278 -0
- package/dist/src/messaging/mcp.d.ts +53 -0
- package/dist/src/messaging/mcp.js +501 -0
- package/dist/src/messaging/notifications.d.ts +231 -0
- package/dist/src/messaging/notifications.js +489 -0
- package/dist/src/messaging/push-delivery.d.ts +77 -0
- package/dist/src/messaging/push-delivery.js +187 -0
- package/dist/src/messaging/tools.d.ts +11 -0
- package/dist/src/messaging/tools.js +102 -0
- package/dist/src/messaging/wake.d.ts +23 -0
- package/dist/src/messaging/wake.js +39 -0
- package/dist/src/operator/agent-projection.d.ts +102 -0
- package/dist/src/operator/agent-projection.js +249 -0
- package/dist/src/operator/commands.d.ts +50 -0
- package/dist/src/operator/commands.js +199 -0
- package/dist/src/operator/diagnostics.d.ts +21 -0
- package/dist/src/operator/diagnostics.js +22 -0
- package/dist/src/operator/protocol.d.ts +511 -0
- package/dist/src/operator/protocol.js +49 -0
- package/dist/src/operator/repository-system.d.ts +14 -0
- package/dist/src/operator/repository-system.js +150 -0
- package/dist/src/operator/repository.d.ts +79 -0
- package/dist/src/operator/repository.js +72 -0
- package/dist/src/operator/traffic-projection.d.ts +19 -0
- package/dist/src/operator/traffic-projection.js +229 -0
- package/dist/src/operator/transport.d.ts +87 -0
- package/dist/src/operator/transport.js +549 -0
- package/dist/src/operator/update-store.d.ts +53 -0
- package/dist/src/operator/update-store.js +162 -0
- package/dist/src/operator/updates.d.ts +94 -0
- package/dist/src/operator/updates.js +280 -0
- package/dist/src/operator/validation.d.ts +114 -0
- package/dist/src/operator/validation.js +916 -0
- package/dist/src/shared/guards.d.ts +59 -0
- package/dist/src/shared/guards.js +119 -0
- package/dist/src/shared/http.d.ts +30 -0
- package/dist/src/shared/http.js +50 -0
- package/dist/src/shared/row.d.ts +2 -0
- package/dist/src/shared/row.js +1 -0
- package/dist/src/shared/shell.d.ts +6 -0
- package/dist/src/shared/shell.js +8 -0
- package/dist/src/tui/actions.d.ts +166 -0
- package/dist/src/tui/actions.js +253 -0
- package/dist/src/tui/agent-tree.d.ts +125 -0
- package/dist/src/tui/agent-tree.js +663 -0
- package/dist/src/tui/app.d.ts +38 -0
- package/dist/src/tui/app.js +378 -0
- package/dist/src/tui/chrome.d.ts +86 -0
- package/dist/src/tui/chrome.js +85 -0
- package/dist/src/tui/client.d.ts +41 -0
- package/dist/src/tui/client.js +279 -0
- package/dist/src/tui/composition.d.ts +41 -0
- package/dist/src/tui/composition.js +146 -0
- package/dist/src/tui/message-views.d.ts +94 -0
- package/dist/src/tui/message-views.js +515 -0
- package/dist/src/tui/model.d.ts +414 -0
- package/dist/src/tui/model.js +758 -0
- package/dist/src/tui/presentation.d.ts +350 -0
- package/dist/src/tui/presentation.js +541 -0
- package/dist/src/tui/production.d.ts +79 -0
- package/dist/src/tui/production.js +253 -0
- package/dist/src/tui/run-terminal.d.ts +32 -0
- package/dist/src/tui/run-terminal.js +54 -0
- package/dist/src/tui/runtime.d.ts +113 -0
- package/dist/src/tui/runtime.js +189 -0
- package/dist/src/tui/theme.d.ts +49 -0
- package/dist/src/tui/theme.js +55 -0
- package/dist/src/tui/transport.d.ts +31 -0
- package/dist/src/tui/transport.js +255 -0
- package/dist/src/version.d.ts +11 -0
- package/dist/src/version.js +52 -0
- package/package.json +142 -0
- package/skills/pager/SKILL.md +112 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { issueOpaqueToken, SCHEMA_MARKER } from "./protocol.js";
|
|
2
|
+
/** The partial index over messages whose `sequence` holds no orderable value. */
|
|
3
|
+
export const MESSAGE_UNORDERABLE_POSITION = {
|
|
4
|
+
name: "message_unorderable_position",
|
|
5
|
+
table: "messages",
|
|
6
|
+
column: "id",
|
|
7
|
+
position: "sequence",
|
|
8
|
+
};
|
|
9
|
+
/** The partial index over ping audits whose `traffic_position` holds no orderable value. */
|
|
10
|
+
export const PING_UNORDERABLE_POSITION = {
|
|
11
|
+
name: "ping_unorderable_position",
|
|
12
|
+
table: "operator_ping_audits",
|
|
13
|
+
column: "audit_id",
|
|
14
|
+
position: "traffic_position",
|
|
15
|
+
};
|
|
16
|
+
/** Every index this build creates over rows whose durable position it cannot order. */
|
|
17
|
+
export const UNORDERABLE_POSITION_INDEXES = [
|
|
18
|
+
MESSAGE_UNORDERABLE_POSITION,
|
|
19
|
+
PING_UNORDERABLE_POSITION,
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Render the partial index over the rows of one table whose position column holds no orderable value.
|
|
23
|
+
* @param index - Index being declared.
|
|
24
|
+
* @returns - The `create index` statement for it.
|
|
25
|
+
*/
|
|
26
|
+
function unorderablePositionIndex(index) {
|
|
27
|
+
return `create index if not exists ${index.name} on ${index.table}(${index.column}) where ${unorderablePosition(index.position)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Render the condition a stored traffic position meets when it can be ordered and projected.
|
|
31
|
+
* @param column - Position column the condition tests.
|
|
32
|
+
* @returns - SQL condition true for a positive safe integer.
|
|
33
|
+
*/
|
|
34
|
+
export function orderablePosition(column) {
|
|
35
|
+
return `typeof(${column}) = 'integer' and ${column} between 1 and ${String(Number.MAX_SAFE_INTEGER)}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render the condition a stored traffic position meets when it cannot be ordered: the complement of
|
|
39
|
+
* `orderablePosition`, spelled the way the partial index spells it so a query can use that index.
|
|
40
|
+
* @param column - Position column the condition tests.
|
|
41
|
+
* @returns - SQL condition true for anything but a positive safe integer.
|
|
42
|
+
*/
|
|
43
|
+
export function unorderablePosition(column) {
|
|
44
|
+
return `not (${orderablePosition(column)})`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Render the create statement for every unorderable-position index, in the list's own order.
|
|
48
|
+
* @param indexes - Declarations to render; the caller passes the list the compatibility check reads.
|
|
49
|
+
* @returns - One `create index` statement per declaration, terminated and newline-separated.
|
|
50
|
+
*/
|
|
51
|
+
function unorderablePositionIndexes(indexes) {
|
|
52
|
+
return indexes.map((index) => `${unorderablePositionIndex(index)};`).join("\n");
|
|
53
|
+
}
|
|
54
|
+
function quoteIdentifier(value) {
|
|
55
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Spell one stored vocabulary once, for the column constraint and the read that narrows it.
|
|
59
|
+
*
|
|
60
|
+
* Keys of an exhaustive record, so the compiler refuses a list that has drifted from its union.
|
|
61
|
+
* @param coverage - Every member of the union, mapped to `true`.
|
|
62
|
+
* @returns - The union's members, in declaration order.
|
|
63
|
+
*/
|
|
64
|
+
function vocabulary(coverage) {
|
|
65
|
+
return Object.keys(coverage);
|
|
66
|
+
}
|
|
67
|
+
/** Every delivery state `messages.state` may hold. */
|
|
68
|
+
export const MESSAGE_STATES = vocabulary({
|
|
69
|
+
queued: true,
|
|
70
|
+
claimed: true,
|
|
71
|
+
acknowledged: true,
|
|
72
|
+
expired: true,
|
|
73
|
+
dead_lettered: true,
|
|
74
|
+
failed: true,
|
|
75
|
+
});
|
|
76
|
+
/** Every lifecycle state `agents.state` may hold. */
|
|
77
|
+
export const AGENT_STATES = vocabulary({
|
|
78
|
+
active: true,
|
|
79
|
+
terminal: true,
|
|
80
|
+
reaped: true,
|
|
81
|
+
});
|
|
82
|
+
/** Every channel a reservation or session may occupy. */
|
|
83
|
+
export const ATTACHMENT_CHANNELS = vocabulary({ messaging: true, delivery: true });
|
|
84
|
+
/** Every author `message_events.source` may name. */
|
|
85
|
+
export const MESSAGE_EVENT_SOURCES = vocabulary({ broker: true, notify: true });
|
|
86
|
+
/** Every presentation tone `message_events.tone` may carry. */
|
|
87
|
+
export const MESSAGE_EVENT_TONES = vocabulary({
|
|
88
|
+
neutral: true,
|
|
89
|
+
danger: true,
|
|
90
|
+
ok: true,
|
|
91
|
+
muted: true,
|
|
92
|
+
warn: true,
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* Render one vocabulary as a SQL `check(column in (...))` clause.
|
|
96
|
+
*
|
|
97
|
+
* Members are sorted by UTF-16 code unit rather than by locale, so reordering a union does not
|
|
98
|
+
* change the text.
|
|
99
|
+
* @param column - Column the constraint applies to.
|
|
100
|
+
* @param allowed - Values the column may hold.
|
|
101
|
+
* @returns - Constraint text for the column's declaration.
|
|
102
|
+
*/
|
|
103
|
+
export function vocabularyClause(column, allowed) {
|
|
104
|
+
const members = [...allowed].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
105
|
+
return `check(${column} in (${members.map((value) => `'${value.replaceAll("'", "''")}'`).join(",")}))`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Report whether a table's stored declaration carries the constraint this build renders for a column.
|
|
109
|
+
*
|
|
110
|
+
* Case is kept: a clause differing only in a member's case admits different values.
|
|
111
|
+
* @param tableSql - The table's `create` statement exactly as `sqlite_master` holds it.
|
|
112
|
+
* @param column - Column the constraint applies to.
|
|
113
|
+
* @param allowed - Values this build's constraint admits.
|
|
114
|
+
* @returns - True when the declaration contains the constraint.
|
|
115
|
+
*/
|
|
116
|
+
export function declaresVocabulary(tableSql, column, allowed) {
|
|
117
|
+
// Whitespace inside a quoted member is part of the member: 'in progress' is not 'inprogress'.
|
|
118
|
+
const compact = (sql) => sql
|
|
119
|
+
.split(/('(?:[^']|'')*')/u)
|
|
120
|
+
.map((part, index) => (index % 2 === 1 ? part : part.replace(/\s+/gu, "")))
|
|
121
|
+
.join("");
|
|
122
|
+
return compact(tableSql).includes(compact(vocabularyClause(column, allowed)));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Owns broker schema creation, in-place-upgrade compatibility, and epoch assignment for one
|
|
126
|
+
* connection. Runs only from the broker's own constructor, before any other reader or writer of
|
|
127
|
+
* the connection exists.
|
|
128
|
+
*/
|
|
129
|
+
export class BrokerSchema {
|
|
130
|
+
#db;
|
|
131
|
+
#now;
|
|
132
|
+
#epochOwner;
|
|
133
|
+
#unorderablePositionIndexes;
|
|
134
|
+
#report;
|
|
135
|
+
/**
|
|
136
|
+
* Bind schema management to one connection.
|
|
137
|
+
* @param db - Connection whose schema this instance prepares.
|
|
138
|
+
* @param options - Clock, epoch-ownership policy, the unorderable-position declarations to create
|
|
139
|
+
* and require (defaulting to this build's; the create path and the check read the same list), and
|
|
140
|
+
* the operator-facing diagnostic sink for anything short of the schema-loss announcement, which is
|
|
141
|
+
* written to stderr directly because it reports data being deleted.
|
|
142
|
+
*/
|
|
143
|
+
constructor(db, options) {
|
|
144
|
+
this.#db = db;
|
|
145
|
+
this.#now = options.now;
|
|
146
|
+
this.#epochOwner = options.epochOwner;
|
|
147
|
+
this.#unorderablePositionIndexes = options.unorderablePositionIndexes ?? UNORDERABLE_POSITION_INDEXES;
|
|
148
|
+
this.#report = options.report;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Recreate an incompatible database, create the current schema if absent, reconcile the traffic
|
|
152
|
+
* position allocator against stored rows, and resolve the broker epoch — in that order.
|
|
153
|
+
* @returns - Broker epoch for this connection.
|
|
154
|
+
*/
|
|
155
|
+
prepare() {
|
|
156
|
+
this.#recreateIfIncompatible();
|
|
157
|
+
this.#createSchema();
|
|
158
|
+
this.#reconcileTrafficPositions();
|
|
159
|
+
return this.#loadEpoch();
|
|
160
|
+
}
|
|
161
|
+
#recreateIfIncompatible() {
|
|
162
|
+
const table = this.#db
|
|
163
|
+
.prepare("select name from sqlite_master where type = 'table' and name not like 'sqlite_%' limit 1")
|
|
164
|
+
.get();
|
|
165
|
+
if (!table)
|
|
166
|
+
return;
|
|
167
|
+
const markerTable = this.#db
|
|
168
|
+
.prepare("select name from sqlite_master where type = 'table' and name = 'schema_marker'")
|
|
169
|
+
.get();
|
|
170
|
+
const marker = markerTable
|
|
171
|
+
? this.#db.prepare("select id from schema_marker limit 1").get()
|
|
172
|
+
: null;
|
|
173
|
+
if (marker?.id !== SCHEMA_MARKER) {
|
|
174
|
+
const found = typeof marker?.id === "string" ? marker.id : "none";
|
|
175
|
+
this.#reportSchemaLoss(`found schema ${found}, this build requires ${SCHEMA_MARKER}`);
|
|
176
|
+
this.#dropSchema();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const mismatch = this.#operatorSchemaMismatch();
|
|
180
|
+
if (mismatch !== null) {
|
|
181
|
+
this.#reportSchemaLoss(`its schema marker matches this build but ${mismatch}`);
|
|
182
|
+
this.#dropSchema();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Announce that an incompatible database is about to be discarded, counting what is lost.
|
|
187
|
+
*
|
|
188
|
+
* Recreation destroys durable identities and undelivered messages, so it must never be silent: an
|
|
189
|
+
* operator who sees nothing has no reason to reach for a backup until the messages are already gone.
|
|
190
|
+
* Written to stderr, not the broker's diagnostic sink: a sink is its owner's to drop, and this
|
|
191
|
+
* announces data being deleted.
|
|
192
|
+
* @param reason - What made the database incompatible, phrased to sit inside the parentheses below.
|
|
193
|
+
*/
|
|
194
|
+
#reportSchemaLoss(reason) {
|
|
195
|
+
const count = (table) => {
|
|
196
|
+
try {
|
|
197
|
+
return Number(this.#db.prepare(`select count(*) as count from ${table}`).get().count);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const agents = count("agents");
|
|
204
|
+
const messages = count("messages");
|
|
205
|
+
if (agents === 0 && messages === 0)
|
|
206
|
+
return;
|
|
207
|
+
process.stderr.write(`pager: discarding an incompatible broker database (${reason}); ` +
|
|
208
|
+
`${String(agents)} agent identities and ${String(messages)} messages are being deleted\n`);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Name the first difference among the structures this build checks for.
|
|
212
|
+
*
|
|
213
|
+
* Checks only the structures listed in this method; anything else in the schema can differ
|
|
214
|
+
* unnoticed.
|
|
215
|
+
* @returns - What differs, or null when nothing this checks differs.
|
|
216
|
+
*/
|
|
217
|
+
#operatorSchemaMismatch() {
|
|
218
|
+
const requiredColumns = new Map([
|
|
219
|
+
["teams", ["workspace_id"]],
|
|
220
|
+
["agents", ["workspace_id", "registration_name", "session_key", "archived_at"]],
|
|
221
|
+
["workspaces", ["path"]],
|
|
222
|
+
["message_events", ["message_id", "id", "at", "type"]],
|
|
223
|
+
["reservations", ["channel"]],
|
|
224
|
+
["sessions", ["channel", "superseded_by", "epoch", "superseded_at"]],
|
|
225
|
+
[
|
|
226
|
+
"messages",
|
|
227
|
+
[
|
|
228
|
+
"id",
|
|
229
|
+
"sender_id",
|
|
230
|
+
"recipient_id",
|
|
231
|
+
"addressed_to",
|
|
232
|
+
"address_outcome",
|
|
233
|
+
"type",
|
|
234
|
+
"body",
|
|
235
|
+
"body_size",
|
|
236
|
+
"content_type",
|
|
237
|
+
"state",
|
|
238
|
+
"created_at",
|
|
239
|
+
"sequence",
|
|
240
|
+
],
|
|
241
|
+
],
|
|
242
|
+
["traffic_positions", ["id"]],
|
|
243
|
+
["operator_commands", ["command_id", "type", "target_id", "accepted_at", "control_id"]],
|
|
244
|
+
[
|
|
245
|
+
"operator_ping_audits",
|
|
246
|
+
["audit_id", "command_id", "control_id", "target_agent_id", "actor", "accepted_at", "traffic_position"],
|
|
247
|
+
],
|
|
248
|
+
["operator_stream_state", ["singleton", "epoch", "sequence"]],
|
|
249
|
+
["operator_stream_events", ["epoch", "sequence", "payload"]],
|
|
250
|
+
]);
|
|
251
|
+
for (const [name, required] of requiredColumns) {
|
|
252
|
+
const columns = this.#db.prepare(`pragma table_info(${quoteIdentifier(name)})`).all();
|
|
253
|
+
const names = new Set(columns.map((row) => String(row.name)));
|
|
254
|
+
if (columns.length === 0)
|
|
255
|
+
return `table ${name} is absent`;
|
|
256
|
+
const missing = required.find((column) => !names.has(column));
|
|
257
|
+
if (missing !== undefined)
|
|
258
|
+
return `table ${name} has no ${missing} column`;
|
|
259
|
+
}
|
|
260
|
+
for (const [table, column] of [
|
|
261
|
+
["traffic_positions", "id"],
|
|
262
|
+
["operator_commands", "command_id"],
|
|
263
|
+
["operator_ping_audits", "audit_id"],
|
|
264
|
+
["operator_stream_state", "singleton"],
|
|
265
|
+
]) {
|
|
266
|
+
if (!this.#hasPrimaryKey(table, column))
|
|
267
|
+
return `table ${table} does not key on ${column}`;
|
|
268
|
+
}
|
|
269
|
+
for (const index of this.#unorderablePositionIndexes) {
|
|
270
|
+
if (!this.#hasIndex(index))
|
|
271
|
+
return `table ${index.table} has no ${index.name} index over its unorderable rows`;
|
|
272
|
+
}
|
|
273
|
+
for (const [table, columns] of [
|
|
274
|
+
["operator_stream_events", ["epoch", "sequence"]],
|
|
275
|
+
["operator_commands", ["control_id"]],
|
|
276
|
+
["messages", ["sequence"]],
|
|
277
|
+
["operator_ping_audits", ["command_id"]],
|
|
278
|
+
["operator_ping_audits", ["control_id"]],
|
|
279
|
+
["operator_ping_audits", ["traffic_position"]],
|
|
280
|
+
]) {
|
|
281
|
+
if (!this.#hasUniqueColumns(table, columns))
|
|
282
|
+
return `table ${table} does not hold ${columns.join(", ")} unique`;
|
|
283
|
+
}
|
|
284
|
+
for (const [table, column, allowed] of [
|
|
285
|
+
["messages", "state", MESSAGE_STATES],
|
|
286
|
+
["agents", "state", AGENT_STATES],
|
|
287
|
+
["reservations", "channel", ATTACHMENT_CHANNELS],
|
|
288
|
+
["sessions", "channel", ATTACHMENT_CHANNELS],
|
|
289
|
+
["message_events", "source", MESSAGE_EVENT_SOURCES],
|
|
290
|
+
["message_events", "tone", MESSAGE_EVENT_TONES],
|
|
291
|
+
]) {
|
|
292
|
+
if (!declaresVocabulary(this.#declaredSql(table), column, allowed)) {
|
|
293
|
+
return `table ${table} does not constrain ${column}`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const trafficSql = this.#tableSql("traffic_positions");
|
|
297
|
+
const commandSql = this.#tableSql("operator_commands");
|
|
298
|
+
const auditSql = this.#tableSql("operator_ping_audits");
|
|
299
|
+
for (const [table, sql, fragment, missing] of [
|
|
300
|
+
["traffic_positions", trafficSql, "autoincrement", "does not allocate positions monotonically"],
|
|
301
|
+
[
|
|
302
|
+
"operator_commands",
|
|
303
|
+
commandSql,
|
|
304
|
+
"typein('ping_agent','expire_message','archive_agent')",
|
|
305
|
+
"does not constrain its command types",
|
|
306
|
+
],
|
|
307
|
+
[
|
|
308
|
+
"operator_commands",
|
|
309
|
+
commandSql,
|
|
310
|
+
"type='ping_agent'andcontrol_idisnotnull",
|
|
311
|
+
"does not require a ping control id",
|
|
312
|
+
],
|
|
313
|
+
["operator_commands", commandSql, "type!='ping_agent'andcontrol_idisnull", "does not refuse a stray control id"],
|
|
314
|
+
["operator_ping_audits", auditSql, "actor='operator'", "does not constrain its actor"],
|
|
315
|
+
["operator_ping_audits", auditSql, "referencesoperator_commands(command_id)", "does not reference its command"],
|
|
316
|
+
]) {
|
|
317
|
+
if (!sql.includes(fragment))
|
|
318
|
+
return `table ${table} ${missing}`;
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
#hasPrimaryKey(table, column) {
|
|
323
|
+
const columns = this.#db.prepare(`pragma table_info(${quoteIdentifier(table)})`).all();
|
|
324
|
+
return columns.some((row) => row.name === column && Number(row.pk) === 1);
|
|
325
|
+
}
|
|
326
|
+
#hasIndex(index) {
|
|
327
|
+
const row = this.#db
|
|
328
|
+
.prepare("select sql from sqlite_master where type = 'index' and name = ? and tbl_name = ?")
|
|
329
|
+
.get(index.name, index.table);
|
|
330
|
+
const normalize = (sql) => sql.toLowerCase().replace(/\s+/gu, "").replace("ifnotexists", "");
|
|
331
|
+
return typeof row?.sql === "string" && normalize(row.sql) === normalize(unorderablePositionIndex(index));
|
|
332
|
+
}
|
|
333
|
+
#hasUniqueColumns(table, columns) {
|
|
334
|
+
const indexes = this.#db.prepare(`pragma index_list(${quoteIdentifier(table)})`).all();
|
|
335
|
+
return indexes.some((index) => {
|
|
336
|
+
if (Number(index.unique) !== 1)
|
|
337
|
+
return false;
|
|
338
|
+
const indexed = this.#db.prepare(`pragma index_info(${quoteIdentifier(String(index.name))})`).all();
|
|
339
|
+
return indexed.map((row) => String(row.name)).join("\0") === columns.join("\0");
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
#declaredSql(table) {
|
|
343
|
+
const row = this.#db.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
|
|
344
|
+
return typeof row?.sql === "string" ? row.sql : "";
|
|
345
|
+
}
|
|
346
|
+
#tableSql(table) {
|
|
347
|
+
const row = this.#db.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
|
|
348
|
+
return typeof row?.sql === "string" ? row.sql.toLowerCase().replace(/\s+/gu, "") : "";
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Seed the position allocator above every position the stored tables already hold.
|
|
352
|
+
*
|
|
353
|
+
* Runs from the broker's constructor and never throws over a row: a position it cannot use is
|
|
354
|
+
* counted and reported, and no stored position is rewritten.
|
|
355
|
+
*/
|
|
356
|
+
#reconcileTrafficPositions() {
|
|
357
|
+
this.#transaction(() => {
|
|
358
|
+
const rows = this.#db
|
|
359
|
+
.prepare(`select sequence as position, (${orderablePosition("sequence")}) as orderable from messages union all select traffic_position as position, (${orderablePosition("traffic_position")}) as orderable from operator_ping_audits`)
|
|
360
|
+
.all();
|
|
361
|
+
const claimants = new Map();
|
|
362
|
+
let maximum = 0;
|
|
363
|
+
let unusable = 0;
|
|
364
|
+
for (const row of rows) {
|
|
365
|
+
if (row.orderable !== 1) {
|
|
366
|
+
unusable += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const position = Number(row.position);
|
|
370
|
+
claimants.set(position, (claimants.get(position) ?? 0) + 1);
|
|
371
|
+
maximum = Math.max(maximum, position);
|
|
372
|
+
}
|
|
373
|
+
for (const count of claimants.values())
|
|
374
|
+
if (count > 1)
|
|
375
|
+
unusable += count;
|
|
376
|
+
if (unusable > 0) {
|
|
377
|
+
this.#report(`pager: ${String(unusable)} stored traffic row(s) hold a position that cannot be used and are ` +
|
|
378
|
+
`left out of the operator's view; the allocator continues above ${String(maximum)}`);
|
|
379
|
+
}
|
|
380
|
+
if (maximum < 1)
|
|
381
|
+
return;
|
|
382
|
+
const allocator = this.#db.prepare("select coalesce(max(id), 0) as position from traffic_positions").get();
|
|
383
|
+
if (Number(allocator.position) < maximum) {
|
|
384
|
+
this.#write("insert or ignore into traffic_positions(id) values (?)", maximum);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
#dropSchema() {
|
|
389
|
+
const tables = this.#db
|
|
390
|
+
.prepare("select name from sqlite_master where type = 'table' and name not like 'sqlite_%'")
|
|
391
|
+
.all();
|
|
392
|
+
this.#db.exec("pragma foreign_keys = OFF");
|
|
393
|
+
try {
|
|
394
|
+
for (const table of tables)
|
|
395
|
+
this.#db.exec(`drop table if exists ${quoteIdentifier(String(table.name))}`);
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
this.#db.exec("pragma foreign_keys = ON");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
#createSchema() {
|
|
402
|
+
this.#db.exec(`
|
|
403
|
+
create table if not exists schema_marker(id text primary key check(id = '${SCHEMA_MARKER}'));
|
|
404
|
+
insert or ignore into schema_marker(id) values ('${SCHEMA_MARKER}');
|
|
405
|
+
create table if not exists broker_epoch(id text primary key, created_at integer not null);
|
|
406
|
+
create table if not exists workspaces(id text primary key, path text not null unique, branch text, created_at integer not null);
|
|
407
|
+
create table if not exists teams(id text primary key, workspace_id text not null, created_at integer not null);
|
|
408
|
+
create table if not exists agents(id text primary key, team_id text not null, workspace_id text not null, parent_id text, state text not null ${vocabularyClause("state", AGENT_STATES)}, created_at integer not null, implementation_id text, implementation_version text, registration_name text, registration_ephemeral integer not null default 0, session_key text, archived_at integer, check(implementation_id is not null or implementation_version is null));
|
|
409
|
+
create unique index if not exists agent_registration_name on agents(workspace_id, registration_name) where registration_name is not null and state != 'reaped';
|
|
410
|
+
create unique index if not exists agent_session_key on agents(workspace_id, implementation_id, session_key) where session_key is not null and state = 'active';
|
|
411
|
+
create table if not exists alias_history(id integer primary key autoincrement, workspace_id text not null, alias text not null, agent_id text not null, unique(workspace_id, alias));
|
|
412
|
+
create table if not exists reservations(reservation_id text primary key, agent_id text not null, channel text not null ${vocabularyClause("channel", ATTACHMENT_CHANNELS)}, expires_at integer not null, consumed_at integer);
|
|
413
|
+
create table if not exists sessions(id text primary key, agent_id text not null, channel text not null ${vocabularyClause("channel", ATTACHMENT_CHANNELS)}, fencing_token text not null, expires_at integer not null, active integer not null, created_at integer not null, superseded_by text, epoch text not null, superseded_at integer);
|
|
414
|
+
create table if not exists traffic_positions(id integer primary key autoincrement);
|
|
415
|
+
create table if not exists messages(id text primary key, team_id text not null, workspace_id text not null, sender_id text not null, recipient_id text not null, addressed_to text not null, address_outcome text not null, subject text not null, type text not null, body text, body_size integer, content_type text, state text not null ${vocabularyClause("state", MESSAGE_STATES)}, created_at integer not null, sequence integer not null, claim_token text, claim_expires_at integer);
|
|
416
|
+
create unique index if not exists message_traffic_position on messages(sequence);
|
|
417
|
+
create table if not exists message_events(id integer primary key autoincrement, message_id text not null, at integer not null, source text not null ${vocabularyClause("source", MESSAGE_EVENT_SOURCES)}, type text not null, text text not null, tone text not null ${vocabularyClause("tone", MESSAGE_EVENT_TONES)});
|
|
418
|
+
create index if not exists message_events_message on message_events(message_id, id);
|
|
419
|
+
create table if not exists claims(message_id text not null, token text not null, agent_id text not null, expires_at integer not null, created_at integer not null, primary key(message_id, token));
|
|
420
|
+
create unique index if not exists claim_token on claims(token);
|
|
421
|
+
create table if not exists wakes(agent_id text primary key, attempts integer not null, next_wake_at integer not null);
|
|
422
|
+
create table if not exists idempotency(agent_id text not null, recipient_id text not null, key text not null, message_id text not null, created_at integer not null, primary key(agent_id, recipient_id, key));
|
|
423
|
+
create table if not exists operator_commands(command_id text primary key, type text not null check(type in ('ping_agent','expire_message','archive_agent')), target_id text not null, accepted_at integer not null, control_id text unique, check((type = 'ping_agent' and control_id is not null) or (type != 'ping_agent' and control_id is null)));
|
|
424
|
+
create table if not exists operator_ping_audits(audit_id text primary key, command_id text not null unique references operator_commands(command_id), control_id text not null unique, target_agent_id text not null, actor text not null check(actor = 'operator'), accepted_at integer not null, traffic_position integer not null unique);
|
|
425
|
+
create table if not exists operator_stream_state(singleton integer primary key check(singleton = 1), epoch text not null, sequence integer not null check(sequence >= 0));
|
|
426
|
+
create table if not exists operator_stream_events(epoch text not null, sequence integer not null check(sequence > 0), payload text not null, unique(epoch, sequence));
|
|
427
|
+
`);
|
|
428
|
+
this.#db.exec(unorderablePositionIndexes(this.#unorderablePositionIndexes));
|
|
429
|
+
}
|
|
430
|
+
#loadEpoch() {
|
|
431
|
+
const existing = this.#db.prepare("select id from broker_epoch limit 1").get();
|
|
432
|
+
if (existing && !this.#epochOwner)
|
|
433
|
+
return String(existing.id);
|
|
434
|
+
const epoch = issueOpaqueToken("epoch");
|
|
435
|
+
this.#write("delete from broker_epoch");
|
|
436
|
+
this.#write("insert into broker_epoch(id, created_at) values (?, ?)", epoch, this.#now());
|
|
437
|
+
return epoch;
|
|
438
|
+
}
|
|
439
|
+
#write(sql, ...parameters) {
|
|
440
|
+
return this.#db.prepare(sql).run(...parameters);
|
|
441
|
+
}
|
|
442
|
+
#transaction(fn) {
|
|
443
|
+
this.#db.exec("begin immediate");
|
|
444
|
+
try {
|
|
445
|
+
const result = fn();
|
|
446
|
+
this.#db.exec("commit");
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
this.#db.exec("rollback");
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Broker } from "./broker.js";
|
|
2
|
+
import type { DatabaseDurability } from "./database.js";
|
|
3
|
+
import type { BrokerOperationalEvent } from "./operator-projection.js";
|
|
4
|
+
import type { AgentLifecycleState, LifecycleTone } from "./schema.js";
|
|
5
|
+
import type { MessageState } from "./protocol.js";
|
|
6
|
+
/** Describes broker generation metadata included in a snapshot. */
|
|
7
|
+
export type SnapshotHealth = {
|
|
8
|
+
/** Identifies the broker epoch that produced this snapshot. */
|
|
9
|
+
brokerEpoch: string;
|
|
10
|
+
/** Records when the broker generated this snapshot. */
|
|
11
|
+
generatedAt: number;
|
|
12
|
+
/** Reports the durability settings observed on the live broker's own database connection. */
|
|
13
|
+
durability: DatabaseDurability;
|
|
14
|
+
};
|
|
15
|
+
/** Describes one workspace represented in a snapshot. */
|
|
16
|
+
export type SnapshotWorkspace = {
|
|
17
|
+
/** Identifies the workspace. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Supplies the workspace path. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Supplies the workspace branch when known. */
|
|
22
|
+
branch: string | null;
|
|
23
|
+
};
|
|
24
|
+
/** Describes one agent represented in a snapshot. */
|
|
25
|
+
export type SnapshotAgent = {
|
|
26
|
+
/** Identifies the agent. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Records the durable agent registration time. */
|
|
29
|
+
createdAt: number;
|
|
30
|
+
/** Identifies the agent team. */
|
|
31
|
+
teamId: string;
|
|
32
|
+
/** Identifies the agent workspace. */
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
/** Identifies the parent agent when available. */
|
|
35
|
+
parentId: string | null;
|
|
36
|
+
/** Reports the broker lifecycle state. */
|
|
37
|
+
state: AgentLifecycleState;
|
|
38
|
+
/** Reports the opaque agent implementation identity and optional version. */
|
|
39
|
+
implementation: {
|
|
40
|
+
/** Supplies the implementation identity chosen by the composition boundary. */ id: string;
|
|
41
|
+
/** Reports implementation version availability and value. */ version: {
|
|
42
|
+
known: true; /** Supplies the version. */
|
|
43
|
+
value: string;
|
|
44
|
+
} | {
|
|
45
|
+
known: false;
|
|
46
|
+
};
|
|
47
|
+
} | {
|
|
48
|
+
/** Reports that the implementation identity is unavailable. */ id: null;
|
|
49
|
+
/** Reports that no version can be associated without an identity. */ version: {
|
|
50
|
+
known: false;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/** Supplies the agent's current routing and reconnection alias. */
|
|
54
|
+
alias: string;
|
|
55
|
+
/** Lists earlier routable aliases in durable history order. */
|
|
56
|
+
previousAliases: string[];
|
|
57
|
+
/** Records when an operator archived this identity, or null while it is not archived. */
|
|
58
|
+
archivedAt: number | null;
|
|
59
|
+
};
|
|
60
|
+
/** Describes one durable lifecycle event. */
|
|
61
|
+
export type SnapshotLifecycleEntry = {
|
|
62
|
+
/** Identifies the lifecycle event. */
|
|
63
|
+
id: number;
|
|
64
|
+
/** Identifies the related message. */
|
|
65
|
+
messageId: string;
|
|
66
|
+
/** Records when the event occurred. */
|
|
67
|
+
at: number;
|
|
68
|
+
/** Identifies the subsystem that emitted the event. */
|
|
69
|
+
source: "broker" | "notify";
|
|
70
|
+
/** Identifies the event kind. */
|
|
71
|
+
type: string;
|
|
72
|
+
/** Supplies the user-facing lifecycle text. */
|
|
73
|
+
text: string;
|
|
74
|
+
/** Selects the lifecycle display tone. */
|
|
75
|
+
tone: LifecycleTone;
|
|
76
|
+
};
|
|
77
|
+
/** Describes one message's delivery as the snapshot reports it. */
|
|
78
|
+
export type SnapshotDelivery = {
|
|
79
|
+
/** Identifies the durable message. */
|
|
80
|
+
messageId: string;
|
|
81
|
+
/** Orders durable message records globally within the broker database. */
|
|
82
|
+
sequence: number;
|
|
83
|
+
/** Identifies the message team. */
|
|
84
|
+
teamId: string;
|
|
85
|
+
/** Identifies the workspace the message was sent from. */
|
|
86
|
+
workspaceId: string;
|
|
87
|
+
/** Identifies the sending agent. */
|
|
88
|
+
from: string;
|
|
89
|
+
/** Identifies the resolved recipient, which every persisted message has. */
|
|
90
|
+
to: string;
|
|
91
|
+
/** Carries the one-line subject the sender supplied. */
|
|
92
|
+
subject: string;
|
|
93
|
+
/** Carries the sender-supplied message type. */
|
|
94
|
+
type: string;
|
|
95
|
+
/** Reports the durable delivery state. */
|
|
96
|
+
state: MessageState;
|
|
97
|
+
/** Records message creation time. */
|
|
98
|
+
createdAt: number;
|
|
99
|
+
/** Counts delivery attempts. */
|
|
100
|
+
attempts: number;
|
|
101
|
+
/** Reports retained body size and representation without exposing body content. */
|
|
102
|
+
body: {
|
|
103
|
+
/** Records retained body size. */
|
|
104
|
+
sizeBytes: number;
|
|
105
|
+
/** Supplies retained body content type. */
|
|
106
|
+
contentType: string;
|
|
107
|
+
};
|
|
108
|
+
/** Lists durable lifecycle events for the delivery. */
|
|
109
|
+
lifecycle: SnapshotLifecycleEntry[];
|
|
110
|
+
};
|
|
111
|
+
/** Describes diagnostic state included in a snapshot. */
|
|
112
|
+
export type SnapshotDiagnostics = {
|
|
113
|
+
/** Reports connection state. */
|
|
114
|
+
connectionState: "connected" | "degraded";
|
|
115
|
+
/** Lists operational events. */
|
|
116
|
+
operationalEvents: BrokerOperationalEvent[];
|
|
117
|
+
};
|
|
118
|
+
/** Represents the complete broker state exposed by a snapshot. */
|
|
119
|
+
export type BrokerSnapshot = {
|
|
120
|
+
/** Identifies the snapshot schema version. */
|
|
121
|
+
version: 1;
|
|
122
|
+
/** Orders broker state changes. */
|
|
123
|
+
sequence: number;
|
|
124
|
+
/** Reports broker health. */
|
|
125
|
+
health: SnapshotHealth;
|
|
126
|
+
/** Lists visible workspaces. */
|
|
127
|
+
workspaces: SnapshotWorkspace[];
|
|
128
|
+
/** Lists visible agents. */
|
|
129
|
+
agents: SnapshotAgent[];
|
|
130
|
+
/** Lists connection state by agent. */
|
|
131
|
+
connections: Array<{
|
|
132
|
+
/** Identifies the agent. */ agentId: string;
|
|
133
|
+
/** Reports connection state. */ state: "connected" | "disconnected";
|
|
134
|
+
/** Records connection expiry. */ expiresAt: number | null;
|
|
135
|
+
/**
|
|
136
|
+
* Records when the open session the connection state is read from (the newest unexpired one,
|
|
137
|
+
* else the newest open one) was created, or null while the agent holds no open session row,
|
|
138
|
+
* whether it has never attached or its last session was explicitly closed. Distinguishes an
|
|
139
|
+
* expired-but-still-open session from one that was closed or never existed, which `state`
|
|
140
|
+
* alone cannot.
|
|
141
|
+
*/
|
|
142
|
+
lastSeenAt: number | null;
|
|
143
|
+
}>;
|
|
144
|
+
/** Lists one entry per durable message. */
|
|
145
|
+
deliveries: SnapshotDelivery[];
|
|
146
|
+
/** Supplies diagnostic state. */
|
|
147
|
+
diagnostics: SnapshotDiagnostics;
|
|
148
|
+
};
|
|
149
|
+
/** Represents a running snapshot HTTP server and its stop operation. */
|
|
150
|
+
export type SnapshotServerHandle = {
|
|
151
|
+
/** Provides the server's loopback base URL. */
|
|
152
|
+
url: string;
|
|
153
|
+
/** Stops the server after closing its connections. */
|
|
154
|
+
close: () => Promise<void>;
|
|
155
|
+
};
|
|
156
|
+
/** Broker operations the snapshot HTTP endpoint reaches. */
|
|
157
|
+
export type SnapshotBrokerPort = Pick<Broker, "readMessageBody" | "snapshotHealth" | "snapshot" | "expireMessage" | "status">;
|
|
158
|
+
/**
|
|
159
|
+
* Start the loopback-only snapshot HTTP endpoint.
|
|
160
|
+
* @param broker - Durable broker port that owns message state.
|
|
161
|
+
* @param port - Requested loopback port, with zero selecting an ephemeral port.
|
|
162
|
+
* @param host - Network interface to bind and advertise.
|
|
163
|
+
* @returns - Handle carrying the bound base URL and a `close` that resolves once the server has stopped.
|
|
164
|
+
*/
|
|
165
|
+
export declare function startSnapshotServer(broker: SnapshotBrokerPort, port?: number, host?: string): Promise<SnapshotServerHandle>;
|
|
166
|
+
/**
|
|
167
|
+
* Return a non-secret stable marker for restart detection without exposing process-binding tokens.
|
|
168
|
+
* @param epoch - Broker generation used to fence stale sessions.
|
|
169
|
+
* @returns - Non-secret epoch fingerprint suitable for public diagnostics.
|
|
170
|
+
*/
|
|
171
|
+
export declare function publicEpoch(epoch: string): string;
|