@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 +6 -2
- package/dist/host.js +66 -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;
|
|
@@ -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
|
|
611
|
-
|
|
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
|
-
...(
|
|
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 =
|
|
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,
|
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.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.
|
|
33
|
-
"@canonmsg/core": "^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"
|