@canonmsg/codex-plugin 0.2.0 → 0.3.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 +21 -1
- package/dist/host.js +114 -116
- package/dist/inbound-policy.d.ts +3 -2
- package/dist/inbound-policy.js +16 -23
- package/dist/setup.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,12 +16,14 @@ codex login status
|
|
|
16
16
|
# Register (approve in Canon when prompted)
|
|
17
17
|
canon-codex-register --name "My Codex" --description "My local coding agent" --phone "+15551234567"
|
|
18
18
|
|
|
19
|
-
# Run inside a project
|
|
19
|
+
# Run inside a project and keep the host process running
|
|
20
20
|
canon-codex --cwd /path/to/project
|
|
21
21
|
```
|
|
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
|
+
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.
|
|
26
|
+
|
|
25
27
|
## What v1 supports
|
|
26
28
|
|
|
27
29
|
- Canon messages routed into Codex turns
|
|
@@ -57,6 +59,24 @@ npm run smoke -- /path/to/project
|
|
|
57
59
|
|
|
58
60
|
## Troubleshooting
|
|
59
61
|
|
|
62
|
+
If Canon messages are not getting replies, first confirm the local host process is still running:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
ps aux | rg canon-codex
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If you installed the package only inside this repo and not globally, run the built host directly:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
node packages/codex-plugin/dist/host.js --cwd /path/to/project --full-auto
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If Canon rejects authenticated requests with `401 Invalid API key`, the stored Canon profile needs a fresh key. Rerun registration for the same profile to overwrite `~/.canon/agents.json`, then restart the host:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
canon-codex-register --name "My Codex" --description "My local coding agent" --phone "+15551234567" --profile my-codex
|
|
78
|
+
```
|
|
79
|
+
|
|
60
80
|
If Codex reports API-key quota errors while another local tool on the same machine uses OpenAI API keys, check Codex's own stored login state:
|
|
61
81
|
|
|
62
82
|
```bash
|
package/dist/host.js
CHANGED
|
@@ -3,8 +3,7 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
3
3
|
setDefaultResultOrder('ipv4first');
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import {
|
|
7
|
-
import { CanonClient, CanonStream, clearSessionState, clearTurnState, DEFAULT_RUNTIME_CAPABILITIES, getActiveProfile, initRTDBAuth, normalizeTurnMetadata, normalizeTurnState, releaseLock, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, writeSessionState, writeTurnState, } from '@canonmsg/core';
|
|
6
|
+
import { buildParticipationHistorySnapshot, buildBehaviorPolicyLines, buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, ExecutionEnvironmentError, isEnabledFlag, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, 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';
|
|
8
7
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
9
8
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
10
9
|
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
@@ -17,16 +16,10 @@ const CODEX_RUNTIME_CAPABILITIES = {
|
|
|
17
16
|
...DEFAULT_RUNTIME_CAPABILITIES,
|
|
18
17
|
supportsInterrupt: true,
|
|
19
18
|
supportsQueue: true,
|
|
20
|
-
supportsNonFinalPermanentMessages:
|
|
19
|
+
supportsNonFinalPermanentMessages: false,
|
|
21
20
|
};
|
|
22
21
|
let workingDir = process.cwd();
|
|
23
22
|
let workspaceOptions = [];
|
|
24
|
-
function normalizeString(value) {
|
|
25
|
-
if (typeof value !== 'string')
|
|
26
|
-
return undefined;
|
|
27
|
-
const trimmed = value.trim();
|
|
28
|
-
return trimmed ? trimmed : undefined;
|
|
29
|
-
}
|
|
30
23
|
function normalizeRuntimeTurnState(value) {
|
|
31
24
|
const normalizedTurn = normalizeTurnState(value);
|
|
32
25
|
if (normalizedTurn) {
|
|
@@ -43,20 +36,6 @@ function normalizeRuntimeTurnState(value) {
|
|
|
43
36
|
}
|
|
44
37
|
return null;
|
|
45
38
|
}
|
|
46
|
-
function buildWorkspaceOptions(primaryCwd, configured) {
|
|
47
|
-
const uniqueDirs = Array.from(new Set([primaryCwd, ...configured].map((dir) => resolve(dir))));
|
|
48
|
-
const seenLabels = new Map();
|
|
49
|
-
return uniqueDirs.map((cwd, index) => {
|
|
50
|
-
const baseLabel = basename(cwd) || cwd;
|
|
51
|
-
const seenCount = (seenLabels.get(baseLabel) ?? 0) + 1;
|
|
52
|
-
seenLabels.set(baseLabel, seenCount);
|
|
53
|
-
return {
|
|
54
|
-
id: index === 0 ? 'default' : `workspace-${index + 1}`,
|
|
55
|
-
label: seenCount === 1 ? baseLabel : `${baseLabel} (${seenCount})`,
|
|
56
|
-
cwd,
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
39
|
async function publishAgentRuntime(agentId, runtime) {
|
|
61
40
|
await rtdbWrite(`/agent-runtime/${agentId}`, {
|
|
62
41
|
clientType: 'codex',
|
|
@@ -67,40 +46,24 @@ async function publishAgentRuntime(agentId, runtime) {
|
|
|
67
46
|
}
|
|
68
47
|
async function loadSessionConfig(conversationId, agentId) {
|
|
69
48
|
const raw = await rtdbRead(`/session-config/${conversationId}/${agentId}`);
|
|
70
|
-
|
|
71
|
-
return null;
|
|
72
|
-
const data = raw;
|
|
73
|
-
return {
|
|
74
|
-
workspaceId: normalizeString(data.workspaceId),
|
|
75
|
-
legacyCwd: normalizeString(data.cwd),
|
|
76
|
-
model: normalizeString(data.model),
|
|
77
|
-
};
|
|
49
|
+
return readSessionWorkspaceConfig(raw);
|
|
78
50
|
}
|
|
79
51
|
function resolveWorkspaceCwd(config) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (config?.legacyCwd) {
|
|
86
|
-
const workspace = workspaceOptions.find((option) => option.cwd === config.legacyCwd);
|
|
87
|
-
if (workspace)
|
|
88
|
-
return workspace.cwd;
|
|
89
|
-
}
|
|
90
|
-
return workspaceOptions[0]?.cwd ?? workingDir;
|
|
91
|
-
}
|
|
92
|
-
function toPublicWorkspaceOptions() {
|
|
93
|
-
return workspaceOptions.map(({ id, label }) => ({ id, label }));
|
|
52
|
+
return resolveConfiguredWorkspaceCwd({
|
|
53
|
+
workspaceOptions,
|
|
54
|
+
config,
|
|
55
|
+
defaultCwd: workingDir,
|
|
56
|
+
});
|
|
94
57
|
}
|
|
95
58
|
function buildCanonPrompt(input) {
|
|
96
59
|
return [
|
|
97
60
|
'You are connected to Canon messaging through a Codex host wrapper.',
|
|
98
|
-
'Reply naturally to Canon participants.',
|
|
99
61
|
'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
|
|
100
62
|
'Short intermediate assistant messages may be shown as ephemeral status while you work.',
|
|
101
63
|
...buildInboundContextLines(input.participantContext),
|
|
64
|
+
...buildBehaviorPolicyLines(input.behavior),
|
|
102
65
|
'Canon participants may be humans or AI agents.',
|
|
103
|
-
'
|
|
66
|
+
'Honor the Canon behavior policy above when deciding how proactively to participate.',
|
|
104
67
|
`Conversation ID: ${input.conversationId}`,
|
|
105
68
|
'',
|
|
106
69
|
'New Canon message:',
|
|
@@ -155,6 +118,9 @@ function formatTurnFailure(errorText) {
|
|
|
155
118
|
const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
|
|
156
119
|
return `Codex failed before sending a final reply: ${shortened}`;
|
|
157
120
|
}
|
|
121
|
+
function sleep(ms) {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
158
124
|
async function main() {
|
|
159
125
|
const { values: args } = parseArgs({
|
|
160
126
|
options: {
|
|
@@ -168,12 +134,17 @@ async function main() {
|
|
|
168
134
|
config: { type: 'string', multiple: true },
|
|
169
135
|
'codex-bin': { type: 'string' },
|
|
170
136
|
'full-auto': { type: 'boolean' },
|
|
137
|
+
'enable-worktrees': { type: 'boolean' },
|
|
171
138
|
'dangerously-bypass-approvals-and-sandbox': { type: 'boolean' },
|
|
172
139
|
},
|
|
173
140
|
strict: true,
|
|
174
141
|
});
|
|
175
142
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
176
|
-
workspaceOptions =
|
|
143
|
+
workspaceOptions = buildConfiguredWorkspaceOptions(workingDir, args.workspace ?? []);
|
|
144
|
+
const allowWorktrees = isEnabledFlag(args['enable-worktrees'] ?? process.env.CANON_ENABLE_WORKTREES);
|
|
145
|
+
if (!allowWorktrees) {
|
|
146
|
+
console.error('[canon-codex] Worktree isolation is disabled; sessions will lock their selected workspace unless explicitly enabled.');
|
|
147
|
+
}
|
|
177
148
|
if (typeof args['ask-for-approval'] === 'string') {
|
|
178
149
|
console.error('[canon-codex] Note: newer Codex CLI releases do not accept --ask-for-approval for `codex exec`; Canon will translate compatible legacy usage when possible.');
|
|
179
150
|
}
|
|
@@ -242,17 +213,9 @@ async function main() {
|
|
|
242
213
|
async function loadParticipantContext(input) {
|
|
243
214
|
const [conversation, recentMessages] = await Promise.all([
|
|
244
215
|
getConversationMeta(input.conversationId),
|
|
245
|
-
client.getMessages(input.conversationId,
|
|
216
|
+
client.getMessages(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => []),
|
|
246
217
|
]);
|
|
247
|
-
const
|
|
248
|
-
.filter((message) => message.senderId !== agentId)
|
|
249
|
-
.map((message) => message.senderType);
|
|
250
|
-
let consecutiveAgentTurns = 0;
|
|
251
|
-
for (const senderType of recentSenderTypes) {
|
|
252
|
-
if (senderType !== 'ai_agent')
|
|
253
|
-
break;
|
|
254
|
-
consecutiveAgentTurns += 1;
|
|
255
|
-
}
|
|
218
|
+
const history = buildParticipationHistorySnapshot(recentMessages, agentId);
|
|
256
219
|
return {
|
|
257
220
|
conversationType: conversation?.type ?? 'unknown',
|
|
258
221
|
memberCount: conversation?.memberIds?.length ?? null,
|
|
@@ -260,10 +223,11 @@ async function main() {
|
|
|
260
223
|
senderName: input.senderName,
|
|
261
224
|
isOwner: input.isOwner,
|
|
262
225
|
mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(agentId),
|
|
263
|
-
recentSenderTypes,
|
|
264
|
-
recentHumanCount:
|
|
265
|
-
recentAgentCount:
|
|
266
|
-
consecutiveAgentTurns,
|
|
226
|
+
recentSenderTypes: history.recentSenderTypes,
|
|
227
|
+
recentHumanCount: history.recentHumanCount,
|
|
228
|
+
recentAgentCount: history.recentAgentCount,
|
|
229
|
+
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
230
|
+
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
267
231
|
};
|
|
268
232
|
}
|
|
269
233
|
function writeState(session) {
|
|
@@ -271,6 +235,8 @@ async function main() {
|
|
|
271
235
|
lastError: session.state.lastError,
|
|
272
236
|
model: session.state.model,
|
|
273
237
|
cwd: session.cwd,
|
|
238
|
+
executionMode: session.environment.mode,
|
|
239
|
+
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
274
240
|
hostMode: true,
|
|
275
241
|
clientType: 'codex',
|
|
276
242
|
state: session.state.state,
|
|
@@ -299,11 +265,17 @@ async function main() {
|
|
|
299
265
|
function clearStreaming(conversationId) {
|
|
300
266
|
rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
|
|
301
267
|
}
|
|
268
|
+
async function handoffFinalMessage(conversationId) {
|
|
269
|
+
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
270
|
+
clearStreaming(conversationId);
|
|
271
|
+
client.setTyping(conversationId, false).catch(() => { });
|
|
272
|
+
}
|
|
302
273
|
function closeSession(conversationId) {
|
|
303
274
|
const session = sessions.get(conversationId);
|
|
304
275
|
if (!session)
|
|
305
276
|
return;
|
|
306
277
|
session.closed = true;
|
|
278
|
+
releaseConversationEnvironment(session.environment);
|
|
307
279
|
clearStreaming(conversationId);
|
|
308
280
|
clearSessionState(conversationId, agentId).catch(() => { });
|
|
309
281
|
clearTurnState(conversationId, agentId).catch(() => { });
|
|
@@ -337,44 +309,59 @@ async function main() {
|
|
|
337
309
|
}
|
|
338
310
|
const creation = (async () => {
|
|
339
311
|
const config = await loadSessionConfig(conversationId, agentId);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
const session = {
|
|
312
|
+
const workspaceCwd = resolveWorkspaceCwd(config);
|
|
313
|
+
const environment = prepareConversationEnvironment({
|
|
314
|
+
agentId,
|
|
344
315
|
conversationId,
|
|
345
|
-
|
|
346
|
-
|
|
316
|
+
workspaceCwd,
|
|
317
|
+
allowWorktrees,
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
const sessionCwd = environment.cwd;
|
|
321
|
+
const sessionModel = config?.model ?? (typeof args.model === 'string' ? args.model : undefined);
|
|
322
|
+
const storedThreadId = loadStoredThreadId(agentId, conversationId, sessionCwd);
|
|
323
|
+
const session = {
|
|
324
|
+
conversationId,
|
|
347
325
|
cwd: sessionCwd,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
: null),
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
326
|
+
environment,
|
|
327
|
+
adapter: new CodexConversationAdapter({
|
|
328
|
+
cwd: sessionCwd,
|
|
329
|
+
threadId: storedThreadId,
|
|
330
|
+
codexBin: typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex',
|
|
331
|
+
model: sessionModel ?? null,
|
|
332
|
+
sandbox: (typeof args.sandbox === 'string' ? args.sandbox : null),
|
|
333
|
+
approvalPolicy: (typeof args['ask-for-approval'] === 'string'
|
|
334
|
+
? args['ask-for-approval']
|
|
335
|
+
: null),
|
|
336
|
+
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
337
|
+
addDirs: args['add-dir'] ?? [],
|
|
338
|
+
configOverrides: args.config ?? [],
|
|
339
|
+
fullAuto: Boolean(args['full-auto']),
|
|
340
|
+
bypassApprovalsAndSandbox: Boolean(args['dangerously-bypass-approvals-and-sandbox']),
|
|
341
|
+
}),
|
|
342
|
+
queue: [],
|
|
343
|
+
running: false,
|
|
344
|
+
state: {
|
|
345
|
+
model: sessionModel,
|
|
346
|
+
state: 'idle',
|
|
347
|
+
},
|
|
348
|
+
turnState: 'idle',
|
|
349
|
+
currentTurnId: null,
|
|
350
|
+
currentTurnOpenedAt: null,
|
|
351
|
+
lastAcceptedIntent: null,
|
|
352
|
+
lastActivity: Date.now(),
|
|
353
|
+
closed: false,
|
|
354
|
+
};
|
|
355
|
+
sessions.set(conversationId, session);
|
|
356
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Environment → ${environment.mode} (${sessionCwd})`);
|
|
357
|
+
writeState(session);
|
|
358
|
+
writeTurn(session);
|
|
359
|
+
return session;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
releaseConversationEnvironment(environment);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
378
365
|
})();
|
|
379
366
|
pendingSessionCreations.set(conversationId, creation);
|
|
380
367
|
try {
|
|
@@ -398,19 +385,31 @@ async function main() {
|
|
|
398
385
|
}
|
|
399
386
|
async function enqueueInboundMessage(input) {
|
|
400
387
|
const content = renderInboundContent(input.message);
|
|
388
|
+
const conversation = await getConversationMeta(input.conversationId);
|
|
389
|
+
const behavior = input.behavior ?? conversation?.behavior;
|
|
401
390
|
const participantContext = await loadParticipantContext({
|
|
402
391
|
conversationId: input.conversationId,
|
|
403
392
|
message: input.message,
|
|
404
393
|
senderName: input.senderName,
|
|
405
394
|
isOwner: input.isOwner,
|
|
406
395
|
});
|
|
407
|
-
const autoReply = decideAutoReply(participantContext);
|
|
396
|
+
const autoReply = decideAutoReply(participantContext, behavior);
|
|
408
397
|
if (!autoReply.allow) {
|
|
409
398
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
410
399
|
return;
|
|
411
400
|
}
|
|
412
401
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
413
|
-
|
|
402
|
+
let session;
|
|
403
|
+
try {
|
|
404
|
+
session = await getOrCreateSession(input.conversationId);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
408
|
+
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
409
|
+
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
410
|
+
await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`).catch(() => { });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
414
413
|
const turnMetadata = normalizeTurnMetadata(input.message.metadata);
|
|
415
414
|
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
416
415
|
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
@@ -418,6 +417,7 @@ async function main() {
|
|
|
418
417
|
content,
|
|
419
418
|
conversationId: input.conversationId,
|
|
420
419
|
participantContext,
|
|
420
|
+
behavior,
|
|
421
421
|
});
|
|
422
422
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
423
423
|
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted);
|
|
@@ -482,8 +482,6 @@ async function main() {
|
|
|
482
482
|
return;
|
|
483
483
|
}
|
|
484
484
|
if (event.type === 'turn.completed') {
|
|
485
|
-
session.turnState = 'completed';
|
|
486
|
-
writeTurn(session);
|
|
487
485
|
writeState(session);
|
|
488
486
|
}
|
|
489
487
|
}, (line) => {
|
|
@@ -492,11 +490,7 @@ async function main() {
|
|
|
492
490
|
if (result.threadId) {
|
|
493
491
|
saveStoredThreadId(agentId, session.conversationId, session.cwd, result.threadId);
|
|
494
492
|
}
|
|
495
|
-
clearStreaming(session.conversationId);
|
|
496
|
-
client.setTyping(session.conversationId, false).catch(() => { });
|
|
497
493
|
if (!result.interrupted && result.finalMessage) {
|
|
498
|
-
session.turnState = 'completed';
|
|
499
|
-
writeTurn(session);
|
|
500
494
|
await client.sendMessage(session.conversationId, result.finalMessage, {
|
|
501
495
|
metadata: {
|
|
502
496
|
turnId: session.currentTurnId,
|
|
@@ -505,14 +499,13 @@ async function main() {
|
|
|
505
499
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
506
500
|
},
|
|
507
501
|
});
|
|
502
|
+
await handoffFinalMessage(session.conversationId);
|
|
508
503
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent reply (${result.finalMessage.length} chars)`);
|
|
509
504
|
}
|
|
510
505
|
else if (!result.interrupted && result.exitCode && result.exitCode !== 0) {
|
|
511
506
|
const userVisibleError = formatTurnFailure(result.errorText);
|
|
512
507
|
session.state.lastError = userVisibleError;
|
|
513
|
-
session.turnState = 'completed';
|
|
514
508
|
writeState(session);
|
|
515
|
-
writeTurn(session);
|
|
516
509
|
if (result.errorText) {
|
|
517
510
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
|
|
518
511
|
}
|
|
@@ -524,21 +517,23 @@ async function main() {
|
|
|
524
517
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
525
518
|
},
|
|
526
519
|
});
|
|
520
|
+
await handoffFinalMessage(session.conversationId);
|
|
521
|
+
}
|
|
522
|
+
else if (!result.interrupted) {
|
|
523
|
+
await handoffFinalMessage(session.conversationId);
|
|
527
524
|
}
|
|
528
525
|
else if (result.interrupted) {
|
|
529
526
|
session.turnState = 'interrupted';
|
|
530
527
|
writeTurn(session);
|
|
528
|
+
clearStreaming(session.conversationId);
|
|
529
|
+
client.setTyping(session.conversationId, false).catch(() => { });
|
|
531
530
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn interrupted`);
|
|
532
531
|
}
|
|
533
532
|
}
|
|
534
533
|
catch (error) {
|
|
535
|
-
clearStreaming(session.conversationId);
|
|
536
|
-
client.setTyping(session.conversationId, false).catch(() => { });
|
|
537
534
|
const message = `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
|
|
538
535
|
session.state.lastError = message;
|
|
539
|
-
session.turnState = 'completed';
|
|
540
536
|
writeState(session);
|
|
541
|
-
writeTurn(session);
|
|
542
537
|
await client.sendMessage(session.conversationId, message, {
|
|
543
538
|
metadata: {
|
|
544
539
|
turnId: session.currentTurnId,
|
|
@@ -547,6 +542,7 @@ async function main() {
|
|
|
547
542
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
548
543
|
},
|
|
549
544
|
}).catch(() => { });
|
|
545
|
+
await handoffFinalMessage(session.conversationId);
|
|
550
546
|
clearStoredThreadId(agentId, session.conversationId);
|
|
551
547
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
552
548
|
}
|
|
@@ -577,7 +573,8 @@ async function main() {
|
|
|
577
573
|
conversationId: payload.conversationId,
|
|
578
574
|
message,
|
|
579
575
|
senderName: message.senderName || message.senderId,
|
|
580
|
-
isOwner:
|
|
576
|
+
isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
|
|
577
|
+
behavior: payload.behavior,
|
|
581
578
|
});
|
|
582
579
|
},
|
|
583
580
|
onConnected: () => console.error('[canon-codex] SSE connected'),
|
|
@@ -589,7 +586,7 @@ async function main() {
|
|
|
589
586
|
await publishAgentRuntime(agentId, {
|
|
590
587
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
591
588
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
592
|
-
availableWorkspaces:
|
|
589
|
+
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
593
590
|
});
|
|
594
591
|
}
|
|
595
592
|
catch (error) {
|
|
@@ -605,8 +602,8 @@ async function main() {
|
|
|
605
602
|
for (const conversation of conversations) {
|
|
606
603
|
if (!conversation.lastMessage || conversation.lastMessage.senderId === agentId)
|
|
607
604
|
continue;
|
|
608
|
-
const
|
|
609
|
-
const latestMessage =
|
|
605
|
+
const latestPage = await client.getMessagesPage(conversation.id, 1);
|
|
606
|
+
const latestMessage = latestPage.messages[0];
|
|
610
607
|
if (!latestMessage || latestMessage.senderId === agentId)
|
|
611
608
|
continue;
|
|
612
609
|
const senderTurnState = latestMessage.senderType === 'ai_agent'
|
|
@@ -627,6 +624,7 @@ async function main() {
|
|
|
627
624
|
message: latestMessage,
|
|
628
625
|
senderName: latestMessage.senderId,
|
|
629
626
|
isOwner: ownerId != null && latestMessage.senderId === ownerId,
|
|
627
|
+
behavior: latestPage.behavior,
|
|
630
628
|
});
|
|
631
629
|
}
|
|
632
630
|
}
|
package/dist/inbound-policy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
|
|
2
2
|
export interface InboundParticipantContext {
|
|
3
3
|
conversationType: CanonConversation['type'] | 'unknown';
|
|
4
4
|
memberCount: number | null;
|
|
@@ -10,10 +10,11 @@ export interface InboundParticipantContext {
|
|
|
10
10
|
recentHumanCount: number;
|
|
11
11
|
recentAgentCount: number;
|
|
12
12
|
consecutiveAgentTurns: number;
|
|
13
|
+
currentAgentStreakStartedByHuman: boolean;
|
|
13
14
|
}
|
|
14
15
|
export interface AutoReplyDecision {
|
|
15
16
|
allow: boolean;
|
|
16
17
|
reason: string;
|
|
17
18
|
}
|
|
18
19
|
export declare function buildInboundContextLines(context: InboundParticipantContext): string[];
|
|
19
|
-
export declare function decideAutoReply(context: InboundParticipantContext): AutoReplyDecision;
|
|
20
|
+
export declare function decideAutoReply(context: InboundParticipantContext, behavior?: ResolvedAgentBehaviorPolicy | null): AutoReplyDecision;
|
package/dist/inbound-policy.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
|
|
1
2
|
function formatRecentSenders(senderTypes) {
|
|
2
3
|
if (senderTypes.length === 0)
|
|
3
4
|
return 'none';
|
|
@@ -22,29 +23,21 @@ export function buildInboundContextLines(context) {
|
|
|
22
23
|
`Recent human messages: ${context.recentHumanCount}`,
|
|
23
24
|
`Recent agent messages: ${context.recentAgentCount}`,
|
|
24
25
|
`Consecutive recent agent turns: ${context.consecutiveAgentTurns}`,
|
|
26
|
+
`Current agent streak started after a human message: ${context.currentAgentStreakStartedByHuman ? 'yes' : 'no'}`,
|
|
25
27
|
];
|
|
26
28
|
}
|
|
27
|
-
export function decideAutoReply(context) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (context.consecutiveAgentTurns >= 2 && context.recentHumanCount === 0) {
|
|
44
|
-
return {
|
|
45
|
-
allow: false,
|
|
46
|
-
reason: 'suppressing likely agent-only loop in a direct conversation',
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return { allow: true, reason: 'direct agent message allowed' };
|
|
29
|
+
export function decideAutoReply(context, behavior) {
|
|
30
|
+
const decision = evaluateParticipationPolicy(behavior ?? resolveAgentBehaviorPolicy(), {
|
|
31
|
+
conversationType: context.conversationType,
|
|
32
|
+
senderType: context.senderType,
|
|
33
|
+
isOwner: context.isOwner,
|
|
34
|
+
mentionedAgent: context.mentionedAgent,
|
|
35
|
+
recentHumanCount: context.recentHumanCount,
|
|
36
|
+
consecutiveAgentTurns: context.consecutiveAgentTurns,
|
|
37
|
+
currentAgentStreakStartedByHuman: context.currentAgentStreakStartedByHuman,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
allow: decision.allow,
|
|
41
|
+
reason: decision.reason,
|
|
42
|
+
};
|
|
50
43
|
}
|
package/dist/setup.js
CHANGED
|
@@ -17,9 +17,11 @@ console.log('');
|
|
|
17
17
|
console.log(' 1. Register your agent');
|
|
18
18
|
console.log(' canon-codex-register --name "My Codex" --description "My local coding agent" --phone "+15551234567"');
|
|
19
19
|
console.log('');
|
|
20
|
-
console.log(' 2. Start the host in a project directory');
|
|
20
|
+
console.log(' 2. Start the host in a project directory and keep it running');
|
|
21
21
|
console.log(' canon-codex --cwd /path/to/project');
|
|
22
22
|
console.log('');
|
|
23
|
+
console.log(' A git repo is not required; any readable directory works.');
|
|
24
|
+
console.log('');
|
|
23
25
|
console.log('Optional flags:');
|
|
24
26
|
console.log(' --model gpt-5.4');
|
|
25
27
|
console.log(' --sandbox workspace-write');
|
|
@@ -27,3 +29,4 @@ console.log(' --full-auto');
|
|
|
27
29
|
console.log('');
|
|
28
30
|
console.log('Note: recent Codex CLI versions use --full-auto for non-interactive write access.');
|
|
29
31
|
console.log('Note: Canon uses the local Codex login state by default (for example ChatGPT/device auth or API-key auth).');
|
|
32
|
+
console.log('Note: If Canon starts returning "Invalid API key", rerun canon-codex-register to replace the saved profile and then restart the host.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/host.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prepack": "npm run build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@canonmsg/core": "^0.
|
|
25
|
+
"@canonmsg/core": "^0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18.0.0"
|