@canonmsg/codex-plugin 0.13.0 → 0.13.2

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 +45 -23
  2. package/package.json +3 -3
package/dist/host.js CHANGED
@@ -5,7 +5,7 @@ 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, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
8
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonHostPrompt, 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, sendMessageWithRetry, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
9
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
10
10
  import { CodexConversationAdapter, } from './adapter.js';
11
11
  import { CodexAppServerAdapter } from './app-server-adapter.js';
@@ -54,6 +54,8 @@ const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
54
54
  const HEARTBEAT_MS = 30_000;
55
55
  const IDLE_CHECK_MS = 60_000;
56
56
  const CONTROL_POLL_MS = 2_000;
57
+ const IDLE_CONTROL_POLL_MS = 10_000;
58
+ const CONTROL_POLL_JITTER_MS = 1_000;
57
59
  const CODEX_RUNTIME_CAPABILITIES = {
58
60
  ...DEFAULT_RUNTIME_CAPABILITIES,
59
61
  supportsInterrupt: true,
@@ -61,6 +63,10 @@ const CODEX_RUNTIME_CAPABILITIES = {
61
63
  supportsQueue: true,
62
64
  supportsNonFinalPermanentMessages: false,
63
65
  };
66
+ function controlPollDelayMs(hasActiveWork) {
67
+ const base = hasActiveWork ? CONTROL_POLL_MS : IDLE_CONTROL_POLL_MS;
68
+ return base + Math.floor(Math.random() * CONTROL_POLL_JITTER_MS);
69
+ }
64
70
  let workingDir = process.cwd();
65
71
  let workspaceOptions = [];
66
72
  let workspaceRoots = [];
@@ -119,6 +125,7 @@ function buildCodexRuntimeDescriptor(input) {
119
125
  result: 'action_or_values',
120
126
  maxTimeoutMs: 30 * 60_000,
121
127
  blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
128
+ actionFieldTypes: ['text', 'textarea', 'select', 'multiSelect', 'boolean'],
122
129
  native: true,
123
130
  },
124
131
  },
@@ -390,6 +397,11 @@ export async function main() {
390
397
  console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
391
398
  const client = new CanonClient(apiKey, baseUrl);
392
399
  initRTDBAuth(client);
400
+ const typingSignals = createTypingStatusPublisher({
401
+ setTyping: (conversationId, typing, status) => status
402
+ ? client.setTyping(conversationId, typing, status)
403
+ : client.setTyping(conversationId, typing),
404
+ });
393
405
  let agentId;
394
406
  let ownerId = null;
395
407
  let ownerName = null;
@@ -660,31 +672,33 @@ export async function main() {
660
672
  turnId: session.currentTurnId ?? block.turnId,
661
673
  })));
662
674
  }
675
+ function buildCodexMessageId(session, kind) {
676
+ return `codex-${kind}-${session.currentTurnId ?? randomUUID()}`;
677
+ }
678
+ function buildCodexRuntimeCardOutcomeMessageId(cardId, status) {
679
+ return `codex-card-${cardId}-${status}`;
680
+ }
663
681
  async function handoffFinalMessage(conversationId) {
664
682
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
665
683
  clearStreaming(conversationId);
666
- client.setTyping(conversationId, false).catch(() => { });
684
+ typingSignals.clear(conversationId).catch(() => { });
667
685
  }
668
686
  function refreshVisibleWorkSignal(session) {
669
687
  if (!session.running || session.closed)
670
688
  return;
671
689
  if (session.turnState !== 'thinking' && session.turnState !== 'tool')
672
690
  return;
673
- client.setTyping(session.conversationId, true, 'thinking').catch(() => { });
691
+ typingSignals.start(session.conversationId, 'thinking').catch(() => { });
674
692
  }
675
693
  function startVisibleWorkSignal(session) {
676
694
  refreshVisibleWorkSignal(session);
677
- if (session.typingKeepaliveTimer)
678
- return;
679
- session.typingKeepaliveTimer = setInterval(() => {
680
- refreshVisibleWorkSignal(session);
681
- }, 3500);
682
695
  }
683
696
  function stopVisibleWorkSignal(session) {
684
697
  if (session.typingKeepaliveTimer) {
685
698
  clearInterval(session.typingKeepaliveTimer);
686
699
  session.typingKeepaliveTimer = null;
687
700
  }
701
+ typingSignals.clear(session.conversationId).catch(() => { });
688
702
  }
689
703
  function closeSession(conversationId) {
690
704
  const session = sessions.get(conversationId);
@@ -699,7 +713,8 @@ export async function main() {
699
713
  clearStreaming(conversationId);
700
714
  runtimeState.clearSessionState(conversationId).catch(() => { });
701
715
  runtimeState.clearTurnState(conversationId).catch(() => { });
702
- client.setTyping(conversationId, false).catch(() => { });
716
+ typingSignals.clear(conversationId).catch(() => { });
717
+ typingSignals.dispose(conversationId);
703
718
  sessions.delete(conversationId);
704
719
  }
705
720
  async function resetRuntimeSession(session) {
@@ -725,7 +740,7 @@ export async function main() {
725
740
  }
726
741
  stopVisibleWorkSignal(session);
727
742
  clearStreaming(conversationId);
728
- client.setTyping(conversationId, false).catch(() => { });
743
+ typingSignals.clear(conversationId).catch(() => { });
729
744
  writeState(session);
730
745
  writeTurn(session);
731
746
  }
@@ -1005,7 +1020,8 @@ export async function main() {
1005
1020
  });
1006
1021
  requestResolved = true;
1007
1022
  const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
1008
- await client.sendMessage(session.conversationId, outcome.text, {
1023
+ await sendMessageWithRetry(client, session.conversationId, outcome.text, {
1024
+ messageId: buildCodexRuntimeCardOutcomeMessageId(cardId, response.status),
1009
1025
  metadata: {
1010
1026
  ...outcome.metadata,
1011
1027
  turnId: session.currentTurnId ?? undefined,
@@ -1031,7 +1047,8 @@ export async function main() {
1031
1047
  cancel: true,
1032
1048
  }).catch(() => null);
1033
1049
  const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1034
- await client.sendMessage(session.conversationId, outcome.text, {
1050
+ await sendMessageWithRetry(client, session.conversationId, outcome.text, {
1051
+ messageId: buildCodexRuntimeCardOutcomeMessageId(cardId, 'cancelled'),
1035
1052
  metadata: {
1036
1053
  ...outcome.metadata,
1037
1054
  turnId: session.currentTurnId ?? undefined,
@@ -1197,7 +1214,8 @@ export async function main() {
1197
1214
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1198
1215
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
1199
1216
  await markQueuedMessageAccepted(input.conversationId, input.message.id, shouldMarkAccepted);
1200
- await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1217
+ await sendMessageWithRetry(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1218
+ messageId: `codex-start-failed-${input.message.id}`,
1201
1219
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1202
1220
  metadata: {
1203
1221
  turnSemantics: 'turn_complete',
@@ -1220,7 +1238,7 @@ export async function main() {
1220
1238
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
1221
1239
  await session.adapter.interrupt().catch(() => { });
1222
1240
  clearStreaming(input.conversationId);
1223
- client.setTyping(input.conversationId, false).catch(() => { });
1241
+ typingSignals.clear(input.conversationId).catch(() => { });
1224
1242
  return;
1225
1243
  }
1226
1244
  enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
@@ -1269,7 +1287,6 @@ export async function main() {
1269
1287
  markTurnProgress(session);
1270
1288
  writeTurn(session);
1271
1289
  stopVisibleWorkSignal(session);
1272
- client.setTyping(session.conversationId, false).catch(() => { });
1273
1290
  writeCodexStreaming(session, event.text, 'streaming');
1274
1291
  return;
1275
1292
  }
@@ -1278,7 +1295,6 @@ export async function main() {
1278
1295
  markTurnProgress(session);
1279
1296
  writeTurn(session);
1280
1297
  stopVisibleWorkSignal(session);
1281
- client.setTyping(session.conversationId, false).catch(() => { });
1282
1298
  upsertTurnBlock(session, {
1283
1299
  id: `plan:${session.currentTurnId}`,
1284
1300
  kind: 'plan',
@@ -1357,7 +1373,8 @@ export async function main() {
1357
1373
  title: 'Codex Plan',
1358
1374
  body: result.finalMessage,
1359
1375
  });
1360
- await client.sendMessage(session.conversationId, planApproval.text, {
1376
+ await sendMessageWithRetry(client, session.conversationId, planApproval.text, {
1377
+ messageId: buildCodexMessageId(session, 'plan'),
1361
1378
  metadata: {
1362
1379
  ...planApproval.metadata,
1363
1380
  turnId: session.currentTurnId,
@@ -1373,7 +1390,8 @@ export async function main() {
1373
1390
  clearStoredThread();
1374
1391
  }
1375
1392
  const turnTrail = buildFinalTurnTrail(session);
1376
- await client.sendMessage(session.conversationId, result.finalMessage, {
1393
+ await sendMessageWithRetry(client, session.conversationId, result.finalMessage, {
1394
+ messageId: buildCodexMessageId(session, 'final'),
1377
1395
  ...(session.activeSelfContextId
1378
1396
  ? { selfContextId: session.activeSelfContextId }
1379
1397
  : {}),
@@ -1395,7 +1413,8 @@ export async function main() {
1395
1413
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
1396
1414
  }
1397
1415
  const turnTrail = buildFinalTurnTrail(session);
1398
- await client.sendMessage(session.conversationId, userVisibleError, {
1416
+ await sendMessageWithRetry(client, session.conversationId, userVisibleError, {
1417
+ messageId: buildCodexMessageId(session, 'error'),
1399
1418
  ...(session.activeSelfContextId
1400
1419
  ? { selfContextId: session.activeSelfContextId }
1401
1420
  : {}),
@@ -1416,7 +1435,7 @@ export async function main() {
1416
1435
  writeTurn(session);
1417
1436
  stopVisibleWorkSignal(session);
1418
1437
  clearStreaming(session.conversationId);
1419
- client.setTyping(session.conversationId, false).catch(() => { });
1438
+ typingSignals.clear(session.conversationId).catch(() => { });
1420
1439
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn interrupted`);
1421
1440
  }
1422
1441
  }
@@ -1426,7 +1445,8 @@ export async function main() {
1426
1445
  : `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
1427
1446
  session.state.lastError = message;
1428
1447
  writeState(session);
1429
- await client.sendMessage(session.conversationId, message, {
1448
+ await sendMessageWithRetry(client, session.conversationId, message, {
1449
+ messageId: buildCodexMessageId(session, 'failure'),
1430
1450
  ...(session.activeSelfContextId
1431
1451
  ? { selfContextId: session.activeSelfContextId }
1432
1452
  : {}),
@@ -1777,6 +1797,8 @@ export async function main() {
1777
1797
  });
1778
1798
  const pollControl = async () => {
1779
1799
  while (!controlStopped) {
1800
+ const hadActiveWork = [...sessions.values()].some((session) => !session.closed
1801
+ && (session.running || session.queue.length > 0 || session.turnState === 'waiting_input'));
1780
1802
  for (const conversationId of [...sessions.keys()]) {
1781
1803
  try {
1782
1804
  const controlRaw = await rtdbRead(`/control/${conversationId}/${agentId}/session`);
@@ -1845,14 +1867,14 @@ export async function main() {
1845
1867
  session.turnState = 'interrupted';
1846
1868
  writeTurn(session);
1847
1869
  clearStreaming(conversationId);
1848
- client.setTyping(conversationId, false).catch(() => { });
1870
+ typingSignals.clear(conversationId).catch(() => { });
1849
1871
  await rtdbWrite(`/control/${conversationId}/${agentId}/signal`, null).catch(() => { });
1850
1872
  }
1851
1873
  catch {
1852
1874
  // Ignore transient RTDB failures.
1853
1875
  }
1854
1876
  }
1855
- await new Promise((resolve) => setTimeout(resolve, CONTROL_POLL_MS));
1877
+ await new Promise((resolve) => setTimeout(resolve, controlPollDelayMs(hadActiveWork)));
1856
1878
  }
1857
1879
  };
1858
1880
  void pollControl();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
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.1.0",
33
- "@canonmsg/core": "^1.2.0"
32
+ "@canonmsg/agent-sdk": "^2.3.0",
33
+ "@canonmsg/core": "^1.6.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"