@canonmsg/codex-plugin 0.9.7 → 0.10.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/dist/adapter.d.ts CHANGED
@@ -64,7 +64,8 @@ export declare class CodexConversationAdapter {
64
64
  setModel(model: string | null): void;
65
65
  isRunning(): boolean;
66
66
  interrupt(): Promise<void>;
67
- runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[]): Promise<CodexTurnResult>;
67
+ runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[], extraAddDirs?: readonly string[]): Promise<CodexTurnResult>;
68
+ private buildAddDirs;
68
69
  private buildArgs;
69
70
  private canResumeWithCurrentPolicy;
70
71
  private clearActiveProcess;
package/dist/adapter.js CHANGED
@@ -51,11 +51,11 @@ export class CodexConversationAdapter {
51
51
  this.child.kill('SIGKILL');
52
52
  }, 5_000);
53
53
  }
54
- async runTurn(prompt, onEvent, onLog, imagePaths = []) {
54
+ async runTurn(prompt, onEvent, onLog, imagePaths = [], extraAddDirs = []) {
55
55
  if (this.child) {
56
56
  throw new Error('A Codex turn is already in progress for this conversation');
57
57
  }
58
- const args = this.buildArgs(prompt, imagePaths);
58
+ const args = this.buildArgs(prompt, imagePaths, extraAddDirs);
59
59
  const child = spawn(this.codexBin, args, {
60
60
  cwd: this.cwd,
61
61
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -147,7 +147,19 @@ export class CodexConversationAdapter {
147
147
  });
148
148
  });
149
149
  }
150
- buildArgs(prompt, imagePaths = []) {
150
+ buildAddDirs(extraAddDirs = []) {
151
+ const seen = new Set();
152
+ const dirs = [];
153
+ for (const value of [...this.addDirs, ...extraAddDirs]) {
154
+ const trimmed = value.trim();
155
+ if (!trimmed || seen.has(trimmed))
156
+ continue;
157
+ seen.add(trimmed);
158
+ dirs.push(value);
159
+ }
160
+ return dirs;
161
+ }
162
+ buildArgs(prompt, imagePaths = [], extraAddDirs = []) {
151
163
  if (this.threadId && this.canResumeWithCurrentPolicy()) {
152
164
  const args = ['exec', 'resume', '--json', '--skip-git-repo-check'];
153
165
  if (this.model) {
@@ -159,6 +171,9 @@ export class CodexConversationAdapter {
159
171
  for (const configOverride of this.configOverrides) {
160
172
  args.push('-c', configOverride);
161
173
  }
174
+ for (const addDir of this.buildAddDirs(extraAddDirs)) {
175
+ args.push('--add-dir', addDir);
176
+ }
162
177
  if (this.fullAuto) {
163
178
  args.push('--full-auto');
164
179
  }
@@ -168,6 +183,9 @@ export class CodexConversationAdapter {
168
183
  for (const imagePath of imagePaths) {
169
184
  args.push('-i', imagePath);
170
185
  }
186
+ if (imagePaths.length > 0) {
187
+ args.push('--');
188
+ }
171
189
  args.push(this.threadId, prompt);
172
190
  return args;
173
191
  }
@@ -189,7 +207,7 @@ export class CodexConversationAdapter {
189
207
  if (this.codexProfile) {
190
208
  args.push('-p', this.codexProfile);
191
209
  }
192
- for (const addDir of this.addDirs) {
210
+ for (const addDir of this.buildAddDirs(extraAddDirs)) {
193
211
  args.push('--add-dir', addDir);
194
212
  }
195
213
  for (const configOverride of this.configOverrides) {
@@ -204,6 +222,9 @@ export class CodexConversationAdapter {
204
222
  for (const imagePath of imagePaths) {
205
223
  args.push('-i', imagePath);
206
224
  }
225
+ if (imagePaths.length > 0) {
226
+ args.push('--');
227
+ }
207
228
  args.push(prompt);
208
229
  return args;
209
230
  }
package/dist/host.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { setDefaultResultOrder } from 'node:dns';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { dirname } from 'node:path';
4
5
  import { parseArgs } from 'node:util';
5
6
  import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
6
- import { buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
7
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
7
8
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
8
9
  import { CodexConversationAdapter, } from './adapter.js';
9
10
  import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
@@ -64,26 +65,9 @@ function buildCodexRuntimeDescriptor(input) {
64
65
  defaultPermissionMode: input.defaultPermissionMode,
65
66
  streamingTextMode: 'snapshot',
66
67
  actions: [
67
- {
68
- id: 'stop',
69
- label: 'Stop',
70
- description: 'Interrupt the current Codex exec turn.',
71
- aliases: ['stop'],
72
- category: 'turn',
73
- placements: ['composer_slash', 'command_palette'],
74
- availability: ['busy'],
75
- dispatch: { kind: 'signal', signal: 'interrupt' },
76
- },
77
- {
78
- id: 'stop-and-clear-queue',
79
- label: 'Stop & clear queue',
80
- description: 'Interrupt the current Codex exec turn and drop queued Canon messages.',
81
- aliases: ['stop-clear', 'clear-queue'],
82
- category: 'turn',
83
- placements: ['composer_slash', 'command_palette', 'session_strip'],
84
- availability: ['busy_with_queue'],
85
- dispatch: { kind: 'signal', signal: 'stop_and_drop' },
86
- },
68
+ RUNTIME_STOP_ACTION,
69
+ RUNTIME_STOP_AND_DROP_ACTION,
70
+ RUNTIME_NEW_SESSION_ACTION,
87
71
  ],
88
72
  });
89
73
  if (input.models.length > 0) {
@@ -207,6 +191,9 @@ function summarizeCommand(command) {
207
191
  function sleep(ms) {
208
192
  return new Promise((resolve) => setTimeout(resolve, ms));
209
193
  }
194
+ function uniqueStrings(values) {
195
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0)));
196
+ }
210
197
  export async function main() {
211
198
  setDefaultResultOrder('ipv4first');
212
199
  const { values: args } = parseArgs({
@@ -448,6 +435,32 @@ export async function main() {
448
435
  client.setTyping(conversationId, false).catch(() => { });
449
436
  sessions.delete(conversationId);
450
437
  }
438
+ async function resetRuntimeSession(session) {
439
+ const conversationId = session.conversationId;
440
+ session.resetRequested = true;
441
+ const droppedPrompts = session.queue.splice(0);
442
+ await markQueuedPromptsRejected(conversationId, droppedPrompts);
443
+ clearStoredThreadId(runtimeId, agentId, conversationId, session.environment.baseCwd, session.environment.mode);
444
+ session.adapter.clearThreadId();
445
+ session.activeSelfContextId = null;
446
+ session.state.lastError = undefined;
447
+ if (session.running) {
448
+ await session.adapter.interrupt();
449
+ session.turnState = 'interrupted';
450
+ }
451
+ else {
452
+ session.turnState = 'idle';
453
+ session.currentTurnId = null;
454
+ session.currentTurnOpenedAt = null;
455
+ session.lastAcceptedIntent = null;
456
+ session.resetRequested = false;
457
+ }
458
+ stopVisibleWorkSignal(session);
459
+ clearStreaming(conversationId);
460
+ client.setTyping(conversationId, false).catch(() => { });
461
+ writeState(session);
462
+ writeTurn(session);
463
+ }
451
464
  function evictOldestIdle() {
452
465
  let oldest = null;
453
466
  for (const session of sessions.values()) {
@@ -525,7 +538,9 @@ export async function main() {
525
538
  turnState: 'idle',
526
539
  currentTurnId: null,
527
540
  currentTurnOpenedAt: null,
541
+ activeSelfContextId: null,
528
542
  lastAcceptedIntent: null,
543
+ resetRequested: false,
529
544
  lastActivity: Date.now(),
530
545
  typingKeepaliveTimer: null,
531
546
  closed: false,
@@ -553,8 +568,8 @@ export async function main() {
553
568
  pendingSessionCreations.delete(conversationId);
554
569
  }
555
570
  }
556
- function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = []) {
557
- const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths };
571
+ function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = []) {
572
+ const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths, mediaAddDirs };
558
573
  if (toFront) {
559
574
  session.queue.unshift(nextPrompt);
560
575
  }
@@ -582,6 +597,7 @@ export async function main() {
582
597
  const imagePaths = materialized
583
598
  .map((attachment) => getCodexImagePath(attachment))
584
599
  .filter((path) => path !== null);
600
+ const mediaAddDirs = uniqueStrings(materialized.map((attachment) => dirname(attachment.path)));
585
601
  const content = renderInboundContent(input.message, materialized);
586
602
  const hydrated = await loadHydratedInboundContext({
587
603
  conversationId: input.conversationId,
@@ -591,11 +607,11 @@ export async function main() {
591
607
  hydratedPage: input.hydratedPage,
592
608
  });
593
609
  const behavior = input.behavior ?? hydrated.behavior;
594
- const workSessions = hydrated.hydratedFromPage
595
- ? hydrated.workSessions
596
- : Array.isArray(input.workSessions)
597
- ? input.workSessions
598
- : hydrated.workSessions;
610
+ const selfContexts = hydrated.hydratedFromPage
611
+ ? hydrated.selfContexts
612
+ : Array.isArray(input.selfContexts)
613
+ ? input.selfContexts
614
+ : hydrated.selfContexts;
599
615
  const participantContext = hydrated.participantContext;
600
616
  const autoReply = decideAutoReply(participantContext, behavior);
601
617
  if (!autoReply.allow) {
@@ -612,6 +628,7 @@ export async function main() {
612
628
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
613
629
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
614
630
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
631
+ ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
615
632
  metadata: {
616
633
  turnSemantics: 'turn_complete',
617
634
  turnComplete: true,
@@ -621,6 +638,7 @@ export async function main() {
621
638
  return;
622
639
  }
623
640
  const turnMetadata = normalizeTurnMetadata(input.message.metadata);
641
+ session.activeSelfContextId = selfContexts[0]?.id ?? null;
624
642
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
625
643
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
626
644
  const prompt = buildCanonPrompt({
@@ -628,18 +646,17 @@ export async function main() {
628
646
  conversationId: input.conversationId,
629
647
  participantContext,
630
648
  behavior,
631
- workSession: input.message.workSession,
632
- workSessions,
649
+ selfContexts,
633
650
  });
634
651
  if (session.running && deliveryIntent === 'interrupt') {
635
- enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths);
652
+ enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs);
636
653
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
637
654
  await session.adapter.interrupt().catch(() => { });
638
655
  clearStreaming(input.conversationId);
639
656
  client.setTyping(input.conversationId, false).catch(() => { });
640
657
  return;
641
658
  }
642
- enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths);
659
+ enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs);
643
660
  }
644
661
  async function runNextTurn(session) {
645
662
  if (session.running || session.closed)
@@ -669,9 +686,13 @@ export async function main() {
669
686
  throw new ExecutionEnvironmentError(modelGuard, modelGuard);
670
687
  }
671
688
  const turnImagePaths = nextTurn.imagePaths ?? [];
689
+ const turnMediaAddDirs = nextTurn.mediaAddDirs ?? [];
672
690
  const handleCodexEvent = (event) => {
673
691
  session.lastActivity = Date.now();
674
692
  if (event.type === 'thread.started') {
693
+ if (session.resetRequested) {
694
+ return;
695
+ }
675
696
  saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
676
697
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
677
698
  return;
@@ -708,7 +729,7 @@ export async function main() {
708
729
  clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
709
730
  session.adapter.clearThreadId();
710
731
  };
711
- const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths);
732
+ const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs);
712
733
  let result = await runTurnOnce();
713
734
  if (!result.interrupted
714
735
  && !result.finalMessage
@@ -719,7 +740,7 @@ export async function main() {
719
740
  clearStoredThread();
720
741
  result = await runTurnOnce();
721
742
  }
722
- if (result.threadId) {
743
+ if (result.threadId && !session.resetRequested) {
723
744
  saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
724
745
  }
725
746
  if (!result.interrupted && result.finalMessage) {
@@ -727,6 +748,9 @@ export async function main() {
727
748
  clearStoredThread();
728
749
  }
729
750
  await client.sendMessage(session.conversationId, result.finalMessage, {
751
+ ...(session.activeSelfContextId
752
+ ? { selfContextId: session.activeSelfContextId }
753
+ : {}),
730
754
  metadata: {
731
755
  turnId: session.currentTurnId,
732
756
  turnSemantics: 'turn_complete',
@@ -745,6 +769,9 @@ export async function main() {
745
769
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
746
770
  }
747
771
  await client.sendMessage(session.conversationId, userVisibleError, {
772
+ ...(session.activeSelfContextId
773
+ ? { selfContextId: session.activeSelfContextId }
774
+ : {}),
748
775
  metadata: {
749
776
  turnId: session.currentTurnId,
750
777
  turnSemantics: 'turn_complete',
@@ -773,6 +800,9 @@ export async function main() {
773
800
  session.state.lastError = message;
774
801
  writeState(session);
775
802
  await client.sendMessage(session.conversationId, message, {
803
+ ...(session.activeSelfContextId
804
+ ? { selfContextId: session.activeSelfContextId }
805
+ : {}),
776
806
  metadata: {
777
807
  turnId: session.currentTurnId,
778
808
  turnSemantics: 'turn_complete',
@@ -794,6 +824,7 @@ export async function main() {
794
824
  session.currentTurnId = null;
795
825
  session.currentTurnOpenedAt = null;
796
826
  session.lastAcceptedIntent = null;
827
+ session.resetRequested = false;
797
828
  session.lastActivity = Date.now();
798
829
  writeState(session);
799
830
  writeTurn(session);
@@ -969,7 +1000,7 @@ export async function main() {
969
1000
  senderName: message.senderName || message.senderId,
970
1001
  isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
971
1002
  behavior: payload.behavior,
972
- workSessions: payload.workSessions,
1003
+ selfContexts: payload.selfContexts,
973
1004
  });
974
1005
  if (message.id) {
975
1006
  saveRuntimeSessionState(runtimeId, {
@@ -1076,7 +1107,7 @@ export async function main() {
1076
1107
  senderName: latestMessage.senderId,
1077
1108
  isOwner: ownerId != null && latestMessage.senderId === ownerId,
1078
1109
  behavior: latestPage.behavior,
1079
- workSessions: latestPage.workSessions,
1110
+ selfContexts: latestPage.selfContexts,
1080
1111
  hydratedPage: latestPage,
1081
1112
  });
1082
1113
  if (latestMessage.id) {
@@ -1136,7 +1167,7 @@ export async function main() {
1136
1167
  continue;
1137
1168
  const signal = raw;
1138
1169
  const timestamp = signal.updatedAt ?? 0;
1139
- if ((signal.type !== 'interrupt' && signal.type !== 'stop_and_drop')
1170
+ if ((signal.type !== 'interrupt' && signal.type !== 'stop_and_drop' && signal.type !== 'new_session')
1140
1171
  || timestamp <= (lastSeenSignal.get(conversationId) ?? 0)) {
1141
1172
  continue;
1142
1173
  }
@@ -1144,6 +1175,12 @@ export async function main() {
1144
1175
  const session = sessions.get(conversationId);
1145
1176
  if (!session || session.closed)
1146
1177
  continue;
1178
+ if (signal.type === 'new_session') {
1179
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] new_session signal`);
1180
+ await resetRuntimeSession(session);
1181
+ await rtdbWrite(`/control/${conversationId}/${agentId}/signal`, null).catch(() => { });
1182
+ continue;
1183
+ }
1147
1184
  if (!session.running && (signal.type !== 'stop_and_drop' || session.queue.length === 0)) {
1148
1185
  await rtdbWrite(`/control/${conversationId}/${agentId}/signal`, null).catch(() => { });
1149
1186
  continue;
@@ -84,11 +84,26 @@ export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd,
84
84
  }
85
85
  export function clearStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode) {
86
86
  if (runtimeId) {
87
+ const existing = baseCwd
88
+ ? loadRuntimeSessionState(runtimeId, {
89
+ conversationId,
90
+ baseCwd,
91
+ executionMode,
92
+ })
93
+ : null;
87
94
  clearRuntimeSessionState(runtimeId, {
88
95
  conversationId,
89
96
  baseCwd,
90
97
  executionMode,
91
98
  });
99
+ if (existing?.lastInboundMessageId && baseCwd) {
100
+ saveRuntimeSessionState(runtimeId, {
101
+ conversationId,
102
+ baseCwd,
103
+ ...(executionMode ? { executionMode } : {}),
104
+ lastInboundMessageId: existing.lastInboundMessageId,
105
+ });
106
+ }
92
107
  return;
93
108
  }
94
109
  const store = loadStore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.9.7",
3
+ "version": "0.10.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^1.1.2",
33
- "@canonmsg/core": "^0.15.4"
32
+ "@canonmsg/agent-sdk": "^1.2.0",
33
+ "@canonmsg/core": "^0.16.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"