@canonmsg/codex-plugin 0.23.5 → 0.23.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +0 -2
- package/dist/adapter.js +0 -2
- package/dist/app-server-adapter.d.ts +1 -3
- package/dist/app-server-adapter.js +1 -5
- package/dist/host.js +20 -22
- package/dist/register.js +13 -112
- package/dist/session-store.d.ts +3 -3
- package/dist/session-store.js +40 -93
- package/dist/setup.js +1 -1
- package/package.json +4 -4
- package/dist/cli-entry.d.ts +0 -2
- package/dist/cli-entry.js +0 -1
- package/dist/inbound-policy.d.ts +0 -22
- package/dist/inbound-policy.js +0 -46
- package/dist/startup-recovery.d.ts +0 -47
- package/dist/startup-recovery.js +0 -72
package/dist/adapter.d.ts
CHANGED
|
@@ -61,7 +61,6 @@ export declare class CodexConversationAdapter {
|
|
|
61
61
|
private model;
|
|
62
62
|
private reasoningEffort;
|
|
63
63
|
private readonly sandbox;
|
|
64
|
-
private readonly legacyApprovalPolicy;
|
|
65
64
|
private readonly codexProfile;
|
|
66
65
|
private readonly addDirs;
|
|
67
66
|
private readonly configOverrides;
|
|
@@ -78,7 +77,6 @@ export declare class CodexConversationAdapter {
|
|
|
78
77
|
model?: string | null;
|
|
79
78
|
reasoningEffort?: string | null;
|
|
80
79
|
sandbox?: CodexSandboxMode | null;
|
|
81
|
-
approvalPolicy?: CodexApprovalPolicy | null;
|
|
82
80
|
codexProfile?: string | null;
|
|
83
81
|
addDirs?: string[];
|
|
84
82
|
configOverrides?: string[];
|
package/dist/adapter.js
CHANGED
|
@@ -7,7 +7,6 @@ export class CodexConversationAdapter {
|
|
|
7
7
|
model;
|
|
8
8
|
reasoningEffort;
|
|
9
9
|
sandbox;
|
|
10
|
-
legacyApprovalPolicy;
|
|
11
10
|
codexProfile;
|
|
12
11
|
addDirs;
|
|
13
12
|
configOverrides;
|
|
@@ -24,7 +23,6 @@ export class CodexConversationAdapter {
|
|
|
24
23
|
this.model = opts.model ?? null;
|
|
25
24
|
this.reasoningEffort = opts.reasoningEffort ?? null;
|
|
26
25
|
this.sandbox = opts.sandbox ?? null;
|
|
27
|
-
this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
|
|
28
26
|
this.codexProfile = opts.codexProfile ?? null;
|
|
29
27
|
this.addDirs = opts.addDirs ?? [];
|
|
30
28
|
this.configOverrides = opts.configOverrides ?? [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
|
|
2
2
|
export type JsonRecord = Record<string, unknown>;
|
|
3
3
|
export interface CodexSkillMetadata {
|
|
4
4
|
name: string;
|
|
@@ -27,7 +27,6 @@ export declare class CodexAppServerAdapter {
|
|
|
27
27
|
private model;
|
|
28
28
|
private reasoningEffort;
|
|
29
29
|
private readonly sandbox;
|
|
30
|
-
private readonly legacyApprovalPolicy;
|
|
31
30
|
private readonly addDirs;
|
|
32
31
|
private readonly configOverrides;
|
|
33
32
|
private readonly fullAuto;
|
|
@@ -60,7 +59,6 @@ export declare class CodexAppServerAdapter {
|
|
|
60
59
|
model?: string | null;
|
|
61
60
|
reasoningEffort?: string | null;
|
|
62
61
|
sandbox?: CodexSandboxMode | null;
|
|
63
|
-
approvalPolicy?: CodexApprovalPolicy | null;
|
|
64
62
|
addDirs?: string[];
|
|
65
63
|
configOverrides?: string[];
|
|
66
64
|
fullAuto?: boolean;
|
|
@@ -8,7 +8,6 @@ export class CodexAppServerAdapter {
|
|
|
8
8
|
model;
|
|
9
9
|
reasoningEffort;
|
|
10
10
|
sandbox;
|
|
11
|
-
legacyApprovalPolicy;
|
|
12
11
|
addDirs;
|
|
13
12
|
configOverrides;
|
|
14
13
|
fullAuto;
|
|
@@ -42,7 +41,6 @@ export class CodexAppServerAdapter {
|
|
|
42
41
|
this.reasoningEffort = opts.reasoningEffort ?? null;
|
|
43
42
|
this.resolvedReasoningEffort = this.reasoningEffort;
|
|
44
43
|
this.sandbox = opts.sandbox ?? null;
|
|
45
|
-
this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
|
|
46
44
|
this.addDirs = opts.addDirs ?? [];
|
|
47
45
|
this.configOverrides = opts.configOverrides ?? [];
|
|
48
46
|
this.fullAuto = opts.fullAuto ?? false;
|
|
@@ -198,9 +196,7 @@ export class CodexAppServerAdapter {
|
|
|
198
196
|
}
|
|
199
197
|
}
|
|
200
198
|
resolveApprovalPolicy() {
|
|
201
|
-
|
|
202
|
-
return 'never';
|
|
203
|
-
return this.legacyApprovalPolicy;
|
|
199
|
+
return (this.bypassApprovalsAndSandbox || this.fullAuto) ? 'never' : null;
|
|
204
200
|
}
|
|
205
201
|
configPayload() {
|
|
206
202
|
const config = {};
|
package/dist/host.js
CHANGED
|
@@ -5,9 +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, collectTurnArtifacts, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, } 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, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
|
-
import { decideAutoReply, } from './inbound-policy.js';
|
|
8
|
+
import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, collectTurnArtifacts, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } 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, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
11
10
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
12
11
|
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
13
12
|
import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } from './codex-app-tools.js';
|
|
@@ -18,8 +17,7 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
|
|
|
18
17
|
import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
|
|
19
18
|
import { startCodexStreamInBackground } from './host-lifecycle.js';
|
|
20
19
|
import { createCodexControlPoller } from './control-channel.js';
|
|
21
|
-
import { runCli } from '
|
|
22
|
-
import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
|
|
20
|
+
import { runCli } from '@canonmsg/core';
|
|
23
21
|
import { applyTextSegmentBlock, beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
|
|
24
22
|
import { FALLBACK_CODEX_EFFORT_OPTIONS, buildCodexEffortOptions, buildCodexModelOptions, readCodexConfiguredEffort, resolveCodexDefaultModel, resolveCodexEffortForModel, } from './model-catalog.js';
|
|
25
23
|
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
@@ -240,13 +238,14 @@ export function buildCodexTurnResponseRouting(input) {
|
|
|
240
238
|
export function getCodexRequestingUserId(message) {
|
|
241
239
|
return message.senderType === 'human' ? message.senderId : null;
|
|
242
240
|
}
|
|
243
|
-
async function publishAgentRuntime(agentId, runtime) {
|
|
244
|
-
await publishHostAgentRuntime(agentId, 'codex', runtime);
|
|
241
|
+
async function publishAgentRuntime(agentId, runtime, rtdb) {
|
|
242
|
+
await publishHostAgentRuntime(agentId, 'codex', runtime, rtdb);
|
|
245
243
|
}
|
|
246
|
-
async function loadSessionConfig(conversationId, agentId) {
|
|
244
|
+
async function loadSessionConfig(conversationId, agentId, rtdb) {
|
|
247
245
|
return loadHostSessionConfig({
|
|
248
246
|
conversationId,
|
|
249
247
|
agentId,
|
|
248
|
+
rtdb,
|
|
250
249
|
extraStringFields: CODEX_SESSION_CONFIG_FIELDS,
|
|
251
250
|
});
|
|
252
251
|
}
|
|
@@ -286,9 +285,7 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
286
285
|
}
|
|
287
286
|
const approvalOverride = mapCanonPermissionToCodex(permissionMode);
|
|
288
287
|
const defaultSandbox = (stringArg(input.args, 'sandbox') ?? null);
|
|
289
|
-
const defaultApprovalPolicy = (stringArg(input.args, 'ask-for-approval') ?? null);
|
|
290
288
|
const sandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
|
|
291
|
-
const approvalPolicy = approvalOverride ? null : defaultApprovalPolicy;
|
|
292
289
|
const fullAuto = approvalOverride ? approvalOverride.fullAuto : boolArg(input.args, 'full-auto');
|
|
293
290
|
const bypassApprovalsAndSandbox = approvalOverride
|
|
294
291
|
? approvalOverride.bypassApprovalsAndSandbox
|
|
@@ -298,7 +295,9 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
298
295
|
executionMode: input.environment.mode,
|
|
299
296
|
permissionMode: permissionMode ?? null,
|
|
300
297
|
sandbox,
|
|
301
|
-
|
|
298
|
+
// `--ask-for-approval` is rejected at startup, so this was always null.
|
|
299
|
+
// The key must stay: it is hashed into every persisted thread fingerprint.
|
|
300
|
+
approvalPolicy: null,
|
|
302
301
|
fullAuto,
|
|
303
302
|
bypassApprovalsAndSandbox,
|
|
304
303
|
});
|
|
@@ -306,7 +305,6 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
306
305
|
...(model ? { model } : {}),
|
|
307
306
|
...(permissionMode ? { permissionMode } : {}),
|
|
308
307
|
sandbox,
|
|
309
|
-
approvalPolicy,
|
|
310
308
|
fullAuto,
|
|
311
309
|
bypassApprovalsAndSandbox,
|
|
312
310
|
fingerprint,
|
|
@@ -620,6 +618,7 @@ export async function main() {
|
|
|
620
618
|
agentId,
|
|
621
619
|
clientType: 'codex',
|
|
622
620
|
hostMode: true,
|
|
621
|
+
rtdb,
|
|
623
622
|
});
|
|
624
623
|
const sessions = new Map();
|
|
625
624
|
const pendingSessionCreations = new Map();
|
|
@@ -939,7 +938,7 @@ export async function main() {
|
|
|
939
938
|
const droppedPrompts = session.queue.splice(0);
|
|
940
939
|
await markQueuedPromptsRejected(conversationId, droppedPrompts);
|
|
941
940
|
rememberDroppedRecoveryCursor(session, droppedPrompts);
|
|
942
|
-
clearStoredThreadId(runtimeId,
|
|
941
|
+
clearStoredThreadId(runtimeId, conversationId, session.environment.baseCwd, session.environment.mode);
|
|
943
942
|
session.adapter.clearThreadId();
|
|
944
943
|
session.activeSelfContextId = null;
|
|
945
944
|
session.state.lastError = undefined;
|
|
@@ -989,7 +988,7 @@ export async function main() {
|
|
|
989
988
|
evictOldestIdle();
|
|
990
989
|
}
|
|
991
990
|
const creation = (async () => {
|
|
992
|
-
const config = await loadSessionConfig(conversationId, agentId);
|
|
991
|
+
const config = await loadSessionConfig(conversationId, agentId, rtdb);
|
|
993
992
|
const sessionExecutionMode = resolveSessionExecutionMode(config);
|
|
994
993
|
const workspaceCwd = resolveWorkspaceCwd(config);
|
|
995
994
|
const environment = prepareConversationEnvironment({
|
|
@@ -1010,7 +1009,7 @@ export async function main() {
|
|
|
1010
1009
|
if (modelGuard) {
|
|
1011
1010
|
throw new ExecutionEnvironmentError(modelGuard, modelGuard);
|
|
1012
1011
|
}
|
|
1013
|
-
const storedThreadId = loadStoredThreadId(runtimeId,
|
|
1012
|
+
const storedThreadId = loadStoredThreadId(runtimeId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
|
|
1014
1013
|
const effectiveModel = policy.model ?? codexDefaultModel;
|
|
1015
1014
|
const initialEffortResolution = resolveCodexEffortForModel({
|
|
1016
1015
|
models: codexModels,
|
|
@@ -1026,7 +1025,6 @@ export async function main() {
|
|
|
1026
1025
|
model: policy.model ?? null,
|
|
1027
1026
|
reasoningEffort: initialEffort,
|
|
1028
1027
|
sandbox: policy.sandbox,
|
|
1029
|
-
approvalPolicy: policy.approvalPolicy,
|
|
1030
1028
|
addDirs: args['add-dir'] ?? [],
|
|
1031
1029
|
configOverrides: args.config ?? [],
|
|
1032
1030
|
fullAuto: policy.fullAuto,
|
|
@@ -1040,7 +1038,6 @@ export async function main() {
|
|
|
1040
1038
|
model: policy.model ?? null,
|
|
1041
1039
|
reasoningEffort: initialEffort,
|
|
1042
1040
|
sandbox: policy.sandbox,
|
|
1043
|
-
approvalPolicy: policy.approvalPolicy,
|
|
1044
1041
|
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
1045
1042
|
addDirs: args['add-dir'] ?? [],
|
|
1046
1043
|
configOverrides: args.config ?? [],
|
|
@@ -1565,7 +1562,7 @@ export async function main() {
|
|
|
1565
1562
|
if (session.resetRequested) {
|
|
1566
1563
|
return;
|
|
1567
1564
|
}
|
|
1568
|
-
saveStoredThreadId(runtimeId,
|
|
1565
|
+
saveStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
|
|
1569
1566
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
|
|
1570
1567
|
return;
|
|
1571
1568
|
}
|
|
@@ -1669,7 +1666,7 @@ export async function main() {
|
|
|
1669
1666
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
|
|
1670
1667
|
};
|
|
1671
1668
|
const clearStoredThread = () => {
|
|
1672
|
-
clearStoredThreadId(runtimeId,
|
|
1669
|
+
clearStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
1673
1670
|
session.adapter.clearThreadId();
|
|
1674
1671
|
};
|
|
1675
1672
|
const runTurnOnce = () => session.adapter.runTurn(turnPrompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs, {
|
|
@@ -1698,7 +1695,7 @@ export async function main() {
|
|
|
1698
1695
|
}
|
|
1699
1696
|
}
|
|
1700
1697
|
if (result.threadId && !session.resetRequested) {
|
|
1701
|
-
saveStoredThreadId(runtimeId,
|
|
1698
|
+
saveStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
|
|
1702
1699
|
}
|
|
1703
1700
|
if (!result.interrupted && result.finalMessage && nextTurn.planMode) {
|
|
1704
1701
|
await routeArtifactsOnce();
|
|
@@ -1831,7 +1828,7 @@ export async function main() {
|
|
|
1831
1828
|
}).catch(() => { });
|
|
1832
1829
|
await handoffFinalMessage(session.conversationId);
|
|
1833
1830
|
if (error instanceof Error && isRecoverableCodexThreadError(error.message)) {
|
|
1834
|
-
clearStoredThreadId(runtimeId,
|
|
1831
|
+
clearStoredThreadId(runtimeId, session.conversationId, session.environment.baseCwd, session.environment.mode);
|
|
1835
1832
|
}
|
|
1836
1833
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
1837
1834
|
}
|
|
@@ -2109,7 +2106,7 @@ export async function main() {
|
|
|
2109
2106
|
});
|
|
2110
2107
|
if (!streamConnected)
|
|
2111
2108
|
return;
|
|
2112
|
-
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
2109
|
+
await publishAgentRuntime(agentId, runtimeDescriptor, rtdb).catch((error) => {
|
|
2113
2110
|
console.error('[canon-codex] Failed to publish agent runtime:', error);
|
|
2114
2111
|
});
|
|
2115
2112
|
if (publishRuntimeDetailsInFlight)
|
|
@@ -2122,6 +2119,7 @@ export async function main() {
|
|
|
2122
2119
|
await publishHostSessionSnapshots({
|
|
2123
2120
|
conversationIds: Array.from(knownConversationIds),
|
|
2124
2121
|
agentId,
|
|
2122
|
+
rtdb,
|
|
2125
2123
|
clientType: 'codex',
|
|
2126
2124
|
runtime: runtimeDescriptor,
|
|
2127
2125
|
workspaceOptions,
|
package/dist/register.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { parseArgs } from 'node:util';
|
|
4
|
-
import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, resolveCanonRuntimeConnection, loadProfiles, updatePendingRegistration, upsertAgentProfile, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
5
|
-
import { runCli } from './cli-entry.js';
|
|
2
|
+
import { registerRegistrationCli, runRegistrationCli, } from '@canonmsg/core';
|
|
6
3
|
const HELP = `canon-codex-register — register or reconnect a Codex agent in Canon
|
|
7
4
|
|
|
8
5
|
USAGE
|
|
@@ -28,113 +25,17 @@ EXAMPLES
|
|
|
28
25
|
canon-codex-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
29
26
|
|
|
30
27
|
After approval, start it with CANON_AGENT=<profile> canon-codex --cwd /path/to/project.`;
|
|
28
|
+
const OPTIONS = {
|
|
29
|
+
moduleUrl: import.meta.url,
|
|
30
|
+
clientType: 'codex',
|
|
31
|
+
cliName: 'canon-codex-register',
|
|
32
|
+
hostBinName: 'canon-codex',
|
|
33
|
+
developerInfo: 'Codex host plugin',
|
|
34
|
+
registeringLabel: 'Codex agent',
|
|
35
|
+
usage: 'Usage: canon-codex-register --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]',
|
|
36
|
+
help: HELP,
|
|
37
|
+
};
|
|
31
38
|
export async function main() {
|
|
32
|
-
|
|
33
|
-
const { values } = parseArgs({
|
|
34
|
-
options: {
|
|
35
|
-
name: { type: 'string' },
|
|
36
|
-
description: { type: 'string' },
|
|
37
|
-
phone: { type: 'string' },
|
|
38
|
-
profile: { type: 'string' },
|
|
39
|
-
environment: { type: 'string' },
|
|
40
|
-
'base-url': { type: 'string' },
|
|
41
|
-
'stream-url': { type: 'string' },
|
|
42
|
-
'rtdb-url': { type: 'string' },
|
|
43
|
-
'firebase-api-key': { type: 'string' },
|
|
44
|
-
},
|
|
45
|
-
strict: true,
|
|
46
|
-
});
|
|
47
|
-
if (!values.name || !values.description || !values.phone) {
|
|
48
|
-
console.error('Usage: canon-codex-register --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
const profileName = values.profile || values.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
52
|
-
const environmentId = values.environment || process.env.CANON_ENVIRONMENT_ID;
|
|
53
|
-
if (!environmentId) {
|
|
54
|
-
throw new Error('--environment or CANON_ENVIRONMENT_ID is required');
|
|
55
|
-
}
|
|
56
|
-
const connection = resolveCanonRuntimeConnection({
|
|
57
|
-
environmentId,
|
|
58
|
-
apiBaseUrl: values['base-url'],
|
|
59
|
-
streamUrl: values['stream-url'],
|
|
60
|
-
rtdbUrl: values['rtdb-url'],
|
|
61
|
-
firebaseWebApiKey: values['firebase-api-key'],
|
|
62
|
-
});
|
|
63
|
-
await verifyCanonRuntimeConnection(connection);
|
|
64
|
-
const existingProfile = loadProfiles()[profileName];
|
|
65
|
-
const existingAgentId = existingProfile?.environmentId === connection.environmentId
|
|
66
|
-
? existingProfile.agentId
|
|
67
|
-
: undefined;
|
|
68
|
-
console.log(`Registering Codex agent "${values.name}" (profile: ${profileName})...`);
|
|
69
|
-
const pending = getOrCreatePendingRegistration(profileName, 'codex', connection);
|
|
70
|
-
const result = await registerAndWaitForApproval({
|
|
71
|
-
name: values.name,
|
|
72
|
-
description: values.description,
|
|
73
|
-
ownerPhone: values.phone,
|
|
74
|
-
developerInfo: 'Codex host plugin',
|
|
75
|
-
clientType: 'codex',
|
|
76
|
-
baseUrl: connection.apiBaseUrl,
|
|
77
|
-
requestedAgentId: existingAgentId,
|
|
78
|
-
localRegistrationId: pending.localRegistrationId,
|
|
79
|
-
}, {
|
|
80
|
-
onSubmitted: (requestId, pollToken) => {
|
|
81
|
-
updatePendingRegistration(profileName, {
|
|
82
|
-
requestId,
|
|
83
|
-
pollToken,
|
|
84
|
-
clientType: 'codex',
|
|
85
|
-
});
|
|
86
|
-
console.log(`Registration submitted (request ID: ${requestId}).`);
|
|
87
|
-
console.log('Waiting for approval in Canon app...');
|
|
88
|
-
},
|
|
89
|
-
onPollUpdate: () => {
|
|
90
|
-
process.stdout.write('.');
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
console.log('');
|
|
94
|
-
switch (result.status) {
|
|
95
|
-
case 'approved': {
|
|
96
|
-
if (!result.apiKey || !result.agentId || !result.agentName) {
|
|
97
|
-
console.error('Approval completed but Canon did not return a usable API key. Run this command again to resume key pickup.');
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
upsertAgentProfile(profileName, {
|
|
101
|
-
apiKey: result.apiKey,
|
|
102
|
-
agentId: result.agentId,
|
|
103
|
-
agentName: result.agentName,
|
|
104
|
-
registeredAt: new Date().toISOString(),
|
|
105
|
-
environmentId: connection.environmentId,
|
|
106
|
-
baseUrl: connection.apiBaseUrl,
|
|
107
|
-
streamUrl: connection.streamUrl,
|
|
108
|
-
rtdbUrl: connection.rtdbUrl,
|
|
109
|
-
firebaseApiKey: connection.firebaseWebApiKey,
|
|
110
|
-
clientType: 'codex',
|
|
111
|
-
});
|
|
112
|
-
if (result.requestId) {
|
|
113
|
-
await ackRegistrationApproval(connection.apiBaseUrl, result.requestId, result.pollToken);
|
|
114
|
-
}
|
|
115
|
-
clearPendingRegistration(profileName);
|
|
116
|
-
console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
|
|
117
|
-
console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
|
|
118
|
-
console.log('Start it with: CANON_AGENT=' + profileName + ' canon-codex --cwd /path/to/project');
|
|
119
|
-
console.log('Keep that terminal open while you want Canon to reach the agent.');
|
|
120
|
-
console.log('Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.');
|
|
121
|
-
console.log('Docs: https://canonmail.com/agents/integrations');
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case 'rejected':
|
|
125
|
-
console.log('Registration was rejected.');
|
|
126
|
-
process.exit(1);
|
|
127
|
-
break;
|
|
128
|
-
case 'timeout':
|
|
129
|
-
console.log('Registration timed out (5 minutes). Try again later.');
|
|
130
|
-
process.exit(1);
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
39
|
+
await runRegistrationCli(OPTIONS);
|
|
133
40
|
}
|
|
134
|
-
|
|
135
|
-
console.error('[canon-codex-register] Fatal error:', error);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}, {
|
|
138
|
-
name: 'canon-codex-register',
|
|
139
|
-
help: HELP,
|
|
140
|
-
});
|
|
41
|
+
registerRegistrationCli(OPTIONS);
|
package/dist/session-store.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export declare function buildCodexThreadPolicyFingerprint(input: {
|
|
|
8
8
|
fullAuto?: boolean;
|
|
9
9
|
bypassApprovalsAndSandbox?: boolean;
|
|
10
10
|
}): string;
|
|
11
|
-
export declare function loadStoredThreadId(runtimeId: string
|
|
12
|
-
export declare function saveStoredThreadId(runtimeId: string
|
|
13
|
-
export declare function clearStoredThreadId(runtimeId: string
|
|
11
|
+
export declare function loadStoredThreadId(runtimeId: string, conversationId: string, baseCwd: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): string | null;
|
|
12
|
+
export declare function saveStoredThreadId(runtimeId: string, conversationId: string, baseCwd: string, threadId: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): void;
|
|
13
|
+
export declare function clearStoredThreadId(runtimeId: string, conversationId: string, baseCwd?: string, executionMode?: ExecutionEnvironmentMode): void;
|
package/dist/session-store.js
CHANGED
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
|
|
5
|
-
const STORE_PATH = join(CANON_DIR, 'codex-sessions.json');
|
|
6
|
-
function loadStore() {
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return { agents: {} };
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function saveStore(store) {
|
|
15
|
-
mkdirSync(CANON_DIR, { recursive: true });
|
|
16
|
-
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
|
|
17
|
-
}
|
|
2
|
+
import { clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
|
|
18
3
|
export function buildCodexThreadPolicyFingerprint(input) {
|
|
19
4
|
return createHash('sha256').update(JSON.stringify({
|
|
20
5
|
version: 2,
|
|
@@ -27,91 +12,53 @@ export function buildCodexThreadPolicyFingerprint(input) {
|
|
|
27
12
|
bypassApprovalsAndSandbox: input.bypassApprovalsAndSandbox === true,
|
|
28
13
|
})).digest('hex').slice(0, 24);
|
|
29
14
|
}
|
|
30
|
-
export function loadStoredThreadId(runtimeId,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (state
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return state.threadId;
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
const store = loadStore();
|
|
51
|
-
const record = store.agents[agentId]?.[conversationId];
|
|
52
|
-
if (!record || record.cwd !== baseCwd)
|
|
53
|
-
return null;
|
|
54
|
-
if (policyFingerprint && record.policyFingerprint !== policyFingerprint) {
|
|
55
|
-
delete store.agents[agentId][conversationId];
|
|
56
|
-
if (Object.keys(store.agents[agentId]).length === 0) {
|
|
57
|
-
delete store.agents[agentId];
|
|
15
|
+
export function loadStoredThreadId(runtimeId, conversationId, baseCwd, executionMode, policyFingerprint) {
|
|
16
|
+
const state = loadRuntimeSessionState(runtimeId, {
|
|
17
|
+
conversationId,
|
|
18
|
+
baseCwd,
|
|
19
|
+
executionMode,
|
|
20
|
+
});
|
|
21
|
+
if (state?.threadId) {
|
|
22
|
+
if (policyFingerprint && state.codexPolicyFingerprint !== policyFingerprint) {
|
|
23
|
+
clearRuntimeSessionState(runtimeId, {
|
|
24
|
+
conversationId,
|
|
25
|
+
baseCwd,
|
|
26
|
+
executionMode,
|
|
27
|
+
});
|
|
28
|
+
return null;
|
|
58
29
|
}
|
|
59
|
-
|
|
60
|
-
return null;
|
|
30
|
+
return state.threadId;
|
|
61
31
|
}
|
|
62
|
-
return
|
|
32
|
+
return null;
|
|
63
33
|
}
|
|
64
|
-
export function saveStoredThreadId(runtimeId,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
executionMode,
|
|
70
|
-
threadId,
|
|
71
|
-
...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
|
|
72
|
-
});
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const store = loadStore();
|
|
76
|
-
store.agents[agentId] ??= {};
|
|
77
|
-
store.agents[agentId][conversationId] = {
|
|
34
|
+
export function saveStoredThreadId(runtimeId, conversationId, baseCwd, threadId, executionMode, policyFingerprint) {
|
|
35
|
+
saveRuntimeSessionState(runtimeId, {
|
|
36
|
+
conversationId,
|
|
37
|
+
baseCwd,
|
|
38
|
+
executionMode,
|
|
78
39
|
threadId,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
updatedAt: new Date().toISOString(),
|
|
82
|
-
};
|
|
83
|
-
saveStore(store);
|
|
40
|
+
...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
|
|
41
|
+
});
|
|
84
42
|
}
|
|
85
|
-
export function clearStoredThreadId(runtimeId,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
? loadRuntimeSessionState(runtimeId, {
|
|
89
|
-
conversationId,
|
|
90
|
-
baseCwd,
|
|
91
|
-
executionMode,
|
|
92
|
-
})
|
|
93
|
-
: null;
|
|
94
|
-
clearRuntimeSessionState(runtimeId, {
|
|
43
|
+
export function clearStoredThreadId(runtimeId, conversationId, baseCwd, executionMode) {
|
|
44
|
+
const existing = baseCwd
|
|
45
|
+
? loadRuntimeSessionState(runtimeId, {
|
|
95
46
|
conversationId,
|
|
96
47
|
baseCwd,
|
|
97
48
|
executionMode,
|
|
49
|
+
})
|
|
50
|
+
: null;
|
|
51
|
+
clearRuntimeSessionState(runtimeId, {
|
|
52
|
+
conversationId,
|
|
53
|
+
baseCwd,
|
|
54
|
+
executionMode,
|
|
55
|
+
});
|
|
56
|
+
if (existing?.lastInboundMessageId && baseCwd) {
|
|
57
|
+
saveRuntimeSessionState(runtimeId, {
|
|
58
|
+
conversationId,
|
|
59
|
+
baseCwd,
|
|
60
|
+
...(executionMode ? { executionMode } : {}),
|
|
61
|
+
lastInboundMessageId: existing.lastInboundMessageId,
|
|
98
62
|
});
|
|
99
|
-
if (existing?.lastInboundMessageId && baseCwd) {
|
|
100
|
-
saveRuntimeSessionState(runtimeId, {
|
|
101
|
-
conversationId,
|
|
102
|
-
baseCwd,
|
|
103
|
-
...(executionMode ? { executionMode } : {}),
|
|
104
|
-
lastInboundMessageId: existing.lastInboundMessageId,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const store = loadStore();
|
|
110
|
-
if (!store.agents[agentId]?.[conversationId])
|
|
111
|
-
return;
|
|
112
|
-
delete store.agents[agentId][conversationId];
|
|
113
|
-
if (Object.keys(store.agents[agentId]).length === 0) {
|
|
114
|
-
delete store.agents[agentId];
|
|
115
63
|
}
|
|
116
|
-
saveStore(store);
|
|
117
64
|
}
|
package/dist/setup.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.6",
|
|
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": "^
|
|
33
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
34
|
-
"@canonmsg/core": "^
|
|
32
|
+
"@canonmsg/agent-sdk": "^8.0.0",
|
|
33
|
+
"@canonmsg/coding-agent-host": "^0.4.0",
|
|
34
|
+
"@canonmsg/core": "^9.0.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.0.0"
|
package/dist/cli-entry.d.ts
DELETED
package/dist/cli-entry.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
package/dist/inbound-policy.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type CanonGroupContext, type CanonGroupContextMode, type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
|
|
2
|
-
export interface InboundParticipantContext {
|
|
3
|
-
conversationType: CanonConversation['type'] | 'unknown';
|
|
4
|
-
memberCount: number | null;
|
|
5
|
-
senderType: 'human' | 'ai_agent';
|
|
6
|
-
senderName: string;
|
|
7
|
-
isOwner: boolean;
|
|
8
|
-
mentionedAgent: boolean;
|
|
9
|
-
groupContext?: CanonGroupContext;
|
|
10
|
-
groupContextMode?: CanonGroupContextMode;
|
|
11
|
-
recentSenderTypes: Array<'human' | 'ai_agent'>;
|
|
12
|
-
recentHumanCount: number;
|
|
13
|
-
recentAgentCount: number;
|
|
14
|
-
consecutiveAgentTurns: number;
|
|
15
|
-
currentAgentStreakStartedByHuman: boolean;
|
|
16
|
-
}
|
|
17
|
-
export interface AutoReplyDecision {
|
|
18
|
-
allow: boolean;
|
|
19
|
-
reason: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function buildInboundContextLines(context: InboundParticipantContext): string[];
|
|
22
|
-
export declare function decideAutoReply(context: InboundParticipantContext, behavior?: ResolvedAgentBehaviorPolicy | null): AutoReplyDecision;
|
package/dist/inbound-policy.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { buildCompactGroupContextLines, evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
|
|
2
|
-
function formatRecentSenders(senderTypes) {
|
|
3
|
-
if (senderTypes.length === 0)
|
|
4
|
-
return 'none';
|
|
5
|
-
return senderTypes.map((senderType) => (senderType === 'ai_agent' ? 'agent' : 'human')).join(' -> ');
|
|
6
|
-
}
|
|
7
|
-
export function buildInboundContextLines(context) {
|
|
8
|
-
const conversationTypeLabel = context.conversationType === 'unknown'
|
|
9
|
-
? 'unknown'
|
|
10
|
-
: `${context.conversationType}${context.memberCount ? ` (${context.memberCount} members)` : ''}`;
|
|
11
|
-
const senderRole = context.isOwner
|
|
12
|
-
? 'The latest sender is the verified human owner of this Canon agent.'
|
|
13
|
-
: context.senderType === 'ai_agent'
|
|
14
|
-
? 'The latest sender is another AI agent in Canon.'
|
|
15
|
-
: 'The latest sender is a human Canon participant.';
|
|
16
|
-
return [
|
|
17
|
-
senderRole,
|
|
18
|
-
`Latest sender name: ${context.senderName}`,
|
|
19
|
-
`Latest sender type: ${context.senderType}`,
|
|
20
|
-
`Conversation type: ${conversationTypeLabel}`,
|
|
21
|
-
...(context.groupContext && context.groupContextMode
|
|
22
|
-
? buildCompactGroupContextLines(context.groupContext, context.groupContextMode)
|
|
23
|
-
: []),
|
|
24
|
-
`Directly addressed to this agent: ${context.mentionedAgent ? 'yes' : 'no'}`,
|
|
25
|
-
`Recent sender pattern: ${formatRecentSenders(context.recentSenderTypes)}`,
|
|
26
|
-
`Recent human messages: ${context.recentHumanCount}`,
|
|
27
|
-
`Recent agent messages: ${context.recentAgentCount}`,
|
|
28
|
-
`Consecutive recent agent turns: ${context.consecutiveAgentTurns}`,
|
|
29
|
-
`Current agent streak started after a human message: ${context.currentAgentStreakStartedByHuman ? 'yes' : 'no'}`,
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
export function decideAutoReply(context, behavior) {
|
|
33
|
-
const decision = evaluateParticipationPolicy(behavior ?? resolveAgentBehaviorPolicy(), {
|
|
34
|
-
conversationType: context.conversationType,
|
|
35
|
-
senderType: context.senderType,
|
|
36
|
-
isOwner: context.isOwner,
|
|
37
|
-
mentionedAgent: context.mentionedAgent,
|
|
38
|
-
recentHumanCount: context.recentHumanCount,
|
|
39
|
-
consecutiveAgentTurns: context.consecutiveAgentTurns,
|
|
40
|
-
currentAgentStreakStartedByHuman: context.currentAgentStreakStartedByHuman,
|
|
41
|
-
});
|
|
42
|
-
return {
|
|
43
|
-
allow: decision.allow,
|
|
44
|
-
reason: decision.reason,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Startup recovery for inbound messages missed while the host was offline.
|
|
3
|
-
*
|
|
4
|
-
* The host persists a `lastInboundMessageId` cursor per conversation. On
|
|
5
|
-
* startup we paginate `getMessagesPage` (newest-first pages, older pages via
|
|
6
|
-
* its `before` message-id parameter) until the cursor is found or a hard
|
|
7
|
-
* per-conversation bound is hit, then replay everything after the cursor.
|
|
8
|
-
*
|
|
9
|
-
* This module is intentionally identical in packages/claude-code-plugin and
|
|
10
|
-
* packages/codex-plugin — keep both copies in sync (future consolidation
|
|
11
|
-
* candidate).
|
|
12
|
-
*/
|
|
13
|
-
export declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
14
|
-
export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
15
|
-
export interface StartupRecoveryMessage {
|
|
16
|
-
id: string;
|
|
17
|
-
senderId: string;
|
|
18
|
-
createdAt?: string;
|
|
19
|
-
metadata?: Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
export interface StartupRecoveryPage {
|
|
22
|
-
messages: StartupRecoveryMessage[];
|
|
23
|
-
}
|
|
24
|
-
export type StartupRecoveryMode =
|
|
25
|
-
/** Cursor found — `messages` is everything strictly after it. */
|
|
26
|
-
'after-cursor'
|
|
27
|
-
/** Cursor present but not found within the bound — `messages` is the bounded recent window. */
|
|
28
|
-
| 'truncated-window'
|
|
29
|
-
/**
|
|
30
|
-
* No usable cursor (fresh runtime file, or the cursor message no longer
|
|
31
|
-
* exists in history) — only the newest inbound message is recovered, since
|
|
32
|
-
* a full-history replay could fire mass duplicate turns.
|
|
33
|
-
*/
|
|
34
|
-
| 'latest-only';
|
|
35
|
-
export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
|
|
36
|
-
mode: StartupRecoveryMode;
|
|
37
|
-
/** Missed inbound messages (own messages excluded), oldest first. */
|
|
38
|
-
messages: TPage['messages'];
|
|
39
|
-
/** First page fetched — reusable as hydration context for recovered turns. */
|
|
40
|
-
newestPage: TPage;
|
|
41
|
-
}
|
|
42
|
-
export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
|
|
43
|
-
fetchPage: (before?: string) => Promise<TPage>;
|
|
44
|
-
cursor: string | null | undefined;
|
|
45
|
-
agentId: string;
|
|
46
|
-
maxMessages?: number;
|
|
47
|
-
}): Promise<StartupRecoveryResult<TPage>>;
|
package/dist/startup-recovery.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Startup recovery for inbound messages missed while the host was offline.
|
|
3
|
-
*
|
|
4
|
-
* The host persists a `lastInboundMessageId` cursor per conversation. On
|
|
5
|
-
* startup we paginate `getMessagesPage` (newest-first pages, older pages via
|
|
6
|
-
* its `before` message-id parameter) until the cursor is found or a hard
|
|
7
|
-
* per-conversation bound is hit, then replay everything after the cursor.
|
|
8
|
-
*
|
|
9
|
-
* This module is intentionally identical in packages/claude-code-plugin and
|
|
10
|
-
* packages/codex-plugin — keep both copies in sync (future consolidation
|
|
11
|
-
* candidate).
|
|
12
|
-
*/
|
|
13
|
-
export const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
14
|
-
export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
15
|
-
export async function collectMissedInboundMessages(input) {
|
|
16
|
-
const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
|
|
17
|
-
const newestPage = await input.fetchPage();
|
|
18
|
-
const collected = [...newestPage.messages];
|
|
19
|
-
const seenIds = new Set(collected.map((message) => message.id));
|
|
20
|
-
const hasCursor = (messages) => input.cursor != null && messages.some((message) => message.id === input.cursor);
|
|
21
|
-
let cursorFound = hasCursor(collected);
|
|
22
|
-
if (input.cursor != null) {
|
|
23
|
-
while (!cursorFound && collected.length < maxMessages) {
|
|
24
|
-
// Pages are newest-first, so the last collected message is the oldest.
|
|
25
|
-
const before = collected[collected.length - 1]?.id;
|
|
26
|
-
if (!before)
|
|
27
|
-
break;
|
|
28
|
-
const page = await input.fetchPage(before);
|
|
29
|
-
const fresh = page.messages.filter((message) => !seenIds.has(message.id));
|
|
30
|
-
// No pagination progress (history exhausted, or the server ignored the
|
|
31
|
-
// `before` cursor because that message was hard-deleted) — stop here.
|
|
32
|
-
if (fresh.length === 0)
|
|
33
|
-
break;
|
|
34
|
-
for (const message of fresh)
|
|
35
|
-
seenIds.add(message.id);
|
|
36
|
-
collected.push(...fresh);
|
|
37
|
-
cursorFound = hasCursor(fresh);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
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));
|
|
55
|
-
let mode;
|
|
56
|
-
let missed;
|
|
57
|
-
if (cursorFound) {
|
|
58
|
-
const cursorIndex = ascending.findIndex((message) => message.id === input.cursor);
|
|
59
|
-
mode = 'after-cursor';
|
|
60
|
-
missed = inboundOnly(ascending.slice(cursorIndex + 1));
|
|
61
|
-
}
|
|
62
|
-
else if (input.cursor != null && collected.length >= maxMessages) {
|
|
63
|
-
mode = 'truncated-window';
|
|
64
|
-
missed = inboundOnly(ascending.slice(-maxMessages));
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
mode = 'latest-only';
|
|
68
|
-
missed = inboundOnly(ascending).slice(-1);
|
|
69
|
-
}
|
|
70
|
-
// Safe: `missed` only holds elements of pages returned by `fetchPage`.
|
|
71
|
-
return { mode, messages: missed, newestPage };
|
|
72
|
-
}
|