@canonmsg/codex-plugin 0.13.3 → 0.14.1

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.
Files changed (2) hide show
  1. package/dist/host.js +56 -39
  2. package/package.json +4 -4
package/dist/host.js CHANGED
@@ -5,8 +5,8 @@ import { spawnSync } from 'node:child_process';
5
5
  import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, } from '@canonmsg/agent-sdk';
8
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonHostPrompt, buildCanonTurnEnvelopeV1, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, 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, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnEnvelopePrompt, resolveCanonHostPromptMode, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
- import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
8
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, 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, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
+ import { decideAutoReply, } from './inbound-policy.js';
10
10
  import { CodexConversationAdapter, } from './adapter.js';
11
11
  import { CodexAppServerAdapter } from './app-server-adapter.js';
12
12
  import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
@@ -50,7 +50,6 @@ Keep this terminal open while you want Canon to reach the agent. Closing it,
50
50
  logging out, rebooting, or sleeping long enough to stop the process takes the
51
51
  local agent offline until you revive it. Docs:
52
52
  https://canonmail.com/agents/integrations`;
53
- const HOST_PROMPT_MODE = resolveCanonHostPromptMode(process.env.CANON_HOST_PROMPT_MODE);
54
53
  const MAX_SESSIONS = 12;
55
54
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
56
55
  const HEARTBEAT_MS = 30_000;
@@ -75,19 +74,6 @@ let workspaceRoots = [];
75
74
  let workspaceRootMetadata = [];
76
75
  function buildCodexRuntimeDescriptor(input) {
77
76
  const commands = [
78
- ...(input.supportsPlanMode
79
- ? [{
80
- id: 'plan',
81
- label: 'Plan first',
82
- description: 'Ask Codex to plan before implementing. Text after /plan becomes the planning prompt.',
83
- aliases: ['plan'],
84
- category: 'plan',
85
- placements: ['composer_slash', 'command_palette'],
86
- availability: ['always'],
87
- trailingTextBehavior: 'send_as_prompt',
88
- dispatch: { kind: 'text_passthrough', template: '/plan {argument}' },
89
- }]
90
- : []),
91
77
  {
92
78
  id: 'runtime-status',
93
79
  label: 'Runtime status',
@@ -116,6 +102,29 @@ function buildCodexRuntimeDescriptor(input) {
116
102
  defaultPermissionMode: input.defaultPermissionMode,
117
103
  presentation: input.presentation,
118
104
  streamingTextMode: 'snapshot',
105
+ ...(input.supportsPlanMode
106
+ ? {
107
+ turnModes: [
108
+ {
109
+ id: 'normal',
110
+ label: 'Normal',
111
+ description: 'Let Codex answer or act normally.',
112
+ scope: 'next_turn',
113
+ default: true,
114
+ activation: { kind: 'message_metadata', value: 'normal' },
115
+ },
116
+ {
117
+ id: 'plan',
118
+ label: 'Plan',
119
+ description: 'Ask Codex to plan before implementing.',
120
+ scope: 'next_turn',
121
+ ownerOnly: true,
122
+ aliases: ['plan'],
123
+ activation: { kind: 'message_metadata', value: 'plan' },
124
+ },
125
+ ],
126
+ }
127
+ : {}),
119
128
  commands,
120
129
  ...(input.supportsRichCards
121
130
  ? {
@@ -229,25 +238,17 @@ function resolveCodexEffectiveRuntimePolicy(input) {
229
238
  };
230
239
  }
231
240
  function buildCanonPrompt(input) {
232
- if (HOST_PROMPT_MODE === 'envelope') {
233
- return renderCanonTurnEnvelopePrompt(buildCanonTurnEnvelopeV1({
234
- content: input.content,
235
- conversationId: input.conversationId,
236
- participantContext: input.participantContext,
237
- behavior: input.behavior,
238
- selfContexts: input.selfContexts,
239
- activeSelfContextId: input.activeSelfContextId,
240
- provenance: input.provenance,
241
- replyContext: input.replyContext,
242
- message: input.message,
243
- hydratedFromPage: input.hydratedFromPage,
244
- }));
245
- }
246
- return buildCanonHostPrompt({
247
- hostLabel: 'Codex',
248
- buildInboundContextLines,
249
- ...input,
250
- });
241
+ return renderCanonTurnBriefPrompt(buildCanonTurnContextV2({
242
+ content: input.content,
243
+ conversationId: input.conversationId,
244
+ participantContext: input.participantContext,
245
+ behavior: input.behavior,
246
+ selfContexts: input.selfContexts,
247
+ activeSelfContextId: input.activeSelfContextId,
248
+ provenance: input.provenance,
249
+ replyContext: input.replyContext,
250
+ message: input.message,
251
+ }));
251
252
  }
252
253
  function renderInboundContent(message, materialized) {
253
254
  return renderCanonHostInboundContent(message, materialized);
@@ -1157,6 +1158,10 @@ export async function main() {
1157
1158
  }
1158
1159
  async function enqueueInboundMessage(input) {
1159
1160
  knownConversationIds.add(input.conversationId);
1161
+ if (input.turnDispatch && input.turnDispatch.kind !== 'run_turn') {
1162
+ console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed server-dispatched turn: ${input.turnDispatch.reason}`);
1163
+ return;
1164
+ }
1160
1165
  if (isRecord(input.message.metadata)
1161
1166
  && input.message.metadata.type === 'plan_approval_reply'
1162
1167
  && typeof input.message.metadata.decision === 'string') {
@@ -1182,8 +1187,12 @@ export async function main() {
1182
1187
  }
1183
1188
  }
1184
1189
  const renderedContent = renderInboundContent(input.message, materialized);
1190
+ const turnMetadata = normalizeTurnMetadata(input.message.metadata);
1191
+ const requestedPlanMode = turnMetadata?.requestedTurnMode === 'plan';
1185
1192
  const planCommand = useAppServer
1186
- ? parsePlanCommand(renderedContent)
1193
+ ? requestedPlanMode
1194
+ ? { planMode: true, content: renderedContent }
1195
+ : parsePlanCommand(renderedContent)
1187
1196
  : { planMode: false, content: renderedContent };
1188
1197
  const content = planCommand.content;
1189
1198
  const hydrated = await loadHydratedInboundContext({
@@ -1212,14 +1221,18 @@ export async function main() {
1212
1221
  .filter((path) => path !== null);
1213
1222
  const mediaAddDirs = uniqueStrings(promptMaterialized.map((attachment) => dirname(attachment.path)));
1214
1223
  const participantContext = hydrated.participantContext;
1215
- const autoReply = decideAutoReply(participantContext, behavior);
1224
+ const autoReply = input.turnDispatch?.kind === 'run_turn'
1225
+ ? {
1226
+ allow: true,
1227
+ reason: input.turnDispatch.reason || 'server dispatch allowed this turn',
1228
+ }
1229
+ : decideAutoReply(participantContext, behavior);
1216
1230
  if (!autoReply.allow) {
1217
1231
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
1218
1232
  return;
1219
1233
  }
1220
1234
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
1221
1235
  markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
1222
- const turnMetadata = normalizeTurnMetadata(input.message.metadata);
1223
1236
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1224
1237
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1225
1238
  let session;
@@ -1252,7 +1265,6 @@ export async function main() {
1252
1265
  provenance: hydrated.provenance,
1253
1266
  replyContext,
1254
1267
  message: input.message,
1255
- hydratedFromPage: hydrated.hydratedFromPage,
1256
1268
  });
1257
1269
  if (session.running && deliveryIntent === 'interrupt') {
1258
1270
  enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
@@ -1687,6 +1699,10 @@ export async function main() {
1687
1699
  const message = payload.message;
1688
1700
  if (message.senderId === agentId)
1689
1701
  return;
1702
+ if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
1703
+ console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
1704
+ return;
1705
+ }
1690
1706
  void enqueueInboundMessage({
1691
1707
  conversationId: payload.conversationId,
1692
1708
  message,
@@ -1696,6 +1712,7 @@ export async function main() {
1696
1712
  activeSelfContextId: payload.activeSelfContextId,
1697
1713
  selfContexts: payload.selfContexts,
1698
1714
  provenance: payload.provenance,
1715
+ turnDispatch: payload.turnDispatch,
1699
1716
  });
1700
1717
  if (message.id) {
1701
1718
  saveRuntimeSessionState(runtimeId, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.13.3",
3
+ "version": "0.14.1",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^2.4.0",
33
- "@canonmsg/core": "^1.7.0"
32
+ "@canonmsg/agent-sdk": "^3.1.0",
33
+ "@canonmsg/core": "^2.1.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"
@@ -54,7 +54,7 @@
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.0.0",
56
56
  "typescript": "~5.7.0",
57
- "vitest": "^3.0.0"
57
+ "vitest": "^4.1.8"
58
58
  },
59
59
  "license": "MIT"
60
60
  }