@canonmsg/codex-plugin 0.30.1 → 0.32.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/host.d.ts +5 -0
- package/dist/host.js +101 -112
- package/package.json +7 -7
package/dist/host.d.ts
CHANGED
|
@@ -247,6 +247,11 @@ export declare function shouldRouteCodexTurnArtifacts(input: {
|
|
|
247
247
|
artifactRoutingMode: TurnArtifactRoutingMode | undefined;
|
|
248
248
|
silenced: boolean;
|
|
249
249
|
finalText: string | null | undefined;
|
|
250
|
+
currentAudience?: {
|
|
251
|
+
memberIds?: readonly string[];
|
|
252
|
+
agentId: string;
|
|
253
|
+
ownerId: string | null | undefined;
|
|
254
|
+
};
|
|
250
255
|
}): TurnArtifactRoutingDecision;
|
|
251
256
|
export declare function main(): Promise<void>;
|
|
252
257
|
export {};
|
package/dist/host.js
CHANGED
|
@@ -5,8 +5,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { dirname } from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
8
|
-
import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting,
|
|
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, createRuntimeHeartbeat, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError,
|
|
8
|
+
import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, } from '@canonmsg/coding-agent-host';
|
|
9
|
+
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildCanonGroupContext, buildCompactGroupContextLines, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, sendMessageWithRetry, isPendingCanonOperation, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
10
|
import { validateCard } from '@canonmsg/rich-cards';
|
|
11
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
12
|
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
@@ -599,8 +599,12 @@ export function selectCodexNativeImagePaths(imagePaths, nativeVisionEnabled) {
|
|
|
599
599
|
* does not currently carry into the catch without a race.
|
|
600
600
|
*/
|
|
601
601
|
export function shouldRouteCodexTurnArtifacts(input) {
|
|
602
|
+
const audience = input.currentAudience;
|
|
603
|
+
const ownerPair = !audience || (audience.ownerId !== audience.agentId
|
|
604
|
+
&& Boolean(audience.ownerId) && audience.memberIds?.length === 2
|
|
605
|
+
&& audience.memberIds.includes(audience.agentId) && audience.memberIds.includes(audience.ownerId));
|
|
602
606
|
return resolveTurnArtifactRouting({
|
|
603
|
-
artifactRoutingMode: input.artifactRoutingMode,
|
|
607
|
+
artifactRoutingMode: ownerPair ? input.artifactRoutingMode : 'disabled',
|
|
604
608
|
silenced: isSilentTurnSuppressed({ silenced: input.silenced, finalText: input.finalText }),
|
|
605
609
|
});
|
|
606
610
|
}
|
|
@@ -684,7 +688,8 @@ export async function main() {
|
|
|
684
688
|
lockHandle?.release();
|
|
685
689
|
throw error;
|
|
686
690
|
}
|
|
687
|
-
const client = new CanonClient(apiKey, baseUrl);
|
|
691
|
+
const client = new CanonClient(apiKey, baseUrl, { environmentId: resolvedAgent.environmentId, streamUrl });
|
|
692
|
+
const endpoint = await client.getEndpoint();
|
|
688
693
|
const rtdb = initRTDBAuth(client, { rtdbUrl, firebaseApiKey });
|
|
689
694
|
const typingSignals = createTypingStatusPublisher({
|
|
690
695
|
setTyping: (conversationId, typing, status) => status
|
|
@@ -854,19 +859,23 @@ export async function main() {
|
|
|
854
859
|
if (cached) {
|
|
855
860
|
conversationCache.set(payload.conversationId, {
|
|
856
861
|
...cached,
|
|
862
|
+
...payload.changes,
|
|
857
863
|
memberIds,
|
|
858
864
|
});
|
|
865
|
+
void client.rememberConversation(conversationCache.get(payload.conversationId))
|
|
866
|
+
.catch((error) => console.error('[canon] Failed to persist conversation update:', error));
|
|
859
867
|
}
|
|
860
868
|
if (membershipChange) {
|
|
861
869
|
pendingMembershipChanges.set(payload.conversationId, membershipChange);
|
|
862
870
|
}
|
|
863
871
|
if (!memberIds.includes(agentId)) {
|
|
864
872
|
knownConversationIds.delete(payload.conversationId);
|
|
865
|
-
|
|
873
|
+
pendingMembershipChanges.delete(payload.conversationId);
|
|
874
|
+
closeSession(payload.conversationId);
|
|
866
875
|
}
|
|
867
876
|
}
|
|
868
877
|
function getGroupContextMode(conversationId, conversation) {
|
|
869
|
-
if (conversation
|
|
878
|
+
if (!conversation)
|
|
870
879
|
return undefined;
|
|
871
880
|
if (pendingMembershipChanges.has(conversationId))
|
|
872
881
|
return 'membership_change';
|
|
@@ -884,7 +893,7 @@ export async function main() {
|
|
|
884
893
|
}
|
|
885
894
|
async function loadHydratedInboundContext(input) {
|
|
886
895
|
const [conversation, page] = await Promise.all([
|
|
887
|
-
getConversationMeta(input.conversationId),
|
|
896
|
+
getConversationMeta(input.conversationId, { refreshIfMemberMissing: agentId }),
|
|
888
897
|
input.hydratedPage
|
|
889
898
|
? Promise.resolve(input.hydratedPage)
|
|
890
899
|
: client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
|
|
@@ -893,6 +902,7 @@ export async function main() {
|
|
|
893
902
|
agentId,
|
|
894
903
|
conversationId: input.conversationId,
|
|
895
904
|
conversation,
|
|
905
|
+
requireAgentMembership: true,
|
|
896
906
|
page,
|
|
897
907
|
activeSelfContextId: input.activeSelfContextId,
|
|
898
908
|
selfContexts: input.selfContexts,
|
|
@@ -1204,6 +1214,10 @@ export async function main() {
|
|
|
1204
1214
|
}
|
|
1205
1215
|
}
|
|
1206
1216
|
async function getOrCreateSession(conversationId) {
|
|
1217
|
+
const current = conversationCache.get(conversationId);
|
|
1218
|
+
if (current && !current.memberIds.includes(agentId)) {
|
|
1219
|
+
throw new Error('Agent is no longer a member of this conversation');
|
|
1220
|
+
}
|
|
1207
1221
|
knownConversationIds.add(conversationId);
|
|
1208
1222
|
const existing = sessions.get(conversationId);
|
|
1209
1223
|
if (existing && !existing.closed) {
|
|
@@ -1776,6 +1790,7 @@ export async function main() {
|
|
|
1776
1790
|
if (input.turnDispatch && input.turnDispatch.kind !== 'run_turn') {
|
|
1777
1791
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed server-dispatched turn: ${input.turnDispatch.reason}`);
|
|
1778
1792
|
recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
|
|
1793
|
+
await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
|
|
1779
1794
|
return;
|
|
1780
1795
|
}
|
|
1781
1796
|
if (input.message.metadata?.type === 'plan_approval_reply') {
|
|
@@ -1784,6 +1799,7 @@ export async function main() {
|
|
|
1784
1799
|
// replies left by an older coding descriptor instead of turning them
|
|
1785
1800
|
// into hidden plan/implementation prompts after an upgrade or restart.
|
|
1786
1801
|
recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
|
|
1802
|
+
await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
|
|
1787
1803
|
return;
|
|
1788
1804
|
}
|
|
1789
1805
|
const planId = readString(input.message.metadata, 'planId');
|
|
@@ -1792,6 +1808,7 @@ export async function main() {
|
|
|
1792
1808
|
metadata: input.message.metadata,
|
|
1793
1809
|
})) {
|
|
1794
1810
|
recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
|
|
1811
|
+
await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
|
|
1795
1812
|
return;
|
|
1796
1813
|
}
|
|
1797
1814
|
const session = await getOrCreateSession(input.conversationId);
|
|
@@ -1866,6 +1883,7 @@ export async function main() {
|
|
|
1866
1883
|
if (!autoReply.allow) {
|
|
1867
1884
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
1868
1885
|
recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
|
|
1886
|
+
await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
|
|
1869
1887
|
return;
|
|
1870
1888
|
}
|
|
1871
1889
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
@@ -1896,6 +1914,7 @@ export async function main() {
|
|
|
1896
1914
|
...(input.replyAuthority ? { replyAuthority: input.replyAuthority } : {}),
|
|
1897
1915
|
}).catch(() => { });
|
|
1898
1916
|
recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
|
|
1917
|
+
await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
|
|
1899
1918
|
return;
|
|
1900
1919
|
}
|
|
1901
1920
|
session.activeSelfContextId = activeSelfContextId;
|
|
@@ -1930,8 +1949,9 @@ export async function main() {
|
|
|
1930
1949
|
replyAuthority: input.replyAuthority ?? null,
|
|
1931
1950
|
});
|
|
1932
1951
|
}
|
|
1933
|
-
function sendTurnArtifactFile(session, file) {
|
|
1952
|
+
function sendTurnArtifactFile(session, file, canPublish) {
|
|
1934
1953
|
return sendMediaFileMessage(client, session.conversationId, file.path, '', {
|
|
1954
|
+
canPublish,
|
|
1935
1955
|
...(session.currentReplyAuthority ? { replyAuthority: session.currentReplyAuthority } : {}),
|
|
1936
1956
|
...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
|
|
1937
1957
|
metadata: {
|
|
@@ -1951,6 +1971,8 @@ export async function main() {
|
|
|
1951
1971
|
if (!nextTurn)
|
|
1952
1972
|
return;
|
|
1953
1973
|
session.running = true;
|
|
1974
|
+
const inboundId = nextTurn.sourceMessageId ? `message:${session.conversationId}:${nextTurn.sourceMessageId}` : null;
|
|
1975
|
+
let journaledInput = false;
|
|
1954
1976
|
session.state.lastError = undefined;
|
|
1955
1977
|
session.state.state = 'running';
|
|
1956
1978
|
session.currentTurnId = randomUUID();
|
|
@@ -2001,22 +2023,29 @@ export async function main() {
|
|
|
2001
2023
|
// the collect-and-post loop, so there is no un-gated path left to the
|
|
2002
2024
|
// workspace: the failure branches still send Canon's notice, and none of
|
|
2003
2025
|
// them can post the artifacts of a turn that chose silence.
|
|
2026
|
+
const decideArtifactRouting = () => shouldRouteCodexTurnArtifacts({
|
|
2027
|
+
artifactRoutingMode: nextTurn.artifactRoutingMode,
|
|
2028
|
+
silenced: session.currentTurnSilenced,
|
|
2029
|
+
finalText: turnFinalText,
|
|
2030
|
+
currentAudience: { memberIds: conversationCache.get(session.conversationId)?.memberIds, agentId, ownerId },
|
|
2031
|
+
});
|
|
2004
2032
|
const artifactRouter = createTurnArtifactRouter({
|
|
2005
|
-
decide:
|
|
2006
|
-
artifactRoutingMode: nextTurn.artifactRoutingMode,
|
|
2007
|
-
silenced: session.currentTurnSilenced,
|
|
2008
|
-
finalText: turnFinalText,
|
|
2009
|
-
}),
|
|
2033
|
+
decide: decideArtifactRouting,
|
|
2010
2034
|
baseline: () => artifactBaseline,
|
|
2011
2035
|
cwd: () => session.cwd,
|
|
2012
|
-
send: (file) => sendTurnArtifactFile(session, file),
|
|
2036
|
+
send: (file) => sendTurnArtifactFile(session, file, () => decideArtifactRouting().route),
|
|
2013
2037
|
log: (line) => console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`),
|
|
2014
2038
|
});
|
|
2015
2039
|
const routeArtifactsOnce = artifactRouter.route;
|
|
2016
2040
|
try {
|
|
2017
2041
|
const turnId = session.currentTurnId ?? randomUUID();
|
|
2018
2042
|
session.currentTurnId = turnId;
|
|
2019
|
-
|
|
2043
|
+
// Queueing does not freeze the audience: refresh at the native turn
|
|
2044
|
+
// boundary while retaining the same thread and its existing memory.
|
|
2045
|
+
const roster = buildCanonGroupContext({
|
|
2046
|
+
conversation: conversationCache.get(session.conversationId), messages: [], agentId, ownerId, ownerName,
|
|
2047
|
+
});
|
|
2048
|
+
let turnPrompt = [nextTurn.prompt, ...(roster ? buildCompactGroupContextLines(roster, 'initial') : [])].join('\n\n');
|
|
2020
2049
|
if (nextTurn.artifactRoutingMode === 'workspace-generated') {
|
|
2021
2050
|
artifactBaseline = await captureTurnArtifactSnapshot({ cwd: session.cwd }).catch((error) => {
|
|
2022
2051
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Artifact snapshot failed:`, error instanceof Error ? error.message : error);
|
|
@@ -2161,6 +2190,12 @@ export async function main() {
|
|
|
2161
2190
|
skillInvocationText: nextTurn.skillInvocationText,
|
|
2162
2191
|
onServerRequest: (request) => handleCodexServerRequest(session, request, nextTurn.requestingUserId ?? null, nextTurn.sourceMessageId ?? null),
|
|
2163
2192
|
});
|
|
2193
|
+
// Canon-origin queues must retain a durable execution owner, including
|
|
2194
|
+
// legacy queues restored before endpoint migration. Native continuations
|
|
2195
|
+
// without a Canon source message remain a separate operator path.
|
|
2196
|
+
if (inboundId && !await endpoint.claimInbound(inboundId))
|
|
2197
|
+
return;
|
|
2198
|
+
journaledInput = !!inboundId;
|
|
2164
2199
|
let result = await runTurnOnce();
|
|
2165
2200
|
if (!result.interrupted
|
|
2166
2201
|
&& !result.finalMessage
|
|
@@ -2176,6 +2211,8 @@ export async function main() {
|
|
|
2176
2211
|
session.currentTurnSilenced = false;
|
|
2177
2212
|
result = await runTurnOnce();
|
|
2178
2213
|
}
|
|
2214
|
+
if (journaledInput)
|
|
2215
|
+
await endpoint.setInboundState(inboundId, 'settled', 'native-completed');
|
|
2179
2216
|
// Both the artifact gate and the final delivery weigh silence against
|
|
2180
2217
|
// this text, and they must weigh the same one — set it before any
|
|
2181
2218
|
// completion branch runs, including the ones that route first.
|
|
@@ -2340,6 +2377,11 @@ export async function main() {
|
|
|
2340
2377
|
}
|
|
2341
2378
|
}
|
|
2342
2379
|
catch (error) {
|
|
2380
|
+
if (isPendingCanonOperation(error)) {
|
|
2381
|
+
session.state.lastError = 'Canon delivery remains pending reconciliation.';
|
|
2382
|
+
writeState(session);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2343
2385
|
const message = error instanceof ExecutionEnvironmentError
|
|
2344
2386
|
? error.userMessage
|
|
2345
2387
|
: error instanceof CanonApiError
|
|
@@ -2634,75 +2676,10 @@ export async function main() {
|
|
|
2634
2676
|
console.error(`[canon-codex] Runtime ${operation} failed:`, error);
|
|
2635
2677
|
},
|
|
2636
2678
|
});
|
|
2637
|
-
let startupRecoveryComplete = false;
|
|
2638
2679
|
async function recoverInboundMessageGaps() {
|
|
2639
|
-
//
|
|
2640
|
-
//
|
|
2641
|
-
// pass, not an automatic retry loop.
|
|
2642
|
-
const knownBeforeRefresh = new Set(knownConversationIds);
|
|
2680
|
+
// Refresh discovery only. Timeline reconstruction is never authorization to
|
|
2681
|
+
// execute historical turns; admitted pending work lives in the endpoint inbox.
|
|
2643
2682
|
await refreshKnownConversationIds(true);
|
|
2644
|
-
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
2645
|
-
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
2646
|
-
: new Set();
|
|
2647
|
-
for (const conversationId of knownConversationIds) {
|
|
2648
|
-
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
2649
|
-
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
2650
|
-
try {
|
|
2651
|
-
const cursor = loadRuntimeSessionState(runtimeId, {
|
|
2652
|
-
conversationId,
|
|
2653
|
-
baseCwd: workingDir,
|
|
2654
|
-
})?.lastInboundMessageId ?? null;
|
|
2655
|
-
const recovered = await collectMissedInboundMessages({
|
|
2656
|
-
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
2657
|
-
cursor,
|
|
2658
|
-
agentId,
|
|
2659
|
-
requireContiguousCursor: true,
|
|
2660
|
-
noCursorMode: conversationsDiscoveredWhileOffline.has(conversationId)
|
|
2661
|
-
? 'bounded-window'
|
|
2662
|
-
: 'latest-only',
|
|
2663
|
-
});
|
|
2664
|
-
recoveryBatch.commit(recovered.messages.map((message) => message.id), recovered.mode === 'incomplete-gap' ? recovered.recoveryCursor : null);
|
|
2665
|
-
if (recovered.mode === 'incomplete-gap') {
|
|
2666
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] recovery_incomplete_gap: cursor is missing; replaying ${recovered.messages.length} bounded inbound message(s) before advancing to ${recovered.recoveryCursor ?? 'no cursor'}`);
|
|
2667
|
-
}
|
|
2668
|
-
for (const message of recovered.messages) {
|
|
2669
|
-
const isPlanReply = isCodexPlanApprovalReply(message.metadata, serviceAgentMode);
|
|
2670
|
-
if (!isPlanReply && !shouldTriggerAgentTurn({
|
|
2671
|
-
senderType: message.senderType,
|
|
2672
|
-
metadata: message.metadata,
|
|
2673
|
-
}).allow) {
|
|
2674
|
-
recoveryCheckpoints.settle(message.id);
|
|
2675
|
-
continue;
|
|
2676
|
-
}
|
|
2677
|
-
if (!claimInboundMessageId(message.id))
|
|
2678
|
-
continue;
|
|
2679
|
-
try {
|
|
2680
|
-
await enqueueInboundMessage({
|
|
2681
|
-
conversationId,
|
|
2682
|
-
message,
|
|
2683
|
-
senderName: message.senderName || message.senderId,
|
|
2684
|
-
isOwner: message.senderId === ownerId,
|
|
2685
|
-
behavior: recovered.newestPage.behavior,
|
|
2686
|
-
activeSelfContextId: recovered.newestPage.activeSelfContextIdByMessageId?.[message.id] ?? null,
|
|
2687
|
-
selfContexts: recovered.newestPage.selfContexts,
|
|
2688
|
-
hydratedPage: recovered.newestPage,
|
|
2689
|
-
});
|
|
2690
|
-
settleInboundMessageId(message.id, true);
|
|
2691
|
-
}
|
|
2692
|
-
catch (error) {
|
|
2693
|
-
settleInboundMessageId(message.id, false);
|
|
2694
|
-
throw error;
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
if (recovered.messages.length > 0) {
|
|
2698
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovered ${recovered.messages.length} inbound message(s) (${recovered.mode})`);
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
catch (error) {
|
|
2702
|
-
recoveryBatch.cancel();
|
|
2703
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovery failed:`, error instanceof Error ? error.message : error);
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
2683
|
}
|
|
2707
2684
|
const reconnectRecovery = createReconnectRecoveryCoordinator(recoverInboundMessageGaps);
|
|
2708
2685
|
const observeReconnectRecovery = (reason, recovery) => {
|
|
@@ -2712,39 +2689,51 @@ export async function main() {
|
|
|
2712
2689
|
console.error(`[canon-codex] ${reason} recovery failed:`, error instanceof Error ? error.message : error);
|
|
2713
2690
|
});
|
|
2714
2691
|
};
|
|
2692
|
+
const inbound = endpoint.acceptInbound({ kind: 'message.created',
|
|
2693
|
+
offer: async (event) => {
|
|
2694
|
+
const payload = event.data;
|
|
2695
|
+
const message = payload.message;
|
|
2696
|
+
if (message.senderId === agentId) {
|
|
2697
|
+
await endpoint.setInboundState(event.id, 'settled', 'own-message');
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
if (!claimInboundMessageId(message.id))
|
|
2701
|
+
return;
|
|
2702
|
+
recoveryCheckpointsFor(payload.conversationId).track(message.id);
|
|
2703
|
+
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
2704
|
+
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
2705
|
+
recoveryCheckpointsFor(payload.conversationId).settle(message.id);
|
|
2706
|
+
settleInboundMessageId(message.id, true);
|
|
2707
|
+
await endpoint.setInboundState(`message:${payload.conversationId}:${message.id}`, 'settled', 'observe-only');
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
await enqueueInboundMessage({
|
|
2711
|
+
conversationId: payload.conversationId,
|
|
2712
|
+
message,
|
|
2713
|
+
senderName: message.senderName || message.senderId,
|
|
2714
|
+
isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
|
|
2715
|
+
behavior: payload.behavior,
|
|
2716
|
+
activeSelfContextId: payload.activeSelfContextId,
|
|
2717
|
+
selfContexts: payload.selfContexts,
|
|
2718
|
+
provenance: payload.provenance,
|
|
2719
|
+
turnDispatch: payload.turnDispatch,
|
|
2720
|
+
replyAuthority: payload.replyAuthority,
|
|
2721
|
+
}).then(() => settleInboundMessageId(message.id, true), (error) => {
|
|
2722
|
+
settleInboundMessageId(message.id, false);
|
|
2723
|
+
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Failed to queue inbound message:`, error instanceof Error ? error.message : error);
|
|
2724
|
+
throw error;
|
|
2725
|
+
});
|
|
2726
|
+
},
|
|
2727
|
+
onError: (error) => console.error('[canon-codex] Endpoint input deferred:', error), });
|
|
2728
|
+
inbound.start();
|
|
2715
2729
|
const stream = new CanonStream({
|
|
2716
|
-
|
|
2730
|
+
endpoint,
|
|
2717
2731
|
agentId,
|
|
2718
|
-
streamUrl,
|
|
2719
2732
|
handler: {
|
|
2720
2733
|
onMessage: (payload) => {
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
if (!claimInboundMessageId(message.id))
|
|
2725
|
-
return;
|
|
2726
|
-
recoveryCheckpointsFor(payload.conversationId).track(message.id);
|
|
2727
|
-
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
2728
|
-
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
2729
|
-
recoveryCheckpointsFor(payload.conversationId).settle(message.id);
|
|
2730
|
-
settleInboundMessageId(message.id, true);
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2733
|
-
void enqueueInboundMessage({
|
|
2734
|
-
conversationId: payload.conversationId,
|
|
2735
|
-
message,
|
|
2736
|
-
senderName: message.senderName || message.senderId,
|
|
2737
|
-
isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
|
|
2738
|
-
behavior: payload.behavior,
|
|
2739
|
-
activeSelfContextId: payload.activeSelfContextId,
|
|
2740
|
-
selfContexts: payload.selfContexts,
|
|
2741
|
-
provenance: payload.provenance,
|
|
2742
|
-
turnDispatch: payload.turnDispatch,
|
|
2743
|
-
replyAuthority: payload.replyAuthority,
|
|
2744
|
-
}).then(() => settleInboundMessageId(message.id, true), (error) => {
|
|
2745
|
-
settleInboundMessageId(message.id, false);
|
|
2746
|
-
console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Failed to queue inbound message:`, error instanceof Error ? error.message : error);
|
|
2747
|
-
});
|
|
2734
|
+
void inbound.receive({ id: `message:${payload.conversationId}:${payload.message.id}`,
|
|
2735
|
+
kind: 'message.created', conversationId: payload.conversationId, durable: true,
|
|
2736
|
+
data: payload });
|
|
2748
2737
|
},
|
|
2749
2738
|
onMessageDeleted: (payload) => {
|
|
2750
2739
|
removeQueuedPrompt(payload.conversationId, payload.messageId);
|
|
@@ -2790,7 +2779,6 @@ export async function main() {
|
|
|
2790
2779
|
await reconnectRecovery.recoverNow().catch((error) => {
|
|
2791
2780
|
console.error('[canon-codex] Startup recovery failed:', error instanceof Error ? error.message : error);
|
|
2792
2781
|
});
|
|
2793
|
-
startupRecoveryComplete = true;
|
|
2794
2782
|
startCodexStreamInBackground(stream, (error) => {
|
|
2795
2783
|
console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
|
|
2796
2784
|
});
|
|
@@ -2826,6 +2814,7 @@ export async function main() {
|
|
|
2826
2814
|
}
|
|
2827
2815
|
runtimeRequests.dispose();
|
|
2828
2816
|
stream.stop();
|
|
2817
|
+
await inbound.close();
|
|
2829
2818
|
await runtimeHeartbeat.dispose();
|
|
2830
2819
|
for (const session of [...sessions.values()]) {
|
|
2831
2820
|
await session.adapter.interrupt().catch(() => { });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
"prepack": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@canonmsg/agent-sdk": "^
|
|
35
|
-
"@canonmsg/agent-tools": "^0.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^
|
|
38
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
34
|
+
"@canonmsg/agent-sdk": "^11.0.0",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.11.0",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.9.0",
|
|
37
|
+
"@canonmsg/core": "^13.0.0",
|
|
38
|
+
"@canonmsg/rich-cards": "^0.10.6",
|
|
39
39
|
"ws": "^8.21.3"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
42
|
+
"node": ">=22.22.3"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"canon",
|