@canonmsg/codex-plugin 0.13.0 → 0.13.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 +44 -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 = [];
@@ -390,6 +396,11 @@ export async function main() {
390
396
  console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
391
397
  const client = new CanonClient(apiKey, baseUrl);
392
398
  initRTDBAuth(client);
399
+ const typingSignals = createTypingStatusPublisher({
400
+ setTyping: (conversationId, typing, status) => status
401
+ ? client.setTyping(conversationId, typing, status)
402
+ : client.setTyping(conversationId, typing),
403
+ });
393
404
  let agentId;
394
405
  let ownerId = null;
395
406
  let ownerName = null;
@@ -660,31 +671,33 @@ export async function main() {
660
671
  turnId: session.currentTurnId ?? block.turnId,
661
672
  })));
662
673
  }
674
+ function buildCodexMessageId(session, kind) {
675
+ return `codex-${kind}-${session.currentTurnId ?? randomUUID()}`;
676
+ }
677
+ function buildCodexRuntimeCardOutcomeMessageId(cardId, status) {
678
+ return `codex-card-${cardId}-${status}`;
679
+ }
663
680
  async function handoffFinalMessage(conversationId) {
664
681
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
665
682
  clearStreaming(conversationId);
666
- client.setTyping(conversationId, false).catch(() => { });
683
+ typingSignals.clear(conversationId).catch(() => { });
667
684
  }
668
685
  function refreshVisibleWorkSignal(session) {
669
686
  if (!session.running || session.closed)
670
687
  return;
671
688
  if (session.turnState !== 'thinking' && session.turnState !== 'tool')
672
689
  return;
673
- client.setTyping(session.conversationId, true, 'thinking').catch(() => { });
690
+ typingSignals.start(session.conversationId, 'thinking').catch(() => { });
674
691
  }
675
692
  function startVisibleWorkSignal(session) {
676
693
  refreshVisibleWorkSignal(session);
677
- if (session.typingKeepaliveTimer)
678
- return;
679
- session.typingKeepaliveTimer = setInterval(() => {
680
- refreshVisibleWorkSignal(session);
681
- }, 3500);
682
694
  }
683
695
  function stopVisibleWorkSignal(session) {
684
696
  if (session.typingKeepaliveTimer) {
685
697
  clearInterval(session.typingKeepaliveTimer);
686
698
  session.typingKeepaliveTimer = null;
687
699
  }
700
+ typingSignals.clear(session.conversationId).catch(() => { });
688
701
  }
689
702
  function closeSession(conversationId) {
690
703
  const session = sessions.get(conversationId);
@@ -699,7 +712,8 @@ export async function main() {
699
712
  clearStreaming(conversationId);
700
713
  runtimeState.clearSessionState(conversationId).catch(() => { });
701
714
  runtimeState.clearTurnState(conversationId).catch(() => { });
702
- client.setTyping(conversationId, false).catch(() => { });
715
+ typingSignals.clear(conversationId).catch(() => { });
716
+ typingSignals.dispose(conversationId);
703
717
  sessions.delete(conversationId);
704
718
  }
705
719
  async function resetRuntimeSession(session) {
@@ -725,7 +739,7 @@ export async function main() {
725
739
  }
726
740
  stopVisibleWorkSignal(session);
727
741
  clearStreaming(conversationId);
728
- client.setTyping(conversationId, false).catch(() => { });
742
+ typingSignals.clear(conversationId).catch(() => { });
729
743
  writeState(session);
730
744
  writeTurn(session);
731
745
  }
@@ -1005,7 +1019,8 @@ export async function main() {
1005
1019
  });
1006
1020
  requestResolved = true;
1007
1021
  const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
1008
- await client.sendMessage(session.conversationId, outcome.text, {
1022
+ await sendMessageWithRetry(client, session.conversationId, outcome.text, {
1023
+ messageId: buildCodexRuntimeCardOutcomeMessageId(cardId, response.status),
1009
1024
  metadata: {
1010
1025
  ...outcome.metadata,
1011
1026
  turnId: session.currentTurnId ?? undefined,
@@ -1031,7 +1046,8 @@ export async function main() {
1031
1046
  cancel: true,
1032
1047
  }).catch(() => null);
1033
1048
  const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1034
- await client.sendMessage(session.conversationId, outcome.text, {
1049
+ await sendMessageWithRetry(client, session.conversationId, outcome.text, {
1050
+ messageId: buildCodexRuntimeCardOutcomeMessageId(cardId, 'cancelled'),
1035
1051
  metadata: {
1036
1052
  ...outcome.metadata,
1037
1053
  turnId: session.currentTurnId ?? undefined,
@@ -1197,7 +1213,8 @@ export async function main() {
1197
1213
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1198
1214
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
1199
1215
  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}`, {
1216
+ await sendMessageWithRetry(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1217
+ messageId: `codex-start-failed-${input.message.id}`,
1201
1218
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1202
1219
  metadata: {
1203
1220
  turnSemantics: 'turn_complete',
@@ -1220,7 +1237,7 @@ export async function main() {
1220
1237
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
1221
1238
  await session.adapter.interrupt().catch(() => { });
1222
1239
  clearStreaming(input.conversationId);
1223
- client.setTyping(input.conversationId, false).catch(() => { });
1240
+ typingSignals.clear(input.conversationId).catch(() => { });
1224
1241
  return;
1225
1242
  }
1226
1243
  enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
@@ -1269,7 +1286,6 @@ export async function main() {
1269
1286
  markTurnProgress(session);
1270
1287
  writeTurn(session);
1271
1288
  stopVisibleWorkSignal(session);
1272
- client.setTyping(session.conversationId, false).catch(() => { });
1273
1289
  writeCodexStreaming(session, event.text, 'streaming');
1274
1290
  return;
1275
1291
  }
@@ -1278,7 +1294,6 @@ export async function main() {
1278
1294
  markTurnProgress(session);
1279
1295
  writeTurn(session);
1280
1296
  stopVisibleWorkSignal(session);
1281
- client.setTyping(session.conversationId, false).catch(() => { });
1282
1297
  upsertTurnBlock(session, {
1283
1298
  id: `plan:${session.currentTurnId}`,
1284
1299
  kind: 'plan',
@@ -1357,7 +1372,8 @@ export async function main() {
1357
1372
  title: 'Codex Plan',
1358
1373
  body: result.finalMessage,
1359
1374
  });
1360
- await client.sendMessage(session.conversationId, planApproval.text, {
1375
+ await sendMessageWithRetry(client, session.conversationId, planApproval.text, {
1376
+ messageId: buildCodexMessageId(session, 'plan'),
1361
1377
  metadata: {
1362
1378
  ...planApproval.metadata,
1363
1379
  turnId: session.currentTurnId,
@@ -1373,7 +1389,8 @@ export async function main() {
1373
1389
  clearStoredThread();
1374
1390
  }
1375
1391
  const turnTrail = buildFinalTurnTrail(session);
1376
- await client.sendMessage(session.conversationId, result.finalMessage, {
1392
+ await sendMessageWithRetry(client, session.conversationId, result.finalMessage, {
1393
+ messageId: buildCodexMessageId(session, 'final'),
1377
1394
  ...(session.activeSelfContextId
1378
1395
  ? { selfContextId: session.activeSelfContextId }
1379
1396
  : {}),
@@ -1395,7 +1412,8 @@ export async function main() {
1395
1412
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
1396
1413
  }
1397
1414
  const turnTrail = buildFinalTurnTrail(session);
1398
- await client.sendMessage(session.conversationId, userVisibleError, {
1415
+ await sendMessageWithRetry(client, session.conversationId, userVisibleError, {
1416
+ messageId: buildCodexMessageId(session, 'error'),
1399
1417
  ...(session.activeSelfContextId
1400
1418
  ? { selfContextId: session.activeSelfContextId }
1401
1419
  : {}),
@@ -1416,7 +1434,7 @@ export async function main() {
1416
1434
  writeTurn(session);
1417
1435
  stopVisibleWorkSignal(session);
1418
1436
  clearStreaming(session.conversationId);
1419
- client.setTyping(session.conversationId, false).catch(() => { });
1437
+ typingSignals.clear(session.conversationId).catch(() => { });
1420
1438
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn interrupted`);
1421
1439
  }
1422
1440
  }
@@ -1426,7 +1444,8 @@ export async function main() {
1426
1444
  : `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
1427
1445
  session.state.lastError = message;
1428
1446
  writeState(session);
1429
- await client.sendMessage(session.conversationId, message, {
1447
+ await sendMessageWithRetry(client, session.conversationId, message, {
1448
+ messageId: buildCodexMessageId(session, 'failure'),
1430
1449
  ...(session.activeSelfContextId
1431
1450
  ? { selfContextId: session.activeSelfContextId }
1432
1451
  : {}),
@@ -1777,6 +1796,8 @@ export async function main() {
1777
1796
  });
1778
1797
  const pollControl = async () => {
1779
1798
  while (!controlStopped) {
1799
+ const hadActiveWork = [...sessions.values()].some((session) => !session.closed
1800
+ && (session.running || session.queue.length > 0 || session.turnState === 'waiting_input'));
1780
1801
  for (const conversationId of [...sessions.keys()]) {
1781
1802
  try {
1782
1803
  const controlRaw = await rtdbRead(`/control/${conversationId}/${agentId}/session`);
@@ -1845,14 +1866,14 @@ export async function main() {
1845
1866
  session.turnState = 'interrupted';
1846
1867
  writeTurn(session);
1847
1868
  clearStreaming(conversationId);
1848
- client.setTyping(conversationId, false).catch(() => { });
1869
+ typingSignals.clear(conversationId).catch(() => { });
1849
1870
  await rtdbWrite(`/control/${conversationId}/${agentId}/signal`, null).catch(() => { });
1850
1871
  }
1851
1872
  catch {
1852
1873
  // Ignore transient RTDB failures.
1853
1874
  }
1854
1875
  }
1855
- await new Promise((resolve) => setTimeout(resolve, CONTROL_POLL_MS));
1876
+ await new Promise((resolve) => setTimeout(resolve, controlPollDelayMs(hadActiveWork)));
1856
1877
  }
1857
1878
  };
1858
1879
  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.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.1.0",
33
- "@canonmsg/core": "^1.2.0"
32
+ "@canonmsg/agent-sdk": "^2.1.1",
33
+ "@canonmsg/core": "^1.3.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"