@canonmsg/codex-plugin 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/adapter.d.ts CHANGED
@@ -36,7 +36,7 @@ export declare class CodexConversationAdapter {
36
36
  private readonly codexBin;
37
37
  private model;
38
38
  private readonly sandbox;
39
- private readonly approvalPolicy;
39
+ private readonly legacyApprovalPolicy;
40
40
  private readonly codexProfile;
41
41
  private readonly addDirs;
42
42
  private readonly configOverrides;
package/dist/adapter.js CHANGED
@@ -5,7 +5,7 @@ export class CodexConversationAdapter {
5
5
  codexBin;
6
6
  model;
7
7
  sandbox;
8
- approvalPolicy;
8
+ legacyApprovalPolicy;
9
9
  codexProfile;
10
10
  addDirs;
11
11
  configOverrides;
@@ -21,7 +21,7 @@ export class CodexConversationAdapter {
21
21
  this.codexBin = opts.codexBin ?? 'codex';
22
22
  this.model = opts.model ?? null;
23
23
  this.sandbox = opts.sandbox ?? null;
24
- this.approvalPolicy = opts.approvalPolicy ?? null;
24
+ this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
25
25
  this.codexProfile = opts.codexProfile ?? null;
26
26
  this.addDirs = opts.addDirs ?? [];
27
27
  this.configOverrides = opts.configOverrides ?? [];
@@ -165,7 +165,7 @@ export class CodexConversationAdapter {
165
165
  const args = ['exec', '--json', '--color', 'never', '-C', this.cwd, '--skip-git-repo-check'];
166
166
  const execMode = resolveExecMode({
167
167
  sandbox: this.sandbox,
168
- approvalPolicy: this.approvalPolicy,
168
+ approvalPolicy: this.legacyApprovalPolicy,
169
169
  fullAuto: this.fullAuto,
170
170
  bypassApprovalsAndSandbox: this.bypassApprovalsAndSandbox,
171
171
  });
@@ -231,15 +231,18 @@ function resolveExecMode(input) {
231
231
  if (input.fullAuto) {
232
232
  return { fullAuto: true, bypassApprovalsAndSandbox: false };
233
233
  }
234
- // Newer Codex CLI releases no longer accept --ask-for-approval for `exec`.
235
- // Preserve the old Canon example (`--sandbox workspace-write --ask-for-approval never`)
236
- // by translating it to the supported `--full-auto` mode.
237
- if (input.approvalPolicy === 'never' &&
238
- (input.sandbox === 'workspace-write' || input.sandbox == null)) {
234
+ if (shouldTranslateLegacyApprovalMode(input)) {
239
235
  return { fullAuto: true, bypassApprovalsAndSandbox: false };
240
236
  }
241
237
  return { fullAuto: false, bypassApprovalsAndSandbox: false };
242
238
  }
239
+ function shouldTranslateLegacyApprovalMode(input) {
240
+ // Newer Codex CLI releases no longer accept --ask-for-approval for `exec`.
241
+ // Keep the compatibility shim isolated here so the rest of the adapter only
242
+ // deals with the supported execution switches.
243
+ return input.approvalPolicy === 'never'
244
+ && (input.sandbox === 'workspace-write' || input.sandbox == null);
245
+ }
243
246
  function isIgnorableCodexLog(line) {
244
247
  return [
245
248
  'Reading additional input from stdin...',
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Host-runtime helpers, inlined from @canonmsg/core.
3
+ *
4
+ * These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
5
+ * RTDB session/runtime surface. They used to live in
6
+ * `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
7
+ * media-parity PR C so core does not have to carry host-specific concerns.
8
+ *
9
+ * Keep this file in lockstep with the equivalent file in
10
+ * `packages/claude-code-plugin/src/host-runtime.ts`. If you change the
11
+ * behavior here, update that copy too and adjust the shared golden
12
+ * fixture test (`packages/codex-plugin/src/host-runtime.test.ts`).
13
+ */
14
+ import { type AgentClientType, type AgentRuntime, type CanonClient, type CanonConversation, type CanonMessage, type CanonMessagesPage, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type SessionWorkspaceConfig } from '@canonmsg/core';
15
+ export interface HostInboundParticipantContext {
16
+ conversationType: CanonConversation['type'] | 'unknown';
17
+ memberCount: number | null;
18
+ senderType: 'human' | 'ai_agent';
19
+ senderName: string;
20
+ isOwner: boolean;
21
+ mentionedAgent: boolean;
22
+ recentSenderTypes: Array<'human' | 'ai_agent'>;
23
+ recentHumanCount: number;
24
+ recentAgentCount: number;
25
+ consecutiveAgentTurns: number;
26
+ currentAgentStreakStartedByHuman: boolean;
27
+ }
28
+ type HostInboundMessage = {
29
+ text?: string | null;
30
+ contentType?: CanonMessage['contentType'] | null;
31
+ audioUrl?: string | null;
32
+ audioDurationMs?: number | null;
33
+ imageUrl?: string | null;
34
+ attachments?: CanonMessage['attachments'];
35
+ senderType?: CanonMessage['senderType'];
36
+ mentions?: string[] | null;
37
+ };
38
+ interface HostWorkspaceResolverOption {
39
+ id: string;
40
+ cwd: string;
41
+ }
42
+ export declare function buildCanonHostPrompt(input: {
43
+ hostLabel: string;
44
+ content: string;
45
+ conversationId: string;
46
+ participantContext: HostInboundParticipantContext;
47
+ behavior?: ResolvedAgentBehaviorPolicy | null;
48
+ workSession?: MessageCreatedPayload['message']['workSession'];
49
+ workSessions?: MessageCreatedPayload['workSessions'];
50
+ buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
51
+ }): string;
52
+ export declare function renderCanonHostInboundContent(message: HostInboundMessage): string;
53
+ export declare function buildHydratedInboundContext(input: {
54
+ agentId: string;
55
+ conversation: CanonConversation | null;
56
+ page?: CanonMessagesPage | null;
57
+ message: HostInboundMessage;
58
+ senderName: string;
59
+ isOwner: boolean;
60
+ }): {
61
+ participantContext: HostInboundParticipantContext;
62
+ behavior?: ResolvedAgentBehaviorPolicy | null;
63
+ workSessions: NonNullable<MessageCreatedPayload['workSessions']>;
64
+ hydratedFromPage: boolean;
65
+ };
66
+ export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
67
+ export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null;
68
+ export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
69
+ conversationId: string;
70
+ agentId: string;
71
+ extraStringFields?: readonly TExtra[];
72
+ }): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null>;
73
+ export declare function resolveHostWorkspaceCwd(input: {
74
+ workspaceOptions: HostWorkspaceResolverOption[];
75
+ config: {
76
+ workspaceId?: string;
77
+ retiredWorkspaceConfig?: boolean;
78
+ } | null;
79
+ defaultCwd: string;
80
+ }): string;
81
+ export declare function createConversationMetadataLoader(input: {
82
+ client: CanonClient;
83
+ conversationCache: Map<string, CanonConversation>;
84
+ cacheTtlMs?: number;
85
+ }): {
86
+ refreshConversationCache(force?: boolean): Promise<void>;
87
+ getConversationMeta(conversationId: string): Promise<CanonConversation | null>;
88
+ };
89
+ export {};
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Host-runtime helpers, inlined from @canonmsg/core.
3
+ *
4
+ * These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
5
+ * RTDB session/runtime surface. They used to live in
6
+ * `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
7
+ * media-parity PR C so core does not have to carry host-specific concerns.
8
+ *
9
+ * Keep this file in lockstep with the equivalent file in
10
+ * `packages/claude-code-plugin/src/host-runtime.ts`. If you change the
11
+ * behavior here, update that copy too and adjust the shared golden
12
+ * fixture test (`packages/codex-plugin/src/host-runtime.test.ts`).
13
+ */
14
+ import { buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
15
+ export function buildCanonHostPrompt(input) {
16
+ const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
17
+ return [
18
+ `You are connected to Canon messaging through a ${input.hostLabel} host wrapper.`,
19
+ 'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
20
+ 'Short intermediate assistant messages may be shown as ephemeral status while you work.',
21
+ ...input.buildInboundContextLines(input.participantContext),
22
+ ...buildBehaviorPolicyLines(input.behavior),
23
+ ...buildWorkSessionsPromptLines(resolvedWorkSessions),
24
+ 'Canon participants may be humans or AI agents.',
25
+ 'Honor the Canon behavior policy above when deciding how proactively to participate.',
26
+ ...(resolvedWorkSessions.length > 0
27
+ ? ['Honor the Canon work-session context above within its stated disclosure limits.']
28
+ : []),
29
+ `Conversation ID: ${input.conversationId}`,
30
+ '',
31
+ 'New Canon message:',
32
+ input.content,
33
+ ].join('\n');
34
+ }
35
+ export function renderCanonHostInboundContent(message) {
36
+ let content = message.text || '';
37
+ const attachment = message.attachments?.[0];
38
+ if (attachment?.kind === 'audio' && attachment.url) {
39
+ const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
40
+ content = content
41
+ ? `[Voice message${duration}: ${attachment.url}]\n${content}`
42
+ : `[Voice message${duration}: ${attachment.url}]`;
43
+ }
44
+ else if (attachment?.kind === 'image' && attachment.url) {
45
+ content = content
46
+ ? `[Image: ${attachment.url}]\n${content}`
47
+ : `[Image: ${attachment.url}]`;
48
+ }
49
+ else if (attachment?.kind === 'file' && attachment.url) {
50
+ const label = attachment.fileName || 'File';
51
+ content = content
52
+ ? `[File: ${label} ${attachment.url}]\n${content}`
53
+ : `[File: ${label} ${attachment.url}]`;
54
+ }
55
+ else if (message.contentType === 'audio' && message.audioUrl) {
56
+ const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
57
+ content = content
58
+ ? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
59
+ : `[Voice message${duration}: ${message.audioUrl}]`;
60
+ }
61
+ else if (message.contentType === 'image' && message.imageUrl) {
62
+ content = content
63
+ ? `[Image: ${message.imageUrl}]\n${content}`
64
+ : `[Image: ${message.imageUrl}]`;
65
+ }
66
+ return content || '[Empty message]';
67
+ }
68
+ export function buildHydratedInboundContext(input) {
69
+ const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
70
+ return {
71
+ participantContext: {
72
+ conversationType: input.conversation?.type ?? 'unknown',
73
+ memberCount: input.conversation?.memberIds?.length ?? null,
74
+ senderType: input.message.senderType ?? 'human',
75
+ senderName: input.senderName,
76
+ isOwner: input.isOwner,
77
+ mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
78
+ recentSenderTypes: history.recentSenderTypes,
79
+ recentHumanCount: history.recentHumanCount,
80
+ recentAgentCount: history.recentAgentCount,
81
+ consecutiveAgentTurns: history.consecutiveAgentTurns,
82
+ currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
83
+ },
84
+ behavior: input.page?.behavior ?? input.conversation?.behavior,
85
+ workSessions: input.page?.workSessions ?? [],
86
+ hydratedFromPage: input.page != null,
87
+ };
88
+ }
89
+ export async function publishHostAgentRuntime(agentId, clientType, runtime) {
90
+ await rtdbWrite(`/agent-runtime/${agentId}`, {
91
+ clientType,
92
+ hostMode: true,
93
+ ...runtime,
94
+ updatedAt: { '.sv': 'timestamp' },
95
+ });
96
+ }
97
+ export function readHostSessionConfig(raw, extraStringFields = []) {
98
+ const baseConfig = readSessionWorkspaceConfig(raw);
99
+ if (!raw || typeof raw !== 'object') {
100
+ return baseConfig;
101
+ }
102
+ const data = raw;
103
+ const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
104
+ const value = normalizeOptionalString(data[field]);
105
+ return value ? [[field, value]] : [];
106
+ }));
107
+ return {
108
+ ...(baseConfig ?? {}),
109
+ ...extraConfig,
110
+ };
111
+ }
112
+ export async function loadHostSessionConfig(input) {
113
+ const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
114
+ return readHostSessionConfig(raw, input.extraStringFields);
115
+ }
116
+ export function resolveHostWorkspaceCwd(input) {
117
+ return resolveConfiguredWorkspaceCwd(input);
118
+ }
119
+ export function createConversationMetadataLoader(input) {
120
+ const cacheTtlMs = input.cacheTtlMs ?? 10_000;
121
+ let conversationCacheLoadedAt = 0;
122
+ async function refreshConversationCache(force = false) {
123
+ if (!force
124
+ && input.conversationCache.size > 0
125
+ && Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
126
+ return;
127
+ }
128
+ const conversations = await input.client.getConversations();
129
+ input.conversationCache.clear();
130
+ for (const conversation of conversations) {
131
+ input.conversationCache.set(conversation.id, conversation);
132
+ }
133
+ conversationCacheLoadedAt = Date.now();
134
+ }
135
+ async function getConversationMeta(conversationId) {
136
+ try {
137
+ await refreshConversationCache();
138
+ const cached = input.conversationCache.get(conversationId);
139
+ if (cached)
140
+ return cached;
141
+ await refreshConversationCache(true);
142
+ return input.conversationCache.get(conversationId) ?? null;
143
+ }
144
+ catch {
145
+ return input.conversationCache.get(conversationId) ?? null;
146
+ }
147
+ }
148
+ return {
149
+ refreshConversationCache,
150
+ getConversationMeta,
151
+ };
152
+ }
package/dist/host.js CHANGED
@@ -3,7 +3,8 @@ import { setDefaultResultOrder } from 'node:dns';
3
3
  setDefaultResultOrder('ipv4first');
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { parseArgs } from 'node:util';
6
- import { buildParticipationHistorySnapshot, buildBehaviorPolicyLines, buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, ExecutionEnvironmentError, isEnabledFlag, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, CanonClient, CanonStream, clearSessionState, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfile, initRTDBAuth, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, releaseLock, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, writeSessionState, writeTurnState, } from '@canonmsg/core';
6
+ import { buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, ExecutionEnvironmentError, isEnabledFlag, CanonClient, CanonStream, clearSessionState, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfile, initRTDBAuth, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, releaseLock, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, writeSessionState, writeTurnState, } from '@canonmsg/core';
7
+ import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
7
8
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
8
9
  import { CodexConversationAdapter, } from './adapter.js';
9
10
  import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
@@ -37,71 +38,27 @@ function normalizeRuntimeTurnState(value) {
37
38
  return null;
38
39
  }
39
40
  async function publishAgentRuntime(agentId, runtime) {
40
- await rtdbWrite(`/agent-runtime/${agentId}`, {
41
- clientType: 'codex',
42
- hostMode: true,
43
- ...runtime,
44
- updatedAt: { '.sv': 'timestamp' },
45
- });
41
+ await publishHostAgentRuntime(agentId, 'codex', runtime);
46
42
  }
47
43
  async function loadSessionConfig(conversationId, agentId) {
48
- const raw = await rtdbRead(`/session-config/${conversationId}/${agentId}`);
49
- return readSessionWorkspaceConfig(raw);
44
+ return loadHostSessionConfig({ conversationId, agentId });
50
45
  }
51
46
  function resolveWorkspaceCwd(config) {
52
- return resolveConfiguredWorkspaceCwd({
47
+ return resolveHostWorkspaceCwd({
53
48
  workspaceOptions,
54
49
  config,
55
50
  defaultCwd: workingDir,
56
51
  });
57
52
  }
58
53
  function buildCanonPrompt(input) {
59
- return [
60
- 'You are connected to Canon messaging through a Codex host wrapper.',
61
- 'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
62
- 'Short intermediate assistant messages may be shown as ephemeral status while you work.',
63
- ...buildInboundContextLines(input.participantContext),
64
- ...buildBehaviorPolicyLines(input.behavior),
65
- 'Canon participants may be humans or AI agents.',
66
- 'Honor the Canon behavior policy above when deciding how proactively to participate.',
67
- `Conversation ID: ${input.conversationId}`,
68
- '',
69
- 'New Canon message:',
70
- input.content,
71
- ].join('\n');
54
+ return buildCanonHostPrompt({
55
+ hostLabel: 'Codex',
56
+ buildInboundContextLines,
57
+ ...input,
58
+ });
72
59
  }
73
60
  function renderInboundContent(message) {
74
- let content = message.text || '';
75
- const attachment = message.attachments?.[0];
76
- if (attachment?.kind === 'audio' && attachment.url) {
77
- const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
78
- content = content
79
- ? `[Voice message${duration}: ${attachment.url}]\n${content}`
80
- : `[Voice message${duration}: ${attachment.url}]`;
81
- }
82
- else if (attachment?.kind === 'image' && attachment.url) {
83
- content = content
84
- ? `[Image: ${attachment.url}]\n${content}`
85
- : `[Image: ${attachment.url}]`;
86
- }
87
- else if (attachment?.kind === 'file' && attachment.url) {
88
- const label = attachment.fileName || 'File';
89
- content = content
90
- ? `[File: ${label} ${attachment.url}]\n${content}`
91
- : `[File: ${label} ${attachment.url}]`;
92
- }
93
- else if (message.contentType === 'audio' && message.audioUrl) {
94
- const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
95
- content = content
96
- ? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
97
- : `[Voice message${duration}: ${message.audioUrl}]`;
98
- }
99
- else if (message.contentType === 'image' && message.imageUrl) {
100
- content = content
101
- ? `[Image: ${message.imageUrl}]\n${content}`
102
- : `[Image: ${message.imageUrl}]`;
103
- }
104
- return content || '[Empty message]';
61
+ return renderCanonHostInboundContent(message);
105
62
  }
106
63
  function summarizeCommand(command) {
107
64
  const trimmed = command.trim();
@@ -173,31 +130,10 @@ async function main() {
173
130
  const sessions = new Map();
174
131
  const pendingSessionCreations = new Map();
175
132
  const conversationCache = new Map();
176
- let conversationCacheLoadedAt = 0;
177
- async function refreshConversationCache(force = false) {
178
- if (!force && conversationCache.size > 0 && Date.now() - conversationCacheLoadedAt < 10_000) {
179
- return;
180
- }
181
- const conversations = await client.getConversations();
182
- conversationCache.clear();
183
- for (const conversation of conversations) {
184
- conversationCache.set(conversation.id, conversation);
185
- }
186
- conversationCacheLoadedAt = Date.now();
187
- }
188
- async function getConversationMeta(conversationId) {
189
- try {
190
- await refreshConversationCache();
191
- const cached = conversationCache.get(conversationId);
192
- if (cached)
193
- return cached;
194
- await refreshConversationCache(true);
195
- return conversationCache.get(conversationId) ?? null;
196
- }
197
- catch {
198
- return conversationCache.get(conversationId) ?? null;
199
- }
200
- }
133
+ const { getConversationMeta } = createConversationMetadataLoader({
134
+ client,
135
+ conversationCache,
136
+ });
201
137
  async function loadSenderRuntimeState(conversationId, senderId) {
202
138
  try {
203
139
  const [turnState, sessionState] = await Promise.all([
@@ -210,25 +146,21 @@ async function main() {
210
146
  return null;
211
147
  }
212
148
  }
213
- async function loadParticipantContext(input) {
214
- const [conversation, recentMessages] = await Promise.all([
149
+ async function loadHydratedInboundContext(input) {
150
+ const [conversation, page] = await Promise.all([
215
151
  getConversationMeta(input.conversationId),
216
- client.getMessages(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => []),
152
+ input.hydratedPage
153
+ ? Promise.resolve(input.hydratedPage)
154
+ : client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
217
155
  ]);
218
- const history = buildParticipationHistorySnapshot(recentMessages, agentId);
219
- return {
220
- conversationType: conversation?.type ?? 'unknown',
221
- memberCount: conversation?.memberIds?.length ?? null,
222
- senderType: input.message.senderType ?? 'human',
156
+ return buildHydratedInboundContext({
157
+ agentId,
158
+ conversation,
159
+ page,
160
+ message: input.message,
223
161
  senderName: input.senderName,
224
162
  isOwner: input.isOwner,
225
- mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(agentId),
226
- recentSenderTypes: history.recentSenderTypes,
227
- recentHumanCount: history.recentHumanCount,
228
- recentAgentCount: history.recentAgentCount,
229
- consecutiveAgentTurns: history.consecutiveAgentTurns,
230
- currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
231
- };
163
+ });
232
164
  }
233
165
  function writeState(session) {
234
166
  writeSessionState(session.conversationId, agentId, {
@@ -385,14 +317,20 @@ async function main() {
385
317
  }
386
318
  async function enqueueInboundMessage(input) {
387
319
  const content = renderInboundContent(input.message);
388
- const conversation = await getConversationMeta(input.conversationId);
389
- const behavior = input.behavior ?? conversation?.behavior;
390
- const participantContext = await loadParticipantContext({
320
+ const hydrated = await loadHydratedInboundContext({
391
321
  conversationId: input.conversationId,
392
322
  message: input.message,
393
323
  senderName: input.senderName,
394
324
  isOwner: input.isOwner,
325
+ hydratedPage: input.hydratedPage,
395
326
  });
327
+ const behavior = input.behavior ?? hydrated.behavior;
328
+ const workSessions = hydrated.hydratedFromPage
329
+ ? hydrated.workSessions
330
+ : Array.isArray(input.workSessions)
331
+ ? input.workSessions
332
+ : hydrated.workSessions;
333
+ const participantContext = hydrated.participantContext;
396
334
  const autoReply = decideAutoReply(participantContext, behavior);
397
335
  if (!autoReply.allow) {
398
336
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
@@ -418,6 +356,8 @@ async function main() {
418
356
  conversationId: input.conversationId,
419
357
  participantContext,
420
358
  behavior,
359
+ workSession: input.message.workSession,
360
+ workSessions,
421
361
  });
422
362
  if (session.running && deliveryIntent === 'interrupt') {
423
363
  enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted);
@@ -589,6 +529,7 @@ async function main() {
589
529
  senderName: message.senderName || message.senderId,
590
530
  isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
591
531
  behavior: payload.behavior,
532
+ workSessions: payload.workSessions,
592
533
  });
593
534
  },
594
535
  onConnected: () => {
@@ -650,6 +591,8 @@ async function main() {
650
591
  senderName: latestMessage.senderId,
651
592
  isOwner: ownerId != null && latestMessage.senderId === ownerId,
652
593
  behavior: latestPage.behavior,
594
+ workSessions: latestPage.workSessions,
595
+ hydratedPage: latestPage,
653
596
  });
654
597
  }
655
598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/host.js",
@@ -22,7 +22,7 @@
22
22
  "prepack": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@canonmsg/core": "^0.5.0"
25
+ "@canonmsg/core": "^0.7.0"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=18.0.0"