@canonmsg/codex-plugin 0.25.1 → 0.26.2
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/README.md +31 -0
- package/dist/app-server-adapter.js +25 -1
- package/dist/codex-app-tools.d.ts +19 -0
- package/dist/codex-app-tools.js +89 -17
- package/dist/host.d.ts +57 -1
- package/dist/host.js +283 -54
- package/dist/session-store.d.ts +2 -0
- package/dist/session-store.js +8 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -60,6 +60,37 @@ You do not need a git repo for host mode. Any readable working directory is vali
|
|
|
60
60
|
- Quiet group turns: in groups the host shows the thinking indicator and the
|
|
61
61
|
answer only; direct chats keep the live preview and margin activity
|
|
62
62
|
|
|
63
|
+
### Service-agent mode
|
|
64
|
+
|
|
65
|
+
Long-running customer agents can use the same Canon host without exposing its
|
|
66
|
+
coding controls:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
canon-codex \
|
|
70
|
+
--cwd /srv/agent-workspace \
|
|
71
|
+
--service-agent \
|
|
72
|
+
--no-native-vision
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--service-agent` removes the coding-oriented dynamic tools and exposes only
|
|
76
|
+
`codex_app.canon_runtime_control` plus `codex_app.no_reply`. Runtime control is
|
|
77
|
+
bound by the host to the active Canon conversation and authenticated human; the
|
|
78
|
+
model cannot select another user, conversation, or responder. It can display a
|
|
79
|
+
card with `send_card`, wait for an action card with `request_card`, or ask one
|
|
80
|
+
standalone question with `request_input`.
|
|
81
|
+
|
|
82
|
+
Service-agent sessions use the configured shared workspace and read-only local
|
|
83
|
+
filesystem automatically. They do not ask conversation members to choose a
|
|
84
|
+
coding workspace, execution mode, model, reasoning level, or permission mode.
|
|
85
|
+
|
|
86
|
+
Authorized conversation members may use a service agent even when they do not
|
|
87
|
+
own its Canon identity. Normal coding-host sessions keep their existing
|
|
88
|
+
owner-only execution boundary.
|
|
89
|
+
|
|
90
|
+
`--no-native-vision` keeps inbound attachment paths in the prompt while
|
|
91
|
+
suppressing Codex's native image input. This lets a service agent use its
|
|
92
|
+
purpose-built OCR tool as the only image-recognition path.
|
|
93
|
+
|
|
63
94
|
## Transports
|
|
64
95
|
|
|
65
96
|
The host picks a transport at startup and logs the choice as
|
|
@@ -382,11 +382,26 @@ export class CodexAppServerAdapter {
|
|
|
382
382
|
}
|
|
383
383
|
async handleServerRequest(request) {
|
|
384
384
|
try {
|
|
385
|
+
const params = isRecord(request.params) ? request.params : {};
|
|
386
|
+
// Detached child threads may use the ordinary coding-thread bridge, but
|
|
387
|
+
// Canon conversation controls belong only to the foreground human turn.
|
|
388
|
+
// A fork can inherit dynamic-tool declarations, so enforce this again at
|
|
389
|
+
// dispatch instead of relying only on the child thread's advertised list.
|
|
390
|
+
if (isForegroundCanonToolCall(params) && !this.isCurrentThreadNotification(params)) {
|
|
391
|
+
this.write({
|
|
392
|
+
id: request.id,
|
|
393
|
+
error: {
|
|
394
|
+
code: -32001,
|
|
395
|
+
message: 'Server request does not belong to the active Canon turn',
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
385
400
|
const result = this.currentRequestHandler
|
|
386
401
|
? await this.currentRequestHandler({
|
|
387
402
|
id: request.id,
|
|
388
403
|
method: request.method,
|
|
389
|
-
params
|
|
404
|
+
params,
|
|
390
405
|
})
|
|
391
406
|
: defaultServerRequestResult(request.method);
|
|
392
407
|
this.write({ id: request.id, result });
|
|
@@ -616,6 +631,15 @@ export class CodexAppServerAdapter {
|
|
|
616
631
|
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
617
632
|
}
|
|
618
633
|
}
|
|
634
|
+
function isForegroundCanonToolCall(params) {
|
|
635
|
+
const rawTool = readString(params, 'tool');
|
|
636
|
+
if (!rawTool)
|
|
637
|
+
return false;
|
|
638
|
+
const tool = rawTool.startsWith('codex_app.')
|
|
639
|
+
? rawTool.slice('codex_app.'.length)
|
|
640
|
+
: rawTool;
|
|
641
|
+
return tool === 'canon_runtime_control' || tool === 'no_reply';
|
|
642
|
+
}
|
|
619
643
|
function parseJson(line) {
|
|
620
644
|
try {
|
|
621
645
|
const parsed = JSON.parse(line);
|
|
@@ -43,6 +43,7 @@ type DynamicToolCallResponse = {
|
|
|
43
43
|
* the host's turn, not to the app-server bridge.
|
|
44
44
|
*/
|
|
45
45
|
export declare const CODEX_NO_REPLY_TOOL_NAME = "no_reply";
|
|
46
|
+
export declare const CANON_RUNTIME_CONTROL_TOOL_NAME = "canon_runtime_control";
|
|
46
47
|
/**
|
|
47
48
|
* What the model actually sees for the tool above, for prompt text that names
|
|
48
49
|
* it (the group posture cue). Only meaningful on the app-server transport —
|
|
@@ -50,10 +51,28 @@ export declare const CODEX_NO_REPLY_TOOL_NAME = "no_reply";
|
|
|
50
51
|
*/
|
|
51
52
|
export declare const CODEX_NO_REPLY_MODEL_TOOL_NAME = "codex_app.no_reply";
|
|
52
53
|
export declare const CODEX_APP_DYNAMIC_TOOLS: ReadonlyArray<DynamicToolSpec>;
|
|
54
|
+
/** The complete model-visible surface for non-coding service agents. */
|
|
55
|
+
export declare const CODEX_SERVICE_AGENT_DYNAMIC_TOOLS: ReadonlyArray<DynamicToolSpec>;
|
|
56
|
+
/**
|
|
57
|
+
* Tools exposed to detached Codex threads created through the bridge. Canon
|
|
58
|
+
* conversation controls belong only to the foreground turn that received the
|
|
59
|
+
* human message; a child thread has neither that turn nor its responder.
|
|
60
|
+
*/
|
|
61
|
+
export declare const CODEX_DETACHED_THREAD_DYNAMIC_TOOLS: ReadonlyArray<DynamicToolSpec>;
|
|
53
62
|
export declare function isCodexAppToolCall(params: Record<string, unknown>): boolean;
|
|
54
63
|
export declare function deniedCodexAppToolResult(reason: string): DynamicToolCallResponse;
|
|
64
|
+
export declare function successfulCodexAppToolResult(payload: unknown): DynamicToolCallResponse;
|
|
55
65
|
/** True when this dynamic-tool call is the deliberate-silence verb. */
|
|
56
66
|
export declare function isCodexNoReplyToolCall(params: CodexAppToolCallParams): boolean;
|
|
67
|
+
export declare function isCanonRuntimeControlToolCall(params: CodexAppToolCallParams): boolean;
|
|
68
|
+
export declare function isCodexServiceAgentToolCall(params: CodexAppToolCallParams): boolean;
|
|
69
|
+
export interface CanonRuntimeControlRequest {
|
|
70
|
+
action: 'send_card' | 'request_card' | 'request_input';
|
|
71
|
+
card?: unknown;
|
|
72
|
+
prompt?: string;
|
|
73
|
+
placeholder?: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function parseCanonRuntimeControlRequest(params: CodexAppToolCallParams): CanonRuntimeControlRequest | null;
|
|
57
76
|
/** Private rationale, when the model supplied one. Logged, never rendered. */
|
|
58
77
|
export declare function readCodexNoReplyReason(params: CodexAppToolCallParams): string | undefined;
|
|
59
78
|
/**
|
package/dist/codex-app-tools.js
CHANGED
|
@@ -89,12 +89,51 @@ function tool(name, description, inputSchema, deferLoading = true) {
|
|
|
89
89
|
* the host's turn, not to the app-server bridge.
|
|
90
90
|
*/
|
|
91
91
|
export const CODEX_NO_REPLY_TOOL_NAME = 'no_reply';
|
|
92
|
+
export const CANON_RUNTIME_CONTROL_TOOL_NAME = 'canon_runtime_control';
|
|
92
93
|
/**
|
|
93
94
|
* What the model actually sees for the tool above, for prompt text that names
|
|
94
95
|
* it (the group posture cue). Only meaningful on the app-server transport —
|
|
95
96
|
* the `exec --json` transport registers no dynamic tools.
|
|
96
97
|
*/
|
|
97
98
|
export const CODEX_NO_REPLY_MODEL_TOOL_NAME = `codex_app.${CODEX_NO_REPLY_TOOL_NAME}`;
|
|
99
|
+
const CANON_RUNTIME_CONTROL_TOOL = tool(CANON_RUNTIME_CONTROL_TOOL_NAME, 'Interact with the current Canon conversation. Send a display card, request one card response, '
|
|
100
|
+
+ 'or ask one short question. Canon binds the current conversation and authenticated responder; '
|
|
101
|
+
+ 'never ask for or invent routing ids.', {
|
|
102
|
+
type: 'object',
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties: {
|
|
105
|
+
action: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
enum: ['send_card', 'request_card', 'request_input'],
|
|
108
|
+
},
|
|
109
|
+
card: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
description: 'A canon.card.v1 document. Use request_card when it contains actions.',
|
|
112
|
+
},
|
|
113
|
+
prompt: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'A short question for request_input.',
|
|
116
|
+
},
|
|
117
|
+
placeholder: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'Optional example or formatting hint for request_input.',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ['action'],
|
|
123
|
+
}, false);
|
|
124
|
+
const CODEX_NO_REPLY_TOOL = tool(CODEX_NO_REPLY_TOOL_NAME, 'End your turn without posting anything to the conversation. Use it in group '
|
|
125
|
+
+ 'chats when you have nothing to add — no message is created, so no other '
|
|
126
|
+
+ 'member or agent is triggered. Optional private reason (logged, never '
|
|
127
|
+
+ 'shown). After calling this, produce no further text.', {
|
|
128
|
+
type: 'object',
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
reason: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'Never rendered; logged only.',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}, false);
|
|
98
137
|
export const CODEX_APP_DYNAMIC_TOOLS = [
|
|
99
138
|
tool('automation_update', 'Create, update, view, or delete Codex app automations. Canon exposes the name for compatibility, but does not manage Desktop automations.', {
|
|
100
139
|
type: 'object',
|
|
@@ -205,24 +244,23 @@ export const CODEX_APP_DYNAMIC_TOOLS = [
|
|
|
205
244
|
},
|
|
206
245
|
required: ['threadId', 'title'],
|
|
207
246
|
}),
|
|
247
|
+
CANON_RUNTIME_CONTROL_TOOL,
|
|
208
248
|
// Canon's `no_reply` verb, projected into the only model-visible tool surface
|
|
209
|
-
// the Codex transport gives us. It is answered by the Canon host itself
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
type: 'object',
|
|
217
|
-
additionalProperties: false,
|
|
218
|
-
properties: {
|
|
219
|
-
reason: {
|
|
220
|
-
type: 'string',
|
|
221
|
-
description: 'Never rendered; logged only.',
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
}, false),
|
|
249
|
+
// the Codex transport gives us. It is answered by the Canon host itself.
|
|
250
|
+
CODEX_NO_REPLY_TOOL,
|
|
251
|
+
];
|
|
252
|
+
/** The complete model-visible surface for non-coding service agents. */
|
|
253
|
+
export const CODEX_SERVICE_AGENT_DYNAMIC_TOOLS = [
|
|
254
|
+
CANON_RUNTIME_CONTROL_TOOL,
|
|
255
|
+
CODEX_NO_REPLY_TOOL,
|
|
225
256
|
];
|
|
257
|
+
/**
|
|
258
|
+
* Tools exposed to detached Codex threads created through the bridge. Canon
|
|
259
|
+
* conversation controls belong only to the foreground turn that received the
|
|
260
|
+
* human message; a child thread has neither that turn nor its responder.
|
|
261
|
+
*/
|
|
262
|
+
export const CODEX_DETACHED_THREAD_DYNAMIC_TOOLS = CODEX_APP_DYNAMIC_TOOLS.filter((entry) => (entry.name !== CANON_RUNTIME_CONTROL_TOOL_NAME
|
|
263
|
+
&& entry.name !== CODEX_NO_REPLY_TOOL_NAME));
|
|
226
264
|
const CODEX_APP_TOOL_NAMES = new Set(CODEX_APP_DYNAMIC_TOOLS.map((entry) => String(entry.name)));
|
|
227
265
|
const UNSUPPORTED_TOOLS = new Map([
|
|
228
266
|
['automation_update', 'Canon does not manage Codex Desktop automations.'],
|
|
@@ -246,10 +284,38 @@ export function isCodexAppToolCall(params) {
|
|
|
246
284
|
export function deniedCodexAppToolResult(reason) {
|
|
247
285
|
return toolResult(false, { error: reason });
|
|
248
286
|
}
|
|
287
|
+
export function successfulCodexAppToolResult(payload) {
|
|
288
|
+
return toolResult(true, payload);
|
|
289
|
+
}
|
|
249
290
|
/** True when this dynamic-tool call is the deliberate-silence verb. */
|
|
250
291
|
export function isCodexNoReplyToolCall(params) {
|
|
251
292
|
return normalizeToolName(params.tool) === CODEX_NO_REPLY_TOOL_NAME;
|
|
252
293
|
}
|
|
294
|
+
export function isCanonRuntimeControlToolCall(params) {
|
|
295
|
+
return normalizeToolName(params.tool) === CANON_RUNTIME_CONTROL_TOOL_NAME;
|
|
296
|
+
}
|
|
297
|
+
export function isCodexServiceAgentToolCall(params) {
|
|
298
|
+
const toolName = normalizeToolName(params.tool);
|
|
299
|
+
return toolName === CANON_RUNTIME_CONTROL_TOOL_NAME || toolName === CODEX_NO_REPLY_TOOL_NAME;
|
|
300
|
+
}
|
|
301
|
+
export function parseCanonRuntimeControlRequest(params) {
|
|
302
|
+
if (!isCanonRuntimeControlToolCall(params))
|
|
303
|
+
return null;
|
|
304
|
+
const args = parseToolArguments(params.arguments);
|
|
305
|
+
const allowedKeys = new Set(['action', 'card', 'prompt', 'placeholder']);
|
|
306
|
+
if (Object.keys(args).some((key) => !allowedKeys.has(key)))
|
|
307
|
+
return null;
|
|
308
|
+
const action = readString(args, 'action');
|
|
309
|
+
if (action !== 'send_card' && action !== 'request_card' && action !== 'request_input') {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
action,
|
|
314
|
+
...(args.card !== undefined ? { card: args.card } : {}),
|
|
315
|
+
...(readString(args, 'prompt') ? { prompt: readString(args, 'prompt') } : {}),
|
|
316
|
+
...(readString(args, 'placeholder') ? { placeholder: readString(args, 'placeholder') } : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
253
319
|
/** Private rationale, when the model supplied one. Logged, never rendered. */
|
|
254
320
|
export function readCodexNoReplyReason(params) {
|
|
255
321
|
const args = parseToolArguments(params.arguments);
|
|
@@ -314,6 +380,12 @@ export async function handleCodexAppToolCall(runtime, params) {
|
|
|
314
380
|
error: 'no_reply is answered by the Canon host, not the app-tool bridge.',
|
|
315
381
|
});
|
|
316
382
|
}
|
|
383
|
+
if (toolName === CANON_RUNTIME_CONTROL_TOOL_NAME) {
|
|
384
|
+
return toolResult(false, {
|
|
385
|
+
tool: toolName,
|
|
386
|
+
error: 'canon_runtime_control is answered by the Canon host, not the app-tool bridge.',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
317
389
|
const unsupportedReason = UNSUPPORTED_TOOLS.get(toolName);
|
|
318
390
|
if (unsupportedReason) {
|
|
319
391
|
return toolResult(false, { tool: toolName, error: unsupportedReason });
|
|
@@ -366,7 +438,7 @@ async function createThread(runtime, args) {
|
|
|
366
438
|
const started = await runtime.adapter.requestAppServer('thread/start', {
|
|
367
439
|
cwd,
|
|
368
440
|
...(readString(args, 'model') ?? runtime.model ? { model: readString(args, 'model') ?? runtime.model } : {}),
|
|
369
|
-
dynamicTools:
|
|
441
|
+
dynamicTools: CODEX_DETACHED_THREAD_DYNAMIC_TOOLS,
|
|
370
442
|
experimentalRawEvents: false,
|
|
371
443
|
persistExtendedHistory: true,
|
|
372
444
|
});
|
package/dist/host.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type TurnArtifactRoutingDecision, type TurnArtifactRoutingMode } from '@canonmsg/coding-agent-host';
|
|
3
|
-
import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type WorkspaceOption, type CanonWorkspaceRootMetadata, type RuntimeStreamingPayload, type TurnLifecycleState, type TurnOutputBlock, type TurnVerbosity, type TurnVerbosityConfig } from '@canonmsg/core';
|
|
3
|
+
import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type PreparedExecutionEnvironment, type WorkspaceOption, type CanonWorkspaceRootMetadata, type RuntimeStreamingPayload, type TurnLifecycleState, type TurnOutputBlock, type TurnVerbosity, type TurnVerbosityConfig } from '@canonmsg/core';
|
|
4
|
+
import { type CodexSandboxMode } from './adapter.js';
|
|
4
5
|
import { type CodexSkillMetadata } from './app-server-adapter.js';
|
|
6
|
+
import { deriveCodexPermissionEnvelope } from './permission-mode.js';
|
|
5
7
|
import { type CodexControlOption } from './model-catalog.js';
|
|
6
8
|
interface HostSessionState {
|
|
7
9
|
lastError?: string;
|
|
@@ -34,6 +36,19 @@ export declare function buildCodexLiveSessionConfig(input: {
|
|
|
34
36
|
permissionMode?: string | undefined;
|
|
35
37
|
model?: string | undefined;
|
|
36
38
|
};
|
|
39
|
+
export declare function buildCodexServiceSnapshotConfig(input: {
|
|
40
|
+
model?: string;
|
|
41
|
+
permissionMode?: string;
|
|
42
|
+
effort?: string;
|
|
43
|
+
workspaceId?: string;
|
|
44
|
+
}): {
|
|
45
|
+
model: string | undefined;
|
|
46
|
+
permissionMode: string | undefined;
|
|
47
|
+
effort: string | undefined;
|
|
48
|
+
workspaceId: string | undefined;
|
|
49
|
+
executionMode: "locked";
|
|
50
|
+
executionBranch: null;
|
|
51
|
+
};
|
|
37
52
|
/** Conservative fallback used only when native app-server discovery is unavailable. */
|
|
38
53
|
export declare const CODEX_EFFORT_OPTIONS: readonly CodexControlOption[];
|
|
39
54
|
export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
|
|
@@ -55,6 +70,7 @@ export declare function buildCodexRuntimeDescriptor(input: {
|
|
|
55
70
|
supportsCompact?: boolean;
|
|
56
71
|
supportsRichCards?: boolean;
|
|
57
72
|
skills?: ReadonlyArray<CodexSkillMetadata>;
|
|
73
|
+
serviceAgentMode?: boolean;
|
|
58
74
|
}): CanonRuntimeDescriptor;
|
|
59
75
|
export declare function buildCodexTurnResponseRouting(input: {
|
|
60
76
|
requestingUserId?: string | null;
|
|
@@ -68,6 +84,45 @@ export declare function getCodexRequestingUserId(message: {
|
|
|
68
84
|
senderId: string;
|
|
69
85
|
senderType?: 'human' | 'ai_agent';
|
|
70
86
|
}): string | null;
|
|
87
|
+
export declare function resolveSessionExecutionMode(config: {
|
|
88
|
+
executionMode?: ExecutionEnvironmentMode;
|
|
89
|
+
} | null | undefined, serviceAgentMode?: boolean): ExecutionEnvironmentMode;
|
|
90
|
+
export declare function resolveCodexSessionConfig<T extends Record<string, unknown>>(config: T | null | undefined, serviceAgentMode?: boolean): T | null;
|
|
91
|
+
export declare function resolveWorkspaceCwd(config: {
|
|
92
|
+
workspaceId?: string;
|
|
93
|
+
retiredWorkspaceConfig?: boolean;
|
|
94
|
+
} | null, serviceAgentMode?: boolean): string;
|
|
95
|
+
interface CodexEffectiveRuntimePolicy {
|
|
96
|
+
model?: string;
|
|
97
|
+
permissionMode?: string;
|
|
98
|
+
sandbox: CodexSandboxMode | null;
|
|
99
|
+
fullAuto: boolean;
|
|
100
|
+
bypassApprovalsAndSandbox: boolean;
|
|
101
|
+
fingerprint: string;
|
|
102
|
+
}
|
|
103
|
+
export declare function resolveCodexEffectiveRuntimePolicy(input: {
|
|
104
|
+
args: Record<string, unknown>;
|
|
105
|
+
config: {
|
|
106
|
+
model?: string;
|
|
107
|
+
permissionMode?: string;
|
|
108
|
+
} | null | undefined;
|
|
109
|
+
permissionEnvelope: ReturnType<typeof deriveCodexPermissionEnvelope>;
|
|
110
|
+
environment: Pick<PreparedExecutionEnvironment, 'baseCwd' | 'mode'>;
|
|
111
|
+
serviceAgentMode?: boolean;
|
|
112
|
+
}): CodexEffectiveRuntimePolicy;
|
|
113
|
+
export declare function resolveCodexPlanCommand(input: {
|
|
114
|
+
content: string;
|
|
115
|
+
requestedPlanMode: boolean;
|
|
116
|
+
useAppServer: boolean;
|
|
117
|
+
serviceAgentMode: boolean;
|
|
118
|
+
}): {
|
|
119
|
+
planMode: boolean;
|
|
120
|
+
content: string;
|
|
121
|
+
};
|
|
122
|
+
export declare function isCodexPlanApprovalReply(metadata: unknown, serviceAgentMode?: boolean): metadata is Record<string, unknown> & {
|
|
123
|
+
type: 'plan_approval_reply';
|
|
124
|
+
decision: string;
|
|
125
|
+
};
|
|
71
126
|
/**
|
|
72
127
|
* `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
|
|
73
128
|
* per-conversation-type default".
|
|
@@ -137,6 +192,7 @@ export declare function planCodexStreamingWrite(input: {
|
|
|
137
192
|
* same decision at its `text_delta` handler.
|
|
138
193
|
*/
|
|
139
194
|
export declare function shouldStopTypingDotsOnStreamedText(turnVerbosity: TurnVerbosity): boolean;
|
|
195
|
+
export declare function selectCodexNativeImagePaths(imagePaths: readonly string[], nativeVisionEnabled: boolean): string[];
|
|
140
196
|
/**
|
|
141
197
|
* Whether this turn may post the media it generated in the workspace.
|
|
142
198
|
*
|
package/dist/host.js
CHANGED
|
@@ -6,10 +6,11 @@ import { dirname } from 'node:path';
|
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
8
8
|
import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting, 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,
|
|
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, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
10
|
+
import { validateCard } from '@canonmsg/rich-cards';
|
|
10
11
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
11
12
|
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
12
|
-
import { CODEX_APP_DYNAMIC_TOOLS, CODEX_NO_REPLY_MODEL_TOOL_NAME, answerCodexNoReply, classifyCodexAppToolRequest, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, readCodexNoReplyReason, } from './codex-app-tools.js';
|
|
13
|
+
import { CODEX_APP_DYNAMIC_TOOLS, CODEX_NO_REPLY_MODEL_TOOL_NAME, CODEX_SERVICE_AGENT_DYNAMIC_TOOLS, answerCodexNoReply, classifyCodexAppToolRequest, deniedCodexAppToolResult, handleCodexAppToolCall, isCanonRuntimeControlToolCall, isCodexAppToolCall, isCodexServiceAgentToolCall, parseCanonRuntimeControlRequest, readCodexNoReplyReason, successfulCodexAppToolResult, } from './codex-app-tools.js';
|
|
13
14
|
import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
|
|
14
15
|
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
15
16
|
import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
|
|
@@ -42,6 +43,8 @@ COMMON FLAGS
|
|
|
42
43
|
How much of a turn's middle readers see.
|
|
43
44
|
Default (auto): verbose in direct chats,
|
|
44
45
|
quiet in groups. Env: CANON_TURN_VERBOSITY
|
|
46
|
+
--service-agent Expose Canon conversation tools, not coding tools
|
|
47
|
+
--no-native-vision Keep attachment paths but omit native image input
|
|
45
48
|
--help, -h Show this help
|
|
46
49
|
--version, -V Show package version
|
|
47
50
|
|
|
@@ -75,6 +78,20 @@ export function buildCodexLiveSessionConfig(input) {
|
|
|
75
78
|
executionBranch: input.executionBranch ?? null,
|
|
76
79
|
};
|
|
77
80
|
}
|
|
81
|
+
export function buildCodexServiceSnapshotConfig(input) {
|
|
82
|
+
// Keep every setup key present, including keys whose host value is
|
|
83
|
+
// undefined. `publishHostSessionSnapshots` merges this over any persisted
|
|
84
|
+
// coding config, so an absent host value clears rather than resurrects a
|
|
85
|
+
// stale member-selected value.
|
|
86
|
+
return {
|
|
87
|
+
model: input.model,
|
|
88
|
+
permissionMode: input.permissionMode,
|
|
89
|
+
effort: input.effort,
|
|
90
|
+
workspaceId: input.workspaceId,
|
|
91
|
+
executionMode: 'locked',
|
|
92
|
+
executionBranch: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
78
95
|
const MAX_SESSIONS = 12;
|
|
79
96
|
// IDLE_TIMEOUT_MS (30 minutes) is shared with the other coding-agent hosts.
|
|
80
97
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -135,6 +152,7 @@ export function buildCodexSkillCommands(skills) {
|
|
|
135
152
|
});
|
|
136
153
|
}
|
|
137
154
|
export function buildCodexRuntimeDescriptor(input) {
|
|
155
|
+
const serviceAgentMode = input.serviceAgentMode === true;
|
|
138
156
|
const commands = [
|
|
139
157
|
{
|
|
140
158
|
id: 'runtime-status',
|
|
@@ -154,7 +172,7 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
154
172
|
RUNTIME_STOP_ACTION,
|
|
155
173
|
RUNTIME_STOP_AND_DROP_ACTION,
|
|
156
174
|
];
|
|
157
|
-
if (input.supportsCompact) {
|
|
175
|
+
if (input.supportsCompact && !serviceAgentMode) {
|
|
158
176
|
commands.push({
|
|
159
177
|
id: 'codex-compact-context',
|
|
160
178
|
label: 'Compact',
|
|
@@ -174,20 +192,22 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
174
192
|
}
|
|
175
193
|
const descriptor = buildFirstPartyCodingRuntimeDescriptor({
|
|
176
194
|
clientType: 'codex',
|
|
177
|
-
models: input.models,
|
|
178
|
-
workspaces: input.workspaces,
|
|
179
|
-
workspaceRoots: input.workspaceRoots,
|
|
180
|
-
executionModes: input.executionModes,
|
|
181
|
-
permissionModes: input.permissionModes,
|
|
182
|
-
defaultPermissionMode: input.defaultPermissionMode,
|
|
195
|
+
models: serviceAgentMode ? [] : input.models,
|
|
196
|
+
workspaces: serviceAgentMode ? [] : input.workspaces,
|
|
197
|
+
workspaceRoots: serviceAgentMode ? undefined : input.workspaceRoots,
|
|
198
|
+
executionModes: serviceAgentMode ? [] : input.executionModes,
|
|
199
|
+
permissionModes: serviceAgentMode ? [] : input.permissionModes,
|
|
200
|
+
defaultPermissionMode: serviceAgentMode ? undefined : input.defaultPermissionMode,
|
|
183
201
|
permissionModeLabel: 'Execution policy',
|
|
184
202
|
modelLiveBehavior: 'next_turn',
|
|
185
|
-
effortOptions:
|
|
186
|
-
|
|
203
|
+
effortOptions: serviceAgentMode
|
|
204
|
+
? []
|
|
205
|
+
: input.effortOptions ?? [...CODEX_EFFORT_OPTIONS],
|
|
206
|
+
defaultEffort: serviceAgentMode ? undefined : input.defaultEffort ?? 'medium',
|
|
187
207
|
effortLiveBehavior: 'next_turn',
|
|
188
208
|
presentation: input.presentation,
|
|
189
209
|
streamingTextMode: 'snapshot',
|
|
190
|
-
...(input.supportsPlanMode
|
|
210
|
+
...(input.supportsPlanMode && !serviceAgentMode
|
|
191
211
|
? {
|
|
192
212
|
turnModes: [
|
|
193
213
|
{
|
|
@@ -217,7 +237,7 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
217
237
|
rich: {
|
|
218
238
|
schema: 'canon.card.v1',
|
|
219
239
|
lifecycle: 'blocking_requires_action',
|
|
220
|
-
responder: 'agent_owner',
|
|
240
|
+
...(serviceAgentMode ? {} : { responder: 'agent_owner' }),
|
|
221
241
|
result: 'action_or_values',
|
|
222
242
|
maxTimeoutMs: 30 * 60_000,
|
|
223
243
|
blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
|
|
@@ -228,6 +248,13 @@ export function buildCodexRuntimeDescriptor(input) {
|
|
|
228
248
|
}
|
|
229
249
|
: {}),
|
|
230
250
|
});
|
|
251
|
+
if (serviceAgentMode) {
|
|
252
|
+
const { runtimeControls: _runtimeControls, ...serviceDescriptor } = descriptor;
|
|
253
|
+
return {
|
|
254
|
+
...serviceDescriptor,
|
|
255
|
+
coreControls: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
231
258
|
if (input.models.length > 0) {
|
|
232
259
|
return descriptor;
|
|
233
260
|
}
|
|
@@ -259,15 +286,20 @@ async function loadSessionConfig(conversationId, agentId, rtdb) {
|
|
|
259
286
|
extraStringFields: CODEX_SESSION_CONFIG_FIELDS,
|
|
260
287
|
});
|
|
261
288
|
}
|
|
262
|
-
function resolveSessionExecutionMode(config) {
|
|
289
|
+
export function resolveSessionExecutionMode(config, serviceAgentMode = false) {
|
|
290
|
+
if (serviceAgentMode)
|
|
291
|
+
return 'locked';
|
|
263
292
|
if (config?.executionMode)
|
|
264
293
|
return config.executionMode;
|
|
265
294
|
throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
|
|
266
295
|
}
|
|
267
|
-
function
|
|
296
|
+
export function resolveCodexSessionConfig(config, serviceAgentMode = false) {
|
|
297
|
+
return serviceAgentMode ? null : config ?? null;
|
|
298
|
+
}
|
|
299
|
+
export function resolveWorkspaceCwd(config, serviceAgentMode = false) {
|
|
268
300
|
return resolveHostWorkspaceCwd({
|
|
269
301
|
workspaceOptions,
|
|
270
|
-
config,
|
|
302
|
+
config: serviceAgentMode ? null : config,
|
|
271
303
|
defaultCwd: workingDir,
|
|
272
304
|
});
|
|
273
305
|
}
|
|
@@ -286,9 +318,11 @@ function stringArg(args, key) {
|
|
|
286
318
|
function boolArg(args, key) {
|
|
287
319
|
return args[key] === true;
|
|
288
320
|
}
|
|
289
|
-
function resolveCodexEffectiveRuntimePolicy(input) {
|
|
321
|
+
export function resolveCodexEffectiveRuntimePolicy(input) {
|
|
290
322
|
const model = input.config?.model ?? stringArg(input.args, 'model');
|
|
291
|
-
const permissionMode = input.
|
|
323
|
+
const permissionMode = input.serviceAgentMode
|
|
324
|
+
? input.permissionEnvelope.defaultPermissionMode
|
|
325
|
+
: input.config?.permissionMode ?? input.permissionEnvelope.defaultPermissionMode;
|
|
292
326
|
if (permissionMode
|
|
293
327
|
&& !input.permissionEnvelope.availablePermissionModes.some((option) => option.value === permissionMode)) {
|
|
294
328
|
throw new ExecutionEnvironmentError(`Permission mode "${permissionMode}" is not supported by this Codex host.`, 'This Canon host was started with stricter approval settings. Choose one of the advertised permission modes or restart the host with more permissive flags.');
|
|
@@ -304,6 +338,9 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
304
338
|
baseCwd: input.environment.baseCwd,
|
|
305
339
|
executionMode: input.environment.mode,
|
|
306
340
|
permissionMode: permissionMode ?? null,
|
|
341
|
+
...(input.serviceAgentMode
|
|
342
|
+
? { serviceAgentMode: true, model: model ?? null }
|
|
343
|
+
: {}),
|
|
307
344
|
sandbox,
|
|
308
345
|
// `--ask-for-approval` is rejected at startup, so this was always null.
|
|
309
346
|
// The key must stay: it is hashed into every persisted thread fingerprint.
|
|
@@ -393,6 +430,14 @@ function parsePlanCommand(content) {
|
|
|
393
430
|
content: rest || 'Please inspect the request and propose a plan before making changes.',
|
|
394
431
|
};
|
|
395
432
|
}
|
|
433
|
+
export function resolveCodexPlanCommand(input) {
|
|
434
|
+
if (!input.useAppServer || input.serviceAgentMode) {
|
|
435
|
+
return { planMode: false, content: input.content };
|
|
436
|
+
}
|
|
437
|
+
return input.requestedPlanMode
|
|
438
|
+
? { planMode: true, content: input.content }
|
|
439
|
+
: parsePlanCommand(input.content);
|
|
440
|
+
}
|
|
396
441
|
function mapCodexQuestions(value) {
|
|
397
442
|
if (!Array.isArray(value))
|
|
398
443
|
return undefined;
|
|
@@ -446,6 +491,12 @@ function mapCodexQuestions(value) {
|
|
|
446
491
|
function isRecord(value) {
|
|
447
492
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
448
493
|
}
|
|
494
|
+
export function isCodexPlanApprovalReply(metadata, serviceAgentMode = false) {
|
|
495
|
+
return !serviceAgentMode
|
|
496
|
+
&& isRecord(metadata)
|
|
497
|
+
&& metadata.type === 'plan_approval_reply'
|
|
498
|
+
&& typeof metadata.decision === 'string';
|
|
499
|
+
}
|
|
449
500
|
function readString(record, key) {
|
|
450
501
|
const value = record[key];
|
|
451
502
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
@@ -538,6 +589,9 @@ export function planCodexStreamingWrite(input) {
|
|
|
538
589
|
export function shouldStopTypingDotsOnStreamedText(turnVerbosity) {
|
|
539
590
|
return turnVerbosity !== 'quiet';
|
|
540
591
|
}
|
|
592
|
+
export function selectCodexNativeImagePaths(imagePaths, nativeVisionEnabled) {
|
|
593
|
+
return nativeVisionEnabled ? [...imagePaths] : [];
|
|
594
|
+
}
|
|
541
595
|
/**
|
|
542
596
|
* Whether this turn may post the media it generated in the workspace.
|
|
543
597
|
*
|
|
@@ -585,6 +639,8 @@ export async function main() {
|
|
|
585
639
|
'show-runtime-detail': { type: 'string', multiple: true },
|
|
586
640
|
'hide-runtime-detail': { type: 'string', multiple: true },
|
|
587
641
|
'turn-verbosity': { type: 'string' },
|
|
642
|
+
'service-agent': { type: 'boolean' },
|
|
643
|
+
'no-native-vision': { type: 'boolean' },
|
|
588
644
|
'full-auto': { type: 'boolean' },
|
|
589
645
|
'dangerously-bypass-approvals-and-sandbox': { type: 'boolean' },
|
|
590
646
|
},
|
|
@@ -600,6 +656,11 @@ export async function main() {
|
|
|
600
656
|
env: process.env.CANON_TURN_VERBOSITY,
|
|
601
657
|
onWarning: (message) => console.error(`[canon-codex] ${message}`),
|
|
602
658
|
});
|
|
659
|
+
const serviceAgentMode = args['service-agent'] === true;
|
|
660
|
+
const nativeVisionEnabled = args['no-native-vision'] !== true;
|
|
661
|
+
const codexDynamicTools = serviceAgentMode
|
|
662
|
+
? CODEX_SERVICE_AGENT_DYNAMIC_TOOLS
|
|
663
|
+
: CODEX_APP_DYNAMIC_TOOLS;
|
|
603
664
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
604
665
|
const workspaceDiscovery = buildConfiguredWorkspaceOptionsWithRoots({
|
|
605
666
|
primaryCwd: workingDir,
|
|
@@ -1116,6 +1177,8 @@ export async function main() {
|
|
|
1116
1177
|
if (!session)
|
|
1117
1178
|
return;
|
|
1118
1179
|
session.closed = true;
|
|
1180
|
+
session.currentTurnAbortController?.abort(new Error('Codex session closed'));
|
|
1181
|
+
session.currentTurnAbortController = null;
|
|
1119
1182
|
stopVisibleWorkSignal(session);
|
|
1120
1183
|
if ('close' in session.adapter && typeof session.adapter.close === 'function') {
|
|
1121
1184
|
session.adapter.close();
|
|
@@ -1139,6 +1202,7 @@ export async function main() {
|
|
|
1139
1202
|
session.activeSelfContextId = null;
|
|
1140
1203
|
session.state.lastError = undefined;
|
|
1141
1204
|
if (session.running) {
|
|
1205
|
+
session.currentTurnAbortController?.abort(new Error('Codex session reset'));
|
|
1142
1206
|
await session.adapter.interrupt();
|
|
1143
1207
|
session.turnState = 'interrupted';
|
|
1144
1208
|
}
|
|
@@ -1185,9 +1249,9 @@ export async function main() {
|
|
|
1185
1249
|
evictOldestIdle();
|
|
1186
1250
|
}
|
|
1187
1251
|
const creation = (async () => {
|
|
1188
|
-
const config = await loadSessionConfig(conversationId, agentId, rtdb);
|
|
1189
|
-
const sessionExecutionMode = resolveSessionExecutionMode(config);
|
|
1190
|
-
const workspaceCwd = resolveWorkspaceCwd(config);
|
|
1252
|
+
const config = resolveCodexSessionConfig(await loadSessionConfig(conversationId, agentId, rtdb), serviceAgentMode);
|
|
1253
|
+
const sessionExecutionMode = resolveSessionExecutionMode(config, serviceAgentMode);
|
|
1254
|
+
const workspaceCwd = resolveWorkspaceCwd(config, serviceAgentMode);
|
|
1191
1255
|
const environment = prepareConversationEnvironment({
|
|
1192
1256
|
agentId,
|
|
1193
1257
|
conversationId,
|
|
@@ -1201,6 +1265,7 @@ export async function main() {
|
|
|
1201
1265
|
config,
|
|
1202
1266
|
permissionEnvelope: codexPermissionEnvelope,
|
|
1203
1267
|
environment,
|
|
1268
|
+
serviceAgentMode,
|
|
1204
1269
|
});
|
|
1205
1270
|
const modelGuard = buildCodexModelGuardMessage(policy.model, codexCliStatus);
|
|
1206
1271
|
if (modelGuard) {
|
|
@@ -1226,7 +1291,7 @@ export async function main() {
|
|
|
1226
1291
|
configOverrides: args.config ?? [],
|
|
1227
1292
|
fullAuto: policy.fullAuto,
|
|
1228
1293
|
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
1229
|
-
dynamicTools:
|
|
1294
|
+
dynamicTools: codexDynamicTools,
|
|
1230
1295
|
})
|
|
1231
1296
|
: new CodexConversationAdapter({
|
|
1232
1297
|
cwd: sessionCwd,
|
|
@@ -1259,6 +1324,7 @@ export async function main() {
|
|
|
1259
1324
|
currentTurnOpenedAt: null,
|
|
1260
1325
|
currentTurnUpdatedAt: null,
|
|
1261
1326
|
currentTurnCanUseCodexAppTools: false,
|
|
1327
|
+
currentTurnAbortController: null,
|
|
1262
1328
|
// Corrected by the first turn that runs; a session with no turn
|
|
1263
1329
|
// publishes nothing anyway, and quiet is never an accident.
|
|
1264
1330
|
turnVerbosity: 'verbose',
|
|
@@ -1333,7 +1399,10 @@ export async function main() {
|
|
|
1333
1399
|
// No `turnVerbosity`: this prompt continues the same conversation, so it
|
|
1334
1400
|
// keeps whatever the session last resolved rather than reverting to the
|
|
1335
1401
|
// default.
|
|
1336
|
-
enqueuePrompt(session, prompt, 'queue', false, result.receiptId ?? null, false, [], [], result.status !== 'approve', {
|
|
1402
|
+
enqueuePrompt(session, prompt, 'queue', false, result.receiptId ?? null, false, [], [], result.status !== 'approve', {
|
|
1403
|
+
canUseCodexAppTools: serviceAgentMode || responseUserId === ownerId,
|
|
1404
|
+
requestingUserId: responseUserId,
|
|
1405
|
+
});
|
|
1337
1406
|
}
|
|
1338
1407
|
function resolveArtifactRoutingMode(participantContext) {
|
|
1339
1408
|
return participantContext.conversationType === 'direct' && participantContext.isOwner
|
|
@@ -1349,7 +1418,7 @@ export async function main() {
|
|
|
1349
1418
|
function resolveCodexTurnModes(participantContext, message) {
|
|
1350
1419
|
return {
|
|
1351
1420
|
artifactRoutingMode: resolveArtifactRoutingMode(participantContext),
|
|
1352
|
-
canUseCodexAppTools: participantContext.isOwner,
|
|
1421
|
+
canUseCodexAppTools: participantContext.isOwner || serviceAgentMode,
|
|
1353
1422
|
turnVerbosity: resolveTurnVerbosity({
|
|
1354
1423
|
configured: configuredTurnVerbosity,
|
|
1355
1424
|
conversationType: participantContext.conversationType,
|
|
@@ -1371,6 +1440,9 @@ export async function main() {
|
|
|
1371
1440
|
const params = request.params;
|
|
1372
1441
|
const expiresAt = Date.now() + 30 * 60_000;
|
|
1373
1442
|
if (request.method === 'item/tool/call' && isCodexAppToolCall(params)) {
|
|
1443
|
+
if (serviceAgentMode && !isCodexServiceAgentToolCall(params)) {
|
|
1444
|
+
return deniedCodexAppToolResult('This service agent exposes only Canon conversation tools.');
|
|
1445
|
+
}
|
|
1374
1446
|
// The admission order — no_reply above the owner gate — is pinned by
|
|
1375
1447
|
// `classifyCodexAppToolRequest`'s tests, not by the shape of this block.
|
|
1376
1448
|
const disposition = classifyCodexAppToolRequest({
|
|
@@ -1393,6 +1465,102 @@ export async function main() {
|
|
|
1393
1465
|
if (disposition === 'denied-non-owner') {
|
|
1394
1466
|
return deniedCodexAppToolResult('Only the Canon owner can use codex_app tools.');
|
|
1395
1467
|
}
|
|
1468
|
+
if (isCanonRuntimeControlToolCall(params)) {
|
|
1469
|
+
const command = parseCanonRuntimeControlRequest(params);
|
|
1470
|
+
if (!command) {
|
|
1471
|
+
return deniedCodexAppToolResult('Invalid canon_runtime_control arguments.');
|
|
1472
|
+
}
|
|
1473
|
+
if (command.action === 'request_input') {
|
|
1474
|
+
if (!command.prompt) {
|
|
1475
|
+
return deniedCodexAppToolResult('request_input requires prompt.');
|
|
1476
|
+
}
|
|
1477
|
+
const responseRouting = buildCodexTurnResponseRouting({ requestingUserId, ownerId });
|
|
1478
|
+
const inputId = `codex_input_${randomUUID()}`;
|
|
1479
|
+
const question = command.placeholder
|
|
1480
|
+
? `${command.prompt}\n\nFormat hint: ${command.placeholder}`
|
|
1481
|
+
: command.prompt;
|
|
1482
|
+
const response = await runtimeRequests.request('input', session.conversationId, {
|
|
1483
|
+
kind: 'clarify',
|
|
1484
|
+
title: 'Input requested',
|
|
1485
|
+
prompt: command.prompt,
|
|
1486
|
+
questions: [{ id: 'response', header: 'Response', question, allowOther: true }],
|
|
1487
|
+
...(responseRouting.responseUserId
|
|
1488
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1489
|
+
: {}),
|
|
1490
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1491
|
+
}, {
|
|
1492
|
+
requestId: inputId,
|
|
1493
|
+
expiresAt,
|
|
1494
|
+
signal: session.currentTurnAbortController?.signal,
|
|
1495
|
+
onCreated: () => {
|
|
1496
|
+
session.turnState = 'waiting_input';
|
|
1497
|
+
markTurnProgress(session);
|
|
1498
|
+
stopVisibleWorkSignal(session);
|
|
1499
|
+
writeTurn(session);
|
|
1500
|
+
writeCodexStreaming(session, null, 'waiting_input');
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
resumeTurnFromWaiting(session);
|
|
1504
|
+
return successfulCodexAppToolResult(response);
|
|
1505
|
+
}
|
|
1506
|
+
const validation = validateCard(command.card);
|
|
1507
|
+
if (!validation.ok || !validation.card) {
|
|
1508
|
+
return deniedCodexAppToolResult(`Invalid canon.card.v1 card: ${validation.errors.join('; ')}`);
|
|
1509
|
+
}
|
|
1510
|
+
const card = validation.card;
|
|
1511
|
+
const interactive = card.blocks.some((block) => block.kind === 'actions');
|
|
1512
|
+
if (command.action === 'send_card') {
|
|
1513
|
+
if (interactive) {
|
|
1514
|
+
return deniedCodexAppToolResult('send_card cannot contain actions; use request_card.');
|
|
1515
|
+
}
|
|
1516
|
+
const cardId = card.cardId ?? `codex_card_${randomUUID()}`;
|
|
1517
|
+
const created = await client.createRuntimeCardRequest({
|
|
1518
|
+
conversationId: session.conversationId,
|
|
1519
|
+
card: { ...card, cardId },
|
|
1520
|
+
cardId,
|
|
1521
|
+
expiresAt,
|
|
1522
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1523
|
+
});
|
|
1524
|
+
return successfulCodexAppToolResult({
|
|
1525
|
+
status: 'sent',
|
|
1526
|
+
cardId: created.cardId,
|
|
1527
|
+
messageId: created.messageId,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
if (!interactive) {
|
|
1531
|
+
return deniedCodexAppToolResult('request_card requires an actions block; use send_card.');
|
|
1532
|
+
}
|
|
1533
|
+
const cardId = card.cardId ?? `codex_card_${randomUUID()}`;
|
|
1534
|
+
const responseRouting = buildCodexTurnResponseRouting({ requestingUserId, ownerId });
|
|
1535
|
+
const response = await runtimeRequests.request('card', session.conversationId, {
|
|
1536
|
+
card,
|
|
1537
|
+
turnId: session.currentTurnId ?? undefined,
|
|
1538
|
+
...(responseRouting.responseUserId
|
|
1539
|
+
? { responseUserId: responseRouting.responseUserId }
|
|
1540
|
+
: {}),
|
|
1541
|
+
}, {
|
|
1542
|
+
requestId: cardId,
|
|
1543
|
+
expiresAt,
|
|
1544
|
+
signal: session.currentTurnAbortController?.signal,
|
|
1545
|
+
onCreated: () => {
|
|
1546
|
+
session.turnState = 'waiting_input';
|
|
1547
|
+
markTurnProgress(session);
|
|
1548
|
+
upsertTurnBlock(session, {
|
|
1549
|
+
id: `card:${cardId}`,
|
|
1550
|
+
kind: 'input',
|
|
1551
|
+
status: 'pending',
|
|
1552
|
+
title: card.title,
|
|
1553
|
+
summary: card.template ?? 'runtime card',
|
|
1554
|
+
});
|
|
1555
|
+
writeTurn(session);
|
|
1556
|
+
stopVisibleWorkSignal(session);
|
|
1557
|
+
writeCodexStreaming(session, null, 'waiting_input');
|
|
1558
|
+
},
|
|
1559
|
+
});
|
|
1560
|
+
completeTurnBlock(session, `card:${cardId}`, `Card ${response.status}`);
|
|
1561
|
+
resumeTurnFromWaiting(session);
|
|
1562
|
+
return successfulCodexAppToolResult(response);
|
|
1563
|
+
}
|
|
1396
1564
|
// `disposition` already ruled on the transport (it is checked first, so a
|
|
1397
1565
|
// wrong transport never reaches the branches above). This repeats the
|
|
1398
1566
|
// test only to narrow the adapter type for the bridge call.
|
|
@@ -1410,9 +1578,13 @@ export async function main() {
|
|
|
1410
1578
|
}
|
|
1411
1579
|
const runtimeCardPayload = runtimeCardRequestPayload(request.method, params);
|
|
1412
1580
|
if (runtimeCardPayload) {
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1581
|
+
const validation = validateCard(runtimeCardPayload);
|
|
1582
|
+
const card = validation.card;
|
|
1583
|
+
if (!validation.ok || !card) {
|
|
1584
|
+
return {
|
|
1585
|
+
status: 'cancelled',
|
|
1586
|
+
error: `Invalid canon.card.v1 card: ${validation.errors.join('; ')}`,
|
|
1587
|
+
};
|
|
1416
1588
|
}
|
|
1417
1589
|
const cardId = readString(params, 'cardId')
|
|
1418
1590
|
?? readString(params, 'itemId')
|
|
@@ -1443,6 +1615,7 @@ export async function main() {
|
|
|
1443
1615
|
}, {
|
|
1444
1616
|
requestId: cardId,
|
|
1445
1617
|
expiresAt,
|
|
1618
|
+
signal: session.currentTurnAbortController?.signal,
|
|
1446
1619
|
onCreated: () => {
|
|
1447
1620
|
requestCreated = true;
|
|
1448
1621
|
session.turnState = 'waiting_input';
|
|
@@ -1464,8 +1637,14 @@ export async function main() {
|
|
|
1464
1637
|
status: 'submitted',
|
|
1465
1638
|
...(cardResult.actionId ? { actionId: cardResult.actionId } : {}),
|
|
1466
1639
|
...(cardResult.values ? { values: cardResult.values } : {}),
|
|
1640
|
+
...(cardResult.respondedBy ? { respondedBy: cardResult.respondedBy } : {}),
|
|
1467
1641
|
}
|
|
1468
|
-
: {
|
|
1642
|
+
: {
|
|
1643
|
+
status: cardResult.status,
|
|
1644
|
+
...('respondedBy' in cardResult && cardResult.respondedBy
|
|
1645
|
+
? { respondedBy: cardResult.respondedBy }
|
|
1646
|
+
: {}),
|
|
1647
|
+
};
|
|
1469
1648
|
requestResolved = true;
|
|
1470
1649
|
const outcome = buildRuntimeCardOutcome(cardId, response.status, { reason: response.status });
|
|
1471
1650
|
await sendMessageWithRetry(client, session.conversationId, outcome.text, {
|
|
@@ -1535,7 +1714,11 @@ export async function main() {
|
|
|
1535
1714
|
},
|
|
1536
1715
|
},
|
|
1537
1716
|
turnId: session.currentTurnId ?? undefined,
|
|
1538
|
-
}, {
|
|
1717
|
+
}, {
|
|
1718
|
+
requestId: inputId,
|
|
1719
|
+
expiresAt,
|
|
1720
|
+
signal: session.currentTurnAbortController?.signal,
|
|
1721
|
+
});
|
|
1539
1722
|
resumeTurnFromWaiting(session);
|
|
1540
1723
|
return { answers: response.status === 'submitted' ? response.answers ?? {} : {} };
|
|
1541
1724
|
}
|
|
@@ -1554,7 +1737,11 @@ export async function main() {
|
|
|
1554
1737
|
? { responseUserId: responseRouting.responseUserId }
|
|
1555
1738
|
: {}),
|
|
1556
1739
|
allowSessionRule: responseRouting.allowSessionRule,
|
|
1557
|
-
}, {
|
|
1740
|
+
}, {
|
|
1741
|
+
requestId: approvalId,
|
|
1742
|
+
expiresAt,
|
|
1743
|
+
signal: session.currentTurnAbortController?.signal,
|
|
1744
|
+
});
|
|
1558
1745
|
resumeTurnFromWaiting(session);
|
|
1559
1746
|
if (request.method === 'item/permissions/requestApproval') {
|
|
1560
1747
|
return response.decision === 'allow'
|
|
@@ -1575,9 +1762,14 @@ export async function main() {
|
|
|
1575
1762
|
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1576
1763
|
return;
|
|
1577
1764
|
}
|
|
1578
|
-
if (
|
|
1579
|
-
|
|
1580
|
-
|
|
1765
|
+
if (input.message.metadata?.type === 'plan_approval_reply') {
|
|
1766
|
+
if (!isCodexPlanApprovalReply(input.message.metadata, serviceAgentMode)) {
|
|
1767
|
+
// A service agent never advertises or enters plan mode. Consume stale
|
|
1768
|
+
// replies left by an older coding descriptor instead of turning them
|
|
1769
|
+
// into hidden plan/implementation prompts after an upgrade or restart.
|
|
1770
|
+
persistInboundRecoveryCursorWhenIdle(input.conversationId, input.message.id);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1581
1773
|
const planId = readString(input.message.metadata, 'planId');
|
|
1582
1774
|
if (planId && runtimeRequests.handleMessage(input.conversationId, {
|
|
1583
1775
|
senderId: input.message.senderId,
|
|
@@ -1595,7 +1787,7 @@ export async function main() {
|
|
|
1595
1787
|
? `The plan was declined — keep planning and wait for guidance before implementing.${feedback ? `\n\nNotes:\n${feedback}` : ''}`
|
|
1596
1788
|
: `Please revise the plan.${feedback ? `\n\nRevision feedback:\n${feedback}` : ''}`;
|
|
1597
1789
|
enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve', {
|
|
1598
|
-
canUseCodexAppTools: input.isOwner,
|
|
1790
|
+
canUseCodexAppTools: input.isOwner || serviceAgentMode,
|
|
1599
1791
|
requestingUserId: getCodexRequestingUserId(input.message),
|
|
1600
1792
|
});
|
|
1601
1793
|
return;
|
|
@@ -1615,11 +1807,12 @@ export async function main() {
|
|
|
1615
1807
|
const renderedContent = renderInboundContent(input.message, materialized);
|
|
1616
1808
|
const turnMetadata = normalizeTurnMetadata(input.message.metadata);
|
|
1617
1809
|
const requestedPlanMode = turnMetadata?.requestedTurnMode === 'plan';
|
|
1618
|
-
const planCommand =
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1810
|
+
const planCommand = resolveCodexPlanCommand({
|
|
1811
|
+
content: renderedContent,
|
|
1812
|
+
requestedPlanMode,
|
|
1813
|
+
useAppServer,
|
|
1814
|
+
serviceAgentMode,
|
|
1815
|
+
});
|
|
1623
1816
|
const content = planCommand.content;
|
|
1624
1817
|
const hydrated = await loadHydratedInboundContext({
|
|
1625
1818
|
conversationId: input.conversationId,
|
|
@@ -1642,9 +1835,10 @@ export async function main() {
|
|
|
1642
1835
|
});
|
|
1643
1836
|
const replyContext = replyMedia.replyContext;
|
|
1644
1837
|
const promptMaterialized = [...replyMedia.materialized, ...materialized];
|
|
1645
|
-
const
|
|
1838
|
+
const discoveredImagePaths = promptMaterialized
|
|
1646
1839
|
.map((attachment) => getCodexImagePath(attachment))
|
|
1647
1840
|
.filter((path) => path !== null);
|
|
1841
|
+
const imagePaths = selectCodexNativeImagePaths(discoveredImagePaths, nativeVisionEnabled);
|
|
1648
1842
|
const mediaAddDirs = uniqueStrings(promptMaterialized.map((attachment) => dirname(attachment.path)));
|
|
1649
1843
|
const participantContext = hydrated.participantContext;
|
|
1650
1844
|
const autoReply = input.turnDispatch?.kind === 'run_turn'
|
|
@@ -1698,6 +1892,7 @@ export async function main() {
|
|
|
1698
1892
|
if (session.running && deliveryIntent === 'interrupt') {
|
|
1699
1893
|
enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, resolveCodexTurnModes(participantContext, input.message));
|
|
1700
1894
|
console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
|
|
1895
|
+
session.currentTurnAbortController?.abort(new Error('Codex turn interrupted by a newer message'));
|
|
1701
1896
|
await session.adapter.interrupt().catch(() => { });
|
|
1702
1897
|
clearStreaming(input.conversationId);
|
|
1703
1898
|
typingSignals.clear(input.conversationId).catch(() => { });
|
|
@@ -1734,6 +1929,7 @@ export async function main() {
|
|
|
1734
1929
|
session.currentTurnOpenedAt = Date.now();
|
|
1735
1930
|
session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
|
|
1736
1931
|
session.currentTurnCanUseCodexAppTools = nextTurn.canUseCodexAppTools === true;
|
|
1932
|
+
session.currentTurnAbortController = new AbortController();
|
|
1737
1933
|
// A continuation prompt (a plan-review result) carries none, and keeps the
|
|
1738
1934
|
// conversation's last answer rather than silently reverting to verbose.
|
|
1739
1935
|
session.turnVerbosity = nextTurn.turnVerbosity ?? session.turnVerbosity;
|
|
@@ -2135,6 +2331,8 @@ export async function main() {
|
|
|
2135
2331
|
console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
|
|
2136
2332
|
}
|
|
2137
2333
|
finally {
|
|
2334
|
+
session.currentTurnAbortController?.abort(new Error('Codex turn ended'));
|
|
2335
|
+
session.currentTurnAbortController = null;
|
|
2138
2336
|
persistInboundRecoveryCursor(session.conversationId, nextTurn.sourceMessageId);
|
|
2139
2337
|
if (session.pendingDroppedRecoveryCursor) {
|
|
2140
2338
|
persistInboundRecoveryCursor(session.conversationId, session.pendingDroppedRecoveryCursor);
|
|
@@ -2184,10 +2382,10 @@ export async function main() {
|
|
|
2184
2382
|
}
|
|
2185
2383
|
}
|
|
2186
2384
|
let streamConnected = false;
|
|
2187
|
-
const hostAvailableExecutionModes =
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
|
|
2385
|
+
const hostAvailableExecutionModes = serviceAgentMode
|
|
2386
|
+
? ['locked']
|
|
2387
|
+
: [...EXECUTION_ENVIRONMENT_MODES];
|
|
2388
|
+
const codexPermissionEnvelope = deriveCodexPermissionEnvelope(serviceAgentMode ? { sandbox: 'read-only' } : args);
|
|
2191
2389
|
const configuredCodexEffort = readCodexConfiguredEffort(args.config ?? []);
|
|
2192
2390
|
let codexModels = [];
|
|
2193
2391
|
let codexModelOptions = buildCodexModelOptions(codexModels, args.model);
|
|
@@ -2206,14 +2404,18 @@ export async function main() {
|
|
|
2206
2404
|
});
|
|
2207
2405
|
let codexSkills = [];
|
|
2208
2406
|
const buildCurrentRuntimeDescriptor = () => ({
|
|
2209
|
-
|
|
2407
|
+
...(serviceAgentMode
|
|
2408
|
+
? {}
|
|
2409
|
+
: {
|
|
2410
|
+
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2411
|
+
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2412
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
2413
|
+
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
2414
|
+
...(codexPermissionEnvelope.defaultPermissionMode
|
|
2415
|
+
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
2416
|
+
: {}),
|
|
2417
|
+
}),
|
|
2210
2418
|
...(codexDefaultModel ? { defaultModel: codexDefaultModel } : {}),
|
|
2211
|
-
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2212
|
-
availableExecutionModes: hostAvailableExecutionModes,
|
|
2213
|
-
availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
2214
|
-
...(codexPermissionEnvelope.defaultPermissionMode
|
|
2215
|
-
? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
|
|
2216
|
-
: {}),
|
|
2217
2419
|
runtimeDescriptor: buildCodexRuntimeDescriptor({
|
|
2218
2420
|
models: codexModelOptions,
|
|
2219
2421
|
effortOptions: codexEffortOptions,
|
|
@@ -2228,6 +2430,7 @@ export async function main() {
|
|
|
2228
2430
|
supportsCompact: useAppServer,
|
|
2229
2431
|
supportsRichCards: useAppServer,
|
|
2230
2432
|
skills: codexSkills,
|
|
2433
|
+
serviceAgentMode,
|
|
2231
2434
|
}),
|
|
2232
2435
|
});
|
|
2233
2436
|
let runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
@@ -2273,6 +2476,11 @@ export async function main() {
|
|
|
2273
2476
|
const session = sessions.get(conversationId);
|
|
2274
2477
|
if (!session || session.closed)
|
|
2275
2478
|
return;
|
|
2479
|
+
if (serviceAgentMode) {
|
|
2480
|
+
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring user session controls for service-agent runtime`);
|
|
2481
|
+
writeState(session);
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2276
2484
|
let modelChanged = false;
|
|
2277
2485
|
if (control.model && control.model !== session.state.model) {
|
|
2278
2486
|
if (codexModelOptions.length > 0 && !codexModelOptions.some((option) => option.value === control.model)) {
|
|
@@ -2345,6 +2553,7 @@ export async function main() {
|
|
|
2345
2553
|
rememberDroppedRecoveryCursor(session, droppedPrompts);
|
|
2346
2554
|
}
|
|
2347
2555
|
if (session.running) {
|
|
2556
|
+
session.currentTurnAbortController?.abort(new Error(`Codex turn interrupted by ${type}`));
|
|
2348
2557
|
await session.adapter.interrupt();
|
|
2349
2558
|
}
|
|
2350
2559
|
session.turnState = 'interrupted';
|
|
@@ -2428,10 +2637,27 @@ export async function main() {
|
|
|
2428
2637
|
workspaceOptions,
|
|
2429
2638
|
defaultCwd: workingDir,
|
|
2430
2639
|
extraSessionConfigFields: CODEX_SESSION_CONFIG_FIELDS,
|
|
2431
|
-
liveSessionConfigByConversation: new Map(Array.from(sessions.
|
|
2640
|
+
liveSessionConfigByConversation: new Map(Array.from(serviceAgentMode ? knownConversationIds : sessions.keys()).map((conversationId) => {
|
|
2641
|
+
const session = sessions.get(conversationId);
|
|
2642
|
+
if (serviceAgentMode) {
|
|
2643
|
+
return [
|
|
2644
|
+
conversationId,
|
|
2645
|
+
buildCodexServiceSnapshotConfig({
|
|
2646
|
+
model: codexDefaultModel ?? session?.state.model ?? undefined,
|
|
2647
|
+
permissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
2648
|
+
effort: codexDefaultEffort ?? session?.state.effort ?? undefined,
|
|
2649
|
+
workspaceId: resolveWorkspaceIdForBaseCwd(workingDir) ?? undefined,
|
|
2650
|
+
}),
|
|
2651
|
+
];
|
|
2652
|
+
}
|
|
2653
|
+
if (!session) {
|
|
2654
|
+
// Non-service snapshots are published only for live sessions,
|
|
2655
|
+
// but keep this branch total if that invariant changes.
|
|
2656
|
+
return [conversationId, {}];
|
|
2657
|
+
}
|
|
2432
2658
|
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
2433
2659
|
return [
|
|
2434
|
-
|
|
2660
|
+
conversationId,
|
|
2435
2661
|
buildCodexLiveSessionConfig({
|
|
2436
2662
|
model: session.state.model,
|
|
2437
2663
|
permissionMode: session.state.permissionMode,
|
|
@@ -2600,7 +2826,7 @@ export async function main() {
|
|
|
2600
2826
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovery cursor was not found within ${STARTUP_RECOVERY_MAX_MESSAGES} messages; replaying the bounded recent window`);
|
|
2601
2827
|
}
|
|
2602
2828
|
for (const message of recovered.messages) {
|
|
2603
|
-
const isPlanReply = message.metadata
|
|
2829
|
+
const isPlanReply = isCodexPlanApprovalReply(message.metadata, serviceAgentMode);
|
|
2604
2830
|
if (!isPlanReply && !shouldTriggerAgentTurn({
|
|
2605
2831
|
senderType: message.senderType,
|
|
2606
2832
|
metadata: message.metadata,
|
|
@@ -2666,6 +2892,9 @@ export async function main() {
|
|
|
2666
2892
|
controlPoller.stop();
|
|
2667
2893
|
clearInterval(heartbeat);
|
|
2668
2894
|
clearInterval(idleCheck);
|
|
2895
|
+
for (const session of sessions.values()) {
|
|
2896
|
+
session.currentTurnAbortController?.abort(new Error('Codex host shutting down'));
|
|
2897
|
+
}
|
|
2669
2898
|
runtimeRequests.dispose();
|
|
2670
2899
|
stream.stop();
|
|
2671
2900
|
await runtimeState.clearAgentRuntime().catch(() => { });
|
package/dist/session-store.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function buildCodexThreadPolicyFingerprint(input: {
|
|
|
3
3
|
baseCwd: string;
|
|
4
4
|
executionMode?: ExecutionEnvironmentMode;
|
|
5
5
|
permissionMode?: string | null;
|
|
6
|
+
model?: string | null;
|
|
7
|
+
serviceAgentMode?: boolean;
|
|
6
8
|
sandbox?: string | null;
|
|
7
9
|
approvalPolicy?: string | null;
|
|
8
10
|
fullAuto?: boolean;
|
package/dist/session-store.js
CHANGED
|
@@ -6,6 +6,14 @@ export function buildCodexThreadPolicyFingerprint(input) {
|
|
|
6
6
|
baseCwd: input.baseCwd,
|
|
7
7
|
executionMode: input.executionMode ?? null,
|
|
8
8
|
permissionMode: input.permissionMode ?? null,
|
|
9
|
+
// Service agents make the model a host-owned runtime choice. Include it,
|
|
10
|
+
// plus an explicit service-mode marker, so the first service-agent
|
|
11
|
+
// release drops coding threads that may retain a member-selected model
|
|
12
|
+
// and later host model changes start a clean thread. Ordinary Codex host
|
|
13
|
+
// fingerprints intentionally remain byte-for-byte compatible.
|
|
14
|
+
...(input.serviceAgentMode
|
|
15
|
+
? { serviceAgentMode: true, model: input.model ?? null }
|
|
16
|
+
: {}),
|
|
9
17
|
sandbox: input.sandbox ?? null,
|
|
10
18
|
approvalPolicy: input.approvalPolicy ?? null,
|
|
11
19
|
fullAuto: input.fullAuto === true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.2",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
|
|
24
|
+
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host ../rich-cards",
|
|
25
25
|
"build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
26
26
|
"dev": "npm run prepare:workspace-deps && tsc --watch",
|
|
27
27
|
"smoke": "node scripts/smoke-test.mjs",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@canonmsg/agent-sdk": "^8.3.0",
|
|
33
33
|
"@canonmsg/coding-agent-host": "^0.5.0",
|
|
34
|
-
"@canonmsg/core": "^10.
|
|
34
|
+
"@canonmsg/core": "^10.3.1",
|
|
35
|
+
"@canonmsg/rich-cards": "^0.10.0"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": ">=18.0.0"
|