remote-codex 0.1.9 → 0.11.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.
Files changed (45) hide show
  1. package/apps/supervisor-api/dist/index.js +11942 -6101
  2. package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-BFD4Ytvg.js → highlighted-body-OFNGDK62-ChrwAL9u.js} +1 -1
  3. package/apps/supervisor-web/dist/assets/index-DHf2HOXx.js +381 -0
  4. package/apps/supervisor-web/dist/assets/index-DpWxXCgt.css +32 -0
  5. package/apps/supervisor-web/dist/assets/{xterm-CukFWbxr.js → xterm-D4sevve4.js} +1 -1
  6. package/apps/supervisor-web/dist/index.html +2 -2
  7. package/config/codex-model-pricing.json +63 -0
  8. package/package.json +5 -2
  9. package/packages/agent-runtime/src/index.ts +4 -0
  10. package/packages/agent-runtime/src/management-errors.ts +11 -0
  11. package/packages/agent-runtime/src/model-pricing.ts +312 -0
  12. package/packages/agent-runtime/src/registry.ts +19 -4
  13. package/packages/agent-runtime/src/runtime-errors.ts +97 -0
  14. package/packages/agent-runtime/src/types.ts +50 -4
  15. package/packages/agent-runtime/src/unavailable-runtime.ts +169 -0
  16. package/packages/claude/src/historyItems.ts +693 -0
  17. package/packages/claude/src/index.ts +2 -0
  18. package/packages/claude/src/runtimeAdapter.test.ts +2138 -0
  19. package/packages/claude/src/runtimeAdapter.ts +2145 -0
  20. package/packages/codex/src/appServerManager.ts +12 -3
  21. package/packages/codex/src/historyItems.test.ts +110 -0
  22. package/packages/codex/src/historyItems.ts +97 -16
  23. package/packages/codex/src/hookHistory.test.ts +59 -0
  24. package/packages/codex/src/index.ts +7 -0
  25. package/packages/codex/src/local-session-store.ts +390 -0
  26. package/packages/codex/src/management/codex-management-service.ts +454 -0
  27. package/packages/codex/src/management/codexHostConfig.test.ts +88 -0
  28. package/packages/codex/src/management/codexHostConfig.ts +188 -0
  29. package/packages/codex/src/management/errors.ts +20 -0
  30. package/packages/codex/src/modelPricing.test.ts +184 -0
  31. package/packages/codex/src/modelPricing.ts +9 -0
  32. package/packages/codex/src/runtime-errors.test.ts +72 -0
  33. package/packages/codex/src/runtime-errors.ts +37 -0
  34. package/packages/codex/src/runtimeAdapter.ts +25 -2
  35. package/packages/codex/src/thread-title.ts +1 -0
  36. package/packages/db/src/repositories.ts +30 -0
  37. package/packages/opencode/src/historyItems.test.ts +504 -0
  38. package/packages/opencode/src/historyItems.ts +896 -0
  39. package/packages/opencode/src/index.ts +2 -0
  40. package/packages/opencode/src/runtimeAdapter.test.ts +1355 -0
  41. package/packages/opencode/src/runtimeAdapter.ts +1469 -0
  42. package/packages/shared/src/agent-providers.ts +56 -0
  43. package/packages/shared/src/index.ts +174 -35
  44. package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +0 -32
  45. package/apps/supervisor-web/dist/assets/index-Rd2EBQac.js +0 -377
@@ -0,0 +1,188 @@
1
+ import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ export type CodexServiceTier = 'fast' | null;
6
+ const SERVICE_TIER_LINE_GLOBAL_PATTERN =
7
+ /^\s*service_tier\s*=\s*("fast"|"flex"|'fast'|'flex').*\n?/gm;
8
+
9
+ function resolveCodexConfigPath(codexHome: string) {
10
+ return path.join(codexHome, 'config.toml');
11
+ }
12
+
13
+ export function parseCodexServiceTier(content: string): CodexServiceTier {
14
+ const match = content.match(
15
+ /^\s*service_tier\s*=\s*["'](?<tier>fast)["']\s*$/m,
16
+ );
17
+ const tier = match?.groups?.tier;
18
+ return tier === 'fast' ? tier : null;
19
+ }
20
+
21
+ export function isFastModeEnabledFromConfig(content: string) {
22
+ return parseCodexServiceTier(content) === 'fast';
23
+ }
24
+
25
+ export function readCodexFastModeSync(codexHome: string) {
26
+ try {
27
+ const content = fs.readFileSync(resolveCodexConfigPath(codexHome), 'utf8');
28
+ return isFastModeEnabledFromConfig(content);
29
+ } catch (error) {
30
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
31
+ return false;
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ export function upsertCodexServiceTier(content: string, enabled: boolean) {
38
+ const normalized = content.replace(/\r\n/g, '\n');
39
+ const withoutServiceTier = normalized.replace(
40
+ SERVICE_TIER_LINE_GLOBAL_PATTERN,
41
+ '',
42
+ );
43
+
44
+ if (!enabled) {
45
+ return withoutServiceTier.replace(/\n{3,}/g, '\n\n').trimEnd()
46
+ + (withoutServiceTier.trim() ? '\n' : '');
47
+ }
48
+
49
+ const nextLine = 'service_tier = "fast"';
50
+ if (!withoutServiceTier.trim()) {
51
+ return `${nextLine}\n`;
52
+ }
53
+
54
+ const firstSectionMatch = withoutServiceTier.match(/^\s*\[[^\]]+\]/m);
55
+ if (!firstSectionMatch || firstSectionMatch.index === undefined) {
56
+ return withoutServiceTier.endsWith('\n')
57
+ ? `${withoutServiceTier}${nextLine}\n`
58
+ : `${withoutServiceTier}\n${nextLine}\n`;
59
+ }
60
+
61
+ const beforeFirstSection = withoutServiceTier.slice(0, firstSectionMatch.index).trimEnd();
62
+ const afterFirstSection = withoutServiceTier
63
+ .slice(firstSectionMatch.index)
64
+ .replace(/^\n+/, '');
65
+ return beforeFirstSection
66
+ ? `${beforeFirstSection}\n${nextLine}\n${afterFirstSection}`
67
+ : `${nextLine}\n${afterFirstSection}`;
68
+ }
69
+
70
+ export async function writeCodexFastMode(codexHome: string, enabled: boolean) {
71
+ const configPath = resolveCodexConfigPath(codexHome);
72
+ let current = '';
73
+ try {
74
+ current = await fsp.readFile(configPath, 'utf8');
75
+ } catch (error) {
76
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ const next = upsertCodexServiceTier(current, enabled);
82
+ await fsp.mkdir(path.dirname(configPath), { recursive: true });
83
+ await fsp.writeFile(configPath, next, 'utf8');
84
+ return next;
85
+ }
86
+
87
+ export function isCodexFeatureEnabledFromConfig(content: string, featureName: string) {
88
+ const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
89
+ const featuresMatch = content.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
90
+ if (!featuresMatch || featuresMatch.index === undefined) {
91
+ return false;
92
+ }
93
+
94
+ const sectionStart = featuresMatch.index + featuresMatch[0].length;
95
+ const nextSectionMatch = content
96
+ .slice(sectionStart)
97
+ .match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
98
+ const sectionEnd =
99
+ nextSectionMatch && nextSectionMatch.index !== undefined
100
+ ? sectionStart + nextSectionMatch.index
101
+ : content.length;
102
+ const featuresBody = content.slice(sectionStart, sectionEnd);
103
+ return new RegExp(`^\\s*${escaped}\\s*=\\s*true\\s*(?:#.*)?$`, 'm').test(featuresBody);
104
+ }
105
+
106
+ export async function readCodexFeatureFlag(
107
+ codexHome: string,
108
+ featureName: string,
109
+ ) {
110
+ try {
111
+ const content = await fsp.readFile(resolveCodexConfigPath(codexHome), 'utf8');
112
+ return isCodexFeatureEnabledFromConfig(content, featureName);
113
+ } catch (error) {
114
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
115
+ return false;
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ export function upsertCodexFeatureFlag(
122
+ content: string,
123
+ featureName: string,
124
+ enabled: boolean,
125
+ ) {
126
+ const normalized = content.replace(/\r\n/g, '\n');
127
+ const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
128
+ const featuresMatch = normalized.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
129
+ const nextLine = `${featureName} = ${enabled ? 'true' : 'false'}`;
130
+
131
+ if (!featuresMatch || featuresMatch.index === undefined) {
132
+ const prefix = normalized.trimEnd();
133
+ return prefix
134
+ ? `${prefix}\n\n[features]\n${nextLine}\n`
135
+ : `[features]\n${nextLine}\n`;
136
+ }
137
+
138
+ const sectionStart = featuresMatch.index + featuresMatch[0].length;
139
+ const nextSectionMatch = normalized
140
+ .slice(sectionStart)
141
+ .match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
142
+ const sectionEnd =
143
+ nextSectionMatch && nextSectionMatch.index !== undefined
144
+ ? sectionStart + nextSectionMatch.index
145
+ : normalized.length;
146
+ const beforeSectionBody = normalized.slice(0, sectionStart);
147
+ const sectionBody = normalized.slice(sectionStart, sectionEnd);
148
+ const afterSection = normalized.slice(sectionEnd);
149
+ const flagPattern = new RegExp(
150
+ `(^[^\\S\\r\\n]*)${escaped}[^\\S\\r\\n]*=[^\\S\\r\\n]*(true|false)[^\\S\\r\\n]*(?:#.*)?$`,
151
+ 'm',
152
+ );
153
+
154
+ if (flagPattern.test(sectionBody)) {
155
+ return (
156
+ beforeSectionBody +
157
+ sectionBody.replace(flagPattern, `$1${nextLine}`) +
158
+ afterSection
159
+ );
160
+ }
161
+
162
+ const bodyWithFlag = `${sectionBody.replace(/\n*$/, '')}\n${nextLine}\n`;
163
+ const normalizedAfterSection = afterSection.startsWith('\n')
164
+ ? afterSection
165
+ : `\n${afterSection}`;
166
+ return beforeSectionBody + bodyWithFlag + normalizedAfterSection;
167
+ }
168
+
169
+ export async function writeCodexFeatureFlag(
170
+ codexHome: string,
171
+ featureName: string,
172
+ enabled: boolean,
173
+ ) {
174
+ const configPath = resolveCodexConfigPath(codexHome);
175
+ let current = '';
176
+ try {
177
+ current = await fsp.readFile(configPath, 'utf8');
178
+ } catch (error) {
179
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ const next = upsertCodexFeatureFlag(current, featureName, enabled);
185
+ await fsp.mkdir(path.dirname(configPath), { recursive: true });
186
+ await fsp.writeFile(configPath, next, 'utf8');
187
+ return next;
188
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ AgentRuntimeManagementError,
3
+ } from '../../../agent-runtime/src/index';
4
+
5
+ export class CodexManagementError extends AgentRuntimeManagementError {
6
+ constructor(
7
+ statusCode: ConstructorParameters<typeof AgentRuntimeManagementError>[0],
8
+ payload: ConstructorParameters<typeof AgentRuntimeManagementError>[1],
9
+ ) {
10
+ super(statusCode, payload);
11
+ this.name = 'CodexManagementError';
12
+ }
13
+ }
14
+
15
+ export function codexBadRequest(message: string): never {
16
+ throw new AgentRuntimeManagementError(400, {
17
+ code: 'bad_request',
18
+ message,
19
+ });
20
+ }
@@ -0,0 +1,184 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import {
8
+ buildTurnPricingSnapshot,
9
+ contextWindowForModel,
10
+ estimateTurnPrice,
11
+ resetPricingConfigCacheForTest,
12
+ supportsFastMode,
13
+ } from './modelPricing';
14
+
15
+ const sampleUsage = {
16
+ total: {
17
+ totalTokens: 3000,
18
+ inputTokens: 1500,
19
+ cachedInputTokens: 500,
20
+ outputTokens: 1500,
21
+ reasoningOutputTokens: 0,
22
+ },
23
+ last: {
24
+ totalTokens: 3000,
25
+ inputTokens: 1500,
26
+ cachedInputTokens: 500,
27
+ outputTokens: 1500,
28
+ reasoningOutputTokens: 0,
29
+ },
30
+ modelContextWindow: 272000,
31
+ };
32
+
33
+ describe('modelPricing', () => {
34
+ afterEach(() => {
35
+ resetPricingConfigCacheForTest();
36
+ vi.unstubAllEnvs();
37
+ });
38
+
39
+ it('prices gpt-5.5 standard turns from the local pricing config', () => {
40
+ const estimate = estimateTurnPrice(sampleUsage, {
41
+ pricingModelKey: 'gpt-5.5',
42
+ pricingTierKey: 'standard',
43
+ });
44
+
45
+ expect(estimate).toMatchObject({
46
+ pricingModelKey: 'gpt-5.5',
47
+ pricingTierKey: 'standard',
48
+ inputUsd: 0.005,
49
+ cachedInputUsd: 0.00025,
50
+ outputUsd: 0.045,
51
+ });
52
+ expect(estimate?.totalUsd).toBeCloseTo(0.05025, 10);
53
+ });
54
+
55
+ it('uses the gpt-5.5-specific fast multiplier and marks it fast-capable', () => {
56
+ expect(supportsFastMode('gpt-5.5')).toBe(true);
57
+ expect(contextWindowForModel('gpt-5.5')).toBe(272000);
58
+
59
+ const estimate = estimateTurnPrice(sampleUsage, {
60
+ pricingModelKey: 'gpt-5.5',
61
+ pricingTierKey: 'fast',
62
+ });
63
+
64
+ expect(estimate).toMatchObject({
65
+ pricingModelKey: 'gpt-5.5',
66
+ pricingTierKey: 'fast',
67
+ inputUsd: 0.0125,
68
+ cachedInputUsd: 0.000625,
69
+ outputUsd: 0.1125,
70
+ });
71
+ expect(estimate?.totalUsd).toBeCloseTo(0.125625, 10);
72
+ });
73
+
74
+ it('prices Claude Sonnet and its 1M context option from the local pricing config', () => {
75
+ expect(supportsFastMode('sonnet')).toBe(false);
76
+ expect(contextWindowForModel('sonnet')).toBe(200000);
77
+ expect(contextWindowForModel('sonnet[1m]')).toBe(1000000);
78
+
79
+ const standardEstimate = estimateTurnPrice(sampleUsage, {
80
+ pricingModelKey: 'sonnet',
81
+ pricingTierKey: 'standard',
82
+ });
83
+ expect(standardEstimate).toMatchObject({
84
+ pricingModelKey: 'sonnet',
85
+ pricingTierKey: 'standard',
86
+ inputUsd: 0.003,
87
+ cachedInputUsd: 0.00015,
88
+ outputUsd: 0.0225,
89
+ });
90
+
91
+ const oneMillionEstimate = estimateTurnPrice(sampleUsage, {
92
+ pricingModelKey: 'sonnet[1m]',
93
+ pricingTierKey: 'standard',
94
+ });
95
+ expect(oneMillionEstimate).toMatchObject({
96
+ pricingModelKey: 'sonnet[1m]',
97
+ pricingTierKey: 'standard',
98
+ inputUsd: 0.006,
99
+ cachedInputUsd: 0.0003,
100
+ outputUsd: 0.03375,
101
+ });
102
+ });
103
+
104
+ it('prices current Claude Opus and Haiku aliases from the local pricing config', () => {
105
+ expect(contextWindowForModel('claude-opus-4-7')).toBe(200000);
106
+ expect(contextWindowForModel('claude-haiku-4-5')).toBe(200000);
107
+
108
+ const opusEstimate = estimateTurnPrice(sampleUsage, {
109
+ pricingModelKey: 'claude-opus-4-7',
110
+ pricingTierKey: 'standard',
111
+ });
112
+ expect(opusEstimate).toMatchObject({
113
+ pricingModelKey: 'claude-opus-4-7',
114
+ pricingTierKey: 'standard',
115
+ inputUsd: 0.005,
116
+ cachedInputUsd: 0.00025,
117
+ outputUsd: 0.0375,
118
+ });
119
+
120
+ const haikuEstimate = estimateTurnPrice(sampleUsage, {
121
+ pricingModelKey: 'claude-haiku-4-5',
122
+ pricingTierKey: 'standard',
123
+ });
124
+ expect(haikuEstimate).toMatchObject({
125
+ pricingModelKey: 'claude-haiku-4-5',
126
+ pricingTierKey: 'standard',
127
+ inputUsd: 0.001,
128
+ cachedInputUsd: 0.00005,
129
+ outputUsd: 0.0075,
130
+ });
131
+ });
132
+
133
+ it('normalizes Claude date-stamped runtime model names to local pricing keys', () => {
134
+ expect(contextWindowForModel('claude-sonnet-4-5-20250929')).toBe(200000);
135
+ expect(supportsFastMode('claude-sonnet-4-5-20250929')).toBe(false);
136
+ expect(buildTurnPricingSnapshot('claude-sonnet-4-5-20250929', false)).toEqual({
137
+ pricingModelKey: 'claude-sonnet-4-5',
138
+ pricingTierKey: 'standard',
139
+ });
140
+
141
+ const estimate = estimateTurnPrice(sampleUsage, {
142
+ pricingModelKey: 'claude-sonnet-4-5-20250929',
143
+ pricingTierKey: 'standard',
144
+ });
145
+
146
+ expect(estimate).toMatchObject({
147
+ pricingModelKey: 'claude-sonnet-4-5',
148
+ pricingTierKey: 'standard',
149
+ inputUsd: 0.003,
150
+ cachedInputUsd: 0.00015,
151
+ outputUsd: 0.0225,
152
+ });
153
+ });
154
+
155
+ it('resolves pricing config from the installed package root when provided', async () => {
156
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-pricing-root-'));
157
+ await fs.mkdir(path.join(tempDir, 'config'), { recursive: true });
158
+ await fs.writeFile(
159
+ path.join(tempDir, 'config', 'codex-model-pricing.json'),
160
+ JSON.stringify({
161
+ currency: 'USD',
162
+ tiers: {
163
+ standard: { multiplier: 1 },
164
+ fast: { multiplier: 2 },
165
+ },
166
+ models: {
167
+ 'package-model': {
168
+ inputUsdPerMillion: 10,
169
+ cachedInputUsdPerMillion: 1,
170
+ outputUsdPerMillion: 20,
171
+ supportsFastMode: true,
172
+ contextWindowTokens: 123000,
173
+ },
174
+ },
175
+ }),
176
+ 'utf8',
177
+ );
178
+ vi.stubEnv('REMOTE_CODEX_PACKAGE_ROOT', tempDir);
179
+ resetPricingConfigCacheForTest();
180
+
181
+ expect(contextWindowForModel('package-model')).toBe(123000);
182
+ expect(supportsFastMode('package-model')).toBe(true);
183
+ });
184
+ });
@@ -0,0 +1,9 @@
1
+ export {
2
+ buildTurnPricingSnapshot,
3
+ contextWindowForModel,
4
+ estimateTurnPrice,
5
+ pricingTierForFastMode,
6
+ resetPricingConfigCacheForTest,
7
+ supportsFastMode,
8
+ type PricingTierKey,
9
+ } from '../../agent-runtime/src/index';
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AgentRuntimeError } from '../../agent-runtime/src/index';
4
+ import { JsonRpcClientError } from './jsonrpc';
5
+ import {
6
+ isCodexRuntimeRequestError,
7
+ isRemoteThreadBootstrapError,
8
+ isUnsupportedHooksListError,
9
+ parseTurnSteerRace,
10
+ unwrapCodexJsonRpcError,
11
+ } from './runtime-errors';
12
+
13
+ describe('codex runtime error helpers', () => {
14
+ it('unwraps Codex JSON-RPC errors from runtime errors', () => {
15
+ const cause = new JsonRpcClientError('method not found', 'remote_error', { code: -32601 });
16
+ const error = new AgentRuntimeError('method not found', 'codex', 'remote_error', {}, cause);
17
+
18
+ expect(unwrapCodexJsonRpcError(error)).toBe(cause);
19
+ expect(isCodexRuntimeRequestError(error)).toBe(true);
20
+ });
21
+
22
+ it('classifies Codex remote errors used by ThreadService fallback paths', () => {
23
+ expect(
24
+ isRemoteThreadBootstrapError(
25
+ new AgentRuntimeError(
26
+ 'failed to load rollout: rollout at /tmp/demo.jsonl is empty',
27
+ 'codex',
28
+ 'remote_error',
29
+ {},
30
+ new JsonRpcClientError(
31
+ 'failed to load rollout: rollout at /tmp/demo.jsonl is empty',
32
+ 'remote_error',
33
+ ),
34
+ ),
35
+ ),
36
+ ).toBe(true);
37
+
38
+ expect(
39
+ isUnsupportedHooksListError(
40
+ new AgentRuntimeError(
41
+ 'endpoint not found: hooks/list',
42
+ 'codex',
43
+ 'remote_error',
44
+ {},
45
+ new JsonRpcClientError('endpoint not found: hooks/list', 'remote_error', {
46
+ code: -32601,
47
+ }),
48
+ ),
49
+ ),
50
+ ).toBe(true);
51
+ });
52
+
53
+ it('parses Codex steer races from wrapped runtime errors', () => {
54
+ expect(
55
+ parseTurnSteerRace(
56
+ new AgentRuntimeError(
57
+ 'expected active turn id `turn-old` but found `turn-new`',
58
+ 'codex',
59
+ 'remote_error',
60
+ {},
61
+ new JsonRpcClientError(
62
+ 'expected active turn id `turn-old` but found `turn-new`',
63
+ 'remote_error',
64
+ ),
65
+ ),
66
+ ),
67
+ ).toEqual({
68
+ type: 'turnIdMismatch',
69
+ actualTurnId: 'turn-new',
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,37 @@
1
+ import {
2
+ AgentRuntimeError,
3
+ isRemoteThreadBootstrapError,
4
+ isRuntimeRequestError,
5
+ isUnsupportedHooksListError,
6
+ parseTurnSteerRace,
7
+ TurnSteerRace,
8
+ } from '../../agent-runtime/src/index';
9
+ import { JsonRpcClientError } from './jsonrpc';
10
+
11
+ export type CodexTurnSteerRace = TurnSteerRace;
12
+ export {
13
+ isRemoteThreadBootstrapError,
14
+ isUnsupportedHooksListError,
15
+ parseTurnSteerRace,
16
+ };
17
+
18
+ export function unwrapCodexJsonRpcError(error: unknown): JsonRpcClientError | null {
19
+ if (error instanceof JsonRpcClientError) {
20
+ return error;
21
+ }
22
+
23
+ if (error instanceof AgentRuntimeError && error.provider === 'codex') {
24
+ return error.cause instanceof JsonRpcClientError ? error.cause : null;
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ function isCodexRemoteError(error: unknown) {
31
+ const codexError = unwrapCodexJsonRpcError(error);
32
+ return codexError?.code === 'remote_error' ? codexError : null;
33
+ }
34
+
35
+ export function isCodexRuntimeRequestError(error: unknown) {
36
+ return Boolean(unwrapCodexJsonRpcError(error)) || isRuntimeRequestError(error);
37
+ }
@@ -23,6 +23,9 @@ import type {
23
23
  StartAgentSessionResult,
24
24
  StartAgentTurnInput,
25
25
  } from '../../agent-runtime/src/index';
26
+ import type {
27
+ AgentBackendInstallationDto,
28
+ } from '../../shared/src/index';
26
29
  import {
27
30
  buildCodexProviderRequestResponse,
28
31
  mapCodexProviderRequest,
@@ -51,6 +54,7 @@ import {
51
54
  ThreadStartInput,
52
55
  TurnStartInput,
53
56
  JsonRpcClientError,
57
+ supportsFastMode,
54
58
  } from './index';
55
59
 
56
60
  export const codexCapabilities: AgentProviderCapabilities = {
@@ -137,6 +141,7 @@ function mapModel(model: Awaited<ReturnType<CodexAppServerManager['listModels']>
137
141
  description: model.description,
138
142
  isDefault: model.isDefault,
139
143
  hidden: model.hidden,
144
+ supportsPerformanceMode: supportsFastMode(model.model),
140
145
  supportedReasoningEfforts: model.supportedReasoningEfforts.map((entry) => ({
141
146
  reasoningEffort: entry.reasoningEffort,
142
147
  description: entry.description,
@@ -209,6 +214,7 @@ function buildSandboxPolicy(
209
214
  }
210
215
 
211
216
  function mapSession(thread: CodexThreadRecord): AgentSessionDetail {
217
+ const threadWithTotal = thread as CodexThreadRecord & { totalTurnCount?: unknown };
212
218
  return {
213
219
  provider: 'codex',
214
220
  providerSessionId: thread.id,
@@ -219,6 +225,10 @@ function mapSession(thread: CodexThreadRecord): AgentSessionDetail {
219
225
  updatedAt: toIsoFromEpoch(thread.updatedAt),
220
226
  status: normalizeStatus(thread.status),
221
227
  turns: thread.turns.map(mapTurn),
228
+ totalTurnCount:
229
+ typeof threadWithTotal.totalTurnCount === 'number'
230
+ ? threadWithTotal.totalTurnCount
231
+ : null,
222
232
  rawSession: thread,
223
233
  };
224
234
  }
@@ -460,6 +470,16 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
460
470
  readonly displayName = 'Codex';
461
471
  readonly description = 'Local Codex app-server runtime.';
462
472
  readonly capabilities = codexCapabilities;
473
+ readonly installation: AgentBackendInstallationDto = {
474
+ packageName: '@openai/codex',
475
+ installed: true,
476
+ installedVersion: null,
477
+ latestVersion: null,
478
+ installCommand: null,
479
+ updateCommand: 'npm install -g @openai/codex@latest',
480
+ busy: false,
481
+ lastError: null,
482
+ };
463
483
  readonly managementSchema: AgentRuntimeManagementSchema = {
464
484
  hostConfigFiles: [
465
485
  {
@@ -573,8 +593,11 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
573
593
  return codexRuntimeCall(() => this.manager.listLoadedThreads());
574
594
  }
575
595
 
576
- async readSession(providerSessionId: string): Promise<AgentSessionDetail> {
577
- return mapSession(await codexRuntimeCall(() => this.manager.readThread(providerSessionId)));
596
+ async readSession(
597
+ providerSessionId: string,
598
+ options: { limit?: number; beforeTurnId?: string | null } = {},
599
+ ): Promise<AgentSessionDetail> {
600
+ return mapSession(await codexRuntimeCall(() => this.manager.readThread(providerSessionId, options)));
578
601
  }
579
602
 
580
603
  async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
@@ -0,0 +1 @@
1
+ export { truncateAutoThreadTitle } from '../../shared/src/index';
@@ -416,6 +416,21 @@ export function deleteThreadTurnMetadataByThreadId(db: DatabaseClient, threadId:
416
416
  db.delete(threadTurnMetadata).where(eq(threadTurnMetadata.threadId, threadId)).run();
417
417
  }
418
418
 
419
+ export function deleteThreadTurnMetadataByThreadAndTurnId(
420
+ db: DatabaseClient,
421
+ threadId: string,
422
+ turnId: string,
423
+ ) {
424
+ db.delete(threadTurnMetadata)
425
+ .where(
426
+ and(
427
+ eq(threadTurnMetadata.threadId, threadId),
428
+ eq(threadTurnMetadata.turnId, turnId),
429
+ ),
430
+ )
431
+ .run();
432
+ }
433
+
419
434
  export function listThreadHistoryItemRecordsByThreadId(
420
435
  db: DatabaseClient,
421
436
  threadId: string,
@@ -476,6 +491,21 @@ export function deleteThreadHistoryItemRecordsByThreadId(
476
491
  db.delete(threadHistoryItems).where(eq(threadHistoryItems.threadId, threadId)).run();
477
492
  }
478
493
 
494
+ export function deleteThreadHistoryItemRecordsByThreadAndTurnId(
495
+ db: DatabaseClient,
496
+ threadId: string,
497
+ turnId: string,
498
+ ) {
499
+ db.delete(threadHistoryItems)
500
+ .where(
501
+ and(
502
+ eq(threadHistoryItems.threadId, threadId),
503
+ eq(threadHistoryItems.turnId, turnId),
504
+ ),
505
+ )
506
+ .run();
507
+ }
508
+
479
509
  export function listThreadPendingSteerRecordsByThreadId(
480
510
  db: DatabaseClient,
481
511
  threadId: string,