sidekick-shared 0.28.2 → 0.29.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 (142) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +176 -30
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +14 -6
  6. package/dist/browser.js +26 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/codexQuota.js +17 -4
  12. package/dist/esm/authStatus.js +87 -0
  13. package/dist/esm/browser.js +6 -2
  14. package/dist/esm/codex.js +18 -0
  15. package/dist/esm/codexProfiles.js +8 -11
  16. package/dist/esm/codexQuota.js +18 -5
  17. package/dist/esm/formatting.js +10 -1
  18. package/dist/esm/index.js +12 -9
  19. package/dist/esm/modelCatalog.js +2 -1
  20. package/dist/esm/modelInfo.js +422 -45
  21. package/dist/esm/multiProviderQuotaService.js +1 -1
  22. package/dist/esm/node.js +9 -3
  23. package/dist/esm/parsers/codexParser.js +175 -54
  24. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  25. package/dist/esm/parsers/openCodeParser.js +83 -21
  26. package/dist/esm/pricing.js +18 -0
  27. package/dist/esm/pricingCatalog.js +14 -7
  28. package/dist/esm/promptHistory.js +66 -4
  29. package/dist/esm/providerFailure.js +124 -3
  30. package/dist/esm/providers/codex.js +2 -24
  31. package/dist/esm/providers/codexDatabase.js +3 -19
  32. package/dist/esm/providers/codexRollouts.js +24 -1
  33. package/dist/esm/providers/openCode.js +9 -17
  34. package/dist/esm/quota.js +356 -24
  35. package/dist/esm/quotaHistory.js +33 -8
  36. package/dist/esm/quotaPoller.js +5 -2
  37. package/dist/esm/quotaResolver.js +31 -6
  38. package/dist/esm/quotaSnapshots.js +65 -10
  39. package/dist/esm/quotaThresholds.js +7 -2
  40. package/dist/esm/report/htmlReportGenerator.js +42 -5
  41. package/dist/esm/report/transcriptParser.js +13 -11
  42. package/dist/esm/schemas/index.js +2 -2
  43. package/dist/esm/schemas/providerFailure.js +3 -1
  44. package/dist/esm/schemas/quota.js +30 -0
  45. package/dist/esm/schemas/quotaHistory.js +10 -2
  46. package/dist/esm/schemas/stateFile.js +9 -0
  47. package/dist/esm/schemas/transcript.js +4 -0
  48. package/dist/esm/sessionPromptHistory.js +2 -2
  49. package/dist/esm/stateFile.js +18 -3
  50. package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
  51. package/dist/esm/statusline/formatter.js +57 -18
  52. package/dist/esm/statusline/index.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/testing/credentialGuard.js +87 -5
  55. package/dist/esm/toolCallOrigins.js +310 -0
  56. package/dist/esm/transcript.js +42 -3
  57. package/dist/esm/usage/usageEvents.js +146 -17
  58. package/dist/esm/usage/usageReports.js +39 -6
  59. package/dist/esm/watchers/jsonlTail.js +378 -59
  60. package/dist/esm/zaiQuotaApi.js +4 -2
  61. package/dist/formatting.d.ts +26 -1
  62. package/dist/formatting.js +11 -1
  63. package/dist/index.d.ts +22 -16
  64. package/dist/index.js +48 -12
  65. package/dist/modelCatalog.js +2 -1
  66. package/dist/modelInfo.d.ts +185 -16
  67. package/dist/modelInfo.js +431 -47
  68. package/dist/multiProviderQuotaService.js +1 -1
  69. package/dist/node.d.ts +16 -5
  70. package/dist/node.js +31 -2
  71. package/dist/parsers/codexParser.d.ts +55 -0
  72. package/dist/parsers/codexParser.js +175 -54
  73. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  74. package/dist/parsers/codexRolloutOrigin.js +111 -0
  75. package/dist/parsers/openCodeParser.d.ts +38 -0
  76. package/dist/parsers/openCodeParser.js +86 -21
  77. package/dist/pricing.d.ts +22 -0
  78. package/dist/pricing.js +50 -0
  79. package/dist/pricingCatalog.d.ts +10 -0
  80. package/dist/pricingCatalog.js +13 -6
  81. package/dist/promptHistory.d.ts +65 -4
  82. package/dist/promptHistory.js +68 -5
  83. package/dist/providerFailure.d.ts +44 -0
  84. package/dist/providerFailure.js +126 -4
  85. package/dist/providers/codex.d.ts +3 -22
  86. package/dist/providers/codex.js +3 -26
  87. package/dist/providers/codexDatabase.d.ts +3 -2
  88. package/dist/providers/codexDatabase.js +4 -22
  89. package/dist/providers/codexRollouts.d.ts +22 -0
  90. package/dist/providers/codexRollouts.js +24 -0
  91. package/dist/providers/openCode.js +8 -16
  92. package/dist/quota.d.ts +131 -1
  93. package/dist/quota.js +366 -24
  94. package/dist/quotaHistory.d.ts +10 -0
  95. package/dist/quotaHistory.js +34 -8
  96. package/dist/quotaPoller.d.ts +11 -3
  97. package/dist/quotaPoller.js +5 -2
  98. package/dist/quotaResolver.js +30 -5
  99. package/dist/quotaSnapshots.js +64 -9
  100. package/dist/quotaThresholds.js +7 -2
  101. package/dist/report/htmlReportGenerator.js +42 -5
  102. package/dist/report/transcriptParser.d.ts +10 -1
  103. package/dist/report/transcriptParser.js +13 -11
  104. package/dist/schemas/index.d.ts +3 -3
  105. package/dist/schemas/index.js +5 -2
  106. package/dist/schemas/providerFailure.d.ts +39 -0
  107. package/dist/schemas/providerFailure.js +2 -0
  108. package/dist/schemas/quota.d.ts +258 -0
  109. package/dist/schemas/quota.js +31 -1
  110. package/dist/schemas/quotaHistory.d.ts +2 -0
  111. package/dist/schemas/quotaHistory.js +10 -2
  112. package/dist/schemas/stateFile.d.ts +34 -0
  113. package/dist/schemas/stateFile.js +10 -1
  114. package/dist/schemas/transcript.d.ts +2 -0
  115. package/dist/schemas/transcript.js +4 -0
  116. package/dist/sessionPromptHistory.d.ts +7 -1
  117. package/dist/sessionPromptHistory.js +1 -1
  118. package/dist/stateFile.d.ts +18 -0
  119. package/dist/stateFile.js +17 -2
  120. package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
  121. package/dist/statusline/claudeStatuslinePayload.js +16 -4
  122. package/dist/statusline/formatter.d.ts +5 -0
  123. package/dist/statusline/formatter.js +56 -17
  124. package/dist/statusline/index.d.ts +4 -4
  125. package/dist/statusline/index.js +5 -1
  126. package/dist/statusline/payload.d.ts +10 -0
  127. package/dist/statusline/payload.js +14 -0
  128. package/dist/testing/credentialGuard.d.ts +14 -0
  129. package/dist/testing/credentialGuard.js +87 -5
  130. package/dist/toolCallOrigins.d.ts +70 -0
  131. package/dist/toolCallOrigins.js +346 -0
  132. package/dist/transcript.d.ts +12 -0
  133. package/dist/transcript.js +42 -3
  134. package/dist/types/opencode.d.ts +2 -0
  135. package/dist/usage/usageEvents.d.ts +54 -6
  136. package/dist/usage/usageEvents.js +146 -17
  137. package/dist/usage/usageReports.d.ts +35 -4
  138. package/dist/usage/usageReports.js +39 -6
  139. package/dist/watchers/jsonlTail.d.ts +101 -4
  140. package/dist/watchers/jsonlTail.js +380 -60
  141. package/dist/zaiQuotaApi.js +3 -1
  142. package/package.json +41 -1
@@ -15,6 +15,10 @@
15
15
  * record over `maxRecordBytes` is skipped without being buffered and reported
16
16
  * in `exclusions`, the only way a record is permanently left out.
17
17
  *
18
+ * `includeText: false` returns the same entries, ordinals, timestamps,
19
+ * metadata, exclusions, and cursor without prompt text, for consumers that
20
+ * only count or attribute prompts. Cursors are interchangeable between modes.
21
+ *
18
22
  * A prompt can be written before its answer. The cursor then keeps the byte
19
23
  * offset of that prompt's line and its answer-tracking state (never text), and
20
24
  * the prompt is returned again under the same identity once its first
@@ -54,8 +58,9 @@ var __importStar = (this && this.__importStar) || (function () {
54
58
  };
55
59
  })();
56
60
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.__promptHistoryTesting = exports.DEFAULT_BOUNDS = void 0;
61
+ exports.__promptHistoryTesting = exports.DEFAULT_BOUNDS = exports.PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES = void 0;
58
62
  exports.collectPromptHistory = collectPromptHistory;
63
+ exports.readPromptOrdinalMarks = readPromptOrdinalMarks;
59
64
  exports.emptyStats = emptyStats;
60
65
  exports.isPromptHistorySessionSettled = isPromptHistorySessionSettled;
61
66
  exports.parsePromptHistoryCursor = parsePromptHistoryCursor;
@@ -72,12 +77,19 @@ const codexRollouts_1 = require("./providers/codexRollouts");
72
77
  const directoryListingCache_1 = require("./providers/directoryListingCache");
73
78
  const workspaceRoots_1 = require("./workspaceRoots");
74
79
  const fileHead_1 = require("./fileHead");
80
+ /**
81
+ * The default `maxRecordBytes` (16 MiB). Prompt ordinals are only comparable
82
+ * between readers that skip the same records, so every reader that publishes
83
+ * or joins on ordinals (prompt history, session prompt history, usage events'
84
+ * `promptOrdinal`) uses this bound.
85
+ */
86
+ exports.PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES = 16 * 1024 * 1024;
75
87
  /** @internal */
76
88
  exports.DEFAULT_BOUNDS = {
77
89
  maxSessions: 1000,
78
90
  maxFileBytes: 16 * 1024 * 1024,
79
91
  maxTotalBytes: 256 * 1024 * 1024,
80
- maxRecordBytes: 16 * 1024 * 1024,
92
+ maxRecordBytes: exports.PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
81
93
  maxEntries: 10_000,
82
94
  deadlineMs: 30_000,
83
95
  };
@@ -152,6 +164,7 @@ async function collectPromptHistory(options) {
152
164
  if (exclusions.length < MAX_EXCLUSIONS)
153
165
  exclusions.push(exclusion);
154
166
  },
167
+ includeText: options.includeText !== false,
155
168
  };
156
169
  let totalBytes = 0;
157
170
  let scannedAny = false;
@@ -265,6 +278,46 @@ async function collectPromptHistory(options) {
265
278
  }
266
279
  return finish();
267
280
  }
281
+ /**
282
+ * Every human prompt of one session file with its ordinal and timestamp, read
283
+ * whole in one pass with the default record bound, so the ordinals are the
284
+ * ones `collectPromptHistory()` returns with default bounds. No scope check
285
+ * and no text: prompts are counted wherever their cwd points. Prompts whose
286
+ * line has no usable timestamp still take an ordinal but are left out. A
287
+ * non-interactive Codex session (subagent, fork, unknown source) has none.
288
+ * In file order. Rejects when the file cannot be opened or changes while its
289
+ * head is read.
290
+ * @internal
291
+ */
292
+ async function readPromptOrdinalMarks(provider, sessionId, filePath) {
293
+ const marks = [];
294
+ const stat = await fs.promises.stat(filePath);
295
+ const unbounded = Number.MAX_SAFE_INTEGER;
296
+ const context = {
297
+ roots: [],
298
+ sinceMs: null,
299
+ stats: emptyStats(),
300
+ realpaths: new Map(),
301
+ bounds: {
302
+ maxSessions: unbounded,
303
+ maxFileBytes: unbounded,
304
+ maxTotalBytes: unbounded,
305
+ maxRecordBytes: exports.PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
306
+ maxEntries: unbounded,
307
+ deadlineMs: unbounded,
308
+ },
309
+ shouldStop: () => null,
310
+ entriesSoFar: () => 0,
311
+ exclude: () => undefined,
312
+ includeText: false,
313
+ onPrompt: ({ ordinal, timestamp }) => {
314
+ if (timestamp !== undefined)
315
+ marks.push({ ordinal, timestampMs: Date.parse(timestamp) });
316
+ },
317
+ };
318
+ await scanFile({ provider, sessionId, filePath, key: `${provider}:${sessionId}:${hashPath(filePath)}` }, stat, undefined, { bytes: unbounded, bound: 'maxFileBytes', forceProgress: true }, context);
319
+ return marks;
320
+ }
268
321
  /** @internal */
269
322
  function emptyStats() {
270
323
  return {
@@ -943,7 +996,7 @@ class LineScanner {
943
996
  return {
944
997
  entry: {
945
998
  ...entry,
946
- text,
999
+ ...this.textFields(text),
947
1000
  cwd: scoped,
948
1001
  ...(place.gitBranch ? { gitBranch: place.gitBranch } : {}),
949
1002
  ...(entry.usage ? { usage: { ...entry.usage } } : {}),
@@ -962,11 +1015,21 @@ class LineScanner {
962
1015
  this.closedCarried = open;
963
1016
  this.open = null;
964
1017
  }
1018
+ /** Entry text fields: the text, or an empty string flagged as omitted. */
1019
+ textFields(text) {
1020
+ return this.context.includeText === false ? { text: '', textOmitted: true } : { text };
1021
+ }
965
1022
  /** Assign an ordinal to a human prompt and emit it when in scope. */
966
1023
  recordPrompt(text, timestamp, cwd, gitBranch, lineOffset, lineBytes, stateBefore) {
967
1024
  const ordinal = this.nextOrdinal++;
968
1025
  this.closeOpen();
969
- if (typeof timestamp !== 'string' || Number.isNaN(Date.parse(timestamp))) {
1026
+ const usable = typeof timestamp === 'string' && !Number.isNaN(Date.parse(timestamp));
1027
+ this.context.onPrompt?.({
1028
+ ordinal,
1029
+ timestamp: usable ? timestamp : undefined,
1030
+ lineOffset,
1031
+ });
1032
+ if (!usable) {
970
1033
  this.context.stats.promptsMissingTimestamp++;
971
1034
  return;
972
1035
  }
@@ -984,7 +1047,7 @@ class LineScanner {
984
1047
  sessionId: this.candidate.sessionId,
985
1048
  ordinal,
986
1049
  timestamp,
987
- text,
1050
+ ...this.textFields(text),
988
1051
  cwd: scoped,
989
1052
  ...(gitBranch ? { gitBranch } : {}),
990
1053
  metadataStatus: 'pending',
@@ -29,10 +29,26 @@ export type ProviderRetryAfter = {
29
29
  at: string;
30
30
  };
31
31
  export declare const PROVIDER_FAILURE_EVIDENCE_SOURCES: readonly ["structured-error", "http-status", "message", "authentication-check"];
32
+ /**
33
+ * The variants Codex reports in `codexErrorInfo` on `error` and failed-turn
34
+ * payloads (app-server camelCase spelling; the snake_case spelling Codex
35
+ * writes to rollout files is accepted too). `rateLimitExceeded` is an older
36
+ * spelling some Codex builds emitted.
37
+ */
38
+ export declare const CODEX_ERROR_INFO_KINDS: readonly ["contextWindowExceeded", "sessionBudgetExceeded", "usageLimitExceeded", "rateLimitExceeded", "serverOverloaded", "cyberPolicy", "httpConnectionFailed", "responseStreamConnectionFailed", "internalServerError", "unauthorized", "badRequest", "threadRollbackFailed", "sandboxError", "responseStreamDisconnected", "responseTooManyFailedAttempts", "activeTurnNotSteerable", "other"];
39
+ export type CodexErrorInfoKind = (typeof CODEX_ERROR_INFO_KINDS)[number];
40
+ /** A decoded `codexErrorInfo`: its closed variant and any upstream HTTP status. */
41
+ export interface CodexErrorInfoSummary {
42
+ kind: CodexErrorInfoKind;
43
+ /** Present when a transport variant carried a valid `httpStatusCode`. */
44
+ httpStatusCode?: number;
45
+ }
32
46
  export interface ProviderFailureEvidence {
33
47
  source: (typeof PROVIDER_FAILURE_EVIDENCE_SOURCES)[number];
34
48
  rule: ProviderFailureCode;
35
49
  checkedAt?: string;
50
+ /** Present only when a Codex `codexErrorInfo` decided the rule. */
51
+ codexErrorInfo?: CodexErrorInfoKind;
36
52
  }
37
53
  /** Safe diagnostic projection. No raw error text, request data, or credentials. */
38
54
  export interface ProviderFailureDiagnosis {
@@ -40,13 +56,41 @@ export interface ProviderFailureDiagnosis {
40
56
  credentialKind: ProviderCredentialKind;
41
57
  diagnosis: ProviderFailureCode;
42
58
  recovery: ProviderRecoveryAction;
59
+ /**
60
+ * Whether sending the same request again, unchanged and after any
61
+ * `retryAfter`, can reasonably succeed. True for a rate limit that will
62
+ * lift, overload, an unavailable service (5xx), a timeout, or a failed
63
+ * connection. False for authentication, missing runtime or configuration,
64
+ * execution policy, an invalid provider session, context overflow, a usage
65
+ * quota exhausted with no known reset, and unknown failures. Conservative:
66
+ * a hint for retry policy, not a promise. The caller still decides whether
67
+ * a repeat is safe (idempotency, output already delivered).
68
+ *
69
+ * Always set by `diagnoseProviderFailure()` since 0.29.0; optional in the
70
+ * type so diagnoses stored or built by older producers stay valid.
71
+ */
72
+ retryable?: boolean;
43
73
  evidence: ProviderFailureEvidence[];
44
74
  authentication?: ProviderAuthenticationEvidence;
45
75
  httpStatus?: number;
46
76
  retryAfter?: ProviderRetryAfter;
47
77
  }
78
+ /**
79
+ * Decode one Codex `codexErrorInfo` value: a string variant
80
+ * (`"serverOverloaded"`, `"usage_limit_exceeded"`) or an object variant
81
+ * (`{ "httpConnectionFailed": { "httpStatusCode": 503 } }`, snake_case keys
82
+ * accepted). Returns `undefined` for anything else, including `null`.
83
+ */
84
+ export declare function decodeCodexErrorInfo(value: unknown): CodexErrorInfoSummary | undefined;
48
85
  /**
49
86
  * Classify a caller-designated terminal failure without I/O or recovery actions.
50
87
  * Public status and runtime readiness do not establish request authentication.
88
+ *
89
+ * A Codex `codexErrorInfo` (or `codex_error_info`) on the error or on any
90
+ * nested `error` / `cause` / `response` / `data` / `body` / `errors` entry is
91
+ * structured evidence. It ranks ahead of the same object's `code` / `type` /
92
+ * `name`, objects are read outer first, and every structured code ranks ahead
93
+ * of HTTP status and message text. Its transport variants also supply
94
+ * `httpStatus`.
51
95
  */
52
96
  export declare function diagnoseProviderFailure(input: ProviderFailureInput): ProviderFailureDiagnosis;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PROVIDER_FAILURE_EVIDENCE_SOURCES = exports.PROVIDER_RECOVERY_ACTIONS = exports.PROVIDER_FAILURE_CODES = exports.PROVIDER_AUTHENTICATION_SOURCES = exports.PROVIDER_AUTHENTICATION_STATES = exports.PROVIDER_CREDENTIAL_KINDS = void 0;
3
+ exports.CODEX_ERROR_INFO_KINDS = exports.PROVIDER_FAILURE_EVIDENCE_SOURCES = exports.PROVIDER_RECOVERY_ACTIONS = exports.PROVIDER_FAILURE_CODES = exports.PROVIDER_AUTHENTICATION_SOURCES = exports.PROVIDER_AUTHENTICATION_STATES = exports.PROVIDER_CREDENTIAL_KINDS = void 0;
4
+ exports.decodeCodexErrorInfo = decodeCodexErrorInfo;
4
5
  exports.diagnoseProviderFailure = diagnoseProviderFailure;
5
6
  exports.PROVIDER_CREDENTIAL_KINDS = ['oauth', 'api-key', 'unknown'];
6
7
  exports.PROVIDER_AUTHENTICATION_STATES = [
@@ -46,6 +47,31 @@ exports.PROVIDER_FAILURE_EVIDENCE_SOURCES = [
46
47
  'message',
47
48
  'authentication-check',
48
49
  ];
50
+ /**
51
+ * The variants Codex reports in `codexErrorInfo` on `error` and failed-turn
52
+ * payloads (app-server camelCase spelling; the snake_case spelling Codex
53
+ * writes to rollout files is accepted too). `rateLimitExceeded` is an older
54
+ * spelling some Codex builds emitted.
55
+ */
56
+ exports.CODEX_ERROR_INFO_KINDS = [
57
+ 'contextWindowExceeded',
58
+ 'sessionBudgetExceeded',
59
+ 'usageLimitExceeded',
60
+ 'rateLimitExceeded',
61
+ 'serverOverloaded',
62
+ 'cyberPolicy',
63
+ 'httpConnectionFailed',
64
+ 'responseStreamConnectionFailed',
65
+ 'internalServerError',
66
+ 'unauthorized',
67
+ 'badRequest',
68
+ 'threadRollbackFailed',
69
+ 'sandboxError',
70
+ 'responseStreamDisconnected',
71
+ 'responseTooManyFailedAttempts',
72
+ 'activeTurnNotSteerable',
73
+ 'other',
74
+ ];
49
75
  function read(value, key) {
50
76
  try {
51
77
  return value && typeof value === 'object' ? value[key] : undefined;
@@ -81,6 +107,40 @@ function retryAfter(value) {
81
107
  const at = timestamp(value);
82
108
  return at ? { kind: 'date', at } : undefined;
83
109
  }
110
+ function compactKey(value) {
111
+ return value.toLowerCase().replace(/[^a-z0-9]/g, '');
112
+ }
113
+ const CODEX_ERROR_INFO_BY_KEY = new Map(exports.CODEX_ERROR_INFO_KINDS.map((kind) => [compactKey(kind), kind]));
114
+ /**
115
+ * Decode one Codex `codexErrorInfo` value: a string variant
116
+ * (`"serverOverloaded"`, `"usage_limit_exceeded"`) or an object variant
117
+ * (`{ "httpConnectionFailed": { "httpStatusCode": 503 } }`, snake_case keys
118
+ * accepted). Returns `undefined` for anything else, including `null`.
119
+ */
120
+ function decodeCodexErrorInfo(value) {
121
+ if (typeof value === 'string') {
122
+ const kind = value.length <= 64 ? CODEX_ERROR_INFO_BY_KEY.get(compactKey(value)) : undefined;
123
+ return kind ? { kind } : undefined;
124
+ }
125
+ if (!value || typeof value !== 'object' || Array.isArray(value))
126
+ return undefined;
127
+ let keys;
128
+ try {
129
+ keys = Object.keys(value);
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ for (const key of keys.slice(0, 8)) {
135
+ const kind = key.length <= 64 ? CODEX_ERROR_INFO_BY_KEY.get(compactKey(key)) : undefined;
136
+ if (!kind)
137
+ continue;
138
+ const payload = read(value, key);
139
+ const status = statusNumber(read(payload, 'httpStatusCode') ?? read(payload, 'http_status_code'));
140
+ return status === undefined ? { kind } : { kind, httpStatusCode: status };
141
+ }
142
+ return undefined;
143
+ }
84
144
  function rejected(kind) {
85
145
  return kind === 'oauth'
86
146
  ? 'oauth_reauthentication_required'
@@ -171,6 +231,44 @@ function classifyStatus(status, kind) {
171
231
  return 'service_unavailable';
172
232
  return undefined;
173
233
  }
234
+ /** Variants with no matching failure code (bad request, policy, budget, other) stay unclassified. */
235
+ function classifyCodexErrorInfo(info, kind) {
236
+ switch (info.kind) {
237
+ case 'usageLimitExceeded':
238
+ case 'rateLimitExceeded':
239
+ return 'rate_limited';
240
+ case 'serverOverloaded':
241
+ return 'overloaded';
242
+ case 'internalServerError':
243
+ return 'service_unavailable';
244
+ case 'unauthorized':
245
+ return rejected(kind);
246
+ case 'contextWindowExceeded':
247
+ return 'context_overflow';
248
+ case 'httpConnectionFailed':
249
+ case 'responseStreamConnectionFailed':
250
+ case 'responseStreamDisconnected':
251
+ case 'responseTooManyFailedAttempts':
252
+ return info.httpStatusCode === undefined
253
+ ? 'connection_failed'
254
+ : classifyStatus(info.httpStatusCode, kind);
255
+ default:
256
+ return undefined;
257
+ }
258
+ }
259
+ function isRetryable(code, quotaExhausted, retry) {
260
+ switch (code) {
261
+ case 'rate_limited':
262
+ return !quotaExhausted || retry !== undefined;
263
+ case 'overloaded':
264
+ case 'service_unavailable':
265
+ case 'timeout':
266
+ case 'connection_failed':
267
+ return true;
268
+ default:
269
+ return false;
270
+ }
271
+ }
174
272
  function classifyMessage(message, kind) {
175
273
  if (/\b(execution policy|sandbox policy|approval)\b.{0,60}\b(denied|blocked|rejected|not allowed)\b|\bdenied by policy\b/i.test(message))
176
274
  return 'execution_policy_denied';
@@ -236,6 +334,13 @@ function recovery(code, kind) {
236
334
  /**
237
335
  * Classify a caller-designated terminal failure without I/O or recovery actions.
238
336
  * Public status and runtime readiness do not establish request authentication.
337
+ *
338
+ * A Codex `codexErrorInfo` (or `codex_error_info`) on the error or on any
339
+ * nested `error` / `cause` / `response` / `data` / `body` / `errors` entry is
340
+ * structured evidence. It ranks ahead of the same object's `code` / `type` /
341
+ * `name`, objects are read outer first, and every structured code ranks ahead
342
+ * of HTTP status and message text. Its transport variants also supply
343
+ * `httpStatus`.
239
344
  */
240
345
  function diagnoseProviderFailure(input) {
241
346
  const codes = [];
@@ -253,11 +358,15 @@ function diagnoseProviderFailure(input) {
253
358
  if (!value || typeof value !== 'object' || visited.has(value))
254
359
  continue;
255
360
  visited.add(value);
361
+ const codex = decodeCodexErrorInfo(read(value, 'codexErrorInfo') ?? read(value, 'codex_error_info'));
362
+ if (codex)
363
+ codes.push(codex);
256
364
  for (const key of ['code', 'type', 'name'])
257
365
  codes.push(text(read(value, key)));
258
366
  messages.push(text(read(value, 'message')));
259
367
  for (const key of ['status', 'statusCode', 'httpStatus', 'status_code'])
260
368
  httpStatus ??= statusNumber(read(value, key));
369
+ httpStatus ??= codex?.httpStatusCode;
261
370
  const headers = read(value, 'headers');
262
371
  const header = typeof Headers !== 'undefined' && headers instanceof Headers
263
372
  ? headers.get('retry-after')
@@ -310,9 +419,20 @@ function diagnoseProviderFailure(input) {
310
419
  source: auth.source,
311
420
  };
312
421
  }
313
- let diagnosis = codes
314
- .map((code) => classifyCode(code, input.credentialKind))
315
- .find((code) => code !== undefined);
422
+ let diagnosis;
423
+ let codexErrorInfo;
424
+ for (const code of codes) {
425
+ if (typeof code === 'string') {
426
+ diagnosis = classifyCode(code, input.credentialKind);
427
+ }
428
+ else {
429
+ diagnosis = classifyCodexErrorInfo(code, input.credentialKind);
430
+ if (diagnosis)
431
+ codexErrorInfo = code.kind;
432
+ }
433
+ if (diagnosis)
434
+ break;
435
+ }
316
436
  let source = 'structured-error';
317
437
  if (!diagnosis && httpStatus !== undefined) {
318
438
  diagnosis = classifyStatus(httpStatus, input.credentialKind);
@@ -348,6 +468,7 @@ function diagnoseProviderFailure(input) {
348
468
  credentialKind: input.credentialKind,
349
469
  diagnosis,
350
470
  recovery: recovery(diagnosis, input.credentialKind),
471
+ retryable: isRetryable(diagnosis, codexErrorInfo === 'usageLimitExceeded', retry),
351
472
  evidence: diagnosis === 'unknown'
352
473
  ? []
353
474
  : [
@@ -357,6 +478,7 @@ function diagnoseProviderFailure(input) {
357
478
  ...(source === 'authentication-check'
358
479
  ? { checkedAt: authentication.checkedAt }
359
480
  : {}),
481
+ ...(codexErrorInfo ? { codexErrorInfo } : {}),
360
482
  },
361
483
  ],
362
484
  ...(authentication ? { authentication } : {}),
@@ -9,31 +9,12 @@
9
9
  * Ported from sidekick-vscode/src/services/providers/CodexSessionProvider.ts
10
10
  */
11
11
  import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
12
+ import { findCodexRolloutFile } from './codexRollouts';
12
13
  import type { ListSessionFilesOptions, SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileInfo, SessionFileStats, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions, WatchedSessionFile } from './types';
13
14
  import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
14
15
  import type { CodexRateLimits } from '../types/codex';
15
- export interface FindCodexRolloutFileOptions {
16
- /**
17
- * Codex home whose `sessions/` subtree to search. Defaults to every
18
- * monitored home: `$CODEX_HOME` if set, else `~/.codex`, plus managed
19
- * profile homes that recorded sessions.
20
- */
21
- codexHome?: string;
22
- }
23
- /**
24
- * Locates the rollout file for a Codex session id by walking
25
- * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
26
- * `<home>/archived_sessions/`, where Codex moves the rollouts of archived
27
- * threads.
28
- *
29
- * Filename-only match, case-insensitive on the id — no file contents are read
30
- * and files are only stat'ed once their name matches, so the walk stays cheap
31
- * even over a large history. When the same id exists in more than one home,
32
- * the most recently modified copy wins. Returns `null` for an unknown, empty,
33
- * or whitespace id. Consumers who want the database's answer instead can query
34
- * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
35
- */
36
- export declare function findCodexRolloutFile(sessionId: string, options?: FindCodexRolloutFileOptions): string | null;
16
+ export { findCodexRolloutFile };
17
+ export type { FindCodexRolloutFileOptions } from './codexRollouts';
37
18
  /**
38
19
  * Session provider for Codex CLI.
39
20
  *
@@ -43,8 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.CodexProvider = void 0;
47
- exports.findCodexRolloutFile = findCodexRolloutFile;
46
+ exports.CodexProvider = exports.findCodexRolloutFile = void 0;
48
47
  const fs = __importStar(require("fs"));
49
48
  const path = __importStar(require("path"));
50
49
  const string_decoder_1 = require("string_decoder");
@@ -56,6 +55,7 @@ const subagentStatsCache_1 = require("../parsers/subagentStatsCache");
56
55
  const rolloutWalker_1 = require("./rolloutWalker");
57
56
  const codexPaths_1 = require("../codexPaths");
58
57
  const codexRollouts_1 = require("./codexRollouts");
58
+ Object.defineProperty(exports, "findCodexRolloutFile", { enumerable: true, get: function () { return codexRollouts_1.findCodexRolloutFile; } });
59
59
  const directoryListingCache_1 = require("./directoryListingCache");
60
60
  const diagnostics_1 = require("./diagnostics");
61
61
  const modelContext_1 = require("../modelContext");
@@ -84,29 +84,6 @@ function hasRolloutFile(dir) {
84
84
  function findRolloutFilesInConfiguredHomes(options = {}) {
85
85
  return (0, rolloutWalker_1.walkRolloutFiles)((0, codexRollouts_1.getSessionsDirs)(), options);
86
86
  }
87
- /**
88
- * Locates the rollout file for a Codex session id by walking
89
- * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
90
- * `<home>/archived_sessions/`, where Codex moves the rollouts of archived
91
- * threads.
92
- *
93
- * Filename-only match, case-insensitive on the id — no file contents are read
94
- * and files are only stat'ed once their name matches, so the walk stays cheap
95
- * even over a large history. When the same id exists in more than one home,
96
- * the most recently modified copy wins. Returns `null` for an unknown, empty,
97
- * or whitespace id. Consumers who want the database's answer instead can query
98
- * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
99
- */
100
- function findCodexRolloutFile(sessionId, options = {}) {
101
- const normalizedId = sessionId.trim().toLowerCase();
102
- if (!normalizedId)
103
- return null;
104
- const sessionsDirs = options.codexHome
105
- ? (0, codexPaths_1.getCodexSessionsDirs)([options.codexHome])
106
- : (0, codexRollouts_1.getSessionsDirs)();
107
- // Name-only filter, one walk across the homes, newest modification first.
108
- return (0, rolloutWalker_1.walkRolloutFiles)(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
109
- }
110
87
  /** Pick the best sessions directory for UI/search entrypoints. */
111
88
  function getSessionsDir() {
112
89
  const dirs = (0, codexPaths_1.getCodexSessionsDirs)(undefined, { includeArchived: false });
@@ -634,7 +611,7 @@ class CodexProvider {
634
611
  }
635
612
  findSessionById(_workspacePath, sessionId) {
636
613
  try {
637
- const found = findCodexRolloutFile(sessionId);
614
+ const found = (0, codexRollouts_1.findCodexRolloutFile)(sessionId);
638
615
  this.recordDatabaseFallback('findSessionById', true);
639
616
  return found;
640
617
  }
@@ -4,8 +4,9 @@
4
4
  */
5
5
  import type { CodexDbThread } from '../types/codex';
6
6
  import type { ProviderRuntimeStatus } from './types';
7
- /** Parent thread id recorded in a Codex `threads.source` JSON value, if any. */
8
- export declare function spawnParentFromSource(source: unknown): string | undefined;
7
+ import { codexSpawnParentFromSource as spawnParentFromSource } from '../parsers/codexRolloutOrigin';
8
+ /** Parent thread id of a spawned subagent's `source`; one rule shared with rollout parsing. */
9
+ export { spawnParentFromSource };
9
10
  export declare class CodexDatabase {
10
11
  private readonly dbPath;
11
12
  private sqlite3Available;
@@ -37,35 +37,17 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  };
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.CodexDatabase = void 0;
41
- exports.spawnParentFromSource = spawnParentFromSource;
40
+ exports.CodexDatabase = exports.spawnParentFromSource = void 0;
42
41
  const fs = __importStar(require("fs"));
43
42
  const path = __importStar(require("path"));
44
43
  const child_process_1 = require("child_process");
44
+ const codexRolloutOrigin_1 = require("../parsers/codexRolloutOrigin");
45
+ Object.defineProperty(exports, "spawnParentFromSource", { enumerable: true, get: function () { return codexRolloutOrigin_1.codexSpawnParentFromSource; } });
45
46
  /**
46
47
  * Ids per inlined `IN (…)` query. Values are passed inside a single argv
47
48
  * element to sqlite3, so an unbounded list would hit E2BIG on large stores.
48
49
  */
49
50
  const SQL_ID_CHUNK_SIZE = 400;
50
- /** Parent thread id recorded in a Codex `threads.source` JSON value, if any. */
51
- function spawnParentFromSource(source) {
52
- let parsed = source;
53
- if (typeof source === 'string') {
54
- if (!source.startsWith('{'))
55
- return undefined;
56
- try {
57
- parsed = JSON.parse(source);
58
- }
59
- catch {
60
- return undefined;
61
- }
62
- }
63
- if (!parsed || typeof parsed !== 'object')
64
- return undefined;
65
- const parent = parsed
66
- .subagent?.thread_spawn?.parent_thread_id;
67
- return typeof parent === 'string' ? parent : undefined;
68
- }
69
51
  class CodexDatabase {
70
52
  dbPath;
71
53
  sqlite3Available = null;
@@ -246,7 +228,7 @@ class CodexDatabase {
246
228
  if (!this.getThreadColumns().has('source'))
247
229
  return [];
248
230
  const rows = this.query('SELECT * FROM threads WHERE source LIKE ? ORDER BY created_at ASC', [`%${parentId}%`]);
249
- return rows.filter((row) => spawnParentFromSource(row.source) === parentId);
231
+ return rows.filter((row) => (0, codexRolloutOrigin_1.codexSpawnParentFromSource)(row.source) === parentId);
250
232
  }
251
233
  /** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
252
234
  getDbMtime() {
@@ -42,3 +42,25 @@ export declare function normalizePath(input: string): string;
42
42
  * sits inside it.
43
43
  */
44
44
  export declare function listCodexSessionFilesAsync(workspacePath?: string, options?: ListSessionFilesOptions, cache?: DirectoryListingCache): Promise<SessionFileInfo[]>;
45
+ export interface FindCodexRolloutFileOptions {
46
+ /**
47
+ * Codex home whose `sessions/` subtree to search. Defaults to every
48
+ * monitored home: `$CODEX_HOME` if set, else `~/.codex`, plus managed
49
+ * profile homes that recorded sessions.
50
+ */
51
+ codexHome?: string;
52
+ }
53
+ /**
54
+ * Locates the rollout file for a Codex session id by walking
55
+ * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
56
+ * `<home>/archived_sessions/`, where Codex moves the rollouts of archived
57
+ * threads.
58
+ *
59
+ * Filename-only match, case-insensitive on the id — no file contents are read
60
+ * and files are only stat'ed once their name matches, so the walk stays cheap
61
+ * even over a large history. When the same id exists in more than one home,
62
+ * the most recently modified copy wins. Returns `null` for an unknown, empty,
63
+ * or whitespace id. Consumers who want the database's answer instead can query
64
+ * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
65
+ */
66
+ export declare function findCodexRolloutFile(sessionId: string, options?: FindCodexRolloutFileOptions): string | null;
@@ -50,6 +50,7 @@ exports.initializeCounters = initializeCounters;
50
50
  exports.cwdMatches = cwdMatches;
51
51
  exports.normalizePath = normalizePath;
52
52
  exports.listCodexSessionFilesAsync = listCodexSessionFilesAsync;
53
+ exports.findCodexRolloutFile = findCodexRolloutFile;
53
54
  const fs = __importStar(require("fs"));
54
55
  const path = __importStar(require("path"));
55
56
  const codexPaths_1 = require("../codexPaths");
@@ -259,3 +260,26 @@ async function listCodexSessionFilesAsync(workspacePath, options = {}, cache = n
259
260
  }
260
261
  return results;
261
262
  }
263
+ /**
264
+ * Locates the rollout file for a Codex session id by walking
265
+ * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
266
+ * `<home>/archived_sessions/`, where Codex moves the rollouts of archived
267
+ * threads.
268
+ *
269
+ * Filename-only match, case-insensitive on the id — no file contents are read
270
+ * and files are only stat'ed once their name matches, so the walk stays cheap
271
+ * even over a large history. When the same id exists in more than one home,
272
+ * the most recently modified copy wins. Returns `null` for an unknown, empty,
273
+ * or whitespace id. Consumers who want the database's answer instead can query
274
+ * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
275
+ */
276
+ function findCodexRolloutFile(sessionId, options = {}) {
277
+ const normalizedId = sessionId.trim().toLowerCase();
278
+ if (!normalizedId)
279
+ return null;
280
+ const sessionsDirs = options.codexHome
281
+ ? (0, codexPaths_1.getCodexSessionsDirs)([options.codexHome])
282
+ : getSessionsDirs();
283
+ // Name-only filter, one walk across the homes, newest modification first.
284
+ return (0, rolloutWalker_1.walkRolloutFiles)(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
285
+ }
@@ -60,7 +60,6 @@ const sessionContext_1 = require("../context/sessionContext");
60
60
  const sessionStats_1 = require("../sessionStats");
61
61
  const openCodeParser_1 = require("../parsers/openCodeParser");
62
62
  const openCodeDatabase_1 = require("./openCodeDatabase");
63
- const usageNormalization_1 = require("../usageNormalization");
64
63
  const subagentUsage_1 = require("../usage/subagentUsage");
65
64
  const zaiQuota_1 = require("../zaiQuota");
66
65
  const diagnostics_1 = require("./diagnostics");
@@ -1493,21 +1492,14 @@ class OpenCodeProvider {
1493
1492
  if (tokens) {
1494
1493
  // Same semantics as the main-session parser (openCodeParser).
1495
1494
  const cache = tokens.cache;
1496
- const providerID = msgData.providerID;
1497
- (0, subagentUsage_1.addUsageToSubagent)(tokenTotals, (0, usageNormalization_1.normalizeProviderUsage)({
1498
- semantics: providerID === 'openai'
1499
- ? 'openai'
1500
- : providerID === 'anthropic'
1501
- ? 'anthropic'
1502
- : 'sidekick',
1503
- provider: providerID ?? 'opencode',
1504
- inputTokens: tokens.input,
1505
- outputTokens: tokens.output,
1506
- cacheReadTokens: cache?.read,
1507
- cacheWriteTokens: cache?.write,
1508
- reasoningTokens: tokens.reasoning,
1509
- reasoningIncludedInOutput: providerID === 'openai',
1510
- }));
1495
+ (0, subagentUsage_1.addUsageToSubagent)(tokenTotals, (0, openCodeParser_1.normalizeOpenCodeUsage)({
1496
+ input: tokens.input,
1497
+ output: tokens.output,
1498
+ cacheRead: cache?.read,
1499
+ cacheWrite: cache?.write,
1500
+ reasoning: tokens.reasoning,
1501
+ total: tokens.total,
1502
+ }, msgData.providerID));
1511
1503
  }
1512
1504
  }
1513
1505
  catch {