@canonmsg/codex-plugin 0.18.9 → 0.18.11

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.
@@ -64,6 +64,7 @@ export declare class CodexAppServerAdapter {
64
64
  setReasoningEffort(effort: string | null): void;
65
65
  isRunning(): boolean;
66
66
  interrupt(): Promise<void>;
67
+ compactThread(): Promise<void>;
67
68
  close(): void;
68
69
  runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[], _extraAddDirs?: readonly string[], options?: CodexRunTurnOptions): Promise<CodexTurnResult>;
69
70
  private resolveApprovalPolicy;
@@ -83,6 +83,15 @@ export class CodexAppServerAdapter {
83
83
  turnId: this.currentTurnId,
84
84
  }).catch(() => { });
85
85
  }
86
+ async compactThread() {
87
+ if (!this.threadId) {
88
+ throw new Error('No Codex thread to compact');
89
+ }
90
+ await this.ensureStarted();
91
+ await this.sendRequest('thread/compact/start', {
92
+ threadId: this.threadId,
93
+ });
94
+ }
86
95
  close() {
87
96
  this.child?.kill('SIGTERM');
88
97
  this.child = null;
@@ -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 } 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;
@@ -17,6 +17,21 @@ export declare function buildCodexInitialSessionState(input: {
17
17
  permissionMode?: string;
18
18
  effort?: string | null;
19
19
  }): HostSessionState;
20
+ export declare function buildCodexLiveSessionConfig(input: {
21
+ model?: string;
22
+ permissionMode?: string;
23
+ effort?: string;
24
+ workspaceId?: string | null;
25
+ executionMode: ExecutionEnvironmentMode;
26
+ executionBranch?: string | null;
27
+ }): {
28
+ executionMode: ExecutionEnvironmentMode;
29
+ executionBranch: string | null;
30
+ workspaceId?: string | undefined;
31
+ effort?: string | undefined;
32
+ permissionMode?: string | undefined;
33
+ model?: string | undefined;
34
+ };
20
35
  /** GPT reasoning-effort levels, applied next turn via model_reasoning_effort. */
21
36
  export declare const CODEX_EFFORT_OPTIONS: readonly [{
22
37
  readonly value: "minimal";
@@ -34,6 +49,26 @@ export declare const CODEX_EFFORT_OPTIONS: readonly [{
34
49
  readonly value: "xhigh";
35
50
  readonly label: "Extra high";
36
51
  }];
52
+ export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
37
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;
38
73
  export declare function main(): Promise<void>;
39
74
  export {};
package/dist/host.js CHANGED
@@ -61,6 +61,16 @@ export function buildCodexInitialSessionState(input) {
61
61
  state: 'idle',
62
62
  };
63
63
  }
64
+ export function buildCodexLiveSessionConfig(input) {
65
+ return {
66
+ ...(input.model ? { model: input.model } : {}),
67
+ ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
68
+ ...(input.effort ? { effort: input.effort } : {}),
69
+ ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
70
+ executionMode: input.executionMode,
71
+ executionBranch: input.executionBranch ?? null,
72
+ };
73
+ }
64
74
  const MAX_SESSIONS = 12;
65
75
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
66
76
  const HEARTBEAT_MS = 30_000;
@@ -85,6 +95,7 @@ export const CODEX_EFFORT_OPTIONS = [
85
95
  { value: 'xhigh', label: 'Extra high' },
86
96
  ];
87
97
  const CODEX_EFFORT_VALUES = new Set(CODEX_EFFORT_OPTIONS.map((option) => option.value));
98
+ export const CODEX_SESSION_CONFIG_FIELDS = ['permissionMode', 'effort'];
88
99
  const MAX_CODEX_SKILL_COMMAND_CHOICES = 50;
89
100
  export function buildCodexSkillCommands(skills) {
90
101
  return skills
@@ -115,7 +126,7 @@ export function buildCodexSkillCommands(skills) {
115
126
  };
116
127
  });
117
128
  }
118
- function buildCodexRuntimeDescriptor(input) {
129
+ export function buildCodexRuntimeDescriptor(input) {
119
130
  const commands = [
120
131
  {
121
132
  id: 'runtime-status',
@@ -135,6 +146,20 @@ function buildCodexRuntimeDescriptor(input) {
135
146
  RUNTIME_STOP_ACTION,
136
147
  RUNTIME_STOP_AND_DROP_ACTION,
137
148
  ];
149
+ if (input.supportsCompact) {
150
+ commands.push({
151
+ id: 'codex-compact-context',
152
+ label: 'Compact',
153
+ description: 'Ask Codex to compact the current thread context.',
154
+ primitive: 'context.compact',
155
+ aliases: ['compact'],
156
+ category: 'session',
157
+ placements: ['composer_slash', 'command_palette'],
158
+ availability: ['idle'],
159
+ ownerOnly: true,
160
+ dispatch: { kind: 'primitive', primitive: 'context.compact' },
161
+ });
162
+ }
138
163
  const skillCommands = input.skills?.length ? buildCodexSkillCommands(input.skills) : [];
139
164
  if (skillCommands.length) {
140
165
  commands.push(...skillCommands);
@@ -224,7 +249,7 @@ async function loadSessionConfig(conversationId, agentId) {
224
249
  return loadHostSessionConfig({
225
250
  conversationId,
226
251
  agentId,
227
- extraStringFields: ['permissionMode', 'effort'],
252
+ extraStringFields: CODEX_SESSION_CONFIG_FIELDS,
228
253
  });
229
254
  }
230
255
  function resolveSessionExecutionMode(config) {
@@ -1731,6 +1756,7 @@ export async function main() {
1731
1756
  defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1732
1757
  presentation: runtimePresentation,
1733
1758
  supportsPlanMode: useAppServer,
1759
+ supportsCompact: useAppServer,
1734
1760
  supportsRichCards: useAppServer,
1735
1761
  skills: codexSkills,
1736
1762
  }),
@@ -1827,6 +1853,34 @@ export async function main() {
1827
1853
  clearStreaming(conversationId);
1828
1854
  typingSignals.clear(conversationId).catch(() => { });
1829
1855
  }
1856
+ async function handleControlPrimitive(event) {
1857
+ const { conversationId, value } = event;
1858
+ const primitiveId = typeof value.id === 'string' ? value.id : '';
1859
+ if (primitiveId !== 'context.compact') {
1860
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring unsupported primitive (${primitiveId || 'unknown'})`);
1861
+ return;
1862
+ }
1863
+ const session = sessions.get(conversationId);
1864
+ if (!session || session.closed) {
1865
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: no live Codex session`);
1866
+ return;
1867
+ }
1868
+ if (!(session.adapter instanceof CodexAppServerAdapter)) {
1869
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: compact is only available for Codex app-server sessions`);
1870
+ return;
1871
+ }
1872
+ try {
1873
+ await session.adapter.compactThread();
1874
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Compact requested`);
1875
+ writeState(session);
1876
+ }
1877
+ catch (error) {
1878
+ const message = error instanceof Error ? error.message : String(error);
1879
+ session.state.lastError = `Could not compact Codex context: ${message}`;
1880
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${session.state.lastError}`);
1881
+ writeState(session);
1882
+ }
1883
+ }
1830
1884
  const controlPoller = createCodexControlPoller({
1831
1885
  rtdb,
1832
1886
  agentId,
@@ -1837,6 +1891,7 @@ export async function main() {
1837
1891
  applySessionControl(conversationId, control);
1838
1892
  },
1839
1893
  onSignal: handleControlSignal,
1894
+ onPrimitive: handleControlPrimitive,
1840
1895
  onError: (error) => {
1841
1896
  // The legacy loop ignored transient RTDB failures; keep read/consume
1842
1897
  // errors quiet but surface handler failures.
@@ -1872,18 +1927,19 @@ export async function main() {
1872
1927
  runtime: runtimeDescriptor,
1873
1928
  workspaceOptions,
1874
1929
  defaultCwd: workingDir,
1875
- extraSessionConfigFields: ['permissionMode'],
1930
+ extraSessionConfigFields: CODEX_SESSION_CONFIG_FIELDS,
1876
1931
  liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
1877
1932
  const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
1878
1933
  return [
1879
1934
  session.conversationId,
1880
- {
1881
- ...(session.state.model ? { model: session.state.model } : {}),
1882
- ...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
1883
- ...(workspaceId ? { workspaceId } : {}),
1935
+ buildCodexLiveSessionConfig({
1936
+ model: session.state.model,
1937
+ permissionMode: session.state.permissionMode,
1938
+ effort: session.state.effort,
1939
+ workspaceId,
1884
1940
  executionMode: session.environment.mode,
1885
1941
  executionBranch: session.environment.branch ?? null,
1886
- },
1942
+ }),
1887
1943
  ];
1888
1944
  })),
1889
1945
  }).catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.18.9",
3
+ "version": "0.18.11",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",