@canonmsg/core 0.17.2 → 0.18.1

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/browser.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
2
  export { resolveCanonBaseUrl } from './base-url.js';
3
3
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
4
- export type { AddMemberResult, AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonResolveAdmissionResult, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
4
+ export type { AddMemberResult, AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonResolveAdmissionResult, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
5
5
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
6
6
  export type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
7
7
  export type { CanonSelfContext, CanonSelfContextType, SelfContextPromptRenderOptions, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, } from './self-context.js';
@@ -23,13 +23,31 @@ export interface HostInboundParticipantContext {
23
23
  }
24
24
  type HostInboundMessage = {
25
25
  id?: string | null;
26
+ senderId?: string | null;
27
+ senderName?: string | null;
26
28
  text?: string | null;
27
29
  contentType?: CanonMessage['contentType'] | null;
28
30
  attachments?: CanonMessage['attachments'];
29
31
  senderType?: CanonMessage['senderType'];
30
32
  mentions?: string[] | null;
31
33
  contactCard?: CanonMessage['contactCard'];
34
+ replyTo?: string | null;
35
+ replyToPosition?: number | null;
36
+ deleted?: boolean | null;
32
37
  };
38
+ export interface CanonReplyContext {
39
+ messageId: string;
40
+ senderId?: string | null;
41
+ senderName?: string | null;
42
+ senderType?: CanonMessage['senderType'] | null;
43
+ text?: string | null;
44
+ contentType?: CanonMessage['contentType'] | null;
45
+ attachments?: CanonMessage['attachments'];
46
+ contactCard?: CanonMessage['contactCard'];
47
+ body: string;
48
+ replyToPosition?: number | null;
49
+ found: boolean;
50
+ }
33
51
  export declare function buildCanonHostPrompt(input: {
34
52
  hostLabel: string;
35
53
  content: string;
@@ -37,9 +55,11 @@ export declare function buildCanonHostPrompt(input: {
37
55
  participantContext: HostInboundParticipantContext;
38
56
  behavior?: ResolvedAgentBehaviorPolicy | null;
39
57
  selfContexts?: MessageCreatedPayload['selfContexts'];
58
+ replyContext?: CanonReplyContext | null;
40
59
  buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
41
60
  sessionContextLines?: string[];
42
61
  }): string;
62
+ export declare function buildCanonReplyContextLines(replyContext: CanonReplyContext | null): string[];
43
63
  /**
44
64
  * Render the **text portion** of an inbound Canon message. Images are
45
65
  * referenced by short placeholders — their actual bytes are delivered to the
@@ -60,6 +80,10 @@ export declare function renderCanonHostInboundContent(message: HostInboundMessag
60
80
  durationMs?: number;
61
81
  index: number;
62
82
  }>): string;
83
+ export declare function resolveCanonReplyContext(input: {
84
+ message: HostInboundMessage;
85
+ messages?: ReadonlyArray<HostInboundMessage> | null;
86
+ }): CanonReplyContext | null;
63
87
  export declare function buildHydratedInboundContext(input: {
64
88
  agentId: string;
65
89
  conversation: CanonConversation | null;
@@ -78,6 +102,7 @@ export declare function buildHydratedInboundContext(input: {
78
102
  behavior?: ResolvedAgentBehaviorPolicy | null;
79
103
  activeSelfContextId: string | null;
80
104
  selfContexts: NonNullable<MessageCreatedPayload['selfContexts']>;
105
+ replyContext: CanonReplyContext | null;
81
106
  hydratedFromPage: boolean;
82
107
  };
83
108
  export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
@@ -29,11 +29,32 @@ export function buildCanonHostPrompt(input) {
29
29
  ? ['Canon session state:', ...input.sessionContextLines]
30
30
  : []),
31
31
  `Conversation ID: ${input.conversationId}`,
32
+ ...buildCanonReplyContextLines(input.replyContext ?? null),
32
33
  '',
33
34
  'New Canon message:',
34
35
  input.content,
35
36
  ].join('\n');
36
37
  }
38
+ export function buildCanonReplyContextLines(replyContext) {
39
+ if (!replyContext)
40
+ return [];
41
+ const sender = replyContext.senderName || replyContext.senderId || 'unknown sender';
42
+ const senderType = replyContext.senderType ? `, ${replyContext.senderType}` : '';
43
+ const position = replyContext.replyToPosition != null
44
+ ? ` at ${formatReplyPosition(replyContext.replyToPosition)}`
45
+ : '';
46
+ const header = replyContext.found
47
+ ? `This message is replying to ${sender}${senderType} (message ${replyContext.messageId}${position}).`
48
+ : `This message is replying to message ${replyContext.messageId}${position}, but that message was not present in fetched history.`;
49
+ return [
50
+ '',
51
+ 'Reply context:',
52
+ header,
53
+ ...(replyContext.found
54
+ ? ['Replied message:', replyContext.body]
55
+ : []),
56
+ ];
57
+ }
37
58
  /**
38
59
  * Render the **text portion** of an inbound Canon message. Images are
39
60
  * referenced by short placeholders — their actual bytes are delivered to the
@@ -62,6 +83,35 @@ export function renderCanonHostInboundContent(message, materialized) {
62
83
  const rendered = [...placeholders, body].filter(Boolean).join('\n');
63
84
  return rendered || '[Empty message]';
64
85
  }
86
+ export function resolveCanonReplyContext(input) {
87
+ const replyTo = normalizeOptionalString(input.message.replyTo);
88
+ if (!replyTo)
89
+ return null;
90
+ const referenced = input.messages
91
+ ?.find((message) => message.id === replyTo && message.deleted !== true)
92
+ ?? null;
93
+ if (!referenced) {
94
+ return {
95
+ messageId: replyTo,
96
+ body: '',
97
+ replyToPosition: input.message.replyToPosition ?? null,
98
+ found: false,
99
+ };
100
+ }
101
+ return {
102
+ messageId: replyTo,
103
+ senderId: referenced.senderId ?? null,
104
+ senderName: referenced.senderName ?? null,
105
+ senderType: referenced.senderType ?? null,
106
+ text: referenced.text ?? null,
107
+ contentType: referenced.contentType ?? null,
108
+ attachments: referenced.attachments ?? [],
109
+ ...(referenced.contactCard ? { contactCard: referenced.contactCard } : {}),
110
+ body: renderCanonHostInboundContent(referenced),
111
+ replyToPosition: input.message.replyToPosition ?? null,
112
+ found: true,
113
+ };
114
+ }
65
115
  function describeContactCard(card) {
66
116
  const parts = [`${card.userType} · userId: ${card.userId}`];
67
117
  if (card.ownerName)
@@ -88,7 +138,8 @@ function describeContactCard(card) {
88
138
  }
89
139
  function describeAttachment(attachment, materialized) {
90
140
  if (attachment.kind === 'image') {
91
- return '[Image attached]';
141
+ const ref = materialized?.path ? ` ${materialized.path}` : '';
142
+ return `[Image attached${ref}]`;
92
143
  }
93
144
  if (attachment.kind === 'audio') {
94
145
  const durationMs = materialized?.durationMs ?? attachment.durationMs;
@@ -101,6 +152,16 @@ function describeAttachment(attachment, materialized) {
101
152
  const ref = materialized?.path ? ` ${materialized.path}` : '';
102
153
  return `[File: ${label}${ref}]`;
103
154
  }
155
+ function formatReplyPosition(positionMs) {
156
+ if (!Number.isFinite(positionMs) || positionMs < 0)
157
+ return `${positionMs}ms`;
158
+ const totalSeconds = Math.floor(positionMs / 1000);
159
+ const minutes = Math.floor(totalSeconds / 60);
160
+ const seconds = totalSeconds % 60;
161
+ return minutes > 0
162
+ ? `${minutes}:${seconds.toString().padStart(2, '0')}`
163
+ : `${seconds}s`;
164
+ }
104
165
  export function buildHydratedInboundContext(input) {
105
166
  const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
106
167
  const activeSelfContextId = resolveMessageActiveSelfContextId({
@@ -142,6 +203,10 @@ export function buildHydratedInboundContext(input) {
142
203
  behavior: input.page?.behavior ?? input.conversation?.behavior,
143
204
  activeSelfContextId: activeSelfContexts.length > 0 ? activeSelfContextId : null,
144
205
  selfContexts: activeSelfContexts,
206
+ replyContext: resolveCanonReplyContext({
207
+ message: input.message,
208
+ messages: input.page?.messages ?? [],
209
+ }),
145
210
  hydratedFromPage: input.page != null,
146
211
  };
147
212
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
- export type { AddMemberResult, AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonGroupContext, CanonGroupContextMode, CanonKnownRecentParticipant, CanonMembershipChange, CanonResolveAdmissionResult, ConversationUpdatedPayload, ContactAddedPayload, ContactApprovedPayload, ContactCardPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonMessage, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
2
+ export type { AddMemberResult, AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonGroupContext, CanonGroupContextMode, CanonKnownRecentParticipant, CanonMembershipChange, CanonResolveAdmissionResult, ConversationUpdatedPayload, ContactAddedPayload, ContactApprovedPayload, ContactCardPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonMessage, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
3
3
  export type { CanonSelfContext, CanonSelfContextType, SelfContextPromptRenderOptions, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, } from './self-context.js';
4
4
  export { buildSelfContextPromptLines, normalizeSelfContexts, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from './self-context.js';
5
5
  export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, buildWorkspaceRootId, discoverWorkspaceProjects, } from './workspace-discovery.js';
@@ -30,13 +30,13 @@ export { RUNTIMES_DIR, buildLocalRuntimeId, clearRuntimeSessionState, describePr
30
30
  export type { LocalRuntimeCatalogEntry, LocalRuntimeKind, LocalRuntimeReviveCapability, LocalRuntimeSessionState, LocalRuntimeStatus, } from './local-runtime-catalog.js';
31
31
  export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
32
32
  export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
33
- export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
34
- export type { AgentSessionSnapshotPatch, RTDBClientHandle, RuntimeInfoPayloadData, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
35
- export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
36
- export type { HostInboundParticipantContext, } from './host-runtime.js';
33
+ export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, readRuntimeActivity, writeRuntimeActivity, removeRuntimeActivityItem, clearRuntimeActivity, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
34
+ export type { AgentSessionSnapshotPatch, RTDBClientHandle, RuntimeActivityPayloadData, RuntimeInfoPayloadData, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
35
+ export { buildCanonHostPrompt, buildCanonReplyContextLines, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveCanonReplyContext, resolveHostWorkspaceCwd, } from './host-runtime.js';
36
+ export type { CanonReplyContext, HostInboundParticipantContext, } from './host-runtime.js';
37
37
  export { buildCanonGroupContext, buildCanonKnownRecentParticipants, buildCompactGroupContextLines, diffCanonMemberIds, } from './group-context.js';
38
38
  export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
39
- export type { RuntimeStatePublisher, RuntimeStatePublisherOptions, RuntimeStreamingPayload, } from './runtime-state-publisher.js';
39
+ export type { RuntimeStatePublisher, RuntimeStatePublisherOptions, RuntimeStreamingPayload, ClearRuntimeActivityOptions, } from './runtime-state-publisher.js';
40
40
  export { formatCanonMessageAsText } from './message-format.js';
41
41
  export { reachOutToCanonContact } from './reach-out.js';
42
42
  export type { CanonReachOutClient, CanonReachOutOptions, CanonReachOutResult, } from './reach-out.js';
package/dist/index.js CHANGED
@@ -29,9 +29,9 @@ export { RUNTIMES_DIR, buildLocalRuntimeId, clearRuntimeSessionState, describePr
29
29
  // Execution environments for host-mode coding sessions
30
30
  export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
31
31
  // RTDB REST helpers (token exchange, session state, generic read/write)
32
- export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
32
+ export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, readRuntimeActivity, writeRuntimeActivity, removeRuntimeActivityItem, clearRuntimeActivity, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
33
33
  // Runtime host plumbing
34
- export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
34
+ export { buildCanonHostPrompt, buildCanonReplyContextLines, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveCanonReplyContext, resolveHostWorkspaceCwd, } from './host-runtime.js';
35
35
  export { buildCanonGroupContext, buildCanonKnownRecentParticipants, buildCompactGroupContextLines, diffCanonMemberIds, } from './group-context.js';
36
36
  export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
37
37
  // Message formatting (LLM-facing text projection)
@@ -15,26 +15,14 @@ export function formatCanonMessageAsText(message) {
15
15
  const cardText = formatContactCard(message.contactCard);
16
16
  return trimmedText ? `${cardText}\n${trimmedText}` : cardText;
17
17
  }
18
- const attachment = pickPrimaryAttachment(message.attachments);
19
- if (attachment?.kind === 'image') {
20
- return trimmedText ? `[image] ${trimmedText}` : '[image]';
21
- }
22
- if (attachment?.kind === 'audio') {
23
- const seconds = typeof attachment.durationMs === 'number'
24
- ? ` ${Math.round(attachment.durationMs / 1000)}s`
25
- : '';
26
- return trimmedText ? `[audio${seconds}] ${trimmedText}` : `[audio${seconds}]`;
27
- }
28
- if (attachment?.kind === 'file') {
29
- const label = attachment.fileName?.trim() || 'file';
30
- return trimmedText ? `[${label}] ${trimmedText}` : `[${label}]`;
18
+ const attachmentLabels = (message.attachments ?? [])
19
+ .filter((attachment) => Boolean(attachment.url))
20
+ .map(formatAttachment);
21
+ if (attachmentLabels.length > 0) {
22
+ return [...attachmentLabels, trimmedText].filter(Boolean).join('\n');
31
23
  }
32
24
  return trimmedText || '[message]';
33
25
  }
34
- function pickPrimaryAttachment(attachments) {
35
- const first = attachments?.[0];
36
- return first?.url ? first : null;
37
- }
38
26
  function formatContactCard(card) {
39
27
  const displayName = card.displayName?.trim() || 'Unknown';
40
28
  const parts = [card.userType, `userId: ${card.userId}`];
@@ -44,3 +32,16 @@ function formatContactCard(card) {
44
32
  parts.push(`about: ${card.about}`);
45
33
  return `[Contact card] "${displayName}" — ${parts.join(' · ')}`;
46
34
  }
35
+ function formatAttachment(attachment) {
36
+ if (attachment.kind === 'image') {
37
+ return '[image]';
38
+ }
39
+ if (attachment.kind === 'audio') {
40
+ const seconds = typeof attachment.durationMs === 'number'
41
+ ? ` ${Math.round(attachment.durationMs / 1000)}s`
42
+ : '';
43
+ return `[audio${seconds}]`;
44
+ }
45
+ const label = attachment.fileName?.trim() || 'file';
46
+ return `[${label}]`;
47
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { CanonClient } from './client.js';
9
9
  import type { DeliveryIntent, RuntimeCapabilities, TurnLifecycleState } from './turn-protocol.js';
10
- import type { CanonControlValue, ModelOption, RuntimeControlError, RuntimeControlState, RuntimeInfoPayload } from './types.js';
10
+ import type { CanonControlValue, CanonRuntimeActivityItem, ModelOption, RuntimeControlError, RuntimeControlState, RuntimeInfoPayload } from './types.js';
11
11
  export interface SessionStatePayload {
12
12
  lastError?: string;
13
13
  model?: string;
@@ -101,6 +101,11 @@ export interface RuntimeInfoPayloadData extends Omit<RuntimeInfoPayload, 'update
101
101
  '.sv': 'timestamp';
102
102
  };
103
103
  }
104
+ export interface RuntimeActivityPayloadData extends Omit<CanonRuntimeActivityItem, 'updatedAt'> {
105
+ updatedAt: number | {
106
+ '.sv': 'timestamp';
107
+ };
108
+ }
104
109
  interface RTDBAuthOptions {
105
110
  rtdbUrl?: string;
106
111
  firebaseApiKey?: string;
@@ -118,6 +123,10 @@ export interface RTDBClientHandle {
118
123
  writeRuntimeInfo(conversationId: string, agentId: string, payload: Omit<RuntimeInfoPayload, 'updatedAt'>): Promise<void>;
119
124
  patchRuntimeInfo(conversationId: string, agentId: string, payload: Partial<Omit<RuntimeInfoPayload, 'updatedAt'>>): Promise<void>;
120
125
  clearRuntimeInfo(conversationId: string, agentId: string): Promise<void>;
126
+ readRuntimeActivity(conversationId: string, agentId: string): Promise<Record<string, CanonRuntimeActivityItem>>;
127
+ writeRuntimeActivity(conversationId: string, agentId: string, item: CanonRuntimeActivityItem): Promise<void>;
128
+ removeRuntimeActivityItem(conversationId: string, agentId: string, itemId: string): Promise<void>;
129
+ clearRuntimeActivity(conversationId: string, agentId: string): Promise<void>;
121
130
  }
122
131
  /**
123
132
  * Initializes the default RTDB helper and returns a scoped client for callers
@@ -143,4 +152,8 @@ export declare function patchAgentSessionSnapshot(conversationId: string, agentI
143
152
  export declare function writeRuntimeInfo(conversationId: string, agentId: string, payload: Omit<RuntimeInfoPayload, 'updatedAt'>): Promise<void>;
144
153
  export declare function patchRuntimeInfo(conversationId: string, agentId: string, payload: Partial<Omit<RuntimeInfoPayload, 'updatedAt'>>): Promise<void>;
145
154
  export declare function clearRuntimeInfo(conversationId: string, agentId: string): Promise<void>;
155
+ export declare function readRuntimeActivity(conversationId: string, agentId: string): Promise<Record<string, CanonRuntimeActivityItem>>;
156
+ export declare function writeRuntimeActivity(conversationId: string, agentId: string, item: CanonRuntimeActivityItem): Promise<void>;
157
+ export declare function removeRuntimeActivityItem(conversationId: string, agentId: string, itemId: string): Promise<void>;
158
+ export declare function clearRuntimeActivity(conversationId: string, agentId: string): Promise<void>;
146
159
  export {};
package/dist/rtdb-rest.js CHANGED
@@ -21,6 +21,16 @@ function buildAgentSessionPath(conversationId, agentId) {
21
21
  function buildRuntimeInfoPath(conversationId, agentId) {
22
22
  return `/runtime-info/${conversationId}/${agentId}`;
23
23
  }
24
+ function buildRuntimeActivityPath(conversationId, agentId) {
25
+ return `/runtime-activity/${conversationId}/${agentId}`;
26
+ }
27
+ function runtimeActivityItemKey(itemId) {
28
+ const normalized = itemId.trim().replace(/[.#$\[\]\/]/g, '_');
29
+ return normalized || 'item';
30
+ }
31
+ function buildRuntimeActivityItemPath(conversationId, agentId, itemId) {
32
+ return `${buildRuntimeActivityPath(conversationId, agentId)}/${runtimeActivityItemKey(itemId)}`;
33
+ }
24
34
  function createRTDBClientHandle(client, options) {
25
35
  const rtdbBase = normalizeRTDBBase(options?.rtdbUrl || DEFAULT_RTDB_BASE);
26
36
  const firebaseApiKey = options?.firebaseApiKey || DEFAULT_FIREBASE_API_KEY;
@@ -247,6 +257,25 @@ function createRTDBClientHandle(client, options) {
247
257
  async function clearRuntimeInfoImpl(conversationId, agentId) {
248
258
  await remove(buildRuntimeInfoPath(conversationId, agentId));
249
259
  }
260
+ async function readRuntimeActivityImpl(conversationId, agentId) {
261
+ const raw = await read(buildRuntimeActivityPath(conversationId, agentId));
262
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
263
+ return {};
264
+ }
265
+ return raw;
266
+ }
267
+ async function writeRuntimeActivityImpl(conversationId, agentId, item) {
268
+ await write(buildRuntimeActivityItemPath(conversationId, agentId, item.id), {
269
+ ...item,
270
+ updatedAt: item.updatedAt || { '.sv': 'timestamp' },
271
+ });
272
+ }
273
+ async function removeRuntimeActivityItemImpl(conversationId, agentId, itemId) {
274
+ await remove(buildRuntimeActivityItemPath(conversationId, agentId, itemId));
275
+ }
276
+ async function clearRuntimeActivityImpl(conversationId, agentId) {
277
+ await remove(buildRuntimeActivityPath(conversationId, agentId));
278
+ }
250
279
  return {
251
280
  read,
252
281
  write,
@@ -260,6 +289,10 @@ function createRTDBClientHandle(client, options) {
260
289
  writeRuntimeInfo: writeRuntimeInfoImpl,
261
290
  patchRuntimeInfo: patchRuntimeInfoImpl,
262
291
  clearRuntimeInfo: clearRuntimeInfoImpl,
292
+ readRuntimeActivity: readRuntimeActivityImpl,
293
+ writeRuntimeActivity: writeRuntimeActivityImpl,
294
+ removeRuntimeActivityItem: removeRuntimeActivityItemImpl,
295
+ clearRuntimeActivity: clearRuntimeActivityImpl,
263
296
  };
264
297
  }
265
298
  /**
@@ -314,3 +347,15 @@ export async function patchRuntimeInfo(conversationId, agentId, payload) {
314
347
  export async function clearRuntimeInfo(conversationId, agentId) {
315
348
  await getDefaultRTDBClient()?.clearRuntimeInfo(conversationId, agentId);
316
349
  }
350
+ export async function readRuntimeActivity(conversationId, agentId) {
351
+ return getDefaultRTDBClient()?.readRuntimeActivity(conversationId, agentId) ?? {};
352
+ }
353
+ export async function writeRuntimeActivity(conversationId, agentId, item) {
354
+ await getDefaultRTDBClient()?.writeRuntimeActivity(conversationId, agentId, item);
355
+ }
356
+ export async function removeRuntimeActivityItem(conversationId, agentId, itemId) {
357
+ await getDefaultRTDBClient()?.removeRuntimeActivityItem(conversationId, agentId, itemId);
358
+ }
359
+ export async function clearRuntimeActivity(conversationId, agentId) {
360
+ await getDefaultRTDBClient()?.clearRuntimeActivity(conversationId, agentId);
361
+ }
@@ -8,12 +8,12 @@ export const EXECUTION_MODE_CONTROL_OPTIONS = [
8
8
  {
9
9
  value: 'worktree',
10
10
  label: 'Isolated worktree',
11
- description: 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.',
11
+ description: 'Best-effort git worktree for this conversation. This is not a security sandbox; if unavailable, Canon may fall back to the shared project.',
12
12
  },
13
13
  {
14
14
  value: 'locked',
15
15
  label: 'Use shared project',
16
- description: 'Runs directly in the selected project folder. Changes happen there.',
16
+ description: 'Runs directly in the selected project folder. File changes happen there.',
17
17
  },
18
18
  ];
19
19
  export function buildRuntimeWorkspaceControlOptions(workspaces) {
@@ -1,4 +1,4 @@
1
- import type { AgentClientType, AgentRuntime, RuntimeInfoPayload } from './types.js';
1
+ import type { AgentClientType, AgentRuntime, CanonRuntimeActivityItem, RuntimeInfoPayload } from './types.js';
2
2
  import { type AgentSessionSnapshotPatch, type RTDBClientHandle, type SessionStatePayload, type TurnStatePayload } from './rtdb-rest.js';
3
3
  export interface RuntimeStatePublisherOptions {
4
4
  agentId: string;
@@ -14,6 +14,12 @@ export interface RuntimeStreamingPayload {
14
14
  '.sv': 'timestamp';
15
15
  };
16
16
  }
17
+ export interface ClearRuntimeActivityOptions {
18
+ itemIds?: ReadonlyArray<string>;
19
+ terminalOnly?: boolean;
20
+ olderThanMs?: number;
21
+ all?: boolean;
22
+ }
17
23
  export interface RuntimeStatePublisher {
18
24
  publishAgentRuntime(runtime: AgentRuntime): Promise<void>;
19
25
  clearAgentRuntime(): Promise<void>;
@@ -25,6 +31,8 @@ export interface RuntimeStatePublisher {
25
31
  writeRuntimeInfo(conversationId: string, payload: Omit<RuntimeInfoPayload, 'updatedAt'>): Promise<void>;
26
32
  patchRuntimeInfo(conversationId: string, payload: Partial<Omit<RuntimeInfoPayload, 'updatedAt'>>): Promise<void>;
27
33
  clearRuntimeInfo(conversationId: string): Promise<void>;
34
+ writeRuntimeActivity(conversationId: string, item: CanonRuntimeActivityItem): Promise<void>;
35
+ clearRuntimeActivity(conversationId: string, options?: ClearRuntimeActivityOptions): Promise<void>;
28
36
  writeStreaming(conversationId: string, payload: RuntimeStreamingPayload): Promise<void>;
29
37
  clearStreaming(conversationId: string): Promise<void>;
30
38
  }
@@ -1,8 +1,39 @@
1
- import { clearRuntimeInfo, clearSessionState, clearTurnState, patchAgentSessionSnapshot, patchRuntimeInfo, rtdbWrite, writeRuntimeInfo, writeSessionState, writeTurnState, } from './rtdb-rest.js';
1
+ import { clearRuntimeInfo, clearRuntimeActivity, clearSessionState, clearTurnState, patchAgentSessionSnapshot, patchRuntimeInfo, readRuntimeActivity, removeRuntimeActivityItem, rtdbWrite, writeRuntimeActivity, writeRuntimeInfo, writeSessionState, writeTurnState, } from './rtdb-rest.js';
2
2
  const SERVER_TIMESTAMP = { '.sv': 'timestamp' };
3
+ const MAX_RUNTIME_ACTIVITY_ITEMS = 50;
4
+ const TERMINAL_RUNTIME_ACTIVITY_CLEAR_MS = 60_000;
5
+ const TERMINAL_RUNTIME_ACTIVITY_STATUSES = new Set(['completed', 'failed', 'blocked']);
6
+ function isTerminalRuntimeActivity(item) {
7
+ return TERMINAL_RUNTIME_ACTIVITY_STATUSES.has(item.status);
8
+ }
3
9
  export function createRuntimeStatePublisher(options) {
4
10
  const { agentId, clientType, hostMode, rtdb } = options;
5
11
  const writePath = (path, data) => (rtdb ? rtdb.write(path, data) : rtdbWrite(path, data));
12
+ const readRuntimeActivityPath = (conversationId) => (rtdb
13
+ ? rtdb.readRuntimeActivity(conversationId, agentId)
14
+ : readRuntimeActivity(conversationId, agentId));
15
+ const removeRuntimeActivityPath = (conversationId, itemId) => (rtdb
16
+ ? rtdb.removeRuntimeActivityItem(conversationId, agentId, itemId)
17
+ : removeRuntimeActivityItem(conversationId, agentId, itemId));
18
+ const clearRuntimeActivityPath = (conversationId) => (rtdb
19
+ ? rtdb.clearRuntimeActivity(conversationId, agentId)
20
+ : clearRuntimeActivity(conversationId, agentId));
21
+ async function pruneRuntimeActivity(conversationId) {
22
+ const itemsById = await readRuntimeActivityPath(conversationId).catch(() => ({}));
23
+ const items = Object.values(itemsById)
24
+ .filter((item) => Boolean(item?.id))
25
+ .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
26
+ const excess = items.slice(MAX_RUNTIME_ACTIVITY_ITEMS);
27
+ await Promise.all(excess.map((item) => removeRuntimeActivityPath(conversationId, item.id).catch(() => { })));
28
+ }
29
+ function scheduleTerminalRuntimeActivityClear(conversationId, item) {
30
+ if (!isTerminalRuntimeActivity(item))
31
+ return;
32
+ const timer = setTimeout(() => {
33
+ void removeRuntimeActivityPath(conversationId, item.id).catch(() => { });
34
+ }, TERMINAL_RUNTIME_ACTIVITY_CLEAR_MS);
35
+ timer.unref?.();
36
+ }
6
37
  return {
7
38
  async publishAgentRuntime(runtime) {
8
39
  await writePath(`/agent-runtime/${agentId}`, {
@@ -55,6 +86,37 @@ export function createRuntimeStatePublisher(options) {
55
86
  ? rtdb.clearRuntimeInfo(conversationId, agentId)
56
87
  : clearRuntimeInfo(conversationId, agentId));
57
88
  },
89
+ async writeRuntimeActivity(conversationId, item) {
90
+ const normalized = {
91
+ ...item,
92
+ updatedAt: item.updatedAt || Date.now(),
93
+ };
94
+ await (rtdb
95
+ ? rtdb.writeRuntimeActivity(conversationId, agentId, normalized)
96
+ : writeRuntimeActivity(conversationId, agentId, normalized));
97
+ void pruneRuntimeActivity(conversationId).catch(() => { });
98
+ scheduleTerminalRuntimeActivityClear(conversationId, normalized);
99
+ },
100
+ async clearRuntimeActivity(conversationId, options = {}) {
101
+ if (options.all) {
102
+ await clearRuntimeActivityPath(conversationId);
103
+ return;
104
+ }
105
+ if (options.itemIds?.length) {
106
+ await Promise.all(options.itemIds.map((itemId) => removeRuntimeActivityPath(conversationId, itemId)));
107
+ return;
108
+ }
109
+ const olderThanMs = options.olderThanMs ?? 0;
110
+ const cutoff = olderThanMs > 0 ? Date.now() - olderThanMs : Number.POSITIVE_INFINITY;
111
+ const terminalOnly = options.terminalOnly ?? true;
112
+ const itemsById = await readRuntimeActivityPath(conversationId).catch(() => ({}));
113
+ const removals = Object.values(itemsById)
114
+ .filter((item) => Boolean(item?.id))
115
+ .filter((item) => (!terminalOnly || isTerminalRuntimeActivity(item)))
116
+ .filter((item) => olderThanMs <= 0 || (item.updatedAt ?? 0) <= cutoff)
117
+ .map((item) => removeRuntimeActivityPath(conversationId, item.id));
118
+ await Promise.all(removals);
119
+ },
58
120
  async writeStreaming(conversationId, payload) {
59
121
  await writePath(`/streaming/${conversationId}/${agentId}`, {
60
122
  ...payload,
package/dist/types.d.ts CHANGED
@@ -226,6 +226,7 @@ export type CanonControlSelectionPolicy = 'inherit' | 'required_explicit';
226
226
  export type CanonRuntimeStreamingMode = 'none' | 'status' | 'snapshot' | 'block' | 'delta';
227
227
  export type CanonRuntimeSurfaceMode = 'host' | 'channel' | 'limited_channel' | 'operator';
228
228
  export type CanonRuntimeDetailTier = 'primary' | 'detail' | 'diagnostic';
229
+ export type CanonRuntimeFactGroup = 'connection' | 'route' | 'runtime' | 'model' | 'session' | 'account' | 'limits';
229
230
  export type CanonRuntimeInventoryStatus = 'ready' | 'auth_needed' | 'unknown' | 'configured' | 'running' | 'error';
230
231
  export type CanonRuntimeStatusTone = 'default' | 'success' | 'warning' | 'danger';
231
232
  export type CanonRuntimeActionAvailability = 'idle' | 'busy' | 'busy_with_queue' | 'waiting_input' | 'always';
@@ -343,6 +344,32 @@ export interface CanonRuntimeStatusItem {
343
344
  sensitive?: boolean;
344
345
  source?: RuntimeControlValueSource;
345
346
  }
347
+ export interface CanonRuntimeFact {
348
+ id: string;
349
+ label: string;
350
+ value: string;
351
+ group: CanonRuntimeFactGroup;
352
+ tier?: CanonRuntimeDetailTier;
353
+ tone?: 'neutral' | 'good' | 'warning' | 'danger';
354
+ copyable?: boolean;
355
+ updatedAt?: number;
356
+ }
357
+ export type CanonRuntimeActivityKind = 'run' | 'tool' | 'command_output' | 'plan' | 'approval' | 'artifact' | 'compaction' | 'status';
358
+ export type CanonRuntimeActivityStatus = 'running' | 'completed' | 'failed' | 'blocked' | 'pending';
359
+ export interface CanonRuntimeActivityItem {
360
+ id: string;
361
+ runId?: string;
362
+ kind: CanonRuntimeActivityKind;
363
+ title: string;
364
+ status: CanonRuntimeActivityStatus;
365
+ summary?: string;
366
+ detail?: string;
367
+ progressText?: string;
368
+ startedAt?: number;
369
+ updatedAt: number;
370
+ endedAt?: number;
371
+ actions?: ReadonlyArray<CanonRuntimeCommandDescriptor>;
372
+ }
346
373
  export interface CanonRuntimeInventoryEntry {
347
374
  id: string;
348
375
  label: string;
@@ -359,6 +386,7 @@ export interface RuntimeInfoPayload {
359
386
  descriptor: CanonRuntimeDescriptor;
360
387
  surfaceMode?: CanonRuntimeSurfaceMode;
361
388
  surfaceLabel?: string;
389
+ facts?: ReadonlyArray<CanonRuntimeFact>;
362
390
  statusItems?: ReadonlyArray<CanonRuntimeStatusItem>;
363
391
  inventories?: ReadonlyArray<CanonRuntimeInventory>;
364
392
  execution?: CanonRuntimeExecutionMetadata | null;
@@ -588,6 +616,7 @@ export interface SessionConfig {
588
616
  export interface PermissionModeOption {
589
617
  value: string;
590
618
  label: string;
619
+ description?: string;
591
620
  }
592
621
  export declare const CLAUDE_PERMISSION_MODE_OPTIONS: readonly [{
593
622
  readonly value: "default";
@@ -646,6 +675,7 @@ export interface AgentSessionSnapshot {
646
675
  runtimeControlErrors?: Record<string, RuntimeControlError> | null;
647
676
  runtimeDescriptor?: CanonRuntimeDescriptor | null;
648
677
  runtimeInfo?: RuntimeInfoPayload | null;
678
+ runtimeActivity?: Record<string, CanonRuntimeActivityItem> | ReadonlyArray<CanonRuntimeActivityItem> | null;
649
679
  workspaceId?: string;
650
680
  workspaceOptions?: WorkspaceOption[];
651
681
  executionMode?: ExecutionEnvironmentMode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.17.2",
3
+ "version": "0.18.1",
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",