@canonmsg/agent-sdk 11.2.1 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +248 -523
  2. package/dist/chunks/chunk-7N6OPPW4.js +5430 -0
  3. package/dist/chunks/chunk-N722GWCP.js +4897 -0
  4. package/dist/chunks/chunk-QZ4PPRQW.js +14247 -0
  5. package/dist/chunks/mls-crypto-LMNQZOS6.js +10 -0
  6. package/dist/cli.js +572 -0
  7. package/dist/drivers/claude.d.ts +42 -0
  8. package/dist/drivers/code.d.ts +18 -0
  9. package/dist/drivers/codex.d.ts +46 -0
  10. package/dist/drivers/connector.d.ts +34 -0
  11. package/dist/index.d.ts +20 -25
  12. package/dist/index.js +91 -13
  13. package/dist/internal/backend-contracts/agentBehaviorPolicy.d.ts +122 -0
  14. package/dist/internal/backend-contracts/agentDirectory.d.ts +32 -0
  15. package/dist/internal/backend-contracts/agentOrigin.d.ts +23 -0
  16. package/dist/internal/backend-contracts/agentSearch.d.ts +17 -0
  17. package/dist/internal/backend-contracts/api-origins.generated.d.ts +4 -0
  18. package/dist/internal/backend-contracts/apiErrors.d.ts +1 -0
  19. package/dist/internal/backend-contracts/artifactActions.d.ts +8 -0
  20. package/dist/internal/backend-contracts/artifacts.d.ts +386 -0
  21. package/dist/internal/backend-contracts/clientFloor.d.ts +41 -0
  22. package/dist/internal/backend-contracts/communication.d.ts +146 -0
  23. package/dist/internal/backend-contracts/connectorRelay.d.ts +179 -0
  24. package/dist/internal/backend-contracts/conversationMembership.d.ts +28 -0
  25. package/dist/internal/backend-contracts/deliveryObservation.d.ts +23 -0
  26. package/dist/internal/backend-contracts/diffRedaction.d.ts +6 -0
  27. package/dist/internal/backend-contracts/endpoint-unread.d.ts +4 -0
  28. package/dist/internal/backend-contracts/endpoint.d.ts +120 -0
  29. package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +48 -0
  30. package/dist/internal/backend-contracts/environment.d.ts +20 -0
  31. package/dist/internal/backend-contracts/externalInbound.d.ts +24 -0
  32. package/dist/internal/backend-contracts/firestoreValues.d.ts +9 -0
  33. package/dist/internal/backend-contracts/index.d.ts +54 -0
  34. package/dist/internal/backend-contracts/media-url.d.ts +20 -0
  35. package/dist/internal/backend-contracts/media.d.ts +51 -0
  36. package/dist/internal/backend-contracts/mediaDelivery.d.ts +46 -0
  37. package/dist/internal/backend-contracts/mediaTransferProtocol.d.ts +17 -0
  38. package/dist/internal/backend-contracts/membershipChange.d.ts +21 -0
  39. package/dist/internal/backend-contracts/message.d.ts +86 -0
  40. package/dist/internal/backend-contracts/messageText.d.ts +6 -0
  41. package/dist/internal/backend-contracts/mlsDevApplication.d.ts +161 -0
  42. package/dist/internal/backend-contracts/mlsDevCredential.d.ts +78 -0
  43. package/dist/internal/backend-contracts/mlsDevCredentialReconciliation.d.ts +45 -0
  44. package/dist/internal/backend-contracts/mlsDevDeviceCustody.d.ts +52 -0
  45. package/dist/internal/backend-contracts/mlsDevEnrollment.d.ts +75 -0
  46. package/dist/internal/backend-contracts/mlsDevFeedReceipt.d.ts +43 -0
  47. package/dist/internal/backend-contracts/mlsDevGroup.d.ts +321 -0
  48. package/dist/internal/backend-contracts/mlsDevGroupFeed.d.ts +51 -0
  49. package/dist/internal/backend-contracts/mlsDevInteraction.d.ts +159 -0
  50. package/dist/internal/backend-contracts/mlsDevKeyPackage.d.ts +42 -0
  51. package/dist/internal/backend-contracts/mlsDevKeyPackages.d.ts +55 -0
  52. package/dist/internal/backend-contracts/mlsEnvironment.d.ts +35 -0
  53. package/dist/internal/backend-contracts/mlsPrincipalAlias.d.ts +23 -0
  54. package/dist/internal/backend-contracts/moderation.d.ts +7 -0
  55. package/dist/internal/backend-contracts/plaintextProcessors.d.ts +63 -0
  56. package/dist/internal/backend-contracts/preparedContent.d.ts +54 -0
  57. package/dist/internal/backend-contracts/replyAuthority.d.ts +14 -0
  58. package/dist/internal/backend-contracts/runtimeCardFields.d.ts +66 -0
  59. package/dist/internal/backend-contracts/runtimeCardStorage.d.ts +3 -0
  60. package/dist/internal/backend-contracts/runtimeDescriptor.d.ts +88 -0
  61. package/dist/internal/backend-contracts/selfContext.d.ts +9 -0
  62. package/dist/internal/backend-contracts/turnProtocol.d.ts +43 -0
  63. package/dist/internal/backend-contracts/upload-mime-types.d.ts +17 -0
  64. package/dist/internal/backend-contracts/verbContract.d.ts +856 -0
  65. package/dist/internal/backend-contracts/verbSchemas.d.ts +1717 -0
  66. package/dist/internal/backend-contracts/verbWire.d.ts +493 -0
  67. package/dist/internal/backend-contracts/webAuth.d.ts +8 -0
  68. package/dist/internal/core/agent-profiles.d.ts +65 -0
  69. package/dist/internal/core/approval-format.d.ts +22 -0
  70. package/dist/internal/core/approval-manager.d.ts +43 -0
  71. package/dist/internal/core/approval-types.d.ts +73 -0
  72. package/dist/internal/core/base-url.d.ts +1 -0
  73. package/dist/internal/core/cli-metadata.d.ts +13 -0
  74. package/dist/internal/core/client.d.ts +432 -0
  75. package/dist/internal/core/completed-output-journal.d.ts +39 -0
  76. package/dist/internal/core/constants.d.ts +1 -0
  77. package/dist/internal/core/endpoint-client.d.ts +56 -0
  78. package/dist/internal/core/execution-environment-mode.d.ts +6 -0
  79. package/dist/internal/core/gifs.d.ts +29 -0
  80. package/dist/internal/core/host-runtime.d.ts +53 -0
  81. package/dist/internal/core/index.d.ts +68 -0
  82. package/dist/internal/core/node-media.d.ts +6 -0
  83. package/dist/internal/core/policy.d.ts +2 -0
  84. package/dist/internal/core/registration-profile.d.ts +10 -0
  85. package/dist/internal/core/registration-session.d.ts +55 -0
  86. package/dist/internal/core/registration.d.ts +5 -0
  87. package/dist/internal/core/retry.d.ts +113 -0
  88. package/dist/internal/core/rtdb-rest.d.ts +141 -0
  89. package/dist/internal/core/runtime-cards.d.ts +286 -0
  90. package/dist/internal/core/runtime-descriptor.d.ts +16 -0
  91. package/dist/internal/core/runtime-endpoints.d.ts +26 -0
  92. package/dist/internal/core/runtime-heartbeat.d.ts +29 -0
  93. package/dist/internal/core/runtime-presentation.d.ts +9 -0
  94. package/dist/internal/core/runtime-request-manager.d.ts +283 -0
  95. package/dist/internal/core/runtime-state-publisher.d.ts +42 -0
  96. package/dist/internal/core/self-context.d.ts +22 -0
  97. package/dist/internal/core/silent-turn.d.ts +49 -0
  98. package/dist/internal/core/stream.d.ts +78 -0
  99. package/dist/internal/core/turn-protocol.d.ts +28 -0
  100. package/dist/internal/core/turn-verbosity.d.ts +85 -0
  101. package/dist/internal/core/types.d.ts +964 -0
  102. package/dist/internal/core/unified-diff.d.ts +35 -0
  103. package/dist/internal/endpoint/artifact-images.d.ts +60 -0
  104. package/dist/internal/endpoint/artifact-reducer.d.ts +35 -0
  105. package/dist/internal/endpoint/artifact-views.d.ts +113 -0
  106. package/dist/internal/endpoint/artifacts.d.ts +75 -0
  107. package/dist/internal/endpoint/delivery.d.ts +47 -0
  108. package/dist/internal/endpoint/engine.d.ts +249 -0
  109. package/dist/internal/endpoint/ephemeral.d.ts +8 -0
  110. package/dist/internal/endpoint/events.d.ts +32 -0
  111. package/dist/internal/endpoint/external-inbound.d.ts +36 -0
  112. package/dist/internal/endpoint/handoff.d.ts +50 -0
  113. package/dist/internal/endpoint/history.d.ts +22 -0
  114. package/dist/internal/endpoint/image-generation-errors.d.ts +2 -0
  115. package/dist/internal/endpoint/inbound.d.ts +29 -0
  116. package/dist/internal/endpoint/index.d.ts +32 -0
  117. package/dist/internal/endpoint/media.d.ts +177 -0
  118. package/dist/internal/endpoint/migration.d.ts +50 -0
  119. package/dist/internal/endpoint/mls-crypto.d.ts +32 -0
  120. package/dist/internal/endpoint/mls-dev-client.d.ts +52 -0
  121. package/dist/internal/endpoint/mls-dev-device.d.ts +69 -0
  122. package/dist/internal/endpoint/mls-dev-groups.d.ts +174 -0
  123. package/dist/internal/endpoint/mls-dev-interactions.d.ts +99 -0
  124. package/dist/internal/endpoint/mls-dev-key-package-service.d.ts +46 -0
  125. package/dist/internal/endpoint/mls-dev-key-packages.d.ts +62 -0
  126. package/dist/internal/endpoint/mls-dev-text.d.ts +67 -0
  127. package/dist/internal/endpoint/mls-dev-transport.d.ts +38 -0
  128. package/dist/internal/endpoint/node-mls-custody-protocol.d.ts +29 -0
  129. package/dist/internal/endpoint/node-mls-custody.d.ts +15 -0
  130. package/dist/internal/endpoint/node.d.ts +7 -0
  131. package/dist/internal/endpoint/observe-query.d.ts +17 -0
  132. package/dist/internal/endpoint/portable-node-mls-custody.d.ts +23 -0
  133. package/dist/internal/endpoint/queue.d.ts +22 -0
  134. package/dist/internal/endpoint/recovery.d.ts +27 -0
  135. package/dist/internal/endpoint/retention.d.ts +7 -0
  136. package/dist/internal/endpoint/store.d.ts +22 -0
  137. package/dist/internal/endpoint/sync.d.ts +33 -0
  138. package/dist/internal/endpoint/transient-publisher.d.ts +8 -0
  139. package/dist/internal/endpoint/transport.d.ts +63 -0
  140. package/dist/media.d.ts +1 -1
  141. package/dist/media.js +42 -541
  142. package/dist/runner/errands.d.ts +118 -0
  143. package/dist/runner/mls.d.ts +97 -0
  144. package/dist/runner/relay.d.ts +87 -0
  145. package/dist/runner/runner.d.ts +98 -0
  146. package/dist/runner/types.d.ts +215 -0
  147. package/package.json +18 -5
  148. package/dist/attached-session.d.ts +0 -58
  149. package/dist/attached-session.js +0 -638
  150. package/dist/bounded-file-read.d.ts +0 -14
  151. package/dist/bounded-file-read.js +0 -34
  152. package/dist/canon-agent.d.ts +0 -236
  153. package/dist/canon-agent.js +0 -2531
  154. package/dist/debouncer.d.ts +0 -18
  155. package/dist/debouncer.js +0 -133
  156. package/dist/inbound-handoff.d.ts +0 -6
  157. package/dist/inbound-handoff.js +0 -17
  158. package/dist/managed-session-host.d.ts +0 -27
  159. package/dist/managed-session-host.js +0 -195
  160. package/dist/managed-session-setup.d.ts +0 -99
  161. package/dist/managed-session-setup.js +0 -234
  162. package/dist/realtime.d.ts +0 -62
  163. package/dist/realtime.js +0 -193
  164. package/dist/runtime-card.d.ts +0 -48
  165. package/dist/runtime-card.js +0 -64
  166. package/dist/session-manager.d.ts +0 -77
  167. package/dist/session-manager.js +0 -285
  168. package/dist/turn-streaming-request.d.ts +0 -17
  169. package/dist/turn-streaming-request.js +0 -22
  170. package/dist/turn-verbosity-option.d.ts +0 -33
  171. package/dist/turn-verbosity-option.js +0 -23
  172. package/dist/types.d.ts +0 -387
  173. package/dist/types.js +0 -1
  174. package/dist/work-session-coordination.d.ts +0 -21
  175. package/dist/work-session-coordination.js +0 -53
  176. package/dist/work-session-host.d.ts +0 -31
  177. package/dist/work-session-host.js +0 -617
  178. package/dist/work-session-interactions.d.ts +0 -12
  179. package/dist/work-session-interactions.js +0 -173
  180. package/dist/work-session-state.d.ts +0 -36
  181. package/dist/work-session-state.js +0 -62
@@ -1,617 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { ApprovalManager, CanonApiError, CanonClient, CanonStream, NativeWorkSessionCreateError, createRuntimeHeartbeat, createRuntimeStatePublisher, initRTDBAuth, resolveCanonRuntimeConnection, verifyCanonRuntimeConnection, } from '@canonmsg/core';
3
- import { createCanonAttachedSession, } from './attached-session.js';
4
- import { createWorkSessionJournal } from './work-session-state.js';
5
- import { createWorkSessionInteractionBridge } from './work-session-interactions.js';
6
- import { createWorkSessionAuthority, fingerprintWorkSessionRequest } from './work-session-coordination.js';
7
- /** One authenticated transport and liveness publisher, with durable per-room native routes. */
8
- export function createCanonWorkSessionHost(options) {
9
- const connection = { ...options.connection };
10
- const runtime = resolveCanonRuntimeConnection({ environmentId: connection.environmentId,
11
- apiBaseUrl: connection.baseUrl, streamUrl: connection.streamUrl,
12
- rtdbUrl: connection.rtdbUrl, firebaseWebApiKey: connection.firebaseApiKey });
13
- const client = new CanonClient(connection.apiKey, runtime.apiBaseUrl, { environmentId: runtime.environmentId, streamUrl: runtime.streamUrl });
14
- const journal = createWorkSessionJournal(options.store, {
15
- environmentId: connection.environmentId, agentId: connection.agentId, hostId: options.hostId,
16
- });
17
- const pollMs = options.pollIntervalMs ?? 2_000;
18
- const heartbeatMs = options.heartbeatIntervalMs ?? 25_000;
19
- const connectTimeoutMs = options.connectTimeoutMs ?? 20_000;
20
- if (![pollMs, heartbeatMs, connectTimeoutMs].every((n) => Number.isFinite(n) && n > 0)
21
- || heartbeatMs > 30_000 || !options.hostId.trim())
22
- throw new Error('Invalid work-session host configuration.');
23
- const runtimeEpoch = randomUUID();
24
- const rooms = new Map();
25
- const handlers = new Set();
26
- const pending = new Set();
27
- let broker;
28
- let manager;
29
- let publisher;
30
- let heartbeat;
31
- let transport;
32
- let stream;
33
- let connected = false;
34
- let agentContext;
35
- let stopped = false;
36
- const authority = createWorkSessionAuthority({ client, hostId: options.hostId,
37
- runtimeEpoch, displayName: options.displayName, isStopped: () => stopped });
38
- let startPromise;
39
- let stopPromise;
40
- let cleanupPromise;
41
- let pollTimer;
42
- let leaseTimer;
43
- let expiryTimer;
44
- let processing;
45
- let renewing;
46
- let reporting;
47
- let reportsRequested = false;
48
- let rejectConnection;
49
- let resolveConnection;
50
- const report = (error, operation) => { try {
51
- options.onError?.(error, operation);
52
- }
53
- catch { /* observer */ } };
54
- const emit = (event) => {
55
- try {
56
- options.onStatus?.(event);
57
- }
58
- catch (error) {
59
- report(error, 'status');
60
- }
61
- };
62
- const auth = authority.auth;
63
- const fence = authority.fence;
64
- function retainLease(state) {
65
- broker = authority.retain(state);
66
- if (expiryTimer)
67
- clearTimeout(expiryTimer);
68
- expiryTimer = setTimeout(() => fail(new Error('Work-session runtime lease expired.'), 'lease-expired'), Math.max(1, state.lease.expiresAt - Date.now() - 5_000));
69
- expiryTimer.unref?.();
70
- }
71
- function updatePresence() {
72
- const ready = connected && !stopped && [...rooms.values()].some((room) => !room.stopped && room.acknowledged && ['ready', 'uncertain'].includes(room.session.getState().status));
73
- if (ready)
74
- heartbeat?.connect();
75
- else
76
- void heartbeat?.disconnect().catch((error) => report(error, 'presence'));
77
- }
78
- function fail(error, operation) {
79
- report(error, operation);
80
- void stop().catch((stopError) => report(stopError, 'stop'));
81
- }
82
- function fanout(name, ...args) {
83
- for (const handler of [...handlers]) {
84
- try {
85
- handler[name]?.(...args);
86
- }
87
- catch (error) {
88
- report(error, `stream-${name}`);
89
- }
90
- }
91
- }
92
- async function removeRoom(conversationId, releaseBinding) {
93
- const room = rooms.get(conversationId);
94
- if (!room)
95
- return;
96
- rooms.delete(conversationId);
97
- room.stopped = true;
98
- room.controller.abort();
99
- try {
100
- await room.session.stop();
101
- }
102
- finally {
103
- room.releaseNative();
104
- updatePresence();
105
- }
106
- if (releaseBinding && !stopped) {
107
- fence();
108
- retainLease(await client.releaseWorkSessionBinding({ ...auth(), conversationId,
109
- workSessionId: room.binding.workSessionId }));
110
- }
111
- }
112
- function activate(conversationId) {
113
- const room = rooms.get(conversationId);
114
- if (!room || stopped)
115
- return;
116
- room.acknowledged = true;
117
- room.activate();
118
- room.session.refreshRuntimeState();
119
- updatePresence();
120
- scheduleReports();
121
- }
122
- async function reportRoom(room) {
123
- if (!room.acknowledged || room.stopped || !options.provider.describe)
124
- return;
125
- fence();
126
- const fields = await options.provider.describe(room.binding.nativeSessionId, room.binding.settings);
127
- if (stopped || rooms.get(room.binding.conversationId) !== room || room.stopped || !room.acknowledged)
128
- return;
129
- fence();
130
- await client.reportWorkSessionDetails({ ...auth(), conversationId: room.binding.conversationId,
131
- workSessionId: room.binding.workSessionId, setupState: 'attached', fields });
132
- }
133
- function scheduleReports() {
134
- if (stopped || !options.provider.describe)
135
- return;
136
- reportsRequested = true;
137
- if (reporting)
138
- return;
139
- reporting = Promise.resolve().then(async () => {
140
- while (reportsRequested && !stopped) {
141
- reportsRequested = false;
142
- const active = [...rooms.values()].filter((room) => room.acknowledged && !room.stopped);
143
- const outcomes = await Promise.allSettled(active.map(reportRoom));
144
- for (const outcome of outcomes) {
145
- if (outcome.status === 'rejected')
146
- report(outcome.reason, 'session-details');
147
- }
148
- }
149
- }).catch((error) => report(error, 'session-details')).finally(() => {
150
- reporting = undefined;
151
- if (reportsRequested && !stopped)
152
- scheduleReports();
153
- });
154
- }
155
- async function attach(binding, confirmed = false) {
156
- fence();
157
- if (typeof binding.nativeSessionId !== 'string')
158
- throw new Error('This host cannot attach a managed-session binding.');
159
- const existing = rooms.get(binding.conversationId);
160
- if (existing && existing.binding.nativeSessionId === binding.nativeSessionId && !existing.stopped) {
161
- if (confirmed)
162
- activate(binding.conversationId);
163
- return;
164
- }
165
- if (existing)
166
- throw new Error('This conversation already has a work session.');
167
- if ([...rooms.values()].some((room) => room.binding.nativeSessionId === binding.nativeSessionId))
168
- throw new Error('This native session already has a different Canon audience.');
169
- const conversation = await client.getConversation(binding.conversationId);
170
- if (!conversation?.memberIds.includes(connection.agentId) || !conversation.memberIds.includes(broker.ownerId))
171
- throw new Error('The owner and agent must both be members of this conversation.');
172
- fence();
173
- const nativeBinding = { ...binding, environmentId: connection.environmentId, agentId: connection.agentId };
174
- const releaseNative = await options.acquireNativeSession(nativeBinding);
175
- const controller = new AbortController();
176
- let activateRoute;
177
- const routeReady = new Promise((resolve) => { activateRoute = resolve; });
178
- controller.signal.addEventListener('abort', activateRoute, { once: true });
179
- let session;
180
- let adapter;
181
- try {
182
- fence();
183
- const bridge = createWorkSessionInteractionBridge({ manager: manager, journal,
184
- hostId: options.hostId, conversationId: binding.conversationId, ownerId: broker.ownerId,
185
- onPendingChange: (value) => {
186
- if (value)
187
- pending.add(binding.conversationId);
188
- else
189
- pending.delete(binding.conversationId);
190
- session?.refreshRuntimeState();
191
- } });
192
- adapter = await options.provider.open(binding.nativeSessionId, {
193
- request: async (request, nativeOptions) => {
194
- const combined = new AbortController();
195
- const abort = () => combined.abort();
196
- controller.signal.addEventListener('abort', abort, { once: true });
197
- nativeOptions.signal.addEventListener('abort', abort, { once: true });
198
- if (controller.signal.aborted || nativeOptions.signal.aborted || stopped)
199
- combined.abort();
200
- try {
201
- // A resumed native approval can arrive during attachment. Wait for
202
- // Canon's binding acknowledgement before exposing its contents.
203
- const nativeAborted = new Promise((resolve) => combined.signal.addEventListener('abort', () => resolve(), { once: true }));
204
- if (combined.signal.aborted)
205
- return { kind: 'cancelled' };
206
- await Promise.race([routeReady, nativeAborted]);
207
- return await bridge.request(request, { signal: combined.signal });
208
- }
209
- finally {
210
- controller.signal.removeEventListener('abort', abort);
211
- nativeOptions.signal.removeEventListener('abort', abort);
212
- }
213
- },
214
- });
215
- fence();
216
- session = createCanonAttachedSession({ connection, binding: nativeBinding, native: adapter,
217
- store: options.createSessionStore(nativeBinding), transport,
218
- onError: (error, operation) => report(error, `session-${operation}`),
219
- onStatus: (status) => {
220
- emit({ status: 'session', conversationId: binding.conversationId, session: status });
221
- if (status.status === 'stopped' && !stopped) {
222
- // Defer until the child's stop promise settles; never await it from its own callback.
223
- queueMicrotask(() => { void removeRoom(binding.conversationId, true).catch((error) => fail(error, 'detach')); });
224
- }
225
- updatePresence();
226
- },
227
- });
228
- rooms.set(binding.conversationId, { binding: binding, session, controller, releaseNative, stopped: false, acknowledged: confirmed, activate: activateRoute });
229
- if (confirmed)
230
- activateRoute();
231
- await session.start();
232
- fence();
233
- if (session.getState().status !== 'ready')
234
- throw new Error('The native session is not ready for Canon input.');
235
- updatePresence();
236
- }
237
- catch (error) {
238
- controller.abort();
239
- rooms.delete(binding.conversationId);
240
- try {
241
- if (session)
242
- await session.stop();
243
- else
244
- await adapter?.close();
245
- }
246
- finally {
247
- releaseNative();
248
- updatePresence();
249
- }
250
- throw error;
251
- }
252
- }
253
- const errorResult = (status, code, message) => ({ status, error: { code, message } });
254
- async function processRequest(request, replayed) {
255
- if (request.status !== 'claimed' && request.status !== 'uncertain')
256
- return;
257
- fence();
258
- const hash = fingerprintWorkSessionRequest(request);
259
- let operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
260
- if (operation && operation.fingerprint !== hash)
261
- throw new Error('Work-session request changed during recovery.');
262
- if (!operation) {
263
- const completion = replayed ? errorResult('uncertain', 'local_state_missing', 'This request was already claimed, but its local record is unavailable. Select the existing native session to recover.') : undefined;
264
- operation = { request, fingerprint: hash, phase: completion ? 'complete' : request.selection.mode === 'create' ? 'creating' : 'created',
265
- ...(completion ? { completion } : request.selection.mode === 'attach' ? { nativeSessionId: request.selection.sessionId, settings: {} } : {}) };
266
- await journal.mutate((state) => { state.operations.push(operation); });
267
- if (!completion && request.selection.mode === 'create') {
268
- let created;
269
- try {
270
- fence();
271
- created = await options.provider.create(request.selection, {
272
- requestId: request.requestId, conversationId: request.conversationId, agentId: connection.agentId,
273
- });
274
- }
275
- catch (error) {
276
- const definite = error instanceof NativeWorkSessionCreateError && error.outcome === 'not_created';
277
- const result = errorResult(definite ? 'failed' : 'uncertain', definite ? 'native_create_failed' : 'native_create_uncertain', definite ? 'The native runtime could not create this session. Refresh setup and try again.'
278
- : 'The native runtime may have created this session. Inspect existing sessions before trying again.');
279
- await saveCompletion(request.requestId, result);
280
- report(error, 'native-create');
281
- operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
282
- }
283
- if (created) {
284
- // The exact ID must reach durable storage before any observer or Canon input can attach.
285
- await journal.mutate((state) => {
286
- const entry = state.operations.find((item) => item.request.requestId === request.requestId);
287
- Object.assign(entry, { phase: 'created', nativeSessionId: created.nativeSessionId, settings: created.settings });
288
- });
289
- operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
290
- }
291
- }
292
- }
293
- else if (operation.phase === 'creating') {
294
- await saveCompletion(request.requestId, errorResult('uncertain', 'native_create_uncertain', 'The previous process stopped during native creation. Inspect existing sessions before trying again.'));
295
- operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
296
- }
297
- fence();
298
- if (!operation.completion) {
299
- const nativeSessionId = operation.nativeSessionId;
300
- if (!nativeSessionId)
301
- throw new Error('Work-session journal has no native session to attach.');
302
- const catalog = await options.provider.catalog();
303
- const settings = request.selection.mode === 'attach'
304
- ? catalog.sessions.find((entry) => entry.id === nativeSessionId)?.settings : operation.settings;
305
- if (!settings) {
306
- await saveCompletion(request.requestId, errorResult('failed', 'native_session_unavailable', 'The selected native session is no longer loaded. Load it and select it again.'));
307
- }
308
- else {
309
- await journal.mutate((state) => { state.operations.find((entry) => entry.request.requestId === request.requestId).phase = 'attaching'; });
310
- try {
311
- await attach({ workSessionId: request.requestId, conversationId: request.conversationId,
312
- hostId: options.hostId, provider: catalog.provider, nativeSessionId, settings, createdAt: Date.now() });
313
- await saveCompletion(request.requestId, { status: 'attached', nativeSessionId, settings });
314
- }
315
- catch (error) {
316
- await removeRoom(request.conversationId, false);
317
- report(error, 'native-attach');
318
- await saveCompletion(request.requestId, errorResult('failed', 'native_attach_failed', 'Canon could not attach this native session. The native session is retained; select it again after reconnecting.'));
319
- }
320
- }
321
- operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
322
- }
323
- fence();
324
- // A previous process may have saved its successful result before Canon
325
- // received it. Reestablish that exact route before confirming readiness.
326
- if (operation.completion?.status === 'attached' && !rooms.has(request.conversationId)) {
327
- try {
328
- await attach({ workSessionId: request.requestId, conversationId: request.conversationId,
329
- hostId: options.hostId, provider: (await options.provider.catalog()).provider,
330
- nativeSessionId: operation.completion.nativeSessionId, settings: operation.completion.settings,
331
- createdAt: request.createdAt });
332
- }
333
- catch (error) {
334
- await removeRoom(request.conversationId, false);
335
- report(error, 'recover-attachment');
336
- // The explicit claim says Canon has no successful binding yet. Preserve
337
- // the native ID for inspection, but do not claim this route is ready.
338
- await saveCompletion(request.requestId, errorResult('failed', 'native_restore_failed', 'The saved native session could not be reconnected. Load it and select it again.'));
339
- operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
340
- }
341
- }
342
- try {
343
- const completed = await authority.acknowledge(journal, request.requestId, operation.completion);
344
- if (completed.status === 'attached')
345
- activate(request.conversationId);
346
- }
347
- catch (error) {
348
- // An unacknowledged bridge must not accept fresh work. The durable completion can be retried.
349
- await removeRoom(request.conversationId, false);
350
- throw error;
351
- }
352
- }
353
- async function saveCompletion(requestId, completion) {
354
- await journal.mutate((state) => {
355
- const entry = state.operations.find((item) => item.request.requestId === requestId);
356
- entry.phase = 'complete';
357
- entry.completion = completion;
358
- });
359
- }
360
- async function doReconcile() {
361
- fence();
362
- if (!connected)
363
- return;
364
- // Finish durable outcomes before taking more work. Explicit claim allows same-host recovery.
365
- const outstanding = journal.read().operations.find((entry) => !entry.acknowledged && !entry.quarantined);
366
- let claimed;
367
- try {
368
- claimed = await authority.claim(outstanding?.request.requestId);
369
- }
370
- catch (error) {
371
- if (outstanding && error instanceof CanonApiError && error.code === 'WORK_SESSION_MEMBERSHIP_REQUIRED') {
372
- await quarantine(outstanding.request.requestId, 'Conversation membership is no longer available.');
373
- return;
374
- }
375
- if (error instanceof CanonApiError && (error.status === 401 || error.status === 403 || error.code === 'WORK_SESSION_LEASE_STALE'))
376
- fail(error, 'request-authority');
377
- throw error;
378
- }
379
- fence();
380
- if (!claimed.request) {
381
- if (outstanding)
382
- await quarantine(outstanding.request.requestId, 'This saved request is no longer available to this runtime.');
383
- return;
384
- }
385
- if (['attached', 'configured', 'failed', 'expired'].includes(claimed.request.status)) {
386
- if (outstanding) {
387
- if (claimed.request.status === 'attached' && claimed.request.binding) {
388
- try {
389
- await attach(claimed.request.binding, true);
390
- }
391
- catch (error) {
392
- report(error, 'recover-confirmed-attachment');
393
- fence();
394
- retainLease(await client.releaseWorkSessionBinding({ ...auth(), conversationId: claimed.request.conversationId,
395
- workSessionId: claimed.request.binding.workSessionId }));
396
- }
397
- }
398
- else
399
- await removeRoom(outstanding.request.conversationId, false);
400
- await journal.mutate((state) => { state.operations.find((entry) => entry.request.requestId === outstanding.request.requestId).acknowledged = true; });
401
- }
402
- return;
403
- }
404
- await processRequest(claimed.request, claimed.replayed);
405
- }
406
- async function quarantine(requestId, reason) {
407
- const operation = journal.read().operations.find((entry) => entry.request.requestId === requestId);
408
- if (!operation)
409
- return;
410
- await removeRoom(operation.request.conversationId, false);
411
- await journal.mutate((state) => { state.operations.find((entry) => entry.request.requestId === requestId).quarantined = reason; });
412
- report(new Error(reason), 'request-quarantined');
413
- }
414
- function reconcile() {
415
- if (stopped)
416
- return Promise.resolve();
417
- processing ??= doReconcile().finally(() => { processing = undefined; });
418
- return processing;
419
- }
420
- async function renew() {
421
- fence();
422
- // Renewal only needs the bound audience. Never enumerate the account's
423
- // unrelated rooms, and bound concurrent reads even for a busy host.
424
- const boundRooms = [...rooms.values()];
425
- for (let offset = 0; offset < boundRooms.length; offset += 8) {
426
- fence();
427
- const checked = await Promise.all(boundRooms.slice(offset, offset + 8).map(async (room) => ({
428
- room, conversation: await client.getConversation(room.binding.conversationId),
429
- })));
430
- fence();
431
- for (const { room, conversation } of checked) {
432
- // A stream update may already have removed/replaced this attachment.
433
- if (rooms.get(room.binding.conversationId) !== room)
434
- continue;
435
- if (!conversation?.memberIds.includes(connection.agentId) || !conversation.memberIds.includes(broker.ownerId))
436
- await removeRoom(room.binding.conversationId, true);
437
- }
438
- }
439
- // Catalog has its own freshness deadline; it is refreshed independently of public presence.
440
- const catalog = await options.provider.catalog();
441
- fence();
442
- const state = await authority.heartbeat(catalog);
443
- if (!stopped)
444
- retainLease(state);
445
- scheduleReports();
446
- }
447
- function start() {
448
- if (stopped)
449
- return Promise.reject(new Error('Work-session host has stopped.'));
450
- startPromise ??= (async () => {
451
- await journal.load();
452
- await verifyCanonRuntimeConnection(runtime);
453
- const identity = await client.getAuthToken();
454
- if (identity.agentId !== connection.agentId)
455
- throw new Error('Canon credential belongs to a different agent.');
456
- if (stopped)
457
- return;
458
- const catalog = await options.provider.catalog();
459
- if (stopped)
460
- return;
461
- retainLease(await authority.register(catalog));
462
- if (stopped) {
463
- fenceShutdown();
464
- return;
465
- }
466
- // Native history inspection/restoration can be slower than one lease.
467
- // Keep the acquired lease alive throughout startup, not only after every
468
- // room has finished attaching. This timer advertises no public presence.
469
- leaseTimer = setInterval(() => {
470
- if (renewing || stopped)
471
- return;
472
- renewing = renew().catch((error) => fail(error, 'lease-renewal')).finally(() => { renewing = undefined; });
473
- }, heartbeatMs);
474
- leaseTimer.unref?.();
475
- manager = new ApprovalManager(client, connection.agentId, broker.ownerId);
476
- publisher = createRuntimeStatePublisher({ agentId: connection.agentId, clientType: connection.clientType ?? 'generic', hostMode: false,
477
- rtdb: initRTDBAuth(client, { rtdbUrl: runtime.rtdbUrl, firebaseApiKey: runtime.firebaseWebApiKey }) });
478
- heartbeat = createRuntimeHeartbeat({ publisher, onError: report,
479
- getRuntime: () => ({ runtimeDescriptor: { coreControls: [], runtimeControls: [], commands: [],
480
- supportsInterrupt: false, supportsInputInterrupt: false, streamingTextMode: 'snapshot' } }) });
481
- transport = { agentId: connection.agentId, environmentId: connection.environmentId, client, publisher,
482
- hasPendingInteraction: (id) => pending.has(id),
483
- isRouteActive: (id) => !stopped && rooms.get(id)?.acknowledged === true,
484
- subscribe: (handler) => {
485
- handlers.add(handler);
486
- // Context is account state, not a one-shot notification. An attachment
487
- // created after stream startup needs the same trusted owner identity.
488
- if (!stopped && agentContext)
489
- handler.onAgentContext?.(agentContext);
490
- if (connected && !stopped)
491
- handler.onConnected?.();
492
- return () => { handlers.delete(handler); };
493
- } };
494
- const connectionReady = new Promise((resolve, reject) => { resolveConnection = resolve; rejectConnection = reject; });
495
- const connectionTimer = setTimeout(() => rejectConnection?.(new Error('Canon stream did not connect; work-session setup was not started.')), connectTimeoutMs);
496
- stream = new CanonStream({ endpoint: await client.getEndpoint(), agentId: connection.agentId, handler: {
497
- onMessage: (payload) => {
498
- if (stopped || !connected)
499
- return;
500
- manager?.handleMessage(payload.conversationId, payload.message);
501
- fanout('onMessage', payload);
502
- },
503
- onAgentContext: (context) => {
504
- if (context.agentId !== connection.agentId) {
505
- fail(new Error('Canon stream identity changed.'), 'stream-identity');
506
- return;
507
- }
508
- agentContext = context;
509
- fanout('onAgentContext', context);
510
- },
511
- onConversationUpdated: (payload) => {
512
- const members = payload.changes.memberIds;
513
- if ((Array.isArray(members) && !members.includes(broker.ownerId))
514
- || payload.membershipChange?.removedMemberIds.includes(broker.ownerId)) {
515
- void removeRoom(payload.conversationId, true).catch((error) => fail(error, 'owner-membership'));
516
- }
517
- fanout('onConversationUpdated', payload);
518
- },
519
- onParticipationSuppressed: (payload) => fanout('onParticipationSuppressed', payload),
520
- onReplayExpired: (payload) => fanout('onReplayExpired', payload),
521
- onConnected: () => { if (stopped)
522
- return; connected = true; resolveConnection?.(); fanout('onConnected'); updatePresence(); },
523
- onDisconnected: () => { connected = false; fanout('onDisconnected'); updatePresence(); },
524
- onError: (error) => report(error, 'stream'),
525
- } });
526
- void stream.start().catch((error) => { rejectConnection?.(error); report(error, 'stream-start'); });
527
- try {
528
- await connectionReady;
529
- }
530
- finally {
531
- clearTimeout(connectionTimer);
532
- resolveConnection = undefined;
533
- rejectConnection = undefined;
534
- }
535
- fence();
536
- // Confirmed same-host routes survive a graceful stop; subscriptions/history remain durable.
537
- for (const binding of broker.bindings) {
538
- try {
539
- await attach(binding, true);
540
- }
541
- catch (error) {
542
- report(error, 'restore-session');
543
- fence();
544
- retainLease(await client.releaseWorkSessionBinding({ ...auth(), conversationId: binding.conversationId, workSessionId: binding.workSessionId }));
545
- }
546
- }
547
- fence();
548
- pollTimer = setInterval(() => { void reconcile().catch((error) => report(error, 'request')); }, pollMs);
549
- pollTimer.unref?.();
550
- emit({ status: 'available' });
551
- await reconcile();
552
- })().catch(async (error) => {
553
- // Cleanup cannot await the start promise that is currently unwinding.
554
- fenceShutdown();
555
- await cleanup().catch((cleanupError) => report(cleanupError, 'startup-cleanup'));
556
- throw error;
557
- });
558
- return startPromise;
559
- }
560
- function fenceShutdown() {
561
- stopped = true;
562
- connected = false;
563
- if (pollTimer)
564
- clearInterval(pollTimer);
565
- if (leaseTimer)
566
- clearInterval(leaseTimer);
567
- if (expiryTimer)
568
- clearTimeout(expiryTimer);
569
- rejectConnection?.(new Error('Work-session host stopped during startup.'));
570
- stream?.stop();
571
- manager?.dispose();
572
- for (const room of rooms.values()) {
573
- room.controller.abort();
574
- // Stop input/publication immediately even if another room's create RPC is in flight.
575
- void room.session.stop().catch((error) => report(error, 'session-stop'));
576
- }
577
- }
578
- function cleanup() {
579
- cleanupPromise ??= (async () => {
580
- await Promise.allSettled([processing, renewing].filter(Boolean));
581
- await Promise.allSettled([...rooms.keys()].map((id) => removeRoom(id, false)));
582
- try {
583
- await heartbeat?.dispose();
584
- }
585
- finally {
586
- try {
587
- if (broker)
588
- await client.releaseWorkSessionRuntime(auth());
589
- }
590
- catch (error) {
591
- report(error, 'release-runtime');
592
- }
593
- try {
594
- await options.provider.close();
595
- }
596
- finally {
597
- await journal.close();
598
- }
599
- }
600
- handlers.clear();
601
- pending.clear();
602
- emit({ status: 'stopped' });
603
- })();
604
- return cleanupPromise;
605
- }
606
- function stop() {
607
- if (stopPromise)
608
- return stopPromise;
609
- fenceShutdown();
610
- stopPromise = (async () => {
611
- await startPromise?.catch(() => { });
612
- await cleanup();
613
- })();
614
- return stopPromise;
615
- }
616
- return { start, stop, reconcile };
617
- }
@@ -1,12 +0,0 @@
1
- import type { ApprovalManager, NativeSessionInteractionBridge } from '@canonmsg/core';
2
- import type { createWorkSessionJournal } from './work-session-state.js';
3
- export interface WorkSessionInteractionBridgeOptions {
4
- manager: ApprovalManager;
5
- journal: ReturnType<typeof createWorkSessionJournal>;
6
- hostId: string;
7
- conversationId: string;
8
- ownerId: string;
9
- onPendingChange?: (pending: boolean) => void;
10
- }
11
- /** Audience-scoped durability around the shared request manager; it owns all Canon polling and cancellation. */
12
- export declare function createWorkSessionInteractionBridge(options: WorkSessionInteractionBridgeOptions): NativeSessionInteractionBridge;