@canonmsg/codex-plugin 0.27.0 → 0.29.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 +0 -17
- package/dist/app-server-adapter.js +1 -7
- package/dist/codex-app-tools.d.ts +8 -26
- package/dist/codex-app-tools.js +30 -84
- package/dist/control-channel.d.ts +4 -14
- package/dist/control-channel.js +3 -10
- package/dist/host.d.ts +3 -38
- package/dist/host.js +124 -289
- package/dist/register.js +2 -1
- package/dist/session-store.d.ts +5 -1
- package/dist/session-store.js +14 -2
- package/package.json +6 -6
package/dist/host.js
CHANGED
|
@@ -6,11 +6,11 @@ import { dirname } from 'node:path';
|
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
8
8
|
import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
|
|
9
|
-
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION,
|
|
9
|
+
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
10
|
import { validateCard } from '@canonmsg/rich-cards';
|
|
11
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
12
|
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
13
|
-
import { CODEX_APP_DYNAMIC_TOOLS, CODEX_NO_REPLY_MODEL_TOOL_NAME, CODEX_SERVICE_AGENT_DYNAMIC_TOOLS,
|
|
13
|
+
import { CODEX_APP_DYNAMIC_TOOLS, CODEX_NO_REPLY_MODEL_TOOL_NAME, CODEX_SERVICE_AGENT_DYNAMIC_TOOLS, answerCodexNoReply, classifyCodexAppToolRequest, deniedCodexAppToolResult, filterCodexCommunicationTools, handleCodexAppToolCall, handleCodexCommunicateToolCall, isCanonRuntimeControlToolCall, isCodexAppToolCall, isCodexCommunicateToolCall, isCodexServiceAgentToolCall, parseCanonRuntimeControlRequest, readCodexNoReplyReason, successfulCodexAppToolResult, } from './codex-app-tools.js';
|
|
14
14
|
import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
|
|
15
15
|
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
16
16
|
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
@@ -30,6 +30,7 @@ COMMON FLAGS
|
|
|
30
30
|
--cwd <path> Project directory to run Codex from
|
|
31
31
|
--workspace <path> Additional project to expose in Canon
|
|
32
32
|
--workspace-root <path> Discover projects under an approved root
|
|
33
|
+
--default-execution-mode <mode> New-conversation default: worktree or locked
|
|
33
34
|
--model <model> Default Codex model for new turns
|
|
34
35
|
--sandbox <mode> Codex sandbox mode
|
|
35
36
|
--full-auto Allow non-interactive write access
|
|
@@ -68,36 +69,14 @@ export function buildCodexInitialSessionState(input) {
|
|
|
68
69
|
state: 'idle',
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
|
-
export function buildCodexLiveSessionConfig(input) {
|
|
72
|
-
return {
|
|
73
|
-
...(input.model ? { model: input.model } : {}),
|
|
74
|
-
...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
|
|
75
|
-
...(input.effort ? { effort: input.effort } : {}),
|
|
76
|
-
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
77
|
-
executionMode: input.executionMode,
|
|
78
|
-
executionBranch: input.executionBranch ?? null,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
export function buildCodexServiceSnapshotConfig(input) {
|
|
82
|
-
// Keep every setup key present, including keys whose host value is
|
|
83
|
-
// undefined. `publishHostSessionSnapshots` merges this over any persisted
|
|
84
|
-
// coding config, so an absent host value clears rather than resurrects a
|
|
85
|
-
// stale member-selected value.
|
|
86
|
-
return {
|
|
87
|
-
model: input.model,
|
|
88
|
-
permissionMode: input.permissionMode,
|
|
89
|
-
effort: input.effort,
|
|
90
|
-
workspaceId: input.workspaceId,
|
|
91
|
-
executionMode: 'locked',
|
|
92
|
-
executionBranch: null,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
72
|
const MAX_SESSIONS = 12;
|
|
96
73
|
export function createCodexRecoveryCheckpointTracker(persist) {
|
|
97
74
|
return createRecoveryCheckpointTracker(persist);
|
|
98
75
|
}
|
|
99
76
|
// IDLE_TIMEOUT_MS (30 minutes) is shared with the other coding-agent hosts.
|
|
100
77
|
const HEARTBEAT_MS = 30_000;
|
|
78
|
+
const LOCAL_CONFIGURATION_REQUIRED_MESSAGE = 'This agent needs local runtime configuration before it can start this conversation. '
|
|
79
|
+
+ 'Its operator must configure it and retry.';
|
|
101
80
|
/** How Codex says it finished the work but Canon would not take the answer. */
|
|
102
81
|
const CODEX_UNDELIVERABLE_FINAL_WORDING = {
|
|
103
82
|
lead: 'The Codex host completed the turn, but Canon could not deliver the reply',
|
|
@@ -112,18 +91,13 @@ const CODEX_RUNTIME_CAPABILITIES = {
|
|
|
112
91
|
supportsNonFinalPermanentMessages: false,
|
|
113
92
|
};
|
|
114
93
|
let workingDir = process.cwd();
|
|
115
|
-
/**
|
|
116
|
-
* Agent-developer setting, resolved once at startup. Deliberately NOT read from
|
|
117
|
-
* `/session-config`: that path is the USER's per-conversation control plane,
|
|
118
|
-
* and owner ruling 5 puts turn verbosity outside user control.
|
|
119
|
-
*/
|
|
94
|
+
/** Turn verbosity is local agent-developer configuration, resolved at startup. */
|
|
120
95
|
let configuredTurnVerbosity = null;
|
|
121
96
|
let workspaceOptions = [];
|
|
122
97
|
let workspaceRoots = [];
|
|
123
98
|
let workspaceRootMetadata = [];
|
|
124
99
|
/** Conservative fallback used only when native app-server discovery is unavailable. */
|
|
125
100
|
export const CODEX_EFFORT_OPTIONS = FALLBACK_CODEX_EFFORT_OPTIONS;
|
|
126
|
-
export const CODEX_SESSION_CONFIG_FIELDS = ['permissionMode', 'effort'];
|
|
127
101
|
const MAX_CODEX_SKILL_COMMAND_CHOICES = 50;
|
|
128
102
|
export function buildCodexSkillCommands(skills) {
|
|
129
103
|
return skills
|
|
@@ -199,6 +173,7 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
199
173
|
workspaces: serviceAgentMode ? [] : input.workspaces,
|
|
200
174
|
workspaceRoots: serviceAgentMode ? undefined : input.workspaceRoots,
|
|
201
175
|
executionModes: serviceAgentMode ? [] : input.executionModes,
|
|
176
|
+
defaultExecutionMode: serviceAgentMode ? 'locked' : input.defaultExecutionMode,
|
|
202
177
|
permissionModes: serviceAgentMode ? [] : input.permissionModes,
|
|
203
178
|
defaultPermissionMode: serviceAgentMode ? undefined : input.defaultPermissionMode,
|
|
204
179
|
permissionModeLabel: 'Execution policy',
|
|
@@ -210,9 +185,6 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
210
185
|
effortLiveBehavior: 'next_turn',
|
|
211
186
|
presentation: input.presentation,
|
|
212
187
|
streamingTextMode: 'snapshot',
|
|
213
|
-
admissionActions: input.supportsCanonCommunicationTools && !serviceAgentMode
|
|
214
|
-
? { ...HOST_ADMISSION_ACTIONS_DISABLED, requestContact: true, reachOut: true }
|
|
215
|
-
: HOST_ADMISSION_ACTIONS_DISABLED,
|
|
216
188
|
...(input.supportsPlanMode && !serviceAgentMode
|
|
217
189
|
? {
|
|
218
190
|
turnModes: [
|
|
@@ -284,29 +256,22 @@ export function getCodexRequestingUserId(message) {
|
|
|
284
256
|
async function publishAgentRuntime(agentId, runtime, rtdb) {
|
|
285
257
|
await publishHostAgentRuntime(agentId, 'codex', runtime, rtdb);
|
|
286
258
|
}
|
|
287
|
-
|
|
288
|
-
return loadHostSessionConfig({
|
|
289
|
-
conversationId,
|
|
290
|
-
agentId,
|
|
291
|
-
rtdb,
|
|
292
|
-
extraStringFields: CODEX_SESSION_CONFIG_FIELDS,
|
|
293
|
-
retryMissingMs: retryMissing ? 3_000 : 0,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
export function resolveSessionExecutionMode(config, serviceAgentMode = false) {
|
|
259
|
+
export function resolveSessionExecutionMode(serviceAgentMode = false, defaultExecutionMode = 'worktree') {
|
|
297
260
|
if (serviceAgentMode)
|
|
298
261
|
return 'locked';
|
|
299
|
-
|
|
300
|
-
return config.executionMode;
|
|
301
|
-
throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
|
|
262
|
+
return defaultExecutionMode;
|
|
302
263
|
}
|
|
303
|
-
|
|
304
|
-
|
|
264
|
+
function resolveConfiguredDefaultExecutionMode(value) {
|
|
265
|
+
if (value == null || value === '')
|
|
266
|
+
return 'worktree';
|
|
267
|
+
if (value === 'worktree' || value === 'locked')
|
|
268
|
+
return value;
|
|
269
|
+
throw new Error('--default-execution-mode must be worktree or locked');
|
|
305
270
|
}
|
|
306
|
-
export function resolveWorkspaceCwd(
|
|
307
|
-
return
|
|
271
|
+
export function resolveWorkspaceCwd() {
|
|
272
|
+
return resolveConfiguredWorkspaceCwd({
|
|
308
273
|
workspaceOptions,
|
|
309
|
-
|
|
274
|
+
workspaceId: workspaceOptions[0]?.id,
|
|
310
275
|
defaultCwd: workingDir,
|
|
311
276
|
});
|
|
312
277
|
}
|
|
@@ -377,8 +342,8 @@ function buildCanonPrompt(input) {
|
|
|
377
342
|
message: input.message,
|
|
378
343
|
})), input.noReplyToolName ? { noReplyToolName: input.noReplyToolName } : {});
|
|
379
344
|
}
|
|
380
|
-
function renderInboundContent(message, materialized
|
|
381
|
-
return renderCanonHostInboundContent(message, materialized
|
|
345
|
+
function renderInboundContent(message, materialized) {
|
|
346
|
+
return renderCanonHostInboundContent(message, materialized);
|
|
382
347
|
}
|
|
383
348
|
async function materializePromptReplyContext(input) {
|
|
384
349
|
if (!input.replyContext?.found || !input.replyContext.attachments?.length) {
|
|
@@ -395,16 +360,6 @@ async function materializePromptReplyContext(input) {
|
|
|
395
360
|
return { replyContext: input.replyContext, materialized: [] };
|
|
396
361
|
}
|
|
397
362
|
}
|
|
398
|
-
function ownerBoundReplyContactTarget(replyContext) {
|
|
399
|
-
const card = replyContext?.found ? replyContext.contactCard : undefined;
|
|
400
|
-
if (!card?.userId)
|
|
401
|
-
return undefined;
|
|
402
|
-
return {
|
|
403
|
-
targetUserId: card.userId,
|
|
404
|
-
...(card.canonContactId ? { canonContactId: card.canonContactId } : {}),
|
|
405
|
-
sourceCardMessageId: replyContext.messageId,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
363
|
function summarizeCommand(command) {
|
|
409
364
|
const trimmed = command.trim();
|
|
410
365
|
if (!trimmed)
|
|
@@ -650,6 +605,7 @@ export async function main() {
|
|
|
650
605
|
'add-dir': { type: 'string', multiple: true },
|
|
651
606
|
workspace: { type: 'string', multiple: true },
|
|
652
607
|
'workspace-root': { type: 'string', multiple: true },
|
|
608
|
+
'default-execution-mode': { type: 'string' },
|
|
653
609
|
config: { type: 'string', multiple: true },
|
|
654
610
|
'codex-bin': { type: 'string' },
|
|
655
611
|
'runtime-visibility': { type: 'string' },
|
|
@@ -674,8 +630,11 @@ export async function main() {
|
|
|
674
630
|
onWarning: (message) => console.error(`[canon-codex] ${message}`),
|
|
675
631
|
});
|
|
676
632
|
const serviceAgentMode = args['service-agent'] === true;
|
|
633
|
+
const defaultExecutionMode = serviceAgentMode
|
|
634
|
+
? 'locked'
|
|
635
|
+
: resolveConfiguredDefaultExecutionMode(args['default-execution-mode']);
|
|
677
636
|
const nativeVisionEnabled = args['no-native-vision'] !== true;
|
|
678
|
-
const
|
|
637
|
+
const baseCodexDynamicTools = serviceAgentMode
|
|
679
638
|
? CODEX_SERVICE_AGENT_DYNAMIC_TOOLS
|
|
680
639
|
: CODEX_APP_DYNAMIC_TOOLS;
|
|
681
640
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
@@ -720,11 +679,13 @@ export async function main() {
|
|
|
720
679
|
let agentId;
|
|
721
680
|
let ownerId = null;
|
|
722
681
|
let ownerName = null;
|
|
682
|
+
let outboundPolicy = null;
|
|
723
683
|
try {
|
|
724
684
|
const ctx = await client.getAgentMe();
|
|
725
685
|
agentId = ctx.agentId;
|
|
726
686
|
ownerId = ctx.ownerId;
|
|
727
687
|
ownerName = ctx.ownerName;
|
|
688
|
+
outboundPolicy = ctx.outboundPolicy;
|
|
728
689
|
console.error(`[canon-codex] Connected as ${ctx.displayName || agentId}`);
|
|
729
690
|
}
|
|
730
691
|
catch {
|
|
@@ -737,6 +698,7 @@ export async function main() {
|
|
|
737
698
|
}
|
|
738
699
|
console.error(`[canon-codex] Authenticated as ${agentId}`);
|
|
739
700
|
}
|
|
701
|
+
let codexDynamicTools = filterCodexCommunicationTools(baseCodexDynamicTools, outboundPolicy);
|
|
740
702
|
// Shared poll/timeout engine. Built-in `input`/`card` descriptors own
|
|
741
703
|
// create+poll (codex passes native/responder policy via payload/options).
|
|
742
704
|
// Approval keeps codex's own resolution shape (no owner-authored outcome
|
|
@@ -927,34 +889,11 @@ export async function main() {
|
|
|
927
889
|
ownerName,
|
|
928
890
|
membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
|
|
929
891
|
groupContextMode: getGroupContextMode(input.conversationId, conversation),
|
|
930
|
-
renderOptions: input.renderOptions,
|
|
931
892
|
});
|
|
932
893
|
}
|
|
933
894
|
function writeState(session) {
|
|
934
|
-
const appliedAt = Date.now();
|
|
935
|
-
const controlState = {};
|
|
936
|
-
if (session.state.model !== undefined) {
|
|
937
|
-
controlState.model = { value: session.state.model, source: 'applied', appliedAt };
|
|
938
|
-
}
|
|
939
|
-
if (session.state.permissionMode !== undefined) {
|
|
940
|
-
controlState.permissionMode = { value: session.state.permissionMode, source: 'applied', appliedAt };
|
|
941
|
-
}
|
|
942
|
-
if (session.state.effort !== undefined) {
|
|
943
|
-
controlState.effort = { value: session.state.effort, source: 'applied', appliedAt };
|
|
944
|
-
}
|
|
945
895
|
runtimeState.writeSessionState(session.conversationId, {
|
|
946
896
|
lastError: session.state.lastError,
|
|
947
|
-
model: session.state.model,
|
|
948
|
-
permissionMode: session.state.permissionMode,
|
|
949
|
-
effort: session.state.effort,
|
|
950
|
-
controlState,
|
|
951
|
-
cwd: session.cwd,
|
|
952
|
-
executionMode: session.environment.mode,
|
|
953
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
954
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
955
|
-
...(resolveExecutionFallbackReason(session.environment)
|
|
956
|
-
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
957
|
-
: {}),
|
|
958
897
|
hostMode: true,
|
|
959
898
|
clientType: 'codex',
|
|
960
899
|
isActive: true,
|
|
@@ -1225,7 +1164,7 @@ export async function main() {
|
|
|
1225
1164
|
session.currentTurnOpenedAt = null;
|
|
1226
1165
|
session.currentTurnUpdatedAt = null;
|
|
1227
1166
|
session.currentTurnCanUseCodexAppTools = false;
|
|
1228
|
-
session.
|
|
1167
|
+
session.currentReplyAuthority = null;
|
|
1229
1168
|
session.currentTurnSilenced = false;
|
|
1230
1169
|
session.lastAcceptedIntent = null;
|
|
1231
1170
|
session.resetRequested = false;
|
|
@@ -1263,20 +1202,47 @@ export async function main() {
|
|
|
1263
1202
|
evictOldestIdle();
|
|
1264
1203
|
}
|
|
1265
1204
|
const creation = (async () => {
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
const environment = prepareConversationEnvironment({
|
|
1205
|
+
const sessionExecutionMode = resolveSessionExecutionMode(serviceAgentMode, defaultExecutionMode);
|
|
1206
|
+
const workspaceCwd = resolveWorkspaceCwd();
|
|
1207
|
+
let environment = prepareConversationEnvironment({
|
|
1270
1208
|
agentId,
|
|
1271
1209
|
conversationId,
|
|
1272
1210
|
workspaceCwd,
|
|
1273
1211
|
allowWorktrees: sessionExecutionMode === 'worktree',
|
|
1274
1212
|
});
|
|
1275
1213
|
try {
|
|
1214
|
+
const persistedMapping = resolveLocalRuntimeSessionState(runtimeId, {
|
|
1215
|
+
conversationId,
|
|
1216
|
+
baseCwd: environment.baseCwd,
|
|
1217
|
+
executionMode: environment.mode,
|
|
1218
|
+
resumeField: 'threadId',
|
|
1219
|
+
configuredBaseCwds: workspaceOptions.map((workspace) => workspace.cwd),
|
|
1220
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
1221
|
+
});
|
|
1222
|
+
if (persistedMapping.status === 'configuration_required') {
|
|
1223
|
+
throw new ExecutionEnvironmentError(persistedMapping.message, LOCAL_CONFIGURATION_REQUIRED_MESSAGE);
|
|
1224
|
+
}
|
|
1225
|
+
if (persistedMapping.status === 'adopted'
|
|
1226
|
+
&& (persistedMapping.state.baseCwd !== environment.baseCwd
|
|
1227
|
+
|| persistedMapping.state.executionMode !== environment.mode)) {
|
|
1228
|
+
const restoredEnvironment = prepareConversationEnvironment({
|
|
1229
|
+
agentId,
|
|
1230
|
+
conversationId,
|
|
1231
|
+
workspaceCwd: persistedMapping.state.baseCwd,
|
|
1232
|
+
allowWorktrees: persistedMapping.state.executionMode === 'worktree',
|
|
1233
|
+
});
|
|
1234
|
+
if (restoredEnvironment.mode !== persistedMapping.state.executionMode) {
|
|
1235
|
+
releaseConversationEnvironment(restoredEnvironment);
|
|
1236
|
+
throw new ExecutionEnvironmentError(`Conversation ${conversationId} requires local execution mode ${persistedMapping.state.executionMode}, but workspace ${persistedMapping.state.baseCwd} can only be opened in ${restoredEnvironment.mode} mode.`, LOCAL_CONFIGURATION_REQUIRED_MESSAGE);
|
|
1237
|
+
}
|
|
1238
|
+
releaseConversationEnvironment(environment);
|
|
1239
|
+
environment = restoredEnvironment;
|
|
1240
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Restoring saved local runtime → ${environment.mode} (${environment.baseCwd})`);
|
|
1241
|
+
}
|
|
1276
1242
|
const sessionCwd = environment.cwd;
|
|
1277
1243
|
const policy = resolveCodexEffectiveRuntimePolicy({
|
|
1278
1244
|
args,
|
|
1279
|
-
config,
|
|
1245
|
+
config: null,
|
|
1280
1246
|
permissionEnvelope: codexPermissionEnvelope,
|
|
1281
1247
|
environment,
|
|
1282
1248
|
serviceAgentMode,
|
|
@@ -1285,12 +1251,17 @@ export async function main() {
|
|
|
1285
1251
|
if (modelGuard) {
|
|
1286
1252
|
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
1287
1253
|
}
|
|
1288
|
-
const storedThreadId = loadStoredThreadId(runtimeId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint
|
|
1254
|
+
const storedThreadId = loadStoredThreadId(runtimeId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint, {
|
|
1255
|
+
...(persistedMapping.status === 'new' || !persistedMapping.state.workspaceId
|
|
1256
|
+
? {}
|
|
1257
|
+
: { workspaceId: persistedMapping.state.workspaceId }),
|
|
1258
|
+
allowLegacyPolicyMigration: persistedMapping.status !== 'new',
|
|
1259
|
+
});
|
|
1289
1260
|
const effectiveModel = policy.model ?? codexDefaultModel;
|
|
1290
1261
|
const initialEffortResolution = resolveCodexEffortForModel({
|
|
1291
1262
|
models: codexModels,
|
|
1292
1263
|
model: effectiveModel,
|
|
1293
|
-
requestedEffort:
|
|
1264
|
+
requestedEffort: configuredCodexEffort,
|
|
1294
1265
|
});
|
|
1295
1266
|
const initialEffort = initialEffortResolution.value;
|
|
1296
1267
|
const adapter = useAppServer
|
|
@@ -1338,8 +1309,7 @@ export async function main() {
|
|
|
1338
1309
|
currentTurnOpenedAt: null,
|
|
1339
1310
|
currentTurnUpdatedAt: null,
|
|
1340
1311
|
currentTurnCanUseCodexAppTools: false,
|
|
1341
|
-
|
|
1342
|
-
currentTurnReplyContactTarget: null,
|
|
1312
|
+
currentReplyAuthority: null,
|
|
1343
1313
|
currentTurnAbortController: null,
|
|
1344
1314
|
// Corrected by the first turn that runs; a session with no turn
|
|
1345
1315
|
// publishes nothing anyway, and quiet is never an accident.
|
|
@@ -1357,6 +1327,10 @@ export async function main() {
|
|
|
1357
1327
|
};
|
|
1358
1328
|
sessions.set(conversationId, session);
|
|
1359
1329
|
await controlPoller.baseline([conversationId]);
|
|
1330
|
+
await runtimeState.patchAgentSessionSnapshot(conversationId, {
|
|
1331
|
+
configurationStatus: 'ready',
|
|
1332
|
+
lastError: null,
|
|
1333
|
+
});
|
|
1360
1334
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Environment → ${environment.mode} (${sessionCwd})`);
|
|
1361
1335
|
writeState(session);
|
|
1362
1336
|
writeTurn(session);
|
|
@@ -1386,10 +1360,9 @@ export async function main() {
|
|
|
1386
1360
|
planMode,
|
|
1387
1361
|
artifactRoutingMode: turn.artifactRoutingMode ?? 'disabled',
|
|
1388
1362
|
canUseCodexAppTools: turn.canUseCodexAppTools ?? false,
|
|
1389
|
-
canUseCodexCanonTools: turn.canUseCodexCanonTools ?? false,
|
|
1390
1363
|
...(turn.turnVerbosity ? { turnVerbosity: turn.turnVerbosity } : {}),
|
|
1391
1364
|
requestingUserId: turn.requestingUserId ?? null,
|
|
1392
|
-
|
|
1365
|
+
replyAuthority: turn.replyAuthority ?? null,
|
|
1393
1366
|
};
|
|
1394
1367
|
if (toFront) {
|
|
1395
1368
|
session.queue.unshift(nextPrompt);
|
|
@@ -1420,38 +1393,6 @@ export async function main() {
|
|
|
1420
1393
|
requestingUserId: responseUserId,
|
|
1421
1394
|
});
|
|
1422
1395
|
}
|
|
1423
|
-
function recordContactLifecycleEvent(request) {
|
|
1424
|
-
if (request.requesterId !== agentId || !request.sourceConversationId)
|
|
1425
|
-
return false;
|
|
1426
|
-
return recordLocalRuntimeContactLifecycleEvent(runtimeId, request);
|
|
1427
|
-
}
|
|
1428
|
-
async function reconcileContactLifecycleInbox() {
|
|
1429
|
-
let cursor = readLocalRuntimeEntry(runtimeId)?.contactLifecycleCursor ?? null;
|
|
1430
|
-
let recorded = 0;
|
|
1431
|
-
let duplicate = 0;
|
|
1432
|
-
let ignored = 0;
|
|
1433
|
-
for (let pageNumber = 0; pageNumber < 10; pageNumber += 1) {
|
|
1434
|
-
const page = await client.listContactRequestLifecyclePage({
|
|
1435
|
-
cursor,
|
|
1436
|
-
limit: 100,
|
|
1437
|
-
});
|
|
1438
|
-
const result = await reconcileContactLifecycleEvents({
|
|
1439
|
-
requests: page.requests,
|
|
1440
|
-
requesterId: agentId,
|
|
1441
|
-
record: (request) => recordLocalRuntimeContactLifecycleEvent(runtimeId, request),
|
|
1442
|
-
});
|
|
1443
|
-
recorded += result.recorded;
|
|
1444
|
-
duplicate += result.duplicate;
|
|
1445
|
-
ignored += result.ignored;
|
|
1446
|
-
cursor = page.nextCursor;
|
|
1447
|
-
saveLocalRuntimeContactLifecycleCursor(runtimeId, cursor);
|
|
1448
|
-
if (!page.hasMore)
|
|
1449
|
-
break;
|
|
1450
|
-
}
|
|
1451
|
-
if (recorded > 0) {
|
|
1452
|
-
console.error(`[canon-codex] Contact lifecycle recovery: recorded=${recorded} duplicate=${duplicate} ignored=${ignored}`);
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
1396
|
function resolveArtifactRoutingMode(participantContext) {
|
|
1456
1397
|
return participantContext.conversationType === 'direct' && participantContext.isOwner
|
|
1457
1398
|
? 'workspace-generated'
|
|
@@ -1463,19 +1404,15 @@ export async function main() {
|
|
|
1463
1404
|
* so a prompt that waits behind another still runs under the answer it
|
|
1464
1405
|
* arrived with.
|
|
1465
1406
|
*/
|
|
1466
|
-
function resolveCodexTurnModes(participantContext, message
|
|
1407
|
+
function resolveCodexTurnModes(participantContext, message) {
|
|
1467
1408
|
return {
|
|
1468
1409
|
artifactRoutingMode: resolveArtifactRoutingMode(participantContext),
|
|
1469
1410
|
canUseCodexAppTools: participantContext.isOwner || serviceAgentMode,
|
|
1470
|
-
canUseCodexCanonTools: participantContext.isOwner && !serviceAgentMode,
|
|
1471
1411
|
turnVerbosity: resolveTurnVerbosity({
|
|
1472
1412
|
configured: configuredTurnVerbosity,
|
|
1473
1413
|
conversationType: participantContext.conversationType,
|
|
1474
1414
|
}),
|
|
1475
1415
|
requestingUserId: getCodexRequestingUserId(message),
|
|
1476
|
-
...(participantContext.isOwner && replyContext
|
|
1477
|
-
? { replyContactTarget: ownerBoundReplyContactTarget(replyContext) }
|
|
1478
|
-
: {}),
|
|
1479
1416
|
};
|
|
1480
1417
|
}
|
|
1481
1418
|
function runtimeCardRequestPayload(method, params) {
|
|
@@ -1492,6 +1429,15 @@ export async function main() {
|
|
|
1492
1429
|
const params = request.params;
|
|
1493
1430
|
const expiresAt = Date.now() + 30 * 60_000;
|
|
1494
1431
|
if (request.method === 'item/tool/call' && isCodexAppToolCall(params)) {
|
|
1432
|
+
if (isCodexCommunicateToolCall(params)) {
|
|
1433
|
+
if (!(session.adapter instanceof CodexAppServerAdapter)) {
|
|
1434
|
+
return deniedCodexAppToolResult('This Codex transport does not support dynamic tools.');
|
|
1435
|
+
}
|
|
1436
|
+
if (outboundPolicy !== 'open' && outboundPolicy !== 'approval-required') {
|
|
1437
|
+
return deniedCodexAppToolResult('Outbound communication is not enabled by the agent operator.');
|
|
1438
|
+
}
|
|
1439
|
+
return handleCodexCommunicateToolCall(client, params);
|
|
1440
|
+
}
|
|
1495
1441
|
if (serviceAgentMode && !isCodexServiceAgentToolCall(params)) {
|
|
1496
1442
|
return deniedCodexAppToolResult('This service agent exposes only Canon conversation tools.');
|
|
1497
1443
|
}
|
|
@@ -1517,23 +1463,6 @@ export async function main() {
|
|
|
1517
1463
|
if (disposition === 'denied-non-owner') {
|
|
1518
1464
|
return deniedCodexAppToolResult('Only the Canon owner can use codex_app tools.');
|
|
1519
1465
|
}
|
|
1520
|
-
if (isCodexCanonVerbToolCall(params)) {
|
|
1521
|
-
if (!session.currentTurnCanUseCodexCanonTools) {
|
|
1522
|
-
return deniedCodexAppToolResult('Canon contact and conversation tools require a real owner-authored inbound turn.');
|
|
1523
|
-
}
|
|
1524
|
-
return await answerCodexCanonVerb({
|
|
1525
|
-
client,
|
|
1526
|
-
params,
|
|
1527
|
-
context: {
|
|
1528
|
-
conversationId: session.conversationId,
|
|
1529
|
-
sourceMessageId,
|
|
1530
|
-
turnId: session.currentTurnId,
|
|
1531
|
-
...(session.currentTurnReplyContactTarget
|
|
1532
|
-
? { replyContactTarget: session.currentTurnReplyContactTarget }
|
|
1533
|
-
: {}),
|
|
1534
|
-
},
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
1466
|
if (isCanonRuntimeControlToolCall(params)) {
|
|
1538
1467
|
const command = parseCanonRuntimeControlRequest(params);
|
|
1539
1468
|
if (!command) {
|
|
@@ -1641,6 +1570,8 @@ export async function main() {
|
|
|
1641
1570
|
currentThreadId: session.adapter.getThreadId(),
|
|
1642
1571
|
currentCwd: session.cwd,
|
|
1643
1572
|
workspaces: workspaceOptions,
|
|
1573
|
+
communicationEnabled: outboundPolicy === 'open'
|
|
1574
|
+
|| outboundPolicy === 'approval-required',
|
|
1644
1575
|
model: session.state.model ?? null,
|
|
1645
1576
|
effort: session.state.effort ?? null,
|
|
1646
1577
|
}, params);
|
|
@@ -1873,8 +1804,7 @@ export async function main() {
|
|
|
1873
1804
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
1874
1805
|
}
|
|
1875
1806
|
}
|
|
1876
|
-
const
|
|
1877
|
-
const renderedContent = renderInboundContent(input.message, materialized, renderOptions);
|
|
1807
|
+
const renderedContent = renderInboundContent(input.message, materialized);
|
|
1878
1808
|
const turnMetadata = normalizeTurnMetadata(input.message.metadata);
|
|
1879
1809
|
const requestedPlanMode = turnMetadata?.requestedTurnMode === 'plan';
|
|
1880
1810
|
const planCommand = resolveCodexPlanCommand({
|
|
@@ -1893,7 +1823,6 @@ export async function main() {
|
|
|
1893
1823
|
selfContexts: input.selfContexts,
|
|
1894
1824
|
provenance: input.provenance,
|
|
1895
1825
|
hydratedPage: input.hydratedPage,
|
|
1896
|
-
renderOptions,
|
|
1897
1826
|
});
|
|
1898
1827
|
const behavior = input.behavior ?? hydrated.behavior;
|
|
1899
1828
|
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
@@ -1933,22 +1862,28 @@ export async function main() {
|
|
|
1933
1862
|
}
|
|
1934
1863
|
catch (error) {
|
|
1935
1864
|
const message = error instanceof Error ? error.message : String(error);
|
|
1936
|
-
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
1937
1865
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
1866
|
+
await runtimeState.patchAgentSessionSnapshot(input.conversationId, {
|
|
1867
|
+
configurationStatus: 'configuration_required',
|
|
1868
|
+
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
1869
|
+
}).catch(() => { });
|
|
1938
1870
|
await markQueuedMessageAccepted(input.conversationId, input.message.id, shouldMarkAccepted);
|
|
1939
|
-
await sendMessageWithRetryChunked(client, input.conversationId,
|
|
1871
|
+
await sendMessageWithRetryChunked(client, input.conversationId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
1940
1872
|
messageId: `codex-start-failed-${input.message.id}`,
|
|
1941
1873
|
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
1942
1874
|
metadata: {
|
|
1875
|
+
turnId: `codex-start:${input.message.id}`,
|
|
1876
|
+
runtimeStatus: 'configuration_required',
|
|
1943
1877
|
turnSemantics: 'turn_complete',
|
|
1944
1878
|
replyBehavior: 'suppress_auto_reply',
|
|
1945
1879
|
},
|
|
1880
|
+
...(input.replyAuthority ? { replyAuthority: input.replyAuthority } : {}),
|
|
1946
1881
|
}).catch(() => { });
|
|
1947
1882
|
recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
|
|
1948
1883
|
return;
|
|
1949
1884
|
}
|
|
1950
1885
|
session.activeSelfContextId = activeSelfContextId;
|
|
1951
|
-
const
|
|
1886
|
+
const prompt = buildCanonPrompt({
|
|
1952
1887
|
content,
|
|
1953
1888
|
conversationId: input.conversationId,
|
|
1954
1889
|
participantContext,
|
|
@@ -1960,12 +1895,11 @@ export async function main() {
|
|
|
1960
1895
|
message: input.message,
|
|
1961
1896
|
...(useAppServer ? { noReplyToolName: CODEX_NO_REPLY_MODEL_TOOL_NAME } : {}),
|
|
1962
1897
|
});
|
|
1963
|
-
const lifecycleContext = input.isOwner
|
|
1964
|
-
? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, input.conversationId))
|
|
1965
|
-
: null;
|
|
1966
|
-
const prompt = lifecycleContext ? `${basePrompt}\n\n${lifecycleContext}` : basePrompt;
|
|
1967
1898
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
1968
|
-
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode,
|
|
1899
|
+
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, {
|
|
1900
|
+
...resolveCodexTurnModes(participantContext, input.message),
|
|
1901
|
+
replyAuthority: input.replyAuthority ?? null,
|
|
1902
|
+
});
|
|
1969
1903
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
1970
1904
|
session.currentTurnAbortController?.abort(new Error('Codex turn interrupted by a newer message'));
|
|
1971
1905
|
await session.adapter.interrupt().catch(() => { });
|
|
@@ -1973,10 +1907,14 @@ export async function main() {
|
|
|
1973
1907
|
typingSignals.clear(input.conversationId).catch(() => { });
|
|
1974
1908
|
return;
|
|
1975
1909
|
}
|
|
1976
|
-
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode,
|
|
1910
|
+
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, {
|
|
1911
|
+
...resolveCodexTurnModes(participantContext, input.message),
|
|
1912
|
+
replyAuthority: input.replyAuthority ?? null,
|
|
1913
|
+
});
|
|
1977
1914
|
}
|
|
1978
1915
|
function sendTurnArtifactFile(session, file) {
|
|
1979
1916
|
return sendMediaFileMessage(client, session.conversationId, file.path, '', {
|
|
1917
|
+
...(session.currentReplyAuthority ? { replyAuthority: session.currentReplyAuthority } : {}),
|
|
1980
1918
|
...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
|
|
1981
1919
|
metadata: {
|
|
1982
1920
|
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
@@ -2004,8 +1942,7 @@ export async function main() {
|
|
|
2004
1942
|
session.currentTurnOpenedAt = Date.now();
|
|
2005
1943
|
session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
|
|
2006
1944
|
session.currentTurnCanUseCodexAppTools = nextTurn.canUseCodexAppTools === true;
|
|
2007
|
-
session.
|
|
2008
|
-
session.currentTurnReplyContactTarget = nextTurn.replyContactTarget ?? null;
|
|
1945
|
+
session.currentReplyAuthority = nextTurn.replyAuthority;
|
|
2009
1946
|
session.currentTurnAbortController = new AbortController();
|
|
2010
1947
|
// A continuation prompt (a plan-review result) carries none, and keeps the
|
|
2011
1948
|
// conversation's last answer rather than silently reverting to verbose.
|
|
@@ -2310,6 +2247,9 @@ export async function main() {
|
|
|
2310
2247
|
const turnTrail = buildFinalTurnTrail(session);
|
|
2311
2248
|
await sendMessageWithRetryChunked(client, session.conversationId, result.finalMessage, {
|
|
2312
2249
|
messageId: buildCodexMessageId(session, 'final'),
|
|
2250
|
+
...(session.currentReplyAuthority
|
|
2251
|
+
? { replyAuthority: session.currentReplyAuthority }
|
|
2252
|
+
: {}),
|
|
2313
2253
|
...(session.activeSelfContextId
|
|
2314
2254
|
? { selfContextId: session.activeSelfContextId }
|
|
2315
2255
|
: {}),
|
|
@@ -2335,6 +2275,9 @@ export async function main() {
|
|
|
2335
2275
|
const turnTrail = buildFinalTurnTrail(session);
|
|
2336
2276
|
await sendMessageWithRetryChunked(client, session.conversationId, userVisibleError, {
|
|
2337
2277
|
messageId: buildCodexMessageId(session, 'error'),
|
|
2278
|
+
...(session.currentReplyAuthority
|
|
2279
|
+
? { replyAuthority: session.currentReplyAuthority }
|
|
2280
|
+
: {}),
|
|
2338
2281
|
...(session.activeSelfContextId
|
|
2339
2282
|
? { selfContextId: session.activeSelfContextId }
|
|
2340
2283
|
: {}),
|
|
@@ -2391,6 +2334,9 @@ export async function main() {
|
|
|
2391
2334
|
await routeArtifactsOnce();
|
|
2392
2335
|
await sendMessageWithRetryChunked(client, session.conversationId, message, {
|
|
2393
2336
|
messageId: buildCodexMessageId(session, 'failure'),
|
|
2337
|
+
...(session.currentReplyAuthority
|
|
2338
|
+
? { replyAuthority: session.currentReplyAuthority }
|
|
2339
|
+
: {}),
|
|
2394
2340
|
...(session.activeSelfContextId
|
|
2395
2341
|
? { selfContextId: session.activeSelfContextId }
|
|
2396
2342
|
: {}),
|
|
@@ -2419,8 +2365,7 @@ export async function main() {
|
|
|
2419
2365
|
session.currentTurnOpenedAt = null;
|
|
2420
2366
|
session.currentTurnUpdatedAt = null;
|
|
2421
2367
|
session.currentTurnCanUseCodexAppTools = false;
|
|
2422
|
-
session.
|
|
2423
|
-
session.currentTurnReplyContactTarget = null;
|
|
2368
|
+
session.currentReplyAuthority = null;
|
|
2424
2369
|
session.currentTurnSilenced = false;
|
|
2425
2370
|
session.lastAcceptedIntent = null;
|
|
2426
2371
|
session.resetRequested = false;
|
|
@@ -2479,6 +2424,7 @@ export async function main() {
|
|
|
2479
2424
|
});
|
|
2480
2425
|
let codexSkills = [];
|
|
2481
2426
|
const buildCurrentRuntimeDescriptor = () => ({
|
|
2427
|
+
defaultExecutionMode,
|
|
2482
2428
|
...(serviceAgentMode
|
|
2483
2429
|
? {}
|
|
2484
2430
|
: {
|
|
@@ -2498,31 +2444,18 @@ export async function main() {
|
|
|
2498
2444
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2499
2445
|
workspaceRoots: workspaceRootMetadata,
|
|
2500
2446
|
executionModes: hostAvailableExecutionModes,
|
|
2447
|
+
defaultExecutionMode,
|
|
2501
2448
|
permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
2502
2449
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
2503
2450
|
presentation: runtimePresentation,
|
|
2504
2451
|
supportsPlanMode: useAppServer,
|
|
2505
2452
|
supportsCompact: useAppServer,
|
|
2506
2453
|
supportsRichCards: useAppServer,
|
|
2507
|
-
supportsCanonCommunicationTools: useAppServer,
|
|
2508
2454
|
skills: codexSkills,
|
|
2509
2455
|
serviceAgentMode,
|
|
2510
2456
|
}),
|
|
2511
2457
|
});
|
|
2512
2458
|
let runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
2513
|
-
const resolveInboundContentRenderOptions = (isOwnerTurn) => {
|
|
2514
|
-
const descriptorActions = runtimeDescriptor.runtimeDescriptor?.admissionActions
|
|
2515
|
-
?? HOST_ADMISSION_ACTIONS_DISABLED;
|
|
2516
|
-
const admissionActions = isOwnerTurn && useAppServer && !serviceAgentMode
|
|
2517
|
-
? descriptorActions
|
|
2518
|
-
: HOST_ADMISSION_ACTIONS_DISABLED;
|
|
2519
|
-
return {
|
|
2520
|
-
admissionActions,
|
|
2521
|
-
...(admissionActions.reachOut
|
|
2522
|
-
? { reachOutToolName: 'codex_app.canon_send_to' }
|
|
2523
|
-
: {}),
|
|
2524
|
-
};
|
|
2525
|
-
};
|
|
2526
2459
|
async function refreshCodexSkillInventory(forceReload = false) {
|
|
2527
2460
|
if (!useAppServer)
|
|
2528
2461
|
return;
|
|
@@ -2561,64 +2494,6 @@ export async function main() {
|
|
|
2561
2494
|
probe.close();
|
|
2562
2495
|
}
|
|
2563
2496
|
}
|
|
2564
|
-
function applySessionControl(conversationId, control) {
|
|
2565
|
-
const session = sessions.get(conversationId);
|
|
2566
|
-
if (!session || session.closed)
|
|
2567
|
-
return;
|
|
2568
|
-
if (serviceAgentMode) {
|
|
2569
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring user session controls for service-agent runtime`);
|
|
2570
|
-
writeState(session);
|
|
2571
|
-
return;
|
|
2572
|
-
}
|
|
2573
|
-
let modelChanged = false;
|
|
2574
|
-
if (control.model && control.model !== session.state.model) {
|
|
2575
|
-
if (codexModelOptions.length > 0 && !codexModelOptions.some((option) => option.value === control.model)) {
|
|
2576
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring model outside the discovered Codex catalog (${control.model})`);
|
|
2577
|
-
writeState(session);
|
|
2578
|
-
return;
|
|
2579
|
-
}
|
|
2580
|
-
const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
|
|
2581
|
-
if (modelGuard) {
|
|
2582
|
-
session.state.lastError = modelGuard;
|
|
2583
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${modelGuard}`);
|
|
2584
|
-
writeState(session);
|
|
2585
|
-
// The poller consumes the node; skip effort handling for this pass,
|
|
2586
|
-
// matching the legacy loop.
|
|
2587
|
-
return;
|
|
2588
|
-
}
|
|
2589
|
-
session.adapter.setModel(control.model);
|
|
2590
|
-
session.state.model = control.model;
|
|
2591
|
-
modelChanged = true;
|
|
2592
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
|
|
2593
|
-
}
|
|
2594
|
-
if (control.permissionMode) {
|
|
2595
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] approval mode is session-creation-only; ignoring mid-session change request (${control.permissionMode})`);
|
|
2596
|
-
// Convergence contract: a consumed session control must always be
|
|
2597
|
-
// answered. Re-publish the currently applied state so clients settle on
|
|
2598
|
-
// the authoritative value instead of holding the composer until timeout.
|
|
2599
|
-
}
|
|
2600
|
-
if (control.effort || modelChanged) {
|
|
2601
|
-
const effortResolution = resolveCodexEffortForModel({
|
|
2602
|
-
models: codexModels,
|
|
2603
|
-
model: session.state.model,
|
|
2604
|
-
requestedEffort: control.effort ?? session.state.effort,
|
|
2605
|
-
});
|
|
2606
|
-
if (effortResolution.value !== session.state.effort) {
|
|
2607
|
-
session.adapter.setReasoningEffort(effortResolution.value);
|
|
2608
|
-
session.state.effort = effortResolution.value ?? undefined;
|
|
2609
|
-
}
|
|
2610
|
-
if (control.effort && !effortResolution.accepted) {
|
|
2611
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${control.effort} is unsupported by ${session.state.model ?? 'the active model'}; reset to ${effortResolution.value ?? 'the model default'}`);
|
|
2612
|
-
}
|
|
2613
|
-
else if (control.effort) {
|
|
2614
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Reasoning effort set for next turn -> ${control.effort}`);
|
|
2615
|
-
}
|
|
2616
|
-
else if (modelChanged && effortResolution.value) {
|
|
2617
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Reasoning effort for ${session.state.model} -> ${effortResolution.value}`);
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
writeState(session);
|
|
2621
|
-
}
|
|
2622
2497
|
async function handleControlSignal(event) {
|
|
2623
2498
|
const { conversationId, type } = event;
|
|
2624
2499
|
const session = sessions.get(conversationId);
|
|
@@ -2684,9 +2559,6 @@ export async function main() {
|
|
|
2684
2559
|
conversationIds: () => sessions.keys(),
|
|
2685
2560
|
hasActiveWork: () => [...sessions.values()].some((session) => !session.closed
|
|
2686
2561
|
&& (session.running || session.queue.length > 0 || session.turnState === 'waiting_input')),
|
|
2687
|
-
onSessionControl: ({ conversationId, control }) => {
|
|
2688
|
-
applySessionControl(conversationId, control);
|
|
2689
|
-
},
|
|
2690
2562
|
onSignal: handleControlSignal,
|
|
2691
2563
|
onPrimitive: handleControlPrimitive,
|
|
2692
2564
|
onError: (error) => {
|
|
@@ -2722,41 +2594,6 @@ export async function main() {
|
|
|
2722
2594
|
agentId,
|
|
2723
2595
|
rtdb,
|
|
2724
2596
|
clientType: 'codex',
|
|
2725
|
-
runtime: runtimeDescriptor,
|
|
2726
|
-
workspaceOptions,
|
|
2727
|
-
defaultCwd: workingDir,
|
|
2728
|
-
extraSessionConfigFields: CODEX_SESSION_CONFIG_FIELDS,
|
|
2729
|
-
liveSessionConfigByConversation: new Map(Array.from(serviceAgentMode ? knownConversationIds : sessions.keys()).map((conversationId) => {
|
|
2730
|
-
const session = sessions.get(conversationId);
|
|
2731
|
-
if (serviceAgentMode) {
|
|
2732
|
-
return [
|
|
2733
|
-
conversationId,
|
|
2734
|
-
buildCodexServiceSnapshotConfig({
|
|
2735
|
-
model: codexDefaultModel ?? session?.state.model ?? undefined,
|
|
2736
|
-
permissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
2737
|
-
effort: codexDefaultEffort ?? session?.state.effort ?? undefined,
|
|
2738
|
-
workspaceId: resolveWorkspaceIdForBaseCwd(workingDir) ?? undefined,
|
|
2739
|
-
}),
|
|
2740
|
-
];
|
|
2741
|
-
}
|
|
2742
|
-
if (!session) {
|
|
2743
|
-
// Non-service snapshots are published only for live sessions,
|
|
2744
|
-
// but keep this branch total if that invariant changes.
|
|
2745
|
-
return [conversationId, {}];
|
|
2746
|
-
}
|
|
2747
|
-
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
2748
|
-
return [
|
|
2749
|
-
conversationId,
|
|
2750
|
-
buildCodexLiveSessionConfig({
|
|
2751
|
-
model: session.state.model,
|
|
2752
|
-
permissionMode: session.state.permissionMode,
|
|
2753
|
-
effort: session.state.effort,
|
|
2754
|
-
workspaceId,
|
|
2755
|
-
executionMode: session.environment.mode,
|
|
2756
|
-
executionBranch: session.environment.branch ?? null,
|
|
2757
|
-
}),
|
|
2758
|
-
];
|
|
2759
|
-
})),
|
|
2760
2597
|
}).catch((error) => {
|
|
2761
2598
|
console.error('[canon-codex] Failed to publish session snapshots:', error);
|
|
2762
2599
|
});
|
|
@@ -2844,12 +2681,6 @@ export async function main() {
|
|
|
2844
2681
|
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
2845
2682
|
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
2846
2683
|
: new Set();
|
|
2847
|
-
try {
|
|
2848
|
-
await reconcileContactLifecycleInbox();
|
|
2849
|
-
}
|
|
2850
|
-
catch (error) {
|
|
2851
|
-
console.error('[canon-codex] Contact lifecycle recovery failed:', error instanceof Error ? error.message : error);
|
|
2852
|
-
}
|
|
2853
2684
|
for (const conversationId of knownConversationIds) {
|
|
2854
2685
|
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
2855
2686
|
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
@@ -2946,6 +2777,7 @@ export async function main() {
|
|
|
2946
2777
|
selfContexts: payload.selfContexts,
|
|
2947
2778
|
provenance: payload.provenance,
|
|
2948
2779
|
turnDispatch: payload.turnDispatch,
|
|
2780
|
+
replyAuthority: payload.replyAuthority,
|
|
2949
2781
|
}).then(() => settleInboundMessageId(message.id, true), (error) => {
|
|
2950
2782
|
settleInboundMessageId(message.id, false);
|
|
2951
2783
|
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Failed to queue inbound message:`, error instanceof Error ? error.message : error);
|
|
@@ -2957,8 +2789,11 @@ export async function main() {
|
|
|
2957
2789
|
onConversationUpdated: (payload) => {
|
|
2958
2790
|
handleConversationUpdated(payload);
|
|
2959
2791
|
},
|
|
2960
|
-
|
|
2961
|
-
|
|
2792
|
+
onAgentContext: (context) => {
|
|
2793
|
+
ownerId = context.ownerId;
|
|
2794
|
+
ownerName = context.ownerName;
|
|
2795
|
+
outboundPolicy = context.outboundPolicy;
|
|
2796
|
+
codexDynamicTools = filterCodexCommunicationTools(baseCodexDynamicTools, outboundPolicy);
|
|
2962
2797
|
},
|
|
2963
2798
|
onConnected: () => {
|
|
2964
2799
|
streamConnected = true;
|