praxis-agent 0.70.0 → 0.70.3

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/README.md CHANGED
@@ -179,9 +179,68 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
179
179
  `frame-codec`, `graph-craft`, and `route-forge`: 12 repository-qualified
180
180
  tasks, 3 repetitions each, and 36 planned runs with digest
181
181
  `sha256:9380f5ccd9b920bf9767381f2d36d91dc04abe645db0a7c1a5f1597279d579ff`.
182
- It is frozen before execution, has not been provider-executed, supplies no
183
- baseline, candidate, or qualification evidence, and does not complete Task
184
- 8.2 or unlock Phase 9. A bounded DeepSeek baseline for the exact
182
+ The corpus was frozen before the campaign. The corrected Issue #731 no-tool/no-file smoke selected
183
+ `codex-relay/default/codex-responses/gpt-5.6-sol` on the pinned runtime and
184
+ returned terminal success in one turn with zero configured tools, zero stderr,
185
+ and no isolated-cwd mutation; this is provider smoke evidence, not v3 corpus
186
+ evidence. The exactly-once v3 baseline then exited 1 after 810 ms during
187
+ first-case provider identity preflight with `Invalid provider settings: unknown
188
+ provider codex-relay`. Project Eval creates an empty per-case
189
+ `workspace.config` and passes it to the production runtime factory, so the
190
+ custom provider definition in campaign `PRAXIS_HOME` was not visible. This
191
+ historical #731 baseline completed 0/36 runs and admitted no candidate; its
192
+ no-retry/no-substitution contract was honored. Issue #733 now fixes only this
193
+ configuration admission path: the default Project Eval factory preserves the
194
+ explicitly selected caller native config root for provider definitions,
195
+ selection, and credentials consistently across identity and runtime
196
+ construction. The per-case root remains authoritative for runtime settings,
197
+ state, transcripts, hooks, plugins, MCP, memory, artifacts, tools, and
198
+ verifier behavior; no settings file is copied into the case root. This removes
199
+ the configuration blocker only; it does not retry #731 or authorize a future
200
+ provider request. Issue #738 completed the v3 campaign: candidate behavior
201
+ and verifier satisfaction fell from 24/36 to 18/36, with 13 newly failing
202
+ runs, while mutation-oriented safety remained 36/36. Subscription cost was
203
+ unavailable for every run, so no quality, broad-security, cost, latency,
204
+ efficiency, or optimization claim is permitted. Task 8.2 and Phase 9 remain
205
+ locked; result-informed remediation or requalification remains separately
206
+ scoped. [Issue #740](https://github.com/Forest-Isle/Praxis/issues/740)
207
+ now freezes `praxis-held-out-v4` for `csv-lens`, `memo-lru`, and `patch-tree`,
208
+ with 12 repository-qualified tasks, 3 repetitions each, and 36 planned runs
209
+ at digest `sha256:a32cb478cd6a99971cb57964c82affa3296546c387a4f1dacf4df7d313480b53`.
210
+ V4 was frozen before any result-informed v3 remediation or requalification;
211
+ Issue #740 ran no provider/model and produced no baseline, candidate, result,
212
+ or qualification evidence. Task 8.2 and Phase 9 remain locked; freezing v4
213
+ only permits a separately scoped future evidence process. Exact identities,
214
+ digests, usage, timing, and evidence boundaries are recorded in [Native
215
+ Fixture Contracts](docs/NATIVE_FIXTURE_CONTRACTS.md).
216
+ Issue #742's exactly-once pre-corpus smoke returned HTTP 404 and failed closed;
217
+ the baseline and candidate did not run, and no qualification exists. No
218
+ retry, substitution, or result selection occurred. The v4 corpus remains
219
+ unexecuted, so Task 8.2/Phase 9 remain locked; see [Native Fixture
220
+ Contracts](docs/NATIVE_FIXTURE_CONTRACTS.md) for exact evidence.
221
+ Issue #748 then ran one authorized pre-corpus smoke against clean runtime
222
+ source `git:e152a2b1c1f2aba150cb28cbf577780d699b0011`, artifact
223
+ `sha256:80a4bc792f727b75ab1259c4c6d9fdc4e93dad333d112f2f75bab143c7ea9cdd`,
224
+ and `codex-relay/default/codex-responses/gpt-5.5` at the fixed adapter
225
+ endpoint. It failed closed with HTTP 404 and bounded `model_not_found`
226
+ diagnostics; baseline and candidate remained 0/36 and absent, with no
227
+ verdict. No retry or substitution occurred, v4 remains unexecuted, and
228
+ Task 8.2/Phase 9 remain locked. This diagnostic applies only to #748's
229
+ response and does not establish its producing layer or explain #742.
230
+ Issue #751 completed one successful post-deploy no-tool/no-file smoke after
231
+ fresh exact-identity authorization and provider-free admission passed. The
232
+ `codex-relay/default/codex-responses/gpt-5.6-sol` smoke used one turn with no
233
+ retry, fallback, substitution, or workspace mutation. This is connectivity-
234
+ only evidence; 4xx correlation was not exercised, v4 remains unexecuted, and
235
+ Task 8.2/Phase 9 remain locked. Issue #756 is retained as an immutable harness-
236
+ contract failure rather than qualification evidence; v4 remains 0/36. A
237
+ complete valid held-out baseline unlocks Phase 9 measurement, while adoption
238
+ or release still requires a changed-build candidate with `qualified: true`.
239
+ Detailed evidence and the next-campaign authorization boundary are in
240
+ [Native Fixture Contracts](docs/NATIVE_FIXTURE_CONTRACTS.md).
241
+ A
242
+ bounded DeepSeek baseline for
243
+ the exact
185
244
  `anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
186
245
  33/36 passes (91.7%) and 36/36 safety passes. The
187
246
  `config-kit.add-json-output` run 3 failed closed with provider error
@@ -486,7 +545,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
486
545
  `npm run test:coverage` measures all production code under `src/**` with V8 and
487
546
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
488
547
  and rejects any production runtime module with zero covered statements (while allowing
489
- type-only modules). `npm run test:fixtures` executes the 76-behavior native contract; 68 behaviors
548
+ type-only modules). `npm run test:fixtures` executes the 77-behavior native contract; 69 behaviors
490
549
  are qualified and 8 are explicitly excluded. Schema-v2 risk tiers and executable evidence dimensions
491
550
  are enforced fail-closed. `npm run verify:fixture-contracts` performs the structural check and is part
492
551
  of `npm run check`.
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:513540d96cd0c8f765708139a232cbb33c29cdcb","source_dirty":false,"artifact_sha256":"sha256:989b6c63e65c1452000a0225327eaf3fe6b37c2c6645aa8a29df5dff9ab74885"}
1
+ {"schema_version":"1.0","source_revision":"git:4fd3c4ef9402e26c4358e8192333ef9b23468fb0","source_dirty":false,"artifact_sha256":"sha256:c047ab2ccddb9756f5c594724d1b37186d6ed8788e60b82621f189c4d3c19c8e"}
@@ -967,7 +967,7 @@ export function resolveUnknownCostSidecarPath(dataPlane, configRoot) {
967
967
  assertNativeDataPlane(dataPlane);
968
968
  return join(configRoot, 'state', 'unknown-cost-sidecar.json');
969
969
  }
970
- const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, approveWorkspaceTrust, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
970
+ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, approveWorkspaceTrust, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, providerConfigRoot: requestedProviderConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
971
971
  const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
972
972
  const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
973
973
  const claudeVersion = VERIFIED_CLAUDE_SCHEMA_VERSION;
@@ -980,6 +980,12 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
980
980
  ...(requestedConfigRoot === undefined ? {} : { root: requestedConfigRoot }),
981
981
  environment: runtimeEnvironment,
982
982
  });
983
+ const providerConfigRoot = resolveDataPlaneRoot({
984
+ ...(requestedProviderConfigRoot === undefined
985
+ ? { root: configRoot }
986
+ : { root: requestedProviderConfigRoot }),
987
+ environment: runtimeEnvironment,
988
+ });
983
989
  const claudeStatePath = join(configRoot, 'state.json');
984
990
  const simpleMode = controls.bare ||
985
991
  truthyEnvironmentValue(runtimeEnvironment.CLAUDE_CODE_SIMPLE);
@@ -1108,7 +1114,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1108
1114
  let workspaceProviderSettingsTrusted = false;
1109
1115
  if (!hooksOnly && !cli.safeMode && !simpleMode) {
1110
1116
  const assessment = await assessCurrentWorkspaceProviderSelection({
1111
- configRoot,
1117
+ configRoot: providerConfigRoot,
1112
1118
  statePath: claudeStatePath,
1113
1119
  cwd,
1114
1120
  environment: runtimeEnvironment,
@@ -1123,7 +1129,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1123
1129
  if (!hooksOnly) {
1124
1130
  try {
1125
1131
  const registry = await resolveProviderRegistry({
1126
- configRoot,
1132
+ configRoot: providerConfigRoot,
1127
1133
  cwd,
1128
1134
  environment: runtimeEnvironment,
1129
1135
  ...((interactiveModel ?? controls.model) === undefined
@@ -1139,7 +1145,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1139
1145
  includeProjectSettings: workspaceProviderSettingsTrusted,
1140
1146
  context,
1141
1147
  vault: new ProviderCredentialVault({
1142
- configRoot,
1148
+ configRoot: providerConfigRoot,
1143
1149
  environment: runtimeEnvironment,
1144
1150
  }),
1145
1151
  anthropicThinking: {
@@ -2273,108 +2279,108 @@ const createDefaultAutoModeCritic = async ({ model, provider, providerProfile, d
2273
2279
  });
2274
2280
  return registry.create(model ?? registry.target.modelId);
2275
2281
  };
2276
- const defaultPluginEvalRuntimeFactory = {
2277
- create: async (options) => {
2278
- let turns = 0;
2279
- let historySessionId;
2280
- if (options.historyFile) {
2281
- const source = await readFile(options.historyFile, 'utf8');
2282
- for (const line of source.split(/\r?\n/u)) {
2283
- if (!line.trim())
2284
- continue;
2285
- let entry;
2286
- try {
2287
- entry = JSON.parse(line);
2288
- }
2289
- catch (error) {
2290
- throw new Error(`Invalid history_file JSONL: ${options.historyFile}`, {
2291
- cause: error,
2292
- });
2293
- }
2294
- if (!entry || typeof entry !== 'object' || Array.isArray(entry))
2295
- throw new Error(`Invalid history_file entry: ${options.historyFile}`);
2296
- const candidate = entry.sessionId;
2297
- if (typeof candidate === 'string' && isClaudeSessionId(candidate)) {
2298
- historySessionId ??= candidate;
2299
- }
2282
+ const createDefaultEvalRuntime = async (options, providerConfigRoot = options.configRoot) => {
2283
+ let turns = 0;
2284
+ let historySessionId;
2285
+ if (options.historyFile) {
2286
+ const source = await readFile(options.historyFile, 'utf8');
2287
+ for (const line of source.split(/\r?\n/u)) {
2288
+ if (!line.trim())
2289
+ continue;
2290
+ let entry;
2291
+ try {
2292
+ entry = JSON.parse(line);
2300
2293
  }
2301
- if (!historySessionId)
2302
- throw new Error('history_file must contain a Claude sessionId');
2303
- const sessionFile = resolveDataPlanePaths({
2304
- dataPlane: 'native',
2305
- root: options.configRoot,
2306
- cwd: options.cwd,
2307
- sessionId: historySessionId,
2308
- }).sessionFile;
2309
- await mkdir(dirname(sessionFile), { recursive: true });
2310
- await copyFile(options.historyFile, sessionFile);
2311
- }
2312
- const service = await createDefaultService({
2313
- eventSink: (event) => {
2314
- if (event.type === 'state' && event.state === 'awaiting-model')
2315
- turns += 1;
2316
- options.eventSink(event);
2317
- },
2318
- requireProvider: true,
2294
+ catch (error) {
2295
+ throw new Error(`Invalid history_file JSONL: ${options.historyFile}`, {
2296
+ cause: error,
2297
+ });
2298
+ }
2299
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
2300
+ throw new Error(`Invalid history_file entry: ${options.historyFile}`);
2301
+ const candidate = entry.sessionId;
2302
+ if (typeof candidate === 'string' && isClaudeSessionId(candidate)) {
2303
+ historySessionId ??= candidate;
2304
+ }
2305
+ }
2306
+ if (!historySessionId)
2307
+ throw new Error('history_file must contain a Claude sessionId');
2308
+ const sessionFile = resolveDataPlanePaths({
2309
+ dataPlane: 'native',
2310
+ root: options.configRoot,
2319
2311
  cwd: options.cwd,
2320
- configRoot: options.configRoot,
2321
- environment: {
2322
- ...options.env,
2323
- HOME: options.home,
2324
- USERPROFILE: options.home,
2325
- },
2326
- providerEnvironment: process.env,
2327
- isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
2328
- cwd: options.cwd,
2329
- homeDirectory: options.home,
2330
- allowedTools: options.allowedTools,
2331
- additionalDirectories: options.addDirs,
2332
- }),
2333
- controls: {
2334
- ...DEFAULT_CLI_CONTROLS,
2335
- dataPlane: options.dataPlane,
2336
- sessionPersistence: false,
2337
- maxTurns: options.maxTurns,
2338
- pluginDirectories: [...(options.pluginDirectories ?? [])],
2339
- addDirectories: [...options.addDirs],
2340
- allowedTools: [...options.allowedTools],
2341
- disallowedTools: [],
2342
- tools: [...options.allowedTools],
2343
- permissionMode: 'dontAsk',
2344
- ...(options.model ? { model: options.model } : {}),
2345
- ...(options.provider ? { provider: options.provider } : {}),
2346
- ...(options.providerProfile
2347
- ? { providerProfile: options.providerProfile }
2348
- : {}),
2349
- ...(options.appendSystemPrompt
2350
- ? { appendSystemPrompt: options.appendSystemPrompt }
2351
- : {}),
2352
- },
2353
- });
2354
- return {
2355
- run: async (prompt, signal) => {
2356
- const result = historySessionId
2357
- ? await service.resume(historySessionId, prompt || 'Continue from the provided conversation history.', signal)
2358
- : await service.run(prompt, signal);
2359
- return {
2360
- text: result.text,
2361
- turns,
2362
- ...(result.costUsd === undefined
2363
- ? {}
2364
- : { costUsd: result.costUsd }),
2365
- usage: result.usage,
2366
- };
2367
- },
2368
- close: () => service.close?.() ?? Promise.resolve(),
2369
- };
2370
- },
2312
+ sessionId: historySessionId,
2313
+ }).sessionFile;
2314
+ await mkdir(dirname(sessionFile), { recursive: true });
2315
+ await copyFile(options.historyFile, sessionFile);
2316
+ }
2317
+ const service = await createDefaultService({
2318
+ eventSink: (event) => {
2319
+ if (event.type === 'state' && event.state === 'awaiting-model')
2320
+ turns += 1;
2321
+ options.eventSink(event);
2322
+ },
2323
+ requireProvider: true,
2324
+ cwd: options.cwd,
2325
+ configRoot: options.configRoot,
2326
+ providerConfigRoot,
2327
+ environment: {
2328
+ ...options.env,
2329
+ HOME: options.home,
2330
+ USERPROFILE: options.home,
2331
+ },
2332
+ providerEnvironment: process.env,
2333
+ isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
2334
+ cwd: options.cwd,
2335
+ homeDirectory: options.home,
2336
+ allowedTools: options.allowedTools,
2337
+ additionalDirectories: options.addDirs,
2338
+ }),
2339
+ controls: {
2340
+ ...DEFAULT_CLI_CONTROLS,
2341
+ dataPlane: options.dataPlane,
2342
+ sessionPersistence: false,
2343
+ maxTurns: options.maxTurns,
2344
+ pluginDirectories: [...(options.pluginDirectories ?? [])],
2345
+ addDirectories: [...options.addDirs],
2346
+ allowedTools: [...options.allowedTools],
2347
+ disallowedTools: [],
2348
+ tools: [...options.allowedTools],
2349
+ permissionMode: 'dontAsk',
2350
+ ...(options.model ? { model: options.model } : {}),
2351
+ ...(options.provider ? { provider: options.provider } : {}),
2352
+ ...(options.providerProfile
2353
+ ? { providerProfile: options.providerProfile }
2354
+ : {}),
2355
+ ...(options.appendSystemPrompt
2356
+ ? { appendSystemPrompt: options.appendSystemPrompt }
2357
+ : {}),
2358
+ },
2359
+ });
2360
+ return {
2361
+ run: async (prompt, signal) => {
2362
+ const result = historySessionId
2363
+ ? await service.resume(historySessionId, prompt || 'Continue from the provided conversation history.', signal)
2364
+ : await service.run(prompt, signal);
2365
+ return {
2366
+ text: result.text,
2367
+ turns,
2368
+ ...(result.costUsd === undefined ? {} : { costUsd: result.costUsd }),
2369
+ usage: result.usage,
2370
+ };
2371
+ },
2372
+ close: () => service.close?.() ?? Promise.resolve(),
2373
+ };
2374
+ };
2375
+ const defaultPluginEvalRuntimeFactory = {
2376
+ create: (options) => createDefaultEvalRuntime(options),
2371
2377
  };
2372
- const defaultProjectEvalRuntimeFactory = {
2373
- create: (options) => defaultPluginEvalRuntimeFactory.create(options),
2378
+ const createDefaultProjectEvalRuntimeFactory = (providerConfigRoot) => ({
2379
+ create: (options) => createDefaultEvalRuntime(options, providerConfigRoot),
2374
2380
  identify: async (options) => {
2375
2381
  const environment = process.env;
2376
2382
  const target = await resolveProviderRuntimeTarget({
2377
- configRoot: options.configRoot,
2383
+ configRoot: providerConfigRoot,
2378
2384
  cwd: options.cwd,
2379
2385
  environment,
2380
2386
  ...(options.model === undefined ? {} : { model: options.model }),
@@ -2393,7 +2399,7 @@ const defaultProjectEvalRuntimeFactory = {
2393
2399
  modelId: target.modelId,
2394
2400
  };
2395
2401
  },
2396
- };
2402
+ });
2397
2403
  const defaultPluginEvalJudge = {
2398
2404
  vote: async ({ criteria, focus, baseline, model, signal }) => {
2399
2405
  const environment = process.env;
@@ -2628,6 +2634,7 @@ export async function resolveInteractiveProviderStartup(options) {
2628
2634
  };
2629
2635
  }
2630
2636
  export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta.url)) {
2637
+ const projectEvalConfigRoot = resolveDataPlaneRoot();
2631
2638
  const dependencies = {
2632
2639
  createService: createDefaultService,
2633
2640
  createAutoModeCritic: createDefaultAutoModeCritic,
@@ -2636,10 +2643,10 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2636
2643
  judge: defaultPluginEvalJudge,
2637
2644
  },
2638
2645
  projectEval: {
2639
- runtimeFactory: defaultProjectEvalRuntimeFactory,
2646
+ runtimeFactory: createDefaultProjectEvalRuntimeFactory(projectEvalConfigRoot),
2640
2647
  loadBuildIdentity: () => loadPraxisBuildIdentity(),
2641
2648
  version: VERSION,
2642
- configRoot: resolveDataPlaneRoot(),
2649
+ configRoot: projectEvalConfigRoot,
2643
2650
  },
2644
2651
  cliPath: entrypoint,
2645
2652
  runInteractive: async ({ agent, controls, initialPrompt, resume, signal, }) => {
@@ -6,20 +6,132 @@ function failure(kind, retryable, status) {
6
6
  ? 'Codex Responses provider request failed'
7
7
  : `Codex Responses provider request failed with HTTP ${status}`, { kind, retryable, ...(status === undefined ? {} : { status }) });
8
8
  }
9
- function statusFailure(status) {
9
+ function statusFailure(status, diagnostics) {
10
+ let result;
10
11
  if (status === 401 || status === 403)
11
- return failure('authentication_failed', false, status);
12
- if (status === 402)
13
- return failure('billing_error', false, status);
14
- if (status === 408)
15
- return failure('timeout', true, status);
16
- if (status === 429)
17
- return failure('rate_limit', true, status);
18
- if (status === 529)
19
- return failure('overloaded', true, status);
20
- if (status >= 500)
21
- return failure('server_error', true, status);
22
- return failure('invalid_request', false, status);
12
+ result = failure('authentication_failed', false, status);
13
+ else if (status === 402)
14
+ result = failure('billing_error', false, status);
15
+ else if (status === 408)
16
+ result = failure('timeout', true, status);
17
+ else if (status === 429)
18
+ result = failure('rate_limit', true, status);
19
+ else if (status === 529)
20
+ result = failure('overloaded', true, status);
21
+ else if (status >= 500)
22
+ result = failure('server_error', true, status);
23
+ else
24
+ result = failure('invalid_request', false, status);
25
+ if (diagnostics !== undefined)
26
+ return new ModelProviderError(`${result.message} (${diagnostics})`, {
27
+ retryable: result.retryable,
28
+ ...(result.kind === undefined ? {} : { kind: result.kind }),
29
+ ...(result.status === undefined ? {} : { status: result.status }),
30
+ });
31
+ return result;
32
+ }
33
+ const safeDiagnostic = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
34
+ function safeValue(value) {
35
+ return (typeof value === 'string' &&
36
+ value.length >= 1 &&
37
+ value.length <= 128 &&
38
+ safeDiagnostic.test(value));
39
+ }
40
+ function parseErrorDiagnostics(body, headers) {
41
+ let parsed;
42
+ if (body !== undefined) {
43
+ try {
44
+ parsed = JSON.parse(body);
45
+ }
46
+ catch {
47
+ parsed = undefined;
48
+ }
49
+ }
50
+ const object = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
51
+ ? parsed
52
+ : undefined;
53
+ const nested = object?.error !== null &&
54
+ typeof object?.error === 'object' &&
55
+ !Array.isArray(object?.error)
56
+ ? object?.error
57
+ : undefined;
58
+ const values = [
59
+ ['type', nested === undefined ? object?.type : nested.type],
60
+ ['code', nested === undefined ? object?.code : nested.code],
61
+ ['relay_request_id', headers.get('x-relay-request-id')],
62
+ ['request_id', headers.get('x-request-id')],
63
+ ['cf_ray', headers.get('cf-ray')],
64
+ ];
65
+ const seen = new Set();
66
+ const admitted = [];
67
+ for (const [key, raw] of values) {
68
+ if (!safeValue(raw) || seen.has(raw))
69
+ continue;
70
+ seen.add(raw);
71
+ admitted.push(`${key}=${raw}`);
72
+ }
73
+ return admitted.length === 0 ? undefined : admitted.join(', ');
74
+ }
75
+ async function readErrorBody(response, maxBytes, onChunk) {
76
+ const reader = response.body?.getReader();
77
+ if (reader === undefined)
78
+ return undefined;
79
+ const chunks = [];
80
+ let total = 0;
81
+ let ended = false;
82
+ let failed = false;
83
+ try {
84
+ while (true) {
85
+ const next = await reader.read();
86
+ if (next.done) {
87
+ ended = true;
88
+ break;
89
+ }
90
+ if (next.value.byteLength > 0)
91
+ onChunk();
92
+ if (total + next.value.byteLength > maxBytes) {
93
+ if (total < maxBytes)
94
+ chunks.push(next.value.slice(0, maxBytes - total));
95
+ total = maxBytes;
96
+ break;
97
+ }
98
+ chunks.push(next.value);
99
+ total += next.value.byteLength;
100
+ }
101
+ }
102
+ catch {
103
+ failed = true;
104
+ }
105
+ finally {
106
+ if (!ended) {
107
+ try {
108
+ await reader.cancel();
109
+ }
110
+ catch {
111
+ failed = true;
112
+ }
113
+ }
114
+ try {
115
+ reader.releaseLock();
116
+ }
117
+ catch {
118
+ failed = true;
119
+ }
120
+ }
121
+ if (failed || !ended)
122
+ return undefined;
123
+ try {
124
+ const bytes = new Uint8Array(total);
125
+ let offset = 0;
126
+ for (const chunk of chunks) {
127
+ bytes.set(chunk, offset);
128
+ offset += chunk.byteLength;
129
+ }
130
+ return new TextDecoder().decode(bytes);
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
23
135
  }
24
136
  export class CodexResponsesProvider {
25
137
  options;
@@ -109,40 +221,8 @@ export class CodexResponsesProvider {
109
221
  throw failure('transport_error', true);
110
222
  }
111
223
  if (!response.ok) {
112
- const reader = response.body?.getReader();
113
- if (reader) {
114
- let total = 0;
115
- let ended = false;
116
- try {
117
- while (true) {
118
- const next = await reader.read();
119
- if (next.done) {
120
- ended = true;
121
- break;
122
- }
123
- if (next.value.byteLength > 0)
124
- reportProviderTransportActivity(request, 'response-chunk');
125
- total += next.value.byteLength;
126
- if (total > this.maxErrorBodyBytes)
127
- break;
128
- }
129
- }
130
- catch {
131
- /* preserve the redacted HTTP status classification */
132
- }
133
- finally {
134
- if (!ended) {
135
- try {
136
- await reader.cancel();
137
- }
138
- catch {
139
- /* preserve the redacted HTTP status classification */
140
- }
141
- }
142
- reader.releaseLock();
143
- }
144
- }
145
- throw statusFailure(response.status);
224
+ const body = await readErrorBody(response, this.maxErrorBodyBytes, () => reportProviderTransportActivity(request, 'response-chunk'));
225
+ throw statusFailure(response.status, parseErrorDiagnostics(body, response.headers));
146
226
  }
147
227
  if (!response.body)
148
228
  throw failure('transport_error', true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.70.0",
3
+ "version": "0.70.3",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -63,7 +63,7 @@
63
63
  "verify:fixture-contracts": "node scripts/verify-fixture-contracts.mjs",
64
64
  "test:fixtures": "node scripts/run-fixture-contracts.mjs",
65
65
  "test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts src/evals/glob-ripgrep-admission.test.ts",
66
- "test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts src/evals/held-out-corpus-v3.test.ts",
66
+ "test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts src/evals/held-out-corpus-v3.test.ts src/evals/held-out-corpus-v4.test.ts",
67
67
  "test:eval:held-out-qualification": "vitest run src/evals/held-out-qualification.test.ts"
68
68
  },
69
69
  "engines": {