@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 +6 -2
- package/dist/host.js +84 -10
- package/dist/inbound-policy.d.ts +3 -1
- package/dist/inbound-policy.js +4 -1
- package/dist/register.js +3 -1
- package/package.json +3 -3
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
|
-
|
|
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`.
|
|
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
|
|
611
|
-
|
|
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
|
-
...(
|
|
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 =
|
|
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,
|
package/dist/inbound-policy.d.ts
CHANGED
|
@@ -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;
|
package/dist/inbound-policy.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
33
|
-
"@canonmsg/core": "^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"
|