@canonmsg/core 0.5.0 → 0.7.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.
@@ -0,0 +1 @@
1
+ export declare function resolveCanonBaseUrl(input?: string | null): string;
@@ -0,0 +1,9 @@
1
+ import { DEFAULT_BASE_URL } from './constants.js';
2
+ export function resolveCanonBaseUrl(input) {
3
+ if (typeof input !== 'string')
4
+ return DEFAULT_BASE_URL;
5
+ const trimmed = input.trim();
6
+ if (!trimmed)
7
+ return DEFAULT_BASE_URL;
8
+ return trimmed.replace(/\/+$/, '');
9
+ }
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, } from './types.js';
2
2
  export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
3
+ export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
4
+ export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
3
5
  export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
4
6
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
5
7
  export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
package/dist/browser.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { AGENT_CAPABILITIES, } from './types.js';
2
+ export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
2
3
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
3
4
  export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type CanonMessage, type CanonConversation, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
2
+ import type { CanonResolvedWorkSession, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions } from './work-session.js';
2
3
  import type { InboundDisposition } from './turn-protocol.js';
3
4
  /**
4
5
  * Thin REST client for Canon's agent API.
@@ -21,6 +22,10 @@ export declare class CanonClient {
21
22
  sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
22
23
  messageId: string;
23
24
  }>;
25
+ createWorkSession(options: CreateWorkSessionOptions): Promise<CanonResolvedWorkSession>;
26
+ getWorkSession(workSessionId: string, conversationId: string): Promise<CanonResolvedWorkSession>;
27
+ upsertWorkSessionConversation(workSessionId: string, conversationId: string, options?: UpdateWorkSessionConversationOptions): Promise<CanonResolvedWorkSession>;
28
+ sendLinkedMessage(options: SendLinkedMessageOptions): Promise<SendLinkedMessageResult>;
24
29
  createConversation(options: CreateConversationOptions): Promise<{
25
30
  conversationId: string;
26
31
  }>;
package/dist/client.js CHANGED
@@ -73,6 +73,54 @@ export class CanonClient {
73
73
  throw new CanonApiError(res.status, await res.text());
74
74
  return res.json();
75
75
  }
76
+ async createWorkSession(options) {
77
+ const res = await fetch(`${this.baseUrl}/work-sessions`, {
78
+ method: 'POST',
79
+ headers: this.authHeaders(),
80
+ body: JSON.stringify(options),
81
+ });
82
+ if (!res.ok)
83
+ throw new CanonApiError(res.status, await res.text());
84
+ return res.json();
85
+ }
86
+ async getWorkSession(workSessionId, conversationId) {
87
+ const params = new URLSearchParams({ conversationId });
88
+ const res = await fetch(`${this.baseUrl}/work-sessions/${workSessionId}?${params}`, { headers: this.authHeaders() });
89
+ if (!res.ok)
90
+ throw new CanonApiError(res.status, await res.text());
91
+ return res.json();
92
+ }
93
+ async upsertWorkSessionConversation(workSessionId, conversationId, options) {
94
+ const res = await fetch(`${this.baseUrl}/work-sessions/${workSessionId}/conversations/${conversationId}`, {
95
+ method: 'PUT',
96
+ headers: this.authHeaders(),
97
+ body: JSON.stringify(options ?? {}),
98
+ });
99
+ if (!res.ok)
100
+ throw new CanonApiError(res.status, await res.text());
101
+ return res.json();
102
+ }
103
+ async sendLinkedMessage(options) {
104
+ const res = await fetch(`${this.baseUrl}/messages/send-linked`, {
105
+ method: 'POST',
106
+ headers: this.authHeaders(),
107
+ body: JSON.stringify({
108
+ sourceConversationId: options.sourceConversationId,
109
+ targetConversationId: options.targetConversationId,
110
+ text: options.text,
111
+ ...(options.workSessionId ? { workSessionId: options.workSessionId } : {}),
112
+ ...(options.createWorkSession
113
+ ? { createWorkSession: options.createWorkSession }
114
+ : {}),
115
+ ...(options.sourceContext ? { sourceContext: options.sourceContext } : {}),
116
+ ...(options.targetContext ? { targetContext: options.targetContext } : {}),
117
+ ...(options.messageOptions ? { messageOptions: options.messageOptions } : {}),
118
+ }),
119
+ });
120
+ if (!res.ok)
121
+ throw new CanonApiError(res.status, await res.text());
122
+ return res.json();
123
+ }
76
124
  async createConversation(options) {
77
125
  const res = await fetch(`${this.baseUrl}/conversations/create`, {
78
126
  method: 'POST',
@@ -23,8 +23,8 @@ export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
23
23
  }
24
24
  export interface SessionWorkspaceConfig {
25
25
  workspaceId?: string;
26
- legacyCwd?: string;
27
26
  model?: string;
27
+ retiredWorkspaceConfig?: boolean;
28
28
  }
29
29
  export declare function normalizeOptionalString(value: unknown): string | undefined;
30
30
  export declare function isEnabledFlag(value: unknown): boolean;
@@ -37,7 +37,7 @@ export declare function resolveConfiguredWorkspaceCwd(input: {
37
37
  workspaceOptions: WorkspaceResolverOption[];
38
38
  config: {
39
39
  workspaceId?: string;
40
- legacyCwd?: string;
40
+ retiredWorkspaceConfig?: boolean;
41
41
  } | null;
42
42
  defaultCwd: string;
43
43
  }): string;
@@ -153,48 +153,43 @@ export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
153
153
  export function buildPublicWorkspaceOptions(workspaceOptions) {
154
154
  return workspaceOptions.map(({ id, label }) => ({ id, label }));
155
155
  }
156
+ function isRetiredWorkspaceId(value) {
157
+ if (!value)
158
+ return false;
159
+ return value === 'default' || /^workspace-\d+$/.test(value);
160
+ }
156
161
  export function readSessionWorkspaceConfig(raw) {
157
162
  if (!raw || typeof raw !== 'object')
158
163
  return null;
159
164
  const data = raw;
165
+ const rawWorkspaceId = normalizeOptionalString(data.workspaceId);
166
+ const stableWorkspaceId = rawWorkspaceId && !isRetiredWorkspaceId(rawWorkspaceId)
167
+ ? rawWorkspaceId
168
+ : undefined;
169
+ const hasRetiredWorkspaceReference = (normalizeOptionalString(data.cwd) !== undefined
170
+ || isRetiredWorkspaceId(rawWorkspaceId));
160
171
  return {
161
- workspaceId: normalizeOptionalString(data.workspaceId),
162
- legacyCwd: normalizeOptionalString(data.cwd),
172
+ workspaceId: stableWorkspaceId,
163
173
  model: normalizeOptionalString(data.model),
174
+ ...(!stableWorkspaceId && hasRetiredWorkspaceReference
175
+ ? { retiredWorkspaceConfig: true }
176
+ : {}),
164
177
  };
165
178
  }
166
- function findWorkspaceByLegacyCwd(workspaceOptions, legacyCwd) {
167
- if (!legacyCwd)
168
- return undefined;
169
- const resolvedLegacyCwd = resolve(legacyCwd);
170
- return workspaceOptions.find((workspace) => resolve(workspace.cwd) === resolvedLegacyCwd);
171
- }
172
179
  export function resolveConfiguredWorkspaceCwd(input) {
173
180
  const fallbackCwd = input.workspaceOptions[0]?.cwd ?? resolve(input.defaultCwd);
174
181
  if (!input.config)
175
182
  return fallbackCwd;
176
- const legacyWorkspace = findWorkspaceByLegacyCwd(input.workspaceOptions, input.config.legacyCwd);
183
+ if (input.config.retiredWorkspaceConfig) {
184
+ throw new ExecutionEnvironmentError('Session config still references a retired workspace format.', 'This Canon coding session was saved with a retired workspace format. Recreate the session or select a current workspace.');
185
+ }
177
186
  const workspaceId = input.config.workspaceId;
178
187
  if (workspaceId) {
179
188
  const workspace = input.workspaceOptions.find((option) => option.id === workspaceId);
180
189
  if (workspace)
181
190
  return workspace.cwd;
182
- if (workspaceId === 'default') {
183
- return fallbackCwd;
184
- }
185
- const legacyWorkspaceMatch = /^workspace-(\d+)$/.exec(workspaceId);
186
- if (legacyWorkspaceMatch) {
187
- const legacyIndex = Number.parseInt(legacyWorkspaceMatch[1] ?? '', 10) - 1;
188
- if (legacyIndex >= 0 && input.workspaceOptions[legacyIndex]) {
189
- return input.workspaceOptions[legacyIndex].cwd;
190
- }
191
- }
192
- if (legacyWorkspace)
193
- return legacyWorkspace.cwd;
194
191
  throw new ExecutionEnvironmentError(`Workspace ${workspaceId} is not configured on this machine.`, 'The workspace saved for this Canon coding session is no longer configured on this machine.');
195
192
  }
196
- if (legacyWorkspace)
197
- return legacyWorkspace.cwd;
198
193
  return fallbackCwd;
199
194
  }
200
195
  export function buildConversationWorktreeSpec(input) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, } from './types.js';
2
2
  export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, CanonMessagesPage, AgentContext, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
3
+ export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CreateWorkSessionOptions, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
4
+ export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
3
5
  export { CanonClient, CanonApiError } from './client.js';
4
6
  export { CanonStream } from './stream.js';
5
7
  export type { StreamHandler } from './stream.js';
@@ -23,3 +25,4 @@ export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecu
23
25
  export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
24
26
  export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
25
27
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
28
+ export { resolveCanonBaseUrl } from './base-url.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Types
2
2
  export { AGENT_CAPABILITIES, } from './types.js';
3
+ export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
3
4
  // Client
4
5
  export { CanonClient, CanonApiError } from './client.js';
5
6
  // Stream
@@ -25,3 +26,5 @@ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, build
25
26
  export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
26
27
  // Constants
27
28
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
29
+ // Base URL resolver
30
+ export { resolveCanonBaseUrl } from './base-url.js';
@@ -50,8 +50,25 @@ export interface TurnStatePayload {
50
50
  '.sv': 'timestamp';
51
51
  };
52
52
  }
53
- /** Must be called once before any RTDB operations. */
54
- export declare function initRTDBAuth(client: CanonClient): void;
53
+ interface RTDBAuthOptions {
54
+ rtdbUrl?: string;
55
+ firebaseApiKey?: string;
56
+ }
57
+ interface RTDBClientHandle {
58
+ read(path: string): Promise<unknown>;
59
+ write(path: string, data: unknown): Promise<void>;
60
+ patch(path: string, data: unknown): Promise<void>;
61
+ remove(path: string): Promise<void>;
62
+ writeSessionState(conversationId: string, agentId: string, state: Omit<SessionStatePayload, 'updatedAt'>): Promise<void>;
63
+ clearSessionState(conversationId: string, agentId: string): Promise<void>;
64
+ writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
65
+ clearTurnState(conversationId: string, agentId: string): Promise<void>;
66
+ }
67
+ /**
68
+ * Initializes the default RTDB helper and returns a scoped client for callers
69
+ * that need per-runtime auth and base-url isolation.
70
+ */
71
+ export declare function initRTDBAuth(client: CanonClient, options?: RTDBAuthOptions): RTDBClientHandle;
55
72
  /** Generic RTDB REST write (PUT). */
56
73
  export declare function rtdbWrite(path: string, data: unknown): Promise<void>;
57
74
  /** Generic RTDB REST read (GET). */
@@ -67,3 +84,4 @@ export declare function writeSessionState(conversationId: string, agentId: strin
67
84
  export declare function clearSessionState(conversationId: string, agentId: string): Promise<void>;
68
85
  export declare function writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
69
86
  export declare function clearTurnState(conversationId: string, agentId: string): Promise<void>;
87
+ export {};
package/dist/rtdb-rest.js CHANGED
@@ -6,163 +6,208 @@
6
6
  * is exchanged for a Firebase ID token before use with RTDB REST.
7
7
  */
8
8
  import { DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
9
- const RTDB_BASE = process.env.CANON_RTDB_URL || DEFAULT_RTDB_URL;
10
- const FIREBASE_API_KEY = process.env.CANON_FIREBASE_API_KEY || FIREBASE_WEB_API_KEY;
11
- // ── Token management ──────────────────────────────────────────────────
12
- let cachedIdToken = null;
13
- let idTokenExpiresAt = 0;
14
- let tokenClient = null;
15
- /** Must be called once before any RTDB operations. */
16
- export function initRTDBAuth(client) {
17
- tokenClient = client;
9
+ const DEFAULT_RTDB_BASE = normalizeRTDBBase(process.env.CANON_RTDB_URL || DEFAULT_RTDB_URL);
10
+ const DEFAULT_FIREBASE_API_KEY = process.env.CANON_FIREBASE_API_KEY || FIREBASE_WEB_API_KEY;
11
+ let defaultRTDBClient = null;
12
+ function normalizeRTDBBase(url) {
13
+ return url.replace(/\/+$/, '');
18
14
  }
19
- /**
20
- * Exchange a Firebase custom token for an ID token via the Identity Toolkit API.
21
- */
22
- async function exchangeCustomTokenForIdToken(customToken) {
23
- const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
24
- const res = await fetch(url, {
25
- method: 'POST',
26
- headers: { 'Content-Type': 'application/json' },
27
- body: JSON.stringify({ token: customToken, returnSecureToken: true }),
28
- });
29
- if (!res.ok) {
30
- const text = await res.text();
31
- throw new Error(`Token exchange failed (${res.status}): ${text}`);
32
- }
33
- const data = await res.json();
34
- return { idToken: data.idToken, expiresIn: parseInt(data.expiresIn, 10) };
15
+ function normalizeRTDBPath(path) {
16
+ return path.startsWith('/') ? path : `/${path}`;
35
17
  }
36
- /** Get a valid ID token for RTDB REST, refreshing if expired or expiring soon. */
37
- async function getToken() {
38
- // Refresh if missing, expired, or expiring within 5 minutes
39
- if (!cachedIdToken || Date.now() > idTokenExpiresAt - 5 * 60 * 1000) {
40
- if (!tokenClient)
18
+ function createRTDBClientHandle(client, options) {
19
+ const rtdbBase = normalizeRTDBBase(options?.rtdbUrl || DEFAULT_RTDB_BASE);
20
+ const firebaseApiKey = options?.firebaseApiKey || DEFAULT_FIREBASE_API_KEY;
21
+ let cachedIdToken = null;
22
+ let idTokenExpiresAt = 0;
23
+ let refreshPromise = null;
24
+ async function exchangeCustomTokenForIdToken(customToken) {
25
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${firebaseApiKey}`;
26
+ const res = await fetch(url, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ token: customToken, returnSecureToken: true }),
30
+ });
31
+ if (!res.ok) {
32
+ const text = await res.text();
33
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
34
+ }
35
+ const data = await res.json();
36
+ return {
37
+ idToken: data.idToken,
38
+ expiresIn: Number.parseInt(data.expiresIn, 10),
39
+ };
40
+ }
41
+ async function getToken(forceRefresh = false) {
42
+ const now = Date.now();
43
+ if (!forceRefresh && cachedIdToken && now <= idTokenExpiresAt - 5 * 60 * 1000) {
44
+ return cachedIdToken;
45
+ }
46
+ if (refreshPromise) {
47
+ return refreshPromise;
48
+ }
49
+ const staleToken = forceRefresh ? null : cachedIdToken;
50
+ refreshPromise = (async () => {
51
+ try {
52
+ const auth = await client.getAuthToken();
53
+ const { idToken, expiresIn } = await exchangeCustomTokenForIdToken(auth.token);
54
+ cachedIdToken = idToken;
55
+ idTokenExpiresAt = Date.now() + (expiresIn * 1000);
56
+ return cachedIdToken;
57
+ }
58
+ catch (err) {
59
+ console.error('[canon] RTDB token refresh failed:', err);
60
+ return staleToken;
61
+ }
62
+ finally {
63
+ refreshPromise = null;
64
+ }
65
+ })();
66
+ return refreshPromise;
67
+ }
68
+ async function request(method, path, data, options = {}) {
69
+ const token = await getToken(false);
70
+ if (!token)
41
71
  return null;
72
+ const url = `${rtdbBase}${normalizeRTDBPath(path)}.json?auth=${encodeURIComponent(token)}`;
73
+ const res = await fetch(url, {
74
+ method,
75
+ ...(method === 'GET'
76
+ ? {}
77
+ : {
78
+ headers: { 'Content-Type': 'application/json' },
79
+ ...(data === undefined ? {} : { body: JSON.stringify(data) }),
80
+ }),
81
+ });
82
+ if (options.retryUnauthorized !== false
83
+ && (res.status === 401 || res.status === 403)) {
84
+ cachedIdToken = null;
85
+ idTokenExpiresAt = 0;
86
+ const refreshedToken = await getToken(true);
87
+ if (!refreshedToken) {
88
+ return res;
89
+ }
90
+ const retryUrl = `${rtdbBase}${normalizeRTDBPath(path)}.json?auth=${encodeURIComponent(refreshedToken)}`;
91
+ return fetch(retryUrl, {
92
+ method,
93
+ ...(method === 'GET'
94
+ ? {}
95
+ : {
96
+ headers: { 'Content-Type': 'application/json' },
97
+ ...(data === undefined ? {} : { body: JSON.stringify(data) }),
98
+ }),
99
+ });
100
+ }
101
+ return res;
102
+ }
103
+ async function requireSuccess(method, path, data, errorPrefix) {
104
+ const res = await request(method, path, data);
105
+ if (!res)
106
+ return;
107
+ if (!res.ok) {
108
+ const text = await res.text();
109
+ throw new Error(`${errorPrefix} (${res.status}): ${text}`);
110
+ }
111
+ }
112
+ async function read(path) {
113
+ const res = await request('GET', path);
114
+ if (!res?.ok)
115
+ return null;
116
+ return res.json();
117
+ }
118
+ async function write(path, data) {
119
+ await requireSuccess('PUT', path, data, 'RTDB write failed');
120
+ }
121
+ async function patch(path, data) {
122
+ await requireSuccess('PATCH', path, data, 'RTDB patch failed');
123
+ }
124
+ async function remove(path) {
125
+ await requireSuccess('DELETE', path, undefined, 'RTDB delete failed');
126
+ }
127
+ async function writeSessionStateImpl(conversationId, agentId, state) {
128
+ await write(`/session-state/${conversationId}/${agentId}`, {
129
+ ...state,
130
+ updatedAt: { '.sv': 'timestamp' },
131
+ });
132
+ }
133
+ async function clearSessionStateImpl(conversationId, agentId) {
42
134
  try {
43
- const auth = await tokenClient.getAuthToken();
44
- const { idToken, expiresIn } = await exchangeCustomTokenForIdToken(auth.token);
45
- cachedIdToken = idToken;
46
- idTokenExpiresAt = Date.now() + expiresIn * 1000;
135
+ await write(`/session-state/${conversationId}/${agentId}`, {
136
+ isActive: false,
137
+ updatedAt: { '.sv': 'timestamp' },
138
+ });
47
139
  }
48
- catch (err) {
49
- console.error('[canon] RTDB token refresh failed:', err);
50
- return cachedIdToken; // Return stale token as fallback
140
+ catch (error) {
141
+ console.error('[canon] RTDB clear failed:', error);
51
142
  }
52
143
  }
53
- return cachedIdToken;
144
+ async function writeTurnStateImpl(conversationId, agentId, state) {
145
+ await write(`/turn-state/${conversationId}/${agentId}`, {
146
+ ...state,
147
+ updatedAt: { '.sv': 'timestamp' },
148
+ });
149
+ }
150
+ async function clearTurnStateImpl(conversationId, agentId) {
151
+ try {
152
+ await write(`/turn-state/${conversationId}/${agentId}`, {
153
+ state: 'idle',
154
+ queueDepth: 0,
155
+ updatedAt: { '.sv': 'timestamp' },
156
+ completedAt: { '.sv': 'timestamp' },
157
+ });
158
+ }
159
+ catch (error) {
160
+ console.error('[canon] RTDB turn clear failed:', error);
161
+ }
162
+ }
163
+ return {
164
+ read,
165
+ write,
166
+ patch,
167
+ remove,
168
+ writeSessionState: writeSessionStateImpl,
169
+ clearSessionState: clearSessionStateImpl,
170
+ writeTurnState: writeTurnStateImpl,
171
+ clearTurnState: clearTurnStateImpl,
172
+ };
173
+ }
174
+ /**
175
+ * Initializes the default RTDB helper and returns a scoped client for callers
176
+ * that need per-runtime auth and base-url isolation.
177
+ */
178
+ export function initRTDBAuth(client, options) {
179
+ const scopedClient = createRTDBClientHandle(client, options);
180
+ defaultRTDBClient = scopedClient;
181
+ return scopedClient;
182
+ }
183
+ function getDefaultRTDBClient() {
184
+ return defaultRTDBClient;
54
185
  }
55
186
  // ── RTDB operations ───────────────────────────────────────────────────
56
187
  /** Generic RTDB REST write (PUT). */
57
188
  export async function rtdbWrite(path, data) {
58
- const token = await getToken();
59
- if (!token)
60
- return;
61
- const url = `${RTDB_BASE}${path}.json?auth=${token}`;
62
- const res = await fetch(url, {
63
- method: 'PUT',
64
- headers: { 'Content-Type': 'application/json' },
65
- body: JSON.stringify(data),
66
- });
67
- if (!res.ok) {
68
- const text = await res.text();
69
- throw new Error(`RTDB write failed (${res.status}): ${text}`);
70
- }
189
+ await getDefaultRTDBClient()?.write(path, data);
71
190
  }
72
191
  /** Generic RTDB REST read (GET). */
73
192
  export async function rtdbRead(path) {
74
- const token = await getToken();
75
- if (!token)
76
- return null;
77
- const url = `${RTDB_BASE}${path}.json?auth=${token}`;
78
- const res = await fetch(url);
79
- if (!res.ok)
80
- return null;
81
- return res.json();
193
+ return getDefaultRTDBClient()?.read(path) ?? null;
82
194
  }
83
195
  /**
84
196
  * Write session state to RTDB via REST API.
85
197
  * Path: /session-state/{conversationId}/{agentId}
86
198
  */
87
199
  export async function writeSessionState(conversationId, agentId, state) {
88
- const token = await getToken();
89
- if (!token)
90
- return;
91
- const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
92
- const body = {
93
- ...state,
94
- updatedAt: { '.sv': 'timestamp' },
95
- };
96
- const res = await fetch(url, {
97
- method: 'PUT',
98
- headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify(body),
100
- });
101
- if (!res.ok) {
102
- const text = await res.text();
103
- throw new Error(`RTDB write failed (${res.status}): ${text}`);
104
- }
200
+ await getDefaultRTDBClient()?.writeSessionState(conversationId, agentId, state);
105
201
  }
106
202
  /**
107
203
  * Clear session state from RTDB (full overwrite with isActive: false).
108
204
  */
109
205
  export async function clearSessionState(conversationId, agentId) {
110
- const token = await getToken();
111
- if (!token)
112
- return;
113
- const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
114
- const body = {
115
- isActive: false,
116
- updatedAt: { '.sv': 'timestamp' },
117
- };
118
- // Use PUT (not PATCH) to clear stale fields like model/cwd
119
- const res = await fetch(url, {
120
- method: 'PUT',
121
- headers: { 'Content-Type': 'application/json' },
122
- body: JSON.stringify(body),
123
- });
124
- if (!res.ok) {
125
- const text = await res.text();
126
- console.error(`[canon] RTDB clear failed (${res.status}): ${text}`);
127
- }
206
+ await getDefaultRTDBClient()?.clearSessionState(conversationId, agentId);
128
207
  }
129
208
  export async function writeTurnState(conversationId, agentId, state) {
130
- const token = await getToken();
131
- if (!token)
132
- return;
133
- const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
134
- const body = {
135
- ...state,
136
- updatedAt: { '.sv': 'timestamp' },
137
- };
138
- const res = await fetch(url, {
139
- method: 'PUT',
140
- headers: { 'Content-Type': 'application/json' },
141
- body: JSON.stringify(body),
142
- });
143
- if (!res.ok) {
144
- const text = await res.text();
145
- throw new Error(`RTDB write failed (${res.status}): ${text}`);
146
- }
209
+ await getDefaultRTDBClient()?.writeTurnState(conversationId, agentId, state);
147
210
  }
148
211
  export async function clearTurnState(conversationId, agentId) {
149
- const token = await getToken();
150
- if (!token)
151
- return;
152
- const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
153
- const body = {
154
- state: 'idle',
155
- queueDepth: 0,
156
- updatedAt: { '.sv': 'timestamp' },
157
- completedAt: { '.sv': 'timestamp' },
158
- };
159
- const res = await fetch(url, {
160
- method: 'PUT',
161
- headers: { 'Content-Type': 'application/json' },
162
- body: JSON.stringify(body),
163
- });
164
- if (!res.ok) {
165
- const text = await res.text();
166
- console.error(`[canon] RTDB turn clear failed (${res.status}): ${text}`);
167
- }
212
+ await getDefaultRTDBClient()?.clearTurnState(conversationId, agentId);
168
213
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ResolvedAgentBehaviorPolicy } from './policy.js';
2
+ import type { CanonWorkSessionContext } from './work-session.js';
2
3
  export type MediaAttachmentKind = 'image' | 'audio' | 'file';
3
4
  export interface MediaAttachment {
4
5
  kind: MediaAttachmentKind;
@@ -25,6 +26,7 @@ export interface CanonMessage {
25
26
  mentions: string[];
26
27
  replyTo: string | null;
27
28
  replyToPosition: number | null;
29
+ workSession?: CanonWorkSessionContext | null;
28
30
  metadata?: Record<string, unknown>;
29
31
  status: 'sent' | 'read';
30
32
  deleted: boolean;
@@ -50,6 +52,7 @@ export interface CanonConversation {
50
52
  export interface CanonMessagesPage {
51
53
  messages: CanonMessage[];
52
54
  behavior?: ResolvedAgentBehaviorPolicy;
55
+ workSessions?: CanonWorkSessionContext[];
53
56
  }
54
57
  export type AgentClientType = 'claude-code' | 'openclaw' | 'codex' | 'generic';
55
58
  /** Declares what session controls an agent type supports. */
@@ -88,6 +91,7 @@ export interface AgentContext {
88
91
  export interface MessageCreatedPayload {
89
92
  conversationId: string;
90
93
  behavior?: ResolvedAgentBehaviorPolicy;
94
+ workSessions?: CanonWorkSessionContext[];
91
95
  message: {
92
96
  id: string;
93
97
  senderId: string;
@@ -105,6 +109,7 @@ export interface MessageCreatedPayload {
105
109
  replyToPosition?: number;
106
110
  mentions?: string[];
107
111
  createdAt?: string;
112
+ workSession?: CanonWorkSessionContext | null;
108
113
  /** Structured metadata for rich UI (approval cards, etc.) */
109
114
  metadata?: Record<string, unknown>;
110
115
  };
@@ -129,6 +134,7 @@ export interface SendMessageOptions {
129
134
  attachments?: MediaAttachment[];
130
135
  contactCardUserId?: string;
131
136
  mentions?: string[];
137
+ workSessionId?: string;
132
138
  /** Structured metadata for rich UI (approval cards, etc.) */
133
139
  metadata?: Record<string, unknown>;
134
140
  }
@@ -0,0 +1,88 @@
1
+ import type { RepresentationMode } from './policy.js';
2
+ import type { SendMessageOptions } from './types.js';
3
+ export type CanonWorkSessionStatus = 'active' | 'paused' | 'completed';
4
+ export type CanonWorkSessionConversationRole = 'requester' | 'coordinator' | 'participant' | 'delegate' | 'observer';
5
+ export type CanonWorkSessionDisclosureMode = 'none' | 'summary' | 'full';
6
+ export interface CanonWorkSessionParticipant {
7
+ conversationId?: string | null;
8
+ participantId?: string | null;
9
+ label?: string | null;
10
+ role?: CanonWorkSessionConversationRole | null;
11
+ }
12
+ export interface CanonWorkSessionContext {
13
+ id: string;
14
+ title?: string | null;
15
+ objective?: string | null;
16
+ status?: CanonWorkSessionStatus | null;
17
+ summary?: string | null;
18
+ representation?: RepresentationMode | null;
19
+ currentConversationRole?: CanonWorkSessionConversationRole | null;
20
+ sourceConversationId?: string | null;
21
+ sourceConversationLabel?: string | null;
22
+ sourceParticipantId?: string | null;
23
+ sourceParticipantLabel?: string | null;
24
+ disclosure?: CanonWorkSessionDisclosureMode | null;
25
+ disclosureNotes?: string[];
26
+ visibleFacts?: string[];
27
+ pendingQuestions?: string[];
28
+ participants?: CanonWorkSessionParticipant[];
29
+ updatedAt?: string | null;
30
+ }
31
+ export interface CanonWorkSession {
32
+ id: string;
33
+ title?: string | null;
34
+ objective?: string | null;
35
+ status: CanonWorkSessionStatus;
36
+ createdAt?: string | null;
37
+ updatedAt?: string | null;
38
+ }
39
+ export interface UpdateWorkSessionConversationOptions {
40
+ summary?: string | null;
41
+ representation?: RepresentationMode | null;
42
+ currentConversationRole?: CanonWorkSessionConversationRole | null;
43
+ sourceConversationId?: string | null;
44
+ sourceConversationLabel?: string | null;
45
+ sourceParticipantId?: string | null;
46
+ sourceParticipantLabel?: string | null;
47
+ disclosure?: CanonWorkSessionDisclosureMode | null;
48
+ disclosureNotes?: string[];
49
+ visibleFacts?: string[];
50
+ pendingQuestions?: string[];
51
+ participants?: CanonWorkSessionParticipant[];
52
+ }
53
+ export interface CreateWorkSessionOptions extends UpdateWorkSessionConversationOptions {
54
+ conversationId: string;
55
+ title?: string | null;
56
+ objective?: string | null;
57
+ status?: CanonWorkSessionStatus | null;
58
+ }
59
+ export interface CanonResolvedWorkSession {
60
+ workSession: CanonWorkSession;
61
+ context: CanonWorkSessionContext;
62
+ }
63
+ export interface SendLinkedMessageOptions {
64
+ sourceConversationId: string;
65
+ targetConversationId: string;
66
+ text: string;
67
+ workSessionId?: string;
68
+ createWorkSession?: Omit<CreateWorkSessionOptions, 'conversationId'>;
69
+ sourceContext?: UpdateWorkSessionConversationOptions;
70
+ targetContext?: UpdateWorkSessionConversationOptions;
71
+ messageOptions?: Omit<SendMessageOptions, 'workSessionId'>;
72
+ }
73
+ export interface SendLinkedMessageResult {
74
+ messageId: string;
75
+ workSessionId: string;
76
+ workSessionCreated: boolean;
77
+ }
78
+ export interface WorkSessionPromptRenderOptions {
79
+ maxVisibleFacts?: number;
80
+ maxPendingQuestions?: number;
81
+ maxDisclosureNotes?: number;
82
+ maxParticipants?: number;
83
+ maxDetailedWorkSessions?: number;
84
+ maxOtherWorkSessions?: number;
85
+ }
86
+ export declare function mergeWorkSessionContexts(explicitWorkSession?: CanonWorkSessionContext | null, activeWorkSessions?: CanonWorkSessionContext[] | null): CanonWorkSessionContext[];
87
+ export declare function buildWorkSessionPromptLines(workSession?: CanonWorkSessionContext | null, options?: WorkSessionPromptRenderOptions): string[];
88
+ export declare function buildWorkSessionsPromptLines(workSessions?: CanonWorkSessionContext[] | null, options?: WorkSessionPromptRenderOptions): string[];
@@ -0,0 +1,238 @@
1
+ const DEFAULT_WORK_SESSION_PROMPT_OPTIONS = {
2
+ maxVisibleFacts: 3,
3
+ maxPendingQuestions: 2,
4
+ maxDisclosureNotes: 1,
5
+ maxParticipants: 3,
6
+ maxDetailedWorkSessions: 1,
7
+ maxOtherWorkSessions: 3,
8
+ };
9
+ function normalizeString(value) {
10
+ if (typeof value !== 'string')
11
+ return null;
12
+ const trimmed = value.trim();
13
+ return trimmed.length > 0 ? trimmed : null;
14
+ }
15
+ function normalizeStringList(value) {
16
+ if (!Array.isArray(value))
17
+ return [];
18
+ return value
19
+ .map((entry) => normalizeString(entry))
20
+ .filter((entry) => entry != null);
21
+ }
22
+ function mergeStringLists(primary, secondary) {
23
+ const merged = [...normalizeStringList(primary), ...normalizeStringList(secondary)];
24
+ const unique = Array.from(new Set(merged));
25
+ return unique.length > 0 ? unique : undefined;
26
+ }
27
+ function mergeParticipants(primary, secondary) {
28
+ const result = [];
29
+ const seen = new Set();
30
+ const pushParticipant = (participant) => {
31
+ if (!participant)
32
+ return;
33
+ const key = [
34
+ normalizeString(participant.conversationId),
35
+ normalizeString(participant.participantId),
36
+ normalizeString(participant.label),
37
+ normalizeString(participant.role),
38
+ ].join('::');
39
+ if (seen.has(key))
40
+ return;
41
+ seen.add(key);
42
+ result.push(participant);
43
+ };
44
+ for (const participant of primary ?? [])
45
+ pushParticipant(participant);
46
+ for (const participant of secondary ?? [])
47
+ pushParticipant(participant);
48
+ return result.length > 0 ? result : undefined;
49
+ }
50
+ function mergeTwoWorkSessions(primary, secondary) {
51
+ const disclosureNotes = mergeStringLists(primary.disclosureNotes, secondary.disclosureNotes);
52
+ const visibleFacts = mergeStringLists(primary.visibleFacts, secondary.visibleFacts);
53
+ const pendingQuestions = mergeStringLists(primary.pendingQuestions, secondary.pendingQuestions);
54
+ const participants = mergeParticipants(primary.participants, secondary.participants);
55
+ return {
56
+ id: primary.id,
57
+ title: primary.title ?? secondary.title ?? null,
58
+ objective: primary.objective ?? secondary.objective ?? null,
59
+ status: primary.status ?? secondary.status ?? null,
60
+ summary: primary.summary ?? secondary.summary ?? null,
61
+ representation: primary.representation ?? secondary.representation ?? null,
62
+ currentConversationRole: primary.currentConversationRole ?? secondary.currentConversationRole ?? null,
63
+ sourceConversationId: primary.sourceConversationId ?? secondary.sourceConversationId ?? null,
64
+ sourceConversationLabel: primary.sourceConversationLabel ?? secondary.sourceConversationLabel ?? null,
65
+ sourceParticipantId: primary.sourceParticipantId ?? secondary.sourceParticipantId ?? null,
66
+ sourceParticipantLabel: primary.sourceParticipantLabel ?? secondary.sourceParticipantLabel ?? null,
67
+ disclosure: primary.disclosure ?? secondary.disclosure ?? null,
68
+ ...(disclosureNotes ? { disclosureNotes } : {}),
69
+ ...(visibleFacts ? { visibleFacts } : {}),
70
+ ...(pendingQuestions ? { pendingQuestions } : {}),
71
+ ...(participants ? { participants } : {}),
72
+ updatedAt: primary.updatedAt ?? secondary.updatedAt ?? null,
73
+ };
74
+ }
75
+ export function mergeWorkSessionContexts(explicitWorkSession, activeWorkSessions) {
76
+ const merged = new Map();
77
+ const add = (workSession) => {
78
+ if (!workSession?.id)
79
+ return;
80
+ const existing = merged.get(workSession.id);
81
+ merged.set(workSession.id, existing ? mergeTwoWorkSessions(existing, workSession) : workSession);
82
+ };
83
+ add(explicitWorkSession);
84
+ for (const workSession of activeWorkSessions ?? []) {
85
+ add(workSession);
86
+ }
87
+ return Array.from(merged.values());
88
+ }
89
+ function formatParticipantLabel(participant) {
90
+ const label = normalizeString(participant.label);
91
+ if (label)
92
+ return label;
93
+ const participantId = normalizeString(participant.participantId);
94
+ if (participantId)
95
+ return participantId;
96
+ return normalizeString(participant.conversationId);
97
+ }
98
+ function resolvePromptOptions(options) {
99
+ return {
100
+ maxVisibleFacts: Math.max(0, options?.maxVisibleFacts
101
+ ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxVisibleFacts),
102
+ maxPendingQuestions: Math.max(0, options?.maxPendingQuestions
103
+ ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxPendingQuestions),
104
+ maxDisclosureNotes: Math.max(0, options?.maxDisclosureNotes
105
+ ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxDisclosureNotes),
106
+ maxParticipants: Math.max(0, options?.maxParticipants ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxParticipants),
107
+ maxDetailedWorkSessions: Math.max(1, options?.maxDetailedWorkSessions
108
+ ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxDetailedWorkSessions),
109
+ maxOtherWorkSessions: Math.max(0, options?.maxOtherWorkSessions
110
+ ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxOtherWorkSessions),
111
+ };
112
+ }
113
+ function takeWithHiddenCount(values, maxCount) {
114
+ if (maxCount <= 0) {
115
+ return { visible: [], hiddenCount: values.length };
116
+ }
117
+ return {
118
+ visible: values.slice(0, maxCount),
119
+ hiddenCount: Math.max(0, values.length - maxCount),
120
+ };
121
+ }
122
+ function buildOriginLabel(workSession) {
123
+ const sourceConversation = normalizeString(workSession.sourceConversationLabel)
124
+ ?? normalizeString(workSession.sourceConversationId);
125
+ const sourceParticipant = normalizeString(workSession.sourceParticipantLabel)
126
+ ?? normalizeString(workSession.sourceParticipantId);
127
+ if (sourceConversation && sourceParticipant) {
128
+ return `${sourceParticipant} in ${sourceConversation}`;
129
+ }
130
+ return sourceParticipant ?? sourceConversation;
131
+ }
132
+ function formatWorkSessionReference(workSession) {
133
+ const title = normalizeString(workSession.title);
134
+ const objective = normalizeString(workSession.objective);
135
+ if (title)
136
+ return `${title} (${workSession.id})`;
137
+ if (objective)
138
+ return `${objective} (${workSession.id})`;
139
+ return workSession.id;
140
+ }
141
+ export function buildWorkSessionPromptLines(workSession, options) {
142
+ if (!workSession)
143
+ return [];
144
+ const promptOptions = resolvePromptOptions(options);
145
+ const title = normalizeString(workSession.title);
146
+ const objective = normalizeString(workSession.objective);
147
+ const summary = normalizeString(workSession.summary);
148
+ const origin = buildOriginLabel(workSession);
149
+ const visibleFacts = takeWithHiddenCount(normalizeStringList(workSession.visibleFacts), promptOptions.maxVisibleFacts);
150
+ const pendingQuestions = takeWithHiddenCount(normalizeStringList(workSession.pendingQuestions), promptOptions.maxPendingQuestions);
151
+ const disclosureNotes = takeWithHiddenCount(normalizeStringList(workSession.disclosureNotes), promptOptions.maxDisclosureNotes);
152
+ const participants = takeWithHiddenCount(Array.isArray(workSession.participants) ? workSession.participants : [], promptOptions.maxParticipants);
153
+ return [
154
+ `Canon work session ID: ${workSession.id}`,
155
+ ...(title ? [`Canon work session title: ${title}`] : []),
156
+ ...(objective ? [`Canon work session objective: ${objective}`] : []),
157
+ ...(workSession.status
158
+ ? [`Canon work session status: ${workSession.status}`]
159
+ : []),
160
+ ...(workSession.currentConversationRole
161
+ ? [
162
+ `This conversation's role in the work session: ${workSession.currentConversationRole}`,
163
+ ]
164
+ : []),
165
+ ...(workSession.representation
166
+ ? [`Participate in this conversation as: ${workSession.representation}`]
167
+ : []),
168
+ ...(origin ? [`Work session origin: ${origin}`] : []),
169
+ ...(workSession.disclosure
170
+ ? [`Allowed disclosure level in this conversation: ${workSession.disclosure}`]
171
+ : []),
172
+ ...(summary ? [`Shared work session summary: ${summary}`] : []),
173
+ ...visibleFacts.visible.map((fact, index) => `Shared fact ${index + 1}: ${fact}`),
174
+ ...(visibleFacts.hiddenCount > 0
175
+ ? [`Additional shared facts omitted for brevity: ${visibleFacts.hiddenCount}`]
176
+ : []),
177
+ ...pendingQuestions.visible.map((question, index) => `Open question ${index + 1}: ${question}`),
178
+ ...(pendingQuestions.hiddenCount > 0
179
+ ? [
180
+ `Additional open questions omitted for brevity: ${pendingQuestions.hiddenCount}`,
181
+ ]
182
+ : []),
183
+ ...disclosureNotes.visible.map((note, index) => `Disclosure note ${index + 1}: ${note}`),
184
+ ...(disclosureNotes.hiddenCount > 0
185
+ ? [
186
+ `Additional disclosure notes omitted for brevity: ${disclosureNotes.hiddenCount}`,
187
+ ]
188
+ : []),
189
+ ...participants.visible.flatMap((participant, index) => {
190
+ const label = formatParticipantLabel(participant);
191
+ if (!label)
192
+ return [];
193
+ const role = normalizeString(participant.role);
194
+ return [
195
+ `Related participant ${index + 1}: ${label}${role ? ` (${role})` : ''}`,
196
+ ];
197
+ }),
198
+ ...(participants.hiddenCount > 0
199
+ ? [
200
+ `Additional related participants omitted for brevity: ${participants.hiddenCount}`,
201
+ ]
202
+ : []),
203
+ 'Canon rule: this shared work-session context is scoped to this conversation only and does not grant access to other conversation transcripts.',
204
+ ];
205
+ }
206
+ export function buildWorkSessionsPromptLines(workSessions, options) {
207
+ const promptOptions = resolvePromptOptions(options);
208
+ if (!Array.isArray(workSessions) || workSessions.length === 0)
209
+ return [];
210
+ if (workSessions.length === 1) {
211
+ return [
212
+ 'Canon active work session for this conversation:',
213
+ ...buildWorkSessionPromptLines(workSessions[0], promptOptions),
214
+ ];
215
+ }
216
+ const detailedWorkSessions = workSessions.slice(0, promptOptions.maxDetailedWorkSessions);
217
+ const hiddenWorkSessions = workSessions.slice(promptOptions.maxDetailedWorkSessions);
218
+ const lines = ['Canon active work sessions for this conversation:'];
219
+ detailedWorkSessions.forEach((workSession, index) => {
220
+ lines.push(detailedWorkSessions.length === 1
221
+ ? 'Primary active work session:'
222
+ : `Detailed work session ${index + 1}:`);
223
+ lines.push(...buildWorkSessionPromptLines(workSession, promptOptions));
224
+ });
225
+ if (hiddenWorkSessions.length > 0) {
226
+ lines.push('Other active work sessions linked here:');
227
+ hiddenWorkSessions
228
+ .slice(0, promptOptions.maxOtherWorkSessions)
229
+ .forEach((workSession, index) => {
230
+ lines.push(`Other work session ${index + 1}: ${formatWorkSessionReference(workSession)}`);
231
+ });
232
+ if (hiddenWorkSessions.length > promptOptions.maxOtherWorkSessions) {
233
+ lines.push(`Additional active work sessions omitted for brevity: ${hiddenWorkSessions.length - promptOptions.maxOtherWorkSessions}`);
234
+ }
235
+ lines.push('Canon rule: do not assume the current message refers to another linked work session unless the conversation clearly points to it.');
236
+ }
237
+ return lines;
238
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,10 +8,12 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
+ "default": "./dist/index.js",
11
12
  "types": "./dist/index.d.ts"
12
13
  },
13
14
  "./browser": {
14
15
  "import": "./dist/browser.js",
16
+ "default": "./dist/browser.js",
15
17
  "types": "./dist/browser.d.ts"
16
18
  }
17
19
  },