@canonmsg/codex-plugin 0.10.0 → 0.11.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/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;
@@ -55,6 +58,23 @@ let workspaceOptions = [];
55
58
  let workspaceRoots = [];
56
59
  let workspaceRootMetadata = [];
57
60
  function buildCodexRuntimeDescriptor(input) {
61
+ const commands = [
62
+ {
63
+ id: 'runtime-status',
64
+ label: 'Runtime status',
65
+ description: 'Open Codex runtime details.',
66
+ primitive: 'runtime.status',
67
+ aliases: ['status'],
68
+ category: 'details',
69
+ placements: ['composer_slash', 'command_palette'],
70
+ availability: ['always'],
71
+ dispatch: { kind: 'open_details', target: 'status' },
72
+ },
73
+ {
74
+ ...RUNTIME_NEW_SESSION_ACTION,
75
+ primitive: 'session.new',
76
+ },
77
+ ];
58
78
  const descriptor = buildFirstPartyCodingRuntimeDescriptor({
59
79
  clientType: 'codex',
60
80
  models: input.models,
@@ -64,6 +84,7 @@ function buildCodexRuntimeDescriptor(input) {
64
84
  permissionModes: input.permissionModes,
65
85
  defaultPermissionMode: input.defaultPermissionMode,
66
86
  streamingTextMode: 'snapshot',
87
+ commands,
67
88
  actions: [
68
89
  RUNTIME_STOP_ACTION,
69
90
  RUNTIME_STOP_AND_DROP_ACTION,
@@ -244,10 +265,12 @@ export async function main() {
244
265
  initRTDBAuth(client);
245
266
  let agentId;
246
267
  let ownerId = null;
268
+ let ownerName = null;
247
269
  try {
248
270
  const ctx = await client.getAgentMe();
249
271
  agentId = ctx.agentId;
250
272
  ownerId = ctx.ownerId;
273
+ ownerName = ctx.ownerName;
251
274
  console.error(`[canon-codex] Connected as ${ctx.displayName || agentId}`);
252
275
  }
253
276
  catch {
@@ -297,6 +320,8 @@ export async function main() {
297
320
  const pendingSessionCreations = new Map();
298
321
  const conversationCache = new Map();
299
322
  const knownConversationIds = new Set();
323
+ const promptedGroupContextConversationIds = new Set();
324
+ const pendingMembershipChanges = new Map();
300
325
  let lastKnownConversationRefreshAt = 0;
301
326
  const { getConversationMeta } = createConversationMetadataLoader({
302
327
  client,
@@ -317,6 +342,45 @@ export async function main() {
317
342
  }
318
343
  lastKnownConversationRefreshAt = Date.now();
319
344
  }
345
+ function handleConversationUpdated(payload) {
346
+ const rawMemberIds = payload.changes.memberIds;
347
+ if (!Array.isArray(rawMemberIds))
348
+ return;
349
+ const memberIds = rawMemberIds.filter((id) => typeof id === 'string');
350
+ const cached = conversationCache.get(payload.conversationId);
351
+ const membershipChange = payload.membershipChange
352
+ ?? (cached ? diffCanonMemberIds(cached.memberIds, memberIds) : null);
353
+ if (cached) {
354
+ conversationCache.set(payload.conversationId, {
355
+ ...cached,
356
+ memberIds,
357
+ });
358
+ }
359
+ if (membershipChange) {
360
+ pendingMembershipChanges.set(payload.conversationId, membershipChange);
361
+ }
362
+ if (!memberIds.includes(agentId)) {
363
+ knownConversationIds.delete(payload.conversationId);
364
+ conversationCache.delete(payload.conversationId);
365
+ }
366
+ }
367
+ function getGroupContextMode(conversationId, conversation) {
368
+ if (conversation?.type !== 'group')
369
+ return undefined;
370
+ if (pendingMembershipChanges.has(conversationId))
371
+ return 'membership_change';
372
+ if (!promptedGroupContextConversationIds.has(conversationId))
373
+ return 'initial';
374
+ return undefined;
375
+ }
376
+ function markGroupContextModeUsed(conversationId, mode) {
377
+ if (!mode)
378
+ return;
379
+ promptedGroupContextConversationIds.add(conversationId);
380
+ if (mode === 'membership_change') {
381
+ pendingMembershipChanges.delete(conversationId);
382
+ }
383
+ }
320
384
  async function loadSenderRuntimeState(conversationId, senderId) {
321
385
  try {
322
386
  return normalizeRuntimeTurnState(await rtdbRead(`/turn-state/${conversationId}/${senderId}`));
@@ -336,9 +400,15 @@ export async function main() {
336
400
  agentId,
337
401
  conversation,
338
402
  page,
403
+ activeSelfContextId: input.activeSelfContextId,
404
+ selfContexts: input.selfContexts,
339
405
  message: input.message,
340
406
  senderName: input.senderName,
341
407
  isOwner: input.isOwner,
408
+ ownerId,
409
+ ownerName,
410
+ membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
411
+ groupContextMode: getGroupContextMode(input.conversationId, conversation),
342
412
  });
343
413
  }
344
414
  function writeState(session) {
@@ -604,14 +674,13 @@ export async function main() {
604
674
  message: input.message,
605
675
  senderName: input.senderName,
606
676
  isOwner: input.isOwner,
677
+ activeSelfContextId: input.activeSelfContextId,
678
+ selfContexts: input.selfContexts,
607
679
  hydratedPage: input.hydratedPage,
608
680
  });
609
681
  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;
682
+ const activeSelfContextId = hydrated.activeSelfContextId;
683
+ const selfContexts = hydrated.selfContexts;
615
684
  const participantContext = hydrated.participantContext;
616
685
  const autoReply = decideAutoReply(participantContext, behavior);
617
686
  if (!autoReply.allow) {
@@ -619,6 +688,7 @@ export async function main() {
619
688
  return;
620
689
  }
621
690
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
691
+ markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
622
692
  let session;
623
693
  try {
624
694
  session = await getOrCreateSession(input.conversationId);
@@ -628,7 +698,7 @@ export async function main() {
628
698
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
629
699
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
630
700
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
631
- ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
701
+ ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
632
702
  metadata: {
633
703
  turnSemantics: 'turn_complete',
634
704
  turnComplete: true,
@@ -638,7 +708,7 @@ export async function main() {
638
708
  return;
639
709
  }
640
710
  const turnMetadata = normalizeTurnMetadata(input.message.metadata);
641
- session.activeSelfContextId = selfContexts[0]?.id ?? null;
711
+ session.activeSelfContextId = activeSelfContextId;
642
712
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
643
713
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
644
714
  const prompt = buildCanonPrompt({
@@ -1000,6 +1070,7 @@ export async function main() {
1000
1070
  senderName: message.senderName || message.senderId,
1001
1071
  isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
1002
1072
  behavior: payload.behavior,
1073
+ activeSelfContextId: payload.activeSelfContextId,
1003
1074
  selfContexts: payload.selfContexts,
1004
1075
  });
1005
1076
  if (message.id) {
@@ -1010,6 +1081,9 @@ export async function main() {
1010
1081
  });
1011
1082
  }
1012
1083
  },
1084
+ onConversationUpdated: (payload) => {
1085
+ handleConversationUpdated(payload);
1086
+ },
1013
1087
  onConnected: () => {
1014
1088
  streamConnected = true;
1015
1089
  void publishRuntimeHeartbeat();
@@ -1104,7 +1178,7 @@ export async function main() {
1104
1178
  await enqueueInboundMessage({
1105
1179
  conversationId: conversation.id,
1106
1180
  message: latestMessage,
1107
- senderName: latestMessage.senderId,
1181
+ senderName: latestMessage.senderName || latestMessage.senderId,
1108
1182
  isOwner: ownerId != null && latestMessage.senderId === ownerId,
1109
1183
  behavior: latestPage.behavior,
1110
1184
  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.1",
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.1",
33
+ "@canonmsg/core": "^0.17.2"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"