@canonmsg/codex-plugin 0.19.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,7 +108,7 @@ ps aux | rg canon-codex
108
108
  If you installed the package only inside this repo and not globally, run the built host directly:
109
109
 
110
110
  ```bash
111
- node adapters/codex-plugin/dist/host.js --cwd /path/to/project --full-auto
111
+ node packages/codex-plugin/dist/host.js --cwd /path/to/project --full-auto
112
112
  ```
113
113
 
114
114
  If `canon-codex` starts but cannot find the `codex` binary, either fix your `PATH` or launch with an explicit binary path:
@@ -149,7 +149,7 @@ CANON_AGENT=frontend canon-codex --cwd ~/projects/frontend
149
149
  ## Development
150
150
 
151
151
  ```bash
152
- cd adapters/codex-plugin
152
+ cd packages/codex-plugin
153
153
  npm install
154
154
  npm run build
155
155
  ```
package/dist/adapter.d.ts CHANGED
@@ -8,7 +8,6 @@ export type CodexEvent = {
8
8
  } | {
9
9
  type: 'message';
10
10
  text: string;
11
- delta?: string;
12
11
  itemId?: string;
13
12
  } | {
14
13
  type: 'plan.updated';
@@ -39,7 +39,6 @@ export declare class CodexAppServerAdapter {
39
39
  private skillsCache;
40
40
  private messageTextByItem;
41
41
  private planText;
42
- private traceTurnEpochMs;
43
42
  constructor(opts: {
44
43
  cwd: string;
45
44
  threadId?: string | null;
@@ -85,18 +84,9 @@ export declare class CodexAppServerAdapter {
85
84
  private handleLine;
86
85
  private handleServerRequest;
87
86
  private handleNotification;
88
- /**
89
- * The turn's final message is the JOIN of every agentMessage item, in
90
- * insertion order — not just the last item. The streamed turn trail folds
91
- * all items (one text segment per item), so the final text must match the
92
- * streamed/folded text or the app renders the turn twice.
93
- */
94
- private joinedAgentMessageText;
95
87
  private isCurrentThreadNotification;
96
88
  private resolveCurrentTurn;
97
89
  private clearActiveTurn;
98
90
  private sendRequest;
99
91
  private write;
100
- private trace;
101
- private traceLine;
102
92
  }
@@ -31,7 +31,6 @@ export class CodexAppServerAdapter {
31
31
  skillsCache = null;
32
32
  messageTextByItem = new Map();
33
33
  planText = '';
34
- traceTurnEpochMs = null;
35
34
  constructor(opts) {
36
35
  this.cwd = opts.cwd;
37
36
  this.threadId = opts.threadId ?? null;
@@ -118,8 +117,6 @@ export class CodexAppServerAdapter {
118
117
  this.interrupted = false;
119
118
  this.messageTextByItem.clear();
120
119
  this.planText = '';
121
- this.traceTurnEpochMs = Date.now();
122
- this.trace('turn/run begin');
123
120
  try {
124
121
  if (this.threadId && this.loadedThreadId !== this.threadId) {
125
122
  const resumed = await this.sendRequest('thread/resume', {
@@ -161,11 +158,9 @@ export class CodexAppServerAdapter {
161
158
  this.currentTurnReject = reject;
162
159
  });
163
160
  turnPromise.catch(() => { });
164
- const turnInput = await this.buildTurnInput(prompt, imagePaths);
165
- this.trace('turn/start sent');
166
161
  const turnStarted = await this.sendRequest('turn/start', {
167
162
  threadId: this.threadId,
168
- input: turnInput,
163
+ input: await this.buildTurnInput(prompt, imagePaths),
169
164
  ...(this.model ? { model: this.model } : {}),
170
165
  ...this.sandboxPolicyPayload(_extraAddDirs),
171
166
  collaborationMode: {
@@ -177,7 +172,6 @@ export class CodexAppServerAdapter {
177
172
  },
178
173
  },
179
174
  });
180
- this.trace('turn/start ack');
181
175
  const turn = turnStarted.turn;
182
176
  if (this.currentTurnResolve) {
183
177
  this.currentTurnId = readString(turn, 'id') ?? null;
@@ -328,7 +322,6 @@ export class CodexAppServerAdapter {
328
322
  const message = parseJson(line);
329
323
  if (!message)
330
324
  return;
331
- this.traceLine(line, message);
332
325
  if ('id' in message && ('result' in message || 'error' in message) && !('method' in message)) {
333
326
  const id = Number(message.id);
334
327
  const pending = this.pending.get(id);
@@ -400,9 +393,9 @@ export class CodexAppServerAdapter {
400
393
  const delta = readRawString(params, 'delta') ?? '';
401
394
  const next = `${this.messageTextByItem.get(itemId) ?? ''}${delta}`;
402
395
  this.messageTextByItem.set(itemId, next);
403
- this.currentFinalMessage = this.joinedAgentMessageText() ?? this.currentFinalMessage;
396
+ this.currentFinalMessage = next.trim() ? next : this.currentFinalMessage;
404
397
  if (next.trim())
405
- this.currentOnEvent?.({ type: 'message', text: next, delta, itemId });
398
+ this.currentOnEvent?.({ type: 'message', text: next, itemId });
406
399
  return;
407
400
  }
408
401
  if (method === 'turn/plan/updated') {
@@ -439,8 +432,7 @@ export class CodexAppServerAdapter {
439
432
  const itemId = readString(item, 'id');
440
433
  const text = readString(item, 'text');
441
434
  if (text) {
442
- this.messageTextByItem.set(itemId ?? 'agent-message', text);
443
- this.currentFinalMessage = this.joinedAgentMessageText() ?? text;
435
+ this.currentFinalMessage = text;
444
436
  this.currentOnEvent?.({
445
437
  type: 'message',
446
438
  text,
@@ -474,7 +466,6 @@ export class CodexAppServerAdapter {
474
466
  return;
475
467
  }
476
468
  if (method === 'turn/completed') {
477
- this.trace('turn/completed notification');
478
469
  const turn = params.turn;
479
470
  const status = turn?.status;
480
471
  if (isRecord(status) && status.type === 'failed') {
@@ -495,18 +486,6 @@ export class CodexAppServerAdapter {
495
486
  this.currentErrorText = stringifyPreview(params);
496
487
  }
497
488
  }
498
- /**
499
- * The turn's final message is the JOIN of every agentMessage item, in
500
- * insertion order — not just the last item. The streamed turn trail folds
501
- * all items (one text segment per item), so the final text must match the
502
- * streamed/folded text or the app renders the turn twice.
503
- */
504
- joinedAgentMessageText() {
505
- const joined = [...this.messageTextByItem.values()]
506
- .filter((text) => text.trim())
507
- .join('\n\n');
508
- return joined.trim() ? joined : null;
509
- }
510
489
  isCurrentThreadNotification(params) {
511
490
  const threadId = readNotificationThreadId(params);
512
491
  if (threadId && this.threadId && threadId !== this.threadId)
@@ -538,7 +517,6 @@ export class CodexAppServerAdapter {
538
517
  this.currentErrorText = null;
539
518
  this.messageTextByItem.clear();
540
519
  this.planText = '';
541
- this.traceTurnEpochMs = null;
542
520
  }
543
521
  sendRequest(method, params) {
544
522
  const id = this.requestSeq++;
@@ -558,33 +536,6 @@ export class CodexAppServerAdapter {
558
536
  throw new Error('Codex app-server is not running');
559
537
  this.child.stdin.write(`${JSON.stringify(message)}\n`);
560
538
  }
561
- trace(message) {
562
- if (!isCodexTraceEnabled())
563
- return;
564
- const elapsedMs = this.traceTurnEpochMs === null ? 0 : Date.now() - this.traceTurnEpochMs;
565
- console.error(`[canon-codex-trace] +${elapsedMs}ms ${message}`);
566
- }
567
- traceLine(line, message) {
568
- if (!isCodexTraceEnabled())
569
- return;
570
- const method = typeof message.method === 'string' ? message.method : null;
571
- if (!method) {
572
- const id = 'id' in message ? ` id=${String(message.id)}` : '';
573
- this.trace(`line bytes=${line.length} response${id}`);
574
- return;
575
- }
576
- if (method === 'item/agentMessage/delta') {
577
- const params = isRecord(message.params) ? message.params : {};
578
- const itemId = readString(params, 'itemId') ?? 'agent-message';
579
- const delta = readRawString(params, 'delta') ?? '';
580
- this.trace(`line bytes=${line.length} method=${method} itemId=${itemId} deltaLen=${delta.length}`);
581
- return;
582
- }
583
- this.trace(`line bytes=${line.length} method=${method}`);
584
- }
585
- }
586
- function isCodexTraceEnabled() {
587
- return process.env.CANON_CODEX_TRACE_EVENTS === '1';
588
539
  }
589
540
  function parseJson(line) {
590
541
  try {
@@ -1,4 +1,4 @@
1
- import type { ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core/contract';
1
+ import type { ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core';
2
2
  /**
3
3
  * Mapping for Codex app-server native approval requests.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { sanitizeUnifiedDiffForSharing, truncateUnifiedDiff } from '@canonmsg/core/contract';
1
+ import { sanitizeUnifiedDiffForSharing, truncateUnifiedDiff } from '@canonmsg/core';
2
2
  function isRecord(value) {
3
3
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
4
4
  }
@@ -1,2 +1,2 @@
1
- export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core/local';
2
- export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core/local';
1
+ export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
2
+ export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
package/dist/cli-entry.js CHANGED
@@ -1 +1 @@
1
- export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core/local';
1
+ export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
@@ -1,32 +1,52 @@
1
1
  /**
2
- * Codex host control-channel wiring (bridge notifications).
2
+ * Codex host RTDB `/control/{conversationId}/{agentId}` channel wiring.
3
3
  *
4
- * Phase 6: the host-side RTDB `/control` poller is DELETED the Bridge is
5
- * the single authoritative consumer for this identity (one adaptive poller
6
- * per daemon; cadence, jitter, baselines and consume semantics live there)
7
- * and pushes `onSessionControl` / `onControlSignal` / `onControlPrimitive`
8
- * notifications instead. This module keeps the codex-shaped handler wiring:
9
- * session + signal ride the shared agent-host router; the primitive channel
10
- * (context.compact) subscribes alongside them.
4
+ * Configures the shared `ControlChannelPoller` from `@canonmsg/core` to the
5
+ * codex host characterization profile (the "codex host profile" describe
6
+ * block in core's control-poller.test.ts is the contract):
11
7
  *
12
- * Consume semantics note (deliberate Phase-6 change, plan §2): the node is
13
- * consumed bridge-side BEFORE dispatch, so handler return values are
14
- * advisory-only and a throwing handler is logged, never retried — the legacy
15
- * leave-in-place-on-error behavior is gone with the host poller.
8
+ * - Keys: `session` + `signal`, plus `primitive` when a primitive handler is
9
+ * configured; read sequentially per conversation, conversations polled one
10
+ * at a time.
11
+ * - Cadence: immediate first cycle, then active/idle delays + jitter with
12
+ * the activity probe sampled BEFORE the cycle runs.
13
+ * - Session controls are always consumed once newer (default consume), even
14
+ * when no live session applied them; signals are NOT consumed when the
15
+ * handler throws (consumeOnError stays false).
16
+ * - Dedupe is primed eagerly via `poller.baseline([conversationId])` at
17
+ * session creation — the second legacy poll site.
16
18
  */
17
- import type { ControlPrimitiveEventRecord, ControlSessionEventRecord, ControlSignalEventRecord } from '@canonmsg/backend-contracts';
18
- import { type ControlNotificationSource, type HostControlHandlerResult } from '@canonmsg/agent-host';
19
+ import { ControlChannelPoller, type ControlChannelRTDB, type ControlHandlerResult, type ControlPollerError, type ControlPrimitiveEvent, type ControlSessionEvent, type ControlSignalEvent } from '@canonmsg/core';
20
+ export declare const CONTROL_POLL_MS = 2000;
21
+ export declare const IDLE_CONTROL_POLL_MS = 10000;
22
+ export declare const CONTROL_POLL_JITTER_MS = 1000;
19
23
  export interface CodexControlChannelInput {
20
- /** Buffered notification source (registered before the hello completes). */
21
- source: ControlNotificationSource;
22
- /** Applies a `/session` control (model/effort; permission is start-only). */
23
- onSessionControl: (event: ControlSessionEventRecord) => Promise<HostControlHandlerResult> | HostControlHandlerResult;
24
- /** Handles a `/signal` node (interrupt / stop_and_drop / new_session). */
25
- onSignal: (event: ControlSignalEventRecord) => Promise<HostControlHandlerResult> | HostControlHandlerResult;
26
- /** Handles a `/primitive` command (context.compact). */
27
- onPrimitive?: (event: ControlPrimitiveEventRecord) => Promise<HostControlHandlerResult> | HostControlHandlerResult;
28
- /** Error sink for throwing handlers (default: console.error). */
29
- onError?: (error: unknown, kind: 'session' | 'signal' | 'primitive', conversationId: string | null) => void;
24
+ /** Scoped RTDB handle from `initRTDBAuth` — never the module-global default. */
25
+ rtdb: ControlChannelRTDB;
26
+ agentId: string;
27
+ /** Live session conversation ids, snapshotted at the start of every cycle. */
28
+ conversationIds: () => Iterable<string>;
29
+ /** Sampled before each cycle to choose the active vs idle delay. */
30
+ hasActiveWork: () => boolean;
31
+ /**
32
+ * Applies a newer `/session` control node. The node is always consumed
33
+ * after the handler resolves; a thrown error leaves it in place (with
34
+ * dedupe already advanced, so it is never retried).
35
+ */
36
+ onSessionControl: (event: ControlSessionEvent) => Promise<ControlHandlerResult> | ControlHandlerResult;
37
+ /**
38
+ * Handles a newer `/signal` node. Return `{ consume: false }` to leave the
39
+ * node in place; thrown errors also leave it (consumeOnError stays false).
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;
48
+ onError?: (error: ControlPollerError) => void;
49
+ /** Injectable jitter source (tests). */
50
+ random?: () => number;
30
51
  }
31
- /** Wire the codex control handlers to the bridge notifications; returns detach. */
32
- export declare function attachCodexControlNotifications(input: CodexControlChannelInput): () => void;
52
+ export declare function createCodexControlPoller(input: CodexControlChannelInput): ControlChannelPoller;
@@ -1,43 +1,56 @@
1
1
  /**
2
- * Codex host control-channel wiring (bridge notifications).
2
+ * Codex host RTDB `/control/{conversationId}/{agentId}` channel wiring.
3
3
  *
4
- * Phase 6: the host-side RTDB `/control` poller is DELETED the Bridge is
5
- * the single authoritative consumer for this identity (one adaptive poller
6
- * per daemon; cadence, jitter, baselines and consume semantics live there)
7
- * and pushes `onSessionControl` / `onControlSignal` / `onControlPrimitive`
8
- * notifications instead. This module keeps the codex-shaped handler wiring:
9
- * session + signal ride the shared agent-host router; the primitive channel
10
- * (context.compact) subscribes alongside them.
4
+ * Configures the shared `ControlChannelPoller` from `@canonmsg/core` to the
5
+ * codex host characterization profile (the "codex host profile" describe
6
+ * block in core's control-poller.test.ts is the contract):
11
7
  *
12
- * Consume semantics note (deliberate Phase-6 change, plan §2): the node is
13
- * consumed bridge-side BEFORE dispatch, so handler return values are
14
- * advisory-only and a throwing handler is logged, never retried — the legacy
15
- * leave-in-place-on-error behavior is gone with the host poller.
8
+ * - Keys: `session` + `signal`, plus `primitive` when a primitive handler is
9
+ * configured; read sequentially per conversation, conversations polled one
10
+ * at a time.
11
+ * - Cadence: immediate first cycle, then active/idle delays + jitter with
12
+ * the activity probe sampled BEFORE the cycle runs.
13
+ * - Session controls are always consumed once newer (default consume), even
14
+ * when no live session applied them; signals are NOT consumed when the
15
+ * handler throws (consumeOnError stays false).
16
+ * - Dedupe is primed eagerly via `poller.baseline([conversationId])` at
17
+ * session creation — the second legacy poll site.
16
18
  */
17
- import { attachHostControlNotifications, } from '@canonmsg/agent-host';
18
- /** Wire the codex control handlers to the bridge notifications; returns detach. */
19
- export function attachCodexControlNotifications(input) {
20
- const report = input.onError ?? ((error, kind, conversationId) => {
21
- const convo = conversationId ? ` [${conversationId.slice(0, 8)}]` : '';
22
- console.error(`[canon-codex]${convo} Control ${kind} handler error:`, error);
19
+ import { ControlChannelPoller, } from '@canonmsg/core';
20
+ export const CONTROL_POLL_MS = 2_000;
21
+ export const IDLE_CONTROL_POLL_MS = 10_000;
22
+ export const CONTROL_POLL_JITTER_MS = 1_000;
23
+ export function createCodexControlPoller(input) {
24
+ return new ControlChannelPoller({
25
+ rtdb: input.rtdb,
26
+ agentId: input.agentId,
27
+ conversationIds: input.conversationIds,
28
+ cadence: {
29
+ kind: 'adaptive',
30
+ activeMs: CONTROL_POLL_MS,
31
+ idleMs: IDLE_CONTROL_POLL_MS,
32
+ jitterMs: CONTROL_POLL_JITTER_MS,
33
+ hasActiveWork: input.hasActiveWork,
34
+ activitySample: 'cycle-start',
35
+ },
36
+ pollOnStart: true,
37
+ conversationConcurrency: 'sequential',
38
+ handlers: {
39
+ session: {
40
+ handle: input.onSessionControl,
41
+ },
42
+ signal: {
43
+ handle: input.onSignal,
44
+ },
45
+ ...(input.onPrimitive
46
+ ? {
47
+ primitive: {
48
+ handle: input.onPrimitive,
49
+ },
50
+ }
51
+ : {}),
52
+ },
53
+ ...(input.onError ? { onError: input.onError } : {}),
54
+ ...(input.random ? { random: input.random } : {}),
23
55
  });
24
- const detachShared = attachHostControlNotifications(input.source, { logPrefix: '[canon-codex]' }, {
25
- onSessionControl: input.onSessionControl,
26
- onControlSignal: input.onSignal,
27
- onError: (error, kind, conversationId) => report(error, kind, conversationId),
28
- });
29
- let detachPrimitive = null;
30
- if (input.onPrimitive) {
31
- const onPrimitive = input.onPrimitive;
32
- detachPrimitive = input.source.onNotification('onControlPrimitive', (params) => {
33
- const event = params;
34
- void Promise.resolve(onPrimitive(event)).catch((error) => {
35
- report(error, 'primitive', event?.conversationId ?? null);
36
- });
37
- });
38
- }
39
- return () => {
40
- detachShared();
41
- detachPrimitive?.();
42
- };
43
56
  }
@@ -0,0 +1,4 @@
1
+ export interface LongLivedStream {
2
+ start(): Promise<void>;
3
+ }
4
+ export declare function startCodexStreamInBackground(stream: LongLivedStream, onError: (error: unknown) => void): void;
@@ -0,0 +1,3 @@
1
+ export function startCodexStreamInBackground(stream, onError) {
2
+ stream.start().catch(onError);
3
+ }
package/dist/host.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type WorkspaceOption, type CanonWorkspaceRootMetadata } from '@canonmsg/core/contract';
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;