@bitkyc08/opencodex 2.7.41 → 2.7.42

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 (80) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
  3. package/gui/dist/assets/index-DfVGuN88.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/base.ts +6 -0
  7. package/src/adapters/kiro-constants.ts +6 -2
  8. package/src/adapters/kiro-retry.ts +175 -10
  9. package/src/adapters/kiro.ts +172 -85
  10. package/src/adapters/mimo-free.ts +1 -0
  11. package/src/adapters/openai-chat.ts +30 -4
  12. package/src/adapters/openai-responses.ts +90 -12
  13. package/src/bridge.ts +91 -43
  14. package/src/claude/desktop-3p-paths.ts +84 -0
  15. package/src/claude/desktop-3p.ts +29 -2
  16. package/src/cli/access.ts +108 -0
  17. package/src/cli/account-auth.ts +223 -0
  18. package/src/cli/account.ts +9 -1
  19. package/src/cli/agent.ts +184 -0
  20. package/src/cli/combo.ts +119 -0
  21. package/src/cli/config-command.ts +145 -0
  22. package/src/cli/debug.ts +20 -8
  23. package/src/cli/doctor.ts +45 -8
  24. package/src/cli/help.ts +65 -13
  25. package/src/cli/index.ts +108 -7
  26. package/src/cli/integrations.ts +142 -0
  27. package/src/cli/models-runtime.ts +212 -0
  28. package/src/cli/models.ts +9 -10
  29. package/src/cli/observe.ts +117 -0
  30. package/src/cli/provider-runtime.ts +152 -0
  31. package/src/cli/provider.ts +23 -1
  32. package/src/cli/runtime-api.ts +325 -0
  33. package/src/cli/star-prompt.ts +3 -3
  34. package/src/cli/status.ts +17 -0
  35. package/src/cli/system-command.ts +112 -0
  36. package/src/codex/auth-api.ts +3 -2
  37. package/src/codex/catalog/aggregation.ts +113 -18
  38. package/src/codex/catalog/provider-fetch.ts +24 -13
  39. package/src/codex/catalog/sync.ts +20 -8
  40. package/src/codex/catalog.ts +2 -1
  41. package/src/codex/refresh.ts +10 -3
  42. package/src/codex/routing.ts +21 -32
  43. package/src/codex/sync.ts +17 -0
  44. package/src/config.ts +48 -0
  45. package/src/generated/jawcode-model-metadata.ts +2 -1
  46. package/src/grok/inject.ts +184 -4
  47. package/src/grok/status.ts +33 -0
  48. package/src/lib/retry-after.ts +55 -0
  49. package/src/lib/windows-elevation.ts +627 -0
  50. package/src/providers/openai-sidecar.ts +46 -2
  51. package/src/providers/registry.ts +52 -0
  52. package/src/server/auth-cors.ts +6 -0
  53. package/src/server/chat-completions.ts +6 -1
  54. package/src/server/claude-messages.ts +20 -1
  55. package/src/server/images.ts +14 -7
  56. package/src/server/management/agent-settings-routes.ts +10 -4
  57. package/src/server/management/combo-routes.ts +0 -1
  58. package/src/server/management/config-routes.ts +0 -1
  59. package/src/server/management/logs-usage-routes.ts +94 -0
  60. package/src/server/management/model-routes.ts +0 -1
  61. package/src/server/management/oauth-account-routes.ts +0 -1
  62. package/src/server/management/provider-routes.ts +0 -1
  63. package/src/server/management/shared.ts +0 -1
  64. package/src/server/management/system-routes.ts +27 -15
  65. package/src/server/management-api.ts +0 -1
  66. package/src/server/memory-watchdog.ts +54 -10
  67. package/src/server/request-log-conversation.ts +168 -0
  68. package/src/server/request-log.ts +122 -2
  69. package/src/server/responses/core.ts +76 -13
  70. package/src/server/responses/passthrough-error.ts +38 -13
  71. package/src/server/startup-action-control.ts +266 -15
  72. package/src/service.ts +512 -3
  73. package/src/storage/cleanup.ts +1538 -0
  74. package/src/storage/scanner.ts +4 -1
  75. package/src/types.ts +16 -0
  76. package/src/update/job.ts +229 -25
  77. package/src/usage/log.ts +39 -0
  78. package/src/web-search/loop.ts +8 -1
  79. package/gui/dist/assets/index-B2J4t3te.css +0 -1
  80. package/gui/dist/assets/index-BmvM6wRb.js +0 -65
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Best-effort chat/session correlation for Logs / usage.jsonl (#330).
3
+ * Opaque ids only — never persist raw emails or Claude Desktop system-hash fallbacks.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+
7
+ /** Reject absurdly long client strings before hashing (DoS / JSONL bloat). */
8
+ export const LOG_CONVERSATION_ID_INPUT_MAX = 4096;
9
+ /** Persisted / filterable form is always a 32-char hex digest. */
10
+ export const LOG_CONVERSATION_ID_LEN = 32;
11
+
12
+ function hasControlChars(value: string): boolean {
13
+ for (let i = 0; i < value.length; i++) {
14
+ const code = value.charCodeAt(i);
15
+ if (code <= 0x1f || code === 0x7f) return true;
16
+ }
17
+ return false;
18
+ }
19
+
20
+ function sanitizeConversationIdInput(raw: string | undefined | null): string | undefined {
21
+ if (typeof raw !== "string") return undefined;
22
+ const trimmed = raw.trim();
23
+ if (!trimmed) return undefined;
24
+ // Reject control characters that would break JSONL / UI paste.
25
+ if (hasControlChars(trimmed)) return undefined;
26
+ if (trimmed.length > LOG_CONVERSATION_ID_INPUT_MAX) return undefined;
27
+ return trimmed;
28
+ }
29
+
30
+ /**
31
+ * Cap / sanitize a correlation id for persistence.
32
+ * Always hashes so client-controlled headers cannot land emails or short secrets in usage.jsonl.
33
+ */
34
+ export function normalizeLogConversationId(raw: string | undefined | null): string | undefined {
35
+ const trimmed = sanitizeConversationIdInput(raw);
36
+ if (!trimmed) return undefined;
37
+ return createHash("sha256").update(trimmed).digest("hex").slice(0, LOG_CONVERSATION_ID_LEN);
38
+ }
39
+
40
+ /**
41
+ * Filter match: accept either the persisted digest or the original preimage
42
+ * (so pasting from Logs detail or the client-facing session id both work).
43
+ */
44
+ export function matchesLogConversationId(
45
+ stored: string | undefined,
46
+ query: string | undefined | null,
47
+ ): boolean {
48
+ if (!stored) return false;
49
+ const trimmed = typeof query === "string" ? query.trim() : "";
50
+ if (!trimmed) return false;
51
+ if (stored === trimmed) return true;
52
+ const hashed = normalizeLogConversationId(trimmed);
53
+ return hashed !== undefined && stored === hashed;
54
+ }
55
+
56
+ /**
57
+ * Codex/Claude/Cursor priority for Responses-shaped requests:
58
+ * parent thread header > session_id / session-id > thread-id > cursor conversation id.
59
+ */
60
+ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
61
+ return headers.get("session_id") ?? headers.get("session-id");
62
+ }
63
+
64
+ export function conversationIdFromResponsesRequest(input: {
65
+ clientThreadId?: string;
66
+ sessionIdHeader?: string | null;
67
+ threadIdHeader?: string | null;
68
+ cursorConversationId?: string;
69
+ }): string | undefined {
70
+ return normalizeLogConversationId(
71
+ input.clientThreadId
72
+ ?? input.sessionIdHeader
73
+ ?? input.threadIdHeader
74
+ ?? input.cursorConversationId,
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Claude Code metadata.user_id only — never the system-hash Desktop fallback.
80
+ * Hashes the raw user_id once (same opacity goal as inbound prompt_cache_key).
81
+ */
82
+ export function conversationIdFromClaudeMetadata(
83
+ metadata: { user_id?: unknown } | null | undefined,
84
+ ): string | undefined {
85
+ if (!metadata || typeof metadata.user_id !== "string") return undefined;
86
+ return normalizeLogConversationId(metadata.user_id);
87
+ }
88
+
89
+ /** @deprecated Prefer conversationIdFromClaudeMetadata; kept for call-site clarity. */
90
+ export function conversationIdFromClaudeCacheKey(
91
+ cacheKeySource: "metadata" | "system" | null | undefined,
92
+ promptCacheKey: string | undefined,
93
+ ): string | undefined {
94
+ if (cacheKeySource !== "metadata") return undefined;
95
+ // prompt_cache_key is already sha256(user_id)[:32] from inbound — persist as-is
96
+ // (do not re-hash) so native and translated paths stay aligned when callers pass
97
+ // the preimage via conversationIdFromClaudeMetadata instead.
98
+ const trimmed = sanitizeConversationIdInput(promptCacheKey);
99
+ if (!trimmed) return undefined;
100
+ if (trimmed.length === LOG_CONVERSATION_ID_LEN && /^[0-9a-f]+$/i.test(trimmed)) return trimmed.toLowerCase();
101
+ return normalizeLogConversationId(trimmed);
102
+ }
103
+
104
+ export interface ConversationLogTotals {
105
+ requests: number;
106
+ totalTokens: number;
107
+ estimatedCostUsd: number;
108
+ pricedRequests: number;
109
+ unpricedRequests: number;
110
+ unmeteredRequests: number;
111
+ }
112
+
113
+ type TotalsSource = {
114
+ totalTokens?: number;
115
+ usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
116
+ usageStatus?: string;
117
+ displayMetrics?: {
118
+ cost?:
119
+ | { kind: "value"; estimate: { cost: { total: number } } }
120
+ | { kind: "unavailable"; reason: string };
121
+ };
122
+ };
123
+
124
+ function rowTokenTotal(entry: TotalsSource): number | undefined {
125
+ if (typeof entry.totalTokens === "number" && Number.isFinite(entry.totalTokens) && entry.totalTokens >= 0) {
126
+ return entry.totalTokens;
127
+ }
128
+ const usageTotal = entry.usage?.totalTokens;
129
+ if (typeof usageTotal === "number" && Number.isFinite(usageTotal) && usageTotal >= 0) return usageTotal;
130
+ const input = entry.usage?.inputTokens;
131
+ const output = entry.usage?.outputTokens;
132
+ if (typeof input === "number" && typeof output === "number" && Number.isFinite(input) && Number.isFinite(output)) {
133
+ return Math.max(0, input) + Math.max(0, output);
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ /** Sum tokens/cost for the currently loaded log slice matching a conversation filter. */
139
+ export function summarizeConversationLogs(entries: readonly TotalsSource[]): ConversationLogTotals {
140
+ let totalTokens = 0;
141
+ let estimatedCostUsd = 0;
142
+ let pricedRequests = 0;
143
+ let unpricedRequests = 0;
144
+ let unmeteredRequests = 0;
145
+ for (const entry of entries) {
146
+ const tokens = rowTokenTotal(entry);
147
+ if (tokens !== undefined) totalTokens += tokens;
148
+ if (entry.usageStatus === "unsupported") {
149
+ unmeteredRequests += 1;
150
+ continue;
151
+ }
152
+ const cost = entry.displayMetrics?.cost;
153
+ if (cost?.kind === "value" && Number.isFinite(cost.estimate.cost.total) && cost.estimate.cost.total >= 0) {
154
+ estimatedCostUsd += cost.estimate.cost.total;
155
+ pricedRequests += 1;
156
+ continue;
157
+ }
158
+ unpricedRequests += 1;
159
+ }
160
+ return {
161
+ requests: entries.length,
162
+ totalTokens,
163
+ estimatedCostUsd,
164
+ pricedRequests,
165
+ unpricedRequests,
166
+ unmeteredRequests,
167
+ };
168
+ }
@@ -8,6 +8,7 @@ import {
8
8
  import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
9
9
  import { readCodexCatalogPath } from "../codex/catalog";
10
10
  import type { OcxUsage } from "../types";
11
+ import type { AdapterRequest } from "../adapters/base";
11
12
  import { redactSecretString } from "../lib/redact";
12
13
  import {
13
14
  appendUsageEntry,
@@ -28,17 +29,23 @@ import {
28
29
  USAGE_DEBUG_BODY_SAMPLE_BYTES,
29
30
  type UsageDebugBodyKind,
30
31
  } from "../usage/debug";
32
+ import { matchesLogConversationId } from "./request-log-conversation";
31
33
 
32
34
  export interface RequestLogContext {
33
35
  model: string;
34
36
  provider: string;
35
37
  /** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
36
38
  firstOutputMs?: number;
39
+ /** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
40
+ conversationId?: string;
37
41
  surface?: "claude" | "claude-desktop" | "grok";
38
42
  requestedModel?: string;
39
43
  /** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
40
44
  comboId?: string;
41
45
  requestedEffort?: string;
46
+ effectiveEffort?: string;
47
+ reasoningWireField?: string;
48
+ reasoningWireValue?: string | number;
42
49
  requestedServiceTier?: string;
43
50
  requestedSpeedLabel?: string;
44
51
  configuredServiceTier?: string;
@@ -70,6 +77,8 @@ export interface RequestLogContext {
70
77
  upstreamError?: string;
71
78
  /** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
72
79
  terminalHttpStatus?: number;
80
+ /** Structured reason from `response.incomplete`; internal-only input to log classification. */
81
+ terminalIncompleteReason?: string;
73
82
  affinity?: "reused" | "new_bind" | "rebound" | "cleared";
74
83
  transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
75
84
  terminalSource?: "upstream" | "synthetic";
@@ -83,8 +92,13 @@ export interface RequestLogEntry {
83
92
  /** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
84
93
  firstOutputMs?: number;
85
94
  surface?: "claude" | "claude-desktop" | "grok";
95
+ /** Best-effort chat/session correlation for Logs grouping (#330). */
96
+ conversationId?: string;
86
97
  requestedModel?: string;
87
98
  requestedEffort?: string;
99
+ effectiveEffort?: string;
100
+ reasoningWireField?: string;
101
+ reasoningWireValue?: string | number;
88
102
  requestedServiceTier?: string;
89
103
  requestedSpeedLabel?: string;
90
104
  configuredServiceTier?: string;
@@ -146,8 +160,12 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
146
160
  provider: entry.provider,
147
161
  ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
148
162
  ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
163
+ ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
149
164
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
150
165
  ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
166
+ ...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
167
+ ...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
168
+ ...(entry.reasoningWireValue !== undefined ? { reasoningWireValue: entry.reasoningWireValue } : {}),
151
169
  ...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
152
170
  ...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
153
171
  ...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
@@ -222,9 +240,13 @@ export function addRequestLog(entry: RequestLogEntry) {
222
240
  provider: entry.provider,
223
241
  model: entry.model,
224
242
  ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
243
+ ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
225
244
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
226
245
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
227
246
  ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
247
+ ...(entry.effectiveEffort ? { effectiveEffort: entry.effectiveEffort } : {}),
248
+ ...(entry.reasoningWireField ? { reasoningWireField: entry.reasoningWireField } : {}),
249
+ ...(entry.reasoningWireValue !== undefined ? { reasoningWireValue: entry.reasoningWireValue } : {}),
228
250
  ...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
229
251
  ...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
230
252
  ...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
@@ -272,6 +294,71 @@ export function recordFirstOutput(
272
294
  }
273
295
  }
274
296
 
297
+ /** Snapshot target-specific requested effort even for runTurn adapters with no AdapterRequest. */
298
+ export function recordAttemptRequestedEffort(logCtx: RequestLogContext): void {
299
+ const attempt = logCtx.activeAttempt;
300
+ if (!attempt) return;
301
+ delete attempt.requestedEffort;
302
+ try {
303
+ if (typeof logCtx.requestedEffort === "string" && logCtx.requestedEffort) {
304
+ attempt.requestedEffort = redactSecretString(logCtx.requestedEffort).slice(0, 64);
305
+ }
306
+ } catch {
307
+ // Request logging is best-effort and must not affect request delivery.
308
+ }
309
+ }
310
+
311
+ /** Copy the adapter's exact outbound reasoning parameter into the durable request log. */
312
+ export function recordAdapterReasoning(
313
+ logCtx: RequestLogContext,
314
+ request: AdapterRequest,
315
+ ): void {
316
+ delete logCtx.effectiveEffort;
317
+ delete logCtx.reasoningWireField;
318
+ delete logCtx.reasoningWireValue;
319
+ const attempt = logCtx.activeAttempt;
320
+ if (attempt) {
321
+ delete attempt.effectiveEffort;
322
+ delete attempt.reasoningWireField;
323
+ delete attempt.reasoningWireValue;
324
+ }
325
+ recordAttemptRequestedEffort(logCtx);
326
+
327
+ // Diagnostics must never make an otherwise valid upstream request fail. Config files
328
+ // written by older versions (or edited by hand) can contain values that violate the
329
+ // current TypeScript shape, so validate the runtime object before redacting strings.
330
+ try {
331
+ const raw: unknown = request.reasoningLog;
332
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
333
+ const reasoning = raw as Record<string, unknown>;
334
+ if (typeof reasoning.effectiveEffort !== "string" || !reasoning.effectiveEffort
335
+ || (reasoning.wireField !== "reasoning_effort"
336
+ && reasoning.wireField !== "thinking_budget"
337
+ && reasoning.wireField !== "thinking.type")
338
+ || (!(typeof reasoning.wireValue === "string" && reasoning.wireValue)
339
+ && !(typeof reasoning.wireValue === "number"
340
+ && Number.isFinite(reasoning.wireValue)
341
+ && reasoning.wireValue >= 0))) {
342
+ return;
343
+ }
344
+
345
+ const effectiveEffort = redactSecretString(reasoning.effectiveEffort).slice(0, 64);
346
+ const wireValue = typeof reasoning.wireValue === "string"
347
+ ? redactSecretString(reasoning.wireValue).slice(0, 64)
348
+ : reasoning.wireValue;
349
+ logCtx.effectiveEffort = effectiveEffort;
350
+ logCtx.reasoningWireField = reasoning.wireField;
351
+ logCtx.reasoningWireValue = wireValue;
352
+ if (attempt) {
353
+ attempt.effectiveEffort = effectiveEffort;
354
+ attempt.reasoningWireField = reasoning.wireField;
355
+ attempt.reasoningWireValue = wireValue;
356
+ }
357
+ } catch {
358
+ // Request logging is best-effort and must not affect request delivery.
359
+ }
360
+ }
361
+
275
362
  export function requestLogErrorCode(status: number, upstreamError?: string): string | undefined {
276
363
  if (status >= 200 && status < 400) return undefined;
277
364
  // Defense in depth: mid-stream web-search aborts used to land as 502 with this message.
@@ -447,7 +534,7 @@ export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload:
447
534
  * run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
448
535
  */
449
536
  function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
450
- if (!text || logCtx.upstreamError) return;
537
+ if (!text) return;
451
538
  try {
452
539
  const json = JSON.parse(text) as {
453
540
  type?: unknown;
@@ -459,6 +546,14 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
459
546
  };
460
547
  };
461
548
  captureTerminalHttpStatus(logCtx, json);
549
+ const reason = json?.response?.incomplete_details?.reason;
550
+ if (json.type === "response.incomplete"
551
+ && logCtx.terminalIncompleteReason === undefined
552
+ && typeof reason === "string"
553
+ && reason.trim()) {
554
+ logCtx.terminalIncompleteReason = reason.trim();
555
+ }
556
+ if (logCtx.upstreamError) return;
462
557
  const message = json?.error?.message
463
558
  ?? json?.last_error?.message
464
559
  ?? json?.response?.error?.message;
@@ -470,11 +565,11 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
470
565
  // the bridge on a stall-timeout or adapter EOF (response.incomplete). Maps the raw reason to a
471
566
  // reader-facing label so a generic 502 in /api/logs explains WHY the turn ended, not just the
472
567
  // mapped HTTP code.
473
- const reason = json?.response?.incomplete_details?.reason;
474
568
  if (typeof reason === "string" && reason.trim()) {
475
569
  logCtx.upstreamError = redactSecretString(incompleteReasonLabel(reason.trim())).slice(0, 500);
476
570
  }
477
571
  } catch {
572
+ if (logCtx.upstreamError) return;
478
573
  const trimmed = text.trim();
479
574
  if (trimmed) {
480
575
  logCtx.upstreamError = redactSecretString(trimmed).slice(0, 500);
@@ -485,6 +580,8 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
485
580
  /** Map a raw `incomplete_details.reason` (emitted by the bridge) to a reader-facing label. */
486
581
  function incompleteReasonLabel(reason: string): string {
487
582
  switch (reason) {
583
+ case "max_output_tokens":
584
+ return `Output reached the requested token limit (${reason})`;
488
585
  case "upstream_stall_timeout":
489
586
  return `Upstream stalled: no data for the stall-timeout window (${reason})`;
490
587
  case "adapter_eof":
@@ -529,6 +626,21 @@ export function httpStatusForRequestLogTerminal(
529
626
  status: ResponsesTerminalStatus,
530
627
  logCtx?: RequestLogContext,
531
628
  ): number {
629
+ /**
630
+ * [Decision Log]
631
+ * - 목적과 의도: Keep request logs aligned with the successful HTTP/SSE contract.
632
+ * - 기존 구현 및 제약 조건: All incomplete terminals were recorded as 502 even when the
633
+ * client-requested output limit was reached normally.
634
+ * - 검토한 주요 대안: Treat every incomplete as success, or infer the reason from display text.
635
+ * - 선택한 방식: Only structured max_output_tokens incompletes map to 200.
636
+ * - 다른 대안 대신 이 방식을 선택한 이유: Stall, EOF, and unknown incompletes must remain
637
+ * visible failures, and display text is not a stable classification contract.
638
+ * - 장점, 단점 및 영향: Logs stop reporting false upstream errors while retaining the
639
+ * incomplete terminal detail; native callers without a structured reason keep old behavior.
640
+ */
641
+ if (status === "incomplete" && logCtx?.terminalIncompleteReason === "max_output_tokens") {
642
+ return 200;
643
+ }
532
644
  if (status === "failed" && logCtx?.terminalHttpStatus !== undefined) {
533
645
  return logCtx.terminalHttpStatus;
534
646
  }
@@ -583,8 +695,12 @@ export function addFinalRequestLog(
583
695
  model: isCombo ? logCtx.requestedModel! : logCtx.model,
584
696
  provider: isCombo ? "combo" : logCtx.provider,
585
697
  ...(logCtx.surface ? { surface: logCtx.surface } : {}),
698
+ ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
586
699
  ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
587
700
  ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
701
+ ...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
702
+ ...(logCtx.reasoningWireField ? { reasoningWireField: logCtx.reasoningWireField } : {}),
703
+ ...(logCtx.reasoningWireValue !== undefined ? { reasoningWireValue: logCtx.reasoningWireValue } : {}),
588
704
  ...(logCtx.requestedServiceTier ? { requestedServiceTier: logCtx.requestedServiceTier } : {}),
589
705
  ...(logCtx.requestedSpeedLabel ? { requestedSpeedLabel: logCtx.requestedSpeedLabel } : {}),
590
706
  ...(logCtx.configuredServiceTier ? { configuredServiceTier: logCtx.configuredServiceTier } : {}),
@@ -629,6 +745,10 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
629
745
  filtered = filtered.filter(entry => entry.provider === provider
630
746
  || entry.attempts?.some(attempt => attempt.provider === provider));
631
747
  }
748
+ const conversationId = params.get("conversationId")?.trim() || params.get("conversation")?.trim();
749
+ if (conversationId) {
750
+ filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
751
+ }
632
752
  const status = params.get("status")?.trim().toLowerCase();
633
753
  if (status) {
634
754
  filtered = /^[1-5]xx$/.test(status)
@@ -27,6 +27,7 @@ import {
27
27
  } from "../../combos";
28
28
  import { isInjectionDebugEnabled } from "../../lib/debug-settings";
29
29
  import { injectionDebugLog } from "../../lib/injection-debug-log";
30
+ import { resolveClientRetryAfter } from "../../lib/retry-after";
30
31
  import { modelInList, namespacedToolName } from "../../types";
31
32
  import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
32
33
  import {
@@ -92,11 +93,18 @@ import {
92
93
  inspectResponseLogJson,
93
94
  noteAttemptSend,
94
95
  readConfiguredCodexServiceTier,
96
+ recordAdapterReasoning,
97
+ recordAttemptRequestedEffort,
95
98
  requestLogSpeedLabel,
96
99
  sealRequestAttemptIdentity,
97
100
  usageFromResponsesPayload,
98
101
  type RequestLogContext,
99
102
  } from "../request-log";
103
+ import {
104
+ conversationIdFromResponsesRequest,
105
+ normalizeLogConversationId,
106
+ sessionIdHeaderFromRequest,
107
+ } from "../request-log-conversation";
100
108
  import type { AttemptRecoveryKind } from "../../usage/log";
101
109
  import {
102
110
  consumeForInspection,
@@ -347,14 +355,30 @@ export async function consumeComboFailure(
347
355
  const message = classificationText === fallback
348
356
  ? fallback
349
357
  : `${fallback}: ${classificationText}`;
350
- const retryAfter = sanitizedRetryAfter(response.headers.get("retry-after"), now);
358
+ const upstreamRetryAfter = response.headers.get("retry-after");
359
+ // Client response may get the synthetic "2" fallback; cooldown metadata must not —
360
+ // otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
361
+ const clientRetryAfter = resolveClientRetryAfter({
362
+ status: response.status,
363
+ message,
364
+ upstreamRetryAfter,
365
+ now,
366
+ });
367
+ const cooldownRetryAfter = resolveClientRetryAfter({
368
+ status: response.status,
369
+ message,
370
+ upstreamRetryAfter,
371
+ now,
372
+ includeDefault: false,
373
+ });
351
374
  return {
352
375
  response: formatErrorResponse(response.status, "upstream_error", message, {
353
376
  ...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
377
+ ...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
354
378
  }),
355
379
  classificationText,
356
380
  ...(upstreamCode !== undefined ? { upstreamCode } : {}),
357
- ...(retryAfter !== undefined ? { retryAfter } : {}),
381
+ ...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
358
382
  ...(usage ? { usage } : {}),
359
383
  };
360
384
  }
@@ -593,6 +617,7 @@ async function applyFinalRouteRequestNormalization(args: {
593
617
  logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
594
618
  }
595
619
  }
620
+ recordAttemptRequestedEffort(logCtx);
596
621
  logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
597
622
  route.modelId,
598
623
  logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
@@ -622,6 +647,19 @@ export async function handleComboResponses(
622
647
  if (!combo) {
623
648
  return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
624
649
  }
650
+ const adoptFailedChildLog = (childLog: RequestLogContext): void => {
651
+ // Attempts remain the complete physical history; the logical row mirrors the most recent
652
+ // failed target so an exhausted combo still has useful top-level reasoning diagnostics.
653
+ Object.assign(logCtx, childLog, {
654
+ requestedModel,
655
+ model: requestedModel,
656
+ provider: "combo",
657
+ comboId,
658
+ attempts: logCtx.attempts,
659
+ activeAttempt: undefined,
660
+ activeAttemptStartedAt: undefined,
661
+ });
662
+ };
625
663
 
626
664
  const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
627
665
  (rawBody as { input?: unknown } | undefined)?.input,
@@ -658,6 +696,8 @@ export async function handleComboResponses(
658
696
  const childLog: RequestLogContext = {
659
697
  model: pick.target.model,
660
698
  provider: pick.target.provider,
699
+ ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
700
+ ...(logCtx.surface ? { surface: logCtx.surface } : {}),
661
701
  };
662
702
  const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`);
663
703
  const childBody = concreteComboRequestBody(
@@ -792,25 +832,19 @@ export async function handleComboResponses(
792
832
  if (comboFailureDecision(failure.response.status, failure.classificationText, {
793
833
  code: failure.upstreamCode,
794
834
  }) === "stop") {
795
- Object.assign(logCtx, childLog, {
796
- requestedModel,
797
- model: requestedModel,
798
- provider: "combo",
799
- comboId,
800
- attempts: logCtx.attempts,
801
- activeAttempt: undefined,
802
- activeAttemptStartedAt: undefined,
803
- });
835
+ adoptFailedChildLog(childLog);
804
836
  return lastFailure;
805
837
  }
806
838
  console.warn(
807
839
  `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
808
840
  );
809
- pick = advanceComboAfterFailure(config, pick, {
841
+ const nextPick = advanceComboAfterFailure(config, pick, {
810
842
  retryAfter: failure.retryAfter,
811
843
  now: Date.now(),
812
844
  eligible: payloadEligible,
813
845
  });
846
+ if (!nextPick) adoptFailedChildLog(childLog);
847
+ pick = nextPick;
814
848
  }
815
849
  return lastFailure!;
816
850
  }
@@ -866,6 +900,16 @@ export async function handleResponses(
866
900
  } catch (err) {
867
901
  return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
868
902
  }
903
+ // Prefer a pre-populated id (routed Claude) over Responses headers that may be
904
+ // absent or synthetically injected (session_id from prompt_cache_key).
905
+ if (!logCtx.conversationId) {
906
+ logCtx.conversationId = conversationIdFromResponsesRequest({
907
+ clientThreadId: parsed._clientThreadId,
908
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
909
+ threadIdHeader: req.headers.get("thread-id"),
910
+ cursorConversationId: parsed._cursorConversationId,
911
+ });
912
+ }
869
913
  logCtx.requestedModel = parsed.modelId;
870
914
  logCtx.requestedEffort = parsed.options.reasoning;
871
915
  logCtx.requestedServiceTier = parsed.options.serviceTier;
@@ -1124,6 +1168,7 @@ export async function handleResponses(
1124
1168
  );
1125
1169
  }
1126
1170
  let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
1171
+ recordAdapterReasoning(logCtx, request);
1127
1172
  const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1128
1173
  ? request.usageLog.inputTokens
1129
1174
  : undefined;
@@ -1213,6 +1258,7 @@ export async function handleResponses(
1213
1258
  config.cacheRetention,
1214
1259
  );
1215
1260
  request = await retryAdapter.buildRequest(parsed, { headers: retryHeaders });
1261
+ recordAdapterReasoning(logCtx, request);
1216
1262
 
1217
1263
  await upstreamResponse.body?.cancel().catch(() => undefined);
1218
1264
  authCtx = retryAuthCtx;
@@ -1500,6 +1546,11 @@ export async function handleResponses(
1500
1546
  message: err instanceof Error ? err.message : String(err),
1501
1547
  });
1502
1548
  } finally {
1549
+ // Cursor assigns a stable conversation id inside runTurn on the first headerless
1550
+ // turn; backfill so Logs can filter/total that opening request (#330 / #522).
1551
+ if (!logCtx.conversationId && parsed._cursorConversationId) {
1552
+ logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
1553
+ }
1503
1554
  queue.close();
1504
1555
  }
1505
1556
  };
@@ -1614,6 +1665,7 @@ export async function handleResponses(
1614
1665
  forceEmptyResponseId: true,
1615
1666
  abortSignal: options.abortSignal,
1616
1667
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
1668
+ onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
1617
1669
  onUsage: usage => {
1618
1670
  logCtx.usageFromBridge = true;
1619
1671
  if (usage) {
@@ -1658,6 +1710,7 @@ export async function handleResponses(
1658
1710
  let activeAdapter = adapter;
1659
1711
 
1660
1712
  const request = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders });
1713
+ recordAdapterReasoning(logCtx, request);
1661
1714
  const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
1662
1715
  ? request.usageLog.inputTokens
1663
1716
  : undefined;
@@ -1710,6 +1763,7 @@ export async function handleResponses(
1710
1763
  headers: selectedForwardHeaders,
1711
1764
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
1712
1765
  });
1766
+ recordAdapterReasoning(logCtx, retryRequest);
1713
1767
  const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
1714
1768
  ? retryRequest.usageLog.inputTokens
1715
1769
  : undefined;
@@ -1830,7 +1884,15 @@ export async function handleResponses(
1830
1884
  );
1831
1885
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
1832
1886
  // material before it reaches the client-facing error surface.
1833
- return formatErrorResponse(upstreamResponse.status, "upstream_error", `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`);
1887
+ const message = `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`;
1888
+ const retryAfter = resolveClientRetryAfter({
1889
+ status: upstreamResponse.status,
1890
+ message,
1891
+ upstreamRetryAfter: upstreamResponse.headers.get("retry-after"),
1892
+ });
1893
+ return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
1894
+ ...(retryAfter !== undefined ? { retryAfter } : {}),
1895
+ });
1834
1896
  }
1835
1897
  }
1836
1898
 
@@ -1850,6 +1912,7 @@ export async function handleResponses(
1850
1912
  headers: selectedForwardHeaders,
1851
1913
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
1852
1914
  });
1915
+ recordAdapterReasoning(logCtx, continuationRequest);
1853
1916
  const continuationEstimate = typeof continuationRequest.usageLog?.inputTokens === "number"
1854
1917
  ? continuationRequest.usageLog.inputTokens
1855
1918
  : undefined;