@canonmsg/core 0.18.0 → 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.
@@ -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
@@ -32,8 +32,8 @@ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, build
32
32
  export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
33
33
  export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, readRuntimeActivity, writeRuntimeActivity, removeRuntimeActivityItem, clearRuntimeActivity, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
34
34
  export type { AgentSessionSnapshotPatch, RTDBClientHandle, RuntimeActivityPayloadData, 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';
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
39
  export type { RuntimeStatePublisher, RuntimeStatePublisherOptions, RuntimeStreamingPayload, ClearRuntimeActivityOptions, } from './runtime-state-publisher.js';
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, build
31
31
  // RTDB REST helpers (token exchange, session state, generic read/write)
32
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
+ }
@@ -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) {
package/dist/types.d.ts CHANGED
@@ -616,6 +616,7 @@ export interface SessionConfig {
616
616
  export interface PermissionModeOption {
617
617
  value: string;
618
618
  label: string;
619
+ description?: string;
619
620
  }
620
621
  export declare const CLAUDE_PERMISSION_MODE_OPTIONS: readonly [{
621
622
  readonly value: "default";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.18.0",
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",