@canonmsg/codex-plugin 0.21.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 +283 -160
- 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,
|
|
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';
|
|
@@ -522,6 +535,66 @@ export async function main() {
|
|
|
522
535
|
}
|
|
523
536
|
console.error(`[canon-codex] Authenticated as ${agentId}`);
|
|
524
537
|
}
|
|
538
|
+
// Shared poll/timeout engine. Built-in `input`/`card` descriptors own
|
|
539
|
+
// create+poll (codex passes native/responder policy via payload/options).
|
|
540
|
+
// Approval keeps codex's own resolution shape (no owner-authored outcome
|
|
541
|
+
// messages / session rules — that is the SDK ApprovalManager's job).
|
|
542
|
+
const runtimeRequests = new RuntimeRequestManager(client, {
|
|
543
|
+
agentId,
|
|
544
|
+
ownerId: ownerId ?? '',
|
|
545
|
+
});
|
|
546
|
+
runtimeRequests.register('approval', {
|
|
547
|
+
kind: 'approval',
|
|
548
|
+
generateId: () => randomUUID(),
|
|
549
|
+
create: async ({ client: apiClient, ctx, conversationId, requestId, payload, expiresAt }) => {
|
|
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({
|
|
554
|
+
conversationId,
|
|
555
|
+
approvalId: requestId,
|
|
556
|
+
toolName: payload.toolName,
|
|
557
|
+
toolSummary: payload.toolSummary,
|
|
558
|
+
category: payload.category,
|
|
559
|
+
risk: payload.risk,
|
|
560
|
+
riskLevel: payload.riskLevel,
|
|
561
|
+
native: payload.native,
|
|
562
|
+
details: payload.details,
|
|
563
|
+
...(payload.diff ? { diff: payload.diff } : {}),
|
|
564
|
+
...(responseUserId ? { responseUserId } : {}),
|
|
565
|
+
allowSessionRule,
|
|
566
|
+
expiresAt,
|
|
567
|
+
...(payload.turnId ? { turnId: payload.turnId } : {}),
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
requestId,
|
|
571
|
+
state: { allowSessionRule: created.allowSessionRule ?? allowSessionRule },
|
|
572
|
+
};
|
|
573
|
+
},
|
|
574
|
+
consume: async ({ client: apiClient, conversationId, requestId, state }) => {
|
|
575
|
+
const response = await apiClient
|
|
576
|
+
.consumeRuntimeApprovalResponse({ conversationId, approvalId: requestId })
|
|
577
|
+
.catch(() => null);
|
|
578
|
+
if (response?.status === 'allow') {
|
|
579
|
+
return {
|
|
580
|
+
state: 'resolved',
|
|
581
|
+
result: {
|
|
582
|
+
decision: 'allow',
|
|
583
|
+
...(state?.allowSessionRule === true && response.sessionRule
|
|
584
|
+
? { sessionRule: response.sessionRule }
|
|
585
|
+
: {}),
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (response?.status === 'deny') {
|
|
590
|
+
return { state: 'resolved', result: { decision: 'deny' } };
|
|
591
|
+
}
|
|
592
|
+
if (response?.status === 'timeout')
|
|
593
|
+
return { state: 'timeout' };
|
|
594
|
+
return { state: 'pending' };
|
|
595
|
+
},
|
|
596
|
+
timeoutResult: () => ({ decision: 'deny' }),
|
|
597
|
+
});
|
|
525
598
|
const launchArgs = [...process.argv.slice(2)];
|
|
526
599
|
if (!launchArgs.some((arg) => arg === '--cwd' || arg.startsWith('--cwd='))) {
|
|
527
600
|
launchArgs.push('--cwd', workingDir);
|
|
@@ -557,6 +630,7 @@ export async function main() {
|
|
|
557
630
|
});
|
|
558
631
|
const sessions = new Map();
|
|
559
632
|
const pendingSessionCreations = new Map();
|
|
633
|
+
let inboundRecoverySequence = 0;
|
|
560
634
|
const conversationCache = new Map();
|
|
561
635
|
const knownConversationIds = new Set();
|
|
562
636
|
const promptedGroupContextConversationIds = new Set();
|
|
@@ -702,6 +776,29 @@ export async function main() {
|
|
|
702
776
|
return;
|
|
703
777
|
await client.updateMessageDisposition(conversationId, sourceMessageId, 'accepted_now').catch(() => { });
|
|
704
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
|
+
}
|
|
705
802
|
async function markQueuedPromptsRejected(conversationId, prompts) {
|
|
706
803
|
await Promise.all(prompts.map((prompt) => {
|
|
707
804
|
if (!prompt.markAccepted || !prompt.sourceMessageId)
|
|
@@ -709,6 +806,18 @@ export async function main() {
|
|
|
709
806
|
return client.updateMessageDisposition(conversationId, prompt.sourceMessageId, 'rejected').catch(() => { });
|
|
710
807
|
}));
|
|
711
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
|
+
}
|
|
712
821
|
function removeQueuedPrompt(conversationId, sourceMessageId) {
|
|
713
822
|
const session = sessions.get(conversationId);
|
|
714
823
|
if (!session || session.queue.length === 0)
|
|
@@ -836,6 +945,7 @@ export async function main() {
|
|
|
836
945
|
session.resetRequested = true;
|
|
837
946
|
const droppedPrompts = session.queue.splice(0);
|
|
838
947
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
948
|
+
rememberDroppedRecoveryCursor(session, droppedPrompts);
|
|
839
949
|
clearStoredThreadId(runtimeId, agentId, conversationId, session.environment.baseCwd, session.environment.mode);
|
|
840
950
|
session.adapter.clearThreadId();
|
|
841
951
|
session.activeSelfContextId = null;
|
|
@@ -960,6 +1070,7 @@ export async function main() {
|
|
|
960
1070
|
currentTurnCanUseCodexAppTools: false,
|
|
961
1071
|
activeSelfContextId: null,
|
|
962
1072
|
lastAcceptedIntent: null,
|
|
1073
|
+
pendingDroppedRecoveryCursor: null,
|
|
963
1074
|
resetRequested: false,
|
|
964
1075
|
lastActivity: Date.now(),
|
|
965
1076
|
typingKeepaliveTimer: null,
|
|
@@ -988,7 +1099,7 @@ export async function main() {
|
|
|
988
1099
|
pendingSessionCreations.delete(conversationId);
|
|
989
1100
|
}
|
|
990
1101
|
}
|
|
991
|
-
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) {
|
|
992
1103
|
const nextPrompt = {
|
|
993
1104
|
prompt,
|
|
994
1105
|
intent,
|
|
@@ -999,6 +1110,8 @@ export async function main() {
|
|
|
999
1110
|
planMode,
|
|
1000
1111
|
artifactRoutingMode,
|
|
1001
1112
|
canUseCodexAppTools,
|
|
1113
|
+
requestingUserId,
|
|
1114
|
+
recoverySequence: ++inboundRecoverySequence,
|
|
1002
1115
|
};
|
|
1003
1116
|
if (toFront) {
|
|
1004
1117
|
session.queue.unshift(nextPrompt);
|
|
@@ -1015,80 +1128,6 @@ export async function main() {
|
|
|
1015
1128
|
? 'workspace-generated'
|
|
1016
1129
|
: 'disabled';
|
|
1017
1130
|
}
|
|
1018
|
-
async function waitForRuntimeInputResponse(input) {
|
|
1019
|
-
while (Date.now() < input.expiresAt) {
|
|
1020
|
-
const response = await client.consumeRuntimeInputResponse({
|
|
1021
|
-
conversationId: input.conversationId,
|
|
1022
|
-
inputId: input.inputId,
|
|
1023
|
-
}).catch(() => null);
|
|
1024
|
-
if (response?.status === 'submitted') {
|
|
1025
|
-
return { status: 'submitted', value: response.value, answers: response.answers };
|
|
1026
|
-
}
|
|
1027
|
-
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
1028
|
-
return { status: response.status };
|
|
1029
|
-
}
|
|
1030
|
-
await sleep(1_000);
|
|
1031
|
-
}
|
|
1032
|
-
const response = await client.consumeRuntimeInputResponse({
|
|
1033
|
-
conversationId: input.conversationId,
|
|
1034
|
-
inputId: input.inputId,
|
|
1035
|
-
}).catch(() => null);
|
|
1036
|
-
if (response?.status === 'submitted') {
|
|
1037
|
-
return { status: 'submitted', value: response.value, answers: response.answers };
|
|
1038
|
-
}
|
|
1039
|
-
return { status: 'timeout' };
|
|
1040
|
-
}
|
|
1041
|
-
async function waitForRuntimeApprovalResponse(input) {
|
|
1042
|
-
while (Date.now() < input.expiresAt) {
|
|
1043
|
-
const response = await client.consumeRuntimeApprovalResponse({
|
|
1044
|
-
conversationId: input.conversationId,
|
|
1045
|
-
approvalId: input.approvalId,
|
|
1046
|
-
}).catch(() => null);
|
|
1047
|
-
if (response?.status === 'allow') {
|
|
1048
|
-
return { decision: 'allow', sessionRule: response.sessionRule };
|
|
1049
|
-
}
|
|
1050
|
-
if (response?.status === 'deny' || response?.status === 'timeout') {
|
|
1051
|
-
return { decision: 'deny' };
|
|
1052
|
-
}
|
|
1053
|
-
await sleep(1_000);
|
|
1054
|
-
}
|
|
1055
|
-
await client.consumeRuntimeApprovalResponse({
|
|
1056
|
-
conversationId: input.conversationId,
|
|
1057
|
-
approvalId: input.approvalId,
|
|
1058
|
-
}).catch(() => null);
|
|
1059
|
-
return { decision: 'deny' };
|
|
1060
|
-
}
|
|
1061
|
-
async function waitForRuntimeCardResponse(input) {
|
|
1062
|
-
while (Date.now() < input.expiresAt) {
|
|
1063
|
-
const response = await client.consumeRuntimeCardResponse({
|
|
1064
|
-
conversationId: input.conversationId,
|
|
1065
|
-
cardId: input.cardId,
|
|
1066
|
-
}).catch(() => null);
|
|
1067
|
-
if (response?.status === 'submitted') {
|
|
1068
|
-
return {
|
|
1069
|
-
status: 'submitted',
|
|
1070
|
-
...(response.actionId ? { actionId: response.actionId } : {}),
|
|
1071
|
-
...(response.values ? { values: response.values } : {}),
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
1075
|
-
return { status: response.status };
|
|
1076
|
-
}
|
|
1077
|
-
await sleep(1_000);
|
|
1078
|
-
}
|
|
1079
|
-
const response = await client.consumeRuntimeCardResponse({
|
|
1080
|
-
conversationId: input.conversationId,
|
|
1081
|
-
cardId: input.cardId,
|
|
1082
|
-
}).catch(() => null);
|
|
1083
|
-
if (response?.status === 'submitted') {
|
|
1084
|
-
return {
|
|
1085
|
-
status: 'submitted',
|
|
1086
|
-
...(response.actionId ? { actionId: response.actionId } : {}),
|
|
1087
|
-
...(response.values ? { values: response.values } : {}),
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
return { status: 'timeout' };
|
|
1091
|
-
}
|
|
1092
1131
|
function runtimeCardRequestPayload(method, params) {
|
|
1093
1132
|
if (method !== 'item/runtimeCard/request'
|
|
1094
1133
|
&& method !== 'runtimeCard/request') {
|
|
@@ -1098,7 +1137,7 @@ export async function main() {
|
|
|
1098
1137
|
const args = isRecord(params.arguments) ? params.arguments : null;
|
|
1099
1138
|
return params.card ?? params.cardDocument ?? input?.card ?? args?.card ?? null;
|
|
1100
1139
|
}
|
|
1101
|
-
async function handleCodexServerRequest(session, request) {
|
|
1140
|
+
async function handleCodexServerRequest(session, request, requestingUserId) {
|
|
1102
1141
|
const requestId = String(request.id);
|
|
1103
1142
|
const params = request.params;
|
|
1104
1143
|
const expiresAt = Date.now() + 30 * 60_000;
|
|
@@ -1130,15 +1169,12 @@ export async function main() {
|
|
|
1130
1169
|
?? requestId;
|
|
1131
1170
|
let requestCreated = false;
|
|
1132
1171
|
let requestResolved = false;
|
|
1172
|
+
const responseRouting = buildCodexTurnResponseRouting({ requestingUserId, ownerId });
|
|
1133
1173
|
try {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
card
|
|
1138
|
-
expiresAt,
|
|
1139
|
-
// Omit responseUserId so the backend targets a reachable member (the
|
|
1140
|
-
// owner if present, else the sole other member) — the owner is often
|
|
1141
|
-
// 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.
|
|
1176
|
+
const cardResult = await runtimeRequests.request('card', session.conversationId, {
|
|
1177
|
+
card,
|
|
1142
1178
|
native: {
|
|
1143
1179
|
runtime: 'codex',
|
|
1144
1180
|
method: request.method,
|
|
@@ -1150,25 +1186,35 @@ export async function main() {
|
|
|
1150
1186
|
},
|
|
1151
1187
|
},
|
|
1152
1188
|
turnId: session.currentTurnId ?? undefined,
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
id: `card:${cardId}`,
|
|
1159
|
-
kind: 'input',
|
|
1160
|
-
status: 'pending',
|
|
1161
|
-
title: card.title,
|
|
1162
|
-
summary: card.template ?? 'runtime card',
|
|
1163
|
-
});
|
|
1164
|
-
writeTurn(session);
|
|
1165
|
-
stopVisibleWorkSignal(session);
|
|
1166
|
-
writeCodexStreaming(session, null, 'waiting_input');
|
|
1167
|
-
const response = await waitForRuntimeCardResponse({
|
|
1168
|
-
conversationId: session.conversationId,
|
|
1169
|
-
cardId,
|
|
1189
|
+
...(responseRouting.responseUserId
|
|
1190
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1191
|
+
: {}),
|
|
1192
|
+
}, {
|
|
1193
|
+
requestId: cardId,
|
|
1170
1194
|
expiresAt,
|
|
1195
|
+
onCreated: () => {
|
|
1196
|
+
requestCreated = true;
|
|
1197
|
+
session.turnState = 'waiting_input';
|
|
1198
|
+
markTurnProgress(session);
|
|
1199
|
+
upsertTurnBlock(session, {
|
|
1200
|
+
id: `card:${cardId}`,
|
|
1201
|
+
kind: 'input',
|
|
1202
|
+
status: 'pending',
|
|
1203
|
+
title: card.title,
|
|
1204
|
+
summary: card.template ?? 'runtime card',
|
|
1205
|
+
});
|
|
1206
|
+
writeTurn(session);
|
|
1207
|
+
stopVisibleWorkSignal(session);
|
|
1208
|
+
writeCodexStreaming(session, null, 'waiting_input');
|
|
1209
|
+
},
|
|
1171
1210
|
});
|
|
1211
|
+
const response = cardResult.status === 'submitted'
|
|
1212
|
+
? {
|
|
1213
|
+
status: 'submitted',
|
|
1214
|
+
...(cardResult.actionId ? { actionId: cardResult.actionId } : {}),
|
|
1215
|
+
...(cardResult.values ? { values: cardResult.values } : {}),
|
|
1216
|
+
}
|
|
1217
|
+
: { status: cardResult.status };
|
|
1172
1218
|
requestResolved = true;
|
|
1173
1219
|
const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
|
|
1174
1220
|
await sendMessageWithRetry(client, session.conversationId, outcome.text, {
|
|
@@ -1216,18 +1262,23 @@ export async function main() {
|
|
|
1216
1262
|
const paramsArguments = isRecord(params.arguments) ? params.arguments : null;
|
|
1217
1263
|
const questions = mapCodexQuestions(params.questions ?? paramsInput?.questions ?? paramsArguments?.questions);
|
|
1218
1264
|
const inputId = readString(params, 'itemId') ?? requestId;
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1265
|
+
const sensitive = Boolean(questions?.some((question) => question.isSecret));
|
|
1266
|
+
const responseRouting = buildCodexTurnResponseRouting({
|
|
1267
|
+
requestingUserId,
|
|
1268
|
+
ownerId,
|
|
1269
|
+
ownerOnly: sensitive,
|
|
1270
|
+
});
|
|
1271
|
+
const response = await runtimeRequests.request('input', session.conversationId, {
|
|
1222
1272
|
kind: 'clarify',
|
|
1223
|
-
expiresAt,
|
|
1224
|
-
responseUserId: ownerId ?? undefined,
|
|
1225
1273
|
title: 'Codex needs input',
|
|
1226
1274
|
prompt: questions?.length
|
|
1227
1275
|
? 'Codex needs your input to continue.'
|
|
1228
1276
|
: 'Codex needs input.',
|
|
1229
1277
|
...(questions ? { questions } : {}),
|
|
1230
|
-
sensitive
|
|
1278
|
+
sensitive,
|
|
1279
|
+
...(responseRouting.responseUserId
|
|
1280
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1281
|
+
: {}),
|
|
1231
1282
|
native: {
|
|
1232
1283
|
runtime: 'codex',
|
|
1233
1284
|
method: request.method,
|
|
@@ -1239,12 +1290,7 @@ export async function main() {
|
|
|
1239
1290
|
},
|
|
1240
1291
|
},
|
|
1241
1292
|
turnId: session.currentTurnId ?? undefined,
|
|
1242
|
-
});
|
|
1243
|
-
const response = await waitForRuntimeInputResponse({
|
|
1244
|
-
conversationId: session.conversationId,
|
|
1245
|
-
inputId,
|
|
1246
|
-
expiresAt,
|
|
1247
|
-
});
|
|
1293
|
+
}, { requestId: inputId, expiresAt });
|
|
1248
1294
|
return { answers: response.status === 'submitted' ? response.answers ?? {} : {} };
|
|
1249
1295
|
}
|
|
1250
1296
|
const mappedApproval = mapCodexAppServerApprovalRequest({
|
|
@@ -1253,31 +1299,16 @@ export async function main() {
|
|
|
1253
1299
|
});
|
|
1254
1300
|
if (mappedApproval) {
|
|
1255
1301
|
const approvalId = readString(params, 'approvalId') ?? readString(params, 'itemId') ?? requestId;
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
toolSummary: mappedApproval.toolSummary,
|
|
1261
|
-
category: mappedApproval.category,
|
|
1262
|
-
risk: mappedApproval.risk,
|
|
1263
|
-
riskLevel: mappedApproval.riskLevel,
|
|
1264
|
-
native: {
|
|
1265
|
-
...mappedApproval.native,
|
|
1266
|
-
requestId,
|
|
1267
|
-
method: request.method,
|
|
1268
|
-
},
|
|
1269
|
-
details: mappedApproval.details,
|
|
1270
|
-
...(mappedApproval.diff ? { diff: mappedApproval.diff } : {}),
|
|
1271
|
-
responseUserId: ownerId ?? undefined,
|
|
1272
|
-
allowSessionRule: true,
|
|
1273
|
-
expiresAt,
|
|
1302
|
+
const responseRouting = buildCodexTurnResponseRouting({ requestingUserId, ownerId });
|
|
1303
|
+
const response = await runtimeRequests.request('approval', session.conversationId, {
|
|
1304
|
+
...mappedApproval,
|
|
1305
|
+
native: { ...mappedApproval.native, requestId, method: request.method },
|
|
1274
1306
|
turnId: session.currentTurnId ?? undefined,
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
});
|
|
1307
|
+
...(responseRouting.responseUserId
|
|
1308
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1309
|
+
: {}),
|
|
1310
|
+
allowSessionRule: responseRouting.allowSessionRule,
|
|
1311
|
+
}, { requestId: approvalId, expiresAt });
|
|
1281
1312
|
if (request.method === 'item/permissions/requestApproval') {
|
|
1282
1313
|
return response.decision === 'allow'
|
|
1283
1314
|
? { permissions: isRecord(params.permissions) ? params.permissions : {}, scope: response.sessionRule ? 'session' : 'turn' }
|
|
@@ -1294,6 +1325,7 @@ export async function main() {
|
|
|
1294
1325
|
knownConversationIds.add(input.conversationId);
|
|
1295
1326
|
if (input.turnDispatch && input.turnDispatch.kind !== 'run_turn') {
|
|
1296
1327
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed server-dispatched turn: ${input.turnDispatch.reason}`);
|
|
1328
|
+
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1297
1329
|
return;
|
|
1298
1330
|
}
|
|
1299
1331
|
if (isRecord(input.message.metadata)
|
|
@@ -1307,7 +1339,7 @@ export async function main() {
|
|
|
1307
1339
|
: decision === 'reject'
|
|
1308
1340
|
? `The plan was declined — keep planning and wait for guidance before implementing.${feedback ? `\n\nNotes:\n${feedback}` : ''}`
|
|
1309
1341
|
: `Please revise the plan.${feedback ? `\n\nRevision feedback:\n${feedback}` : ''}`;
|
|
1310
|
-
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));
|
|
1311
1343
|
return;
|
|
1312
1344
|
}
|
|
1313
1345
|
let materialized = [];
|
|
@@ -1365,6 +1397,7 @@ export async function main() {
|
|
|
1365
1397
|
: decideAutoReply(participantContext, behavior);
|
|
1366
1398
|
if (!autoReply.allow) {
|
|
1367
1399
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
1400
|
+
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1368
1401
|
return;
|
|
1369
1402
|
}
|
|
1370
1403
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
@@ -1388,6 +1421,7 @@ export async function main() {
|
|
|
1388
1421
|
replyBehavior: 'suppress_auto_reply',
|
|
1389
1422
|
},
|
|
1390
1423
|
}).catch(() => { });
|
|
1424
|
+
persistInboundRecoveryCursor(input.conversationId, input.message.id);
|
|
1391
1425
|
return;
|
|
1392
1426
|
}
|
|
1393
1427
|
session.activeSelfContextId = activeSelfContextId;
|
|
@@ -1404,7 +1438,7 @@ export async function main() {
|
|
|
1404
1438
|
});
|
|
1405
1439
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
1406
1440
|
const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
|
|
1407
|
-
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));
|
|
1408
1442
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
1409
1443
|
await session.adapter.interrupt().catch(() => { });
|
|
1410
1444
|
clearStreaming(input.conversationId);
|
|
@@ -1412,7 +1446,7 @@ export async function main() {
|
|
|
1412
1446
|
return;
|
|
1413
1447
|
}
|
|
1414
1448
|
const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
|
|
1415
|
-
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));
|
|
1416
1450
|
}
|
|
1417
1451
|
function sendTurnArtifactFile(session, file) {
|
|
1418
1452
|
return sendMediaFileMessage(client, session.conversationId, file.path, '', {
|
|
@@ -1611,7 +1645,7 @@ export async function main() {
|
|
|
1611
1645
|
};
|
|
1612
1646
|
const runTurnOnce = () => session.adapter.runTurn(turnPrompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs, {
|
|
1613
1647
|
planMode: nextTurn.planMode,
|
|
1614
|
-
onServerRequest: (request) => handleCodexServerRequest(session, request),
|
|
1648
|
+
onServerRequest: (request) => handleCodexServerRequest(session, request, nextTurn.requestingUserId ?? null),
|
|
1615
1649
|
});
|
|
1616
1650
|
let result = await runTurnOnce();
|
|
1617
1651
|
if (!result.interrupted
|
|
@@ -1628,19 +1662,23 @@ export async function main() {
|
|
|
1628
1662
|
}
|
|
1629
1663
|
if (!result.interrupted && result.finalMessage && nextTurn.planMode) {
|
|
1630
1664
|
await routeArtifactsOnce();
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1665
|
+
const responseRouting = buildCodexTurnResponseRouting({
|
|
1666
|
+
requestingUserId: nextTurn.requestingUserId,
|
|
1667
|
+
ownerId,
|
|
1668
|
+
});
|
|
1669
|
+
// Route plan-CREATE through the unified spine (`/runtime-plan/request`):
|
|
1670
|
+
// seeds the server-owned pending node/attention/state and authors the
|
|
1671
|
+
// `plan_approval` card. Resolution is UNCHANGED — Codex still re-queues a
|
|
1672
|
+
// fresh turn off the server-authored `plan_approval_reply`.
|
|
1673
|
+
await client.createRuntimePlanRequest({
|
|
1674
|
+
conversationId: session.conversationId,
|
|
1675
|
+
planId: session.currentTurnId ?? randomUUID(),
|
|
1633
1676
|
title: 'Codex Plan',
|
|
1634
1677
|
body: result.finalMessage,
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
...planApproval.metadata,
|
|
1640
|
-
turnId: session.currentTurnId,
|
|
1641
|
-
turnSemantics: 'control',
|
|
1642
|
-
replyBehavior: 'suppress_auto_reply',
|
|
1643
|
-
},
|
|
1678
|
+
...(responseRouting.responseUserId
|
|
1679
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1680
|
+
: {}),
|
|
1681
|
+
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
1644
1682
|
});
|
|
1645
1683
|
await handoffFinalMessage(session.conversationId);
|
|
1646
1684
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent plan approval card`);
|
|
@@ -1659,6 +1697,7 @@ export async function main() {
|
|
|
1659
1697
|
metadata: {
|
|
1660
1698
|
turnId: session.currentTurnId,
|
|
1661
1699
|
turnSemantics: 'turn_complete',
|
|
1700
|
+
...(nextTurn.sourceMessageId ? { sourceMessageId: nextTurn.sourceMessageId } : {}),
|
|
1662
1701
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1663
1702
|
...(turnTrail.length > 0 ? { turnTrail } : {}),
|
|
1664
1703
|
},
|
|
@@ -1683,6 +1722,7 @@ export async function main() {
|
|
|
1683
1722
|
metadata: {
|
|
1684
1723
|
turnId: session.currentTurnId,
|
|
1685
1724
|
turnSemantics: 'turn_complete',
|
|
1725
|
+
...(nextTurn.sourceMessageId ? { sourceMessageId: nextTurn.sourceMessageId } : {}),
|
|
1686
1726
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1687
1727
|
...(turnTrail.length > 0 ? { turnTrail } : {}),
|
|
1688
1728
|
},
|
|
@@ -1719,6 +1759,7 @@ export async function main() {
|
|
|
1719
1759
|
metadata: {
|
|
1720
1760
|
turnId: session.currentTurnId,
|
|
1721
1761
|
turnSemantics: 'turn_complete',
|
|
1762
|
+
...(nextTurn.sourceMessageId ? { sourceMessageId: nextTurn.sourceMessageId } : {}),
|
|
1722
1763
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
1723
1764
|
},
|
|
1724
1765
|
}).catch(() => { });
|
|
@@ -1729,6 +1770,11 @@ export async function main() {
|
|
|
1729
1770
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
1730
1771
|
}
|
|
1731
1772
|
finally {
|
|
1773
|
+
persistInboundRecoveryCursor(session.conversationId, nextTurn.sourceMessageId);
|
|
1774
|
+
if (session.pendingDroppedRecoveryCursor) {
|
|
1775
|
+
persistInboundRecoveryCursor(session.conversationId, session.pendingDroppedRecoveryCursor);
|
|
1776
|
+
session.pendingDroppedRecoveryCursor = null;
|
|
1777
|
+
}
|
|
1732
1778
|
stopVisibleWorkSignal(session);
|
|
1733
1779
|
session.running = false;
|
|
1734
1780
|
session.state.state = 'idle';
|
|
@@ -1747,6 +1793,30 @@ export async function main() {
|
|
|
1747
1793
|
}
|
|
1748
1794
|
}
|
|
1749
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
|
+
}
|
|
1750
1820
|
let streamConnected = false;
|
|
1751
1821
|
const hostAvailableExecutionModes = [
|
|
1752
1822
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
@@ -1866,6 +1936,7 @@ export async function main() {
|
|
|
1866
1936
|
if (type === 'stop_and_drop') {
|
|
1867
1937
|
const droppedPrompts = session.queue.splice(0);
|
|
1868
1938
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
1939
|
+
rememberDroppedRecoveryCursor(session, droppedPrompts);
|
|
1869
1940
|
}
|
|
1870
1941
|
if (session.running) {
|
|
1871
1942
|
await session.adapter.interrupt();
|
|
@@ -2049,8 +2120,12 @@ export async function main() {
|
|
|
2049
2120
|
const message = payload.message;
|
|
2050
2121
|
if (message.senderId === agentId)
|
|
2051
2122
|
return;
|
|
2123
|
+
if (!claimInboundMessageId(message.id))
|
|
2124
|
+
return;
|
|
2052
2125
|
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
2053
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);
|
|
2054
2129
|
return;
|
|
2055
2130
|
}
|
|
2056
2131
|
void enqueueInboundMessage({
|
|
@@ -2063,14 +2138,10 @@ export async function main() {
|
|
|
2063
2138
|
selfContexts: payload.selfContexts,
|
|
2064
2139
|
provenance: payload.provenance,
|
|
2065
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);
|
|
2066
2144
|
});
|
|
2067
|
-
if (message.id) {
|
|
2068
|
-
saveRuntimeSessionState(runtimeId, {
|
|
2069
|
-
conversationId: payload.conversationId,
|
|
2070
|
-
baseCwd: workingDir,
|
|
2071
|
-
lastInboundMessageId: message.id,
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
2145
|
},
|
|
2075
2146
|
onMessageDeleted: (payload) => {
|
|
2076
2147
|
removeQueuedPrompt(payload.conversationId, payload.messageId);
|
|
@@ -2106,6 +2177,57 @@ export async function main() {
|
|
|
2106
2177
|
catch (error) {
|
|
2107
2178
|
console.error('[canon-codex] Failed to load startup conversations:', error);
|
|
2108
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
|
+
}
|
|
2109
2231
|
startCodexStreamInBackground(stream, (error) => {
|
|
2110
2232
|
console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
|
|
2111
2233
|
});
|
|
@@ -2136,6 +2258,7 @@ export async function main() {
|
|
|
2136
2258
|
controlPoller.stop();
|
|
2137
2259
|
clearInterval(heartbeat);
|
|
2138
2260
|
clearInterval(idleCheck);
|
|
2261
|
+
runtimeRequests.dispose();
|
|
2139
2262
|
stream.stop();
|
|
2140
2263
|
await runtimeState.clearAgentRuntime().catch(() => { });
|
|
2141
2264
|
for (const session of [...sessions.values()]) {
|
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.
|
|
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.
|
|
32
|
+
"@canonmsg/agent-sdk": "^5.1.1",
|
|
33
33
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
34
|
-
"@canonmsg/core": "^4.
|
|
34
|
+
"@canonmsg/core": "^4.2.2"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.0.0"
|