@canonmsg/codex-plugin 0.11.12 → 0.13.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/adapter.d.ts +16 -1
- package/dist/adapter.js +1 -1
- package/dist/app-server-adapter.d.ts +62 -0
- package/dist/app-server-adapter.js +502 -0
- package/dist/host.js +543 -70
- package/package.json +3 -3
package/dist/host.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setDefaultResultOrder } from 'node:dns';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { dirname } from 'node:path';
|
|
5
6
|
import { parseArgs } from 'node:util';
|
|
6
7
|
import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, } from '@canonmsg/agent-sdk';
|
|
7
|
-
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, 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,
|
|
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
9
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
9
10
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
11
|
+
import { CodexAppServerAdapter } from './app-server-adapter.js';
|
|
12
|
+
import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
|
|
10
13
|
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
11
14
|
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
12
15
|
import { detectCodexCliVersion } from './codex-cli-version.js';
|
|
@@ -64,6 +67,19 @@ let workspaceRoots = [];
|
|
|
64
67
|
let workspaceRootMetadata = [];
|
|
65
68
|
function buildCodexRuntimeDescriptor(input) {
|
|
66
69
|
const commands = [
|
|
70
|
+
...(input.supportsPlanMode
|
|
71
|
+
? [{
|
|
72
|
+
id: 'plan',
|
|
73
|
+
label: 'Plan first',
|
|
74
|
+
description: 'Ask Codex to plan before implementing. Text after /plan becomes the planning prompt.',
|
|
75
|
+
aliases: ['plan'],
|
|
76
|
+
category: 'plan',
|
|
77
|
+
placements: ['composer_slash', 'command_palette'],
|
|
78
|
+
availability: ['always'],
|
|
79
|
+
trailingTextBehavior: 'send_as_prompt',
|
|
80
|
+
dispatch: { kind: 'text_passthrough', template: '/plan {argument}' },
|
|
81
|
+
}]
|
|
82
|
+
: []),
|
|
67
83
|
{
|
|
68
84
|
id: 'runtime-status',
|
|
69
85
|
label: 'Runtime status',
|
|
@@ -79,6 +95,8 @@ function buildCodexRuntimeDescriptor(input) {
|
|
|
79
95
|
...RUNTIME_NEW_SESSION_ACTION,
|
|
80
96
|
primitive: 'session.new',
|
|
81
97
|
},
|
|
98
|
+
RUNTIME_STOP_ACTION,
|
|
99
|
+
RUNTIME_STOP_AND_DROP_ACTION,
|
|
82
100
|
];
|
|
83
101
|
const descriptor = buildFirstPartyCodingRuntimeDescriptor({
|
|
84
102
|
clientType: 'codex',
|
|
@@ -91,11 +109,21 @@ function buildCodexRuntimeDescriptor(input) {
|
|
|
91
109
|
presentation: input.presentation,
|
|
92
110
|
streamingTextMode: 'snapshot',
|
|
93
111
|
commands,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
...(input.supportsRichCards
|
|
113
|
+
? {
|
|
114
|
+
runtimeCards: {
|
|
115
|
+
rich: {
|
|
116
|
+
schema: 'canon.card.v1',
|
|
117
|
+
lifecycle: 'blocking_requires_action',
|
|
118
|
+
responder: 'agent_owner',
|
|
119
|
+
result: 'action_or_values',
|
|
120
|
+
maxTimeoutMs: 30 * 60_000,
|
|
121
|
+
blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
|
|
122
|
+
native: true,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
99
127
|
});
|
|
100
128
|
if (input.models.length > 0) {
|
|
101
129
|
return descriptor;
|
|
@@ -119,18 +147,6 @@ function buildCodexModelOptions(model) {
|
|
|
119
147
|
? [{ value: model.trim(), label: modelOptionLabel(model.trim()) }]
|
|
120
148
|
: [];
|
|
121
149
|
}
|
|
122
|
-
function normalizeRuntimeTurnState(value) {
|
|
123
|
-
const normalizedTurn = normalizeTurnState(value);
|
|
124
|
-
if (normalizedTurn) {
|
|
125
|
-
return {
|
|
126
|
-
state: normalizedTurn.state,
|
|
127
|
-
...(normalizedTurn.openedAt !== undefined ? { openedAt: normalizedTurn.openedAt } : {}),
|
|
128
|
-
...(normalizedTurn.updatedAt !== undefined ? { updatedAt: normalizedTurn.updatedAt } : {}),
|
|
129
|
-
...(normalizedTurn.turnUpdatedAt !== undefined ? { turnUpdatedAt: normalizedTurn.turnUpdatedAt } : {}),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
150
|
async function publishAgentRuntime(agentId, runtime) {
|
|
135
151
|
await publishHostAgentRuntime(agentId, 'codex', runtime);
|
|
136
152
|
}
|
|
@@ -249,6 +265,78 @@ function stringArgs(value) {
|
|
|
249
265
|
? value.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
250
266
|
: undefined;
|
|
251
267
|
}
|
|
268
|
+
function supportsCodexAppServer(codexBin) {
|
|
269
|
+
if (process.env.CANON_CODEX_TRANSPORT === 'exec')
|
|
270
|
+
return false;
|
|
271
|
+
if (process.env.CANON_CODEX_TRANSPORT === 'app-server')
|
|
272
|
+
return true;
|
|
273
|
+
const result = spawnSync(codexBin, ['app-server', '--help'], {
|
|
274
|
+
encoding: 'utf8',
|
|
275
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
276
|
+
});
|
|
277
|
+
return result.status === 0;
|
|
278
|
+
}
|
|
279
|
+
function parsePlanCommand(content) {
|
|
280
|
+
const trimmed = content.trimStart();
|
|
281
|
+
if (!trimmed.startsWith('/plan'))
|
|
282
|
+
return { planMode: false, content };
|
|
283
|
+
const rest = trimmed.replace(/^\/plan(?:\s+)?/i, '').trim();
|
|
284
|
+
return {
|
|
285
|
+
planMode: true,
|
|
286
|
+
content: rest || 'Please inspect the request and propose a plan before making changes.',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function mapCodexQuestions(value) {
|
|
290
|
+
if (!Array.isArray(value))
|
|
291
|
+
return undefined;
|
|
292
|
+
const questions = value.slice(0, 12).flatMap((entry) => {
|
|
293
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
294
|
+
return [];
|
|
295
|
+
const record = entry;
|
|
296
|
+
const id = typeof record.id === 'string' && record.id.trim() ? record.id.trim().slice(0, 120) : null;
|
|
297
|
+
const question = typeof record.question === 'string' && record.question.trim()
|
|
298
|
+
? record.question.trim().slice(0, 1000)
|
|
299
|
+
: null;
|
|
300
|
+
if (!id || !/^[A-Za-z0-9_.:-]{1,120}$/.test(id) || !question)
|
|
301
|
+
return [];
|
|
302
|
+
const header = typeof record.header === 'string' && record.header.trim()
|
|
303
|
+
? record.header.trim().slice(0, 120)
|
|
304
|
+
: undefined;
|
|
305
|
+
const rawOptions = Array.isArray(record.options) ? record.options : [];
|
|
306
|
+
const choices = rawOptions.slice(0, 12).flatMap((option) => {
|
|
307
|
+
if (!option || typeof option !== 'object' || Array.isArray(option))
|
|
308
|
+
return [];
|
|
309
|
+
const optionRecord = option;
|
|
310
|
+
const label = typeof optionRecord.label === 'string' && optionRecord.label.trim()
|
|
311
|
+
? optionRecord.label.trim().slice(0, 120)
|
|
312
|
+
: null;
|
|
313
|
+
if (!label)
|
|
314
|
+
return [];
|
|
315
|
+
const description = typeof optionRecord.description === 'string' && optionRecord.description.trim()
|
|
316
|
+
? optionRecord.description.trim().slice(0, 300)
|
|
317
|
+
: undefined;
|
|
318
|
+
return [{ label, value: label, ...(description ? { description } : {}) }];
|
|
319
|
+
});
|
|
320
|
+
return [{
|
|
321
|
+
id,
|
|
322
|
+
question,
|
|
323
|
+
...(header ? { header } : {}),
|
|
324
|
+
...(choices.length > 0 ? { choices } : {}),
|
|
325
|
+
...(choices.length > 0 && record.allowOther !== false ? { allowOther: true } : {}),
|
|
326
|
+
...(record.allowOther === true || record.isOther === true ? { allowOther: true } : {}),
|
|
327
|
+
...(record.isSecret === true ? { isSecret: true } : {}),
|
|
328
|
+
...(record.multiSelect === true ? { multiSelect: true } : {}),
|
|
329
|
+
}];
|
|
330
|
+
});
|
|
331
|
+
return questions.length > 0 ? questions : undefined;
|
|
332
|
+
}
|
|
333
|
+
function isRecord(value) {
|
|
334
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
335
|
+
}
|
|
336
|
+
function readString(record, key) {
|
|
337
|
+
const value = record[key];
|
|
338
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
339
|
+
}
|
|
252
340
|
export async function main() {
|
|
253
341
|
setDefaultResultOrder('ipv4first');
|
|
254
342
|
const { values: args } = parseArgs({
|
|
@@ -290,12 +378,14 @@ export async function main() {
|
|
|
290
378
|
}
|
|
291
379
|
const codexBin = typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex';
|
|
292
380
|
const codexCliStatus = detectCodexCliVersion(codexBin);
|
|
381
|
+
const useAppServer = supportsCodexAppServer(codexBin);
|
|
293
382
|
if (codexCliStatus.version) {
|
|
294
383
|
console.error(`[canon-codex] Detected Codex CLI ${codexCliStatus.version} (${codexBin})`);
|
|
295
384
|
}
|
|
296
385
|
else {
|
|
297
386
|
console.error(`[canon-codex] Could not detect Codex CLI version for ${codexBin}: ${codexCliStatus.error ?? 'unknown result'}`);
|
|
298
387
|
}
|
|
388
|
+
console.error(`[canon-codex] Codex transport: ${useAppServer ? 'app-server' : 'exec --json'}`);
|
|
299
389
|
const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-codex]', expectedClientType: 'codex' });
|
|
300
390
|
console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
301
391
|
const client = new CanonClient(apiKey, baseUrl);
|
|
@@ -418,14 +508,6 @@ export async function main() {
|
|
|
418
508
|
pendingMembershipChanges.delete(conversationId);
|
|
419
509
|
}
|
|
420
510
|
}
|
|
421
|
-
async function loadSenderRuntimeState(conversationId, senderId) {
|
|
422
|
-
try {
|
|
423
|
-
return normalizeRuntimeTurnState(await rtdbRead(`/turn-state/${conversationId}/${senderId}`));
|
|
424
|
-
}
|
|
425
|
-
catch {
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
511
|
async function loadHydratedInboundContext(input) {
|
|
430
512
|
const [conversation, page] = await Promise.all([
|
|
431
513
|
getConversationMeta(input.conversationId),
|
|
@@ -523,6 +605,61 @@ export async function main() {
|
|
|
523
605
|
function clearStreaming(conversationId) {
|
|
524
606
|
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
525
607
|
}
|
|
608
|
+
function writeCodexStreaming(session, text, status) {
|
|
609
|
+
if (text !== null) {
|
|
610
|
+
session.turnLiveText = text;
|
|
611
|
+
}
|
|
612
|
+
else if (status !== 'thinking' && session.turnLiveText === 'Thinking…') {
|
|
613
|
+
session.turnLiveText = '';
|
|
614
|
+
}
|
|
615
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
616
|
+
text: session.turnLiveText,
|
|
617
|
+
status,
|
|
618
|
+
messageId: session.currentTurnId ?? undefined,
|
|
619
|
+
turnId: session.currentTurnId,
|
|
620
|
+
blocks: session.turnBlocks,
|
|
621
|
+
}).catch(() => { });
|
|
622
|
+
}
|
|
623
|
+
function upsertTurnBlock(session, block) {
|
|
624
|
+
const now = Date.now();
|
|
625
|
+
const index = session.turnBlocks.findIndex((existing) => existing.id === block.id);
|
|
626
|
+
const existing = index >= 0 ? session.turnBlocks[index] : null;
|
|
627
|
+
const next = {
|
|
628
|
+
...(existing ?? {
|
|
629
|
+
sequence: session.turnBlocks.length + 1,
|
|
630
|
+
createdAt: now,
|
|
631
|
+
}),
|
|
632
|
+
...block,
|
|
633
|
+
turnId: session.currentTurnId ?? block.id,
|
|
634
|
+
updatedAt: now,
|
|
635
|
+
};
|
|
636
|
+
session.turnBlocks = index >= 0
|
|
637
|
+
? [
|
|
638
|
+
...session.turnBlocks.slice(0, index),
|
|
639
|
+
next,
|
|
640
|
+
...session.turnBlocks.slice(index + 1),
|
|
641
|
+
]
|
|
642
|
+
: [...session.turnBlocks, next];
|
|
643
|
+
}
|
|
644
|
+
function completeTurnBlock(session, id, summary) {
|
|
645
|
+
const existing = session.turnBlocks.find((block) => block.id === id);
|
|
646
|
+
if (!existing)
|
|
647
|
+
return;
|
|
648
|
+
upsertTurnBlock(session, {
|
|
649
|
+
id,
|
|
650
|
+
kind: existing.kind,
|
|
651
|
+
status: 'completed',
|
|
652
|
+
title: existing.title,
|
|
653
|
+
text: existing.text,
|
|
654
|
+
summary: summary ?? existing.summary,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function buildFinalTurnTrail(session) {
|
|
658
|
+
return buildBoundedTurnTrail(session.turnBlocks.map((block) => ({
|
|
659
|
+
...block,
|
|
660
|
+
turnId: session.currentTurnId ?? block.turnId,
|
|
661
|
+
})));
|
|
662
|
+
}
|
|
526
663
|
async function handoffFinalMessage(conversationId) {
|
|
527
664
|
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
528
665
|
clearStreaming(conversationId);
|
|
@@ -555,6 +692,9 @@ export async function main() {
|
|
|
555
692
|
return;
|
|
556
693
|
session.closed = true;
|
|
557
694
|
stopVisibleWorkSignal(session);
|
|
695
|
+
if ('close' in session.adapter && typeof session.adapter.close === 'function') {
|
|
696
|
+
session.adapter.close();
|
|
697
|
+
}
|
|
558
698
|
releaseConversationEnvironment(session.environment);
|
|
559
699
|
clearStreaming(conversationId);
|
|
560
700
|
runtimeState.clearSessionState(conversationId).catch(() => { });
|
|
@@ -638,11 +778,20 @@ export async function main() {
|
|
|
638
778
|
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
639
779
|
}
|
|
640
780
|
const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
781
|
+
const adapter = useAppServer
|
|
782
|
+
? new CodexAppServerAdapter({
|
|
783
|
+
cwd: sessionCwd,
|
|
784
|
+
threadId: storedThreadId,
|
|
785
|
+
codexBin,
|
|
786
|
+
model: policy.model ?? null,
|
|
787
|
+
sandbox: policy.sandbox,
|
|
788
|
+
approvalPolicy: policy.approvalPolicy,
|
|
789
|
+
addDirs: args['add-dir'] ?? [],
|
|
790
|
+
configOverrides: args.config ?? [],
|
|
791
|
+
fullAuto: policy.fullAuto,
|
|
792
|
+
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
793
|
+
})
|
|
794
|
+
: new CodexConversationAdapter({
|
|
646
795
|
cwd: sessionCwd,
|
|
647
796
|
threadId: storedThreadId,
|
|
648
797
|
codexBin,
|
|
@@ -654,7 +803,12 @@ export async function main() {
|
|
|
654
803
|
configOverrides: args.config ?? [],
|
|
655
804
|
fullAuto: policy.fullAuto,
|
|
656
805
|
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
657
|
-
})
|
|
806
|
+
});
|
|
807
|
+
const session = {
|
|
808
|
+
conversationId,
|
|
809
|
+
cwd: sessionCwd,
|
|
810
|
+
environment,
|
|
811
|
+
adapter,
|
|
658
812
|
queue: [],
|
|
659
813
|
running: false,
|
|
660
814
|
state: {
|
|
@@ -673,6 +827,8 @@ export async function main() {
|
|
|
673
827
|
lastActivity: Date.now(),
|
|
674
828
|
typingKeepaliveTimer: null,
|
|
675
829
|
closed: false,
|
|
830
|
+
turnLiveText: '',
|
|
831
|
+
turnBlocks: [],
|
|
676
832
|
};
|
|
677
833
|
sessions.set(conversationId, session);
|
|
678
834
|
await Promise.all([
|
|
@@ -697,8 +853,8 @@ export async function main() {
|
|
|
697
853
|
pendingSessionCreations.delete(conversationId);
|
|
698
854
|
}
|
|
699
855
|
}
|
|
700
|
-
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = []) {
|
|
701
|
-
const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths, mediaAddDirs };
|
|
856
|
+
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false) {
|
|
857
|
+
const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths, mediaAddDirs, planMode };
|
|
702
858
|
if (toFront) {
|
|
703
859
|
session.queue.unshift(nextPrompt);
|
|
704
860
|
}
|
|
@@ -709,8 +865,276 @@ export async function main() {
|
|
|
709
865
|
writeTurn(session);
|
|
710
866
|
void runNextTurn(session);
|
|
711
867
|
}
|
|
868
|
+
async function waitForRuntimeInputResponse(input) {
|
|
869
|
+
while (Date.now() < input.expiresAt) {
|
|
870
|
+
const response = await client.consumeRuntimeInputResponse({
|
|
871
|
+
conversationId: input.conversationId,
|
|
872
|
+
inputId: input.inputId,
|
|
873
|
+
}).catch(() => null);
|
|
874
|
+
if (response?.status === 'submitted') {
|
|
875
|
+
return { status: 'submitted', value: response.value, answers: response.answers };
|
|
876
|
+
}
|
|
877
|
+
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
878
|
+
return { status: response.status };
|
|
879
|
+
}
|
|
880
|
+
await sleep(1_000);
|
|
881
|
+
}
|
|
882
|
+
const response = await client.consumeRuntimeInputResponse({
|
|
883
|
+
conversationId: input.conversationId,
|
|
884
|
+
inputId: input.inputId,
|
|
885
|
+
}).catch(() => null);
|
|
886
|
+
if (response?.status === 'submitted') {
|
|
887
|
+
return { status: 'submitted', value: response.value, answers: response.answers };
|
|
888
|
+
}
|
|
889
|
+
return { status: 'timeout' };
|
|
890
|
+
}
|
|
891
|
+
async function waitForRuntimeApprovalResponse(input) {
|
|
892
|
+
while (Date.now() < input.expiresAt) {
|
|
893
|
+
const response = await client.consumeRuntimeApprovalResponse({
|
|
894
|
+
conversationId: input.conversationId,
|
|
895
|
+
approvalId: input.approvalId,
|
|
896
|
+
}).catch(() => null);
|
|
897
|
+
if (response?.status === 'allow') {
|
|
898
|
+
return { decision: 'allow', sessionRule: response.sessionRule };
|
|
899
|
+
}
|
|
900
|
+
if (response?.status === 'deny' || response?.status === 'timeout') {
|
|
901
|
+
return { decision: 'deny' };
|
|
902
|
+
}
|
|
903
|
+
await sleep(1_000);
|
|
904
|
+
}
|
|
905
|
+
await client.consumeRuntimeApprovalResponse({
|
|
906
|
+
conversationId: input.conversationId,
|
|
907
|
+
approvalId: input.approvalId,
|
|
908
|
+
}).catch(() => null);
|
|
909
|
+
return { decision: 'deny' };
|
|
910
|
+
}
|
|
911
|
+
async function waitForRuntimeCardResponse(input) {
|
|
912
|
+
while (Date.now() < input.expiresAt) {
|
|
913
|
+
const response = await client.consumeRuntimeCardResponse({
|
|
914
|
+
conversationId: input.conversationId,
|
|
915
|
+
cardId: input.cardId,
|
|
916
|
+
}).catch(() => null);
|
|
917
|
+
if (response?.status === 'submitted') {
|
|
918
|
+
return {
|
|
919
|
+
status: 'submitted',
|
|
920
|
+
...(response.actionId ? { actionId: response.actionId } : {}),
|
|
921
|
+
...(response.values ? { values: response.values } : {}),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
925
|
+
return { status: response.status };
|
|
926
|
+
}
|
|
927
|
+
await sleep(1_000);
|
|
928
|
+
}
|
|
929
|
+
const response = await client.consumeRuntimeCardResponse({
|
|
930
|
+
conversationId: input.conversationId,
|
|
931
|
+
cardId: input.cardId,
|
|
932
|
+
}).catch(() => null);
|
|
933
|
+
if (response?.status === 'submitted') {
|
|
934
|
+
return {
|
|
935
|
+
status: 'submitted',
|
|
936
|
+
...(response.actionId ? { actionId: response.actionId } : {}),
|
|
937
|
+
...(response.values ? { values: response.values } : {}),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return { status: 'timeout' };
|
|
941
|
+
}
|
|
942
|
+
function runtimeCardRequestPayload(method, params) {
|
|
943
|
+
if (method !== 'item/runtimeCard/request'
|
|
944
|
+
&& method !== 'runtimeCard/request') {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
const input = isRecord(params.input) ? params.input : null;
|
|
948
|
+
const args = isRecord(params.arguments) ? params.arguments : null;
|
|
949
|
+
return params.card ?? params.cardDocument ?? input?.card ?? args?.card ?? null;
|
|
950
|
+
}
|
|
951
|
+
async function handleCodexServerRequest(session, request) {
|
|
952
|
+
const requestId = String(request.id);
|
|
953
|
+
const params = request.params;
|
|
954
|
+
const expiresAt = Date.now() + 30 * 60_000;
|
|
955
|
+
const runtimeCardPayload = runtimeCardRequestPayload(request.method, params);
|
|
956
|
+
if (runtimeCardPayload) {
|
|
957
|
+
const card = parseRuntimeCardV1(runtimeCardPayload);
|
|
958
|
+
if (!card) {
|
|
959
|
+
return { status: 'cancelled', error: 'Invalid canon.card.v1 card' };
|
|
960
|
+
}
|
|
961
|
+
const cardId = readString(params, 'cardId')
|
|
962
|
+
?? readString(params, 'itemId')
|
|
963
|
+
?? card.cardId
|
|
964
|
+
?? requestId;
|
|
965
|
+
let requestCreated = false;
|
|
966
|
+
let requestResolved = false;
|
|
967
|
+
try {
|
|
968
|
+
await client.createRuntimeCardRequest({
|
|
969
|
+
conversationId: session.conversationId,
|
|
970
|
+
cardId,
|
|
971
|
+
card: { ...card, cardId },
|
|
972
|
+
expiresAt,
|
|
973
|
+
// Omit responseUserId so the backend targets a reachable member (the
|
|
974
|
+
// owner if present, else the sole other member) — the owner is often
|
|
975
|
+
// not a member of agent-to-user DMs.
|
|
976
|
+
native: {
|
|
977
|
+
runtime: 'codex',
|
|
978
|
+
method: request.method,
|
|
979
|
+
requestId,
|
|
980
|
+
turnId: readString(params, 'turnId') ?? session.currentTurnId ?? undefined,
|
|
981
|
+
handles: {
|
|
982
|
+
itemId: readString(params, 'itemId') ?? '',
|
|
983
|
+
threadId: readString(params, 'threadId') ?? '',
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
turnId: session.currentTurnId ?? undefined,
|
|
987
|
+
});
|
|
988
|
+
requestCreated = true;
|
|
989
|
+
session.turnState = 'waiting_input';
|
|
990
|
+
markTurnProgress(session);
|
|
991
|
+
upsertTurnBlock(session, {
|
|
992
|
+
id: `card:${cardId}`,
|
|
993
|
+
kind: 'input',
|
|
994
|
+
status: 'pending',
|
|
995
|
+
title: card.title,
|
|
996
|
+
summary: card.template ?? 'runtime card',
|
|
997
|
+
});
|
|
998
|
+
writeTurn(session);
|
|
999
|
+
stopVisibleWorkSignal(session);
|
|
1000
|
+
writeCodexStreaming(session, null, 'waiting_input');
|
|
1001
|
+
const response = await waitForRuntimeCardResponse({
|
|
1002
|
+
conversationId: session.conversationId,
|
|
1003
|
+
cardId,
|
|
1004
|
+
expiresAt,
|
|
1005
|
+
});
|
|
1006
|
+
requestResolved = true;
|
|
1007
|
+
const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
|
|
1008
|
+
await client.sendMessage(session.conversationId, outcome.text, {
|
|
1009
|
+
metadata: {
|
|
1010
|
+
...outcome.metadata,
|
|
1011
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1012
|
+
turnSemantics: 'control',
|
|
1013
|
+
replyBehavior: 'suppress_auto_reply',
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
completeTurnBlock(session, `card:${cardId}`, `Card ${response.status}`);
|
|
1017
|
+
if (session.turnState === 'waiting_input') {
|
|
1018
|
+
session.turnState = 'thinking';
|
|
1019
|
+
markTurnProgress(session);
|
|
1020
|
+
writeTurn(session);
|
|
1021
|
+
startVisibleWorkSignal(session);
|
|
1022
|
+
writeCodexStreaming(session, null, 'thinking');
|
|
1023
|
+
}
|
|
1024
|
+
return response;
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
if (requestCreated && !requestResolved) {
|
|
1028
|
+
await client.consumeRuntimeCardResponse({
|
|
1029
|
+
conversationId: session.conversationId,
|
|
1030
|
+
cardId,
|
|
1031
|
+
cancel: true,
|
|
1032
|
+
}).catch(() => null);
|
|
1033
|
+
const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
|
|
1034
|
+
await client.sendMessage(session.conversationId, outcome.text, {
|
|
1035
|
+
metadata: {
|
|
1036
|
+
...outcome.metadata,
|
|
1037
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1038
|
+
turnSemantics: 'control',
|
|
1039
|
+
replyBehavior: 'suppress_auto_reply',
|
|
1040
|
+
},
|
|
1041
|
+
}).catch(() => null);
|
|
1042
|
+
}
|
|
1043
|
+
throw error;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (request.method === 'item/tool/requestUserInput') {
|
|
1047
|
+
const paramsInput = isRecord(params.input) ? params.input : null;
|
|
1048
|
+
const paramsArguments = isRecord(params.arguments) ? params.arguments : null;
|
|
1049
|
+
const questions = mapCodexQuestions(params.questions ?? paramsInput?.questions ?? paramsArguments?.questions);
|
|
1050
|
+
const inputId = readString(params, 'itemId') ?? requestId;
|
|
1051
|
+
await client.createRuntimeInputRequest({
|
|
1052
|
+
conversationId: session.conversationId,
|
|
1053
|
+
inputId,
|
|
1054
|
+
kind: 'clarify',
|
|
1055
|
+
expiresAt,
|
|
1056
|
+
responseUserId: ownerId ?? undefined,
|
|
1057
|
+
title: 'Codex needs input',
|
|
1058
|
+
prompt: questions?.length
|
|
1059
|
+
? 'Codex needs your input to continue.'
|
|
1060
|
+
: 'Codex needs input.',
|
|
1061
|
+
...(questions ? { questions } : {}),
|
|
1062
|
+
sensitive: Boolean(questions?.some((question) => question.isSecret)),
|
|
1063
|
+
native: {
|
|
1064
|
+
runtime: 'codex',
|
|
1065
|
+
method: request.method,
|
|
1066
|
+
requestId,
|
|
1067
|
+
turnId: readString(params, 'turnId') ?? session.currentTurnId ?? undefined,
|
|
1068
|
+
handles: {
|
|
1069
|
+
itemId: readString(params, 'itemId') ?? '',
|
|
1070
|
+
threadId: readString(params, 'threadId') ?? '',
|
|
1071
|
+
},
|
|
1072
|
+
},
|
|
1073
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1074
|
+
});
|
|
1075
|
+
const response = await waitForRuntimeInputResponse({
|
|
1076
|
+
conversationId: session.conversationId,
|
|
1077
|
+
inputId,
|
|
1078
|
+
expiresAt,
|
|
1079
|
+
});
|
|
1080
|
+
return { answers: response.status === 'submitted' ? response.answers ?? {} : {} };
|
|
1081
|
+
}
|
|
1082
|
+
const mappedApproval = mapCodexAppServerApprovalRequest({
|
|
1083
|
+
method: request.method,
|
|
1084
|
+
params,
|
|
1085
|
+
});
|
|
1086
|
+
if (mappedApproval) {
|
|
1087
|
+
const approvalId = readString(params, 'approvalId') ?? readString(params, 'itemId') ?? requestId;
|
|
1088
|
+
await client.createRuntimeApprovalRequest({
|
|
1089
|
+
conversationId: session.conversationId,
|
|
1090
|
+
approvalId,
|
|
1091
|
+
toolName: mappedApproval.toolName,
|
|
1092
|
+
toolSummary: mappedApproval.toolSummary,
|
|
1093
|
+
category: mappedApproval.category,
|
|
1094
|
+
risk: mappedApproval.risk,
|
|
1095
|
+
riskLevel: mappedApproval.riskLevel,
|
|
1096
|
+
native: {
|
|
1097
|
+
...mappedApproval.native,
|
|
1098
|
+
requestId,
|
|
1099
|
+
method: request.method,
|
|
1100
|
+
},
|
|
1101
|
+
details: mappedApproval.details,
|
|
1102
|
+
responseUserId: ownerId ?? undefined,
|
|
1103
|
+
allowSessionRule: true,
|
|
1104
|
+
expiresAt,
|
|
1105
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1106
|
+
});
|
|
1107
|
+
const response = await waitForRuntimeApprovalResponse({
|
|
1108
|
+
conversationId: session.conversationId,
|
|
1109
|
+
approvalId,
|
|
1110
|
+
expiresAt,
|
|
1111
|
+
});
|
|
1112
|
+
if (request.method === 'item/permissions/requestApproval') {
|
|
1113
|
+
return response.decision === 'allow'
|
|
1114
|
+
? { permissions: isRecord(params.permissions) ? params.permissions : {}, scope: response.sessionRule ? 'session' : 'turn' }
|
|
1115
|
+
: { permissions: {}, scope: 'turn' };
|
|
1116
|
+
}
|
|
1117
|
+
return mapCanonApprovalResultToCodexDecision({
|
|
1118
|
+
decision: response.decision,
|
|
1119
|
+
...(response.sessionRule ? { sessionRule: response.sessionRule } : {}),
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
return {};
|
|
1123
|
+
}
|
|
712
1124
|
async function enqueueInboundMessage(input) {
|
|
713
1125
|
knownConversationIds.add(input.conversationId);
|
|
1126
|
+
if (isRecord(input.message.metadata)
|
|
1127
|
+
&& input.message.metadata.type === 'plan_approval_reply'
|
|
1128
|
+
&& typeof input.message.metadata.decision === 'string') {
|
|
1129
|
+
const session = await getOrCreateSession(input.conversationId);
|
|
1130
|
+
const feedback = readString(input.message.metadata, 'feedback');
|
|
1131
|
+
const decision = input.message.metadata.decision;
|
|
1132
|
+
const prompt = decision === 'approve'
|
|
1133
|
+
? 'The plan was approved. Implement the approved plan now.'
|
|
1134
|
+
: `Please revise the plan.${feedback ? `\n\nRevision feedback:\n${feedback}` : ''}`;
|
|
1135
|
+
enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
714
1138
|
let materialized = [];
|
|
715
1139
|
if (input.message.id) {
|
|
716
1140
|
try {
|
|
@@ -723,7 +1147,11 @@ export async function main() {
|
|
|
723
1147
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
724
1148
|
}
|
|
725
1149
|
}
|
|
726
|
-
const
|
|
1150
|
+
const renderedContent = renderInboundContent(input.message, materialized);
|
|
1151
|
+
const planCommand = useAppServer
|
|
1152
|
+
? parsePlanCommand(renderedContent)
|
|
1153
|
+
: { planMode: false, content: renderedContent };
|
|
1154
|
+
const content = planCommand.content;
|
|
727
1155
|
const hydrated = await loadHydratedInboundContext({
|
|
728
1156
|
conversationId: input.conversationId,
|
|
729
1157
|
message: input.message,
|
|
@@ -773,7 +1201,6 @@ export async function main() {
|
|
|
773
1201
|
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
774
1202
|
metadata: {
|
|
775
1203
|
turnSemantics: 'turn_complete',
|
|
776
|
-
turnComplete: true,
|
|
777
1204
|
replyBehavior: 'suppress_auto_reply',
|
|
778
1205
|
},
|
|
779
1206
|
}).catch(() => { });
|
|
@@ -789,14 +1216,14 @@ export async function main() {
|
|
|
789
1216
|
replyContext,
|
|
790
1217
|
});
|
|
791
1218
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
792
|
-
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs);
|
|
1219
|
+
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
|
|
793
1220
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
794
1221
|
await session.adapter.interrupt().catch(() => { });
|
|
795
1222
|
clearStreaming(input.conversationId);
|
|
796
1223
|
client.setTyping(input.conversationId, false).catch(() => { });
|
|
797
1224
|
return;
|
|
798
1225
|
}
|
|
799
|
-
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs);
|
|
1226
|
+
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
|
|
800
1227
|
}
|
|
801
1228
|
async function runNextTurn(session) {
|
|
802
1229
|
if (session.running || session.closed)
|
|
@@ -808,6 +1235,8 @@ export async function main() {
|
|
|
808
1235
|
session.state.lastError = undefined;
|
|
809
1236
|
session.state.state = 'running';
|
|
810
1237
|
session.currentTurnId = randomUUID();
|
|
1238
|
+
session.turnLiveText = '';
|
|
1239
|
+
session.turnBlocks = [];
|
|
811
1240
|
session.currentTurnOpenedAt = Date.now();
|
|
812
1241
|
session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
|
|
813
1242
|
session.lastAcceptedIntent = nextTurn.intent;
|
|
@@ -817,12 +1246,7 @@ export async function main() {
|
|
|
817
1246
|
writeState(session);
|
|
818
1247
|
writeTurn(session);
|
|
819
1248
|
startVisibleWorkSignal(session);
|
|
820
|
-
|
|
821
|
-
text: 'Thinking…',
|
|
822
|
-
status: 'thinking',
|
|
823
|
-
messageId: session.currentTurnId ?? undefined,
|
|
824
|
-
turnId: session.currentTurnId,
|
|
825
|
-
}).catch(() => { });
|
|
1249
|
+
writeCodexStreaming(session, 'Thinking…', 'thinking');
|
|
826
1250
|
try {
|
|
827
1251
|
const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
|
|
828
1252
|
if (modelGuard) {
|
|
@@ -846,12 +1270,31 @@ export async function main() {
|
|
|
846
1270
|
writeTurn(session);
|
|
847
1271
|
stopVisibleWorkSignal(session);
|
|
848
1272
|
client.setTyping(session.conversationId, false).catch(() => { });
|
|
849
|
-
|
|
1273
|
+
writeCodexStreaming(session, event.text, 'streaming');
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (event.type === 'plan.updated') {
|
|
1277
|
+
session.turnState = 'streaming';
|
|
1278
|
+
markTurnProgress(session);
|
|
1279
|
+
writeTurn(session);
|
|
1280
|
+
stopVisibleWorkSignal(session);
|
|
1281
|
+
client.setTyping(session.conversationId, false).catch(() => { });
|
|
1282
|
+
upsertTurnBlock(session, {
|
|
1283
|
+
id: `plan:${session.currentTurnId}`,
|
|
1284
|
+
kind: 'plan',
|
|
1285
|
+
status: 'running',
|
|
1286
|
+
title: 'Plan',
|
|
850
1287
|
text: event.text,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1288
|
+
});
|
|
1289
|
+
writeCodexStreaming(session, event.text, 'streaming');
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (event.type === 'waiting') {
|
|
1293
|
+
session.turnState = 'waiting_input';
|
|
1294
|
+
markTurnProgress(session);
|
|
1295
|
+
writeTurn(session);
|
|
1296
|
+
stopVisibleWorkSignal(session);
|
|
1297
|
+
writeCodexStreaming(session, null, 'waiting_input');
|
|
855
1298
|
return;
|
|
856
1299
|
}
|
|
857
1300
|
if (event.type === 'command.started') {
|
|
@@ -859,20 +1302,24 @@ export async function main() {
|
|
|
859
1302
|
markTurnProgress(session);
|
|
860
1303
|
writeTurn(session);
|
|
861
1304
|
startVisibleWorkSignal(session);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1305
|
+
upsertTurnBlock(session, {
|
|
1306
|
+
id: `command:${session.currentTurnId}`,
|
|
1307
|
+
kind: 'tool',
|
|
1308
|
+
status: 'running',
|
|
1309
|
+
title: summarizeCommand(event.command),
|
|
1310
|
+
summary: 'Command running',
|
|
1311
|
+
});
|
|
1312
|
+
writeCodexStreaming(session, null, 'tool');
|
|
868
1313
|
return;
|
|
869
1314
|
}
|
|
870
1315
|
if (event.type === 'command.completed') {
|
|
1316
|
+
completeTurnBlock(session, `command:${session.currentTurnId}`, 'Command completed');
|
|
871
1317
|
if (session.turnState === 'tool') {
|
|
872
1318
|
session.turnState = 'thinking';
|
|
873
1319
|
markTurnProgress(session);
|
|
874
1320
|
writeTurn(session);
|
|
875
1321
|
startVisibleWorkSignal(session);
|
|
1322
|
+
writeCodexStreaming(session, null, 'thinking');
|
|
876
1323
|
}
|
|
877
1324
|
return;
|
|
878
1325
|
}
|
|
@@ -887,7 +1334,10 @@ export async function main() {
|
|
|
887
1334
|
clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
888
1335
|
session.adapter.clearThreadId();
|
|
889
1336
|
};
|
|
890
|
-
const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs
|
|
1337
|
+
const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs, {
|
|
1338
|
+
planMode: nextTurn.planMode,
|
|
1339
|
+
onServerRequest: (request) => handleCodexServerRequest(session, request),
|
|
1340
|
+
});
|
|
891
1341
|
let result = await runTurnOnce();
|
|
892
1342
|
if (!result.interrupted
|
|
893
1343
|
&& !result.finalMessage
|
|
@@ -901,10 +1351,28 @@ export async function main() {
|
|
|
901
1351
|
if (result.threadId && !session.resetRequested) {
|
|
902
1352
|
saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
|
|
903
1353
|
}
|
|
904
|
-
if (!result.interrupted && result.finalMessage) {
|
|
1354
|
+
if (!result.interrupted && result.finalMessage && nextTurn.planMode) {
|
|
1355
|
+
const planApproval = buildPlanApprovalRequest(session.currentTurnId ?? randomUUID(), 'Plan ready for review.', {
|
|
1356
|
+
responseUserId: ownerId ?? undefined,
|
|
1357
|
+
title: 'Codex Plan',
|
|
1358
|
+
body: result.finalMessage,
|
|
1359
|
+
});
|
|
1360
|
+
await client.sendMessage(session.conversationId, planApproval.text, {
|
|
1361
|
+
metadata: {
|
|
1362
|
+
...planApproval.metadata,
|
|
1363
|
+
turnId: session.currentTurnId,
|
|
1364
|
+
turnSemantics: 'control',
|
|
1365
|
+
replyBehavior: 'suppress_auto_reply',
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
await handoffFinalMessage(session.conversationId);
|
|
1369
|
+
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent plan approval card`);
|
|
1370
|
+
}
|
|
1371
|
+
else if (!result.interrupted && result.finalMessage) {
|
|
905
1372
|
if (isRecoverableCodexThreadError(result.errorText)) {
|
|
906
1373
|
clearStoredThread();
|
|
907
1374
|
}
|
|
1375
|
+
const turnTrail = buildFinalTurnTrail(session);
|
|
908
1376
|
await client.sendMessage(session.conversationId, result.finalMessage, {
|
|
909
1377
|
...(session.activeSelfContextId
|
|
910
1378
|
? { selfContextId: session.activeSelfContextId }
|
|
@@ -912,8 +1380,8 @@ export async function main() {
|
|
|
912
1380
|
metadata: {
|
|
913
1381
|
turnId: session.currentTurnId,
|
|
914
1382
|
turnSemantics: 'turn_complete',
|
|
915
|
-
turnComplete: true,
|
|
916
1383
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1384
|
+
...(turnTrail.length > 0 ? { turnTrail } : {}),
|
|
917
1385
|
},
|
|
918
1386
|
});
|
|
919
1387
|
await handoffFinalMessage(session.conversationId);
|
|
@@ -926,6 +1394,7 @@ export async function main() {
|
|
|
926
1394
|
if (result.errorText) {
|
|
927
1395
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
|
|
928
1396
|
}
|
|
1397
|
+
const turnTrail = buildFinalTurnTrail(session);
|
|
929
1398
|
await client.sendMessage(session.conversationId, userVisibleError, {
|
|
930
1399
|
...(session.activeSelfContextId
|
|
931
1400
|
? { selfContextId: session.activeSelfContextId }
|
|
@@ -933,8 +1402,8 @@ export async function main() {
|
|
|
933
1402
|
metadata: {
|
|
934
1403
|
turnId: session.currentTurnId,
|
|
935
1404
|
turnSemantics: 'turn_complete',
|
|
936
|
-
turnComplete: true,
|
|
937
1405
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1406
|
+
...(turnTrail.length > 0 ? { turnTrail } : {}),
|
|
938
1407
|
},
|
|
939
1408
|
});
|
|
940
1409
|
await handoffFinalMessage(session.conversationId);
|
|
@@ -964,7 +1433,6 @@ export async function main() {
|
|
|
964
1433
|
metadata: {
|
|
965
1434
|
turnId: session.currentTurnId,
|
|
966
1435
|
turnSemantics: 'turn_complete',
|
|
967
|
-
turnComplete: true,
|
|
968
1436
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
969
1437
|
},
|
|
970
1438
|
}).catch(() => { });
|
|
@@ -1024,6 +1492,8 @@ export async function main() {
|
|
|
1024
1492
|
permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
1025
1493
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
1026
1494
|
presentation: runtimePresentation,
|
|
1495
|
+
supportsPlanMode: useAppServer,
|
|
1496
|
+
supportsRichCards: useAppServer,
|
|
1027
1497
|
}),
|
|
1028
1498
|
};
|
|
1029
1499
|
async function baselineControlSignal(conversationId) {
|
|
@@ -1108,12 +1578,14 @@ export async function main() {
|
|
|
1108
1578
|
{
|
|
1109
1579
|
id: 'transport',
|
|
1110
1580
|
label: 'Transport',
|
|
1111
|
-
value: 'exec --json',
|
|
1581
|
+
value: useAppServer ? 'app-server' : 'exec --json',
|
|
1112
1582
|
},
|
|
1113
1583
|
{
|
|
1114
1584
|
id: 'streaming',
|
|
1115
1585
|
label: 'Live output',
|
|
1116
|
-
value:
|
|
1586
|
+
value: useAppServer
|
|
1587
|
+
? 'Plans, questions, approvals, tools, and message deltas'
|
|
1588
|
+
: 'Thinking, tools, and completed-message previews',
|
|
1117
1589
|
},
|
|
1118
1590
|
{
|
|
1119
1591
|
id: 'codex-cli',
|
|
@@ -1124,8 +1596,8 @@ export async function main() {
|
|
|
1124
1596
|
{
|
|
1125
1597
|
id: 'nativeActions',
|
|
1126
1598
|
label: 'Native actions',
|
|
1127
|
-
value: 'Limited until app-server transport',
|
|
1128
|
-
tone: 'warning',
|
|
1599
|
+
value: useAppServer ? 'Enabled' : 'Limited until app-server transport',
|
|
1600
|
+
...(useAppServer ? {} : { tone: 'warning' }),
|
|
1129
1601
|
},
|
|
1130
1602
|
],
|
|
1131
1603
|
execution: {
|
|
@@ -1139,8 +1611,9 @@ export async function main() {
|
|
|
1139
1611
|
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
1140
1612
|
},
|
|
1141
1613
|
notes: [
|
|
1142
|
-
|
|
1143
|
-
|
|
1614
|
+
useAppServer
|
|
1615
|
+
? 'This Codex host uses the app-server transport, so Canon can route native plan mode, runtime questions, approvals, and live turn updates.'
|
|
1616
|
+
: 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not native plan questions or structured approvals.',
|
|
1144
1617
|
],
|
|
1145
1618
|
};
|
|
1146
1619
|
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
@@ -1215,6 +1688,8 @@ export async function main() {
|
|
|
1215
1688
|
permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
1216
1689
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
1217
1690
|
presentation: runtimePresentation,
|
|
1691
|
+
supportsPlanMode: useAppServer,
|
|
1692
|
+
supportsRichCards: useAppServer,
|
|
1218
1693
|
}),
|
|
1219
1694
|
};
|
|
1220
1695
|
}
|
|
@@ -1235,6 +1710,8 @@ export async function main() {
|
|
|
1235
1710
|
permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
1236
1711
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
1237
1712
|
presentation: runtimePresentation,
|
|
1713
|
+
supportsPlanMode: useAppServer,
|
|
1714
|
+
supportsRichCards: useAppServer,
|
|
1238
1715
|
}),
|
|
1239
1716
|
};
|
|
1240
1717
|
}
|
|
@@ -1264,13 +1741,9 @@ export async function main() {
|
|
|
1264
1741
|
? inboundMessages.slice(cursorIndex + 1)
|
|
1265
1742
|
: inboundMessages.slice(-1);
|
|
1266
1743
|
for (const latestMessage of messagesToRecover) {
|
|
1267
|
-
const senderTurnState = latestMessage.senderType === 'ai_agent'
|
|
1268
|
-
? await loadSenderRuntimeState(conversation.id, latestMessage.senderId)
|
|
1269
|
-
: null;
|
|
1270
1744
|
const triggerDecision = shouldTriggerAgentTurn({
|
|
1271
1745
|
senderType: latestMessage.senderType ?? 'human',
|
|
1272
1746
|
metadata: latestMessage.metadata,
|
|
1273
|
-
senderTurnState,
|
|
1274
1747
|
});
|
|
1275
1748
|
if (!triggerDecision.allow) {
|
|
1276
1749
|
console.error(`[canon-codex] [${conversation.id.slice(0, 8)}] Skipping startup recovery for suppressed message (${triggerDecision.reason})`);
|