@canonmsg/codex-plugin 0.18.4 → 0.18.5

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/host.d.ts CHANGED
@@ -1,4 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ interface HostSessionState {
3
+ lastError?: string;
4
+ model?: string;
5
+ permissionMode?: string;
6
+ effort?: string;
7
+ /** Tokens consumed by the latest completed turn; Codex reports no context window. */
8
+ contextUsage?: {
9
+ totalTokens: number;
10
+ };
11
+ state: 'idle' | 'running';
12
+ }
13
+ export declare function buildCodexInitialSessionState(input: {
14
+ model?: string;
15
+ permissionMode?: string;
16
+ effort?: string | null;
17
+ }): HostSessionState;
2
18
  /** GPT reasoning-effort levels, applied next turn via model_reasoning_effort. */
3
19
  export declare const CODEX_EFFORT_OPTIONS: readonly [{
4
20
  readonly value: "minimal";
@@ -17,3 +33,4 @@ export declare const CODEX_EFFORT_OPTIONS: readonly [{
17
33
  readonly label: "Extra high";
18
34
  }];
19
35
  export declare function main(): Promise<void>;
36
+ export {};
package/dist/host.js CHANGED
@@ -5,8 +5,8 @@ import { spawnSync } from 'node:child_process';
5
5
  import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
8
- import { buildTurnArtifactContextLine, ensureTurnArtifactDir, flushTurnArtifacts, } from '@canonmsg/coding-agent-host';
9
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, 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, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
8
+ import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
9
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, 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, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
10
  import { decideAutoReply, } from './inbound-policy.js';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
12
12
  import { CodexAppServerAdapter } from './app-server-adapter.js';
@@ -53,6 +53,14 @@ Keep this terminal open while you want Canon to reach the agent. Closing it,
53
53
  logging out, rebooting, or sleeping long enough to stop the process takes the
54
54
  local agent offline until you revive it. Docs:
55
55
  https://canonmail.com/agents/integrations`;
56
+ export function buildCodexInitialSessionState(input) {
57
+ return {
58
+ model: input.model,
59
+ permissionMode: input.permissionMode,
60
+ ...(input.effort ? { effort: input.effort } : {}),
61
+ state: 'idle',
62
+ };
63
+ }
56
64
  const MAX_SESSIONS = 12;
57
65
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
58
66
  const HEARTBEAT_MS = 30_000;
@@ -248,7 +256,7 @@ function resolveCodexEffectiveRuntimePolicy(input) {
248
256
  };
249
257
  }
250
258
  function buildCanonPrompt(input) {
251
- return renderCanonTurnBriefPrompt(buildCanonTurnContextV2({
259
+ return renderCodingHostInboundPrompt(buildCanonInboundFrameV1(buildCanonTurnContextV2({
252
260
  content: input.content,
253
261
  conversationId: input.conversationId,
254
262
  participantContext: input.participantContext,
@@ -257,9 +265,8 @@ function buildCanonPrompt(input) {
257
265
  activeSelfContextId: input.activeSelfContextId,
258
266
  provenance: input.provenance,
259
267
  replyContext: input.replyContext,
260
- sessionContextLines: input.sessionContextLines,
261
268
  message: input.message,
262
- }));
269
+ })));
263
270
  }
264
271
  function renderInboundContent(message, materialized) {
265
272
  return renderCanonHostInboundContent(message, materialized);
@@ -867,11 +874,11 @@ export async function main() {
867
874
  adapter,
868
875
  queue: [],
869
876
  running: false,
870
- state: {
877
+ state: buildCodexInitialSessionState({
871
878
  model: policy.model,
872
879
  permissionMode: policy.permissionMode,
873
- state: 'idle',
874
- },
880
+ effort: initialEffort,
881
+ }),
875
882
  policyFingerprint: policy.fingerprint,
876
883
  turnState: 'idle',
877
884
  currentTurnId: null,
@@ -907,8 +914,17 @@ export async function main() {
907
914
  pendingSessionCreations.delete(conversationId);
908
915
  }
909
916
  }
910
- function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false) {
911
- const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths, mediaAddDirs, planMode };
917
+ function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled') {
918
+ const nextPrompt = {
919
+ prompt,
920
+ intent,
921
+ sourceMessageId,
922
+ markAccepted,
923
+ imagePaths,
924
+ mediaAddDirs,
925
+ planMode,
926
+ artifactRoutingMode,
927
+ };
912
928
  if (toFront) {
913
929
  session.queue.unshift(nextPrompt);
914
930
  }
@@ -919,6 +935,11 @@ export async function main() {
919
935
  writeTurn(session);
920
936
  void runNextTurn(session);
921
937
  }
938
+ function resolveArtifactRoutingMode(participantContext) {
939
+ return participantContext.conversationType === 'direct' && participantContext.isOwner
940
+ ? 'workspace-generated'
941
+ : 'disabled';
942
+ }
922
943
  async function waitForRuntimeInputResponse(input) {
923
944
  while (Date.now() < input.expiresAt) {
924
945
  const response = await client.consumeRuntimeInputResponse({
@@ -1291,43 +1312,50 @@ export async function main() {
1291
1312
  message: input.message,
1292
1313
  });
1293
1314
  if (session.running && deliveryIntent === 'interrupt') {
1294
- enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
1315
+ const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
1316
+ enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
1295
1317
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
1296
1318
  await session.adapter.interrupt().catch(() => { });
1297
1319
  clearStreaming(input.conversationId);
1298
1320
  typingSignals.clear(input.conversationId).catch(() => { });
1299
1321
  return;
1300
1322
  }
1301
- enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
1323
+ const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
1324
+ enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
1302
1325
  }
1303
- async function routeSessionArtifacts(session, artifactDir) {
1304
- if (!artifactDir)
1326
+ function sendTurnArtifactFile(session, file) {
1327
+ return sendMediaFileMessage(client, session.conversationId, file.path, '', {
1328
+ ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1329
+ metadata: {
1330
+ ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
1331
+ // Media is permanent conversation content (promoted like a final
1332
+ // reply) but must never re-trigger other agents — the host's own
1333
+ // final text message stays the only turn-complete trigger.
1334
+ turnSemantics: 'turn_complete',
1335
+ replyBehavior: 'suppress_auto_reply',
1336
+ },
1337
+ });
1338
+ }
1339
+ async function routeWorkspaceGeneratedArtifacts(session, baseline) {
1340
+ if (!baseline)
1305
1341
  return;
1306
1342
  const logPrefix = `[canon-codex] [${session.conversationId.slice(0, 8)}]`;
1307
1343
  try {
1308
- const result = await flushTurnArtifacts({
1309
- artifactDir,
1310
- send: (file) => sendMediaFileMessage(client, session.conversationId, file.path, '', {
1311
- ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1312
- metadata: {
1313
- ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
1314
- // Media is permanent conversation content (promoted like a final
1315
- // reply) but must never re-trigger other agents — the host's own
1316
- // final text message stays the only turn-complete trigger.
1317
- turnSemantics: 'turn_complete',
1318
- replyBehavior: 'suppress_auto_reply',
1319
- },
1320
- }),
1344
+ const result = await collectTurnArtifacts({
1345
+ cwd: session.cwd,
1346
+ baseline,
1321
1347
  });
1322
- for (const entry of result.sent) {
1323
- console.error(`${logPrefix} Routed artifact ${entry.file.fileName} (${entry.messageId})`
1324
- + (entry.removeFailed ? '; removal failed, so it may resend later' : ''));
1325
- }
1326
- for (const failure of result.failed) {
1327
- console.error(`${logPrefix} Artifact upload failed for ${failure.file.fileName}; left in place: ${failure.error}`);
1348
+ for (const file of result.files) {
1349
+ try {
1350
+ const { messageId } = await sendTurnArtifactFile(session, file);
1351
+ console.error(`${logPrefix} Routed generated artifact ${file.relativePath ?? file.fileName} (${messageId})`);
1352
+ }
1353
+ catch (error) {
1354
+ console.error(`${logPrefix} Artifact upload failed for ${file.relativePath ?? file.fileName}: ${error instanceof Error ? error.message : String(error)}`);
1355
+ }
1328
1356
  }
1329
1357
  for (const skipped of result.skipped) {
1330
- if (skipped.reason === 'too-large' || skipped.reason === 'file-cap' || skipped.reason === 'unsupported') {
1358
+ if (skipped.reason === 'too-large' || skipped.reason === 'file-cap' || skipped.reason === 'scan-limit') {
1331
1359
  console.error(`${logPrefix} Artifact skipped ${skipped.fileName} (${skipped.reason})`);
1332
1360
  }
1333
1361
  }
@@ -1361,19 +1389,26 @@ export async function main() {
1361
1389
  // Status-only seed: 'thinking' renders as a working filament row on the
1362
1390
  // clients; text here would be bubbled as speech (v4 register rule).
1363
1391
  writeCodexStreaming(session, '', 'thinking');
1364
- let artifactDir = null;
1392
+ let artifactBaseline = null;
1365
1393
  let artifactsRouted = false;
1366
1394
  const routeArtifactsOnce = async () => {
1367
1395
  if (artifactsRouted)
1368
1396
  return;
1369
1397
  artifactsRouted = true;
1370
- await routeSessionArtifacts(session, artifactDir);
1398
+ if (nextTurn.artifactRoutingMode === 'workspace-generated') {
1399
+ await routeWorkspaceGeneratedArtifacts(session, artifactBaseline);
1400
+ }
1371
1401
  };
1372
1402
  try {
1373
1403
  const turnId = session.currentTurnId ?? randomUUID();
1374
1404
  session.currentTurnId = turnId;
1375
- artifactDir = await ensureTurnArtifactDir(session.cwd, turnId);
1376
- const turnPrompt = `${nextTurn.prompt}\n\n${buildTurnArtifactContextLine(artifactDir)}`;
1405
+ let turnPrompt = nextTurn.prompt;
1406
+ if (nextTurn.artifactRoutingMode === 'workspace-generated') {
1407
+ artifactBaseline = await captureTurnArtifactSnapshot({ cwd: session.cwd }).catch((error) => {
1408
+ console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Artifact snapshot failed:`, error instanceof Error ? error.message : error);
1409
+ return null;
1410
+ });
1411
+ }
1377
1412
  const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
1378
1413
  if (modelGuard) {
1379
1414
  throw new ExecutionEnvironmentError(modelGuard, modelGuard);
@@ -1824,7 +1859,7 @@ export async function main() {
1824
1859
  {
1825
1860
  id: 'mediaOut',
1826
1861
  label: 'Media out',
1827
- value: 'Explicit turn artifact directory',
1862
+ value: 'Generated media artifacts',
1828
1863
  },
1829
1864
  ],
1830
1865
  execution: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,8 +30,8 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@canonmsg/agent-sdk": "^3.2.3",
33
- "@canonmsg/coding-agent-host": "^0.2.0",
34
- "@canonmsg/core": "^2.6.0"
33
+ "@canonmsg/coding-agent-host": "^0.2.2",
34
+ "@canonmsg/core": "^2.8.0"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"