@canonmsg/codex-plugin 0.18.10 → 0.18.12

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.
@@ -1,4 +1,5 @@
1
1
  import type { CodexApprovalPolicy, CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
2
+ export type JsonRecord = Record<string, unknown>;
2
3
  export interface CodexSkillMetadata {
3
4
  name: string;
4
5
  path: string;
@@ -18,6 +19,7 @@ export declare class CodexAppServerAdapter {
18
19
  private readonly configOverrides;
19
20
  private readonly fullAuto;
20
21
  private readonly bypassApprovalsAndSandbox;
22
+ private readonly dynamicTools;
21
23
  private child;
22
24
  private threadId;
23
25
  private loadedThreadId;
@@ -49,6 +51,7 @@ export declare class CodexAppServerAdapter {
49
51
  configOverrides?: string[];
50
52
  fullAuto?: boolean;
51
53
  bypassApprovalsAndSandbox?: boolean;
54
+ dynamicTools?: readonly JsonRecord[];
52
55
  });
53
56
  getThreadId(): string | null;
54
57
  clearThreadId(): void;
@@ -64,6 +67,8 @@ export declare class CodexAppServerAdapter {
64
67
  setReasoningEffort(effort: string | null): void;
65
68
  isRunning(): boolean;
66
69
  interrupt(): Promise<void>;
70
+ compactThread(): Promise<void>;
71
+ requestAppServer(method: string, params: unknown): Promise<unknown>;
67
72
  close(): void;
68
73
  runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[], _extraAddDirs?: readonly string[], options?: CodexRunTurnOptions): Promise<CodexTurnResult>;
69
74
  private resolveApprovalPolicy;
@@ -79,6 +84,7 @@ export declare class CodexAppServerAdapter {
79
84
  private handleLine;
80
85
  private handleServerRequest;
81
86
  private handleNotification;
87
+ private isCurrentThreadNotification;
82
88
  private resolveCurrentTurn;
83
89
  private clearActiveTurn;
84
90
  private sendRequest;
@@ -11,6 +11,7 @@ export class CodexAppServerAdapter {
11
11
  configOverrides;
12
12
  fullAuto;
13
13
  bypassApprovalsAndSandbox;
14
+ dynamicTools;
14
15
  child = null;
15
16
  threadId;
16
17
  loadedThreadId = null;
@@ -42,6 +43,7 @@ export class CodexAppServerAdapter {
42
43
  this.configOverrides = opts.configOverrides ?? [];
43
44
  this.fullAuto = opts.fullAuto ?? false;
44
45
  this.bypassApprovalsAndSandbox = opts.bypassApprovalsAndSandbox ?? false;
46
+ this.dynamicTools = opts.dynamicTools ?? [];
45
47
  }
46
48
  getThreadId() {
47
49
  return this.threadId;
@@ -83,6 +85,19 @@ export class CodexAppServerAdapter {
83
85
  turnId: this.currentTurnId,
84
86
  }).catch(() => { });
85
87
  }
88
+ async compactThread() {
89
+ if (!this.threadId) {
90
+ throw new Error('No Codex thread to compact');
91
+ }
92
+ await this.ensureStarted();
93
+ await this.sendRequest('thread/compact/start', {
94
+ threadId: this.threadId,
95
+ });
96
+ }
97
+ async requestAppServer(method, params) {
98
+ await this.ensureStarted();
99
+ return await this.sendRequest(method, params);
100
+ }
86
101
  close() {
87
102
  this.child?.kill('SIGTERM');
88
103
  this.child = null;
@@ -113,6 +128,7 @@ export class CodexAppServerAdapter {
113
128
  approvalPolicy: this.resolveApprovalPolicy(),
114
129
  excludeTurns: true,
115
130
  persistExtendedHistory: true,
131
+ ...(this.dynamicTools.length ? { dynamicTools: this.dynamicTools } : {}),
116
132
  });
117
133
  this.loadedThreadId = this.threadId;
118
134
  this.rememberResolvedModel(resumed);
@@ -124,6 +140,7 @@ export class CodexAppServerAdapter {
124
140
  ...(this.sandbox ? { sandbox: this.sandbox } : {}),
125
141
  ...this.configPayload(),
126
142
  approvalPolicy: this.resolveApprovalPolicy(),
143
+ ...(this.dynamicTools.length ? { dynamicTools: this.dynamicTools } : {}),
127
144
  experimentalRawEvents: false,
128
145
  persistExtendedHistory: true,
129
146
  });
@@ -349,16 +366,18 @@ export class CodexAppServerAdapter {
349
366
  }
350
367
  }
351
368
  handleNotification(method, params) {
352
- if (method === 'turn/started') {
353
- this.currentTurnId = readString(params.turn, 'id') ?? this.currentTurnId;
354
- this.currentOnEvent?.({ type: 'turn.started' });
355
- return;
356
- }
357
369
  if (method === 'skills/changed') {
358
370
  this.skillsCache = null;
359
371
  this.currentOnEvent?.({ type: 'skills.changed' });
360
372
  return;
361
373
  }
374
+ if (!this.isCurrentThreadNotification(params))
375
+ return;
376
+ if (method === 'turn/started') {
377
+ this.currentTurnId = readString(params.turn, 'id') ?? this.currentTurnId;
378
+ this.currentOnEvent?.({ type: 'turn.started' });
379
+ return;
380
+ }
362
381
  if (method === 'thread/status/changed') {
363
382
  const status = params.status;
364
383
  if (status?.type === 'active' && Array.isArray(status.activeFlags)) {
@@ -467,6 +486,15 @@ export class CodexAppServerAdapter {
467
486
  this.currentErrorText = stringifyPreview(params);
468
487
  }
469
488
  }
489
+ isCurrentThreadNotification(params) {
490
+ const threadId = readNotificationThreadId(params);
491
+ if (threadId && this.threadId && threadId !== this.threadId)
492
+ return false;
493
+ const turnId = readNotificationTurnId(params);
494
+ if (turnId && this.currentTurnId && turnId !== this.currentTurnId)
495
+ return false;
496
+ return true;
497
+ }
470
498
  resolveCurrentTurn() {
471
499
  const result = {
472
500
  threadId: this.threadId,
@@ -529,6 +557,18 @@ function readRawString(record, key) {
529
557
  const value = record?.[key];
530
558
  return typeof value === 'string' ? value : undefined;
531
559
  }
560
+ function readNotificationThreadId(params) {
561
+ return readString(params, 'threadId')
562
+ ?? readString(params.thread, 'id')
563
+ ?? readString(params.turn, 'threadId')
564
+ ?? readString(params.item, 'threadId')
565
+ ?? readString(params.status, 'threadId');
566
+ }
567
+ function readNotificationTurnId(params) {
568
+ return readString(params, 'turnId')
569
+ ?? readString(params.turn, 'id')
570
+ ?? readString(params.item, 'turnId');
571
+ }
532
572
  function readNullableString(record, key) {
533
573
  const value = record?.[key];
534
574
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
@@ -0,0 +1,42 @@
1
+ type JsonRecord = Record<string, unknown>;
2
+ interface DynamicToolSpec {
3
+ [key: string]: unknown;
4
+ namespace: 'codex_app';
5
+ name: string;
6
+ description: string;
7
+ inputSchema: JsonRecord;
8
+ deferLoading?: boolean;
9
+ }
10
+ export interface CodexAppToolAdapter {
11
+ requestAppServer(method: string, params: unknown): Promise<unknown>;
12
+ }
13
+ export interface CodexAppToolWorkspace {
14
+ id: string;
15
+ label: string;
16
+ cwd: string;
17
+ }
18
+ export interface CodexAppToolRuntime {
19
+ adapter: CodexAppToolAdapter;
20
+ currentThreadId: string | null;
21
+ currentCwd: string;
22
+ workspaces: ReadonlyArray<CodexAppToolWorkspace>;
23
+ model?: string | null;
24
+ effort?: string | null;
25
+ }
26
+ export interface CodexAppToolCallParams {
27
+ arguments?: unknown;
28
+ namespace?: unknown;
29
+ tool?: unknown;
30
+ }
31
+ type DynamicToolCallResponse = {
32
+ success: boolean;
33
+ contentItems: Array<{
34
+ type: 'inputText';
35
+ text: string;
36
+ }>;
37
+ };
38
+ export declare const CODEX_APP_DYNAMIC_TOOLS: ReadonlyArray<DynamicToolSpec>;
39
+ export declare function isCodexAppToolCall(params: Record<string, unknown>): boolean;
40
+ export declare function deniedCodexAppToolResult(reason: string): DynamicToolCallResponse;
41
+ export declare function handleCodexAppToolCall(runtime: CodexAppToolRuntime, params: CodexAppToolCallParams): Promise<DynamicToolCallResponse>;
42
+ export {};
@@ -0,0 +1,519 @@
1
+ const emptyObjectSchema = {
2
+ type: 'object',
3
+ properties: {},
4
+ additionalProperties: false,
5
+ };
6
+ const modelProperties = {
7
+ model: {
8
+ type: 'string',
9
+ description: 'Optional model override. Omit unless the user explicitly asks for a specific model.',
10
+ },
11
+ thinking: {
12
+ type: 'string',
13
+ description: 'Optional reasoning effort override.',
14
+ enum: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
15
+ },
16
+ };
17
+ const createThreadTargetSchema = {
18
+ description: 'Where to create the thread. Canon currently supports local project targets only.',
19
+ anyOf: [
20
+ {
21
+ type: 'object',
22
+ additionalProperties: false,
23
+ properties: {
24
+ type: { type: 'string', enum: ['project'] },
25
+ projectId: { type: 'string', description: 'Canon workspace id.' },
26
+ environment: {
27
+ anyOf: [
28
+ {
29
+ type: 'object',
30
+ additionalProperties: false,
31
+ properties: {
32
+ type: { type: 'string', enum: ['local'] },
33
+ },
34
+ required: ['type'],
35
+ },
36
+ {
37
+ type: 'object',
38
+ additionalProperties: true,
39
+ properties: {
40
+ type: { type: 'string', enum: ['worktree'] },
41
+ },
42
+ required: ['type'],
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ required: ['type', 'projectId', 'environment'],
48
+ },
49
+ {
50
+ type: 'object',
51
+ additionalProperties: false,
52
+ properties: {
53
+ type: { type: 'string', enum: ['projectless'] },
54
+ directoryName: { type: 'string' },
55
+ },
56
+ required: ['type'],
57
+ },
58
+ ],
59
+ };
60
+ const forkEnvironmentSchema = {
61
+ description: 'Where the fork should run. Canon currently supports same-directory forks only.',
62
+ anyOf: [
63
+ {
64
+ type: 'object',
65
+ additionalProperties: false,
66
+ properties: {
67
+ type: { type: 'string', enum: ['same-directory'] },
68
+ },
69
+ required: ['type'],
70
+ },
71
+ {
72
+ type: 'object',
73
+ additionalProperties: true,
74
+ properties: {
75
+ type: { type: 'string', enum: ['worktree'] },
76
+ },
77
+ required: ['type'],
78
+ },
79
+ ],
80
+ };
81
+ function tool(name, description, inputSchema, deferLoading = true) {
82
+ return { namespace: 'codex_app', name, description, inputSchema, deferLoading };
83
+ }
84
+ export const CODEX_APP_DYNAMIC_TOOLS = [
85
+ tool('automation_update', 'Create, update, view, or delete Codex app automations. Canon exposes the name for compatibility, but does not manage Desktop automations.', {
86
+ type: 'object',
87
+ additionalProperties: false,
88
+ properties: {
89
+ id: { type: 'string' },
90
+ mode: { type: 'string' },
91
+ kind: { type: 'string' },
92
+ name: { type: 'string' },
93
+ prompt: { type: 'string' },
94
+ rrule: { type: 'string' },
95
+ cwds: {
96
+ anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
97
+ },
98
+ destination: { type: 'string' },
99
+ executionEnvironment: { type: 'string' },
100
+ localEnvironmentConfigPath: { type: ['string', 'null'] },
101
+ model: { type: 'string' },
102
+ reasoningEffort: { type: 'string' },
103
+ targetThreadId: { type: 'string' },
104
+ status: { type: 'string' },
105
+ },
106
+ }),
107
+ tool('navigate_to_codex_page', 'Navigate the Codex Desktop UI. Canon exposes the name for compatibility, but has no Codex Desktop page to navigate.', { type: 'object', additionalProperties: true, properties: {} }),
108
+ tool('read_thread_terminal', 'Read the Codex Desktop terminal output for this thread. Canon exposes the name for compatibility, but has no Desktop terminal pane.', emptyObjectSchema, false),
109
+ tool('load_workspace_dependencies', 'Locate bundled Desktop workspace dependency runtimes. Canon exposes the name for compatibility, but does not provide Desktop bundle paths.', emptyObjectSchema, false),
110
+ tool('fork_thread', 'Fork a Codex thread. Omit threadId to fork the calling thread. Canon supports same-directory forks.', {
111
+ type: 'object',
112
+ additionalProperties: false,
113
+ properties: {
114
+ threadId: { type: 'string' },
115
+ environment: forkEnvironmentSchema,
116
+ },
117
+ }),
118
+ tool('handoff_thread', 'Move a Codex thread between a checkout and worktree. Canon exposes the name for compatibility, but does not manage Desktop handoffs.', {
119
+ type: 'object',
120
+ additionalProperties: false,
121
+ properties: { threadId: { type: 'string' } },
122
+ required: ['threadId'],
123
+ }),
124
+ tool('get_handoff_status', 'Read Codex Desktop handoff status. Canon exposes the name for compatibility, but does not manage Desktop handoffs.', {
125
+ type: 'object',
126
+ additionalProperties: false,
127
+ properties: { threadId: { type: 'string' } },
128
+ }),
129
+ tool('list_projects', 'List Canon workspaces available to Codex app tools.', emptyObjectSchema),
130
+ tool('create_thread', 'Create a separate Codex thread only when the user explicitly asks for a new or separate thread. Canon supports local project targets.', {
131
+ type: 'object',
132
+ additionalProperties: false,
133
+ properties: {
134
+ prompt: { type: 'string', description: 'Initial prompt for the new thread.' },
135
+ target: createThreadTargetSchema,
136
+ ...modelProperties,
137
+ },
138
+ required: ['prompt'],
139
+ }),
140
+ tool('list_threads', 'List recent Codex threads in Canon-configured workspaces.', {
141
+ type: 'object',
142
+ additionalProperties: false,
143
+ properties: {
144
+ query: { type: 'string' },
145
+ limit: { type: 'number' },
146
+ archived: { type: 'boolean' },
147
+ },
148
+ }),
149
+ tool('read_thread', 'Read recent status and turn summaries for one Codex thread without opening it.', {
150
+ type: 'object',
151
+ additionalProperties: false,
152
+ properties: {
153
+ threadId: { type: 'string' },
154
+ },
155
+ required: ['threadId'],
156
+ }),
157
+ tool('send_message_to_thread', 'Send a follow-up prompt to an existing Codex thread in the background.', {
158
+ type: 'object',
159
+ additionalProperties: false,
160
+ properties: {
161
+ threadId: { type: 'string' },
162
+ prompt: { type: 'string' },
163
+ ...modelProperties,
164
+ },
165
+ required: ['threadId', 'prompt'],
166
+ }),
167
+ tool('set_thread_pinned', 'Pin or unpin a Codex thread. Canon exposes the name for compatibility, but pinned state is Desktop-only.', {
168
+ type: 'object',
169
+ additionalProperties: false,
170
+ properties: {
171
+ threadId: { type: 'string' },
172
+ pinned: { type: 'boolean' },
173
+ },
174
+ required: ['threadId', 'pinned'],
175
+ }),
176
+ tool('set_thread_archived', 'Archive or unarchive a Codex thread.', {
177
+ type: 'object',
178
+ additionalProperties: false,
179
+ properties: {
180
+ threadId: { type: 'string' },
181
+ archived: { type: 'boolean' },
182
+ },
183
+ required: ['archived'],
184
+ }),
185
+ tool('set_thread_title', 'Rename a Codex thread.', {
186
+ type: 'object',
187
+ additionalProperties: false,
188
+ properties: {
189
+ threadId: { type: 'string' },
190
+ title: { type: 'string' },
191
+ },
192
+ required: ['threadId', 'title'],
193
+ }),
194
+ ];
195
+ const CODEX_APP_TOOL_NAMES = new Set(CODEX_APP_DYNAMIC_TOOLS.map((entry) => String(entry.name)));
196
+ const UNSUPPORTED_TOOLS = new Map([
197
+ ['automation_update', 'Canon does not manage Codex Desktop automations.'],
198
+ ['navigate_to_codex_page', 'Canon has no Codex Desktop page to navigate.'],
199
+ ['read_thread_terminal', 'Canon has no Codex Desktop terminal pane to read.'],
200
+ ['load_workspace_dependencies', 'Canon does not provide Codex Desktop bundled dependency paths.'],
201
+ ['handoff_thread', 'Canon does not manage Codex Desktop handoffs.'],
202
+ ['get_handoff_status', 'Canon does not manage Codex Desktop handoffs.'],
203
+ ['set_thread_pinned', 'Pinned thread state is Codex Desktop-only.'],
204
+ ]);
205
+ export function isCodexAppToolCall(params) {
206
+ const namespace = typeof params.namespace === 'string' ? params.namespace : null;
207
+ const rawTool = typeof params.tool === 'string' ? params.tool.trim() : '';
208
+ const toolName = normalizeToolName(params.tool);
209
+ if (namespace && namespace !== 'codex_app')
210
+ return rawTool.startsWith('codex_app.');
211
+ return namespace === 'codex_app'
212
+ || rawTool.startsWith('codex_app.')
213
+ || (toolName ? CODEX_APP_TOOL_NAMES.has(toolName) : false);
214
+ }
215
+ export function deniedCodexAppToolResult(reason) {
216
+ return toolResult(false, { error: reason });
217
+ }
218
+ export async function handleCodexAppToolCall(runtime, params) {
219
+ const toolName = normalizeToolName(params.tool);
220
+ if (!toolName || !CODEX_APP_TOOL_NAMES.has(toolName)) {
221
+ return toolResult(false, { error: `Unsupported codex_app tool: ${String(params.tool ?? 'unknown')}` });
222
+ }
223
+ const unsupportedReason = UNSUPPORTED_TOOLS.get(toolName);
224
+ if (unsupportedReason) {
225
+ return toolResult(false, { tool: toolName, error: unsupportedReason });
226
+ }
227
+ const args = parseToolArguments(params.arguments);
228
+ try {
229
+ switch (toolName) {
230
+ case 'list_projects':
231
+ return toolResult(true, {
232
+ projects: listToolWorkspaces(runtime).map((workspace) => ({
233
+ id: workspace.id,
234
+ label: workspace.label,
235
+ cwd: workspace.cwd,
236
+ environments: ['local'],
237
+ })),
238
+ unsupported: {
239
+ worktreeEnvironments: 'Canon host app-server tools only create local project threads.',
240
+ },
241
+ });
242
+ case 'create_thread':
243
+ return await createThread(runtime, args);
244
+ case 'fork_thread':
245
+ return await forkThread(runtime, args);
246
+ case 'list_threads':
247
+ return await listThreads(runtime, args);
248
+ case 'read_thread':
249
+ return await readThread(runtime, args);
250
+ case 'send_message_to_thread':
251
+ return await sendMessageToThread(runtime, args);
252
+ case 'set_thread_archived':
253
+ return await setThreadArchived(runtime, args);
254
+ case 'set_thread_title':
255
+ return await setThreadTitle(runtime, args);
256
+ default:
257
+ return toolResult(false, { tool: toolName, error: 'Tool is registered but has no Canon handler.' });
258
+ }
259
+ }
260
+ catch (error) {
261
+ return toolResult(false, {
262
+ tool: toolName,
263
+ error: error instanceof Error ? error.message : String(error),
264
+ });
265
+ }
266
+ }
267
+ async function createThread(runtime, args) {
268
+ const prompt = readString(args, 'prompt');
269
+ if (!prompt)
270
+ throw new Error('create_thread requires a non-empty prompt.');
271
+ const cwd = resolveCreateThreadCwd(runtime, args);
272
+ const started = await runtime.adapter.requestAppServer('thread/start', {
273
+ cwd,
274
+ ...(readString(args, 'model') ?? runtime.model ? { model: readString(args, 'model') ?? runtime.model } : {}),
275
+ dynamicTools: CODEX_APP_DYNAMIC_TOOLS,
276
+ experimentalRawEvents: false,
277
+ persistExtendedHistory: true,
278
+ });
279
+ const threadId = readThreadId(started);
280
+ if (!threadId)
281
+ throw new Error('Codex app-server did not return a thread id for the new thread.');
282
+ const turnStarted = await runtime.adapter.requestAppServer('turn/start', buildTurnStartParams({
283
+ threadId,
284
+ prompt,
285
+ model: readString(args, 'model') ?? null,
286
+ effort: readString(args, 'thinking') ?? readString(args, 'effort') ?? runtime.effort ?? null,
287
+ }));
288
+ return toolResult(true, {
289
+ threadId,
290
+ turnId: readTurnId(turnStarted) ?? null,
291
+ status: 'submitted',
292
+ cwd,
293
+ });
294
+ }
295
+ async function forkThread(runtime, args) {
296
+ const environment = isRecord(args.environment) ? args.environment : null;
297
+ const environmentType = readString(environment, 'type');
298
+ if (environmentType && environmentType !== 'same-directory') {
299
+ throw new Error('Canon only supports same-directory codex_app.fork_thread calls.');
300
+ }
301
+ const sourceThreadId = readString(args, 'threadId') ?? runtime.currentThreadId;
302
+ if (!sourceThreadId)
303
+ throw new Error('fork_thread requires a source thread id.');
304
+ const source = await assertThreadAllowed(runtime, sourceThreadId);
305
+ const forked = await runtime.adapter.requestAppServer('thread/fork', {
306
+ threadId: sourceThreadId,
307
+ cwd: source.cwd ?? runtime.currentCwd,
308
+ excludeTurns: true,
309
+ persistExtendedHistory: true,
310
+ });
311
+ const threadId = readThreadId(forked);
312
+ if (!threadId)
313
+ throw new Error('Codex app-server did not return a forked thread id.');
314
+ return toolResult(true, {
315
+ threadId,
316
+ sourceThreadId,
317
+ status: 'forked',
318
+ cwd: source.cwd ?? runtime.currentCwd,
319
+ });
320
+ }
321
+ async function listThreads(runtime, args) {
322
+ const limit = clampLimit(readNumber(args, 'limit'));
323
+ const result = await runtime.adapter.requestAppServer('thread/list', {
324
+ cwd: allowedCwds(runtime),
325
+ limit,
326
+ searchTerm: readString(args, 'query') ?? readString(args, 'searchTerm') ?? null,
327
+ archived: typeof args.archived === 'boolean' ? args.archived : false,
328
+ });
329
+ return toolResult(true, result);
330
+ }
331
+ async function readThread(runtime, args) {
332
+ const threadId = readString(args, 'threadId');
333
+ if (!threadId)
334
+ throw new Error('read_thread requires threadId.');
335
+ await assertThreadAllowed(runtime, threadId);
336
+ const result = await runtime.adapter.requestAppServer('thread/read', {
337
+ threadId,
338
+ includeTurns: true,
339
+ });
340
+ return toolResult(true, result);
341
+ }
342
+ async function sendMessageToThread(runtime, args) {
343
+ const threadId = readString(args, 'threadId');
344
+ const prompt = readString(args, 'prompt');
345
+ if (!threadId)
346
+ throw new Error('send_message_to_thread requires threadId.');
347
+ if (!prompt)
348
+ throw new Error('send_message_to_thread requires a non-empty prompt.');
349
+ await assertThreadAllowed(runtime, threadId);
350
+ const result = await runtime.adapter.requestAppServer('turn/start', buildTurnStartParams({
351
+ threadId,
352
+ prompt,
353
+ model: readString(args, 'model') ?? null,
354
+ effort: readString(args, 'thinking') ?? readString(args, 'effort') ?? null,
355
+ }));
356
+ return toolResult(true, {
357
+ threadId,
358
+ turnId: readTurnId(result) ?? null,
359
+ status: 'submitted',
360
+ });
361
+ }
362
+ async function setThreadArchived(runtime, args) {
363
+ const threadId = readString(args, 'threadId') ?? runtime.currentThreadId;
364
+ if (!threadId)
365
+ throw new Error('set_thread_archived requires threadId outside the calling thread.');
366
+ if (typeof args.archived !== 'boolean')
367
+ throw new Error('set_thread_archived requires archived boolean.');
368
+ await assertThreadAllowed(runtime, threadId);
369
+ const result = await runtime.adapter.requestAppServer(args.archived ? 'thread/archive' : 'thread/unarchive', { threadId });
370
+ return toolResult(true, { threadId, archived: args.archived, result });
371
+ }
372
+ async function setThreadTitle(runtime, args) {
373
+ const threadId = readString(args, 'threadId');
374
+ const title = readString(args, 'title');
375
+ if (!threadId)
376
+ throw new Error('set_thread_title requires threadId.');
377
+ if (!title)
378
+ throw new Error('set_thread_title requires a non-empty title.');
379
+ await assertThreadAllowed(runtime, threadId);
380
+ const result = await runtime.adapter.requestAppServer('thread/name/set', {
381
+ threadId,
382
+ name: title,
383
+ });
384
+ return toolResult(true, { threadId, title, result });
385
+ }
386
+ function resolveCreateThreadCwd(runtime, args) {
387
+ const target = isRecord(args.target) ? args.target : null;
388
+ const environment = isRecord(target?.environment) ? target.environment : null;
389
+ const environmentType = readString(environment, 'type');
390
+ if (environmentType === 'worktree') {
391
+ throw new Error('Canon does not create codex_app threads in new worktrees yet.');
392
+ }
393
+ if (readString(target, 'type') === 'projectless') {
394
+ throw new Error('Canon does not support projectless codex_app threads yet.');
395
+ }
396
+ const projectId = readString(target, 'projectId')
397
+ ?? readString(args, 'projectId')
398
+ ?? readString(args, 'workspaceId');
399
+ if (!projectId)
400
+ return runtime.currentCwd;
401
+ const workspace = listToolWorkspaces(runtime).find((entry) => entry.id === projectId);
402
+ if (!workspace)
403
+ throw new Error(`Unknown Canon workspace/project id: ${projectId}`);
404
+ return workspace.cwd;
405
+ }
406
+ async function assertThreadAllowed(runtime, threadId) {
407
+ if (threadId === runtime.currentThreadId) {
408
+ return { cwd: runtime.currentCwd, raw: null };
409
+ }
410
+ const result = await runtime.adapter.requestAppServer('thread/read', {
411
+ threadId,
412
+ includeTurns: false,
413
+ });
414
+ const cwd = readThreadCwd(result);
415
+ if (!cwd || !allowedCwdSet(runtime).has(cwd)) {
416
+ throw new Error(`Thread ${threadId} is not in a Canon-configured workspace.`);
417
+ }
418
+ return { cwd, raw: result };
419
+ }
420
+ function buildTurnStartParams(input) {
421
+ return {
422
+ threadId: input.threadId,
423
+ input: [{ type: 'text', text: input.prompt, text_elements: [] }],
424
+ ...(input.model ? { model: input.model } : {}),
425
+ ...(input.effort ? { effort: input.effort } : {}),
426
+ };
427
+ }
428
+ function listToolWorkspaces(runtime) {
429
+ const seen = new Set();
430
+ const workspaces = [];
431
+ for (const workspace of runtime.workspaces) {
432
+ if (!workspace.cwd || seen.has(workspace.cwd))
433
+ continue;
434
+ seen.add(workspace.cwd);
435
+ workspaces.push(workspace);
436
+ }
437
+ if (runtime.currentCwd && !seen.has(runtime.currentCwd)) {
438
+ workspaces.unshift({
439
+ id: 'current',
440
+ label: 'Current session',
441
+ cwd: runtime.currentCwd,
442
+ });
443
+ }
444
+ return workspaces;
445
+ }
446
+ function allowedCwds(runtime) {
447
+ return listToolWorkspaces(runtime).map((workspace) => workspace.cwd);
448
+ }
449
+ function allowedCwdSet(runtime) {
450
+ return new Set(allowedCwds(runtime));
451
+ }
452
+ function clampLimit(value) {
453
+ if (!value || !Number.isFinite(value))
454
+ return 20;
455
+ return Math.max(1, Math.min(100, Math.floor(value)));
456
+ }
457
+ function parseToolArguments(value) {
458
+ if (isRecord(value))
459
+ return value;
460
+ if (typeof value === 'string' && value.trim()) {
461
+ try {
462
+ const parsed = JSON.parse(value);
463
+ return isRecord(parsed) ? parsed : {};
464
+ }
465
+ catch {
466
+ return {};
467
+ }
468
+ }
469
+ return {};
470
+ }
471
+ function normalizeToolName(value) {
472
+ if (typeof value !== 'string' || !value.trim())
473
+ return null;
474
+ const trimmed = value.trim();
475
+ return trimmed.startsWith('codex_app.') ? trimmed.slice('codex_app.'.length) : trimmed;
476
+ }
477
+ function readThreadId(value) {
478
+ if (!isRecord(value))
479
+ return undefined;
480
+ return readString(value, 'threadId')
481
+ ?? readString(value, 'id')
482
+ ?? readString(isRecord(value.thread) ? value.thread : undefined, 'id');
483
+ }
484
+ function readTurnId(value) {
485
+ if (!isRecord(value))
486
+ return undefined;
487
+ return readString(value, 'turnId')
488
+ ?? readString(value, 'id')
489
+ ?? readString(isRecord(value.turn) ? value.turn : undefined, 'id');
490
+ }
491
+ function readThreadCwd(value) {
492
+ if (!isRecord(value))
493
+ return null;
494
+ const thread = isRecord(value.thread) ? value.thread : undefined;
495
+ return readString(thread, 'cwd')
496
+ ?? readString(thread, 'workingDirectory')
497
+ ?? readString(value, 'cwd')
498
+ ?? null;
499
+ }
500
+ function readString(record, key) {
501
+ const value = record?.[key];
502
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
503
+ }
504
+ function readNumber(record, key) {
505
+ const value = record[key];
506
+ return typeof value === 'number' ? value : undefined;
507
+ }
508
+ function isRecord(value) {
509
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
510
+ }
511
+ function toolResult(success, payload) {
512
+ return {
513
+ success,
514
+ contentItems: [{
515
+ type: 'inputText',
516
+ text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2),
517
+ }],
518
+ };
519
+ }
@@ -5,8 +5,9 @@
5
5
  * codex host characterization profile (the "codex host profile" describe
6
6
  * block in core's control-poller.test.ts is the contract):
7
7
  *
8
- * - Keys: `session` + `signal` (never `primitive`), read sequentially per
9
- * conversation, conversations polled one at a time.
8
+ * - Keys: `session` + `signal`, plus `primitive` when a primitive handler is
9
+ * configured; read sequentially per conversation, conversations polled one
10
+ * at a time.
10
11
  * - Cadence: immediate first cycle, then active/idle delays + jitter with
11
12
  * the activity probe sampled BEFORE the cycle runs.
12
13
  * - Session controls are always consumed once newer (default consume), even
@@ -15,7 +16,7 @@
15
16
  * - Dedupe is primed eagerly via `poller.baseline([conversationId])` at
16
17
  * session creation — the second legacy poll site.
17
18
  */
18
- import { ControlChannelPoller, type ControlChannelRTDB, type ControlHandlerResult, type ControlPollerError, type ControlSessionEvent, type ControlSignalEvent } from '@canonmsg/core';
19
+ import { ControlChannelPoller, type ControlChannelRTDB, type ControlHandlerResult, type ControlPollerError, type ControlPrimitiveEvent, type ControlSessionEvent, type ControlSignalEvent } from '@canonmsg/core';
19
20
  export declare const CONTROL_POLL_MS = 2000;
20
21
  export declare const IDLE_CONTROL_POLL_MS = 10000;
21
22
  export declare const CONTROL_POLL_JITTER_MS = 1000;
@@ -38,6 +39,12 @@ export interface CodexControlChannelInput {
38
39
  * node in place; thrown errors also leave it (consumeOnError stays false).
39
40
  */
40
41
  onSignal: (event: ControlSignalEvent) => Promise<ControlHandlerResult> | ControlHandlerResult;
42
+ /**
43
+ * Handles a newer `/primitive` node. Primitive commands are consumed by
44
+ * default after the handler resolves or throws, matching core's command
45
+ * semantics so one failed command cannot replay forever.
46
+ */
47
+ onPrimitive?: (event: ControlPrimitiveEvent) => Promise<ControlHandlerResult> | ControlHandlerResult;
41
48
  onError?: (error: ControlPollerError) => void;
42
49
  /** Injectable jitter source (tests). */
43
50
  random?: () => number;
@@ -5,8 +5,9 @@
5
5
  * codex host characterization profile (the "codex host profile" describe
6
6
  * block in core's control-poller.test.ts is the contract):
7
7
  *
8
- * - Keys: `session` + `signal` (never `primitive`), read sequentially per
9
- * conversation, conversations polled one at a time.
8
+ * - Keys: `session` + `signal`, plus `primitive` when a primitive handler is
9
+ * configured; read sequentially per conversation, conversations polled one
10
+ * at a time.
10
11
  * - Cadence: immediate first cycle, then active/idle delays + jitter with
11
12
  * the activity probe sampled BEFORE the cycle runs.
12
13
  * - Session controls are always consumed once newer (default consume), even
@@ -41,6 +42,13 @@ export function createCodexControlPoller(input) {
41
42
  signal: {
42
43
  handle: input.onSignal,
43
44
  },
45
+ ...(input.onPrimitive
46
+ ? {
47
+ primitive: {
48
+ handle: input.onPrimitive,
49
+ },
50
+ }
51
+ : {}),
44
52
  },
45
53
  ...(input.onError ? { onError: input.onError } : {}),
46
54
  ...(input.random ? { random: input.random } : {}),
package/dist/host.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { type CanonRuntimeCommandDescriptor, type ExecutionEnvironmentMode } from '@canonmsg/core';
2
+ import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type WorkspaceOption, type CanonWorkspaceRootMetadata } from '@canonmsg/core';
3
3
  import { type CodexSkillMetadata } from './app-server-adapter.js';
4
4
  interface HostSessionState {
5
5
  lastError?: string;
@@ -51,5 +51,24 @@ export declare const CODEX_EFFORT_OPTIONS: readonly [{
51
51
  }];
52
52
  export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
53
53
  export declare function buildCodexSkillCommands(skills: ReadonlyArray<CodexSkillMetadata>): CanonRuntimeCommandDescriptor[];
54
+ export declare function buildCodexRuntimeDescriptor(input: {
55
+ models: Array<{
56
+ value: string;
57
+ label: string;
58
+ }>;
59
+ workspaces: WorkspaceOption[];
60
+ workspaceRoots?: CanonWorkspaceRootMetadata[];
61
+ executionModes: ExecutionEnvironmentMode[];
62
+ permissionModes: Array<{
63
+ value: string;
64
+ label: string;
65
+ }>;
66
+ defaultPermissionMode?: string;
67
+ presentation?: CanonRuntimePresentationPolicy;
68
+ supportsPlanMode: boolean;
69
+ supportsCompact?: boolean;
70
+ supportsRichCards?: boolean;
71
+ skills?: ReadonlyArray<CodexSkillMetadata>;
72
+ }): CanonRuntimeDescriptor;
54
73
  export declare function main(): Promise<void>;
55
74
  export {};
package/dist/host.js CHANGED
@@ -10,6 +10,7 @@ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_
10
10
  import { decideAutoReply, } from './inbound-policy.js';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
12
12
  import { CodexAppServerAdapter } from './app-server-adapter.js';
13
+ import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } 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';
@@ -126,7 +127,7 @@ export function buildCodexSkillCommands(skills) {
126
127
  };
127
128
  });
128
129
  }
129
- function buildCodexRuntimeDescriptor(input) {
130
+ export function buildCodexRuntimeDescriptor(input) {
130
131
  const commands = [
131
132
  {
132
133
  id: 'runtime-status',
@@ -146,6 +147,20 @@ function buildCodexRuntimeDescriptor(input) {
146
147
  RUNTIME_STOP_ACTION,
147
148
  RUNTIME_STOP_AND_DROP_ACTION,
148
149
  ];
150
+ if (input.supportsCompact) {
151
+ commands.push({
152
+ id: 'codex-compact-context',
153
+ label: 'Compact',
154
+ description: 'Ask Codex to compact the current thread context.',
155
+ primitive: 'context.compact',
156
+ aliases: ['compact'],
157
+ category: 'session',
158
+ placements: ['composer_slash', 'command_palette'],
159
+ availability: ['idle'],
160
+ ownerOnly: true,
161
+ dispatch: { kind: 'primitive', primitive: 'context.compact' },
162
+ });
163
+ }
149
164
  const skillCommands = input.skills?.length ? buildCodexSkillCommands(input.skills) : [];
150
165
  if (skillCommands.length) {
151
166
  commands.push(...skillCommands);
@@ -835,6 +850,7 @@ export async function main() {
835
850
  session.currentTurnId = null;
836
851
  session.currentTurnOpenedAt = null;
837
852
  session.currentTurnUpdatedAt = null;
853
+ session.currentTurnCanUseCodexAppTools = false;
838
854
  session.lastAcceptedIntent = null;
839
855
  session.resetRequested = false;
840
856
  }
@@ -909,6 +925,7 @@ export async function main() {
909
925
  configOverrides: args.config ?? [],
910
926
  fullAuto: policy.fullAuto,
911
927
  bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
928
+ dynamicTools: CODEX_APP_DYNAMIC_TOOLS,
912
929
  })
913
930
  : new CodexConversationAdapter({
914
931
  cwd: sessionCwd,
@@ -941,6 +958,7 @@ export async function main() {
941
958
  currentTurnId: null,
942
959
  currentTurnOpenedAt: null,
943
960
  currentTurnUpdatedAt: null,
961
+ currentTurnCanUseCodexAppTools: false,
944
962
  activeSelfContextId: null,
945
963
  lastAcceptedIntent: null,
946
964
  resetRequested: false,
@@ -971,7 +989,7 @@ export async function main() {
971
989
  pendingSessionCreations.delete(conversationId);
972
990
  }
973
991
  }
974
- function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled') {
992
+ function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled', canUseCodexAppTools = false) {
975
993
  const nextPrompt = {
976
994
  prompt,
977
995
  intent,
@@ -981,6 +999,7 @@ export async function main() {
981
999
  mediaAddDirs,
982
1000
  planMode,
983
1001
  artifactRoutingMode,
1002
+ canUseCodexAppTools,
984
1003
  };
985
1004
  if (toFront) {
986
1005
  session.queue.unshift(nextPrompt);
@@ -1084,6 +1103,22 @@ export async function main() {
1084
1103
  const requestId = String(request.id);
1085
1104
  const params = request.params;
1086
1105
  const expiresAt = Date.now() + 30 * 60_000;
1106
+ if (request.method === 'item/tool/call' && isCodexAppToolCall(params)) {
1107
+ if (!(session.adapter instanceof CodexAppServerAdapter)) {
1108
+ return deniedCodexAppToolResult('codex_app tools require the Codex app-server transport.');
1109
+ }
1110
+ if (!session.currentTurnCanUseCodexAppTools) {
1111
+ return deniedCodexAppToolResult('Only the Canon owner can use codex_app tools.');
1112
+ }
1113
+ return await handleCodexAppToolCall({
1114
+ adapter: session.adapter,
1115
+ currentThreadId: session.adapter.getThreadId(),
1116
+ currentCwd: session.cwd,
1117
+ workspaces: workspaceOptions,
1118
+ model: session.state.model ?? null,
1119
+ effort: session.state.effort ?? null,
1120
+ }, params);
1121
+ }
1087
1122
  const runtimeCardPayload = runtimeCardRequestPayload(request.method, params);
1088
1123
  if (runtimeCardPayload) {
1089
1124
  const card = parseRuntimeCardV1(runtimeCardPayload);
@@ -1273,7 +1308,7 @@ export async function main() {
1273
1308
  : decision === 'reject'
1274
1309
  ? `The plan was declined — keep planning and wait for guidance before implementing.${feedback ? `\n\nNotes:\n${feedback}` : ''}`
1275
1310
  : `Please revise the plan.${feedback ? `\n\nRevision feedback:\n${feedback}` : ''}`;
1276
- enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve');
1311
+ enqueuePrompt(session, prompt, 'queue', false, input.message.id, false, [], [], decision !== 'approve', 'disabled', input.isOwner);
1277
1312
  return;
1278
1313
  }
1279
1314
  let materialized = [];
@@ -1370,7 +1405,7 @@ export async function main() {
1370
1405
  });
1371
1406
  if (session.running && deliveryIntent === 'interrupt') {
1372
1407
  const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
1373
- enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
1408
+ enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner);
1374
1409
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
1375
1410
  await session.adapter.interrupt().catch(() => { });
1376
1411
  clearStreaming(input.conversationId);
@@ -1378,7 +1413,7 @@ export async function main() {
1378
1413
  return;
1379
1414
  }
1380
1415
  const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
1381
- enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
1416
+ enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode, participantContext.isOwner);
1382
1417
  }
1383
1418
  function sendTurnArtifactFile(session, file) {
1384
1419
  return sendMediaFileMessage(client, session.conversationId, file.path, '', {
@@ -1436,6 +1471,7 @@ export async function main() {
1436
1471
  session.turnCommandBlocks = createCommandBlockTracker();
1437
1472
  session.currentTurnOpenedAt = Date.now();
1438
1473
  session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
1474
+ session.currentTurnCanUseCodexAppTools = nextTurn.canUseCodexAppTools === true;
1439
1475
  session.lastAcceptedIntent = nextTurn.intent;
1440
1476
  session.turnState = 'thinking';
1441
1477
  session.lastActivity = Date.now();
@@ -1701,6 +1737,7 @@ export async function main() {
1701
1737
  session.currentTurnId = null;
1702
1738
  session.currentTurnOpenedAt = null;
1703
1739
  session.currentTurnUpdatedAt = null;
1740
+ session.currentTurnCanUseCodexAppTools = false;
1704
1741
  session.lastAcceptedIntent = null;
1705
1742
  session.resetRequested = false;
1706
1743
  session.lastActivity = Date.now();
@@ -1742,6 +1779,7 @@ export async function main() {
1742
1779
  defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1743
1780
  presentation: runtimePresentation,
1744
1781
  supportsPlanMode: useAppServer,
1782
+ supportsCompact: useAppServer,
1745
1783
  supportsRichCards: useAppServer,
1746
1784
  skills: codexSkills,
1747
1785
  }),
@@ -1838,6 +1876,34 @@ export async function main() {
1838
1876
  clearStreaming(conversationId);
1839
1877
  typingSignals.clear(conversationId).catch(() => { });
1840
1878
  }
1879
+ async function handleControlPrimitive(event) {
1880
+ const { conversationId, value } = event;
1881
+ const primitiveId = typeof value.id === 'string' ? value.id : '';
1882
+ if (primitiveId !== 'context.compact') {
1883
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring unsupported primitive (${primitiveId || 'unknown'})`);
1884
+ return;
1885
+ }
1886
+ const session = sessions.get(conversationId);
1887
+ if (!session || session.closed) {
1888
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: no live Codex session`);
1889
+ return;
1890
+ }
1891
+ if (!(session.adapter instanceof CodexAppServerAdapter)) {
1892
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: compact is only available for Codex app-server sessions`);
1893
+ return;
1894
+ }
1895
+ try {
1896
+ await session.adapter.compactThread();
1897
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Compact requested`);
1898
+ writeState(session);
1899
+ }
1900
+ catch (error) {
1901
+ const message = error instanceof Error ? error.message : String(error);
1902
+ session.state.lastError = `Could not compact Codex context: ${message}`;
1903
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${session.state.lastError}`);
1904
+ writeState(session);
1905
+ }
1906
+ }
1841
1907
  const controlPoller = createCodexControlPoller({
1842
1908
  rtdb,
1843
1909
  agentId,
@@ -1848,6 +1914,7 @@ export async function main() {
1848
1914
  applySessionControl(conversationId, control);
1849
1915
  },
1850
1916
  onSignal: handleControlSignal,
1917
+ onPrimitive: handleControlPrimitive,
1851
1918
  onError: (error) => {
1852
1919
  // The legacy loop ignored transient RTDB failures; keep read/consume
1853
1920
  // errors quiet but surface handler failures.
@@ -17,7 +17,7 @@ function saveStore(store) {
17
17
  }
18
18
  export function buildCodexThreadPolicyFingerprint(input) {
19
19
  return createHash('sha256').update(JSON.stringify({
20
- version: 1,
20
+ version: 2,
21
21
  baseCwd: input.baseCwd,
22
22
  executionMode: input.executionMode ?? null,
23
23
  permissionMode: input.permissionMode ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.18.10",
3
+ "version": "0.18.12",
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": "^3.4.1",
32
+ "@canonmsg/agent-sdk": "^3.4.2",
33
33
  "@canonmsg/coding-agent-host": "^0.2.2",
34
- "@canonmsg/core": "^2.9.2"
34
+ "@canonmsg/core": "^3.0.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"