@canonmsg/codex-plugin 0.10.0 → 0.11.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/README.md CHANGED
@@ -22,7 +22,9 @@ canon-codex --cwd /path/to/project
22
22
 
23
23
  Registration saves a Canon profile in `~/.canon/agents.json`, the same shared profile store used by the Claude Code integration and supported by the OpenClaw plugin.
24
24
 
25
- If the terminal closes or the machine restarts, the agent goes offline until you start the host again. Install `@canonmsg/local-agents` and run `canon-necromance` to list every recorded local agent from newest to oldest, then revive one in the foreground:
25
+ `canon-codex` is the local agent process. Keep that terminal open while you want Canon to reach the agent. Closing it, logging out, rebooting, or sleeping long enough to stop the process takes the local agent offline until you revive it.
26
+
27
+ Install `@canonmsg/local-agents` and run `canon-necromance` to list every recorded local agent from newest to oldest, then revive one in the foreground:
26
28
 
27
29
  ```bash
28
30
  npm install -g @canonmsg/local-agents
@@ -30,7 +32,9 @@ canon-necromance
30
32
  canon-necromance revive my-codex
31
33
  ```
32
34
 
33
- Do not run registration again unless Canon tells you the saved API key is invalid. If you registered multiple profiles, relaunch the same one with `CANON_AGENT=<profile> canon-codex --cwd /path/to/project`. Keep the revived terminal open; closing it takes this local agent offline.
35
+ Do not run registration again unless Canon tells you the saved API key is invalid. If you registered multiple profiles, relaunch the same one with `CANON_AGENT=<profile> canon-codex --cwd /path/to/project`.
36
+
37
+ Public docs: <https://canonmail.com/agents/integrations>. Coding-host concepts: <https://canonmail.com/agents/coding-agents>.
34
38
 
35
39
  You do not need a git repo for host mode. The plugin passes `--skip-git-repo-check` to Codex, so any readable working directory is valid.
36
40
 
package/dist/host.js CHANGED
@@ -4,7 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { dirname } from 'node:path';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
7
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
7
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
8
8
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
9
9
  import { CodexConversationAdapter, } from './adapter.js';
10
10
  import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
@@ -38,7 +38,10 @@ EXAMPLES
38
38
  canon-codex --cwd ~/dev/canon
39
39
  canon-codex --cwd ~/dev/canon --workspace-root ~/dev --full-auto
40
40
 
41
- Keep this terminal open while you want Canon to reach the agent.`;
41
+ Keep this terminal open while you want Canon to reach the agent. Closing it,
42
+ logging out, rebooting, or sleeping long enough to stop the process takes the
43
+ local agent offline until you revive it. Docs:
44
+ https://canonmail.com/agents/integrations`;
42
45
  const MAX_SESSIONS = 12;
43
46
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
44
47
  const HEARTBEAT_MS = 30_000;
@@ -244,10 +247,12 @@ export async function main() {
244
247
  initRTDBAuth(client);
245
248
  let agentId;
246
249
  let ownerId = null;
250
+ let ownerName = null;
247
251
  try {
248
252
  const ctx = await client.getAgentMe();
249
253
  agentId = ctx.agentId;
250
254
  ownerId = ctx.ownerId;
255
+ ownerName = ctx.ownerName;
251
256
  console.error(`[canon-codex] Connected as ${ctx.displayName || agentId}`);
252
257
  }
253
258
  catch {
@@ -297,6 +302,8 @@ export async function main() {
297
302
  const pendingSessionCreations = new Map();
298
303
  const conversationCache = new Map();
299
304
  const knownConversationIds = new Set();
305
+ const promptedGroupContextConversationIds = new Set();
306
+ const pendingMembershipChanges = new Map();
300
307
  let lastKnownConversationRefreshAt = 0;
301
308
  const { getConversationMeta } = createConversationMetadataLoader({
302
309
  client,
@@ -317,6 +324,45 @@ export async function main() {
317
324
  }
318
325
  lastKnownConversationRefreshAt = Date.now();
319
326
  }
327
+ function handleConversationUpdated(payload) {
328
+ const rawMemberIds = payload.changes.memberIds;
329
+ if (!Array.isArray(rawMemberIds))
330
+ return;
331
+ const memberIds = rawMemberIds.filter((id) => typeof id === 'string');
332
+ const cached = conversationCache.get(payload.conversationId);
333
+ const membershipChange = payload.membershipChange
334
+ ?? (cached ? diffCanonMemberIds(cached.memberIds, memberIds) : null);
335
+ if (cached) {
336
+ conversationCache.set(payload.conversationId, {
337
+ ...cached,
338
+ memberIds,
339
+ });
340
+ }
341
+ if (membershipChange) {
342
+ pendingMembershipChanges.set(payload.conversationId, membershipChange);
343
+ }
344
+ if (!memberIds.includes(agentId)) {
345
+ knownConversationIds.delete(payload.conversationId);
346
+ conversationCache.delete(payload.conversationId);
347
+ }
348
+ }
349
+ function getGroupContextMode(conversationId, conversation) {
350
+ if (conversation?.type !== 'group')
351
+ return undefined;
352
+ if (pendingMembershipChanges.has(conversationId))
353
+ return 'membership_change';
354
+ if (!promptedGroupContextConversationIds.has(conversationId))
355
+ return 'initial';
356
+ return undefined;
357
+ }
358
+ function markGroupContextModeUsed(conversationId, mode) {
359
+ if (!mode)
360
+ return;
361
+ promptedGroupContextConversationIds.add(conversationId);
362
+ if (mode === 'membership_change') {
363
+ pendingMembershipChanges.delete(conversationId);
364
+ }
365
+ }
320
366
  async function loadSenderRuntimeState(conversationId, senderId) {
321
367
  try {
322
368
  return normalizeRuntimeTurnState(await rtdbRead(`/turn-state/${conversationId}/${senderId}`));
@@ -336,9 +382,15 @@ export async function main() {
336
382
  agentId,
337
383
  conversation,
338
384
  page,
385
+ activeSelfContextId: input.activeSelfContextId,
386
+ selfContexts: input.selfContexts,
339
387
  message: input.message,
340
388
  senderName: input.senderName,
341
389
  isOwner: input.isOwner,
390
+ ownerId,
391
+ ownerName,
392
+ membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
393
+ groupContextMode: getGroupContextMode(input.conversationId, conversation),
342
394
  });
343
395
  }
344
396
  function writeState(session) {
@@ -604,14 +656,13 @@ export async function main() {
604
656
  message: input.message,
605
657
  senderName: input.senderName,
606
658
  isOwner: input.isOwner,
659
+ activeSelfContextId: input.activeSelfContextId,
660
+ selfContexts: input.selfContexts,
607
661
  hydratedPage: input.hydratedPage,
608
662
  });
609
663
  const behavior = input.behavior ?? hydrated.behavior;
610
- const selfContexts = hydrated.hydratedFromPage
611
- ? hydrated.selfContexts
612
- : Array.isArray(input.selfContexts)
613
- ? input.selfContexts
614
- : hydrated.selfContexts;
664
+ const activeSelfContextId = hydrated.activeSelfContextId;
665
+ const selfContexts = hydrated.selfContexts;
615
666
  const participantContext = hydrated.participantContext;
616
667
  const autoReply = decideAutoReply(participantContext, behavior);
617
668
  if (!autoReply.allow) {
@@ -619,6 +670,7 @@ export async function main() {
619
670
  return;
620
671
  }
621
672
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
673
+ markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
622
674
  let session;
623
675
  try {
624
676
  session = await getOrCreateSession(input.conversationId);
@@ -628,7 +680,7 @@ export async function main() {
628
680
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
629
681
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
630
682
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
631
- ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
683
+ ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
632
684
  metadata: {
633
685
  turnSemantics: 'turn_complete',
634
686
  turnComplete: true,
@@ -638,7 +690,7 @@ export async function main() {
638
690
  return;
639
691
  }
640
692
  const turnMetadata = normalizeTurnMetadata(input.message.metadata);
641
- session.activeSelfContextId = selfContexts[0]?.id ?? null;
693
+ session.activeSelfContextId = activeSelfContextId;
642
694
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
643
695
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
644
696
  const prompt = buildCanonPrompt({
@@ -1000,6 +1052,7 @@ export async function main() {
1000
1052
  senderName: message.senderName || message.senderId,
1001
1053
  isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
1002
1054
  behavior: payload.behavior,
1055
+ activeSelfContextId: payload.activeSelfContextId,
1003
1056
  selfContexts: payload.selfContexts,
1004
1057
  });
1005
1058
  if (message.id) {
@@ -1010,6 +1063,9 @@ export async function main() {
1010
1063
  });
1011
1064
  }
1012
1065
  },
1066
+ onConversationUpdated: (payload) => {
1067
+ handleConversationUpdated(payload);
1068
+ },
1013
1069
  onConnected: () => {
1014
1070
  streamConnected = true;
1015
1071
  void publishRuntimeHeartbeat();
@@ -1104,7 +1160,7 @@ export async function main() {
1104
1160
  await enqueueInboundMessage({
1105
1161
  conversationId: conversation.id,
1106
1162
  message: latestMessage,
1107
- senderName: latestMessage.senderId,
1163
+ senderName: latestMessage.senderName || latestMessage.senderId,
1108
1164
  isOwner: ownerId != null && latestMessage.senderId === ownerId,
1109
1165
  behavior: latestPage.behavior,
1110
1166
  selfContexts: latestPage.selfContexts,
@@ -1,4 +1,4 @@
1
- import { type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
1
+ import { type CanonGroupContext, type CanonGroupContextMode, type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
2
2
  export interface InboundParticipantContext {
3
3
  conversationType: CanonConversation['type'] | 'unknown';
4
4
  memberCount: number | null;
@@ -6,6 +6,8 @@ export interface InboundParticipantContext {
6
6
  senderName: string;
7
7
  isOwner: boolean;
8
8
  mentionedAgent: boolean;
9
+ groupContext?: CanonGroupContext;
10
+ groupContextMode?: CanonGroupContextMode;
9
11
  recentSenderTypes: Array<'human' | 'ai_agent'>;
10
12
  recentHumanCount: number;
11
13
  recentAgentCount: number;
@@ -1,4 +1,4 @@
1
- import { evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
1
+ import { buildCompactGroupContextLines, evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
2
2
  function formatRecentSenders(senderTypes) {
3
3
  if (senderTypes.length === 0)
4
4
  return 'none';
@@ -18,6 +18,9 @@ export function buildInboundContextLines(context) {
18
18
  `Latest sender name: ${context.senderName}`,
19
19
  `Latest sender type: ${context.senderType}`,
20
20
  `Conversation type: ${conversationTypeLabel}`,
21
+ ...(context.groupContext && context.groupContextMode
22
+ ? buildCompactGroupContextLines(context.groupContext, context.groupContextMode)
23
+ : []),
21
24
  `Directly addressed to this agent: ${context.mentionedAgent ? 'yes' : 'no'}`,
22
25
  `Recent sender pattern: ${formatRecentSenders(context.recentSenderTypes)}`,
23
26
  `Recent human messages: ${context.recentHumanCount}`,
package/dist/register.js CHANGED
@@ -97,7 +97,9 @@ export async function main() {
97
97
  console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
98
98
  console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
99
99
  console.log('Start it with: CANON_AGENT=' + profileName + ' canon-codex --cwd /path/to/project');
100
- console.log('Keep that terminal open. Closing it takes this local agent offline.');
100
+ console.log('Keep that terminal open while you want Canon to reach the agent.');
101
+ console.log('Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.');
102
+ console.log('Docs: https://canonmail.com/agents/integrations');
101
103
  break;
102
104
  }
103
105
  case 'rejected':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^1.2.0",
33
- "@canonmsg/core": "^0.16.0"
32
+ "@canonmsg/agent-sdk": "^1.3.0",
33
+ "@canonmsg/core": "^0.17.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"