@canonmsg/codex-plugin 0.18.10 → 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, 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
@@ -126,7 +126,7 @@ export function buildCodexSkillCommands(skills) {
126
126
  };
127
127
  });
128
128
  }
129
- function buildCodexRuntimeDescriptor(input) {
129
+ export function buildCodexRuntimeDescriptor(input) {
130
130
  const commands = [
131
131
  {
132
132
  id: 'runtime-status',
@@ -146,6 +146,20 @@ function buildCodexRuntimeDescriptor(input) {
146
146
  RUNTIME_STOP_ACTION,
147
147
  RUNTIME_STOP_AND_DROP_ACTION,
148
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
+ }
149
163
  const skillCommands = input.skills?.length ? buildCodexSkillCommands(input.skills) : [];
150
164
  if (skillCommands.length) {
151
165
  commands.push(...skillCommands);
@@ -1742,6 +1756,7 @@ export async function main() {
1742
1756
  defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1743
1757
  presentation: runtimePresentation,
1744
1758
  supportsPlanMode: useAppServer,
1759
+ supportsCompact: useAppServer,
1745
1760
  supportsRichCards: useAppServer,
1746
1761
  skills: codexSkills,
1747
1762
  }),
@@ -1838,6 +1853,34 @@ export async function main() {
1838
1853
  clearStreaming(conversationId);
1839
1854
  typingSignals.clear(conversationId).catch(() => { });
1840
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
+ }
1841
1884
  const controlPoller = createCodexControlPoller({
1842
1885
  rtdb,
1843
1886
  agentId,
@@ -1848,6 +1891,7 @@ export async function main() {
1848
1891
  applySessionControl(conversationId, control);
1849
1892
  },
1850
1893
  onSignal: handleControlSignal,
1894
+ onPrimitive: handleControlPrimitive,
1851
1895
  onError: (error) => {
1852
1896
  // The legacy loop ignored transient RTDB failures; keep read/consume
1853
1897
  // errors quiet but surface handler failures.
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.11",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",