@canonmsg/agent-sdk 11.0.2 → 11.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/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
+ export { createCanonManagedSessionHost } from './managed-session-host.js';
3
+ export type { CanonManagedSessionHostOptions } from './managed-session-host.js';
2
4
  export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
3
5
  export type { EnsureAgentProfileOptions, RegistrationCredentials, RegistrationProgress, RegistrationSession, RegistrationStatus, RegistrationStore, ResumeRegistrationOptions } from '@canonmsg/core';
4
6
  export type { AgentContactsAPI, AgentConversationsAPI, AgentDirectoryAPI, AgentUsersAPI, } from './canon-agent.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
+ export { createCanonManagedSessionHost } from './managed-session-host.js';
2
3
  export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
3
4
  export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
4
5
  export { SessionManager } from './session-manager.js';
@@ -0,0 +1,27 @@
1
+ import { CanonClient, type WorkSessionBinding, type WorkSessionCatalog, type WorkSessionDetailFields, type WorkSessionRequest, type WorkSessionSettings } from '@canonmsg/core';
2
+ import { type WorkSessionHostStore } from './work-session-state.js';
3
+ export interface CanonManagedSessionHostOptions {
4
+ client: CanonClient;
5
+ agentId: string;
6
+ environmentId: string;
7
+ hostId: string;
8
+ displayName: string;
9
+ store: WorkSessionHostStore;
10
+ catalog(): Promise<WorkSessionCatalog>;
11
+ /** Must durably save the resolved selection before returning. May be called again after a crash. */
12
+ configure(request: WorkSessionRequest): Promise<{
13
+ settings: WorkSessionSettings;
14
+ fields: WorkSessionDetailFields;
15
+ }>;
16
+ onError?: (error: unknown, operation: string) => void;
17
+ pollIntervalMs?: number;
18
+ heartbeatIntervalMs?: number;
19
+ }
20
+ /** Broker coordination for a managed host whose ordinary message loop owns turns. */
21
+ export declare function createCanonManagedSessionHost(options: CanonManagedSessionHostOptions): {
22
+ start: () => Promise<void>;
23
+ stop: () => Promise<void>;
24
+ reconcile: () => Promise<void>;
25
+ report: (conversationId: string, fields: WorkSessionDetailFields, setupState?: "configured" | "attached") => Promise<void>;
26
+ binding: (conversationId: string) => WorkSessionBinding | undefined;
27
+ };
@@ -0,0 +1,195 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CanonApiError, } from '@canonmsg/core';
3
+ import { createWorkSessionJournal } from './work-session-state.js';
4
+ import { createWorkSessionAuthority, fingerprintWorkSessionRequest } from './work-session-coordination.js';
5
+ /** Broker coordination for a managed host whose ordinary message loop owns turns. */
6
+ export function createCanonManagedSessionHost(options) {
7
+ const journal = createWorkSessionJournal(options.store, {
8
+ environmentId: options.environmentId, agentId: options.agentId, hostId: options.hostId,
9
+ });
10
+ const runtimeEpoch = randomUUID();
11
+ const pollMs = options.pollIntervalMs ?? 2_000;
12
+ const heartbeatMs = options.heartbeatIntervalMs ?? 25_000;
13
+ if (!options.hostId || !Number.isFinite(pollMs) || pollMs <= 0 || !Number.isFinite(heartbeatMs)
14
+ || heartbeatMs <= 0 || heartbeatMs > 30_000)
15
+ throw new Error('Invalid managed-session host configuration.');
16
+ let broker;
17
+ let stopped = false;
18
+ const authority = createWorkSessionAuthority({ client: options.client, hostId: options.hostId,
19
+ runtimeEpoch, displayName: options.displayName, isStopped: () => stopped });
20
+ let started = false;
21
+ let pollTimer;
22
+ let heartbeatTimer;
23
+ let processing;
24
+ let renewing;
25
+ let starting;
26
+ const reportError = (error, operation) => { try {
27
+ options.onError?.(error, operation);
28
+ }
29
+ catch { /* observer */ } };
30
+ const auth = authority.auth;
31
+ const fence = authority.fence;
32
+ function retain(state) {
33
+ broker = authority.retain(state);
34
+ }
35
+ async function processRequest(request, replayed) {
36
+ if (request.status !== 'claimed' && request.status !== 'uncertain')
37
+ return;
38
+ fence();
39
+ const hash = fingerprintWorkSessionRequest(request);
40
+ let configuredFields;
41
+ let entry = journal.read().operations.find((operation) => operation.request.requestId === request.requestId);
42
+ if (entry && entry.fingerprint !== hash)
43
+ throw new Error('Managed-session request changed during recovery.');
44
+ if (!entry) {
45
+ entry = { request, fingerprint: hash, phase: 'configuring',
46
+ ...(replayed ? { completion: { status: 'uncertain', error: { code: 'local_state_missing',
47
+ message: 'The host lost the saved setup result. Inspect this local runtime before trying again.' } }, phase: 'complete' } : {}),
48
+ };
49
+ await journal.mutate((state) => { state.operations.push(entry); });
50
+ }
51
+ if (!entry.completion) {
52
+ if (request.selection.mode !== 'configure') {
53
+ entry.completion = { status: 'failed', error: { code: 'unsupported_setup', message: 'This host only supports managed setup.' } };
54
+ }
55
+ else {
56
+ try {
57
+ const result = await options.configure(request);
58
+ fence();
59
+ entry.completion = { status: 'configured', settings: result.settings };
60
+ configuredFields = result.fields;
61
+ await journal.mutate((state) => {
62
+ const saved = state.operations.find((operation) => operation.request.requestId === request.requestId);
63
+ saved.phase = 'complete';
64
+ saved.completion = entry.completion;
65
+ });
66
+ }
67
+ catch (error) {
68
+ reportError(error, 'configure');
69
+ entry.completion = { status: 'failed', error: { code: 'managed_configuration_failed',
70
+ message: (error instanceof Error ? error.message : String(error)).slice(0, 512) } };
71
+ }
72
+ }
73
+ if (entry.completion.status !== 'configured') {
74
+ await journal.mutate((state) => {
75
+ const saved = state.operations.find((operation) => operation.request.requestId === request.requestId);
76
+ saved.phase = 'complete';
77
+ saved.completion = entry.completion;
78
+ });
79
+ }
80
+ }
81
+ fence();
82
+ const completed = await authority.acknowledge(journal, request.requestId, entry.completion);
83
+ if (completed.binding) {
84
+ broker.bindings = [...broker.bindings.filter((binding) => binding.conversationId !== completed.conversationId), completed.binding];
85
+ }
86
+ if (configuredFields && completed.status === 'configured') {
87
+ await report(request.conversationId, configuredFields).catch((error) => reportError(error, 'report-configured'));
88
+ }
89
+ }
90
+ async function doReconcile() {
91
+ fence();
92
+ const outstanding = journal.read().operations.find((entry) => !entry.acknowledged && !entry.quarantined);
93
+ let claimed;
94
+ try {
95
+ claimed = await authority.claim(outstanding?.request.requestId);
96
+ }
97
+ catch (error) {
98
+ if (outstanding && error instanceof CanonApiError && error.code === 'WORK_SESSION_MEMBERSHIP_REQUIRED') {
99
+ await journal.mutate((state) => {
100
+ state.operations.find((entry) => entry.request.requestId === outstanding.request.requestId).quarantined = 'Conversation membership was removed.';
101
+ });
102
+ return;
103
+ }
104
+ throw error;
105
+ }
106
+ fence();
107
+ if (!claimed.request) {
108
+ if (outstanding)
109
+ await journal.mutate((state) => {
110
+ state.operations.find((entry) => entry.request.requestId === outstanding.request.requestId).quarantined = 'Request no longer belongs to this runtime.';
111
+ });
112
+ return;
113
+ }
114
+ if (['configured', 'failed', 'expired'].includes(claimed.request.status)) {
115
+ if (outstanding)
116
+ await journal.mutate((state) => {
117
+ state.operations.find((entry) => entry.request.requestId === outstanding.request.requestId).acknowledged = true;
118
+ });
119
+ if (claimed.request.binding) {
120
+ broker.bindings = [...broker.bindings.filter((binding) => binding.conversationId !== claimed.request.conversationId), claimed.request.binding];
121
+ }
122
+ return;
123
+ }
124
+ await processRequest(claimed.request, claimed.replayed);
125
+ }
126
+ function reconcile() {
127
+ if (stopped || !started)
128
+ return Promise.resolve();
129
+ processing ??= doReconcile().finally(() => { processing = undefined; });
130
+ return processing;
131
+ }
132
+ async function renew() {
133
+ fence();
134
+ const state = await authority.heartbeat(await options.catalog());
135
+ if (!stopped)
136
+ retain(state);
137
+ }
138
+ function start() {
139
+ if (stopped)
140
+ throw new Error('Managed-session host already stopped.');
141
+ if (started && broker && broker.lease.expiresAt - Date.now() >= 5_000)
142
+ return Promise.resolve();
143
+ starting ??= (async () => {
144
+ if (!started)
145
+ await journal.load();
146
+ retain(await authority.register(await options.catalog()));
147
+ if (stopped)
148
+ return;
149
+ if (!started) {
150
+ started = true;
151
+ heartbeatTimer = setInterval(() => {
152
+ if (!renewing)
153
+ renewing = renew().catch((error) => reportError(error, 'lease-renewal')).finally(() => { renewing = undefined; });
154
+ }, heartbeatMs);
155
+ heartbeatTimer.unref?.();
156
+ pollTimer = setInterval(() => { void reconcile().catch((error) => reportError(error, 'request')); }, pollMs);
157
+ pollTimer.unref?.();
158
+ }
159
+ await reconcile();
160
+ })().finally(() => { starting = undefined; });
161
+ return starting;
162
+ }
163
+ async function report(conversationId, fields, setupState) {
164
+ fence();
165
+ const binding = broker.bindings.find((entry) => entry.conversationId === conversationId);
166
+ await options.client.reportWorkSessionDetails({ ...auth(), conversationId,
167
+ ...(binding ? { workSessionId: binding.workSessionId } : {}),
168
+ ...(setupState ? { setupState } : binding ? { setupState: binding.kind === 'managed' ? 'configured' : 'attached' } : {}),
169
+ fields });
170
+ }
171
+ async function stop() {
172
+ if (stopped)
173
+ return;
174
+ stopped = true;
175
+ if (pollTimer)
176
+ clearInterval(pollTimer);
177
+ if (heartbeatTimer)
178
+ clearInterval(heartbeatTimer);
179
+ await Promise.allSettled([starting, processing, renewing].filter(Boolean));
180
+ try {
181
+ if (broker)
182
+ await options.client.releaseWorkSessionRuntime(auth());
183
+ }
184
+ catch (error) {
185
+ reportError(error, 'release-runtime');
186
+ }
187
+ await journal.close();
188
+ }
189
+ function binding(conversationId) {
190
+ if (stopped || !broker || broker.lease.expiresAt - Date.now() < 5_000)
191
+ return undefined;
192
+ return broker.bindings.find((entry) => entry.conversationId === conversationId && entry.kind === 'managed');
193
+ }
194
+ return { start, stop, reconcile, report, binding };
195
+ }
@@ -0,0 +1,21 @@
1
+ import { CanonClient, type WorkSessionCatalog, type WorkSessionCompletion, type WorkSessionRequest, type WorkSessionRuntimeAuth, type WorkSessionRuntimeState } from '@canonmsg/core';
2
+ import type { WorkSessionJournal } from './work-session-state.js';
3
+ /** The same request identity is used by native creation and managed setup recovery. */
4
+ export declare function fingerprintWorkSessionRequest(request: WorkSessionRequest): string;
5
+ /** Shared broker authority; each host retains its own delivery and request dispatch loop. */
6
+ export declare function createWorkSessionAuthority(options: {
7
+ client: CanonClient;
8
+ hostId: string;
9
+ runtimeEpoch: string;
10
+ displayName: string;
11
+ isStopped(): boolean;
12
+ }): {
13
+ auth: () => WorkSessionRuntimeAuth;
14
+ fence: () => void;
15
+ retain: (state: WorkSessionRuntimeState) => WorkSessionRuntimeState;
16
+ register: (catalog: WorkSessionCatalog) => Promise<WorkSessionRuntimeState>;
17
+ heartbeat: (catalog: WorkSessionCatalog) => Promise<WorkSessionRuntimeState>;
18
+ claim: (requestId?: string) => Promise<import("@canonmsg/backend-contracts").WorkSessionClaimResult>;
19
+ acknowledge: (journal: WorkSessionJournal, requestId: string, result: WorkSessionCompletion) => Promise<WorkSessionRequest>;
20
+ state: () => WorkSessionRuntimeState | undefined;
21
+ };
@@ -0,0 +1,53 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** The same request identity is used by native creation and managed setup recovery. */
3
+ export function fingerprintWorkSessionRequest(request) {
4
+ return createHash('sha256').update(JSON.stringify([
5
+ request.agentId, request.conversationId, request.requestId, request.hostId,
6
+ request.runtimeEpoch, request.catalogRevision, request.selection, request.requestedBy,
7
+ ])).digest('hex');
8
+ }
9
+ /** Shared broker authority; each host retains its own delivery and request dispatch loop. */
10
+ export function createWorkSessionAuthority(options) {
11
+ let current;
12
+ function auth() {
13
+ if (!current)
14
+ throw new Error('Work-session runtime is not registered.');
15
+ return { hostId: options.hostId, runtimeEpoch: options.runtimeEpoch,
16
+ leaseToken: current.lease.leaseToken };
17
+ }
18
+ function fence() {
19
+ if (options.isStopped() || !current || current.lease.expiresAt - Date.now() < 5_000)
20
+ throw new Error('Work-session runtime no longer has a fresh lease.');
21
+ }
22
+ function retain(state) {
23
+ if (state.lease.hostId !== options.hostId || state.lease.runtimeEpoch !== options.runtimeEpoch
24
+ || (current && current.ownerId !== state.ownerId))
25
+ throw new Error('Work-session runtime identity changed.');
26
+ current = state;
27
+ return state;
28
+ }
29
+ async function register(catalog) {
30
+ return retain(await options.client.registerWorkSessionRuntime({ hostId: options.hostId,
31
+ runtimeEpoch: options.runtimeEpoch, displayName: options.displayName, catalog }));
32
+ }
33
+ async function heartbeat(catalog) {
34
+ fence();
35
+ return retain(await options.client.heartbeatWorkSessionRuntime({ ...auth(), catalog }));
36
+ }
37
+ function claim(requestId) {
38
+ fence();
39
+ return options.client.claimWorkSessionRequest({ ...auth(), ...(requestId ? { requestId } : {}) });
40
+ }
41
+ async function acknowledge(journal, requestId, result) {
42
+ fence();
43
+ const completed = await options.client.completeWorkSessionRequest({ ...auth(), requestId, result });
44
+ if (completed.status !== result.status)
45
+ throw new Error('Canon did not acknowledge the requested work-session outcome.');
46
+ await journal.mutate((state) => {
47
+ state.operations.find((entry) => entry.request.requestId === requestId).acknowledged = true;
48
+ });
49
+ return completed;
50
+ }
51
+ return { auth, fence, retain, register, heartbeat, claim, acknowledge,
52
+ state: () => current };
53
+ }
@@ -1,8 +1,9 @@
1
- import { createHash, randomUUID } from 'node:crypto';
1
+ import { randomUUID } from 'node:crypto';
2
2
  import { ApprovalManager, CanonApiError, CanonClient, CanonStream, NativeWorkSessionCreateError, createRuntimeHeartbeat, createRuntimeStatePublisher, initRTDBAuth, resolveCanonRuntimeConnection, verifyCanonRuntimeConnection, } from '@canonmsg/core';
3
3
  import { createCanonAttachedSession, } from './attached-session.js';
4
4
  import { createWorkSessionJournal } from './work-session-state.js';
5
5
  import { createWorkSessionInteractionBridge } from './work-session-interactions.js';
6
+ import { createWorkSessionAuthority, fingerprintWorkSessionRequest } from './work-session-coordination.js';
6
7
  /** One authenticated transport and liveness publisher, with durable per-room native routes. */
7
8
  export function createCanonWorkSessionHost(options) {
8
9
  const connection = { ...options.connection };
@@ -32,6 +33,8 @@ export function createCanonWorkSessionHost(options) {
32
33
  let connected = false;
33
34
  let agentContext;
34
35
  let stopped = false;
36
+ const authority = createWorkSessionAuthority({ client, hostId: options.hostId,
37
+ runtimeEpoch, displayName: options.displayName, isStopped: () => stopped });
35
38
  let startPromise;
36
39
  let stopPromise;
37
40
  let cleanupPromise;
@@ -40,6 +43,8 @@ export function createCanonWorkSessionHost(options) {
40
43
  let expiryTimer;
41
44
  let processing;
42
45
  let renewing;
46
+ let reporting;
47
+ let reportsRequested = false;
43
48
  let rejectConnection;
44
49
  let resolveConnection;
45
50
  const report = (error, operation) => { try {
@@ -54,20 +59,10 @@ export function createCanonWorkSessionHost(options) {
54
59
  report(error, 'status');
55
60
  }
56
61
  };
57
- function auth() {
58
- if (!broker)
59
- throw new Error('Work-session runtime is not registered.');
60
- return { hostId: options.hostId, runtimeEpoch, leaseToken: broker.lease.leaseToken };
61
- }
62
- function fence() {
63
- if (stopped || !broker || broker.lease.expiresAt - Date.now() < 5_000)
64
- throw new Error('Work-session runtime no longer has a fresh lease.');
65
- }
62
+ const auth = authority.auth;
63
+ const fence = authority.fence;
66
64
  function retainLease(state) {
67
- if (state.lease.hostId !== options.hostId || state.lease.runtimeEpoch !== runtimeEpoch
68
- || (broker && state.ownerId !== broker.ownerId))
69
- throw new Error('Work-session runtime identity changed.');
70
- broker = state;
65
+ broker = authority.retain(state);
71
66
  if (expiryTimer)
72
67
  clearTimeout(expiryTimer);
73
68
  expiryTimer = setTimeout(() => fail(new Error('Work-session runtime lease expired.'), 'lease-expired'), Math.max(1, state.lease.expiresAt - Date.now() - 5_000));
@@ -122,9 +117,45 @@ export function createCanonWorkSessionHost(options) {
122
117
  room.activate();
123
118
  room.session.refreshRuntimeState();
124
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
+ });
125
154
  }
126
155
  async function attach(binding, confirmed = false) {
127
156
  fence();
157
+ if (typeof binding.nativeSessionId !== 'string')
158
+ throw new Error('This host cannot attach a managed-session binding.');
128
159
  const existing = rooms.get(binding.conversationId);
129
160
  if (existing && existing.binding.nativeSessionId === binding.nativeSessionId && !existing.stopped) {
130
161
  if (confirmed)
@@ -194,7 +225,7 @@ export function createCanonWorkSessionHost(options) {
194
225
  updatePresence();
195
226
  },
196
227
  });
197
- rooms.set(binding.conversationId, { binding, session, controller, releaseNative, stopped: false, acknowledged: confirmed, activate: activateRoute });
228
+ rooms.set(binding.conversationId, { binding: binding, session, controller, releaseNative, stopped: false, acknowledged: confirmed, activate: activateRoute });
198
229
  if (confirmed)
199
230
  activateRoute();
200
231
  await session.start();
@@ -219,16 +250,12 @@ export function createCanonWorkSessionHost(options) {
219
250
  throw error;
220
251
  }
221
252
  }
222
- const fingerprint = (request) => createHash('sha256').update(JSON.stringify([
223
- request.agentId, request.conversationId, request.requestId, request.hostId,
224
- request.runtimeEpoch, request.catalogRevision, request.selection, request.requestedBy,
225
- ])).digest('hex');
226
253
  const errorResult = (status, code, message) => ({ status, error: { code, message } });
227
254
  async function processRequest(request, replayed) {
228
255
  if (request.status !== 'claimed' && request.status !== 'uncertain')
229
256
  return;
230
257
  fence();
231
- const hash = fingerprint(request);
258
+ const hash = fingerprintWorkSessionRequest(request);
232
259
  let operation = journal.read().operations.find((entry) => entry.request.requestId === request.requestId);
233
260
  if (operation && operation.fingerprint !== hash)
234
261
  throw new Error('Work-session request changed during recovery.');
@@ -313,12 +340,7 @@ export function createCanonWorkSessionHost(options) {
313
340
  }
314
341
  }
315
342
  try {
316
- const completed = await client.completeWorkSessionRequest({ ...auth(), requestId: request.requestId, result: operation.completion });
317
- if (completed.status !== operation.completion.status) {
318
- await removeRoom(request.conversationId, false);
319
- throw new Error('Canon did not acknowledge the requested work-session outcome.');
320
- }
321
- await journal.mutate((state) => { state.operations.find((entry) => entry.request.requestId === request.requestId).acknowledged = true; });
343
+ const completed = await authority.acknowledge(journal, request.requestId, operation.completion);
322
344
  if (completed.status === 'attached')
323
345
  activate(request.conversationId);
324
346
  }
@@ -343,7 +365,7 @@ export function createCanonWorkSessionHost(options) {
343
365
  const outstanding = journal.read().operations.find((entry) => !entry.acknowledged && !entry.quarantined);
344
366
  let claimed;
345
367
  try {
346
- claimed = await client.claimWorkSessionRequest({ ...auth(), ...(outstanding ? { requestId: outstanding.request.requestId } : {}) });
368
+ claimed = await authority.claim(outstanding?.request.requestId);
347
369
  }
348
370
  catch (error) {
349
371
  if (outstanding && error instanceof CanonApiError && error.code === 'WORK_SESSION_MEMBERSHIP_REQUIRED') {
@@ -360,7 +382,7 @@ export function createCanonWorkSessionHost(options) {
360
382
  await quarantine(outstanding.request.requestId, 'This saved request is no longer available to this runtime.');
361
383
  return;
362
384
  }
363
- if (['attached', 'failed', 'expired'].includes(claimed.request.status)) {
385
+ if (['attached', 'configured', 'failed', 'expired'].includes(claimed.request.status)) {
364
386
  if (outstanding) {
365
387
  if (claimed.request.status === 'attached' && claimed.request.binding) {
366
388
  try {
@@ -417,9 +439,10 @@ export function createCanonWorkSessionHost(options) {
417
439
  // Catalog has its own freshness deadline; it is refreshed independently of public presence.
418
440
  const catalog = await options.provider.catalog();
419
441
  fence();
420
- const state = await client.heartbeatWorkSessionRuntime({ ...auth(), catalog });
442
+ const state = await authority.heartbeat(catalog);
421
443
  if (!stopped)
422
444
  retainLease(state);
445
+ scheduleReports();
423
446
  }
424
447
  function start() {
425
448
  if (stopped)
@@ -435,7 +458,7 @@ export function createCanonWorkSessionHost(options) {
435
458
  const catalog = await options.provider.catalog();
436
459
  if (stopped)
437
460
  return;
438
- retainLease(await client.registerWorkSessionRuntime({ hostId: options.hostId, runtimeEpoch, displayName: options.displayName, catalog }));
461
+ retainLease(await authority.register(catalog));
439
462
  if (stopped) {
440
463
  fenceShutdown();
441
464
  return;
@@ -2,7 +2,7 @@ import { type ExclusiveJsonFileStore, type NativeSessionInteractionResult, type
2
2
  export interface WorkSessionOperation {
3
3
  request: WorkSessionRequest;
4
4
  fingerprint: string;
5
- phase: 'creating' | 'created' | 'attaching' | 'complete';
5
+ phase: 'configuring' | 'creating' | 'created' | 'attaching' | 'complete';
6
6
  nativeSessionId?: string;
7
7
  settings?: WorkSessionSettings;
8
8
  completion?: WorkSessionCompletion;
@@ -16,7 +16,7 @@ export function createWorkSessionJournal(store, identity) {
16
16
  || loaded.hostId !== identity.hostId || !Array.isArray(loaded.operations)
17
17
  || !Array.isArray(loaded.interactions)
18
18
  || loaded.operations.some((entry) => !entry?.request?.requestId || !entry.fingerprint
19
- || !['creating', 'created', 'attaching', 'complete'].includes(entry.phase)
19
+ || !['configuring', 'creating', 'created', 'attaching', 'complete'].includes(entry.phase)
20
20
  || (entry.phase === 'complete' && !entry.completion))
21
21
  || loaded.interactions.some((entry) => !entry?.id || !entry.fingerprint || !Number.isFinite(entry.expiresAt))) {
22
22
  throw new Error('Work-session journal cannot be verified; retain it for recovery.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "11.0.2",
3
+ "version": "11.1.0",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,12 @@
28
28
  "node": ">=22.22.3"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^13.0.5"
31
+ "@canonmsg/core": "^13.1.0"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/HeyBobChan/canon.git",
36
+ "directory": "packages/agent-sdk"
32
37
  },
33
38
  "publishConfig": {
34
39
  "access": "public"