@canonmsg/codex-plugin 0.22.0 → 0.22.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.
- package/dist/host.d.ts +12 -0
- package/dist/host.js +196 -29
- package/dist/startup-recovery.d.ts +1 -0
- package/dist/startup-recovery.js +15 -2
- package/package.json +3 -3
package/dist/host.d.ts
CHANGED
|
@@ -70,5 +70,17 @@ export declare function buildCodexRuntimeDescriptor(input: {
|
|
|
70
70
|
supportsRichCards?: boolean;
|
|
71
71
|
skills?: ReadonlyArray<CodexSkillMetadata>;
|
|
72
72
|
}): CanonRuntimeDescriptor;
|
|
73
|
+
export declare function buildCodexTurnResponseRouting(input: {
|
|
74
|
+
requestingUserId?: string | null;
|
|
75
|
+
ownerId?: string | null;
|
|
76
|
+
ownerOnly?: boolean;
|
|
77
|
+
}): {
|
|
78
|
+
responseUserId?: string;
|
|
79
|
+
allowSessionRule: boolean;
|
|
80
|
+
};
|
|
81
|
+
export declare function getCodexRequestingUserId(message: {
|
|
82
|
+
senderId: string;
|
|
83
|
+
senderType?: 'human' | 'ai_agent';
|
|
84
|
+
}): string | null;
|
|
73
85
|
export declare function main(): Promise<void>;
|
|
74
86
|
export {};
|
package/dist/host.js
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname } from 'node:path';
|
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
8
8
|
import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
|
|
9
|
-
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, 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, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
9
|
+
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, 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, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
10
|
import { decideAutoReply, } from './inbound-policy.js';
|
|
11
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
12
|
import { CodexAppServerAdapter } from './app-server-adapter.js';
|
|
@@ -19,6 +19,7 @@ import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodex
|
|
|
19
19
|
import { startCodexStreamInBackground } from './host-lifecycle.js';
|
|
20
20
|
import { createCodexControlPoller } from './control-channel.js';
|
|
21
21
|
import { runCli } from './cli-entry.js';
|
|
22
|
+
import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
|
|
22
23
|
import { applyTextSegmentBlock, beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
|
|
23
24
|
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
24
25
|
|
|
@@ -228,6 +229,18 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
228
229
|
coreControls: descriptor.coreControls.filter((control) => control.id !== 'model'),
|
|
229
230
|
};
|
|
230
231
|
}
|
|
232
|
+
export function buildCodexTurnResponseRouting(input) {
|
|
233
|
+
const responseUserId = input.ownerOnly
|
|
234
|
+
? input.ownerId ?? undefined
|
|
235
|
+
: input.requestingUserId ?? input.ownerId ?? undefined;
|
|
236
|
+
return {
|
|
237
|
+
...(responseUserId ? { responseUserId } : {}),
|
|
238
|
+
allowSessionRule: Boolean(responseUserId && input.ownerId && responseUserId === input.ownerId),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export function getCodexRequestingUserId(message) {
|
|
242
|
+
return message.senderType === 'human' ? message.senderId : null;
|
|
243
|
+
}
|
|
231
244
|
function modelOptionLabel(model) {
|
|
232
245
|
if (/^gpt-5\.5(?:$|[-_:])/i.test(model))
|
|
233
246
|
return 'GPT-5.5';
|
|
@@ -534,7 +547,10 @@ export async function main() {
|
|
|
534
547
|
kind: 'approval',
|
|
535
548
|
generateId: () => randomUUID(),
|
|
536
549
|
create: async ({ client: apiClient, ctx, conversationId, requestId, payload, expiresAt }) => {
|
|
537
|
-
|
|
550
|
+
const responseUserId = (payload.responseUserId ?? ctx.ownerId) || undefined;
|
|
551
|
+
const allowSessionRule = payload.allowSessionRule !== false
|
|
552
|
+
&& Boolean(responseUserId && responseUserId === ctx.ownerId);
|
|
553
|
+
const created = await apiClient.createRuntimeApprovalRequest({
|
|
538
554
|
conversationId,
|
|
539
555
|
approvalId: requestId,
|
|
540
556
|
toolName: payload.toolName,
|
|
@@ -545,14 +561,17 @@ export async function main() {
|
|
|
545
561
|
native: payload.native,
|
|
546
562
|
details: payload.details,
|
|
547
563
|
...(payload.diff ? { diff: payload.diff } : {}),
|
|
548
|
-
responseUserId
|
|
549
|
-
allowSessionRule
|
|
564
|
+
...(responseUserId ? { responseUserId } : {}),
|
|
565
|
+
allowSessionRule,
|
|
550
566
|
expiresAt,
|
|
551
567
|
...(payload.turnId ? { turnId: payload.turnId } : {}),
|
|
552
568
|
});
|
|
553
|
-
return {
|
|
569
|
+
return {
|
|
570
|
+
requestId,
|
|
571
|
+
state: { allowSessionRule: created.allowSessionRule ?? allowSessionRule },
|
|
572
|
+
};
|
|
554
573
|
},
|
|
555
|
-
consume: async ({ client: apiClient, conversationId, requestId }) => {
|
|
574
|
+
consume: async ({ client: apiClient, conversationId, requestId, state }) => {
|
|
556
575
|
const response = await apiClient
|
|
557
576
|
.consumeRuntimeApprovalResponse({ conversationId, approvalId: requestId })
|
|
558
577
|
.catch(() => null);
|
|
@@ -561,7 +580,9 @@ export async function main() {
|
|
|
561
580
|
state: 'resolved',
|
|
562
581
|
result: {
|
|
563
582
|
decision: 'allow',
|
|
564
|
-
...(
|
|
583
|
+
...(state?.allowSessionRule === true && response.sessionRule
|
|
584
|
+
? { sessionRule: response.sessionRule }
|
|
585
|
+
: {}),
|
|
565
586
|
},
|
|
566
587
|
};
|
|
567
588
|
}
|
|
@@ -609,6 +630,7 @@ export async function main() {
|
|
|
609
630
|
});
|
|
610
631
|
const sessions = new Map();
|
|
611
632
|
const pendingSessionCreations = new Map();
|
|
633
|
+
let inboundRecoverySequence = 0;
|
|
612
634
|
const conversationCache = new Map();
|
|
613
635
|
const knownConversationIds = new Set();
|
|
614
636
|
const promptedGroupContextConversationIds = new Set();
|
|
@@ -754,6 +776,29 @@ export async function main() {
|
|
|
754
776
|
return;
|
|
755
777
|
await client.updateMessageDisposition(conversationId, sourceMessageId, 'accepted_now').catch(() => { });
|
|
756
778
|
}
|
|
779
|
+
function persistInboundRecoveryCursor(conversationId, messageId) {
|
|
780
|
+
if (!messageId)
|
|
781
|
+
return;
|
|
782
|
+
try {
|
|
783
|
+
saveRuntimeSessionState(runtimeId, {
|
|
784
|
+
conversationId,
|
|
785
|
+
baseCwd: workingDir,
|
|
786
|
+
lastInboundMessageId: messageId,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Failed to persist inbound recovery cursor:`, error instanceof Error ? error.message : error);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function persistInboundRecoveryCursorWhenIdle(conversationId, messageId) {
|
|
794
|
+
const session = sessions.get(conversationId);
|
|
795
|
+
// A later queued turn will advance past this non-triggering message. Do
|
|
796
|
+
// not write it ahead of earlier work and then let that work regress the
|
|
797
|
+
// cursor when it completes.
|
|
798
|
+
if (session?.running || (session?.queue.length ?? 0) > 0)
|
|
799
|
+
return;
|
|
800
|
+
persistInboundRecoveryCursor(conversationId, messageId);
|
|
801
|
+
}
|
|
757
802
|
async function markQueuedPromptsRejected(conversationId, prompts) {
|
|
758
803
|
await Promise.all(prompts.map((prompt) => {
|
|
759
804
|
if (!prompt.markAccepted || !prompt.sourceMessageId)
|
|
@@ -761,6 +806,18 @@ export async function main() {
|
|
|
761
806
|
return client.updateMessageDisposition(conversationId, prompt.sourceMessageId, 'rejected').catch(() => { });
|
|
762
807
|
}));
|
|
763
808
|
}
|
|
809
|
+
function rememberDroppedRecoveryCursor(session, prompts) {
|
|
810
|
+
const latest = prompts.reduce((current, prompt) => (prompt.sourceMessageId && (!current || prompt.recoverySequence > current.recoverySequence)
|
|
811
|
+
? prompt
|
|
812
|
+
: current), null)?.sourceMessageId;
|
|
813
|
+
if (!latest)
|
|
814
|
+
return;
|
|
815
|
+
if (session.running) {
|
|
816
|
+
session.pendingDroppedRecoveryCursor = latest;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
persistInboundRecoveryCursor(session.conversationId, latest);
|
|
820
|
+
}
|
|
764
821
|
function removeQueuedPrompt(conversationId, sourceMessageId) {
|
|
765
822
|
const session = sessions.get(conversationId);
|
|
766
823
|
if (!session || session.queue.length === 0)
|
|
@@ -888,6 +945,7 @@ export async function main() {
|
|
|
888
945
|
session.resetRequested = true;
|
|
889
946
|
const droppedPrompts = session.queue.splice(0);
|
|
890
947
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
948
|
+
rememberDroppedRecoveryCursor(session, droppedPrompts);
|
|
891
949
|
clearStoredThreadId(runtimeId, agentId, conversationId, session.environment.baseCwd, session.environment.mode);
|
|
892
950
|
session.adapter.clearThreadId();
|
|
893
951
|
session.activeSelfContextId = null;
|
|
@@ -1012,6 +1070,7 @@ export async function main() {
|
|
|
1012
1070
|
currentTurnCanUseCodexAppTools: false,
|
|
1013
1071
|
activeSelfContextId: null,
|
|
1014
1072
|
lastAcceptedIntent: null,
|
|
1073
|
+
pendingDroppedRecoveryCursor: null,
|
|
1015
1074
|
resetRequested: false,
|
|
1016
1075
|
lastActivity: Date.now(),
|
|
1017
1076
|
typingKeepaliveTimer: null,
|
|
@@ -1040,7 +1099,7 @@ export async function main() {
|
|
|
1040
1099
|
pendingSessionCreations.delete(conversationId);
|
|
1041
1100
|
}
|
|
1042
1101
|
}
|
|
1043
|
-
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled', canUseCodexAppTools = false) {
|
|
1102
|
+
function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled', canUseCodexAppTools = false, requestingUserId = null) {
|
|
1044
1103
|
const nextPrompt = {
|
|
1045
1104
|
prompt,
|
|
1046
1105
|
intent,
|
|
@@ -1051,6 +1110,8 @@ export async function main() {
|
|
|
1051
1110
|
planMode,
|
|
1052
1111
|
artifactRoutingMode,
|
|
1053
1112
|
canUseCodexAppTools,
|
|
1113
|
+
requestingUserId,
|
|
1114
|
+
recoverySequence: ++inboundRecoverySequence,
|
|
1054
1115
|
};
|
|
1055
1116
|
if (toFront) {
|
|
1056
1117
|
session.queue.unshift(nextPrompt);
|
|
@@ -1076,7 +1137,7 @@ export async function main() {
|
|
|
1076
1137
|
const args = isRecord(params.arguments) ? params.arguments : null;
|
|
1077
1138
|
return params.card ?? params.cardDocument ?? input?.card ?? args?.card ?? null;
|
|
1078
1139
|
}
|
|
1079
|
-
async function handleCodexServerRequest(session, request) {
|
|
1140
|
+
async function handleCodexServerRequest(session, request, requestingUserId) {
|
|
1080
1141
|
const requestId = String(request.id);
|
|
1081
1142
|
const params = request.params;
|
|
1082
1143
|
const expiresAt = Date.now() + 30 * 60_000;
|
|
@@ -1108,11 +1169,10 @@ export async function main() {
|
|
|
1108
1169
|
?? requestId;
|
|
1109
1170
|
let requestCreated = false;
|
|
1110
1171
|
let requestResolved = false;
|
|
1172
|
+
const responseRouting = buildCodexTurnResponseRouting({ requestingUserId, ownerId });
|
|
1111
1173
|
try {
|
|
1112
|
-
// Built-in card descriptor owns create+poll;
|
|
1113
|
-
//
|
|
1114
|
-
// is omitted (default `infer` policy) so the backend targets a reachable
|
|
1115
|
-
// member (owner is often not a member of agent-to-user DMs).
|
|
1174
|
+
// Built-in card descriptor owns create+poll; Codex passes the native
|
|
1175
|
+
// handles, turn context, and the human responder captured for this turn.
|
|
1116
1176
|
const cardResult = await runtimeRequests.request('card', session.conversationId, {
|
|
1117
1177
|
card,
|
|
1118
1178
|
native: {
|
|
@@ -1126,6 +1186,9 @@ export async function main() {
|
|
|
1126
1186
|
},
|
|
1127
1187
|
},
|
|
1128
1188
|
turnId: session.currentTurnId ?? undefined,
|
|
1189
|
+
...(responseRouting.responseUserId
|
|
1190
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1191
|
+
: {}),
|
|
1129
1192
|
}, {
|
|
1130
1193
|
requestId: cardId,
|
|
1131
1194
|
expiresAt,
|
|
@@ -1199,8 +1262,12 @@ export async function main() {
|
|
|
1199
1262
|
const paramsArguments = isRecord(params.arguments) ? params.arguments : null;
|
|
1200
1263
|
const questions = mapCodexQuestions(params.questions ?? paramsInput?.questions ?? paramsArguments?.questions);
|
|
1201
1264
|
const inputId = readString(params, 'itemId') ?? requestId;
|
|
1202
|
-
|
|
1203
|
-
|
|
1265
|
+
const sensitive = Boolean(questions?.some((question) => question.isSecret));
|
|
1266
|
+
const responseRouting = buildCodexTurnResponseRouting({
|
|
1267
|
+
requestingUserId,
|
|
1268
|
+
ownerId,
|
|
1269
|
+
ownerOnly: sensitive,
|
|
1270
|
+
});
|
|
1204
1271
|
const response = await runtimeRequests.request('input', session.conversationId, {
|
|
1205
1272
|
kind: 'clarify',
|
|
1206
1273
|
title: 'Codex needs input',
|
|
@@ -1208,7 +1275,10 @@ export async function main() {
|
|
|
1208
1275
|
? 'Codex needs your input to continue.'
|
|
1209
1276
|
: 'Codex needs input.',
|
|
1210
1277
|
...(questions ? { questions } : {}),
|
|
1211
|
-
sensitive
|
|
1278
|
+
sensitive,
|
|
1279
|
+
...(responseRouting.responseUserId
|
|
1280
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1281
|
+
: {}),
|
|
1212
1282
|
native: {
|
|
1213
1283
|
runtime: 'codex',
|
|
1214
1284
|
method: request.method,
|
|
@@ -1229,11 +1299,15 @@ export async function main() {
|
|
|
1229
1299
|
});
|
|
1230
1300
|
if (mappedApproval) {
|
|
1231
1301
|
const approvalId = readString(params, 'approvalId') ?? readString(params, 'itemId') ?? requestId;
|
|
1232
|
-
|
|
1302
|
+
const responseRouting = buildCodexTurnResponseRouting({ requestingUserId, ownerId });
|
|
1233
1303
|
const response = await runtimeRequests.request('approval', session.conversationId, {
|
|
1234
1304
|
...mappedApproval,
|
|
1235
1305
|
native: { ...mappedApproval.native, requestId, method: request.method },
|
|
1236
1306
|
turnId: session.currentTurnId ?? undefined,
|
|
1307
|
+
...(responseRouting.responseUserId
|
|
1308
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1309
|
+
: {}),
|
|
1310
|
+
allowSessionRule: responseRouting.allowSessionRule,
|
|
1237
1311
|
}, { requestId: approvalId, expiresAt });
|
|
1238
1312
|
if (request.method === 'item/permissions/requestApproval') {
|
|
1239
1313
|
return response.decision === 'allow'
|
|
@@ -1251,6 +1325,7 @@ export async function main() {
|
|
|
1251
1325
|
knownConversationIds.add(input.conversationId);
|
|
1252
1326
|
if (input.turnDispatch && input.turnDispatch.kind !== 'run_turn') {
|
|
1253
1327
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed server-dispatched turn: ${input.turnDispatch.reason}`);
|
|
1328
|
+
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1254
1329
|
return;
|
|
1255
1330
|
}
|
|
1256
1331
|
if (isRecord(input.message.metadata)
|
|
@@ -1264,7 +1339,7 @@ export async function main() {
|
|
|
1264
1339
|
: decision === 'reject'
|
|
1265
1340
|
? `The plan was declined — keep planning and wait for guidance before implementing.${feedback ? `\n\nNotes:\n${feedback}` : ''}`
|
|
1266
1341
|
: `Please revise the plan.${feedback ? `\n\nRevision feedback:\n${feedback}` : ''}`;
|
|
1267
|
-
enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve', 'disabled', input.isOwner);
|
|
1342
|
+
enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve', 'disabled', input.isOwner, getCodexRequestingUserId(input.message));
|
|
1268
1343
|
return;
|
|
1269
1344
|
}
|
|
1270
1345
|
let materialized = [];
|
|
@@ -1322,6 +1397,7 @@ export async function main() {
|
|
|
1322
1397
|
: decideAutoReply(participantContext, behavior);
|
|
1323
1398
|
if (!autoReply.allow) {
|
|
1324
1399
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
1400
|
+
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1325
1401
|
return;
|
|
1326
1402
|
}
|
|
1327
1403
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
@@ -1345,6 +1421,7 @@ export async function main() {
|
|
|
1345
1421
|
replyBehavior: 'suppress_auto_reply',
|
|
1346
1422
|
},
|
|
1347
1423
|
}).catch(() => { });
|
|
1424
|
+
persistInboundRecoveryCursor(input.conversationId, input.message.id);
|
|
1348
1425
|
return;
|
|
1349
1426
|
}
|
|
1350
1427
|
session.activeSelfContextId = activeSelfContextId;
|
|
@@ -1361,7 +1438,7 @@ export async function main() {
|
|
|
1361
1438
|
});
|
|
1362
1439
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
1363
1440
|
const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
|
|
1364
|
-
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner);
|
|
1441
|
+
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner, getCodexRequestingUserId(input.message));
|
|
1365
1442
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
1366
1443
|
await session.adapter.interrupt().catch(() => { });
|
|
1367
1444
|
clearStreaming(input.conversationId);
|
|
@@ -1369,7 +1446,7 @@ export async function main() {
|
|
|
1369
1446
|
return;
|
|
1370
1447
|
}
|
|
1371
1448
|
const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
|
|
1372
|
-
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner);
|
|
1449
|
+
enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner, getCodexRequestingUserId(input.message));
|
|
1373
1450
|
}
|
|
1374
1451
|
function sendTurnArtifactFile(session, file) {
|
|
1375
1452
|
return sendMediaFileMessage(client, session.conversationId, file.path, '', {
|
|
@@ -1568,7 +1645,7 @@ export async function main() {
|
|
|
1568
1645
|
};
|
|
1569
1646
|
const runTurnOnce = () => session.adapter.runTurn(turnPrompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs, {
|
|
1570
1647
|
planMode: nextTurn.planMode,
|
|
1571
|
-
onServerRequest: (request) => handleCodexServerRequest(session, request),
|
|
1648
|
+
onServerRequest: (request) => handleCodexServerRequest(session, request, nextTurn.requestingUserId ?? null),
|
|
1572
1649
|
});
|
|
1573
1650
|
let result = await runTurnOnce();
|
|
1574
1651
|
if (!result.interrupted
|
|
@@ -1585,6 +1662,10 @@ export async function main() {
|
|
|
1585
1662
|
}
|
|
1586
1663
|
if (!result.interrupted && result.finalMessage && nextTurn.planMode) {
|
|
1587
1664
|
await routeArtifactsOnce();
|
|
1665
|
+
const responseRouting = buildCodexTurnResponseRouting({
|
|
1666
|
+
requestingUserId: nextTurn.requestingUserId,
|
|
1667
|
+
ownerId,
|
|
1668
|
+
});
|
|
1588
1669
|
// Route plan-CREATE through the unified spine (`/runtime-plan/request`):
|
|
1589
1670
|
// seeds the server-owned pending node/attention/state and authors the
|
|
1590
1671
|
// `plan_approval` card. Resolution is UNCHANGED — Codex still re-queues a
|
|
@@ -1594,7 +1675,9 @@ export async function main() {
|
|
|
1594
1675
|
planId: session.currentTurnId ?? randomUUID(),
|
|
1595
1676
|
title: 'Codex Plan',
|
|
1596
1677
|
body: result.finalMessage,
|
|
1597
|
-
...(
|
|
1678
|
+
...(responseRouting.responseUserId
|
|
1679
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1680
|
+
: {}),
|
|
1598
1681
|
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
1599
1682
|
});
|
|
1600
1683
|
await handoffFinalMessage(session.conversationId);
|
|
@@ -1614,6 +1697,7 @@ export async function main() {
|
|
|
1614
1697
|
metadata: {
|
|
1615
1698
|
turnId: session.currentTurnId,
|
|
1616
1699
|
turnSemantics: 'turn_complete',
|
|
1700
|
+
...(nextTurn.sourceMessageId ? { sourceMessageId: nextTurn.sourceMessageId } : {}),
|
|
1617
1701
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1618
1702
|
...(turnTrail.length > 0 ? { turnTrail } : {}),
|
|
1619
1703
|
},
|
|
@@ -1638,6 +1722,7 @@ export async function main() {
|
|
|
1638
1722
|
metadata: {
|
|
1639
1723
|
turnId: session.currentTurnId,
|
|
1640
1724
|
turnSemantics: 'turn_complete',
|
|
1725
|
+
...(nextTurn.sourceMessageId ? { sourceMessageId: nextTurn.sourceMessageId } : {}),
|
|
1641
1726
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1642
1727
|
...(turnTrail.length > 0 ? { turnTrail } : {}),
|
|
1643
1728
|
},
|
|
@@ -1674,6 +1759,7 @@ export async function main() {
|
|
|
1674
1759
|
metadata: {
|
|
1675
1760
|
turnId: session.currentTurnId,
|
|
1676
1761
|
turnSemantics: 'turn_complete',
|
|
1762
|
+
...(nextTurn.sourceMessageId ? { sourceMessageId: nextTurn.sourceMessageId } : {}),
|
|
1677
1763
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1678
1764
|
},
|
|
1679
1765
|
}).catch(() => { });
|
|
@@ -1684,6 +1770,11 @@ export async function main() {
|
|
|
1684
1770
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
1685
1771
|
}
|
|
1686
1772
|
finally {
|
|
1773
|
+
persistInboundRecoveryCursor(session.conversationId, nextTurn.sourceMessageId);
|
|
1774
|
+
if (session.pendingDroppedRecoveryCursor) {
|
|
1775
|
+
persistInboundRecoveryCursor(session.conversationId, session.pendingDroppedRecoveryCursor);
|
|
1776
|
+
session.pendingDroppedRecoveryCursor = null;
|
|
1777
|
+
}
|
|
1687
1778
|
stopVisibleWorkSignal(session);
|
|
1688
1779
|
session.running = false;
|
|
1689
1780
|
session.state.state = 'idle';
|
|
@@ -1702,6 +1793,30 @@ export async function main() {
|
|
|
1702
1793
|
}
|
|
1703
1794
|
}
|
|
1704
1795
|
}
|
|
1796
|
+
const acceptedInboundMessageIds = new Set();
|
|
1797
|
+
const inFlightInboundMessageIds = new Set();
|
|
1798
|
+
function claimInboundMessageId(messageId) {
|
|
1799
|
+
if (!messageId)
|
|
1800
|
+
return true;
|
|
1801
|
+
if (acceptedInboundMessageIds.has(messageId) || inFlightInboundMessageIds.has(messageId))
|
|
1802
|
+
return false;
|
|
1803
|
+
inFlightInboundMessageIds.add(messageId);
|
|
1804
|
+
return true;
|
|
1805
|
+
}
|
|
1806
|
+
function settleInboundMessageId(messageId, accepted) {
|
|
1807
|
+
if (!messageId)
|
|
1808
|
+
return;
|
|
1809
|
+
inFlightInboundMessageIds.delete(messageId);
|
|
1810
|
+
if (!accepted)
|
|
1811
|
+
return;
|
|
1812
|
+
acceptedInboundMessageIds.add(messageId);
|
|
1813
|
+
while (acceptedInboundMessageIds.size > 2_048) {
|
|
1814
|
+
const oldest = acceptedInboundMessageIds.values().next().value;
|
|
1815
|
+
if (!oldest)
|
|
1816
|
+
break;
|
|
1817
|
+
acceptedInboundMessageIds.delete(oldest);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1705
1820
|
let streamConnected = false;
|
|
1706
1821
|
const hostAvailableExecutionModes = [
|
|
1707
1822
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
@@ -1821,6 +1936,7 @@ export async function main() {
|
|
|
1821
1936
|
if (type === 'stop_and_drop') {
|
|
1822
1937
|
const droppedPrompts = session.queue.splice(0);
|
|
1823
1938
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
1939
|
+
rememberDroppedRecoveryCursor(session, droppedPrompts);
|
|
1824
1940
|
}
|
|
1825
1941
|
if (session.running) {
|
|
1826
1942
|
await session.adapter.interrupt();
|
|
@@ -2004,8 +2120,12 @@ export async function main() {
|
|
|
2004
2120
|
const message = payload.message;
|
|
2005
2121
|
if (message.senderId === agentId)
|
|
2006
2122
|
return;
|
|
2123
|
+
if (!claimInboundMessageId(message.id))
|
|
2124
|
+
return;
|
|
2007
2125
|
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
2008
2126
|
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
2127
|
+
persistInboundRecoveryCursorWhenIdle(payload.conversationId, message.id);
|
|
2128
|
+
settleInboundMessageId(message.id, true);
|
|
2009
2129
|
return;
|
|
2010
2130
|
}
|
|
2011
2131
|
void enqueueInboundMessage({
|
|
@@ -2018,14 +2138,10 @@ export async function main() {
|
|
|
2018
2138
|
selfContexts: payload.selfContexts,
|
|
2019
2139
|
provenance: payload.provenance,
|
|
2020
2140
|
turnDispatch: payload.turnDispatch,
|
|
2141
|
+
}).then(() => settleInboundMessageId(message.id, true), (error) => {
|
|
2142
|
+
settleInboundMessageId(message.id, false);
|
|
2143
|
+
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Failed to queue inbound message:`, error instanceof Error ? error.message : error);
|
|
2021
2144
|
});
|
|
2022
|
-
if (message.id) {
|
|
2023
|
-
saveRuntimeSessionState(runtimeId, {
|
|
2024
|
-
conversationId: payload.conversationId,
|
|
2025
|
-
baseCwd: workingDir,
|
|
2026
|
-
lastInboundMessageId: message.id,
|
|
2027
|
-
});
|
|
2028
|
-
}
|
|
2029
2145
|
},
|
|
2030
2146
|
onMessageDeleted: (payload) => {
|
|
2031
2147
|
removeQueuedPrompt(payload.conversationId, payload.messageId);
|
|
@@ -2061,6 +2177,57 @@ export async function main() {
|
|
|
2061
2177
|
catch (error) {
|
|
2062
2178
|
console.error('[canon-codex] Failed to load startup conversations:', error);
|
|
2063
2179
|
}
|
|
2180
|
+
for (const conversationId of knownConversationIds) {
|
|
2181
|
+
try {
|
|
2182
|
+
const cursor = loadRuntimeSessionState(runtimeId, {
|
|
2183
|
+
conversationId,
|
|
2184
|
+
baseCwd: workingDir,
|
|
2185
|
+
})?.lastInboundMessageId ?? null;
|
|
2186
|
+
const recovered = await collectMissedInboundMessages({
|
|
2187
|
+
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
2188
|
+
cursor,
|
|
2189
|
+
agentId,
|
|
2190
|
+
});
|
|
2191
|
+
if (recovered.mode === 'truncated-window') {
|
|
2192
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovery cursor was not found within ${STARTUP_RECOVERY_MAX_MESSAGES} messages; replaying the bounded recent window`);
|
|
2193
|
+
}
|
|
2194
|
+
for (const message of recovered.messages) {
|
|
2195
|
+
const isPlanReply = message.metadata?.type === 'plan_approval_reply';
|
|
2196
|
+
if (!isPlanReply && !shouldTriggerAgentTurn({
|
|
2197
|
+
senderType: message.senderType,
|
|
2198
|
+
metadata: message.metadata,
|
|
2199
|
+
}).allow) {
|
|
2200
|
+
persistInboundRecoveryCursorWhenIdle(conversationId, message.id);
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
if (!claimInboundMessageId(message.id))
|
|
2204
|
+
continue;
|
|
2205
|
+
try {
|
|
2206
|
+
await enqueueInboundMessage({
|
|
2207
|
+
conversationId,
|
|
2208
|
+
message,
|
|
2209
|
+
senderName: message.senderName || message.senderId,
|
|
2210
|
+
isOwner: message.senderId === ownerId,
|
|
2211
|
+
behavior: recovered.newestPage.behavior,
|
|
2212
|
+
activeSelfContextId: recovered.newestPage.activeSelfContextIdByMessageId?.[message.id] ?? null,
|
|
2213
|
+
selfContexts: recovered.newestPage.selfContexts,
|
|
2214
|
+
hydratedPage: recovered.newestPage,
|
|
2215
|
+
});
|
|
2216
|
+
settleInboundMessageId(message.id, true);
|
|
2217
|
+
}
|
|
2218
|
+
catch (error) {
|
|
2219
|
+
settleInboundMessageId(message.id, false);
|
|
2220
|
+
throw error;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if (recovered.messages.length > 0) {
|
|
2224
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovered ${recovered.messages.length} inbound message(s) (${recovered.mode})`);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
catch (error) {
|
|
2228
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Startup recovery failed:`, error instanceof Error ? error.message : error);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2064
2231
|
startCodexStreamInBackground(stream, (error) => {
|
|
2065
2232
|
console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
|
|
2066
2233
|
});
|
package/dist/startup-recovery.js
CHANGED
|
@@ -37,8 +37,21 @@ export async function collectMissedInboundMessages(input) {
|
|
|
37
37
|
cursorFound = hasCursor(fresh);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// The REST API already gives us a deterministic newest-first sequence,
|
|
41
|
+
// including messages with equal or missing timestamps. Reverse that wire
|
|
42
|
+
// order rather than re-sorting on a lossy display timestamp.
|
|
43
|
+
const ascending = [...collected].reverse();
|
|
44
|
+
const completedSourceMessageIds = new Set(collected.flatMap((message) => {
|
|
45
|
+
if (message.senderId !== input.agentId)
|
|
46
|
+
return [];
|
|
47
|
+
const metadata = message.metadata;
|
|
48
|
+
if (metadata?.turnSemantics !== 'turn_complete')
|
|
49
|
+
return [];
|
|
50
|
+
return typeof metadata.sourceMessageId === 'string'
|
|
51
|
+
? [metadata.sourceMessageId]
|
|
52
|
+
: [];
|
|
53
|
+
}));
|
|
54
|
+
const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId && !completedSourceMessageIds.has(message.id));
|
|
42
55
|
let mode;
|
|
43
56
|
let missed;
|
|
44
57
|
if (cursorFound) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@canonmsg/agent-sdk": "^5.1.
|
|
32
|
+
"@canonmsg/agent-sdk": "^5.1.1",
|
|
33
33
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
34
|
-
"@canonmsg/core": "^4.2.
|
|
34
|
+
"@canonmsg/core": "^4.2.2"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.0.0"
|