@bitkyc08/opencodex 2.48.0-preview.20260908 → 2.49.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 (97) hide show
  1. package/README.md +8 -5
  2. package/SPONSORS.md +1 -1
  3. package/assets/sponsors/orcarouter.png +0 -0
  4. package/assets/sponsors/packycode.png +0 -0
  5. package/gui/dist/assets/index-BoBRSehJ.css +1 -0
  6. package/gui/dist/assets/index-BtyONQrZ.js +115 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/packycode.svg +19 -0
  9. package/gui/dist/provider-icons/qoder.svg +5 -0
  10. package/package.json +5 -3
  11. package/src/adapters/anthropic.ts +31 -16
  12. package/src/adapters/codebuddy/adapter.ts +85 -0
  13. package/src/adapters/codebuddy/profiles.ts +52 -0
  14. package/src/adapters/coding-agent/profile.ts +100 -0
  15. package/src/adapters/coding-agent/protocol.ts +463 -0
  16. package/src/adapters/coding-agent/turn.ts +353 -0
  17. package/src/adapters/google.ts +15 -11
  18. package/src/adapters/mimo-free.ts +3 -0
  19. package/src/adapters/openai-chat.ts +2 -2
  20. package/src/adapters/openai-responses.ts +18 -11
  21. package/src/adapters/qoder/adapter.ts +70 -0
  22. package/src/adapters/qoder/live-models.ts +89 -0
  23. package/src/adapters/qoder/profiles.ts +36 -0
  24. package/src/adapters/registry.ts +12 -0
  25. package/src/adapters/responses-tool-schema.ts +113 -8
  26. package/src/cli/dispatch.ts +2 -0
  27. package/src/cli/doctor.ts +40 -0
  28. package/src/cli/effort.ts +24 -8
  29. package/src/cli/help.ts +2 -0
  30. package/src/cli/index.ts +24 -1
  31. package/src/cli/provider-runtime.ts +2 -1
  32. package/src/cli/registry.ts +2 -2
  33. package/src/cli/system-command.ts +10 -3
  34. package/src/cli/usage-report.ts +9 -5
  35. package/src/codex/account-lifecycle.ts +35 -2
  36. package/src/codex/auth-api.ts +6 -3
  37. package/src/codex/auth-collision.ts +12 -2
  38. package/src/codex/auth-context.ts +75 -7
  39. package/src/codex/catalog/provider-fetch.ts +73 -9
  40. package/src/codex/inject.ts +127 -30
  41. package/src/codex/internal/catalog-writer.ts +3 -0
  42. package/src/codex/journal.ts +61 -12
  43. package/src/codex/model-cache.ts +11 -4
  44. package/src/codex/native-profile-startup.ts +72 -5
  45. package/src/codex/native-profile-store.ts +2 -2
  46. package/src/codex/ocx-compaction-history.ts +226 -0
  47. package/src/codex/project-config-warnings.ts +3 -1
  48. package/src/codex/quota.ts +18 -8
  49. package/src/codex/reserve-availability.ts +21 -5
  50. package/src/codex/runtime.ts +45 -1
  51. package/src/codex/sync.ts +5 -0
  52. package/src/config.ts +1 -0
  53. package/src/generated/compatibility-version.json +126 -74
  54. package/src/images/loop.ts +1 -0
  55. package/src/images/xai-video-client.ts +2 -0
  56. package/src/integrations/registry.ts +1 -0
  57. package/src/lib/process-control.ts +52 -8
  58. package/src/lib/upstream-retry.ts +1 -0
  59. package/src/oauth/chatgpt.ts +83 -0
  60. package/src/oauth/xai.ts +151 -8
  61. package/src/providers/api-key-selection-capture.ts +10 -0
  62. package/src/providers/api-key-selection.ts +2 -7
  63. package/src/providers/caller-authorization.ts +36 -0
  64. package/src/providers/codebuddy-models.ts +184 -0
  65. package/src/providers/derive.ts +5 -0
  66. package/src/providers/free-directory.ts +26 -2
  67. package/src/providers/openai-sidecar.ts +35 -11
  68. package/src/providers/qoder-models.ts +25 -0
  69. package/src/providers/registry.ts +139 -0
  70. package/src/responses/compaction.ts +4 -0
  71. package/src/responses/task-input.ts +21 -1
  72. package/src/router.ts +1 -1
  73. package/src/server/chat-completions.ts +26 -9
  74. package/src/server/claude-messages.ts +12 -2
  75. package/src/server/images.ts +1 -0
  76. package/src/server/management/account-selection-stream.ts +13 -4
  77. package/src/server/management/config-routes.ts +24 -5
  78. package/src/server/management/logs-usage-routes.ts +1 -0
  79. package/src/server/management/native-integration-routes.ts +2 -1
  80. package/src/server/management/provider-routes.ts +24 -0
  81. package/src/server/management-api.ts +14 -0
  82. package/src/server/port-reclaim.ts +11 -26
  83. package/src/server/request-log.ts +6 -0
  84. package/src/server/responses/collaboration.ts +4 -9
  85. package/src/server/responses/core.ts +141 -19
  86. package/src/server/responses/fetch-helpers.ts +18 -7
  87. package/src/server/responses/policy-fallback.ts +12 -0
  88. package/src/service.ts +36 -2
  89. package/src/storage/cleanup.ts +77 -45
  90. package/src/types/config.ts +7 -1
  91. package/src/usage/log.ts +24 -0
  92. package/src/vision/anthropic-describe.ts +1 -0
  93. package/src/web-search/anthropic-executor.ts +1 -0
  94. package/src/web-search/loop.ts +1 -0
  95. package/src/web-search/progress-stream.ts +4 -0
  96. package/gui/dist/assets/index-B6EFoatP.js +0 -115
  97. package/gui/dist/assets/index-D5SiRo8X.css +0 -1
@@ -96,6 +96,7 @@ export interface CleanupResult {
96
96
  trashDir?: string;
97
97
  error?: CleanupErrorCode;
98
98
  removedPaths: string[];
99
+ skippedReferencedPaths?: string[];
99
100
  }
100
101
 
101
102
  const STATE_DB_FILE = /^state_(\d+)\.sqlite$/;
@@ -684,52 +685,59 @@ function loadMatchingThreads(db: Database, candidates: ArchivedCandidate[], code
684
685
  }
685
686
 
686
687
  /**
687
- * True when any matched thread is still linked to a thread outside the delete set
688
- * (spawn edges) or uses paginated history that other live threads may depend on via fork.
689
- * Throws real DB errors (busy/corruption) so callers can refuse cleanup.
688
+ * Partition matched threads into deletable and referenced snapshots. Linked spawn/fork
689
+ * history and paginated histories stay in the skipped set. Throws real DB errors.
690
690
  */
691
- function findReferencedHistory(
691
+ function filterReferencedHistory(
692
692
  db: Database,
693
693
  threads: ThreadSnapshot[],
694
- ): boolean {
695
- if (threads.length === 0) return false;
696
- const ids = threads.map(t => t.id);
697
- const idSet = new Set(ids);
698
-
699
- // Paginated history keeps durable projections tied to the rollout — refuse cleanup.
700
- if (threads.some(t => (t.history_mode ?? "").toLowerCase() === "paginated")) {
701
- return true;
702
- }
703
-
704
- // Spawn edges that cross the delete boundary keep history reachable.
705
- if (tableExists(db, "thread_spawn_edges")) {
706
- for (const chunk of chunkIds(ids, SQLITE_ID_CHUNK)) {
694
+ ): { safe: ThreadSnapshot[]; skipped: ThreadSnapshot[] } {
695
+ let safe = threads.filter(t => (t.history_mode ?? "").toLowerCase() !== "paginated");
696
+ const skipped = new Map(threads
697
+ .filter(t => (t.history_mode ?? "").toLowerCase() === "paginated")
698
+ .map(t => [t.id, t]));
699
+
700
+ while (safe.length > 0) {
701
+ const idSet = new Set(safe.map(t => t.id));
702
+ const unsafeIds = new Set<string>();
703
+
704
+ // Spawn edges that cross the delete boundary keep history reachable.
705
+ if (tableExists(db, "thread_spawn_edges")) {
706
+ for (const chunk of chunkIds([...idSet], SQLITE_ID_CHUNK)) {
707
707
  const placeholders = chunk.map(() => "?").join(",");
708
708
  const edges = db.query<{ parent_thread_id: string; child_thread_id: string }, string[]>(
709
709
  `SELECT parent_thread_id, child_thread_id FROM thread_spawn_edges
710
710
  WHERE parent_thread_id IN (${placeholders}) OR child_thread_id IN (${placeholders})`,
711
711
  ).all(...chunk, ...chunk);
712
712
  for (const edge of edges) {
713
- if (!idSet.has(edge.parent_thread_id) || !idSet.has(edge.child_thread_id)) {
714
- return true;
713
+ if (!idSet.has(edge.parent_thread_id)) unsafeIds.add(edge.child_thread_id);
714
+ if (!idSet.has(edge.child_thread_id)) unsafeIds.add(edge.parent_thread_id);
715
+ }
716
+ }
717
+ }
718
+
719
+ // Other threads that list one of ours as forked_from / parent (when columns exist).
720
+ for (const column of ["forked_from_id", "parent_thread_id", "source_thread_id"] as const) {
721
+ if (!columnExists(db, "threads", column)) continue;
722
+ for (const chunk of chunkIds([...idSet], SQLITE_ID_CHUNK * 2)) {
723
+ const placeholders = chunk.map(() => "?").join(",");
724
+ const rows = db.query<{ id: string; ref: string }, string[]>(
725
+ `SELECT id, ${column} AS ref FROM threads WHERE ${column} IN (${placeholders})`,
726
+ ).all(...chunk);
727
+ for (const row of rows) {
728
+ if (!idSet.has(row.id)) unsafeIds.add(row.ref);
715
729
  }
716
730
  }
717
731
  }
718
- }
719
732
 
720
- // Other threads that list one of ours as forked_from / parent (when columns exist).
721
- for (const column of ["forked_from_id", "parent_thread_id", "source_thread_id"] as const) {
722
- if (!columnExists(db, "threads", column)) continue;
723
- for (const chunk of chunkIds(ids, SQLITE_ID_CHUNK * 2)) {
724
- const placeholders = chunk.map(() => "?").join(",");
725
- const rows = db.query<{ id: string }, string[]>(
726
- `SELECT id FROM threads WHERE ${column} IN (${placeholders})`,
727
- ).all(...chunk);
728
- if (rows.some(r => !idSet.has(r.id))) return true;
733
+ if (unsafeIds.size === 0) break;
734
+ for (const thread of safe) {
735
+ if (unsafeIds.has(thread.id)) skipped.set(thread.id, thread);
729
736
  }
737
+ safe = safe.filter(thread => !unsafeIds.has(thread.id));
730
738
  }
731
739
 
732
- return false;
740
+ return { safe, skipped: [...skipped.values()] };
733
741
  }
734
742
 
735
743
  function tableExists(db: Database, name: string): boolean {
@@ -778,6 +786,7 @@ function deleteThreadsAndDependents(db: Database, threadIds: string[]): void {
778
786
  interface ReconcileOk {
779
787
  ok: true;
780
788
  threads: ThreadSnapshot[];
789
+ skipped: ThreadSnapshot[];
781
790
  }
782
791
  interface ReconcileErr {
783
792
  ok: false;
@@ -1463,14 +1472,14 @@ function withWritableDb(
1463
1472
  }
1464
1473
  }
1465
1474
 
1466
- /** Load matching archived threads and refuse referenced history — no deletes yet. */
1475
+ /** Load matching archived threads and retain referenced history — no deletes yet. */
1467
1476
  function loadThreadsForCleanup(
1468
1477
  stateDbPath: string,
1469
1478
  candidates: ArchivedCandidate[],
1470
1479
  codexHome: string,
1471
1480
  busyTimeoutMs: number,
1472
1481
  ): ReconcileOk | ReconcileErr {
1473
- if (!stateDbPath || !existsSync(stateDbPath)) return { ok: true, threads: [] };
1482
+ if (!stateDbPath || !existsSync(stateDbPath)) return { ok: true, threads: [], skipped: [] };
1474
1483
  let db: Database | undefined;
1475
1484
  try {
1476
1485
  db = openDbWritable(stateDbPath, busyTimeoutMs);
@@ -1478,10 +1487,8 @@ function loadThreadsForCleanup(
1478
1487
  if (threads.some(t => Number(t.is_pinned ?? 0) === 1)) {
1479
1488
  return { ok: false, error: "pinned_thread" };
1480
1489
  }
1481
- if (findReferencedHistory(db, threads)) {
1482
- return { ok: false, error: "referenced_history" };
1483
- }
1484
- return { ok: true, threads };
1490
+ const filtered = filterReferencedHistory(db, threads);
1491
+ return { ok: true, threads: filtered.safe, skipped: filtered.skipped };
1485
1492
  } catch (error) {
1486
1493
  return { ok: false, error: mapDbError(error) };
1487
1494
  } finally {
@@ -1504,7 +1511,7 @@ function reconcileDeletedThreads(
1504
1511
  stageDir: string,
1505
1512
  hooks?: ReconcileTestHooks,
1506
1513
  ): ReconcileOk | ReconcileErr {
1507
- if (!paths.state || !existsSync(paths.state)) return { ok: true, threads: [] };
1514
+ if (!paths.state || !existsSync(paths.state)) return { ok: true, threads: [], skipped: [] };
1508
1515
 
1509
1516
  if (hooks?.beforeReconcileLock) hooks.beforeReconcileLock();
1510
1517
 
@@ -1546,7 +1553,7 @@ function reconcileDeletedThreads(
1546
1553
  stateDb.exec("ROLLBACK");
1547
1554
  return { ok: false, error: "pinned_thread" };
1548
1555
  }
1549
- if (findReferencedHistory(stateDb, threads)) {
1556
+ if (filterReferencedHistory(stateDb, threads).safe.length !== threads.length) {
1550
1557
  stateDb.exec("ROLLBACK");
1551
1558
  return { ok: false, error: "referenced_history" };
1552
1559
  }
@@ -1582,7 +1589,7 @@ function reconcileDeletedThreads(
1582
1589
  if (hooks?.afterSatelliteMutations) hooks.afterSatelliteMutations();
1583
1590
 
1584
1591
  // Re-check under the same lock before committing state deletes.
1585
- if (findReferencedHistory(stateDb, threads)) {
1592
+ if (filterReferencedHistory(stateDb, threads).safe.length !== threads.length) {
1586
1593
  stateDb.exec("ROLLBACK");
1587
1594
  return failWithRestore("referenced_history");
1588
1595
  }
@@ -1590,7 +1597,7 @@ function reconcileDeletedThreads(
1590
1597
  if (hooks?.failBeforeStateCommit) throw new Error("test_fail_before_state_commit");
1591
1598
  stateDb.exec("COMMIT");
1592
1599
  // Keep satellite-backup.json for quarantine restore; permanent purge removes the stage.
1593
- return { ok: true, threads };
1600
+ return { ok: true, threads, skipped: [] };
1594
1601
  } catch (error) {
1595
1602
  if (satelliteLocks) rollbackAllSatelliteLocks(satelliteLocks);
1596
1603
  throw error;
@@ -1895,7 +1902,30 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1895
1902
  const normalized = normalizeArchivedRolloutPath(thread.rollout_path, codexHome);
1896
1903
  if (normalized) threadByRelPath.set(normalized, thread);
1897
1904
  }
1898
- const manifestEntries: CleanupManifestEntry[] = preview.candidates.map(candidate => {
1905
+ const skippedReferencedPaths = loaded.skipped
1906
+ .map(thread => normalizeArchivedRolloutPath(thread.rollout_path, codexHome))
1907
+ .filter((path): path is string => path !== null);
1908
+ const matchedPaths = new Set([
1909
+ ...threadByRelPath.keys(),
1910
+ ...skippedReferencedPaths,
1911
+ ]);
1912
+ const candidates = preview.candidates.filter(candidate => {
1913
+ return !matchedPaths.has(candidate.relPath) || threadByRelPath.has(candidate.relPath);
1914
+ });
1915
+ if (candidates.length === 0) {
1916
+ removeStageIfEmpty(stageDir, []);
1917
+ removeEmptyTrashRoot(codexHome);
1918
+ return {
1919
+ ok: true,
1920
+ mode,
1921
+ percent,
1922
+ count: 0,
1923
+ bytes: 0,
1924
+ removedPaths: [],
1925
+ ...(skippedReferencedPaths.length ? { skippedReferencedPaths } : {}),
1926
+ };
1927
+ }
1928
+ const manifestEntries: CleanupManifestEntry[] = candidates.map(candidate => {
1899
1929
  const thread = threadByRelPath.get(candidate.relPath);
1900
1930
  return {
1901
1931
  relPath: candidate.relPath,
@@ -1936,7 +1966,7 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1936
1966
  return fail(mode, percent, "fs_failed");
1937
1967
  }
1938
1968
 
1939
- const stageResult = stageCandidates(codexHome, preview.candidates, stageDir, {
1969
+ const stageResult = stageCandidates(codexHome, candidates, stageDir, {
1940
1970
  blockDestBasenames: blockStageDest.size > 0 ? blockStageDest : undefined,
1941
1971
  });
1942
1972
  if (!stageResult.ok) {
@@ -1956,7 +1986,7 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1956
1986
 
1957
1987
  const deleted = reconcileDeletedThreads(
1958
1988
  paths,
1959
- preview.candidates,
1989
+ candidates,
1960
1990
  codexHome,
1961
1991
  busyTimeoutMs,
1962
1992
  stageDir,
@@ -1973,8 +2003,8 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1973
2003
  return fail(mode, percent, deleted.error, keepTrash ? { trashDir } : undefined);
1974
2004
  }
1975
2005
 
1976
- const removedPaths = preview.candidates.map(c => c.relPath);
1977
- const bytes = preview.candidates.reduce((sum, c) => sum + c.bytes, 0);
2006
+ const removedPaths = candidates.map(c => c.relPath);
2007
+ const bytes = candidates.reduce((sum, c) => sum + c.bytes, 0);
1978
2008
 
1979
2009
  if (mode === "quarantine") {
1980
2010
  return {
@@ -1985,6 +2015,7 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
1985
2015
  bytes,
1986
2016
  trashDir,
1987
2017
  removedPaths,
2018
+ ...(skippedReferencedPaths.length ? { skippedReferencedPaths } : {}),
1988
2019
  };
1989
2020
  }
1990
2021
 
@@ -2038,6 +2069,7 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
2038
2069
  count: removedPaths.length,
2039
2070
  bytes,
2040
2071
  removedPaths,
2072
+ ...(skippedReferencedPaths.length ? { skippedReferencedPaths } : {}),
2041
2073
  };
2042
2074
  }
2043
2075
 
@@ -693,6 +693,12 @@ export interface OcxConfig {
693
693
  * non-loopback binds, whose admission token contract is unchanged.
694
694
  */
695
695
  codexDesktopAuthless?: boolean;
696
+ /**
697
+ * Opt into Codex-owned client compaction while keeping OpenCodex routing. On an authenticated
698
+ * loopback bind, inject the dedicated `opencodex` model provider instead of overriding the
699
+ * built-in `openai` provider, so Codex does not select native remote compaction. Default off.
700
+ */
701
+ codexClientCompaction?: boolean;
696
702
  /**
697
703
  * Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
698
704
  * so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
@@ -753,7 +759,7 @@ export interface OcxConfig {
753
759
  */
754
760
  codexAccountPickerEnabled?: boolean;
755
761
  /**
756
- * Show the GPT-5.3-Codex-Spark weekly window on Codex quota surfaces. Default false.
762
+ * Show the GPT-5.3-Codex-Spark 5-hour and weekly windows on Codex quota surfaces. Default false.
757
763
  *
758
764
  * Spark is a single-model window that reads 0% for most operators, and on a multi-account
759
765
  * pool it doubles the bar count for information almost nobody acts on. Hidden by default and
package/src/usage/log.ts CHANGED
@@ -172,6 +172,10 @@ export interface PersistedUsageEntry {
172
172
  closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
173
173
  /** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
174
174
  upstreamError?: string;
175
+ /** Where the terminal/failure was observed; absent on historic rows. */
176
+ transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
177
+ /** Whether the terminal came from upstream or a proxy-generated tail. */
178
+ terminalSource?: "upstream" | "synthetic";
175
179
  /**
176
180
  * Bounded route-decision trace (RI-01): why this provider/model/account was
177
181
  * selected. Additive field; old rows without it parse unchanged. Never
@@ -217,6 +221,22 @@ export function isKnownInboundProtocol(value: unknown): value is NonNullable<Per
217
221
  return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
218
222
  }
219
223
 
224
+ const KNOWN_TRANSPORT_PHASES = new Set<NonNullable<PersistedUsageEntry["transportPhase"]>>([
225
+ "pre_headers", "mid_stream", "terminal_sse",
226
+ ]);
227
+
228
+ export function isKnownTransportPhase(value: unknown): value is NonNullable<PersistedUsageEntry["transportPhase"]> {
229
+ return typeof value === "string" && KNOWN_TRANSPORT_PHASES.has(value as NonNullable<PersistedUsageEntry["transportPhase"]>);
230
+ }
231
+
232
+ const KNOWN_TERMINAL_SOURCES = new Set<NonNullable<PersistedUsageEntry["terminalSource"]>>([
233
+ "upstream", "synthetic",
234
+ ]);
235
+
236
+ export function isKnownTerminalSource(value: unknown): value is NonNullable<PersistedUsageEntry["terminalSource"]> {
237
+ return typeof value === "string" && KNOWN_TERMINAL_SOURCES.has(value as NonNullable<PersistedUsageEntry["terminalSource"]>);
238
+ }
239
+
220
240
  export function usageLogPath(configDir?: string): string {
221
241
  return join(configDir ?? getConfigDir(), "usage.jsonl");
222
242
  }
@@ -511,6 +531,8 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
511
531
  const responseServiceTier = sanitizeLogMetadataString(entry.responseServiceTier);
512
532
  const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
513
533
  const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(entry.claudeCompatibility);
534
+ const transportPhase = isKnownTransportPhase(entry.transportPhase) ? entry.transportPhase : undefined;
535
+ const terminalSource = isKnownTerminalSource(entry.terminalSource) ? entry.terminalSource : undefined;
514
536
  const routeDecision = entry.routeDecision
515
537
  ? normalizeRouteDecisionTrace(entry.routeDecision)
516
538
  : undefined;
@@ -579,6 +601,8 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
579
601
  ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
580
602
  ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
581
603
  ...(Array.isArray(entry.attempts) ? { attempts } : {}),
604
+ ...(transportPhase ? { transportPhase } : {}),
605
+ ...(terminalSource ? { terminalSource } : {}),
582
606
  ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
583
607
  ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
584
608
  ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
@@ -200,6 +200,7 @@ export async function describeImageAnthropic(
200
200
  const res = await fetchWithResetRetry(
201
201
  recovery => fetch(`${base}/v1/messages`, applyUpstreamRecoveryInit({
202
202
  method: "POST",
203
+ redirect: "manual",
203
204
  headers,
204
205
  body: JSON.stringify(body),
205
206
  signal: linkedSignal.signal,
@@ -210,6 +210,7 @@ export async function runAnthropicWebSearch(
210
210
  // ignored a bare `Connection: close` (oven-sh/bun#20492).
211
211
  recovery => fetch(url, applyUpstreamRecoveryInit({
212
212
  method: "POST",
213
+ redirect: "manual",
213
214
  headers,
214
215
  body: JSON.stringify(body),
215
216
  signal: linkedSignal.signal,
@@ -478,6 +478,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
478
478
  // replay on this leg eligible for the same dead socket the reset came from.
479
479
  return requestFetch(request.url, applyUpstreamRecoveryInit({
480
480
  method: request.method,
481
+ redirect: "manual",
481
482
  headers: h,
482
483
  body: request.body,
483
484
  signal: headerDeadline.signal,
@@ -303,6 +303,10 @@ export async function* parseStreamWithProgress(
303
303
  }
304
304
  if (event.type === "done" || event.type === "incomplete") {
305
305
  heldTerminal = event;
306
+ // Response-byte inactivity ends once the adapter has produced a terminal event.
307
+ // From here the separate post-terminal drain guard owns the bounded wait for
308
+ // iterator cleanup, so leaving the inactivity timer armed creates a false timeout.
309
+ clearInactivity();
306
310
  continue;
307
311
  }
308
312
  await handoff.deliver(event);