@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.
- package/dist/host.js +45 -23
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
33
|
-
"@canonmsg/core": "^1.
|
|
32
|
+
"@canonmsg/agent-sdk": "^2.3.0",
|
|
33
|
+
"@canonmsg/core": "^1.6.0"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|