@canonmsg/codex-plugin 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +11 -8
- package/dist/host-runtime.d.ts +89 -0
- package/dist/host-runtime.js +152 -0
- package/dist/host.js +40 -97
- package/package.json +2 -2
package/dist/adapter.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare class CodexConversationAdapter {
|
|
|
36
36
|
private readonly codexBin;
|
|
37
37
|
private model;
|
|
38
38
|
private readonly sandbox;
|
|
39
|
-
private readonly
|
|
39
|
+
private readonly legacyApprovalPolicy;
|
|
40
40
|
private readonly codexProfile;
|
|
41
41
|
private readonly addDirs;
|
|
42
42
|
private readonly configOverrides;
|
package/dist/adapter.js
CHANGED
|
@@ -5,7 +5,7 @@ export class CodexConversationAdapter {
|
|
|
5
5
|
codexBin;
|
|
6
6
|
model;
|
|
7
7
|
sandbox;
|
|
8
|
-
|
|
8
|
+
legacyApprovalPolicy;
|
|
9
9
|
codexProfile;
|
|
10
10
|
addDirs;
|
|
11
11
|
configOverrides;
|
|
@@ -21,7 +21,7 @@ export class CodexConversationAdapter {
|
|
|
21
21
|
this.codexBin = opts.codexBin ?? 'codex';
|
|
22
22
|
this.model = opts.model ?? null;
|
|
23
23
|
this.sandbox = opts.sandbox ?? null;
|
|
24
|
-
this.
|
|
24
|
+
this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
|
|
25
25
|
this.codexProfile = opts.codexProfile ?? null;
|
|
26
26
|
this.addDirs = opts.addDirs ?? [];
|
|
27
27
|
this.configOverrides = opts.configOverrides ?? [];
|
|
@@ -165,7 +165,7 @@ export class CodexConversationAdapter {
|
|
|
165
165
|
const args = ['exec', '--json', '--color', 'never', '-C', this.cwd, '--skip-git-repo-check'];
|
|
166
166
|
const execMode = resolveExecMode({
|
|
167
167
|
sandbox: this.sandbox,
|
|
168
|
-
approvalPolicy: this.
|
|
168
|
+
approvalPolicy: this.legacyApprovalPolicy,
|
|
169
169
|
fullAuto: this.fullAuto,
|
|
170
170
|
bypassApprovalsAndSandbox: this.bypassApprovalsAndSandbox,
|
|
171
171
|
});
|
|
@@ -231,15 +231,18 @@ function resolveExecMode(input) {
|
|
|
231
231
|
if (input.fullAuto) {
|
|
232
232
|
return { fullAuto: true, bypassApprovalsAndSandbox: false };
|
|
233
233
|
}
|
|
234
|
-
|
|
235
|
-
// Preserve the old Canon example (`--sandbox workspace-write --ask-for-approval never`)
|
|
236
|
-
// by translating it to the supported `--full-auto` mode.
|
|
237
|
-
if (input.approvalPolicy === 'never' &&
|
|
238
|
-
(input.sandbox === 'workspace-write' || input.sandbox == null)) {
|
|
234
|
+
if (shouldTranslateLegacyApprovalMode(input)) {
|
|
239
235
|
return { fullAuto: true, bypassApprovalsAndSandbox: false };
|
|
240
236
|
}
|
|
241
237
|
return { fullAuto: false, bypassApprovalsAndSandbox: false };
|
|
242
238
|
}
|
|
239
|
+
function shouldTranslateLegacyApprovalMode(input) {
|
|
240
|
+
// Newer Codex CLI releases no longer accept --ask-for-approval for `exec`.
|
|
241
|
+
// Keep the compatibility shim isolated here so the rest of the adapter only
|
|
242
|
+
// deals with the supported execution switches.
|
|
243
|
+
return input.approvalPolicy === 'never'
|
|
244
|
+
&& (input.sandbox === 'workspace-write' || input.sandbox == null);
|
|
245
|
+
}
|
|
243
246
|
function isIgnorableCodexLog(line) {
|
|
244
247
|
return [
|
|
245
248
|
'Reading additional input from stdin...',
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-runtime helpers, inlined from @canonmsg/core.
|
|
3
|
+
*
|
|
4
|
+
* These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
|
|
5
|
+
* RTDB session/runtime surface. They used to live in
|
|
6
|
+
* `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
|
|
7
|
+
* media-parity PR C so core does not have to carry host-specific concerns.
|
|
8
|
+
*
|
|
9
|
+
* Keep this file in lockstep with the equivalent file in
|
|
10
|
+
* `packages/claude-code-plugin/src/host-runtime.ts`. If you change the
|
|
11
|
+
* behavior here, update that copy too and adjust the shared golden
|
|
12
|
+
* fixture test (`packages/codex-plugin/src/host-runtime.test.ts`).
|
|
13
|
+
*/
|
|
14
|
+
import { type AgentClientType, type AgentRuntime, type CanonClient, type CanonConversation, type CanonMessage, type CanonMessagesPage, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type SessionWorkspaceConfig } from '@canonmsg/core';
|
|
15
|
+
export interface HostInboundParticipantContext {
|
|
16
|
+
conversationType: CanonConversation['type'] | 'unknown';
|
|
17
|
+
memberCount: number | null;
|
|
18
|
+
senderType: 'human' | 'ai_agent';
|
|
19
|
+
senderName: string;
|
|
20
|
+
isOwner: boolean;
|
|
21
|
+
mentionedAgent: boolean;
|
|
22
|
+
recentSenderTypes: Array<'human' | 'ai_agent'>;
|
|
23
|
+
recentHumanCount: number;
|
|
24
|
+
recentAgentCount: number;
|
|
25
|
+
consecutiveAgentTurns: number;
|
|
26
|
+
currentAgentStreakStartedByHuman: boolean;
|
|
27
|
+
}
|
|
28
|
+
type HostInboundMessage = {
|
|
29
|
+
text?: string | null;
|
|
30
|
+
contentType?: CanonMessage['contentType'] | null;
|
|
31
|
+
audioUrl?: string | null;
|
|
32
|
+
audioDurationMs?: number | null;
|
|
33
|
+
imageUrl?: string | null;
|
|
34
|
+
attachments?: CanonMessage['attachments'];
|
|
35
|
+
senderType?: CanonMessage['senderType'];
|
|
36
|
+
mentions?: string[] | null;
|
|
37
|
+
};
|
|
38
|
+
interface HostWorkspaceResolverOption {
|
|
39
|
+
id: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function buildCanonHostPrompt(input: {
|
|
43
|
+
hostLabel: string;
|
|
44
|
+
content: string;
|
|
45
|
+
conversationId: string;
|
|
46
|
+
participantContext: HostInboundParticipantContext;
|
|
47
|
+
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
48
|
+
workSession?: MessageCreatedPayload['message']['workSession'];
|
|
49
|
+
workSessions?: MessageCreatedPayload['workSessions'];
|
|
50
|
+
buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
|
|
51
|
+
}): string;
|
|
52
|
+
export declare function renderCanonHostInboundContent(message: HostInboundMessage): string;
|
|
53
|
+
export declare function buildHydratedInboundContext(input: {
|
|
54
|
+
agentId: string;
|
|
55
|
+
conversation: CanonConversation | null;
|
|
56
|
+
page?: CanonMessagesPage | null;
|
|
57
|
+
message: HostInboundMessage;
|
|
58
|
+
senderName: string;
|
|
59
|
+
isOwner: boolean;
|
|
60
|
+
}): {
|
|
61
|
+
participantContext: HostInboundParticipantContext;
|
|
62
|
+
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
63
|
+
workSessions: NonNullable<MessageCreatedPayload['workSessions']>;
|
|
64
|
+
hydratedFromPage: boolean;
|
|
65
|
+
};
|
|
66
|
+
export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
|
|
67
|
+
export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null;
|
|
68
|
+
export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
|
|
69
|
+
conversationId: string;
|
|
70
|
+
agentId: string;
|
|
71
|
+
extraStringFields?: readonly TExtra[];
|
|
72
|
+
}): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null>;
|
|
73
|
+
export declare function resolveHostWorkspaceCwd(input: {
|
|
74
|
+
workspaceOptions: HostWorkspaceResolverOption[];
|
|
75
|
+
config: {
|
|
76
|
+
workspaceId?: string;
|
|
77
|
+
retiredWorkspaceConfig?: boolean;
|
|
78
|
+
} | null;
|
|
79
|
+
defaultCwd: string;
|
|
80
|
+
}): string;
|
|
81
|
+
export declare function createConversationMetadataLoader(input: {
|
|
82
|
+
client: CanonClient;
|
|
83
|
+
conversationCache: Map<string, CanonConversation>;
|
|
84
|
+
cacheTtlMs?: number;
|
|
85
|
+
}): {
|
|
86
|
+
refreshConversationCache(force?: boolean): Promise<void>;
|
|
87
|
+
getConversationMeta(conversationId: string): Promise<CanonConversation | null>;
|
|
88
|
+
};
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-runtime helpers, inlined from @canonmsg/core.
|
|
3
|
+
*
|
|
4
|
+
* These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
|
|
5
|
+
* RTDB session/runtime surface. They used to live in
|
|
6
|
+
* `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
|
|
7
|
+
* media-parity PR C so core does not have to carry host-specific concerns.
|
|
8
|
+
*
|
|
9
|
+
* Keep this file in lockstep with the equivalent file in
|
|
10
|
+
* `packages/claude-code-plugin/src/host-runtime.ts`. If you change the
|
|
11
|
+
* behavior here, update that copy too and adjust the shared golden
|
|
12
|
+
* fixture test (`packages/codex-plugin/src/host-runtime.test.ts`).
|
|
13
|
+
*/
|
|
14
|
+
import { buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
|
|
15
|
+
export function buildCanonHostPrompt(input) {
|
|
16
|
+
const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
|
|
17
|
+
return [
|
|
18
|
+
`You are connected to Canon messaging through a ${input.hostLabel} host wrapper.`,
|
|
19
|
+
'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
|
|
20
|
+
'Short intermediate assistant messages may be shown as ephemeral status while you work.',
|
|
21
|
+
...input.buildInboundContextLines(input.participantContext),
|
|
22
|
+
...buildBehaviorPolicyLines(input.behavior),
|
|
23
|
+
...buildWorkSessionsPromptLines(resolvedWorkSessions),
|
|
24
|
+
'Canon participants may be humans or AI agents.',
|
|
25
|
+
'Honor the Canon behavior policy above when deciding how proactively to participate.',
|
|
26
|
+
...(resolvedWorkSessions.length > 0
|
|
27
|
+
? ['Honor the Canon work-session context above within its stated disclosure limits.']
|
|
28
|
+
: []),
|
|
29
|
+
`Conversation ID: ${input.conversationId}`,
|
|
30
|
+
'',
|
|
31
|
+
'New Canon message:',
|
|
32
|
+
input.content,
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
export function renderCanonHostInboundContent(message) {
|
|
36
|
+
let content = message.text || '';
|
|
37
|
+
const attachment = message.attachments?.[0];
|
|
38
|
+
if (attachment?.kind === 'audio' && attachment.url) {
|
|
39
|
+
const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
|
|
40
|
+
content = content
|
|
41
|
+
? `[Voice message${duration}: ${attachment.url}]\n${content}`
|
|
42
|
+
: `[Voice message${duration}: ${attachment.url}]`;
|
|
43
|
+
}
|
|
44
|
+
else if (attachment?.kind === 'image' && attachment.url) {
|
|
45
|
+
content = content
|
|
46
|
+
? `[Image: ${attachment.url}]\n${content}`
|
|
47
|
+
: `[Image: ${attachment.url}]`;
|
|
48
|
+
}
|
|
49
|
+
else if (attachment?.kind === 'file' && attachment.url) {
|
|
50
|
+
const label = attachment.fileName || 'File';
|
|
51
|
+
content = content
|
|
52
|
+
? `[File: ${label} ${attachment.url}]\n${content}`
|
|
53
|
+
: `[File: ${label} ${attachment.url}]`;
|
|
54
|
+
}
|
|
55
|
+
else if (message.contentType === 'audio' && message.audioUrl) {
|
|
56
|
+
const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
|
|
57
|
+
content = content
|
|
58
|
+
? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
|
|
59
|
+
: `[Voice message${duration}: ${message.audioUrl}]`;
|
|
60
|
+
}
|
|
61
|
+
else if (message.contentType === 'image' && message.imageUrl) {
|
|
62
|
+
content = content
|
|
63
|
+
? `[Image: ${message.imageUrl}]\n${content}`
|
|
64
|
+
: `[Image: ${message.imageUrl}]`;
|
|
65
|
+
}
|
|
66
|
+
return content || '[Empty message]';
|
|
67
|
+
}
|
|
68
|
+
export function buildHydratedInboundContext(input) {
|
|
69
|
+
const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
|
|
70
|
+
return {
|
|
71
|
+
participantContext: {
|
|
72
|
+
conversationType: input.conversation?.type ?? 'unknown',
|
|
73
|
+
memberCount: input.conversation?.memberIds?.length ?? null,
|
|
74
|
+
senderType: input.message.senderType ?? 'human',
|
|
75
|
+
senderName: input.senderName,
|
|
76
|
+
isOwner: input.isOwner,
|
|
77
|
+
mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
|
|
78
|
+
recentSenderTypes: history.recentSenderTypes,
|
|
79
|
+
recentHumanCount: history.recentHumanCount,
|
|
80
|
+
recentAgentCount: history.recentAgentCount,
|
|
81
|
+
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
82
|
+
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
83
|
+
},
|
|
84
|
+
behavior: input.page?.behavior ?? input.conversation?.behavior,
|
|
85
|
+
workSessions: input.page?.workSessions ?? [],
|
|
86
|
+
hydratedFromPage: input.page != null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function publishHostAgentRuntime(agentId, clientType, runtime) {
|
|
90
|
+
await rtdbWrite(`/agent-runtime/${agentId}`, {
|
|
91
|
+
clientType,
|
|
92
|
+
hostMode: true,
|
|
93
|
+
...runtime,
|
|
94
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function readHostSessionConfig(raw, extraStringFields = []) {
|
|
98
|
+
const baseConfig = readSessionWorkspaceConfig(raw);
|
|
99
|
+
if (!raw || typeof raw !== 'object') {
|
|
100
|
+
return baseConfig;
|
|
101
|
+
}
|
|
102
|
+
const data = raw;
|
|
103
|
+
const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
|
|
104
|
+
const value = normalizeOptionalString(data[field]);
|
|
105
|
+
return value ? [[field, value]] : [];
|
|
106
|
+
}));
|
|
107
|
+
return {
|
|
108
|
+
...(baseConfig ?? {}),
|
|
109
|
+
...extraConfig,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export async function loadHostSessionConfig(input) {
|
|
113
|
+
const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
|
|
114
|
+
return readHostSessionConfig(raw, input.extraStringFields);
|
|
115
|
+
}
|
|
116
|
+
export function resolveHostWorkspaceCwd(input) {
|
|
117
|
+
return resolveConfiguredWorkspaceCwd(input);
|
|
118
|
+
}
|
|
119
|
+
export function createConversationMetadataLoader(input) {
|
|
120
|
+
const cacheTtlMs = input.cacheTtlMs ?? 10_000;
|
|
121
|
+
let conversationCacheLoadedAt = 0;
|
|
122
|
+
async function refreshConversationCache(force = false) {
|
|
123
|
+
if (!force
|
|
124
|
+
&& input.conversationCache.size > 0
|
|
125
|
+
&& Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const conversations = await input.client.getConversations();
|
|
129
|
+
input.conversationCache.clear();
|
|
130
|
+
for (const conversation of conversations) {
|
|
131
|
+
input.conversationCache.set(conversation.id, conversation);
|
|
132
|
+
}
|
|
133
|
+
conversationCacheLoadedAt = Date.now();
|
|
134
|
+
}
|
|
135
|
+
async function getConversationMeta(conversationId) {
|
|
136
|
+
try {
|
|
137
|
+
await refreshConversationCache();
|
|
138
|
+
const cached = input.conversationCache.get(conversationId);
|
|
139
|
+
if (cached)
|
|
140
|
+
return cached;
|
|
141
|
+
await refreshConversationCache(true);
|
|
142
|
+
return input.conversationCache.get(conversationId) ?? null;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return input.conversationCache.get(conversationId) ?? null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
refreshConversationCache,
|
|
150
|
+
getConversationMeta,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/dist/host.js
CHANGED
|
@@ -3,7 +3,8 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
3
3
|
setDefaultResultOrder('ipv4first');
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import {
|
|
6
|
+
import { buildConfiguredWorkspaceOptions, buildPublicWorkspaceOptions, ExecutionEnvironmentError, isEnabledFlag, CanonClient, CanonStream, clearSessionState, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfile, initRTDBAuth, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, releaseLock, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, writeSessionState, writeTurnState, } from '@canonmsg/core';
|
|
7
|
+
import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
|
|
7
8
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
8
9
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
9
10
|
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
@@ -37,71 +38,27 @@ function normalizeRuntimeTurnState(value) {
|
|
|
37
38
|
return null;
|
|
38
39
|
}
|
|
39
40
|
async function publishAgentRuntime(agentId, runtime) {
|
|
40
|
-
await
|
|
41
|
-
clientType: 'codex',
|
|
42
|
-
hostMode: true,
|
|
43
|
-
...runtime,
|
|
44
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
45
|
-
});
|
|
41
|
+
await publishHostAgentRuntime(agentId, 'codex', runtime);
|
|
46
42
|
}
|
|
47
43
|
async function loadSessionConfig(conversationId, agentId) {
|
|
48
|
-
|
|
49
|
-
return readSessionWorkspaceConfig(raw);
|
|
44
|
+
return loadHostSessionConfig({ conversationId, agentId });
|
|
50
45
|
}
|
|
51
46
|
function resolveWorkspaceCwd(config) {
|
|
52
|
-
return
|
|
47
|
+
return resolveHostWorkspaceCwd({
|
|
53
48
|
workspaceOptions,
|
|
54
49
|
config,
|
|
55
50
|
defaultCwd: workingDir,
|
|
56
51
|
});
|
|
57
52
|
}
|
|
58
53
|
function buildCanonPrompt(input) {
|
|
59
|
-
return
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
...buildBehaviorPolicyLines(input.behavior),
|
|
65
|
-
'Canon participants may be humans or AI agents.',
|
|
66
|
-
'Honor the Canon behavior policy above when deciding how proactively to participate.',
|
|
67
|
-
`Conversation ID: ${input.conversationId}`,
|
|
68
|
-
'',
|
|
69
|
-
'New Canon message:',
|
|
70
|
-
input.content,
|
|
71
|
-
].join('\n');
|
|
54
|
+
return buildCanonHostPrompt({
|
|
55
|
+
hostLabel: 'Codex',
|
|
56
|
+
buildInboundContextLines,
|
|
57
|
+
...input,
|
|
58
|
+
});
|
|
72
59
|
}
|
|
73
60
|
function renderInboundContent(message) {
|
|
74
|
-
|
|
75
|
-
const attachment = message.attachments?.[0];
|
|
76
|
-
if (attachment?.kind === 'audio' && attachment.url) {
|
|
77
|
-
const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
|
|
78
|
-
content = content
|
|
79
|
-
? `[Voice message${duration}: ${attachment.url}]\n${content}`
|
|
80
|
-
: `[Voice message${duration}: ${attachment.url}]`;
|
|
81
|
-
}
|
|
82
|
-
else if (attachment?.kind === 'image' && attachment.url) {
|
|
83
|
-
content = content
|
|
84
|
-
? `[Image: ${attachment.url}]\n${content}`
|
|
85
|
-
: `[Image: ${attachment.url}]`;
|
|
86
|
-
}
|
|
87
|
-
else if (attachment?.kind === 'file' && attachment.url) {
|
|
88
|
-
const label = attachment.fileName || 'File';
|
|
89
|
-
content = content
|
|
90
|
-
? `[File: ${label} ${attachment.url}]\n${content}`
|
|
91
|
-
: `[File: ${label} ${attachment.url}]`;
|
|
92
|
-
}
|
|
93
|
-
else if (message.contentType === 'audio' && message.audioUrl) {
|
|
94
|
-
const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
|
|
95
|
-
content = content
|
|
96
|
-
? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
|
|
97
|
-
: `[Voice message${duration}: ${message.audioUrl}]`;
|
|
98
|
-
}
|
|
99
|
-
else if (message.contentType === 'image' && message.imageUrl) {
|
|
100
|
-
content = content
|
|
101
|
-
? `[Image: ${message.imageUrl}]\n${content}`
|
|
102
|
-
: `[Image: ${message.imageUrl}]`;
|
|
103
|
-
}
|
|
104
|
-
return content || '[Empty message]';
|
|
61
|
+
return renderCanonHostInboundContent(message);
|
|
105
62
|
}
|
|
106
63
|
function summarizeCommand(command) {
|
|
107
64
|
const trimmed = command.trim();
|
|
@@ -173,31 +130,10 @@ async function main() {
|
|
|
173
130
|
const sessions = new Map();
|
|
174
131
|
const pendingSessionCreations = new Map();
|
|
175
132
|
const conversationCache = new Map();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
const conversations = await client.getConversations();
|
|
182
|
-
conversationCache.clear();
|
|
183
|
-
for (const conversation of conversations) {
|
|
184
|
-
conversationCache.set(conversation.id, conversation);
|
|
185
|
-
}
|
|
186
|
-
conversationCacheLoadedAt = Date.now();
|
|
187
|
-
}
|
|
188
|
-
async function getConversationMeta(conversationId) {
|
|
189
|
-
try {
|
|
190
|
-
await refreshConversationCache();
|
|
191
|
-
const cached = conversationCache.get(conversationId);
|
|
192
|
-
if (cached)
|
|
193
|
-
return cached;
|
|
194
|
-
await refreshConversationCache(true);
|
|
195
|
-
return conversationCache.get(conversationId) ?? null;
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return conversationCache.get(conversationId) ?? null;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
133
|
+
const { getConversationMeta } = createConversationMetadataLoader({
|
|
134
|
+
client,
|
|
135
|
+
conversationCache,
|
|
136
|
+
});
|
|
201
137
|
async function loadSenderRuntimeState(conversationId, senderId) {
|
|
202
138
|
try {
|
|
203
139
|
const [turnState, sessionState] = await Promise.all([
|
|
@@ -210,25 +146,21 @@ async function main() {
|
|
|
210
146
|
return null;
|
|
211
147
|
}
|
|
212
148
|
}
|
|
213
|
-
async function
|
|
214
|
-
const [conversation,
|
|
149
|
+
async function loadHydratedInboundContext(input) {
|
|
150
|
+
const [conversation, page] = await Promise.all([
|
|
215
151
|
getConversationMeta(input.conversationId),
|
|
216
|
-
|
|
152
|
+
input.hydratedPage
|
|
153
|
+
? Promise.resolve(input.hydratedPage)
|
|
154
|
+
: client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
|
|
217
155
|
]);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
156
|
+
return buildHydratedInboundContext({
|
|
157
|
+
agentId,
|
|
158
|
+
conversation,
|
|
159
|
+
page,
|
|
160
|
+
message: input.message,
|
|
223
161
|
senderName: input.senderName,
|
|
224
162
|
isOwner: input.isOwner,
|
|
225
|
-
|
|
226
|
-
recentSenderTypes: history.recentSenderTypes,
|
|
227
|
-
recentHumanCount: history.recentHumanCount,
|
|
228
|
-
recentAgentCount: history.recentAgentCount,
|
|
229
|
-
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
230
|
-
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
231
|
-
};
|
|
163
|
+
});
|
|
232
164
|
}
|
|
233
165
|
function writeState(session) {
|
|
234
166
|
writeSessionState(session.conversationId, agentId, {
|
|
@@ -385,14 +317,20 @@ async function main() {
|
|
|
385
317
|
}
|
|
386
318
|
async function enqueueInboundMessage(input) {
|
|
387
319
|
const content = renderInboundContent(input.message);
|
|
388
|
-
const
|
|
389
|
-
const behavior = input.behavior ?? conversation?.behavior;
|
|
390
|
-
const participantContext = await loadParticipantContext({
|
|
320
|
+
const hydrated = await loadHydratedInboundContext({
|
|
391
321
|
conversationId: input.conversationId,
|
|
392
322
|
message: input.message,
|
|
393
323
|
senderName: input.senderName,
|
|
394
324
|
isOwner: input.isOwner,
|
|
325
|
+
hydratedPage: input.hydratedPage,
|
|
395
326
|
});
|
|
327
|
+
const behavior = input.behavior ?? hydrated.behavior;
|
|
328
|
+
const workSessions = hydrated.hydratedFromPage
|
|
329
|
+
? hydrated.workSessions
|
|
330
|
+
: Array.isArray(input.workSessions)
|
|
331
|
+
? input.workSessions
|
|
332
|
+
: hydrated.workSessions;
|
|
333
|
+
const participantContext = hydrated.participantContext;
|
|
396
334
|
const autoReply = decideAutoReply(participantContext, behavior);
|
|
397
335
|
if (!autoReply.allow) {
|
|
398
336
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
@@ -418,6 +356,8 @@ async function main() {
|
|
|
418
356
|
conversationId: input.conversationId,
|
|
419
357
|
participantContext,
|
|
420
358
|
behavior,
|
|
359
|
+
workSession: input.message.workSession,
|
|
360
|
+
workSessions,
|
|
421
361
|
});
|
|
422
362
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
423
363
|
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted);
|
|
@@ -589,6 +529,7 @@ async function main() {
|
|
|
589
529
|
senderName: message.senderName || message.senderId,
|
|
590
530
|
isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
|
|
591
531
|
behavior: payload.behavior,
|
|
532
|
+
workSessions: payload.workSessions,
|
|
592
533
|
});
|
|
593
534
|
},
|
|
594
535
|
onConnected: () => {
|
|
@@ -650,6 +591,8 @@ async function main() {
|
|
|
650
591
|
senderName: latestMessage.senderId,
|
|
651
592
|
isOwner: ownerId != null && latestMessage.senderId === ownerId,
|
|
652
593
|
behavior: latestPage.behavior,
|
|
594
|
+
workSessions: latestPage.workSessions,
|
|
595
|
+
hydratedPage: latestPage,
|
|
653
596
|
});
|
|
654
597
|
}
|
|
655
598
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/host.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prepack": "npm run build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@canonmsg/core": "^0.
|
|
25
|
+
"@canonmsg/core": "^0.7.0"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18.0.0"
|