@dsh-enhanced/coding-subscription-provider 0.1.2 → 0.1.4

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/lib/adapter.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { resolve } from 'node:path';
2
- import { LlmAdapter, LlmError, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
2
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
3
3
  import { verifySubscriptionAuth } from './auth.js';
4
4
  import { discoverCodexModels, } from './codex-catalog.js';
5
- import { configForProvider } from './config.js';
5
+ import { CodexCredentialStore } from './codex-direct-auth.js';
6
+ import { runCodexDirectResponses, } from './codex-direct-responses.js';
7
+ import { configForProvider, } from './config.js';
6
8
  import { buildPrompt } from './prompt.js';
7
9
  import { runCliText } from './process.js';
8
10
  import { buildInvocation } from './providers.js';
11
+ import { resolveTrustedSessionCwd, TrustedSessionCwdError } from './session-cwd.js';
9
12
  import { discoverClaudeModels, discoverCursorModels, discoverGrokModels, } from './subscription-catalog.js';
10
13
  export const routeDefinitions = [
11
14
  { route: 'codex-subscription', cli: 'codex', name: 'Codex Subscription (local CLI)', maturity: 'stable' },
@@ -14,6 +17,7 @@ export const routeDefinitions = [
14
17
  { route: 'grok-subscription', cli: 'grok', name: 'Grok Subscription (local Grok Build)', maturity: 'beta' },
15
18
  ];
16
19
  const CATALOG_TTL_MS = 5 * 60_000;
20
+ const CATALOG_STDERR_DIAGNOSTIC = 'model catalog probe wrote to stderr; content withheld';
17
21
  const noAutomaticRetry = Object.freeze({
18
22
  mode: 'normal',
19
23
  maxRetries: 0,
@@ -34,9 +38,15 @@ function abortReason(signal) {
34
38
  function cliFailure(definition, command, error) {
35
39
  if (error instanceof Error && error.cause === 'abort')
36
40
  return error;
41
+ if (error instanceof TrustedSessionCwdError) {
42
+ return new LlmError(`${definition.name} requires a live loop-owned session whose canonical cwd matches the configured local workspace`, 'LOCAL_SESSION_CWD_REQUIRED', { cause: error });
43
+ }
37
44
  if (error instanceof Error && error.cause === 'subscription-auth') {
38
45
  return new LlmError(`${definition.name} refused the request because a subscription-compatible login could not be verified`, 'SUBSCRIPTION_AUTH_REQUIRED', { cause: error });
39
46
  }
47
+ if (error instanceof Error && error.cause === 'working-directory') {
48
+ return new LlmError(`${definition.name} refused the configured working directory; set config.cwd to a Git repository (relative paths resolve from the DSH process directory) and restart DSH`, 'CLI_WORKING_DIRECTORY', { cause: error });
49
+ }
40
50
  if (error instanceof Error && error.cause === 'protocol') {
41
51
  return new LlmError(`${definition.name} returned an unrecognized or incomplete event stream`, 'CLI_PROTOCOL_ERROR', { cause: error });
42
52
  }
@@ -49,8 +59,86 @@ function cliFailure(definition, command, error) {
49
59
  }
50
60
  return new LlmError(`${definition.name} failed; verify that the official CLI is installed and logged in with the intended subscription`, 'CLI_FAILED', { cause: error });
51
61
  }
62
+ export const CODEX_DIRECT_PROVIDER_HTTP_CODE = 'CODEX_DIRECT_PROVIDER_HTTP';
63
+ export const CODEX_DIRECT_PROVIDER_FAILURE_CODE = 'CODEX_DIRECT_PROVIDER_FAILURE';
64
+ export const CODEX_DIRECT_CONTENT_FILTER_CODE = 'CODEX_DIRECT_CONTENT_FILTER';
65
+ export const CODEX_DIRECT_TRANSPORT_ERROR_CODE = 'CODEX_DIRECT_TRANSPORT_ERROR';
66
+ function directFailure(definition, error) {
67
+ if (error instanceof LlmError)
68
+ return error;
69
+ if (error instanceof Error && error.cause === 'abort')
70
+ return error;
71
+ if (error instanceof TrustedSessionCwdError) {
72
+ return new LlmError(`${definition.name} requires a live loop-owned session whose canonical cwd matches the configured local workspace`, 'LOCAL_SESSION_CWD_REQUIRED', { cause: error });
73
+ }
74
+ const cause = error instanceof Error ? error.cause : undefined;
75
+ const status = validHttpStatus(error);
76
+ if (cause === 'subscription-auth') {
77
+ return new LlmError('Codex private Responses could not use the local ChatGPT session', 'SUBSCRIPTION_AUTH_REQUIRED', status === undefined ? undefined : { status });
78
+ }
79
+ if (cause === 'timeout') {
80
+ return new LlmError('Codex private Responses request timed out', 'CLI_TIMEOUT');
81
+ }
82
+ if (cause === 'protocol') {
83
+ return new LlmError('Codex private Responses returned an unrecognized or incomplete stream', 'CLI_PROTOCOL_ERROR');
84
+ }
85
+ if (cause === 'provider-http') {
86
+ return new LlmError('Codex private Responses provider request failed', CODEX_DIRECT_PROVIDER_HTTP_CODE, status === undefined ? undefined : { status });
87
+ }
88
+ if (cause === 'context-window') {
89
+ return new LlmError('Codex private Responses exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE);
90
+ }
91
+ if (cause === 'quota') {
92
+ return new LlmError('Codex private Responses account quota was exhausted', QUOTA_EXCEEDED_CODE);
93
+ }
94
+ if (cause === 'provider-failure') {
95
+ return new LlmError('Codex private Responses reported a provider failure', CODEX_DIRECT_PROVIDER_FAILURE_CODE);
96
+ }
97
+ if (cause === 'content-filter') {
98
+ return new LlmError('Codex private Responses output was filtered', CODEX_DIRECT_CONTENT_FILTER_CODE);
99
+ }
100
+ if (cause === 'empty-response') {
101
+ return new LlmError('Codex private Responses returned no visible output', EMPTY_RESPONSE_CODE);
102
+ }
103
+ if (cause === 'transport') {
104
+ return new LlmError('Codex private Responses transport failed', CODEX_DIRECT_TRANSPORT_ERROR_CODE);
105
+ }
106
+ return new LlmError('Codex private Responses request failed', 'CODEX_DIRECT_RESPONSES_FAILED', { cause: error });
107
+ }
108
+ function validHttpStatus(error) {
109
+ if (typeof error !== 'object' || error === null)
110
+ return undefined;
111
+ const status = error.status;
112
+ return typeof status === 'number' && Number.isInteger(status) && status >= 100 && status <= 599
113
+ ? status
114
+ : undefined;
115
+ }
116
+ /** Stable code for a request that needs tool calls this text-only route cannot make. */
117
+ export const TOOL_CALLS_UNSUPPORTED_CODE = 'tool_calls_unsupported';
118
+ /**
119
+ * Refuse a request that supplies tool schemas.
120
+ *
121
+ * These routes drive local coding CLIs that only return assistant text; they
122
+ * cannot emit a `tool-call` block. Silently dropping `options.tools` would let an
123
+ * unattended automation appear to run while never invoking its allowlisted tools,
124
+ * so the request fails loudly and names the working alternative instead.
125
+ */
126
+ function toolCallsUnsupported(definition) {
127
+ return new LlmError(`${definition.name} is a text-only route and cannot perform tool calls. `
128
+ + 'Use a provider that emits tool calls (for example @dsh-enhanced/traex-acp-provider) '
129
+ + 'for agent or automation work that requires tools.', TOOL_CALLS_UNSUPPORTED_CODE);
130
+ }
131
+ /** Report a local prompt bound through DSH's canonical overflow path so compaction can retry it. */
132
+ function preflightFailure(error) {
133
+ if (error instanceof Error && error.cause === 'prompt-limit') {
134
+ return new LlmError(error.message, CONTEXT_WINDOW_EXCEEDED_CODE, { cause: error });
135
+ }
136
+ return error instanceof Error ? error : new Error('coding subscription provider could not serialize the DSH request');
137
+ }
52
138
  /** Classify the original transport error into a stable, credential-free outcome for diagnostics. */
53
139
  function outcomeFor(error) {
140
+ if (error instanceof TrustedSessionCwdError)
141
+ return 'working-directory';
54
142
  const cause = error instanceof Error ? error.cause : undefined;
55
143
  if (cause === 'abort')
56
144
  return 'aborted';
@@ -58,6 +146,8 @@ function outcomeFor(error) {
58
146
  return 'timeout';
59
147
  if (cause === 'subscription-auth')
60
148
  return 'auth-required';
149
+ if (cause === 'working-directory')
150
+ return 'working-directory';
61
151
  if (cause === 'protocol')
62
152
  return 'protocol';
63
153
  if (cause === 'process-exit')
@@ -89,6 +179,17 @@ function reasoningInfo(model) {
89
179
  },
90
180
  };
91
181
  }
182
+ function directReasoningInfo(profile) {
183
+ return {
184
+ reasoning: {
185
+ efforts: profile.directReasoningEfforts.map(effort => ({
186
+ id: ReasoningEffortId(effort),
187
+ name: effort,
188
+ })),
189
+ defaultEffort: ReasoningEffortId(profile.directDefaultReasoningEffort),
190
+ },
191
+ };
192
+ }
92
193
  /** Experimental LLM-compatible facade over local, already-authenticated coding agents. */
93
194
  export class CodingSubscriptionAdapter extends LlmAdapter {
94
195
  config;
@@ -98,12 +199,19 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
98
199
  discoverClaudeModels;
99
200
  discoverCursorModels;
100
201
  discoverGrokModels;
202
+ runCodexDirect;
203
+ codexCredentials;
204
+ getAttachments;
101
205
  verifyAuth;
102
206
  onDiagnostic;
103
207
  onSettled;
208
+ liveSessions;
209
+ requestAttestor;
210
+ now;
104
211
  lifecycle = new AbortController();
105
212
  codexCatalogCache;
106
213
  subscriptionCatalogCache = new Map();
214
+ catalogRefreshes = new Map();
107
215
  constructor(config, dependencies = {}) {
108
216
  super();
109
217
  this.config = config;
@@ -113,9 +221,23 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
113
221
  this.discoverClaudeModels = dependencies.discoverClaudeModels ?? discoverClaudeModels;
114
222
  this.discoverCursorModels = dependencies.discoverCursorModels ?? discoverCursorModels;
115
223
  this.discoverGrokModels = dependencies.discoverGrokModels ?? discoverGrokModels;
224
+ this.runCodexDirect = dependencies.runCodexDirect ?? runCodexDirectResponses;
225
+ this.codexCredentials = dependencies.codexCredentials ?? new CodexCredentialStore();
226
+ this.getAttachments = dependencies.getAttachments ?? (() => dependencies.attachments);
116
227
  this.verifyAuth = dependencies.verifyAuth ?? verifySubscriptionAuth;
117
228
  this.onDiagnostic = dependencies.onDiagnostic;
118
229
  this.onSettled = dependencies.onSettled;
230
+ this.liveSessions = dependencies.liveSessions;
231
+ this.requestAttestor = dependencies.requestAttestor;
232
+ this.now = dependencies.now ?? Date.now;
233
+ }
234
+ trustedCwd(request) {
235
+ return resolveTrustedSessionCwd({
236
+ request,
237
+ configuredCwd: this.cwd,
238
+ sessions: this.liveSessions,
239
+ ...(this.requestAttestor === undefined ? {} : { attestor: this.requestAttestor }),
240
+ });
119
241
  }
120
242
  reportSettled(context) {
121
243
  if (this.onSettled === undefined)
@@ -129,29 +251,44 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
129
251
  }
130
252
  providerInfo(provider) {
131
253
  const definition = definitionFor(provider);
132
- return { id: provider, name: definition.name };
254
+ return {
255
+ id: provider,
256
+ name: definition.cli === 'codex' && this.config.codex.transport === 'direct-responses'
257
+ ? 'Codex Subscription (private Responses, experimental)'
258
+ : definition.name,
259
+ };
133
260
  }
134
261
  providerRetryPolicy(_provider) {
135
262
  return noAutomaticRetry;
136
263
  }
137
- async loadCodexCatalog(signal = this.lifecycle.signal) {
138
- if (signal.aborted)
139
- throw abortReason(signal);
264
+ peekCodexCatalog() {
140
265
  const cached = this.codexCatalogCache;
141
- if (cached !== undefined && cached.expiresAt > Date.now())
266
+ if (cached !== undefined && cached.expiresAt > this.now())
267
+ return cached.catalog;
268
+ this.codexCatalogCache = undefined;
269
+ return undefined;
270
+ }
271
+ peekSubscriptionCatalog(provider) {
272
+ const cached = this.subscriptionCatalogCache.get(provider);
273
+ if (cached !== undefined && cached.expiresAt > this.now())
142
274
  return cached.catalog;
275
+ this.subscriptionCatalogCache.delete(provider);
276
+ return undefined;
277
+ }
278
+ /**
279
+ * Observe the advisory Codex CLI catalog with bounded, prompt-free control-plane I/O.
280
+ * Authentication is owned by `refreshCatalog`; this method only performs discovery.
281
+ */
282
+ async observeCodexCatalog(cwd, signal) {
283
+ if (signal.aborted)
284
+ throw abortReason(signal);
285
+ const cached = this.peekCodexCatalog();
286
+ if (cached !== undefined)
287
+ return cached;
143
288
  const definition = definitionFor('codex-subscription');
144
289
  const profile = this.config.codex;
145
290
  try {
146
- await this.verifyAuth('codex', {
147
- command: profile.command,
148
- cwd: this.cwd,
149
- timeoutMs: this.config.authProbeTimeoutMs,
150
- maxOutputBytes: this.config.maxAuthProbeBytes,
151
- extraEnvNames: this.config.extraEnvNames,
152
- signal,
153
- });
154
- const catalog = await this.discoverCodexModels({ command: profile.command, cwd: this.cwd }, {
291
+ const catalog = await this.discoverCodexModels({ command: profile.command, cwd }, {
155
292
  signal,
156
293
  timeoutMs: this.config.authProbeTimeoutMs,
157
294
  killGraceMs: this.config.killGraceMs,
@@ -159,23 +296,31 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
159
296
  maxOutputBytes: this.config.maxOutputBytes,
160
297
  maxStderrBytes: this.config.maxStderrBytes,
161
298
  extraEnvNames: this.config.extraEnvNames,
162
- onDiagnostic: diagnostic => this.onDiagnostic?.(definition.route, diagnostic),
299
+ onDiagnostic: () => {
300
+ try {
301
+ this.onDiagnostic?.(definition.route, CATALOG_STDERR_DIAGNOSTIC);
302
+ }
303
+ catch {
304
+ // A diagnostic sink must never affect catalog discovery.
305
+ }
306
+ },
163
307
  });
164
- this.codexCatalogCache = { catalog, expiresAt: Date.now() + CATALOG_TTL_MS };
308
+ if (signal.aborted)
309
+ throw abortReason(signal);
310
+ this.codexCatalogCache = { catalog, expiresAt: this.now() + CATALOG_TTL_MS };
165
311
  return catalog;
166
312
  }
167
313
  catch (error) {
168
314
  this.codexCatalogCache = undefined;
169
- throw cliFailure(definition, profile.command, error);
315
+ throw error;
170
316
  }
171
317
  }
172
- async loadSubscriptionCatalog(provider, signal = this.lifecycle.signal) {
318
+ async observeSubscriptionCatalog(provider, cwd, signal) {
173
319
  if (signal.aborted)
174
320
  throw abortReason(signal);
175
- const cached = this.subscriptionCatalogCache.get(provider);
176
- if (cached !== undefined && cached.expiresAt > Date.now())
177
- return cached.catalog;
178
- const definition = routeDefinitions.find(candidate => candidate.cli === provider);
321
+ const cached = this.peekSubscriptionCatalog(provider);
322
+ if (cached !== undefined)
323
+ return cached;
179
324
  const profile = configForProvider(this.config, provider);
180
325
  const discoverer = provider === 'claude'
181
326
  ? this.discoverClaudeModels
@@ -183,51 +328,117 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
183
328
  ? this.discoverCursorModels
184
329
  : this.discoverGrokModels;
185
330
  try {
331
+ const catalog = await discoverer({ command: profile.command, cwd }, {
332
+ signal,
333
+ timeoutMs: this.config.authProbeTimeoutMs,
334
+ maxOutputBytes: this.config.maxAuthProbeBytes,
335
+ extraEnvNames: this.config.extraEnvNames,
336
+ });
337
+ if (signal.aborted)
338
+ throw abortReason(signal);
339
+ this.subscriptionCatalogCache.set(provider, { catalog, expiresAt: this.now() + CATALOG_TTL_MS });
340
+ return catalog;
341
+ }
342
+ catch (error) {
343
+ this.subscriptionCatalogCache.delete(provider);
344
+ throw error;
345
+ }
346
+ }
347
+ observedCatalog(provider) {
348
+ return provider === 'codex'
349
+ ? this.peekCodexCatalog()
350
+ : this.peekSubscriptionCatalog(provider);
351
+ }
352
+ observeCatalog(provider, cwd, signal) {
353
+ return provider === 'codex'
354
+ ? this.observeCodexCatalog(cwd, signal)
355
+ : this.observeSubscriptionCatalog(provider, cwd, signal);
356
+ }
357
+ /**
358
+ * Refresh one CLI catalog after the existing subscription-auth gate. Concurrent
359
+ * list callers share the complete auth + discovery operation, so a cold picker
360
+ * cannot fan out local probes. Generation never enters or waits on this path.
361
+ */
362
+ refreshCatalog(provider, cwd) {
363
+ const refreshSignal = this.lifecycle.signal;
364
+ if (refreshSignal.aborted)
365
+ return Promise.reject(abortReason(refreshSignal));
366
+ const cached = this.observedCatalog(provider);
367
+ if (cached !== undefined)
368
+ return Promise.resolve(cached);
369
+ const active = this.catalogRefreshes.get(provider);
370
+ if (active !== undefined)
371
+ return active;
372
+ const profile = configForProvider(this.config, provider);
373
+ let refresh;
374
+ refresh = (async () => {
186
375
  await this.verifyAuth(provider, {
187
376
  command: profile.command,
188
- cwd: this.cwd,
377
+ cwd,
189
378
  timeoutMs: this.config.authProbeTimeoutMs,
190
379
  maxOutputBytes: this.config.maxAuthProbeBytes,
191
380
  extraEnvNames: this.config.extraEnvNames,
192
- signal,
381
+ signal: refreshSignal,
193
382
  ...(provider === 'grok'
194
383
  ? { userVerifiedSubscription: this.config.grok.userVerifiedSubscription }
195
384
  : {}),
196
385
  });
197
- const catalog = await discoverer({ command: profile.command, cwd: this.cwd }, {
198
- signal,
199
- timeoutMs: this.config.authProbeTimeoutMs,
200
- maxOutputBytes: this.config.maxAuthProbeBytes,
201
- extraEnvNames: this.config.extraEnvNames,
202
- });
203
- this.subscriptionCatalogCache.set(provider, { catalog, expiresAt: Date.now() + CATALOG_TTL_MS });
204
- return catalog;
386
+ if (refreshSignal.aborted)
387
+ throw abortReason(refreshSignal);
388
+ return this.observeCatalog(provider, cwd, refreshSignal);
389
+ })().finally(() => {
390
+ if (this.catalogRefreshes.get(provider) === refresh)
391
+ this.catalogRefreshes.delete(provider);
392
+ });
393
+ this.catalogRefreshes.set(provider, refresh);
394
+ return refresh;
395
+ }
396
+ reportCatalogRefreshFailure(definition, error) {
397
+ if (this.lifecycle.signal.aborted)
398
+ return;
399
+ const diagnostic = `model catalog refresh failed; outcome=${outcomeFor(error)}; using configured models`;
400
+ try {
401
+ this.onDiagnostic?.(definition.route, diagnostic);
205
402
  }
206
- catch (error) {
207
- this.subscriptionCatalogCache.delete(provider);
208
- throw cliFailure(definition, profile.command, error);
403
+ catch {
404
+ // A diagnostic sink must never turn advisory catalog failure into listModels failure.
209
405
  }
210
406
  }
211
407
  async listModels(provider) {
212
408
  const definition = definitionFor(provider);
213
409
  const profile = configForProvider(this.config, definition.cli);
214
- const catalog = definition.cli === 'codex'
215
- ? await this.loadCodexCatalog()
216
- : await this.loadSubscriptionCatalog(definition.cli);
217
- const defaultModel = catalog.models.find(model => model.id === catalog.defaultModel);
410
+ const directCodex = definition.cli === 'codex' && this.config.codex.transport === 'direct-responses';
411
+ let catalog;
412
+ if (!directCodex && !this.lifecycle.signal.aborted) {
413
+ try {
414
+ catalog = await this.refreshCatalog(definition.cli, this.cwd);
415
+ }
416
+ catch (error) {
417
+ this.reportCatalogRefreshFailure(definition, error);
418
+ }
419
+ }
420
+ const attachments = directCodex ? this.getAttachments() : undefined;
421
+ const inputModalities = directCodex && attachments !== undefined
422
+ ? ['text', 'image']
423
+ : ['text'];
424
+ const defaultModel = catalog?.models.find(model => model.id === catalog.defaultModel);
218
425
  const entries = new Map();
219
426
  for (const model of profile.models) {
220
427
  entries.set(model, {
221
428
  provider,
222
429
  id: model,
223
- name: model === 'default'
224
- ? `${definition.name} default${defaultModel === undefined ? '' : ` (${defaultModel.name})`}`
225
- : model,
226
- description: `${definition.maturity} local coding-agent delegation; authentication and billing remain in the official CLI`,
227
- inputModalities: ['text'],
430
+ name: directCodex && model === 'default'
431
+ ? `Codex Subscription (private Responses) default (${this.config.codex.directModel})`
432
+ : model === 'default'
433
+ ? `${definition.name} default${defaultModel === undefined ? '' : ` (${defaultModel.name})`}`
434
+ : model,
435
+ description: directCodex
436
+ ? 'experimental direct private Codex Responses transport using the local ChatGPT session'
437
+ : `${definition.maturity} local coding-agent delegation; authentication and billing remain in the official CLI`,
438
+ inputModalities,
228
439
  });
229
440
  }
230
- for (const model of catalog.models) {
441
+ for (const model of catalog?.models ?? []) {
231
442
  entries.set(model.id, {
232
443
  provider,
233
444
  id: model.id,
@@ -245,21 +456,25 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
245
456
  if (effectiveSignal.aborted)
246
457
  throw abortReason(effectiveSignal);
247
458
  const definition = definitionFor(provider);
248
- const catalog = definition.cli === 'codex'
249
- ? await this.loadCodexCatalog(effectiveSignal)
250
- : await this.loadSubscriptionCatalog(definition.cli, effectiveSignal);
459
+ const directCodex = definition.cli === 'codex' && this.config.codex.transport === 'direct-responses';
460
+ const catalog = directCodex ? undefined : this.observedCatalog(definition.cli);
251
461
  const catalogModel = model === 'default'
252
- ? catalog.models.find(candidate => candidate.id === catalog.defaultModel)
253
- : catalog.models.find(candidate => candidate.id === model);
462
+ ? catalog?.models.find(candidate => candidate.id === catalog.defaultModel)
463
+ : catalog?.models.find(candidate => candidate.id === model);
464
+ const attachments = directCodex ? this.getAttachments() : undefined;
254
465
  return {
255
466
  provider,
256
467
  id: model,
257
- name: model === 'default'
258
- ? `${definition.name} default${catalogModel === undefined ? '' : ` (${catalogModel.name})`}`
259
- : (catalogModel?.name ?? model),
260
- description: catalogModel?.description ?? `${definition.maturity} local coding-agent delegation`,
261
- inputModalities: ['text'],
262
- ...reasoningInfo(catalogModel),
468
+ name: directCodex && model === 'default'
469
+ ? `Codex Subscription (private Responses) default (${this.config.codex.directModel})`
470
+ : model === 'default'
471
+ ? `${definition.name} default${catalogModel === undefined ? '' : ` (${catalogModel.name})`}`
472
+ : (catalogModel?.name ?? model),
473
+ description: catalogModel?.description ?? (directCodex
474
+ ? 'experimental direct private Codex Responses transport using the local ChatGPT session'
475
+ : `${definition.maturity} local coding-agent delegation`),
476
+ inputModalities: directCodex && attachments !== undefined ? ['text', 'image'] : ['text'],
477
+ ...(directCodex ? directReasoningInfo(this.config.codex) : reasoningInfo(catalogModel)),
263
478
  };
264
479
  }
265
480
  async *stream(options) {
@@ -272,7 +487,7 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
272
487
  let transportContext;
273
488
  // Track the best-known lifecycle facts so a single settled report fires on ANY exit,
274
489
  // including a consumer that returns early after block-start or a text delta.
275
- let phase = 'auth';
490
+ let phase = 'preflight';
276
491
  let promptSubmissionState = 'not-submitted';
277
492
  let outcome = 'aborted';
278
493
  let reported = false;
@@ -295,10 +510,99 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
295
510
  });
296
511
  };
297
512
  try {
513
+ if (signal.aborted)
514
+ throw abortReason(signal);
515
+ const directCodex = definition.cli === 'codex' && this.config.codex.transport === 'direct-responses';
516
+ // Refuse tool work before any subprocess starts: this route cannot emit a
517
+ // tool call, and a silent drop would look like a successful empty run.
518
+ if (!directCodex && options.tools !== undefined && options.tools.length > 0) {
519
+ outcome = 'preflight';
520
+ throw toolCallsUnsupported(definition);
521
+ }
522
+ let cwd;
523
+ try {
524
+ cwd = this.trustedCwd(options);
525
+ }
526
+ catch (error) {
527
+ outcome = outcomeFor(error);
528
+ throw cliFailure(definition, profile.command, error);
529
+ }
530
+ if (directCodex) {
531
+ const timeoutSignal = AbortSignal.timeout(this.config.timeoutMs);
532
+ const directSignal = AbortSignal.any([signal, timeoutSignal]);
533
+ try {
534
+ // Revalidate immediately before the first credential/file/network seam.
535
+ this.trustedCwd(options);
536
+ // `maxTokens` is a host-local Agent Loop budget. The private Codex
537
+ // request has no max_output_tokens field, so do not hand this hint to
538
+ // the direct runner (which deliberately rejects unsupported controls).
539
+ const { maxTokens: hostMaxTokens, ...directInput } = options;
540
+ void hostMaxTokens;
541
+ const directOptions = {
542
+ ...directInput,
543
+ model: options.model === 'default' ? this.config.codex.directModel : options.model,
544
+ signal: directSignal,
545
+ };
546
+ const attachments = this.getAttachments();
547
+ const chunks = this.runCodexDirect(directOptions, {
548
+ request: (body, requestSignal) => {
549
+ phase = 'stream';
550
+ promptSubmissionState = 'unknown';
551
+ return this.codexCredentials.requestResponses(body, requestSignal);
552
+ },
553
+ maxRequestBytes: this.config.codex.maxRequestBytes,
554
+ maxRequestImageBytes: this.config.codex.maxRequestImageBytes,
555
+ ...(attachments === undefined ? {} : { attachments }),
556
+ });
557
+ for await (const chunk of chunks) {
558
+ if (chunk.type === 'text-delta' && chunk.text.length > 0)
559
+ assistantTextForwarded = true;
560
+ yield chunk;
561
+ }
562
+ outcome = 'ok';
563
+ return;
564
+ }
565
+ catch (error) {
566
+ if (signal.aborted) {
567
+ outcome = 'aborted';
568
+ throw abortReason(signal);
569
+ }
570
+ if (error instanceof Error && error.cause === 'prompt-limit') {
571
+ phase = 'preflight';
572
+ promptSubmissionState = 'not-submitted';
573
+ outcome = 'preflight';
574
+ throw preflightFailure(error);
575
+ }
576
+ const failure = timeoutSignal.aborted && !signal.aborted
577
+ ? new Error('Codex direct Responses request timed out', { cause: 'timeout' })
578
+ : error;
579
+ outcome = outcomeFor(failure);
580
+ throw directFailure(definition, failure);
581
+ }
582
+ }
583
+ // Build and bound the argv locally before any auth probe or generation subprocess starts.
584
+ let invocation;
585
+ try {
586
+ invocation = buildInvocation(definition.cli, {
587
+ cwd,
588
+ prompt: buildPrompt(options, this.config.maxPromptBytes),
589
+ model: options.model,
590
+ ...(definition.cli !== 'cursor' && options.reasoningEffort !== undefined
591
+ ? { reasoningEffort: String(options.reasoningEffort) }
592
+ : {}),
593
+ maxTurns: profile.maxTurns,
594
+ command: profile.command,
595
+ });
596
+ }
597
+ catch (error) {
598
+ outcome = 'preflight';
599
+ throw preflightFailure(error);
600
+ }
601
+ phase = 'auth';
298
602
  try {
299
603
  await this.verifyAuth(definition.cli, {
300
604
  command: profile.command,
301
- cwd: this.cwd,
605
+ cwd,
302
606
  timeoutMs: this.config.authProbeTimeoutMs,
303
607
  maxOutputBytes: this.config.maxAuthProbeBytes,
304
608
  extraEnvNames: this.config.extraEnvNames,
@@ -317,24 +621,16 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
317
621
  outcome = outcomeFor(error);
318
622
  throw cliFailure(definition, profile.command, error);
319
623
  }
320
- // Preflight prompt serialization runs before spawn; a failure here is provably not-submitted.
624
+ // Revalidate the authority boundary after auth so a stale/replaced session
625
+ // cannot reach the generation process. Catalog discovery belongs exclusively
626
+ // to listModels and never delays a generation request.
321
627
  phase = 'preflight';
322
- let invocation;
323
628
  try {
324
- invocation = buildInvocation(definition.cli, {
325
- cwd: this.cwd,
326
- prompt: buildPrompt(options, this.config.maxPromptBytes),
327
- model: options.model,
328
- ...(definition.cli !== 'cursor' && options.reasoningEffort !== undefined
329
- ? { reasoningEffort: String(options.reasoningEffort) }
330
- : {}),
331
- maxTurns: profile.maxTurns,
332
- command: profile.command,
333
- });
629
+ cwd = this.trustedCwd(options);
334
630
  }
335
631
  catch (error) {
336
- outcome = 'preflight';
337
- throw error;
632
+ outcome = outcomeFor(error);
633
+ throw cliFailure(definition, profile.command, error);
338
634
  }
339
635
  const runnerOptions = {
340
636
  timeoutMs: this.config.timeoutMs,
@@ -343,7 +639,7 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
343
639
  maxOutputBytes: this.config.maxOutputBytes,
344
640
  maxStderrBytes: this.config.maxStderrBytes,
345
641
  extraEnvNames: this.config.extraEnvNames,
346
- onDiagnostic: diagnostic => this.onDiagnostic?.(definition.route, diagnostic),
642
+ onDiagnostic: diagnostic => this.onDiagnostic?.(definition.route, redactDiagnostic(diagnostic)),
347
643
  onSettled: context => { transportContext = context; },
348
644
  signal,
349
645
  };
@@ -357,6 +653,7 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
357
653
  // Calling an injected runner may itself perform work or throw synchronously, so
358
654
  // stop claiming not-submitted immediately before control crosses that boundary.
359
655
  promptSubmissionState = 'unknown';
656
+ invocation = { ...invocation, cwd: this.trustedCwd(options) };
360
657
  const deltas = this.runText(invocation, runnerOptions);
361
658
  for await (const delta of deltas) {
362
659
  if (delta.length === 0)
@@ -380,11 +677,12 @@ export class CodingSubscriptionAdapter extends LlmAdapter {
380
677
  }
381
678
  /** Abort every active/future subprocess when the owning Cordis fiber unloads. */
382
679
  shutdown() {
383
- this.codexCatalogCache = undefined;
384
- this.subscriptionCatalogCache.clear();
385
680
  if (!this.lifecycle.signal.aborted) {
386
681
  this.lifecycle.abort(new Error('coding subscription provider unloaded', { cause: 'abort' }));
387
682
  }
683
+ this.codexCatalogCache = undefined;
684
+ this.subscriptionCatalogCache.clear();
685
+ this.catalogRefreshes.clear();
388
686
  }
389
687
  }
390
688
  export function enabledRoutes(config) {