@canonmsg/codex-plugin 0.6.4 โ†’ 0.6.6

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.
@@ -37,6 +37,12 @@ interface HostWorkspaceResolverOption {
37
37
  id: string;
38
38
  cwd: string;
39
39
  }
40
+ export declare const HOST_ADMISSION_ACTION_CAPABILITIES: Readonly<{
41
+ canStartDirectConversation: false;
42
+ canSendContactRequest: false;
43
+ canApprovePendingContactRequests: false;
44
+ canRejectPendingContactRequests: false;
45
+ }>;
40
46
  export declare function buildCanonHostPrompt(input: {
41
47
  hostLabel: string;
42
48
  content: string;
@@ -81,6 +87,21 @@ export declare function buildHydratedInboundContext(input: {
81
87
  hydratedFromPage: boolean;
82
88
  };
83
89
  export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
90
+ export declare function publishHostSessionSnapshots(input: {
91
+ conversationIds: string[];
92
+ agentId: string;
93
+ clientType: AgentClientType;
94
+ runtime: AgentRuntime;
95
+ workspaceOptions: HostWorkspaceResolverOption[];
96
+ defaultCwd: string;
97
+ liveSessionConfigByConversation?: ReadonlyMap<string, {
98
+ model?: string;
99
+ permissionMode?: string;
100
+ workspaceId?: string;
101
+ executionMode?: SessionWorkspaceConfig['executionMode'];
102
+ executionBranch?: string | null;
103
+ }>;
104
+ }): Promise<void>;
84
105
  export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null;
85
106
  export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
86
107
  conversationId: string;
@@ -11,7 +11,13 @@
11
11
  * behavior here, update that copy too and adjust the shared golden
12
12
  * fixture test (`packages/codex-plugin/src/host-runtime.test.ts`).
13
13
  */
14
- import { buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
14
+ import { buildAgentSessionSnapshot, buildConversationWorktreeSpec, buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, patchAgentSessionSnapshot, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
15
+ export const HOST_ADMISSION_ACTION_CAPABILITIES = Object.freeze({
16
+ canStartDirectConversation: false,
17
+ canSendContactRequest: false,
18
+ canApprovePendingContactRequests: false,
19
+ canRejectPendingContactRequests: false,
20
+ });
15
21
  export function buildCanonHostPrompt(input) {
16
22
  const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
17
23
  return [
@@ -69,7 +75,21 @@ function describeContactCard(card) {
69
75
  if (card.about)
70
76
  parts.push(`about: ${card.about}`);
71
77
  const identity = `๐Ÿ“‡ Contact card: "${card.displayName}" (${parts.join(' ยท ')}).`;
72
- const hint = `This card is informational only in host mode. Canon does not currently expose a host-side tool here for starting a new direct conversation or sending a contact request to userId ${card.userId}.`;
78
+ const missingCapabilities = [
79
+ !HOST_ADMISSION_ACTION_CAPABILITIES.canStartDirectConversation
80
+ ? 'start a direct conversation'
81
+ : null,
82
+ !HOST_ADMISSION_ACTION_CAPABILITIES.canSendContactRequest
83
+ ? 'send a contact request'
84
+ : null,
85
+ !HOST_ADMISSION_ACTION_CAPABILITIES.canApprovePendingContactRequests
86
+ ? 'approve pending requests'
87
+ : null,
88
+ !HOST_ADMISSION_ACTION_CAPABILITIES.canRejectPendingContactRequests
89
+ ? 'reject pending requests'
90
+ : null,
91
+ ].filter(Boolean).join(', ');
92
+ const hint = `This host can inspect the card, but Canon admission actions are missing here. Missing capabilities: ${missingCapabilities}. Use another Canon surface for userId ${card.userId}.`;
73
93
  return `${identity}\n${hint}`;
74
94
  }
75
95
  function describeAttachment(attachment, materialized) {
@@ -116,6 +136,64 @@ export async function publishHostAgentRuntime(agentId, clientType, runtime) {
116
136
  updatedAt: { '.sv': 'timestamp' },
117
137
  });
118
138
  }
139
+ export async function publishHostSessionSnapshots(input) {
140
+ if (input.conversationIds.length === 0) {
141
+ return;
142
+ }
143
+ await Promise.all(input.conversationIds.map(async (conversationId) => {
144
+ const persistedConfig = await loadHostSessionConfig({
145
+ conversationId,
146
+ agentId: input.agentId,
147
+ extraStringFields: ['permissionMode'],
148
+ });
149
+ const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
150
+ const mergedConfig = {
151
+ ...(persistedConfig ?? {}),
152
+ ...(liveConfig ?? {}),
153
+ };
154
+ const snapshot = buildAgentSessionSnapshot({
155
+ conversationId,
156
+ agentId: input.agentId,
157
+ runtime: {
158
+ ...input.runtime,
159
+ clientType: input.clientType,
160
+ hostMode: true,
161
+ },
162
+ sessionConfig: {
163
+ ...(mergedConfig.model ? { model: mergedConfig.model } : {}),
164
+ ...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
165
+ ...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
166
+ ...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
167
+ },
168
+ lastHeartbeatAt: undefined,
169
+ });
170
+ let executionBranch = liveConfig?.executionBranch ?? null;
171
+ if (!executionBranch && snapshot.executionMode === 'worktree' && snapshot.workspaceId) {
172
+ const workspace = input.workspaceOptions.find((option) => option.id === snapshot.workspaceId);
173
+ if (workspace) {
174
+ executionBranch = buildConversationWorktreeSpec({
175
+ agentId: input.agentId,
176
+ conversationId,
177
+ workspaceCwd: workspace.cwd,
178
+ }).branch;
179
+ }
180
+ }
181
+ return patchAgentSessionSnapshot(conversationId, input.agentId, {
182
+ clientType: input.clientType,
183
+ hostMode: true,
184
+ model: snapshot.model ?? null,
185
+ permissionMode: snapshot.permissionMode ?? null,
186
+ workspaceId: snapshot.workspaceId ?? null,
187
+ executionMode: snapshot.executionMode ?? null,
188
+ executionBranch,
189
+ modelOptions: snapshot.modelOptions,
190
+ permissionModeOptions: snapshot.permissionModeOptions,
191
+ workspaceOptions: snapshot.workspaceOptions,
192
+ availableExecutionModes: snapshot.availableExecutionModes,
193
+ lastHeartbeatAt: { '.sv': 'timestamp' },
194
+ });
195
+ }));
196
+ }
119
197
  export function readHostSessionConfig(raw, extraStringFields = []) {
120
198
  const baseConfig = readSessionWorkspaceConfig(raw);
121
199
  if (!raw || typeof raw !== 'object') {
package/dist/host.js CHANGED
@@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
6
6
  import { buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, 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
+ import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
8
8
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
9
9
  import { CodexConversationAdapter, } from './adapter.js';
10
10
  import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
@@ -138,10 +138,27 @@ export async function main() {
138
138
  const sessions = new Map();
139
139
  const pendingSessionCreations = new Map();
140
140
  const conversationCache = new Map();
141
+ const knownConversationIds = new Set();
142
+ let lastKnownConversationRefreshAt = 0;
141
143
  const { getConversationMeta } = createConversationMetadataLoader({
142
144
  client,
143
145
  conversationCache,
144
146
  });
147
+ function resolveWorkspaceIdForBaseCwd(baseCwd) {
148
+ return workspaceOptions.find((option) => option.cwd === baseCwd)?.id;
149
+ }
150
+ async function refreshKnownConversationIds(force = false) {
151
+ if (!force && Date.now() - lastKnownConversationRefreshAt < HEARTBEAT_MS) {
152
+ return;
153
+ }
154
+ const conversations = await client.getConversations();
155
+ knownConversationIds.clear();
156
+ for (const conversation of conversations) {
157
+ knownConversationIds.add(conversation.id);
158
+ conversationCache.set(conversation.id, conversation);
159
+ }
160
+ lastKnownConversationRefreshAt = Date.now();
161
+ }
145
162
  async function loadSenderRuntimeState(conversationId, senderId) {
146
163
  try {
147
164
  const [turnState, sessionState] = await Promise.all([
@@ -236,6 +253,7 @@ export async function main() {
236
253
  }
237
254
  }
238
255
  async function getOrCreateSession(conversationId) {
256
+ knownConversationIds.add(conversationId);
239
257
  const existing = sessions.get(conversationId);
240
258
  if (existing && !existing.closed) {
241
259
  existing.lastActivity = Date.now();
@@ -335,6 +353,7 @@ export async function main() {
335
353
  void runNextTurn(session);
336
354
  }
337
355
  async function enqueueInboundMessage(input) {
356
+ knownConversationIds.add(input.conversationId);
338
357
  let materialized = [];
339
358
  if (input.message.id) {
340
359
  try {
@@ -555,9 +574,34 @@ export async function main() {
555
574
  const publishRuntimeHeartbeat = async () => {
556
575
  if (!streamConnected)
557
576
  return;
577
+ await refreshKnownConversationIds().catch((error) => {
578
+ console.error('[canon-codex] Failed to refresh known conversations:', error);
579
+ });
558
580
  await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
559
581
  console.error('[canon-codex] Failed to publish agent runtime:', error);
560
582
  });
583
+ await publishHostSessionSnapshots({
584
+ conversationIds: Array.from(knownConversationIds),
585
+ agentId,
586
+ clientType: 'codex',
587
+ runtime: runtimeDescriptor,
588
+ workspaceOptions,
589
+ defaultCwd: workingDir,
590
+ liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
591
+ const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
592
+ return [
593
+ session.conversationId,
594
+ {
595
+ ...(session.state.model ? { model: session.state.model } : {}),
596
+ ...(workspaceId ? { workspaceId } : {}),
597
+ executionMode: session.environment.mode,
598
+ executionBranch: session.environment.branch ?? null,
599
+ },
600
+ ];
601
+ })),
602
+ }).catch((error) => {
603
+ console.error('[canon-codex] Failed to publish session snapshots:', error);
604
+ });
561
605
  };
562
606
  const stream = new CanonStream({
563
607
  apiKey,
@@ -614,7 +658,10 @@ export async function main() {
614
658
  }
615
659
  try {
616
660
  const conversations = await client.getConversations();
661
+ lastKnownConversationRefreshAt = Date.now();
617
662
  for (const conversation of conversations) {
663
+ knownConversationIds.add(conversation.id);
664
+ conversationCache.set(conversation.id, conversation);
618
665
  clearStreaming(conversation.id);
619
666
  clearSessionState(conversation.id, agentId).catch(() => { });
620
667
  clearTurnState(conversation.id, agentId).catch(() => { });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,15 +21,16 @@
21
21
  "scripts"
22
22
  ],
23
23
  "scripts": {
24
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
25
- "dev": "tsc --watch",
24
+ "prepare:workspace-deps": "npm --prefix ../core run build && npm --prefix ../agent-sdk run build",
25
+ "build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
26
+ "dev": "npm run prepare:workspace-deps && tsc --watch",
26
27
  "smoke": "node scripts/smoke-test.mjs",
27
28
  "test": "vitest run",
28
29
  "prepack": "npm run build"
29
30
  },
30
31
  "dependencies": {
31
- "@canonmsg/agent-sdk": "^0.8.2",
32
- "@canonmsg/core": "^0.7.3"
32
+ "@canonmsg/agent-sdk": "^0.8.3",
33
+ "@canonmsg/core": "^0.7.5"
33
34
  },
34
35
  "engines": {
35
36
  "node": ">=18.0.0"