@canonmsg/codex-plugin 0.9.6 → 0.9.8

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,6 +1,7 @@
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
7
  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';
@@ -207,6 +208,9 @@ function summarizeCommand(command) {
207
208
  function sleep(ms) {
208
209
  return new Promise((resolve) => setTimeout(resolve, ms));
209
210
  }
211
+ function uniqueStrings(values) {
212
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0)));
213
+ }
210
214
  export async function main() {
211
215
  setDefaultResultOrder('ipv4first');
212
216
  const { values: args } = parseArgs({
@@ -525,6 +529,7 @@ export async function main() {
525
529
  turnState: 'idle',
526
530
  currentTurnId: null,
527
531
  currentTurnOpenedAt: null,
532
+ activeSelfContextId: null,
528
533
  lastAcceptedIntent: null,
529
534
  lastActivity: Date.now(),
530
535
  typingKeepaliveTimer: null,
@@ -553,8 +558,8 @@ export async function main() {
553
558
  pendingSessionCreations.delete(conversationId);
554
559
  }
555
560
  }
556
- function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = []) {
557
- const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths };
561
+ function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = []) {
562
+ const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths, mediaAddDirs };
558
563
  if (toFront) {
559
564
  session.queue.unshift(nextPrompt);
560
565
  }
@@ -582,6 +587,7 @@ export async function main() {
582
587
  const imagePaths = materialized
583
588
  .map((attachment) => getCodexImagePath(attachment))
584
589
  .filter((path) => path !== null);
590
+ const mediaAddDirs = uniqueStrings(materialized.map((attachment) => dirname(attachment.path)));
585
591
  const content = renderInboundContent(input.message, materialized);
586
592
  const hydrated = await loadHydratedInboundContext({
587
593
  conversationId: input.conversationId,
@@ -591,11 +597,11 @@ export async function main() {
591
597
  hydratedPage: input.hydratedPage,
592
598
  });
593
599
  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;
600
+ const selfContexts = hydrated.hydratedFromPage
601
+ ? hydrated.selfContexts
602
+ : Array.isArray(input.selfContexts)
603
+ ? input.selfContexts
604
+ : hydrated.selfContexts;
599
605
  const participantContext = hydrated.participantContext;
600
606
  const autoReply = decideAutoReply(participantContext, behavior);
601
607
  if (!autoReply.allow) {
@@ -612,14 +618,17 @@ export async function main() {
612
618
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
613
619
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
614
620
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
621
+ ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
615
622
  metadata: {
616
623
  turnSemantics: 'turn_complete',
617
624
  turnComplete: true,
625
+ replyBehavior: 'suppress_auto_reply',
618
626
  },
619
627
  }).catch(() => { });
620
628
  return;
621
629
  }
622
630
  const turnMetadata = normalizeTurnMetadata(input.message.metadata);
631
+ session.activeSelfContextId = selfContexts[0]?.id ?? null;
623
632
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
624
633
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
625
634
  const prompt = buildCanonPrompt({
@@ -627,18 +636,17 @@ export async function main() {
627
636
  conversationId: input.conversationId,
628
637
  participantContext,
629
638
  behavior,
630
- workSession: input.message.workSession,
631
- workSessions,
639
+ selfContexts,
632
640
  });
633
641
  if (session.running && deliveryIntent === 'interrupt') {
634
- enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths);
642
+ enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs);
635
643
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
636
644
  await session.adapter.interrupt().catch(() => { });
637
645
  clearStreaming(input.conversationId);
638
646
  client.setTyping(input.conversationId, false).catch(() => { });
639
647
  return;
640
648
  }
641
- enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths);
649
+ enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs);
642
650
  }
643
651
  async function runNextTurn(session) {
644
652
  if (session.running || session.closed)
@@ -668,6 +676,7 @@ export async function main() {
668
676
  throw new ExecutionEnvironmentError(modelGuard, modelGuard);
669
677
  }
670
678
  const turnImagePaths = nextTurn.imagePaths ?? [];
679
+ const turnMediaAddDirs = nextTurn.mediaAddDirs ?? [];
671
680
  const handleCodexEvent = (event) => {
672
681
  session.lastActivity = Date.now();
673
682
  if (event.type === 'thread.started') {
@@ -707,7 +716,7 @@ export async function main() {
707
716
  clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
708
717
  session.adapter.clearThreadId();
709
718
  };
710
- const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths);
719
+ const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths, turnMediaAddDirs);
711
720
  let result = await runTurnOnce();
712
721
  if (!result.interrupted
713
722
  && !result.finalMessage
@@ -726,6 +735,9 @@ export async function main() {
726
735
  clearStoredThread();
727
736
  }
728
737
  await client.sendMessage(session.conversationId, result.finalMessage, {
738
+ ...(session.activeSelfContextId
739
+ ? { selfContextId: session.activeSelfContextId }
740
+ : {}),
729
741
  metadata: {
730
742
  turnId: session.currentTurnId,
731
743
  turnSemantics: 'turn_complete',
@@ -744,6 +756,9 @@ export async function main() {
744
756
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
745
757
  }
746
758
  await client.sendMessage(session.conversationId, userVisibleError, {
759
+ ...(session.activeSelfContextId
760
+ ? { selfContextId: session.activeSelfContextId }
761
+ : {}),
747
762
  metadata: {
748
763
  turnId: session.currentTurnId,
749
764
  turnSemantics: 'turn_complete',
@@ -772,6 +787,9 @@ export async function main() {
772
787
  session.state.lastError = message;
773
788
  writeState(session);
774
789
  await client.sendMessage(session.conversationId, message, {
790
+ ...(session.activeSelfContextId
791
+ ? { selfContextId: session.activeSelfContextId }
792
+ : {}),
775
793
  metadata: {
776
794
  turnId: session.currentTurnId,
777
795
  turnSemantics: 'turn_complete',
@@ -968,7 +986,7 @@ export async function main() {
968
986
  senderName: message.senderName || message.senderId,
969
987
  isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
970
988
  behavior: payload.behavior,
971
- workSessions: payload.workSessions,
989
+ selfContexts: payload.selfContexts,
972
990
  });
973
991
  if (message.id) {
974
992
  saveRuntimeSessionState(runtimeId, {
@@ -1075,7 +1093,7 @@ export async function main() {
1075
1093
  senderName: latestMessage.senderId,
1076
1094
  isOwner: ownerId != null && latestMessage.senderId === ownerId,
1077
1095
  behavior: latestPage.behavior,
1078
- workSessions: latestPage.workSessions,
1096
+ selfContexts: latestPage.selfContexts,
1079
1097
  hydratedPage: latestPage,
1080
1098
  });
1081
1099
  if (latestMessage.id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
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.0",
33
- "@canonmsg/core": "^0.15.2"
32
+ "@canonmsg/agent-sdk": "^1.1.4",
33
+ "@canonmsg/core": "^0.15.4"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"