@agimon-ai/doompi-session 0.0.1-alpha.2 → 0.0.1-alpha.21

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 (139) hide show
  1. package/dist/{src/schemas/apiContracts.cjs → apiContracts.cjs} +7 -3
  2. package/dist/apiContracts.cjs.map +1 -0
  3. package/dist/{src/schemas/apiContracts.d.mts.map → apiContracts.d.mts.map} +1 -1
  4. package/dist/{src/schemas/apiContracts.mjs → apiContracts.mjs} +1 -1
  5. package/dist/apiContracts.mjs.map +1 -0
  6. package/dist/{src/services/backgroundWorkService/index.cjs → backgroundWorkService-BC0743Nn.cjs} +17 -7
  7. package/dist/backgroundWorkService-BC0743Nn.cjs.map +1 -0
  8. package/dist/{src/services/backgroundWorkService/index.mjs → backgroundWorkService-ChVXCzxj.mjs} +3 -3
  9. package/dist/backgroundWorkService-ChVXCzxj.mjs.map +1 -0
  10. package/dist/extensions/pi.cjs +12 -6
  11. package/dist/extensions/pi.cjs.map +1 -1
  12. package/dist/extensions/pi.d.mts +1 -1
  13. package/dist/extensions/pi.d.mts.map +1 -1
  14. package/dist/extensions/pi.mjs +9 -3
  15. package/dist/extensions/pi.mjs.map +1 -1
  16. package/dist/extensions/server.cjs +154 -10
  17. package/dist/extensions/server.cjs.map +1 -1
  18. package/dist/extensions/server.d.mts +1 -1
  19. package/dist/extensions/server.d.mts.map +1 -1
  20. package/dist/extensions/server.mjs +146 -4
  21. package/dist/extensions/server.mjs.map +1 -1
  22. package/dist/index.cjs +24 -31
  23. package/dist/index.d.mts +295 -11
  24. package/dist/index.d.mts.map +1 -0
  25. package/dist/index.mjs +2 -9
  26. package/dist/sessionDelivery-C_2qtUoE.cjs +954 -0
  27. package/dist/sessionDelivery-C_2qtUoE.cjs.map +1 -0
  28. package/dist/sessionDelivery-DNq4a8LB.mjs +799 -0
  29. package/dist/sessionDelivery-DNq4a8LB.mjs.map +1 -0
  30. package/package.json +16 -16
  31. package/dist/_virtual/_rolldown/runtime.cjs +0 -23
  32. package/dist/api-contracts.cjs +0 -7
  33. package/dist/api-contracts.d.cts +0 -2
  34. package/dist/api-contracts.d.mts +0 -2
  35. package/dist/api-contracts.mjs +0 -2
  36. package/dist/extensions/pi.d.cts +0 -5
  37. package/dist/extensions/pi.d.cts.map +0 -1
  38. package/dist/extensions/server.d.cts +0 -5
  39. package/dist/extensions/server.d.cts.map +0 -1
  40. package/dist/index.d.cts +0 -11
  41. package/dist/src/extensions/(backend)/api/session-peer/route.server.cjs +0 -7
  42. package/dist/src/extensions/(backend)/api/session-peer/route.server.cjs.map +0 -1
  43. package/dist/src/extensions/(backend)/api/session-peer/route.server.mjs +0 -8
  44. package/dist/src/extensions/(backend)/api/session-peer/route.server.mjs.map +0 -1
  45. package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs +0 -10
  46. package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs.map +0 -1
  47. package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs +0 -11
  48. package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs.map +0 -1
  49. package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs +0 -101
  50. package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs.map +0 -1
  51. package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs +0 -99
  52. package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs.map +0 -1
  53. package/dist/src/schemas/apiContracts.cjs.map +0 -1
  54. package/dist/src/schemas/apiContracts.d.cts +0 -52
  55. package/dist/src/schemas/apiContracts.d.cts.map +0 -1
  56. package/dist/src/schemas/apiContracts.mjs.map +0 -1
  57. package/dist/src/services/backgroundWorkService/index.cjs.map +0 -1
  58. package/dist/src/services/backgroundWorkService/index.d.cts +0 -9
  59. package/dist/src/services/backgroundWorkService/index.d.cts.map +0 -1
  60. package/dist/src/services/backgroundWorkService/index.d.mts +0 -9
  61. package/dist/src/services/backgroundWorkService/index.d.mts.map +0 -1
  62. package/dist/src/services/backgroundWorkService/index.mjs.map +0 -1
  63. package/dist/src/services/peerInbox/index.cjs +0 -18
  64. package/dist/src/services/peerInbox/index.cjs.map +0 -1
  65. package/dist/src/services/peerInbox/index.mjs +0 -17
  66. package/dist/src/services/peerInbox/index.mjs.map +0 -1
  67. package/dist/src/services/peerInboxApi/index.cjs +0 -48
  68. package/dist/src/services/peerInboxApi/index.cjs.map +0 -1
  69. package/dist/src/services/peerInboxApi/index.mjs +0 -48
  70. package/dist/src/services/peerInboxApi/index.mjs.map +0 -1
  71. package/dist/src/services/peerTransport/index.cjs +0 -206
  72. package/dist/src/services/peerTransport/index.cjs.map +0 -1
  73. package/dist/src/services/peerTransport/index.d.cts +0 -49
  74. package/dist/src/services/peerTransport/index.d.cts.map +0 -1
  75. package/dist/src/services/peerTransport/index.d.mts +0 -49
  76. package/dist/src/services/peerTransport/index.d.mts.map +0 -1
  77. package/dist/src/services/peerTransport/index.mjs +0 -195
  78. package/dist/src/services/peerTransport/index.mjs.map +0 -1
  79. package/dist/src/services/sessionDelivery/index.cjs +0 -277
  80. package/dist/src/services/sessionDelivery/index.cjs.map +0 -1
  81. package/dist/src/services/sessionDelivery/index.d.cts +0 -10
  82. package/dist/src/services/sessionDelivery/index.d.cts.map +0 -1
  83. package/dist/src/services/sessionDelivery/index.d.mts +0 -10
  84. package/dist/src/services/sessionDelivery/index.d.mts.map +0 -1
  85. package/dist/src/services/sessionDelivery/index.mjs +0 -274
  86. package/dist/src/services/sessionDelivery/index.mjs.map +0 -1
  87. package/dist/src/services/sessionDelivery/type.cjs +0 -11
  88. package/dist/src/services/sessionDelivery/type.cjs.map +0 -1
  89. package/dist/src/services/sessionDelivery/type.d.cts +0 -73
  90. package/dist/src/services/sessionDelivery/type.d.cts.map +0 -1
  91. package/dist/src/services/sessionDelivery/type.d.mts +0 -73
  92. package/dist/src/services/sessionDelivery/type.d.mts.map +0 -1
  93. package/dist/src/services/sessionDelivery/type.mjs +0 -10
  94. package/dist/src/services/sessionDelivery/type.mjs.map +0 -1
  95. package/dist/src/services/sessionDirectory/index.cjs +0 -120
  96. package/dist/src/services/sessionDirectory/index.cjs.map +0 -1
  97. package/dist/src/services/sessionDirectory/index.d.cts +0 -7
  98. package/dist/src/services/sessionDirectory/index.d.cts.map +0 -1
  99. package/dist/src/services/sessionDirectory/index.d.mts +0 -7
  100. package/dist/src/services/sessionDirectory/index.d.mts.map +0 -1
  101. package/dist/src/services/sessionDirectory/index.mjs +0 -120
  102. package/dist/src/services/sessionDirectory/index.mjs.map +0 -1
  103. package/dist/src/services/sessionDirectory/type.d.cts +0 -54
  104. package/dist/src/services/sessionDirectory/type.d.cts.map +0 -1
  105. package/dist/src/services/sessionDirectory/type.d.mts +0 -54
  106. package/dist/src/services/sessionDirectory/type.d.mts.map +0 -1
  107. package/dist/src/services/sessionGroups/index.cjs +0 -105
  108. package/dist/src/services/sessionGroups/index.cjs.map +0 -1
  109. package/dist/src/services/sessionGroups/index.d.cts +0 -7
  110. package/dist/src/services/sessionGroups/index.d.cts.map +0 -1
  111. package/dist/src/services/sessionGroups/index.d.mts +0 -7
  112. package/dist/src/services/sessionGroups/index.d.mts.map +0 -1
  113. package/dist/src/services/sessionGroups/index.mjs +0 -104
  114. package/dist/src/services/sessionGroups/index.mjs.map +0 -1
  115. package/dist/src/services/sessionGroups/type.cjs +0 -11
  116. package/dist/src/services/sessionGroups/type.cjs.map +0 -1
  117. package/dist/src/services/sessionGroups/type.d.cts +0 -32
  118. package/dist/src/services/sessionGroups/type.d.cts.map +0 -1
  119. package/dist/src/services/sessionGroups/type.d.mts +0 -32
  120. package/dist/src/services/sessionGroups/type.d.mts.map +0 -1
  121. package/dist/src/services/sessionGroups/type.mjs +0 -11
  122. package/dist/src/services/sessionGroups/type.mjs.map +0 -1
  123. package/dist/src/services/sessionIntercom/index.cjs +0 -89
  124. package/dist/src/services/sessionIntercom/index.cjs.map +0 -1
  125. package/dist/src/services/sessionIntercom/index.d.cts +0 -11
  126. package/dist/src/services/sessionIntercom/index.d.cts.map +0 -1
  127. package/dist/src/services/sessionIntercom/index.d.mts +0 -11
  128. package/dist/src/services/sessionIntercom/index.d.mts.map +0 -1
  129. package/dist/src/services/sessionIntercom/index.mjs +0 -85
  130. package/dist/src/services/sessionIntercom/index.mjs.map +0 -1
  131. package/dist/src/services/sessionIntercom/type.cjs +0 -16
  132. package/dist/src/services/sessionIntercom/type.cjs.map +0 -1
  133. package/dist/src/services/sessionIntercom/type.d.cts +0 -60
  134. package/dist/src/services/sessionIntercom/type.d.cts.map +0 -1
  135. package/dist/src/services/sessionIntercom/type.d.mts +0 -60
  136. package/dist/src/services/sessionIntercom/type.d.mts.map +0 -1
  137. package/dist/src/services/sessionIntercom/type.mjs +0 -14
  138. package/dist/src/services/sessionIntercom/type.mjs.map +0 -1
  139. /package/dist/{src/schemas/apiContracts.d.mts → apiContracts.d.mts} +0 -0
@@ -0,0 +1,799 @@
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ //#region src/services/peerTransport/index.ts
6
+ const PEER_CONFIG_VERSION = 1;
7
+ const PEER_CONFIG_FILE = "peers.json";
8
+ const PEER_ROUTE = "/api/plugins/session-peer/inbox";
9
+ const PEER_HEADER = "x-doompi-session-peer";
10
+ const TIMESTAMP_HEADER = "x-doompi-session-timestamp";
11
+ const SIGNATURE_HEADER = "x-doompi-session-signature";
12
+ const MAX_CLOCK_SKEW_MS = 3e5;
13
+ const MAX_REPLAY_ENTRIES = 1e4;
14
+ const REPLAY_DATABASE_FILE = "peer-replay.sqlite";
15
+ function validId(value) {
16
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
17
+ }
18
+ function peerOf(value) {
19
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
20
+ const record = value;
21
+ if (!validId(record.hostId) || typeof record.url !== "string" || typeof record.secret !== "string") return void 0;
22
+ let url;
23
+ try {
24
+ url = new URL(record.url);
25
+ } catch {
26
+ return;
27
+ }
28
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return void 0;
29
+ const allowedSessionIds = Array.isArray(record.allowedSessionIds) && record.allowedSessionIds.every(validId) ? record.allowedSessionIds : void 0;
30
+ const allowedVoiceSessionIds = Array.isArray(record.allowedVoiceSessionIds) && record.allowedVoiceSessionIds.every(validId) ? record.allowedVoiceSessionIds : [];
31
+ if (allowedSessionIds === void 0 || record.secret.length < 32) return void 0;
32
+ return {
33
+ hostId: record.hostId,
34
+ url: url.toString(),
35
+ secret: record.secret,
36
+ allowedSessionIds,
37
+ allowedVoiceSessionIds
38
+ };
39
+ }
40
+ function sessionPeerConfigPath(homeDirectory) {
41
+ return path.join(homeDirectory, ".pi", ".doom", "session", PEER_CONFIG_FILE);
42
+ }
43
+ /** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */
44
+ function readSessionPeerConfig(homeDirectory) {
45
+ let parsed;
46
+ try {
47
+ const file = sessionPeerConfigPath(homeDirectory);
48
+ if ((fs.statSync(file).mode & 63) !== 0) return void 0;
49
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
50
+ } catch {
51
+ return;
52
+ }
53
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
54
+ const record = parsed;
55
+ if (record.version !== PEER_CONFIG_VERSION || !validId(record.hostId) || !Array.isArray(record.peers)) return void 0;
56
+ const peers = record.peers.map(peerOf);
57
+ if (peers.some((peer) => peer === void 0)) return void 0;
58
+ const resolved = peers;
59
+ if (new Set(resolved.map((peer) => peer.hostId)).size !== resolved.length || resolved.some((peer) => peer.hostId === record.hostId)) return void 0;
60
+ return {
61
+ version: PEER_CONFIG_VERSION,
62
+ hostId: record.hostId,
63
+ peers: resolved
64
+ };
65
+ }
66
+ /** `peer/<host-id>/<session-id>` is the only remote target spelling. */
67
+ function parseRemoteSessionReference(value) {
68
+ const match = /^peer\/([^/]+)\/([^/]+)$/u.exec(value);
69
+ return match && validId(match[1]) && validId(match[2]) ? {
70
+ hostId: match[1],
71
+ sessionId: match[2]
72
+ } : void 0;
73
+ }
74
+ function peerSessionReference(hostId, sessionId) {
75
+ if (!validId(hostId) || !validId(sessionId)) throw new TypeError("Remote host and session identifiers must be safe opaque IDs.");
76
+ return `peer/${hostId}/${sessionId}`;
77
+ }
78
+ function signature(secret, method, pathname, timestamp, body) {
79
+ const digest = createHash("sha256").update(body).digest("base64url");
80
+ return createHmac("sha256", secret).update(`${method}\n${pathname}\n${timestamp}\n${digest}`).digest("base64url");
81
+ }
82
+ function peerRequestHeaders(sourceHostId, peer, method, pathname, body, now = Date.now()) {
83
+ if (!validId(sourceHostId)) throw new TypeError("Source host identity must be a safe opaque ID.");
84
+ const timestamp = String(now);
85
+ return new Headers({
86
+ "content-type": "application/json",
87
+ [PEER_HEADER]: sourceHostId,
88
+ [TIMESTAMP_HEADER]: timestamp,
89
+ [SIGNATURE_HEADER]: signature(peer.secret, method, pathname, timestamp, body)
90
+ });
91
+ }
92
+ /** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */
93
+ function createPeerReplayGuard(homeDirectory) {
94
+ const directory = path.dirname(sessionPeerConfigPath(homeDirectory));
95
+ fs.mkdirSync(directory, {
96
+ recursive: true,
97
+ mode: 448
98
+ });
99
+ const database = new DatabaseSync(path.join(directory, REPLAY_DATABASE_FILE));
100
+ database.exec(`
101
+ PRAGMA journal_mode = WAL;
102
+ CREATE TABLE IF NOT EXISTS peer_request_replays (
103
+ signature TEXT PRIMARY KEY,
104
+ expires_at INTEGER NOT NULL
105
+ );
106
+ `);
107
+ const removeExpired = database.prepare("DELETE FROM peer_request_replays WHERE expires_at < ?");
108
+ const insert = database.prepare("INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?)");
109
+ const count = database.prepare("SELECT COUNT(*) AS count FROM peer_request_replays");
110
+ const trim = database.prepare(`
111
+ DELETE FROM peer_request_replays
112
+ WHERE signature IN (
113
+ SELECT signature FROM peer_request_replays ORDER BY expires_at ASC, signature ASC LIMIT ?
114
+ )
115
+ `);
116
+ return {
117
+ admit(request, now = Date.now()) {
118
+ const supplied = request.headers.get(SIGNATURE_HEADER);
119
+ if (!supplied) return false;
120
+ database.exec("BEGIN IMMEDIATE");
121
+ try {
122
+ removeExpired.run(now);
123
+ const accepted = insert.run(supplied, now + MAX_CLOCK_SKEW_MS).changes === 1;
124
+ const total = Number(count.get().count);
125
+ if (total > MAX_REPLAY_ENTRIES) trim.run(total - MAX_REPLAY_ENTRIES);
126
+ database.exec("COMMIT");
127
+ return accepted;
128
+ } catch (cause) {
129
+ database.exec("ROLLBACK");
130
+ throw cause;
131
+ }
132
+ },
133
+ close: () => database.close()
134
+ };
135
+ }
136
+ function verifyPeerRequest(config, request, body, now = Date.now()) {
137
+ const hostId = request.headers.get(PEER_HEADER);
138
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
139
+ const supplied = request.headers.get(SIGNATURE_HEADER);
140
+ if (!hostId || !timestamp || !supplied || !/^\d{1,16}$/u.test(timestamp)) return void 0;
141
+ const sentAt = Number(timestamp);
142
+ if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > MAX_CLOCK_SKEW_MS) return void 0;
143
+ const peer = config.peers.find((candidate) => candidate.hostId === hostId);
144
+ if (!peer) return void 0;
145
+ const expected = Buffer.from(signature(peer.secret, request.method, new URL(request.url).pathname, timestamp, body));
146
+ const actual = Buffer.from(supplied);
147
+ return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected) ? peer : void 0;
148
+ }
149
+ function envelopeOf$1(value) {
150
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
151
+ const record = value;
152
+ return validId(record.sourceSessionId) && validId(record.targetSessionId) && typeof record.type === "string" && record.type !== "" ? {
153
+ sourceSessionId: record.sourceSessionId,
154
+ targetSessionId: record.targetSessionId,
155
+ type: record.type,
156
+ payload: record.payload
157
+ } : void 0;
158
+ }
159
+ function parseSessionPeerEnvelope(value) {
160
+ return envelopeOf$1(value);
161
+ }
162
+ /** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */
163
+ function createPeerCommunication(options) {
164
+ const config = readSessionPeerConfig(options.homeDirectory);
165
+ const send = options.fetch ?? fetch;
166
+ return {
167
+ sessionId: options.local.sessionId,
168
+ publish(target, type, payload) {
169
+ const remote = parseRemoteSessionReference(target);
170
+ if (!remote) return options.local.publish(target, type, payload);
171
+ const peer = config?.peers.find((candidate) => candidate.hostId === remote.hostId);
172
+ if (!config || !peer || !peer.allowedSessionIds.includes(remote.sessionId)) return false;
173
+ const body = JSON.stringify({
174
+ sourceSessionId: options.local.sessionId,
175
+ targetSessionId: remote.sessionId,
176
+ type,
177
+ payload
178
+ });
179
+ const url = new URL(PEER_ROUTE, peer.url);
180
+ send(url, {
181
+ method: "POST",
182
+ headers: peerRequestHeaders(config.hostId, peer, "POST", "/inbox", body),
183
+ body
184
+ }).catch(() => void 0);
185
+ return true;
186
+ },
187
+ subscribe: (type, listener) => options.local.subscribe(type, listener),
188
+ onPeerReady: (listener) => options.local.onPeerReady(listener),
189
+ close: () => options.local.close()
190
+ };
191
+ }
192
+ //#endregion
193
+ //#region src/services/sessionDirectory/index.ts
194
+ const UNKNOWN_FACTS = Object.freeze({
195
+ reachability: "unknown",
196
+ runtime: "unknown",
197
+ activity: "unknown",
198
+ voice: Object.freeze({
199
+ eligible: "unknown",
200
+ readiness: "unknown"
201
+ })
202
+ });
203
+ function keyOf(observation) {
204
+ return peerSessionReference(observation.hostId, observation.sessionId);
205
+ }
206
+ function publicEntry(observation, now, label, capabilities) {
207
+ const freshUntil = observation.observedAt + observation.staleAfterMs;
208
+ const fresh = now <= freshUntil;
209
+ const facts = fresh ? observation : UNKNOWN_FACTS;
210
+ return Object.freeze({
211
+ reference: keyOf(observation),
212
+ ...label === void 0 ? {} : { label },
213
+ capabilities: Object.freeze([...capabilities]),
214
+ hostIncarnation: observation.hostIncarnation,
215
+ sessionIncarnation: observation.sessionIncarnation,
216
+ sequence: observation.sequence,
217
+ observedAt: observation.observedAt,
218
+ freshUntil,
219
+ fresh,
220
+ reachability: facts.reachability,
221
+ runtime: facts.runtime,
222
+ activity: facts.activity,
223
+ voice: Object.freeze({ ...facts.voice })
224
+ });
225
+ }
226
+ /** In-memory authenticated presence cache. Restart deliberately forgets every observation. */
227
+ function createSessionDirectory(options) {
228
+ const now = options.now ?? Date.now;
229
+ const observations = /* @__PURE__ */ new Map();
230
+ const snapshotRequired = /* @__PURE__ */ new Set();
231
+ const subscribers = /* @__PURE__ */ new Set();
232
+ const discover = (callerReference, allowedReferences) => {
233
+ const result = [];
234
+ for (const observation of observations.values()) {
235
+ const reference = keyOf(observation);
236
+ if (allowedReferences && !allowedReferences.has(reference)) continue;
237
+ const grant = options.authorizeDiscovery(callerReference, observation);
238
+ if (!grant) continue;
239
+ result.push(publicEntry(observation, now(), grant.label ?? observation.label, grant.capabilities));
240
+ }
241
+ return result.sort((left, right) => left.reference.localeCompare(right.reference));
242
+ };
243
+ const notify = () => {
244
+ for (const subscriber of subscribers) subscriber.listener(discover(subscriber.callerReference));
245
+ };
246
+ return {
247
+ observe(observation) {
248
+ const key = keyOf(observation);
249
+ const current = observations.get(key);
250
+ if (snapshotRequired.has(observation.hostId)) return false;
251
+ if (current?.hostIncarnation === observation.hostIncarnation) {
252
+ if (current.sessionIncarnation === observation.sessionIncarnation && observation.sequence <= current.sequence) return false;
253
+ if (observation.sequence !== current.sequence + 1) {
254
+ snapshotRequired.add(observation.hostId);
255
+ for (const [reference, candidate] of observations) if (candidate.hostId === observation.hostId) observations.set(reference, {
256
+ ...candidate,
257
+ ...UNKNOWN_FACTS,
258
+ observedAt: now(),
259
+ staleAfterMs: 0
260
+ });
261
+ notify();
262
+ return false;
263
+ }
264
+ }
265
+ observations.set(key, Object.freeze({
266
+ ...observation,
267
+ voice: Object.freeze({ ...observation.voice })
268
+ }));
269
+ notify();
270
+ return true;
271
+ },
272
+ replaceHostSnapshot(hostId, hostIncarnation, replacements) {
273
+ if (replacements.some((observation) => observation.hostId !== hostId || observation.hostIncarnation !== hostIncarnation)) throw new TypeError("A presence snapshot must contain only the declared host incarnation.");
274
+ for (const [reference, observation] of observations) if (observation.hostId === hostId) observations.delete(reference);
275
+ for (const observation of replacements) observations.set(keyOf(observation), Object.freeze({
276
+ ...observation,
277
+ voice: Object.freeze({ ...observation.voice })
278
+ }));
279
+ snapshotRequired.delete(hostId);
280
+ notify();
281
+ },
282
+ hostDisconnected(hostId) {
283
+ for (const [reference, observation] of observations) {
284
+ if (observation.hostId !== hostId) continue;
285
+ observations.set(reference, {
286
+ ...observation,
287
+ reachability: "unreachable"
288
+ });
289
+ }
290
+ notify();
291
+ },
292
+ discover,
293
+ resolveDeliveryTarget(callerReference, reference) {
294
+ const observation = observations.get(reference);
295
+ return observation && options.authorizeDiscovery(callerReference, observation) ? observation.deliveryTarget : void 0;
296
+ },
297
+ subscribe(callerReference, listener) {
298
+ const subscriber = {
299
+ callerReference,
300
+ listener
301
+ };
302
+ subscribers.add(subscriber);
303
+ listener(discover(callerReference));
304
+ return () => subscribers.delete(subscriber);
305
+ }
306
+ };
307
+ }
308
+ //#endregion
309
+ //#region src/services/sessionGroups/type.ts
310
+ var SessionGroupRevisionError = class extends Error {
311
+ constructor(groupId, expected, actual) {
312
+ super(`Session group '${groupId}' revision conflict: expected ${expected}, current ${actual}.`);
313
+ this.name = "SessionGroupRevisionError";
314
+ }
315
+ };
316
+ //#endregion
317
+ //#region src/services/sessionGroups/index.ts
318
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
319
+ const REFERENCE_PATTERN = /^peer\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
320
+ function validateGroupId(value) {
321
+ if (!ID_PATTERN.test(value)) throw new TypeError("Session group IDs must be safe opaque IDs.");
322
+ }
323
+ function validateReference(value) {
324
+ if (!REFERENCE_PATTERN.test(value)) throw new TypeError(`Session group member '${value}' is not a qualified reference.`);
325
+ }
326
+ /** Opens a revisioned SQLite registry for communication-only group membership. */
327
+ function createSessionGroupStore(options) {
328
+ const database = new DatabaseSync(options.databasePath);
329
+ database.exec(`
330
+ PRAGMA journal_mode = WAL;
331
+ PRAGMA foreign_keys = ON;
332
+ CREATE TABLE IF NOT EXISTS session_groups (
333
+ group_id TEXT PRIMARY KEY,
334
+ owner_authority TEXT NOT NULL,
335
+ revision INTEGER NOT NULL
336
+ );
337
+ CREATE TABLE IF NOT EXISTS session_group_members (
338
+ group_id TEXT NOT NULL REFERENCES session_groups(group_id) ON DELETE CASCADE,
339
+ member_reference TEXT NOT NULL,
340
+ PRIMARY KEY (group_id, member_reference)
341
+ );
342
+ `);
343
+ const read = (groupId) => {
344
+ const row = database.prepare("SELECT * FROM session_groups WHERE group_id = ?").get(groupId);
345
+ if (!row) return void 0;
346
+ const members = database.prepare("SELECT member_reference FROM session_group_members WHERE group_id = ? ORDER BY member_reference").all(groupId);
347
+ return Object.freeze({
348
+ id: row.group_id,
349
+ ownerAuthority: row.owner_authority,
350
+ revision: row.revision,
351
+ members: Object.freeze(members.map((member) => member.member_reference))
352
+ });
353
+ };
354
+ const enroll = (groupId, members) => {
355
+ const insert = database.prepare("INSERT OR IGNORE INTO session_group_members (group_id, member_reference) VALUES (?, ?)");
356
+ for (const member of new Set(members)) {
357
+ validateReference(member);
358
+ if (options.validateEnrollment?.(groupId, member) === false) throw new Error(`Session group enrollment policy rejected '${member}'.`);
359
+ insert.run(groupId, member);
360
+ }
361
+ };
362
+ return {
363
+ create(groupId, ownerAuthority, members) {
364
+ validateGroupId(groupId);
365
+ if (!ownerAuthority.trim()) throw new TypeError("Session group owner authority is required.");
366
+ database.exec("BEGIN IMMEDIATE");
367
+ try {
368
+ database.prepare("INSERT INTO session_groups (group_id, owner_authority, revision) VALUES (?, ?, 1)").run(groupId, ownerAuthority);
369
+ enroll(groupId, members);
370
+ database.exec("COMMIT");
371
+ } catch (cause) {
372
+ database.exec("ROLLBACK");
373
+ throw cause;
374
+ }
375
+ return read(groupId);
376
+ },
377
+ mutate(mutation) {
378
+ database.exec("BEGIN IMMEDIATE");
379
+ try {
380
+ const current = read(mutation.groupId);
381
+ if (!current) throw new Error(`Session group '${mutation.groupId}' does not exist.`);
382
+ if (mutation.authority !== current.ownerAuthority && options.authorizeManagement?.(mutation.authority, current) !== true) throw new Error(`Authority '${mutation.authority}' cannot manage Session group '${mutation.groupId}'.`);
383
+ if (mutation.expectedRevision !== current.revision) throw new SessionGroupRevisionError(mutation.groupId, mutation.expectedRevision, current.revision);
384
+ enroll(mutation.groupId, mutation.add ?? []);
385
+ const remove = database.prepare("DELETE FROM session_group_members WHERE group_id = ? AND member_reference = ?");
386
+ for (const member of new Set(mutation.remove ?? [])) {
387
+ validateReference(member);
388
+ remove.run(mutation.groupId, member);
389
+ }
390
+ database.prepare("UPDATE session_groups SET revision = revision + 1 WHERE group_id = ? AND revision = ?").run(mutation.groupId, current.revision);
391
+ database.exec("COMMIT");
392
+ } catch (cause) {
393
+ database.exec("ROLLBACK");
394
+ throw cause;
395
+ }
396
+ return read(mutation.groupId);
397
+ },
398
+ get: read,
399
+ groupsFor(memberReference) {
400
+ validateReference(memberReference);
401
+ return database.prepare(`SELECT g.group_id FROM session_groups g
402
+ INNER JOIN session_group_members m ON m.group_id = g.group_id
403
+ WHERE m.member_reference = ? ORDER BY g.group_id`).all(memberReference).map((row) => read(row.group_id));
404
+ },
405
+ canMessage(groupId, sourceReference, targetReference) {
406
+ validateReference(sourceReference);
407
+ validateReference(targetReference);
408
+ const row = database.prepare(`SELECT COUNT(*) AS count FROM session_group_members
409
+ WHERE group_id = ? AND member_reference IN (?, ?)`).get(groupId, sourceReference, targetReference);
410
+ return Number(row.count) === (sourceReference === targetReference ? 1 : 2);
411
+ },
412
+ close: () => database.close()
413
+ };
414
+ }
415
+ //#endregion
416
+ //#region src/services/sessionIntercom/type.ts
417
+ const DOOM_SESSION_INTERCOM_SERVICE = "doom/session-intercom";
418
+ function readSessionIntercom(context) {
419
+ return context.get(DOOM_SESSION_INTERCOM_SERVICE);
420
+ }
421
+ function requireSessionIntercom(context) {
422
+ const service = readSessionIntercom(context);
423
+ if (!service) throw new Error("Session intercom is unavailable. Activate Session with a configured persistent host identity.");
424
+ return service;
425
+ }
426
+ //#endregion
427
+ //#region src/services/sessionIntercom/index.ts
428
+ const MESSAGE_KIND = "session-message";
429
+ const REPORT_KIND = "session-report";
430
+ function metadataFor(options, request, reportStatus) {
431
+ const group = options.groups.get(request.groupId);
432
+ if (!group || !options.groups.canMessage(request.groupId, options.selfReference, request.targetReference)) throw new Error(`Session group '${request.groupId}' does not authorize this message.`);
433
+ return {
434
+ sourceReference: options.selfReference,
435
+ targetReference: request.targetReference,
436
+ groupId: request.groupId,
437
+ groupRevision: String(group.revision),
438
+ ...request.correlationId === void 0 ? {} : { correlationId: request.correlationId },
439
+ ...reportStatus === void 0 ? {} : { reportStatus }
440
+ };
441
+ }
442
+ /** Agent-facing intercom operations. A composition may acquire this service without gaining session controls. */
443
+ function createSessionIntercom(options) {
444
+ const deliver = async (request, kind, status) => {
445
+ const recipientKey = options.directory.resolveDeliveryTarget(options.selfReference, request.targetReference);
446
+ if (!recipientKey) throw new Error(`Session '${request.targetReference}' is not discoverable.`);
447
+ const metadata = metadataFor(options, request, status);
448
+ return options.delivery.deliver({
449
+ ...request.deliveryId === void 0 ? {} : { deliveryId: request.deliveryId },
450
+ recipientKey,
451
+ kind,
452
+ prompt: `[Session ${status === void 0 ? "message" : `report: ${status}`} from ${options.selfReference}] ${request.text}`,
453
+ metadata
454
+ });
455
+ };
456
+ return Object.freeze({
457
+ discover() {
458
+ const members = new Set(options.groups.groupsFor(options.selfReference).flatMap((group) => group.members).filter((reference) => reference !== options.selfReference));
459
+ return options.directory.discover(options.selfReference, members);
460
+ },
461
+ send: (request) => deliver(request, MESSAGE_KIND),
462
+ report: (request) => deliver(request, REPORT_KIND, request.status),
463
+ messages() {
464
+ return options.delivery.inbox().filter((entry) => entry.kind === MESSAGE_KIND || entry.kind === REPORT_KIND).filter((entry) => {
465
+ const groupId = entry.metadata.groupId;
466
+ return groupId !== void 0 && options.groups.get(groupId)?.members.includes(options.selfReference) === true;
467
+ }).flatMap((entry) => {
468
+ const sourceReference = entry.metadata.sourceReference;
469
+ const targetReference = entry.metadata.targetReference;
470
+ const groupId = entry.metadata.groupId;
471
+ if (!sourceReference || !targetReference || !groupId) return [];
472
+ const reportStatus = entry.metadata.reportStatus;
473
+ return [{
474
+ deliveryId: entry.deliveryId,
475
+ sourceReference,
476
+ targetReference,
477
+ groupId,
478
+ ...entry.metadata.correlationId === void 0 ? {} : { correlationId: entry.metadata.correlationId },
479
+ text: entry.prompt,
480
+ ...reportStatus === "done" || reportStatus === "blocked" || reportStatus === "failed" ? { reportStatus } : {},
481
+ state: entry.state,
482
+ consumed: entry.consumed
483
+ }];
484
+ });
485
+ },
486
+ delivery(deliveryId) {
487
+ const entry = options.delivery.outbox(deliveryId);
488
+ const groupId = entry?.metadata.groupId;
489
+ return entry && groupId !== void 0 && options.groups.get(groupId)?.members.includes(options.selfReference) === true ? entry : void 0;
490
+ }
491
+ });
492
+ }
493
+ /** Builds the dynamic delivery check used before every send, retry, and recipient acceptance. */
494
+ function createSessionGroupDeliveryAuthorizer(options) {
495
+ return (peerKey, metadata) => {
496
+ const sourceReference = metadata?.sourceReference;
497
+ const targetReference = metadata?.targetReference;
498
+ const groupId = metadata?.groupId;
499
+ if (sourceReference && targetReference && groupId) {
500
+ const peerReference = options.peerReference(peerKey);
501
+ if (!peerReference) return false;
502
+ return (sourceReference === options.selfReference && targetReference === peerReference || targetReference === options.selfReference && sourceReference === peerReference) && options.groups.canMessage(groupId, sourceReference, targetReference);
503
+ }
504
+ return options.fallback?.(peerKey, metadata) === true;
505
+ };
506
+ }
507
+ //#endregion
508
+ //#region src/services/sessionDelivery/type.ts
509
+ const DOOM_SESSION_DELIVERY_SERVICE = "doom/session-delivery";
510
+ /** Reads the currently active provider each time, without retaining a stale binding. */
511
+ function readDoomSessionDelivery(context) {
512
+ return context.get(DOOM_SESSION_DELIVERY_SERVICE);
513
+ }
514
+ //#endregion
515
+ //#region src/services/sessionDelivery/index.ts
516
+ const DELIVERY_EVENT = "doom/session-delivery/envelope";
517
+ const ACK_EVENT = "doom/session-delivery/ack";
518
+ const DEFAULT_DELIVERY = "prompt";
519
+ const INBOX_STATES = /* @__PURE__ */ new Set([
520
+ "accepted",
521
+ "admitting",
522
+ "admitted",
523
+ "recovery_required"
524
+ ]);
525
+ function isRecord(value) {
526
+ return typeof value === "object" && value !== null && !Array.isArray(value);
527
+ }
528
+ function isMode(value) {
529
+ return value === "prompt" || value === "steer" || value === "followUp";
530
+ }
531
+ function metadataOf(value) {
532
+ if (!isRecord(value)) return void 0;
533
+ const metadata = {};
534
+ for (const [key, item] of Object.entries(value)) {
535
+ if (typeof item !== "string") return void 0;
536
+ metadata[key] = item;
537
+ }
538
+ return metadata;
539
+ }
540
+ function envelopeOf(value) {
541
+ if (!isRecord(value)) return void 0;
542
+ const metadata = metadataOf(value.metadata);
543
+ if (typeof value.deliveryId !== "string" || !value.deliveryId || typeof value.kind !== "string" || !value.kind || typeof value.prompt !== "string" || !isMode(value.delivery) || metadata === void 0) return;
544
+ return {
545
+ deliveryId: value.deliveryId,
546
+ kind: value.kind,
547
+ prompt: value.prompt,
548
+ delivery: value.delivery,
549
+ metadata
550
+ };
551
+ }
552
+ function ackOf(value) {
553
+ if (!isRecord(value)) return void 0;
554
+ if (typeof value.deliveryId !== "string" || typeof value.state !== "string" || !INBOX_STATES.has(value.state)) return;
555
+ return value;
556
+ }
557
+ function normalizedMetadata(metadata) {
558
+ const normalized = {};
559
+ for (const [key, value] of Object.entries(metadata ?? {})) if (value !== void 0) normalized[key] = value;
560
+ return normalized;
561
+ }
562
+ function matchesMetadata(entry, query) {
563
+ return Object.entries(query ?? {}).every(([key, value]) => value === void 0 || entry[key] === value);
564
+ }
565
+ function sameMetadata(left, right) {
566
+ return matchesMetadata(left, right) && matchesMetadata(right, left);
567
+ }
568
+ /** Opens and starts one durable delivery endpoint. */
569
+ function createSessionDeliveryService(options) {
570
+ if (!options.recipientKey.trim() || options.recipientKey !== options.recipientKey.trim()) throw new TypeError("Session delivery requires a trimmed, non-empty recipient key.");
571
+ const database = new DatabaseSync(options.databasePath);
572
+ database.exec(`
573
+ PRAGMA journal_mode = WAL;
574
+ CREATE TABLE IF NOT EXISTS session_delivery_outbox (
575
+ delivery_id TEXT PRIMARY KEY,
576
+ recipient_key TEXT NOT NULL,
577
+ kind TEXT NOT NULL,
578
+ prompt TEXT NOT NULL,
579
+ delivery_mode TEXT NOT NULL,
580
+ metadata_json TEXT NOT NULL,
581
+ state TEXT NOT NULL,
582
+ recipient_state TEXT,
583
+ created_at INTEGER NOT NULL,
584
+ acknowledged_at INTEGER
585
+ );
586
+ CREATE TABLE IF NOT EXISTS session_delivery_inbox (
587
+ delivery_id TEXT PRIMARY KEY,
588
+ sender_key TEXT NOT NULL,
589
+ recipient_key TEXT NOT NULL,
590
+ kind TEXT NOT NULL,
591
+ prompt TEXT NOT NULL,
592
+ delivery_mode TEXT NOT NULL,
593
+ metadata_json TEXT NOT NULL,
594
+ state TEXT NOT NULL,
595
+ created_at INTEGER NOT NULL,
596
+ updated_at INTEGER NOT NULL,
597
+ consumed_at INTEGER
598
+ );
599
+ `);
600
+ database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE state = 'admitting'").run(Date.now());
601
+ let closed = false;
602
+ const scheduled = /* @__PURE__ */ new Set();
603
+ let admissionQueue = Promise.resolve();
604
+ const publishAck = (deliveryId, senderKey, state) => {
605
+ options.communication.publish(senderKey, ACK_EVENT, {
606
+ deliveryId,
607
+ state
608
+ });
609
+ };
610
+ const readInboxRow = (deliveryId) => database.prepare("SELECT * FROM session_delivery_inbox WHERE delivery_id = ?").get(deliveryId);
611
+ const scheduleAdmission = (deliveryId) => {
612
+ if (closed || scheduled.has(deliveryId)) return;
613
+ scheduled.add(deliveryId);
614
+ admissionQueue = admissionQueue.then(async () => {
615
+ try {
616
+ if (closed) return;
617
+ const row = readInboxRow(deliveryId);
618
+ if (!row || row.state !== "accepted") return;
619
+ database.prepare("UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
620
+ publishAck(deliveryId, row.sender_key, "admitting");
621
+ try {
622
+ await options.admitPrompt(row.prompt, row.delivery_mode);
623
+ } catch {
624
+ database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
625
+ publishAck(deliveryId, row.sender_key, "recovery_required");
626
+ return;
627
+ }
628
+ database.prepare("UPDATE session_delivery_inbox SET state = 'admitted', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
629
+ publishAck(deliveryId, row.sender_key, "admitted");
630
+ } finally {
631
+ scheduled.delete(deliveryId);
632
+ }
633
+ }).catch(() => {
634
+ scheduled.delete(deliveryId);
635
+ try {
636
+ const row = readInboxRow(deliveryId);
637
+ if (row?.state !== "admitting") return;
638
+ database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
639
+ try {
640
+ publishAck(deliveryId, row.sender_key, "recovery_required");
641
+ } catch {}
642
+ } catch {}
643
+ });
644
+ };
645
+ const receiveEnvelope = (senderKey, value) => {
646
+ if (closed) return void 0;
647
+ const envelope = envelopeOf(value);
648
+ if (!envelope || !options.authorizePeer(senderKey, envelope.metadata)) return void 0;
649
+ database.prepare(`INSERT OR IGNORE INTO session_delivery_inbox
650
+ (delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)
651
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`).run(envelope.deliveryId, senderKey, options.recipientKey, envelope.kind, envelope.prompt, envelope.delivery, JSON.stringify(envelope.metadata), Date.now(), Date.now());
652
+ const current = readInboxRow(envelope.deliveryId);
653
+ if (!current) return void 0;
654
+ if (!(current.sender_key === senderKey && current.recipient_key === options.recipientKey && current.kind === envelope.kind && current.prompt === envelope.prompt && current.delivery_mode === envelope.delivery && sameMetadata(JSON.parse(current.metadata_json), envelope.metadata))) return void 0;
655
+ publishAck(envelope.deliveryId, current.sender_key, current.state);
656
+ if (current.state === "accepted") scheduleAdmission(envelope.deliveryId);
657
+ return current.state;
658
+ };
659
+ const receiveAck = (recipientKey, value) => {
660
+ const ack = ackOf(value);
661
+ if (closed || !ack) return;
662
+ database.prepare(`UPDATE session_delivery_outbox
663
+ SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?
664
+ WHERE delivery_id = ? AND recipient_key = ?
665
+ AND (recipient_state IS NULL
666
+ OR (recipient_state = 'accepted' AND ? != 'accepted')
667
+ OR (recipient_state = 'admitting' AND ? IN ('admitted', 'recovery_required')))`).run(ack.state, Date.now(), ack.deliveryId, recipientKey, ack.state, ack.state);
668
+ };
669
+ const publishOutbox = (deliveryId) => {
670
+ const row = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND (recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required'))").get(deliveryId);
671
+ if (!row) return;
672
+ const metadata = JSON.parse(row.metadata_json);
673
+ if (!options.authorizePeer(row.recipient_key, metadata)) return;
674
+ options.communication.publish(row.recipient_key, DELIVERY_EVENT, {
675
+ deliveryId: row.delivery_id,
676
+ kind: row.kind,
677
+ prompt: row.prompt,
678
+ delivery: row.delivery_mode,
679
+ metadata
680
+ });
681
+ };
682
+ const publishQueued = (recipientKey) => {
683
+ for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND (recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required'))").all(recipientKey)) publishOutbox(row.delivery_id);
684
+ };
685
+ const publishAllQueued = () => {
686
+ for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_state IS NULL OR recipient_state NOT IN ('admitted', 'recovery_required')").all()) publishOutbox(row.delivery_id);
687
+ };
688
+ const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, receiveEnvelope);
689
+ const releaseAck = options.communication.subscribe(ACK_EVENT, receiveAck);
690
+ const releaseReady = options.communication.onPeerReady(publishQueued);
691
+ for (const row of database.prepare("SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'").all()) publishAck(row.delivery_id, row.sender_key, "recovery_required");
692
+ for (const row of database.prepare("SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'").all()) scheduleAdmission(row.delivery_id);
693
+ publishAllQueued();
694
+ const retryTimer = setInterval(publishAllQueued, 1e3);
695
+ retryTimer.unref();
696
+ return Object.freeze({
697
+ recipientKey: options.recipientKey,
698
+ async deliver(request) {
699
+ if (closed) throw new Error("Session delivery service is closed.");
700
+ const deliveryId = request.deliveryId ?? crypto.randomUUID();
701
+ const mode = request.delivery ?? DEFAULT_DELIVERY;
702
+ if (!deliveryId.trim() || !request.recipientKey.trim() || !request.kind.trim() || !isMode(mode)) throw new TypeError("Delivery id, recipient key, kind, and mode must be valid.");
703
+ if (!options.authorizePeer(request.recipientKey, request.metadata)) throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);
704
+ const metadata = normalizedMetadata(request.metadata);
705
+ database.prepare(`INSERT OR IGNORE INTO session_delivery_outbox
706
+ (delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)
707
+ VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`).run(deliveryId, request.recipientKey, request.kind, request.prompt, mode, JSON.stringify(metadata), Date.now());
708
+ const current = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
709
+ if (!current || current.recipient_key !== request.recipientKey || current.kind !== request.kind || current.prompt !== request.prompt || current.delivery_mode !== mode || !sameMetadata(JSON.parse(current.metadata_json), metadata)) throw new TypeError(`Delivery id '${deliveryId}' is already bound to another envelope.`);
710
+ publishOutbox(deliveryId);
711
+ return { deliveryId };
712
+ },
713
+ async waitForAdmission(deliveryId, timeoutMs = 5e3) {
714
+ const deadline = Date.now() + Math.max(0, timeoutMs);
715
+ let state;
716
+ do {
717
+ state = database.prepare("SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId)?.recipient_state ?? void 0;
718
+ if (state === "admitted" || state === "recovery_required") return state;
719
+ if (Date.now() >= deadline) return state;
720
+ await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
721
+ } while (!closed);
722
+ return state;
723
+ },
724
+ receive(senderKey, kind, payload) {
725
+ if (kind === DELIVERY_EVENT) return receiveEnvelope(senderKey, payload);
726
+ if (kind === ACK_EVENT) {
727
+ receiveAck(senderKey, payload);
728
+ return "accepted";
729
+ }
730
+ },
731
+ inbox(query = {}) {
732
+ return database.prepare("SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id").all().map((row) => {
733
+ const metadata = JSON.parse(row.metadata_json);
734
+ return {
735
+ deliveryId: row.delivery_id,
736
+ senderKey: row.sender_key,
737
+ recipientKey: row.recipient_key,
738
+ kind: row.kind,
739
+ prompt: row.prompt,
740
+ delivery: row.delivery_mode,
741
+ metadata,
742
+ state: row.state,
743
+ consumed: row.consumed_at !== null
744
+ };
745
+ }).filter((entry) => (query.kind === void 0 || entry.kind === query.kind) && (query.state === void 0 || entry.state === query.state) && (query.consumed === void 0 || entry.consumed === query.consumed) && matchesMetadata(entry.metadata, query.metadata));
746
+ },
747
+ consume(deliveryId) {
748
+ return database.prepare("UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL").run(Date.now(), deliveryId).changes === 1;
749
+ },
750
+ outbox(deliveryId) {
751
+ const row = database.prepare("SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
752
+ if (!row) return void 0;
753
+ return {
754
+ deliveryId: row.delivery_id,
755
+ recipientKey: row.recipient_key,
756
+ kind: row.kind,
757
+ metadata: JSON.parse(row.metadata_json),
758
+ state: row.state,
759
+ ...row.recipient_state === null ? {} : { recipientState: row.recipient_state }
760
+ };
761
+ },
762
+ async close() {
763
+ if (closed) return;
764
+ closed = true;
765
+ clearInterval(retryTimer);
766
+ releaseEnvelope();
767
+ releaseAck();
768
+ releaseReady();
769
+ try {
770
+ await admissionQueue;
771
+ } finally {
772
+ database.close();
773
+ }
774
+ }
775
+ });
776
+ }
777
+ /**
778
+ * Installs a provider-owned endpoint in the current Cordis context.
779
+ *
780
+ * Returns the instance because a provider cannot read its own service back out:
781
+ * `ctx.get` is strict and hides an implementation while its providing fiber is
782
+ * LOADING, which is the whole of that fiber's own callback.
783
+ */
784
+ function provideSessionDeliveryService(context, options) {
785
+ const service = createSessionDeliveryService(options);
786
+ const unprovide = context.provide(DOOM_SESSION_DELIVERY_SERVICE, service);
787
+ context.effect(() => () => service.close(), "session delivery database");
788
+ return {
789
+ service,
790
+ close: async () => {
791
+ unprovide();
792
+ await service.close();
793
+ }
794
+ };
795
+ }
796
+ //#endregion
797
+ export { peerRequestHeaders as _, createSessionGroupDeliveryAuthorizer as a, sessionPeerConfigPath as b, readSessionIntercom as c, SessionGroupRevisionError as d, createSessionDirectory as f, parseSessionPeerEnvelope as g, parseRemoteSessionReference as h, readDoomSessionDelivery as i, requireSessionIntercom as l, createPeerReplayGuard as m, provideSessionDeliveryService as n, createSessionIntercom as o, createPeerCommunication as p, DOOM_SESSION_DELIVERY_SERVICE as r, DOOM_SESSION_INTERCOM_SERVICE as s, createSessionDeliveryService as t, createSessionGroupStore as u, peerSessionReference as v, verifyPeerRequest as x, readSessionPeerConfig as y };
798
+
799
+ //# sourceMappingURL=sessionDelivery-DNq4a8LB.mjs.map