@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/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, HOST_ADMISSION_ACTIONS_DISABLED, 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, formatPendingContactLifecycleContext, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, readLocalRuntimeEntry, reconcileContactLifecycleEvents, recordLocalRuntimeContactLifecycleEvent, saveLocalRuntimeContactLifecycleCursor, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, takeLocalRuntimeContactLifecycleEvents, upsertLocalRuntimeEntry, } from '@canonmsg/core';
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, answerCodexCanonVerb, answerCodexNoReply, classifyCodexAppToolRequest, deniedCodexAppToolResult, handleCodexAppToolCall, isCanonRuntimeControlToolCall, isCodexAppToolCall, isCodexCanonVerbToolCall, isCodexServiceAgentToolCall, parseCanonRuntimeControlRequest, readCodexNoReplyReason, successfulCodexAppToolResult, } from './codex-app-tools.js';
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
- async function loadSessionConfig(conversationId, agentId, rtdb, retryMissing = true) {
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
- if (config?.executionMode)
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
- export function resolveCodexSessionConfig(config, serviceAgentMode = false) {
304
- return serviceAgentMode ? null : config ?? null;
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(config, serviceAgentMode = false) {
307
- return resolveHostWorkspaceCwd({
271
+ export function resolveWorkspaceCwd() {
272
+ return resolveConfiguredWorkspaceCwd({
308
273
  workspaceOptions,
309
- config: serviceAgentMode ? null : config,
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, options) {
381
- return renderCanonHostInboundContent(message, materialized, options);
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 codexDynamicTools = serviceAgentMode
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.currentTurnCanUseCodexCanonTools = false;
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 config = resolveCodexSessionConfig(await loadSessionConfig(conversationId, agentId, rtdb, !serviceAgentMode), serviceAgentMode);
1267
- const sessionExecutionMode = resolveSessionExecutionMode(config, serviceAgentMode);
1268
- const workspaceCwd = resolveWorkspaceCwd(config, serviceAgentMode);
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: config?.effort ?? configuredCodexEffort,
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
- currentTurnCanUseCodexCanonTools: false,
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
- ...(turn.replyContactTarget ? { replyContactTarget: turn.replyContactTarget } : {}),
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, replyContext) {
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 renderOptions = resolveInboundContentRenderOptions(input.isOwner);
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, `I couldn't start a coding session for this workspace: ${userMessage}`, {
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 basePrompt = buildCanonPrompt({
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, resolveCodexTurnModes(participantContext, input.message, replyContext));
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, resolveCodexTurnModes(participantContext, input.message, replyContext));
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.currentTurnCanUseCodexCanonTools = nextTurn.canUseCodexCanonTools === true;
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.currentTurnCanUseCodexCanonTools = false;
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
- onContactRequestUpdated: (payload) => {
2961
- recordContactLifecycleEvent(payload);
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;