@canonmsg/codex-plugin 0.9.3 → 0.9.5
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/dist/adapter.d.ts +1 -0
- package/dist/adapter.js +3 -0
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -26
- package/dist/codex-cli-version.d.ts +2 -0
- package/dist/codex-cli-version.js +22 -0
- package/dist/error-format.d.ts +16 -0
- package/dist/error-format.js +106 -0
- package/dist/host-lifecycle.d.ts +4 -0
- package/dist/host-lifecycle.js +3 -0
- package/dist/host.js +284 -117
- package/dist/register.js +24 -0
- package/dist/session-store.d.ts +11 -2
- package/dist/session-store.js +36 -3
- package/dist/setup.js +14 -0
- package/package.json +3 -3
package/dist/adapter.d.ts
CHANGED
package/dist/adapter.js
CHANGED
package/dist/cli-entry.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
|
2
|
+
export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
|
package/dist/cli-entry.js
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
export function isDirectExecution(moduleUrl) {
|
|
5
|
-
const entry = process.argv[1];
|
|
6
|
-
if (!entry)
|
|
7
|
-
return false;
|
|
8
|
-
const abs = resolve(entry);
|
|
9
|
-
let real = abs;
|
|
10
|
-
try {
|
|
11
|
-
real = realpathSync(abs);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// argv[1] may not exist on disk in rare harnesses; fall back to abs
|
|
15
|
-
}
|
|
16
|
-
return (pathToFileURL(real).href === moduleUrl ||
|
|
17
|
-
pathToFileURL(abs).href === moduleUrl);
|
|
18
|
-
}
|
|
19
|
-
export function runCli(moduleUrl, main, onError) {
|
|
20
|
-
if (!isDirectExecution(moduleUrl)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
Promise.resolve()
|
|
24
|
-
.then(() => main())
|
|
25
|
-
.catch(onError);
|
|
26
|
-
}
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { parseCodexCliVersion, } from './error-format.js';
|
|
3
|
+
export function detectCodexCliVersion(codexBin, timeoutMs = 2_000) {
|
|
4
|
+
const result = spawnSync(codexBin, ['--version'], {
|
|
5
|
+
encoding: 'utf-8',
|
|
6
|
+
timeout: timeoutMs,
|
|
7
|
+
});
|
|
8
|
+
const raw = [result.stdout, result.stderr]
|
|
9
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
10
|
+
.join('\n')
|
|
11
|
+
.trim();
|
|
12
|
+
return {
|
|
13
|
+
codexBin,
|
|
14
|
+
raw: raw || null,
|
|
15
|
+
version: parseCodexCliVersion(raw),
|
|
16
|
+
error: result.error
|
|
17
|
+
? result.error.message
|
|
18
|
+
: result.status === 0
|
|
19
|
+
? null
|
|
20
|
+
: `codex --version exited with status ${result.status ?? 'unknown'}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MIN_CODEX_VERSION_FOR_GPT_5_5 = "0.125.0";
|
|
2
|
+
export interface CodexCliVersionStatus {
|
|
3
|
+
codexBin: string;
|
|
4
|
+
raw: string | null;
|
|
5
|
+
version: string | null;
|
|
6
|
+
error: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function normalizeCodexDiagnosticText(value: string | null | undefined): string | null;
|
|
9
|
+
export declare function isRecoverableCodexThreadError(value: string | null | undefined): boolean;
|
|
10
|
+
export declare function isCodexUpgradeRequiredError(value: string | null | undefined): boolean;
|
|
11
|
+
export declare function compareSemverLike(a: string, b: string): number;
|
|
12
|
+
export declare function parseCodexCliVersion(raw: string | null | undefined): string | null;
|
|
13
|
+
export declare function isGpt55Model(model: string | null | undefined): boolean;
|
|
14
|
+
export declare function codexCliUpgradeMessage(model: string): string;
|
|
15
|
+
export declare function buildCodexModelGuardMessage(model: string | null | undefined, status: Pick<CodexCliVersionStatus, 'version'>): string | null;
|
|
16
|
+
export declare function formatCodexTurnFailure(errorText: string | null | undefined): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const MIN_CODEX_VERSION_FOR_GPT_5_5 = '0.125.0';
|
|
2
|
+
function extractErrorMessage(value) {
|
|
3
|
+
if (!value || typeof value !== 'object')
|
|
4
|
+
return null;
|
|
5
|
+
const data = value;
|
|
6
|
+
const direct = typeof data.message === 'string' ? data.message.trim() : '';
|
|
7
|
+
if (direct)
|
|
8
|
+
return direct;
|
|
9
|
+
const error = data.error;
|
|
10
|
+
if (typeof error === 'string' && error.trim())
|
|
11
|
+
return error.trim();
|
|
12
|
+
if (error && typeof error === 'object') {
|
|
13
|
+
const nested = error;
|
|
14
|
+
const message = typeof nested.message === 'string' ? nested.message.trim() : '';
|
|
15
|
+
if (message)
|
|
16
|
+
return message;
|
|
17
|
+
const type = typeof nested.type === 'string' ? nested.type.trim() : '';
|
|
18
|
+
if (type)
|
|
19
|
+
return type;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function parseJsonMessage(text) {
|
|
24
|
+
try {
|
|
25
|
+
return extractErrorMessage(JSON.parse(text));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function normalizeCodexDiagnosticText(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return null;
|
|
34
|
+
const normalized = value.replace(/^error:\s*/i, '').trim();
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return null;
|
|
37
|
+
const directJson = parseJsonMessage(normalized);
|
|
38
|
+
if (directJson)
|
|
39
|
+
return directJson;
|
|
40
|
+
for (const line of normalized.split(/\r?\n/)) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed.startsWith('{'))
|
|
43
|
+
continue;
|
|
44
|
+
const lineJson = parseJsonMessage(trimmed);
|
|
45
|
+
if (lineJson)
|
|
46
|
+
return lineJson;
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
export function isRecoverableCodexThreadError(value) {
|
|
51
|
+
const normalized = normalizeCodexDiagnosticText(value)?.toLowerCase() ?? '';
|
|
52
|
+
return /thread .*not found/.test(normalized)
|
|
53
|
+
|| normalized.includes('unknown thread')
|
|
54
|
+
|| normalized.includes('find_thread_path')
|
|
55
|
+
|| normalized.includes('state db discrepancy');
|
|
56
|
+
}
|
|
57
|
+
export function isCodexUpgradeRequiredError(value) {
|
|
58
|
+
const normalized = normalizeCodexDiagnosticText(value)?.toLowerCase() ?? '';
|
|
59
|
+
return normalized.includes('requires a newer version of codex')
|
|
60
|
+
|| (normalized.includes('upgrade') && normalized.includes('codex'));
|
|
61
|
+
}
|
|
62
|
+
export function compareSemverLike(a, b) {
|
|
63
|
+
const parse = (value) => value.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
64
|
+
const left = parse(a);
|
|
65
|
+
const right = parse(b);
|
|
66
|
+
const length = Math.max(left.length, right.length, 3);
|
|
67
|
+
for (let index = 0; index < length; index += 1) {
|
|
68
|
+
const diff = (left[index] ?? 0) - (right[index] ?? 0);
|
|
69
|
+
if (diff !== 0)
|
|
70
|
+
return diff;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
export function parseCodexCliVersion(raw) {
|
|
75
|
+
if (!raw)
|
|
76
|
+
return null;
|
|
77
|
+
const match = raw.match(/\b(\d+\.\d+\.\d+)\b/);
|
|
78
|
+
return match?.[1] ?? null;
|
|
79
|
+
}
|
|
80
|
+
export function isGpt55Model(model) {
|
|
81
|
+
return typeof model === 'string' && /^gpt-5\.5(?:$|[-_:])/i.test(model.trim());
|
|
82
|
+
}
|
|
83
|
+
export function codexCliUpgradeMessage(model) {
|
|
84
|
+
return `The selected Codex model (${model}) requires a newer local Codex CLI. Update @openai/codex to ${MIN_CODEX_VERSION_FOR_GPT_5_5} or newer, then restart this Canon host.`;
|
|
85
|
+
}
|
|
86
|
+
export function buildCodexModelGuardMessage(model, status) {
|
|
87
|
+
const modelId = typeof model === 'string' ? model.trim() : '';
|
|
88
|
+
if (!isGpt55Model(modelId))
|
|
89
|
+
return null;
|
|
90
|
+
if (!status.version)
|
|
91
|
+
return null;
|
|
92
|
+
return compareSemverLike(status.version, MIN_CODEX_VERSION_FOR_GPT_5_5) < 0
|
|
93
|
+
? codexCliUpgradeMessage(modelId)
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
export function formatCodexTurnFailure(errorText) {
|
|
97
|
+
const normalized = normalizeCodexDiagnosticText(errorText);
|
|
98
|
+
if (!normalized) {
|
|
99
|
+
return 'The Codex session stopped unexpectedly before sending a final reply.';
|
|
100
|
+
}
|
|
101
|
+
if (isCodexUpgradeRequiredError(normalized)) {
|
|
102
|
+
return codexCliUpgradeMessage('gpt-5.5');
|
|
103
|
+
}
|
|
104
|
+
const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
|
|
105
|
+
return `Codex failed before sending a final reply: ${shortened}`;
|
|
106
|
+
}
|
package/dist/host.js
CHANGED
|
@@ -3,12 +3,41 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
|
|
6
|
-
import { 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, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
6
|
+
import { 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
7
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
8
8
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
9
|
-
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
9
|
+
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
10
10
|
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
11
|
+
import { detectCodexCliVersion } from './codex-cli-version.js';
|
|
12
|
+
import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
|
|
13
|
+
import { startCodexStreamInBackground } from './host-lifecycle.js';
|
|
11
14
|
import { runCli } from './cli-entry.js';
|
|
15
|
+
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
16
|
+
|
|
17
|
+
USAGE
|
|
18
|
+
canon-codex [flags]
|
|
19
|
+
|
|
20
|
+
COMMON FLAGS
|
|
21
|
+
--cwd <path> Project directory to run Codex from
|
|
22
|
+
--workspace <path> Additional project to expose in Canon
|
|
23
|
+
--workspace-root <path> Discover projects under an approved root
|
|
24
|
+
--model <model> Default Codex model for new turns
|
|
25
|
+
--sandbox <mode> Codex sandbox mode
|
|
26
|
+
--full-auto Allow non-interactive write access
|
|
27
|
+
--codex-bin <path> Codex CLI binary to run
|
|
28
|
+
--config <key=value> Forward config to Codex CLI
|
|
29
|
+
--help, -h Show this help
|
|
30
|
+
--version, -V Show package version
|
|
31
|
+
|
|
32
|
+
AUTH
|
|
33
|
+
CANON_AGENT=<profile> canon-codex --cwd /path/to/project
|
|
34
|
+
CANON_API_KEY=agk_live_... canon-codex --cwd /path/to/project
|
|
35
|
+
|
|
36
|
+
EXAMPLES
|
|
37
|
+
canon-codex --cwd ~/dev/canon
|
|
38
|
+
canon-codex --cwd ~/dev/canon --workspace-root ~/dev --full-auto
|
|
39
|
+
|
|
40
|
+
Keep this terminal open while you want Canon to reach the agent.`;
|
|
12
41
|
const MAX_SESSIONS = 12;
|
|
13
42
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14
43
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -25,7 +54,7 @@ let workspaceOptions = [];
|
|
|
25
54
|
let workspaceRoots = [];
|
|
26
55
|
let workspaceRootMetadata = [];
|
|
27
56
|
function buildCodexRuntimeDescriptor(input) {
|
|
28
|
-
|
|
57
|
+
const descriptor = buildFirstPartyCodingRuntimeDescriptor({
|
|
29
58
|
clientType: 'codex',
|
|
30
59
|
models: input.models,
|
|
31
60
|
workspaces: input.workspaces,
|
|
@@ -57,6 +86,27 @@ function buildCodexRuntimeDescriptor(input) {
|
|
|
57
86
|
},
|
|
58
87
|
],
|
|
59
88
|
});
|
|
89
|
+
if (input.models.length > 0) {
|
|
90
|
+
return descriptor;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...descriptor,
|
|
94
|
+
coreControls: descriptor.coreControls.filter((control) => control.id !== 'model'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function modelOptionLabel(model) {
|
|
98
|
+
if (/^gpt-5\.5(?:$|[-_:])/i.test(model))
|
|
99
|
+
return 'GPT-5.5';
|
|
100
|
+
if (/^gpt-5\.4-mini(?:$|[-_:])/i.test(model))
|
|
101
|
+
return 'GPT-5.4 Mini';
|
|
102
|
+
if (/^gpt-5\.4(?:$|[-_:])/i.test(model))
|
|
103
|
+
return 'GPT-5.4';
|
|
104
|
+
return model;
|
|
105
|
+
}
|
|
106
|
+
function buildCodexModelOptions(model) {
|
|
107
|
+
return typeof model === 'string' && model.trim()
|
|
108
|
+
? [{ value: model.trim(), label: modelOptionLabel(model.trim()) }]
|
|
109
|
+
: [];
|
|
60
110
|
}
|
|
61
111
|
function normalizeRuntimeTurnState(value) {
|
|
62
112
|
const normalizedTurn = normalizeTurnState(value);
|
|
@@ -112,14 +162,6 @@ function summarizeCommand(command) {
|
|
|
112
162
|
const shortened = trimmed.length > 140 ? `${trimmed.slice(0, 137)}...` : trimmed;
|
|
113
163
|
return `Running: ${shortened}`;
|
|
114
164
|
}
|
|
115
|
-
function formatTurnFailure(errorText) {
|
|
116
|
-
if (!errorText) {
|
|
117
|
-
return 'The Codex session stopped unexpectedly before sending a final reply.';
|
|
118
|
-
}
|
|
119
|
-
const normalized = errorText.replace(/^error:\s*/i, '').trim();
|
|
120
|
-
const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
|
|
121
|
-
return `Codex failed before sending a final reply: ${shortened}`;
|
|
122
|
-
}
|
|
123
165
|
function sleep(ms) {
|
|
124
166
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
125
167
|
}
|
|
@@ -157,6 +199,14 @@ export async function main() {
|
|
|
157
199
|
if (typeof args['ask-for-approval'] === 'string') {
|
|
158
200
|
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.');
|
|
159
201
|
}
|
|
202
|
+
const codexBin = typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex';
|
|
203
|
+
const codexCliStatus = detectCodexCliVersion(codexBin);
|
|
204
|
+
if (codexCliStatus.version) {
|
|
205
|
+
console.error(`[canon-codex] Detected Codex CLI ${codexCliStatus.version} (${codexBin})`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.error(`[canon-codex] Could not detect Codex CLI version for ${codexBin}: ${codexCliStatus.error ?? 'unknown result'}`);
|
|
209
|
+
}
|
|
160
210
|
const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-codex]', expectedClientType: 'codex' });
|
|
161
211
|
console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
162
212
|
const client = new CanonClient(apiKey, baseUrl);
|
|
@@ -264,6 +314,7 @@ export async function main() {
|
|
|
264
314
|
runtimeState.writeSessionState(session.conversationId, {
|
|
265
315
|
lastError: session.state.lastError,
|
|
266
316
|
model: session.state.model,
|
|
317
|
+
permissionMode: session.state.permissionMode,
|
|
267
318
|
cwd: session.cwd,
|
|
268
319
|
executionMode: session.environment.mode,
|
|
269
320
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
@@ -295,6 +346,13 @@ export async function main() {
|
|
|
295
346
|
return;
|
|
296
347
|
await client.updateMessageDisposition(conversationId, sourceMessageId, 'accepted_now').catch(() => { });
|
|
297
348
|
}
|
|
349
|
+
async function markQueuedPromptsRejected(conversationId, prompts) {
|
|
350
|
+
await Promise.all(prompts.map((prompt) => {
|
|
351
|
+
if (!prompt.markAccepted || !prompt.sourceMessageId)
|
|
352
|
+
return Promise.resolve();
|
|
353
|
+
return client.updateMessageDisposition(conversationId, prompt.sourceMessageId, 'rejected').catch(() => { });
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
298
356
|
function clearStreaming(conversationId) {
|
|
299
357
|
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
300
358
|
}
|
|
@@ -376,15 +434,37 @@ export async function main() {
|
|
|
376
434
|
try {
|
|
377
435
|
const sessionCwd = environment.cwd;
|
|
378
436
|
const sessionModel = config?.model ?? (typeof args.model === 'string' ? args.model : undefined);
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
437
|
+
const modelGuard = buildCodexModelGuardMessage(sessionModel, codexCliStatus);
|
|
438
|
+
if (modelGuard) {
|
|
439
|
+
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
440
|
+
}
|
|
441
|
+
const effectivePermissionMode = config?.permissionMode ?? codexPermissionEnvelope.defaultPermissionMode;
|
|
442
|
+
if (effectivePermissionMode
|
|
443
|
+
&& !codexPermissionEnvelope.availablePermissionModes.some((option) => option.value === effectivePermissionMode)) {
|
|
444
|
+
throw new ExecutionEnvironmentError(`Permission mode "${effectivePermissionMode}" is not supported by this Codex host.`, 'This Canon host was started with stricter approval settings. Choose one of the advertised permission modes or restart the host with more permissive flags.');
|
|
383
445
|
}
|
|
384
|
-
const approvalOverride = mapCanonPermissionToCodex(
|
|
446
|
+
const approvalOverride = mapCanonPermissionToCodex(effectivePermissionMode);
|
|
385
447
|
const defaultSandbox = (typeof args.sandbox === 'string' ? args.sandbox : null);
|
|
386
448
|
const defaultFullAuto = Boolean(args['full-auto']);
|
|
387
449
|
const defaultBypass = Boolean(args['dangerously-bypass-approvals-and-sandbox']);
|
|
450
|
+
const legacyApprovalPolicy = (typeof args['ask-for-approval'] === 'string'
|
|
451
|
+
? args['ask-for-approval']
|
|
452
|
+
: null);
|
|
453
|
+
const effectiveSandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
|
|
454
|
+
const effectiveFullAuto = approvalOverride ? approvalOverride.fullAuto : defaultFullAuto;
|
|
455
|
+
const effectiveBypass = approvalOverride
|
|
456
|
+
? approvalOverride.bypassApprovalsAndSandbox
|
|
457
|
+
: defaultBypass;
|
|
458
|
+
const policyFingerprint = buildCodexThreadPolicyFingerprint({
|
|
459
|
+
baseCwd: environment.baseCwd,
|
|
460
|
+
executionMode: environment.mode,
|
|
461
|
+
permissionMode: effectivePermissionMode ?? null,
|
|
462
|
+
sandbox: effectiveSandbox,
|
|
463
|
+
approvalPolicy: approvalOverride ? null : legacyApprovalPolicy,
|
|
464
|
+
fullAuto: effectiveFullAuto,
|
|
465
|
+
bypassApprovalsAndSandbox: effectiveBypass,
|
|
466
|
+
});
|
|
467
|
+
const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policyFingerprint);
|
|
388
468
|
const session = {
|
|
389
469
|
conversationId,
|
|
390
470
|
cwd: sessionCwd,
|
|
@@ -392,26 +472,24 @@ export async function main() {
|
|
|
392
472
|
adapter: new CodexConversationAdapter({
|
|
393
473
|
cwd: sessionCwd,
|
|
394
474
|
threadId: storedThreadId,
|
|
395
|
-
codexBin
|
|
475
|
+
codexBin,
|
|
396
476
|
model: sessionModel ?? null,
|
|
397
|
-
sandbox:
|
|
398
|
-
approvalPolicy:
|
|
399
|
-
? args['ask-for-approval']
|
|
400
|
-
: null),
|
|
477
|
+
sandbox: effectiveSandbox,
|
|
478
|
+
approvalPolicy: approvalOverride ? null : legacyApprovalPolicy,
|
|
401
479
|
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
402
480
|
addDirs: args['add-dir'] ?? [],
|
|
403
481
|
configOverrides: args.config ?? [],
|
|
404
|
-
fullAuto:
|
|
405
|
-
bypassApprovalsAndSandbox:
|
|
406
|
-
? approvalOverride.bypassApprovalsAndSandbox
|
|
407
|
-
: defaultBypass,
|
|
482
|
+
fullAuto: effectiveFullAuto,
|
|
483
|
+
bypassApprovalsAndSandbox: effectiveBypass,
|
|
408
484
|
}),
|
|
409
485
|
queue: [],
|
|
410
486
|
running: false,
|
|
411
487
|
state: {
|
|
412
488
|
model: sessionModel,
|
|
489
|
+
permissionMode: effectivePermissionMode,
|
|
413
490
|
state: 'idle',
|
|
414
491
|
},
|
|
492
|
+
policyFingerprint,
|
|
415
493
|
turnState: 'idle',
|
|
416
494
|
currentTurnId: null,
|
|
417
495
|
currentTurnOpenedAt: null,
|
|
@@ -421,6 +499,10 @@ export async function main() {
|
|
|
421
499
|
closed: false,
|
|
422
500
|
};
|
|
423
501
|
sessions.set(conversationId, session);
|
|
502
|
+
await Promise.all([
|
|
503
|
+
baselineControlSignal(conversationId),
|
|
504
|
+
baselineSessionControl(conversationId),
|
|
505
|
+
]);
|
|
424
506
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Environment → ${environment.mode} (${sessionCwd})`);
|
|
425
507
|
writeState(session);
|
|
426
508
|
writeTurn(session);
|
|
@@ -549,11 +631,15 @@ export async function main() {
|
|
|
549
631
|
status: 'thinking',
|
|
550
632
|
}).catch(() => { });
|
|
551
633
|
try {
|
|
634
|
+
const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
|
|
635
|
+
if (modelGuard) {
|
|
636
|
+
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
637
|
+
}
|
|
552
638
|
const turnImagePaths = nextTurn.imagePaths ?? [];
|
|
553
|
-
const
|
|
639
|
+
const handleCodexEvent = (event) => {
|
|
554
640
|
session.lastActivity = Date.now();
|
|
555
641
|
if (event.type === 'thread.started') {
|
|
556
|
-
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode);
|
|
642
|
+
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
|
|
557
643
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
|
|
558
644
|
return;
|
|
559
645
|
}
|
|
@@ -581,13 +667,32 @@ export async function main() {
|
|
|
581
667
|
if (event.type === 'turn.completed') {
|
|
582
668
|
writeState(session);
|
|
583
669
|
}
|
|
584
|
-
}
|
|
670
|
+
};
|
|
671
|
+
const logCodexLine = (line) => {
|
|
585
672
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
|
|
586
|
-
}
|
|
673
|
+
};
|
|
674
|
+
const clearStoredThread = () => {
|
|
675
|
+
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
676
|
+
session.adapter.clearThreadId();
|
|
677
|
+
};
|
|
678
|
+
const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths);
|
|
679
|
+
let result = await runTurnOnce();
|
|
680
|
+
if (!result.interrupted
|
|
681
|
+
&& !result.finalMessage
|
|
682
|
+
&& result.exitCode
|
|
683
|
+
&& result.exitCode !== 0
|
|
684
|
+
&& isRecoverableCodexThreadError(result.errorText)) {
|
|
685
|
+
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Stored thread was not found; clearing and retrying once`);
|
|
686
|
+
clearStoredThread();
|
|
687
|
+
result = await runTurnOnce();
|
|
688
|
+
}
|
|
587
689
|
if (result.threadId) {
|
|
588
|
-
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode);
|
|
690
|
+
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
|
|
589
691
|
}
|
|
590
692
|
if (!result.interrupted && result.finalMessage) {
|
|
693
|
+
if (isRecoverableCodexThreadError(result.errorText)) {
|
|
694
|
+
clearStoredThread();
|
|
695
|
+
}
|
|
591
696
|
await client.sendMessage(session.conversationId, result.finalMessage, {
|
|
592
697
|
metadata: {
|
|
593
698
|
turnId: session.currentTurnId,
|
|
@@ -600,7 +705,7 @@ export async function main() {
|
|
|
600
705
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent reply (${result.finalMessage.length} chars)`);
|
|
601
706
|
}
|
|
602
707
|
else if (!result.interrupted && result.exitCode && result.exitCode !== 0) {
|
|
603
|
-
const userVisibleError =
|
|
708
|
+
const userVisibleError = formatCodexTurnFailure(result.errorText);
|
|
604
709
|
session.state.lastError = userVisibleError;
|
|
605
710
|
writeState(session);
|
|
606
711
|
if (result.errorText) {
|
|
@@ -629,7 +734,9 @@ export async function main() {
|
|
|
629
734
|
}
|
|
630
735
|
}
|
|
631
736
|
catch (error) {
|
|
632
|
-
const message =
|
|
737
|
+
const message = error instanceof ExecutionEnvironmentError
|
|
738
|
+
? error.userMessage
|
|
739
|
+
: `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
|
|
633
740
|
session.state.lastError = message;
|
|
634
741
|
writeState(session);
|
|
635
742
|
await client.sendMessage(session.conversationId, message, {
|
|
@@ -641,7 +748,7 @@ export async function main() {
|
|
|
641
748
|
},
|
|
642
749
|
}).catch(() => { });
|
|
643
750
|
await handoffFinalMessage(session.conversationId);
|
|
644
|
-
if (error instanceof Error &&
|
|
751
|
+
if (error instanceof Error && isRecoverableCodexThreadError(error.message)) {
|
|
645
752
|
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
646
753
|
}
|
|
647
754
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
@@ -663,11 +770,14 @@ export async function main() {
|
|
|
663
770
|
}
|
|
664
771
|
}
|
|
665
772
|
let controlStopped = false;
|
|
773
|
+
const lastSeenControl = new Map();
|
|
774
|
+
const lastSeenSignal = new Map();
|
|
666
775
|
let streamConnected = false;
|
|
667
776
|
const hostAvailableExecutionModes = [
|
|
668
777
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
669
778
|
];
|
|
670
779
|
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
780
|
+
const codexModelOptions = buildCodexModelOptions(args.model);
|
|
671
781
|
let runtimeDescriptor = {
|
|
672
782
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
673
783
|
...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
|
|
@@ -678,7 +788,7 @@ export async function main() {
|
|
|
678
788
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
679
789
|
: {}),
|
|
680
790
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
681
|
-
models:
|
|
791
|
+
models: codexModelOptions,
|
|
682
792
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
683
793
|
workspaceRoots: workspaceRootMetadata,
|
|
684
794
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -686,6 +796,29 @@ export async function main() {
|
|
|
686
796
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
687
797
|
}),
|
|
688
798
|
};
|
|
799
|
+
async function baselineControlSignal(conversationId) {
|
|
800
|
+
if (lastSeenSignal.has(conversationId))
|
|
801
|
+
return;
|
|
802
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`).catch(() => null);
|
|
803
|
+
if (!raw || typeof raw !== 'object')
|
|
804
|
+
return;
|
|
805
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
806
|
+
if (timestamp > 0) {
|
|
807
|
+
lastSeenSignal.set(conversationId, timestamp);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function baselineSessionControl(conversationId) {
|
|
811
|
+
if (lastSeenControl.has(conversationId))
|
|
812
|
+
return;
|
|
813
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/session`).catch(() => null);
|
|
814
|
+
if (!raw || typeof raw !== 'object')
|
|
815
|
+
return;
|
|
816
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
817
|
+
if (timestamp > 0) {
|
|
818
|
+
lastSeenControl.set(conversationId, timestamp);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
let publishRuntimeDetailsInFlight = false;
|
|
689
822
|
const publishRuntimeHeartbeat = async () => {
|
|
690
823
|
heartbeatLocalRuntimeEntry(runtimeId, {
|
|
691
824
|
agentId,
|
|
@@ -695,84 +828,99 @@ export async function main() {
|
|
|
695
828
|
});
|
|
696
829
|
if (!streamConnected)
|
|
697
830
|
return;
|
|
698
|
-
await refreshKnownConversationIds().catch((error) => {
|
|
699
|
-
console.error('[canon-codex] Failed to refresh known conversations:', error);
|
|
700
|
-
});
|
|
701
831
|
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
702
832
|
console.error('[canon-codex] Failed to publish agent runtime:', error);
|
|
703
833
|
});
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
834
|
+
if (publishRuntimeDetailsInFlight)
|
|
835
|
+
return;
|
|
836
|
+
publishRuntimeDetailsInFlight = true;
|
|
837
|
+
try {
|
|
838
|
+
await refreshKnownConversationIds().catch((error) => {
|
|
839
|
+
console.error('[canon-codex] Failed to refresh known conversations:', error);
|
|
840
|
+
});
|
|
841
|
+
await publishHostSessionSnapshots({
|
|
842
|
+
conversationIds: Array.from(knownConversationIds),
|
|
843
|
+
agentId,
|
|
844
|
+
clientType: 'codex',
|
|
845
|
+
runtime: runtimeDescriptor,
|
|
846
|
+
workspaceOptions,
|
|
847
|
+
defaultCwd: workingDir,
|
|
848
|
+
extraSessionConfigFields: ['permissionMode'],
|
|
849
|
+
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
850
|
+
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
851
|
+
return [
|
|
852
|
+
session.conversationId,
|
|
853
|
+
{
|
|
854
|
+
...(session.state.model ? { model: session.state.model } : {}),
|
|
855
|
+
...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
|
|
856
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
857
|
+
executionMode: session.environment.mode,
|
|
858
|
+
executionBranch: session.environment.branch ?? null,
|
|
859
|
+
},
|
|
860
|
+
];
|
|
861
|
+
})),
|
|
862
|
+
}).catch((error) => {
|
|
863
|
+
console.error('[canon-codex] Failed to publish session snapshots:', error);
|
|
864
|
+
});
|
|
865
|
+
await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
|
|
866
|
+
const session = sessions.get(conversationId);
|
|
867
|
+
const workspaceId = session
|
|
868
|
+
? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
|
|
869
|
+
: runtimeDescriptor.defaultWorkspaceId;
|
|
870
|
+
const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
|
|
871
|
+
const descriptor = runtimeDescriptor.runtimeDescriptor;
|
|
872
|
+
if (!descriptor)
|
|
873
|
+
return;
|
|
874
|
+
const payload = {
|
|
875
|
+
descriptor,
|
|
876
|
+
surfaceMode: 'host',
|
|
877
|
+
statusItems: [
|
|
878
|
+
{
|
|
879
|
+
id: 'transport',
|
|
880
|
+
label: 'Transport',
|
|
881
|
+
value: 'exec --json',
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: 'streaming',
|
|
885
|
+
label: 'Live output',
|
|
886
|
+
value: 'Thinking, tools, and completed-message previews',
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: 'codex-cli',
|
|
890
|
+
label: 'Codex CLI',
|
|
891
|
+
value: codexCliStatus.version ?? (codexCliStatus.raw ?? 'Version unknown'),
|
|
892
|
+
tone: codexCliStatus.version ? 'default' : 'warning',
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: 'nativeActions',
|
|
896
|
+
label: 'Native actions',
|
|
897
|
+
value: 'Limited until app-server transport',
|
|
898
|
+
tone: 'warning',
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
execution: {
|
|
902
|
+
resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
|
|
903
|
+
resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
|
|
904
|
+
workspaceRootId: workspace?.workspaceRootId ?? null,
|
|
905
|
+
workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
|
|
906
|
+
executionMode: session?.environment.mode ?? null,
|
|
907
|
+
executionBranch: session?.environment.branch ?? null,
|
|
908
|
+
worktreePath: session?.environment.worktreePath ?? null,
|
|
909
|
+
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
755
910
|
},
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
'Codex review, compact/rollback, live plan/diff/reasoning updates, PTY command execution, plugin/app/MCP inventory, and structured approvals require the future app-server transport.',
|
|
770
|
-
],
|
|
771
|
-
};
|
|
772
|
-
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
773
|
-
})).catch((error) => {
|
|
774
|
-
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
775
|
-
});
|
|
911
|
+
notes: [
|
|
912
|
+
'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not token-by-token text deltas.',
|
|
913
|
+
'Codex review, compact/rollback, live plan/diff/reasoning updates, PTY command execution, plugin/app/MCP inventory, and structured approvals require the future app-server transport.',
|
|
914
|
+
],
|
|
915
|
+
};
|
|
916
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
917
|
+
})).catch((error) => {
|
|
918
|
+
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
finally {
|
|
922
|
+
publishRuntimeDetailsInFlight = false;
|
|
923
|
+
}
|
|
776
924
|
};
|
|
777
925
|
const stream = new CanonStream({
|
|
778
926
|
apiKey,
|
|
@@ -822,7 +970,7 @@ export async function main() {
|
|
|
822
970
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
823
971
|
: {}),
|
|
824
972
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
825
|
-
models:
|
|
973
|
+
models: codexModelOptions,
|
|
826
974
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
827
975
|
workspaceRoots: workspaceRootMetadata,
|
|
828
976
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -841,7 +989,7 @@ export async function main() {
|
|
|
841
989
|
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
842
990
|
: {}),
|
|
843
991
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
844
|
-
models:
|
|
992
|
+
models: codexModelOptions,
|
|
845
993
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
846
994
|
workspaceRoots: workspaceRootMetadata,
|
|
847
995
|
executionModes: hostAvailableExecutionModes,
|
|
@@ -911,11 +1059,9 @@ export async function main() {
|
|
|
911
1059
|
catch (error) {
|
|
912
1060
|
console.error('[canon-codex] Failed to load startup conversations:', error);
|
|
913
1061
|
}
|
|
914
|
-
|
|
1062
|
+
startCodexStreamInBackground(stream, (error) => {
|
|
915
1063
|
console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
|
|
916
1064
|
});
|
|
917
|
-
const lastSeenControl = new Map();
|
|
918
|
-
const lastSeenSignal = new Map();
|
|
919
1065
|
const pollControl = async () => {
|
|
920
1066
|
while (!controlStopped) {
|
|
921
1067
|
for (const conversationId of [...sessions.keys()]) {
|
|
@@ -929,6 +1075,14 @@ export async function main() {
|
|
|
929
1075
|
const session = sessions.get(conversationId);
|
|
930
1076
|
if (session && !session.closed) {
|
|
931
1077
|
if (control.model && control.model !== session.state.model) {
|
|
1078
|
+
const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
|
|
1079
|
+
if (modelGuard) {
|
|
1080
|
+
session.state.lastError = modelGuard;
|
|
1081
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${modelGuard}`);
|
|
1082
|
+
writeState(session);
|
|
1083
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
932
1086
|
session.adapter.setModel(control.model);
|
|
933
1087
|
session.state.model = control.model;
|
|
934
1088
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
|
|
@@ -941,6 +1095,7 @@ export async function main() {
|
|
|
941
1095
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] effort control is not mapped yet (${control.effort})`);
|
|
942
1096
|
}
|
|
943
1097
|
}
|
|
1098
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
|
|
944
1099
|
}
|
|
945
1100
|
}
|
|
946
1101
|
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`);
|
|
@@ -956,15 +1111,23 @@ export async function main() {
|
|
|
956
1111
|
const session = sessions.get(conversationId);
|
|
957
1112
|
if (!session || session.closed)
|
|
958
1113
|
continue;
|
|
1114
|
+
if (!session.running && (signal.type !== 'stop_and_drop' || session.queue.length === 0)) {
|
|
1115
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/signal`, null).catch(() => { });
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
959
1118
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${signal.type} signal`);
|
|
960
|
-
await session.adapter.interrupt();
|
|
961
|
-
session.turnState = 'interrupted';
|
|
962
1119
|
if (signal.type === 'stop_and_drop') {
|
|
963
|
-
session.queue.
|
|
1120
|
+
const droppedPrompts = session.queue.splice(0);
|
|
1121
|
+
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
1122
|
+
}
|
|
1123
|
+
if (session.running) {
|
|
1124
|
+
await session.adapter.interrupt();
|
|
964
1125
|
}
|
|
1126
|
+
session.turnState = 'interrupted';
|
|
965
1127
|
writeTurn(session);
|
|
966
1128
|
clearStreaming(conversationId);
|
|
967
1129
|
client.setTyping(conversationId, false).catch(() => { });
|
|
1130
|
+
await rtdbWrite(`/control/${conversationId}/${agentId}/signal`, null).catch(() => { });
|
|
968
1131
|
}
|
|
969
1132
|
catch {
|
|
970
1133
|
// Ignore transient RTDB failures.
|
|
@@ -1015,7 +1178,11 @@ export async function main() {
|
|
|
1015
1178
|
await new Promise(() => { });
|
|
1016
1179
|
}
|
|
1017
1180
|
runCli(import.meta.url, main, (error) => {
|
|
1018
|
-
|
|
1181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1182
|
+
console.error(`[canon-codex] ${message}`);
|
|
1019
1183
|
getActiveProfileLock()?.release();
|
|
1020
1184
|
process.exit(1);
|
|
1185
|
+
}, {
|
|
1186
|
+
name: 'canon-codex',
|
|
1187
|
+
help: HELP,
|
|
1021
1188
|
});
|
package/dist/register.js
CHANGED
|
@@ -4,6 +4,27 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
|
|
6
6
|
import { runCli } from './cli-entry.js';
|
|
7
|
+
const HELP = `canon-codex-register — register or reconnect a Codex agent in Canon
|
|
8
|
+
|
|
9
|
+
USAGE
|
|
10
|
+
canon-codex-register --name <name> --description <text> --phone <e164> [flags]
|
|
11
|
+
|
|
12
|
+
REQUIRED
|
|
13
|
+
--name <name> Agent display name shown in Canon
|
|
14
|
+
--description <text> Short profile description
|
|
15
|
+
--phone <e164> Owner phone number, for example +15551234567
|
|
16
|
+
|
|
17
|
+
FLAGS
|
|
18
|
+
--profile <name> Local profile name in ~/.canon/agents.json
|
|
19
|
+
--base-url <url> Canon API base URL override
|
|
20
|
+
--help, -h Show this help
|
|
21
|
+
--version, -V Show package version
|
|
22
|
+
|
|
23
|
+
EXAMPLES
|
|
24
|
+
canon-codex-register --name "My Codex" --description "Local coding agent" --phone "+15551234567"
|
|
25
|
+
canon-codex-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
26
|
+
|
|
27
|
+
After approval, start it with CANON_AGENT=<profile> canon-codex --cwd /path/to/project.`;
|
|
7
28
|
export async function main() {
|
|
8
29
|
setDefaultResultOrder('ipv4first');
|
|
9
30
|
const { values } = parseArgs({
|
|
@@ -92,4 +113,7 @@ export async function main() {
|
|
|
92
113
|
runCli(import.meta.url, main, (error) => {
|
|
93
114
|
console.error('[canon-codex-register] Fatal error:', error);
|
|
94
115
|
process.exit(1);
|
|
116
|
+
}, {
|
|
117
|
+
name: 'canon-codex-register',
|
|
118
|
+
help: HELP,
|
|
95
119
|
});
|
package/dist/session-store.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { type ExecutionEnvironmentMode } from '@canonmsg/core';
|
|
2
|
-
export declare function
|
|
3
|
-
|
|
2
|
+
export declare function buildCodexThreadPolicyFingerprint(input: {
|
|
3
|
+
baseCwd: string;
|
|
4
|
+
executionMode?: ExecutionEnvironmentMode;
|
|
5
|
+
permissionMode?: string | null;
|
|
6
|
+
sandbox?: string | null;
|
|
7
|
+
approvalPolicy?: string | null;
|
|
8
|
+
fullAuto?: boolean;
|
|
9
|
+
bypassApprovalsAndSandbox?: boolean;
|
|
10
|
+
}): string;
|
|
11
|
+
export declare function loadStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): string | null;
|
|
12
|
+
export declare function saveStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, threadId: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): void;
|
|
4
13
|
export declare function clearStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd?: string, executionMode?: ExecutionEnvironmentMode): void;
|
package/dist/session-store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
|
|
4
5
|
const STORE_PATH = join(CANON_DIR, 'codex-sessions.json');
|
|
@@ -14,29 +15,60 @@ function saveStore(store) {
|
|
|
14
15
|
mkdirSync(CANON_DIR, { recursive: true });
|
|
15
16
|
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
|
|
16
17
|
}
|
|
17
|
-
export function
|
|
18
|
+
export function buildCodexThreadPolicyFingerprint(input) {
|
|
19
|
+
return createHash('sha256').update(JSON.stringify({
|
|
20
|
+
version: 1,
|
|
21
|
+
baseCwd: input.baseCwd,
|
|
22
|
+
executionMode: input.executionMode ?? null,
|
|
23
|
+
permissionMode: input.permissionMode ?? null,
|
|
24
|
+
sandbox: input.sandbox ?? null,
|
|
25
|
+
approvalPolicy: input.approvalPolicy ?? null,
|
|
26
|
+
fullAuto: input.fullAuto === true,
|
|
27
|
+
bypassApprovalsAndSandbox: input.bypassApprovalsAndSandbox === true,
|
|
28
|
+
})).digest('hex').slice(0, 24);
|
|
29
|
+
}
|
|
30
|
+
export function loadStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode, policyFingerprint) {
|
|
18
31
|
if (runtimeId) {
|
|
19
32
|
const state = loadRuntimeSessionState(runtimeId, {
|
|
20
33
|
conversationId,
|
|
21
34
|
baseCwd,
|
|
22
35
|
executionMode,
|
|
23
36
|
});
|
|
24
|
-
if (state?.threadId)
|
|
37
|
+
if (state?.threadId) {
|
|
38
|
+
if (policyFingerprint && state.codexPolicyFingerprint !== policyFingerprint) {
|
|
39
|
+
clearRuntimeSessionState(runtimeId, {
|
|
40
|
+
conversationId,
|
|
41
|
+
baseCwd,
|
|
42
|
+
executionMode,
|
|
43
|
+
});
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
25
46
|
return state.threadId;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
26
49
|
}
|
|
27
50
|
const store = loadStore();
|
|
28
51
|
const record = store.agents[agentId]?.[conversationId];
|
|
29
52
|
if (!record || record.cwd !== baseCwd)
|
|
30
53
|
return null;
|
|
54
|
+
if (policyFingerprint && record.policyFingerprint !== policyFingerprint) {
|
|
55
|
+
delete store.agents[agentId][conversationId];
|
|
56
|
+
if (Object.keys(store.agents[agentId]).length === 0) {
|
|
57
|
+
delete store.agents[agentId];
|
|
58
|
+
}
|
|
59
|
+
saveStore(store);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
31
62
|
return record.threadId;
|
|
32
63
|
}
|
|
33
|
-
export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode) {
|
|
64
|
+
export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode, policyFingerprint) {
|
|
34
65
|
if (runtimeId) {
|
|
35
66
|
saveRuntimeSessionState(runtimeId, {
|
|
36
67
|
conversationId,
|
|
37
68
|
baseCwd,
|
|
38
69
|
executionMode,
|
|
39
70
|
threadId,
|
|
71
|
+
...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
|
|
40
72
|
});
|
|
41
73
|
return;
|
|
42
74
|
}
|
|
@@ -45,6 +77,7 @@ export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd,
|
|
|
45
77
|
store.agents[agentId][conversationId] = {
|
|
46
78
|
threadId,
|
|
47
79
|
cwd: baseCwd,
|
|
80
|
+
...(policyFingerprint ? { policyFingerprint } : {}),
|
|
48
81
|
updatedAt: new Date().toISOString(),
|
|
49
82
|
};
|
|
50
83
|
saveStore(store);
|
package/dist/setup.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { runCli } from './cli-entry.js';
|
|
4
|
+
const HELP = `canon-codex-setup — print Canon Codex setup guidance
|
|
5
|
+
|
|
6
|
+
USAGE
|
|
7
|
+
canon-codex-setup
|
|
8
|
+
|
|
9
|
+
FLAGS
|
|
10
|
+
--help, -h Show this help
|
|
11
|
+
--version, -V Show package version
|
|
12
|
+
|
|
13
|
+
This command does not register an agent. It checks for the Codex CLI and prints
|
|
14
|
+
the registration and host commands to run next.`;
|
|
4
15
|
export function main() {
|
|
5
16
|
console.log('Canon Codex Plugin Setup');
|
|
6
17
|
console.log('========================\n');
|
|
@@ -38,4 +49,7 @@ export function main() {
|
|
|
38
49
|
runCli(import.meta.url, main, (error) => {
|
|
39
50
|
console.error('[canon-codex-setup] Fatal error:', error);
|
|
40
51
|
process.exit(1);
|
|
52
|
+
}, {
|
|
53
|
+
name: 'canon-codex-setup',
|
|
54
|
+
help: HELP,
|
|
41
55
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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.15.
|
|
32
|
+
"@canonmsg/agent-sdk": "^1.1.0",
|
|
33
|
+
"@canonmsg/core": "^0.15.1"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|