@gakim-digital/dexter-bridge 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local Companion bridge for the Dexter Framer plugin — runs Codex or Claude Code on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "dexter-bridge": "./bin/dexter-bridge.js"
7
+ "dexter-bridge": "bin/dexter-bridge.js"
8
8
  },
9
9
  "files": [
10
10
  "bin",
package/src/agent.js CHANGED
@@ -12,8 +12,11 @@ import {
12
12
  normalizeCompanionModelName,
13
13
  } from './config.js';
14
14
  import {
15
+ buildModelTurnDeltaPrompt,
16
+ buildModelTurnFallbackPrompt,
15
17
  buildModelTurnPrompt,
16
18
  extractJsonObject,
19
+ modelTurnOutputSchema,
17
20
  normalizeModelTurnCompletion,
18
21
  runSummary,
19
22
  } from './protocol.js';
@@ -27,6 +30,48 @@ import {
27
30
  } from './logger.js';
28
31
  import { createLocalAgentAdapter } from './providers/index.js';
29
32
 
33
+ const companionModelSessions = new Map();
34
+ const sharedProviderAdapters = new Map();
35
+ const MAX_COMPANION_MODEL_SESSIONS = 64;
36
+
37
+ function enabledFlag(value, fallback = false) {
38
+ const raw = String(value ?? '').trim().toLowerCase();
39
+ if (!raw) return fallback;
40
+ return !['0', 'false', 'no', 'off', 'disabled'].includes(raw);
41
+ }
42
+
43
+ function deltaModelSessionsEnabled(env = process.env) {
44
+ return enabledFlag(env.DEXTER_DELTA_MODEL_SESSIONS, true);
45
+ }
46
+
47
+ function companionSessionKey(run) {
48
+ return run?.modelTurn?.session?.sessionId || run?.turnId || run?.runId;
49
+ }
50
+
51
+ function rememberedCompanionSession(key) {
52
+ const session = key ? companionModelSessions.get(key) : null;
53
+ if (!session) return null;
54
+ companionModelSessions.delete(key);
55
+ companionModelSessions.set(key, session);
56
+ return session;
57
+ }
58
+
59
+ function rememberCompanionSession(key, session) {
60
+ if (!key) return;
61
+ companionModelSessions.delete(key);
62
+ companionModelSessions.set(key, session);
63
+ while (companionModelSessions.size > MAX_COMPANION_MODEL_SESSIONS) {
64
+ const oldest = companionModelSessions.keys().next().value;
65
+ if (!oldest) break;
66
+ companionModelSessions.delete(oldest);
67
+ }
68
+ }
69
+
70
+ function resumeFailure(error) {
71
+ const message = error?.message || String(error || '');
72
+ return /resume|session|thread|conversation|not found|unknown id|expired/i.test(message);
73
+ }
74
+
30
75
  export const AGENT_DEFINITIONS = {
31
76
  'claude-code': {
32
77
  id: 'claude-code',
@@ -150,7 +195,7 @@ function argsWithSelectedModel(args, modelDefinition, definition) {
150
195
  function argsWithStructuredOutput(args, definition, env = process.env) {
151
196
  if (!structuredUsageEnabled(env)) return args;
152
197
  if (definition.id === 'claude-code') {
153
- return argsIncludeFlag(args, '--output-format') ? args : [...args, '--output-format', 'json'];
198
+ return [...argsWithoutFlagValue(args, '--output-format'), '--output-format', 'json'];
154
199
  }
155
200
  if (definition.id === 'codex') {
156
201
  return argsIncludeFlag(args, '--json') ? args : [...args, '--json'];
@@ -158,6 +203,54 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
158
203
  return args;
159
204
  }
160
205
 
206
+ function argsWithClaudeIsolation(args, definition) {
207
+ if (definition.id !== 'claude-code') return args;
208
+ const isolated = argsWithoutVariadicFlag(args, '--tools');
209
+ isolated.push('--tools', '');
210
+ if (!argsIncludeFlag(isolated, '--disable-slash-commands')) isolated.push('--disable-slash-commands');
211
+ if (!argsIncludeFlag(isolated, '--safe-mode')) isolated.push('--safe-mode');
212
+ if (!argsIncludeFlag(isolated, '--strict-mcp-config')) isolated.push('--strict-mcp-config');
213
+ if (!argsIncludeFlag(isolated, '--no-chrome')) isolated.push('--no-chrome');
214
+ return isolated;
215
+ }
216
+
217
+ function argsWithoutFlagValue(args, flag) {
218
+ const filtered = [];
219
+ for (let index = 0; index < args.length; index += 1) {
220
+ const arg = args[index];
221
+ if (arg === flag) {
222
+ index += 1;
223
+ continue;
224
+ }
225
+ if (arg.startsWith(`${flag}=`)) continue;
226
+ filtered.push(arg);
227
+ }
228
+ return filtered;
229
+ }
230
+
231
+ function argsWithoutVariadicFlag(args, flag) {
232
+ const filtered = [];
233
+ for (let index = 0; index < args.length; index += 1) {
234
+ const arg = args[index];
235
+ if (arg === flag) {
236
+ while (index + 1 < args.length && !String(args[index + 1]).startsWith('-')) index += 1;
237
+ continue;
238
+ }
239
+ if (arg.startsWith(`${flag}=`)) continue;
240
+ filtered.push(arg);
241
+ }
242
+ return filtered;
243
+ }
244
+
245
+ function argsWithOutputSchema(args, definition, outputSchema) {
246
+ if (definition.id !== 'claude-code' || !outputSchema) return args;
247
+ return [
248
+ ...argsWithoutFlagValue(args, '--json-schema'),
249
+ '--json-schema',
250
+ JSON.stringify(outputSchema),
251
+ ];
252
+ }
253
+
161
254
  function argsWithPromptInput(args, definition) {
162
255
  if (definition.id !== 'codex' || args.includes('-')) return args;
163
256
  return [...args, '-'];
@@ -222,7 +315,9 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
222
315
  const requiredArgs = argsWithRequiredAgentFlags(baseArgs, definition);
223
316
  const modelArgs = argsWithSelectedModel(requiredArgs, modelDefinition, definition);
224
317
  const structuredArgs = argsWithStructuredOutput(modelArgs, definition, env);
225
- const resumedArgs = argsWithResumedSession(structuredArgs, definition, options.resumeSessionId);
318
+ const isolatedArgs = argsWithClaudeIsolation(structuredArgs, definition);
319
+ const schemaArgs = argsWithOutputSchema(isolatedArgs, definition, options.outputSchema);
320
+ const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
226
321
  return argsWithPromptInput(resumedArgs, definition);
227
322
  }
228
323
 
@@ -738,6 +833,7 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
738
833
  const modelDefinition = companionModelDefinition(options.model, definition.id);
739
834
  const args = buildAgentArgs(definition, modelDefinition, process.env, {
740
835
  resumeSessionId: options.resumeSessionId,
836
+ outputSchema: options.outputSchema,
741
837
  });
742
838
  const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
743
839
  options.trace?.info('agent_step_invoke', {
@@ -781,7 +877,20 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
781
877
  ? requestedModel.trim()
782
878
  : selectedModel?.id || definition.model;
783
879
  const adapter = options.providerAdapter || null;
784
- const prompt = buildModelTurnPrompt(run.modelTurn);
880
+ const sessionKey = companionSessionKey(run);
881
+ const rememberedSession = rememberedCompanionSession(sessionKey);
882
+ const deltaRequested =
883
+ deltaModelSessionsEnabled(options.env)
884
+ && run?.modelTurn?.session?.contextMode === 'delta';
885
+ let callContextMode = deltaRequested && rememberedSession ? 'delta' : 'full';
886
+ let prompt = callContextMode === 'delta'
887
+ ? buildModelTurnDeltaPrompt(run.modelTurn)
888
+ : deltaRequested
889
+ ? buildModelTurnFallbackPrompt(run.modelTurn)
890
+ : buildModelTurnPrompt(run.modelTurn);
891
+ const callStartedAt = Date.now();
892
+ let providerSessionId;
893
+ let fallbackAfterResumeFailure = false;
785
894
  const statusResponse = await send('status', {
786
895
  stage: 'model_turn',
787
896
  message: `${definition.label} is generating the next Dexter action.`,
@@ -791,10 +900,39 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
791
900
  throw new CompanionRunCancelledError();
792
901
  }
793
902
  const usageAccumulator = createCompanionUsageAccumulator(definition.id);
903
+ const failedModelCall = (error) => {
904
+ const failure = error instanceof Error
905
+ ? error
906
+ : new Error(String(error || 'The companion model call failed.'));
907
+ usageAccumulator.add(failure.companionUsage || {});
908
+ const snapshot = usageAccumulator.snapshot();
909
+ failure.companionUsage = {
910
+ ...snapshot,
911
+ modelCalls: [{
912
+ callId: run.runId,
913
+ step: Number(run?.modelTurn?.step ?? 0),
914
+ model: selectedModelId,
915
+ status: 'failed',
916
+ durationMs: Date.now() - callStartedAt,
917
+ promptChars: prompt.length,
918
+ requestedContextMode: deltaRequested ? 'delta' : 'full',
919
+ contextMode: callContextMode,
920
+ resumed: callContextMode === 'delta',
921
+ resumeSessionAvailable: Boolean(rememberedSession),
922
+ fallbackAfterResumeFailure,
923
+ ...snapshot.tokenUsage,
924
+ estimatedCostUSD: snapshot.estimatedCostUSD,
925
+ }],
926
+ };
927
+ return failure;
928
+ };
794
929
  let resultText;
795
930
  if (adapter) {
796
- const sessionId = adapterSessionId(run);
931
+ const sessionId = sessionKey || adapterSessionId(run);
797
932
  const invocationModel = adapterInvocationModel(run, selectedModel, definition.id);
933
+ if (callContextMode === 'full' && rememberedSession) {
934
+ await adapter.resetSession?.(sessionId);
935
+ }
798
936
  options.trace?.info('agent_adapter_invoke', {
799
937
  adapter: adapter.id,
800
938
  agent: definition.id,
@@ -803,17 +941,19 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
803
941
  invocationModel,
804
942
  promptChars: prompt.length,
805
943
  });
806
- const result = await callProviderAdapter(adapter, {
944
+ const adapterInput = {
807
945
  runId: run.runId,
808
946
  sessionId,
809
947
  prompt,
810
948
  model: invocationModel,
949
+ outputSchema: modelTurnOutputSchema(run.modelTurn),
811
950
  timeoutMs: boundedDurationMs(
812
951
  options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS,
813
952
  120000,
814
953
  1000,
815
954
  ),
816
- }, {
955
+ };
956
+ const adapterOptions = {
817
957
  send,
818
958
  trace: options.trace,
819
959
  controlPollMs: boundedDurationMs(
@@ -822,9 +962,30 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
822
962
  10,
823
963
  30000,
824
964
  ),
825
- });
965
+ };
966
+ let result;
967
+ try {
968
+ result = await callProviderAdapter(adapter, adapterInput, adapterOptions);
969
+ } catch (error) {
970
+ if (callContextMode !== 'delta' || !resumeFailure(error)) throw failedModelCall(error);
971
+ usageAccumulator.add(error?.companionUsage || {});
972
+ fallbackAfterResumeFailure = true;
973
+ callContextMode = 'full';
974
+ prompt = buildModelTurnFallbackPrompt(run.modelTurn);
975
+ await adapter.resetSession?.(sessionId);
976
+ options.trace?.warn('agent_adapter_resume_fallback', {
977
+ sessionId,
978
+ error: errorMeta(error),
979
+ fallbackPromptChars: prompt.length,
980
+ });
981
+ result = await callProviderAdapter(adapter, { ...adapterInput, prompt }, adapterOptions)
982
+ .catch((error) => {
983
+ throw failedModelCall(error);
984
+ });
985
+ }
826
986
  usageAccumulator.add(result);
827
987
  resultText = result?.text;
988
+ providerSessionId = result?.threadId || result?.sessionId;
828
989
  } else {
829
990
  const runtime = await resolveAgentRuntime(definition, selectedModel, {
830
991
  env: options.env,
@@ -832,25 +993,90 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
832
993
  if (!runtime.ok) {
833
994
  throw new Error(runtime.error || `${definition.label} is not available.`);
834
995
  }
835
- const result = await callLocalJsonAgent(definition.id, prompt, {
836
- ...options,
837
- model: selectedModelId,
838
- runtime,
839
- });
996
+ const resumeSessionId =
997
+ callContextMode === 'delta'
998
+ && agentSessionResumeEnabled(definition, options.env)
999
+ ? rememberedSession?.providerSessionId
1000
+ : undefined;
1001
+ let result;
1002
+ try {
1003
+ result = await callLocalJsonAgent(definition.id, prompt, {
1004
+ ...options,
1005
+ model: selectedModelId,
1006
+ runtime,
1007
+ resumeSessionId,
1008
+ outputSchema: modelTurnOutputSchema(run.modelTurn),
1009
+ });
1010
+ } catch (error) {
1011
+ if (callContextMode !== 'delta' || !resumeFailure(error)) throw failedModelCall(error);
1012
+ fallbackAfterResumeFailure = true;
1013
+ callContextMode = 'full';
1014
+ prompt = buildModelTurnFallbackPrompt(run.modelTurn);
1015
+ options.trace?.warn('agent_cli_resume_fallback', {
1016
+ resumeSessionId,
1017
+ error: errorMeta(error),
1018
+ fallbackPromptChars: prompt.length,
1019
+ });
1020
+ result = await callLocalJsonAgent(definition.id, prompt, {
1021
+ ...options,
1022
+ model: selectedModelId,
1023
+ runtime,
1024
+ resumeSessionId: undefined,
1025
+ outputSchema: modelTurnOutputSchema(run.modelTurn),
1026
+ }).catch((error) => {
1027
+ throw failedModelCall(error);
1028
+ });
1029
+ }
840
1030
  const parsed = parseAgentOutput(definition.id, result.stdout);
841
1031
  usageAccumulator.add(parsed);
842
1032
  resultText = parsed.resultText;
1033
+ providerSessionId = parsed.sessionId;
1034
+ }
1035
+ if (providerSessionId || adapter) {
1036
+ rememberCompanionSession(sessionKey, {
1037
+ providerSessionId: providerSessionId || rememberedSession?.providerSessionId || sessionKey,
1038
+ agent: definition.id,
1039
+ updatedAt: Date.now(),
1040
+ });
1041
+ }
1042
+ const usageSnapshot = usageAccumulator.snapshot();
1043
+ const callTelemetry = {
1044
+ callId: run.runId,
1045
+ step: Number(run?.modelTurn?.step ?? 0),
1046
+ model: selectedModelId,
1047
+ durationMs: Date.now() - callStartedAt,
1048
+ promptChars: prompt.length,
1049
+ requestedContextMode: deltaRequested ? 'delta' : 'full',
1050
+ contextMode: callContextMode,
1051
+ resumed: callContextMode === 'delta',
1052
+ resumeSessionAvailable: Boolean(rememberedSession),
1053
+ fallbackAfterResumeFailure,
1054
+ ...usageSnapshot.tokenUsage,
1055
+ estimatedCostUSD: usageSnapshot.estimatedCostUSD,
1056
+ };
1057
+ const usage = {
1058
+ ...usageSnapshot,
1059
+ modelCalls: [{ ...callTelemetry, status: 'succeeded' }],
1060
+ };
1061
+ let completion;
1062
+ try {
1063
+ completion = normalizeModelTurnCompletion(
1064
+ extractJsonObject(resultText),
1065
+ selectedModelId,
1066
+ );
1067
+ } catch (error) {
1068
+ error.companionUsage = {
1069
+ ...usage,
1070
+ modelCalls: [{ ...callTelemetry, status: 'rejected' }],
1071
+ };
1072
+ throw error;
843
1073
  }
844
- const completion = normalizeModelTurnCompletion(
845
- extractJsonObject(resultText),
846
- selectedModelId,
847
- );
848
1074
  await send('done', {
849
1075
  operationType: 'chat',
850
1076
  outcome: 'answer',
851
1077
  completion,
852
1078
  model: selectedModelId,
853
- ...usageAccumulator.snapshot(),
1079
+ ...usage,
854
1080
  });
855
1081
  }
856
1082
 
@@ -885,14 +1111,26 @@ export async function executeRun(run, {
885
1111
  const runModel = run?.companion?.model?.id || run?.model || selectedModel;
886
1112
  const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
887
1113
  const adapterProvided = providerAdapter !== undefined;
888
- const activeAdapter = adapterProvided
889
- ? providerAdapter
890
- : createLocalAgentAdapter(normalizedAgent, {
1114
+ const shareAdapter = !adapterProvided && deltaModelSessionsEnabled(env);
1115
+ let activeAdapter = adapterProvided ? providerAdapter : null;
1116
+ if (!adapterProvided && shareAdapter) {
1117
+ activeAdapter = sharedProviderAdapters.get(normalizedAgent) || null;
1118
+ if (!activeAdapter) {
1119
+ activeAdapter = createLocalAgentAdapter(normalizedAgent, {
1120
+ ...adapterOptions,
1121
+ env: adapterOptions?.env || env,
1122
+ trace,
1123
+ });
1124
+ if (activeAdapter) sharedProviderAdapters.set(normalizedAgent, activeAdapter);
1125
+ }
1126
+ } else if (!adapterProvided) {
1127
+ activeAdapter = createLocalAgentAdapter(normalizedAgent, {
891
1128
  ...adapterOptions,
892
1129
  env: adapterOptions?.env || env,
893
1130
  trace,
894
1131
  });
895
- const ownsAdapter = !adapterProvided && Boolean(activeAdapter);
1132
+ }
1133
+ const ownsAdapter = !adapterProvided && !shareAdapter && Boolean(activeAdapter);
896
1134
  if (
897
1135
  activeAdapter?.id
898
1136
  && activeAdapter.id !== normalizedAgent
@@ -945,6 +1183,12 @@ export async function executeRun(run, {
945
1183
  }
946
1184
  }
947
1185
 
1186
+ export function __resetAgentSessionsForTests() {
1187
+ companionModelSessions.clear();
1188
+ for (const adapter of sharedProviderAdapters.values()) adapter?.close?.();
1189
+ sharedProviderAdapters.clear();
1190
+ }
1191
+
948
1192
  export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
949
1193
  return runProcess(command, args, '', { timeoutMs })
950
1194
  .then((result) => ({ ok: true, command, output: (result.stdout || result.stderr || '').trim() }))
@@ -92,7 +92,13 @@ export function parseClaudeOutput(stdout) {
92
92
  };
93
93
  }
94
94
 
95
- const hasEnvelope = typeof parsed.result === 'string' || parsed.usage || parsed.total_cost_usd !== undefined || parsed.modelUsage;
95
+ const structuredOutput = parsed.structured_output ?? parsed.structuredOutput;
96
+ const hasEnvelope =
97
+ typeof parsed.result === 'string'
98
+ || structuredOutput !== undefined
99
+ || parsed.usage
100
+ || parsed.total_cost_usd !== undefined
101
+ || parsed.modelUsage;
96
102
  if (!hasEnvelope) {
97
103
  return {
98
104
  resultText: String(stdout || '').trim(),
@@ -108,7 +114,13 @@ export function parseClaudeOutput(stdout) {
108
114
  const modelUsage = normalizeModelUsage(parsed.modelUsage || parsed.model_usage);
109
115
  const estimatedCostUSD = optionalNonNegativeNumber(parsed.total_cost_usd ?? parsed.totalCostUsd);
110
116
  return {
111
- resultText: typeof parsed.result === 'string' ? parsed.result.trim() : '',
117
+ resultText: structuredOutput && typeof structuredOutput === 'object'
118
+ ? JSON.stringify(structuredOutput)
119
+ : typeof structuredOutput === 'string'
120
+ ? structuredOutput.trim()
121
+ : typeof parsed.result === 'string'
122
+ ? parsed.result.trim()
123
+ : '',
112
124
  tokenUsage,
113
125
  model: typeof parsed.model === 'string' ? parsed.model : undefined,
114
126
  sessionId: typeof parsed.session_id === 'string'
package/src/config.js CHANGED
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
5
  export const DEFAULT_API_BASE_URL = 'http://localhost:3800/iwm-api/0.0.1';
6
- export const BRIDGE_VERSION = '0.5.0';
6
+ export const BRIDGE_VERSION = '0.5.1';
7
7
  export const BRIDGE_CAPABILITIES = [
8
8
  'model-turn-v1',
9
9
  ];
package/src/protocol.js CHANGED
@@ -71,16 +71,18 @@ export function compactToolCatalog(tools = []) {
71
71
  .filter((tool) => tool.name);
72
72
  }
73
73
 
74
- export function buildModelTurnPrompt(modelTurn = {}) {
75
- const messages = Array.isArray(modelTurn.messages)
76
- ? modelTurn.messages.slice(-64).map((message) => ({
74
+ function compactMessages(messages = [], limit = 64) {
75
+ return Array.isArray(messages)
76
+ ? messages.slice(-limit).map((message) => ({
77
77
  role: message?.role,
78
78
  content: compactModelTurnContent(message?.content),
79
79
  toolCalls: message?.toolCalls,
80
80
  toolCallId: message?.toolCallId,
81
81
  }))
82
82
  : [];
83
- const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
83
+ }
84
+
85
+ function modelTurnInstructions() {
84
86
  return [
85
87
  'You are the model engine for Dexter. The server owns the agent loop and executes all tools.',
86
88
  'Return exactly one JSON object and no markdown.',
@@ -89,13 +91,49 @@ export function buildModelTurnPrompt(modelTurn = {}) {
89
91
  'Each toolCalls[].arguments value must be a JSON-encoded string whose decoded value is an object.',
90
92
  'Use only tools listed below. Do not claim a tool executed; only request it.',
91
93
  'When the task is complete, return toolCalls:[] and finishReason:"stop".',
92
- '',
93
- `Messages:\n${JSON.stringify(messages)}`,
94
+ ];
95
+ }
96
+
97
+ export function buildModelTurnPrompt(modelTurn = {}) {
98
+ const messages = compactMessages(modelTurn.messages, 64);
99
+ const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
100
+ return [
101
+ ...modelTurnInstructions(),
94
102
  '',
95
103
  `Tools:\n${JSON.stringify(tools)}`,
104
+ '',
105
+ `Messages:\n${JSON.stringify(messages)}`,
96
106
  ].join('\n');
97
107
  }
98
108
 
109
+ export function buildModelTurnDeltaPrompt(modelTurn = {}) {
110
+ const messages = compactMessages(modelTurn.messages, 24);
111
+ const includeTools = modelTurn?.session?.toolCatalogChanged === true;
112
+ const tools = includeTools
113
+ ? compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : [])
114
+ : [];
115
+ return [
116
+ 'Continue the existing Dexter model session. The server has already supplied the doctrine, goal, prior messages, and tool catalog.',
117
+ 'Apply only the new canonical messages/state changes below.',
118
+ 'Return exactly one JSON object using the previously established response contract.',
119
+ ...(includeTools ? ['', `Updated tools:\n${JSON.stringify(tools)}`] : []),
120
+ '',
121
+ `New messages:\n${JSON.stringify(messages)}`,
122
+ modelTurn?.session?.lastStateDigest
123
+ ? `State digest: ${modelTurn.session.lastStateDigest}`
124
+ : '',
125
+ ].filter(Boolean).join('\n');
126
+ }
127
+
128
+ export function buildModelTurnFallbackPrompt(modelTurn = {}) {
129
+ return buildModelTurnPrompt({
130
+ ...modelTurn,
131
+ messages: Array.isArray(modelTurn.fallbackMessages)
132
+ ? modelTurn.fallbackMessages
133
+ : modelTurn.messages,
134
+ });
135
+ }
136
+
99
137
  export function modelTurnOutputSchema(modelTurn = {}) {
100
138
  const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
101
139
  const toolNames = tools.map((tool) => tool.name);
@@ -1,5 +1,6 @@
1
1
  import { createJsonRpcClient } from './jsonRpcClient.js';
2
2
  import { normalizeCompanionTokenUsage } from '../agentOutput.js';
3
+ import { BRIDGE_VERSION } from '../config.js';
3
4
 
4
5
  /**
5
6
  * Codex App Server adapter — the primary local-agent path.
@@ -20,7 +21,7 @@ import { normalizeCompanionTokenUsage } from '../agentOutput.js';
20
21
  const CLIENT_INFO = {
21
22
  name: 'dexter_bridge',
22
23
  title: 'Dexter Bridge',
23
- version: '0.5.0',
24
+ version: BRIDGE_VERSION,
24
25
  };
25
26
 
26
27
  export const CODEX_APP_SERVER_METHODS = {
@@ -382,10 +383,12 @@ export function createCodexAppServerAdapter({
382
383
  if (event.method === CODEX_APP_SERVER_NOTIFICATIONS.turnCompleted) {
383
384
  const turn = event.params?.turn || {};
384
385
  if (turn.status === 'failed') {
385
- finish(reject, new Error(codexTurnErrorMessage(
386
+ const error = new Error(codexTurnErrorMessage(
386
387
  turn,
387
388
  'Codex turn failed without an error message.',
388
- )));
389
+ ));
390
+ error.companionUsage = normalizeUsage(turn);
391
+ finish(reject, error);
389
392
  return;
390
393
  }
391
394
  finish(resolve, {
@@ -424,6 +427,18 @@ export function createCodexAppServerAdapter({
424
427
  }
425
428
  }
426
429
 
430
+ async function resetSession(sessionId) {
431
+ const threadId = threadsByRun.get(sessionId);
432
+ if (!threadId) return;
433
+ threadsByRun.delete(sessionId);
434
+ if (!client || client.closed) return;
435
+ await client.request(
436
+ CODEX_APP_SERVER_METHODS.threadUnsubscribe,
437
+ { threadId },
438
+ { timeoutMs: 10000 },
439
+ ).catch(() => undefined);
440
+ }
441
+
427
442
  async function logout() {
428
443
  if (!client || client.closed) return;
429
444
  try {
@@ -451,6 +466,7 @@ export function createCodexAppServerAdapter({
451
466
  usage,
452
467
  runModelTurn,
453
468
  cancel,
469
+ resetSession,
454
470
  logout,
455
471
  close,
456
472
  };