sidekick-shared 0.28.3 → 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.
- package/CHANGELOG.md +64 -0
- package/README.md +149 -19
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +12 -6
- package/dist/browser.js +17 -1
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +4 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +9 -6
- package/dist/esm/modelCatalog.js +2 -1
- package/dist/esm/modelInfo.js +422 -45
- package/dist/esm/node.js +9 -3
- package/dist/esm/parsers/codexParser.js +175 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +83 -21
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +14 -7
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/codex.js +2 -24
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +42 -3
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/jsonlTail.js +378 -59
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +16 -10
- package/dist/index.js +35 -12
- package/dist/modelCatalog.js +2 -1
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +431 -47
- package/dist/node.d.ts +16 -5
- package/dist/node.js +31 -2
- package/dist/parsers/codexParser.d.ts +55 -0
- package/dist/parsers/codexParser.js +175 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +86 -21
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +13 -6
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +3 -26
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +42 -3
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/jsonlTail.d.ts +101 -4
- package/dist/watchers/jsonlTail.js +380 -60
- package/package.json +41 -1
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { promises as fs } from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { _setCatalogContextWindows } from './modelContext';
|
|
22
|
-
import { _setPricingOverrides } from './modelInfo';
|
|
22
|
+
import { _setPricingOverrides, getPricingCatalogInfo } from './modelInfo';
|
|
23
23
|
import { getConfigDir } from './paths';
|
|
24
24
|
import { atomicWriteJson } from './writers/atomic';
|
|
25
25
|
// ── Constants ──
|
|
@@ -36,6 +36,10 @@ const DEFAULT_TIMEOUT_MS = 3_000;
|
|
|
36
36
|
* 2. Otherwise attempt a network fetch; on success, persist + apply.
|
|
37
37
|
* 3. On any failure, load a stale cache if present; else leave overrides empty.
|
|
38
38
|
*
|
|
39
|
+
* `getPricingCatalogInfo().source` afterwards reads `litellm` for a fresh fetch
|
|
40
|
+
* or a cache within `ttlMs`, and `stale-cache` for a cache older than `ttlMs`
|
|
41
|
+
* — whether the refresh failed or `offline` skipped it.
|
|
42
|
+
*
|
|
39
43
|
* Always resolves — never throws. Offline-safe.
|
|
40
44
|
*/
|
|
41
45
|
export async function hydratePricingCatalog(options = {}) {
|
|
@@ -44,8 +48,9 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
44
48
|
const now = Date.now();
|
|
45
49
|
// 1. Fresh cache? (Offline callers take any cache, however old.)
|
|
46
50
|
const cached = await readCache(cachePath);
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const cacheFresh = cached !== null && now - Date.parse(cached.fetchedAt) < ttlMs;
|
|
52
|
+
if (cached && (offline || cacheFresh)) {
|
|
53
|
+
return applyCacheFile(cached, 'cache', cacheFresh ? 'litellm' : 'stale-cache');
|
|
49
54
|
}
|
|
50
55
|
// 2. Try network.
|
|
51
56
|
if (fetchImpl && !offline) {
|
|
@@ -58,12 +63,12 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
58
63
|
contextWindows: normalizeLiteLlmContextWindows(fetched),
|
|
59
64
|
};
|
|
60
65
|
await writeCache(cachePath, payload, logger);
|
|
61
|
-
return applyCacheFile(payload, 'network');
|
|
66
|
+
return applyCacheFile(payload, 'network', 'litellm');
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
// 3. Fall back to stale cache if present.
|
|
65
70
|
if (cached) {
|
|
66
|
-
return applyCacheFile(cached, 'cache');
|
|
71
|
+
return applyCacheFile(cached, 'cache', 'stale-cache');
|
|
67
72
|
}
|
|
68
73
|
// 4. Nothing we can do — stay on static tables.
|
|
69
74
|
return {
|
|
@@ -71,6 +76,7 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
71
76
|
entries: 0,
|
|
72
77
|
contextWindowEntries: 0,
|
|
73
78
|
fetchedAt: new Date(now).toISOString(),
|
|
79
|
+
digest: getPricingCatalogInfo().digest,
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
@@ -78,9 +84,9 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
78
84
|
* `contextWindows` is absent on caches written before context hydration; those
|
|
79
85
|
* contribute no overrides rather than clearing whatever is already loaded.
|
|
80
86
|
*/
|
|
81
|
-
function applyCacheFile(cache, source) {
|
|
87
|
+
function applyCacheFile(cache, source, basis) {
|
|
82
88
|
const contextWindows = cache.contextWindows ?? {};
|
|
83
|
-
_setPricingOverrides(cache.overrides);
|
|
89
|
+
_setPricingOverrides(cache.overrides, { source: basis, fetchedAt: cache.fetchedAt });
|
|
84
90
|
if (Object.keys(contextWindows).length > 0) {
|
|
85
91
|
_setCatalogContextWindows(contextWindows);
|
|
86
92
|
}
|
|
@@ -89,6 +95,7 @@ function applyCacheFile(cache, source) {
|
|
|
89
95
|
entries: Object.keys(cache.overrides).length,
|
|
90
96
|
contextWindowEntries: Object.keys(contextWindows).length,
|
|
91
97
|
fetchedAt: cache.fetchedAt,
|
|
98
|
+
digest: getPricingCatalogInfo().digest,
|
|
92
99
|
};
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* record over `maxRecordBytes` is skipped without being buffered and reported
|
|
15
15
|
* in `exclusions`, the only way a record is permanently left out.
|
|
16
16
|
*
|
|
17
|
+
* `includeText: false` returns the same entries, ordinals, timestamps,
|
|
18
|
+
* metadata, exclusions, and cursor without prompt text, for consumers that
|
|
19
|
+
* only count or attribute prompts. Cursors are interchangeable between modes.
|
|
20
|
+
*
|
|
17
21
|
* A prompt can be written before its answer. The cursor then keeps the byte
|
|
18
22
|
* offset of that prompt's line and its answer-tracking state (never text), and
|
|
19
23
|
* the prompt is returned again under the same identity once its first
|
|
@@ -28,12 +32,19 @@ import { listCodexSessionFilesAsync } from './providers/codexRollouts';
|
|
|
28
32
|
import { DirectoryListingCache } from './providers/directoryListingCache';
|
|
29
33
|
import { realpathOrNull, resolveWorkspaceRoots } from './workspaceRoots';
|
|
30
34
|
import { fileHeadOf, HEAD_BYTES, headMatches } from './fileHead';
|
|
35
|
+
/**
|
|
36
|
+
* The default `maxRecordBytes` (16 MiB). Prompt ordinals are only comparable
|
|
37
|
+
* between readers that skip the same records, so every reader that publishes
|
|
38
|
+
* or joins on ordinals (prompt history, session prompt history, usage events'
|
|
39
|
+
* `promptOrdinal`) uses this bound.
|
|
40
|
+
*/
|
|
41
|
+
export const PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES = 16 * 1024 * 1024;
|
|
31
42
|
/** @internal */
|
|
32
43
|
export const DEFAULT_BOUNDS = {
|
|
33
44
|
maxSessions: 1000,
|
|
34
45
|
maxFileBytes: 16 * 1024 * 1024,
|
|
35
46
|
maxTotalBytes: 256 * 1024 * 1024,
|
|
36
|
-
maxRecordBytes:
|
|
47
|
+
maxRecordBytes: PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
|
|
37
48
|
maxEntries: 10_000,
|
|
38
49
|
deadlineMs: 30_000,
|
|
39
50
|
};
|
|
@@ -108,6 +119,7 @@ export async function collectPromptHistory(options) {
|
|
|
108
119
|
if (exclusions.length < MAX_EXCLUSIONS)
|
|
109
120
|
exclusions.push(exclusion);
|
|
110
121
|
},
|
|
122
|
+
includeText: options.includeText !== false,
|
|
111
123
|
};
|
|
112
124
|
let totalBytes = 0;
|
|
113
125
|
let scannedAny = false;
|
|
@@ -221,6 +233,46 @@ export async function collectPromptHistory(options) {
|
|
|
221
233
|
}
|
|
222
234
|
return finish();
|
|
223
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Every human prompt of one session file with its ordinal and timestamp, read
|
|
238
|
+
* whole in one pass with the default record bound, so the ordinals are the
|
|
239
|
+
* ones `collectPromptHistory()` returns with default bounds. No scope check
|
|
240
|
+
* and no text: prompts are counted wherever their cwd points. Prompts whose
|
|
241
|
+
* line has no usable timestamp still take an ordinal but are left out. A
|
|
242
|
+
* non-interactive Codex session (subagent, fork, unknown source) has none.
|
|
243
|
+
* In file order. Rejects when the file cannot be opened or changes while its
|
|
244
|
+
* head is read.
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
247
|
+
export async function readPromptOrdinalMarks(provider, sessionId, filePath) {
|
|
248
|
+
const marks = [];
|
|
249
|
+
const stat = await fs.promises.stat(filePath);
|
|
250
|
+
const unbounded = Number.MAX_SAFE_INTEGER;
|
|
251
|
+
const context = {
|
|
252
|
+
roots: [],
|
|
253
|
+
sinceMs: null,
|
|
254
|
+
stats: emptyStats(),
|
|
255
|
+
realpaths: new Map(),
|
|
256
|
+
bounds: {
|
|
257
|
+
maxSessions: unbounded,
|
|
258
|
+
maxFileBytes: unbounded,
|
|
259
|
+
maxTotalBytes: unbounded,
|
|
260
|
+
maxRecordBytes: PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
|
|
261
|
+
maxEntries: unbounded,
|
|
262
|
+
deadlineMs: unbounded,
|
|
263
|
+
},
|
|
264
|
+
shouldStop: () => null,
|
|
265
|
+
entriesSoFar: () => 0,
|
|
266
|
+
exclude: () => undefined,
|
|
267
|
+
includeText: false,
|
|
268
|
+
onPrompt: ({ ordinal, timestamp }) => {
|
|
269
|
+
if (timestamp !== undefined)
|
|
270
|
+
marks.push({ ordinal, timestampMs: Date.parse(timestamp) });
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
await scanFile({ provider, sessionId, filePath, key: `${provider}:${sessionId}:${hashPath(filePath)}` }, stat, undefined, { bytes: unbounded, bound: 'maxFileBytes', forceProgress: true }, context);
|
|
274
|
+
return marks;
|
|
275
|
+
}
|
|
224
276
|
/** @internal */
|
|
225
277
|
export function emptyStats() {
|
|
226
278
|
return {
|
|
@@ -899,7 +951,7 @@ class LineScanner {
|
|
|
899
951
|
return {
|
|
900
952
|
entry: {
|
|
901
953
|
...entry,
|
|
902
|
-
text,
|
|
954
|
+
...this.textFields(text),
|
|
903
955
|
cwd: scoped,
|
|
904
956
|
...(place.gitBranch ? { gitBranch: place.gitBranch } : {}),
|
|
905
957
|
...(entry.usage ? { usage: { ...entry.usage } } : {}),
|
|
@@ -918,11 +970,21 @@ class LineScanner {
|
|
|
918
970
|
this.closedCarried = open;
|
|
919
971
|
this.open = null;
|
|
920
972
|
}
|
|
973
|
+
/** Entry text fields: the text, or an empty string flagged as omitted. */
|
|
974
|
+
textFields(text) {
|
|
975
|
+
return this.context.includeText === false ? { text: '', textOmitted: true } : { text };
|
|
976
|
+
}
|
|
921
977
|
/** Assign an ordinal to a human prompt and emit it when in scope. */
|
|
922
978
|
recordPrompt(text, timestamp, cwd, gitBranch, lineOffset, lineBytes, stateBefore) {
|
|
923
979
|
const ordinal = this.nextOrdinal++;
|
|
924
980
|
this.closeOpen();
|
|
925
|
-
|
|
981
|
+
const usable = typeof timestamp === 'string' && !Number.isNaN(Date.parse(timestamp));
|
|
982
|
+
this.context.onPrompt?.({
|
|
983
|
+
ordinal,
|
|
984
|
+
timestamp: usable ? timestamp : undefined,
|
|
985
|
+
lineOffset,
|
|
986
|
+
});
|
|
987
|
+
if (!usable) {
|
|
926
988
|
this.context.stats.promptsMissingTimestamp++;
|
|
927
989
|
return;
|
|
928
990
|
}
|
|
@@ -940,7 +1002,7 @@ class LineScanner {
|
|
|
940
1002
|
sessionId: this.candidate.sessionId,
|
|
941
1003
|
ordinal,
|
|
942
1004
|
timestamp,
|
|
943
|
-
text,
|
|
1005
|
+
...this.textFields(text),
|
|
944
1006
|
cwd: scoped,
|
|
945
1007
|
...(gitBranch ? { gitBranch } : {}),
|
|
946
1008
|
metadataStatus: 'pending',
|
|
@@ -42,6 +42,31 @@ export const PROVIDER_FAILURE_EVIDENCE_SOURCES = [
|
|
|
42
42
|
'message',
|
|
43
43
|
'authentication-check',
|
|
44
44
|
];
|
|
45
|
+
/**
|
|
46
|
+
* The variants Codex reports in `codexErrorInfo` on `error` and failed-turn
|
|
47
|
+
* payloads (app-server camelCase spelling; the snake_case spelling Codex
|
|
48
|
+
* writes to rollout files is accepted too). `rateLimitExceeded` is an older
|
|
49
|
+
* spelling some Codex builds emitted.
|
|
50
|
+
*/
|
|
51
|
+
export const CODEX_ERROR_INFO_KINDS = [
|
|
52
|
+
'contextWindowExceeded',
|
|
53
|
+
'sessionBudgetExceeded',
|
|
54
|
+
'usageLimitExceeded',
|
|
55
|
+
'rateLimitExceeded',
|
|
56
|
+
'serverOverloaded',
|
|
57
|
+
'cyberPolicy',
|
|
58
|
+
'httpConnectionFailed',
|
|
59
|
+
'responseStreamConnectionFailed',
|
|
60
|
+
'internalServerError',
|
|
61
|
+
'unauthorized',
|
|
62
|
+
'badRequest',
|
|
63
|
+
'threadRollbackFailed',
|
|
64
|
+
'sandboxError',
|
|
65
|
+
'responseStreamDisconnected',
|
|
66
|
+
'responseTooManyFailedAttempts',
|
|
67
|
+
'activeTurnNotSteerable',
|
|
68
|
+
'other',
|
|
69
|
+
];
|
|
45
70
|
function read(value, key) {
|
|
46
71
|
try {
|
|
47
72
|
return value && typeof value === 'object' ? value[key] : undefined;
|
|
@@ -77,6 +102,40 @@ function retryAfter(value) {
|
|
|
77
102
|
const at = timestamp(value);
|
|
78
103
|
return at ? { kind: 'date', at } : undefined;
|
|
79
104
|
}
|
|
105
|
+
function compactKey(value) {
|
|
106
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
107
|
+
}
|
|
108
|
+
const CODEX_ERROR_INFO_BY_KEY = new Map(CODEX_ERROR_INFO_KINDS.map((kind) => [compactKey(kind), kind]));
|
|
109
|
+
/**
|
|
110
|
+
* Decode one Codex `codexErrorInfo` value: a string variant
|
|
111
|
+
* (`"serverOverloaded"`, `"usage_limit_exceeded"`) or an object variant
|
|
112
|
+
* (`{ "httpConnectionFailed": { "httpStatusCode": 503 } }`, snake_case keys
|
|
113
|
+
* accepted). Returns `undefined` for anything else, including `null`.
|
|
114
|
+
*/
|
|
115
|
+
export function decodeCodexErrorInfo(value) {
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
const kind = value.length <= 64 ? CODEX_ERROR_INFO_BY_KEY.get(compactKey(value)) : undefined;
|
|
118
|
+
return kind ? { kind } : undefined;
|
|
119
|
+
}
|
|
120
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
121
|
+
return undefined;
|
|
122
|
+
let keys;
|
|
123
|
+
try {
|
|
124
|
+
keys = Object.keys(value);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
for (const key of keys.slice(0, 8)) {
|
|
130
|
+
const kind = key.length <= 64 ? CODEX_ERROR_INFO_BY_KEY.get(compactKey(key)) : undefined;
|
|
131
|
+
if (!kind)
|
|
132
|
+
continue;
|
|
133
|
+
const payload = read(value, key);
|
|
134
|
+
const status = statusNumber(read(payload, 'httpStatusCode') ?? read(payload, 'http_status_code'));
|
|
135
|
+
return status === undefined ? { kind } : { kind, httpStatusCode: status };
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
80
139
|
function rejected(kind) {
|
|
81
140
|
return kind === 'oauth'
|
|
82
141
|
? 'oauth_reauthentication_required'
|
|
@@ -167,6 +226,44 @@ function classifyStatus(status, kind) {
|
|
|
167
226
|
return 'service_unavailable';
|
|
168
227
|
return undefined;
|
|
169
228
|
}
|
|
229
|
+
/** Variants with no matching failure code (bad request, policy, budget, other) stay unclassified. */
|
|
230
|
+
function classifyCodexErrorInfo(info, kind) {
|
|
231
|
+
switch (info.kind) {
|
|
232
|
+
case 'usageLimitExceeded':
|
|
233
|
+
case 'rateLimitExceeded':
|
|
234
|
+
return 'rate_limited';
|
|
235
|
+
case 'serverOverloaded':
|
|
236
|
+
return 'overloaded';
|
|
237
|
+
case 'internalServerError':
|
|
238
|
+
return 'service_unavailable';
|
|
239
|
+
case 'unauthorized':
|
|
240
|
+
return rejected(kind);
|
|
241
|
+
case 'contextWindowExceeded':
|
|
242
|
+
return 'context_overflow';
|
|
243
|
+
case 'httpConnectionFailed':
|
|
244
|
+
case 'responseStreamConnectionFailed':
|
|
245
|
+
case 'responseStreamDisconnected':
|
|
246
|
+
case 'responseTooManyFailedAttempts':
|
|
247
|
+
return info.httpStatusCode === undefined
|
|
248
|
+
? 'connection_failed'
|
|
249
|
+
: classifyStatus(info.httpStatusCode, kind);
|
|
250
|
+
default:
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function isRetryable(code, quotaExhausted, retry) {
|
|
255
|
+
switch (code) {
|
|
256
|
+
case 'rate_limited':
|
|
257
|
+
return !quotaExhausted || retry !== undefined;
|
|
258
|
+
case 'overloaded':
|
|
259
|
+
case 'service_unavailable':
|
|
260
|
+
case 'timeout':
|
|
261
|
+
case 'connection_failed':
|
|
262
|
+
return true;
|
|
263
|
+
default:
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
170
267
|
function classifyMessage(message, kind) {
|
|
171
268
|
if (/\b(execution policy|sandbox policy|approval)\b.{0,60}\b(denied|blocked|rejected|not allowed)\b|\bdenied by policy\b/i.test(message))
|
|
172
269
|
return 'execution_policy_denied';
|
|
@@ -232,6 +329,13 @@ function recovery(code, kind) {
|
|
|
232
329
|
/**
|
|
233
330
|
* Classify a caller-designated terminal failure without I/O or recovery actions.
|
|
234
331
|
* Public status and runtime readiness do not establish request authentication.
|
|
332
|
+
*
|
|
333
|
+
* A Codex `codexErrorInfo` (or `codex_error_info`) on the error or on any
|
|
334
|
+
* nested `error` / `cause` / `response` / `data` / `body` / `errors` entry is
|
|
335
|
+
* structured evidence. It ranks ahead of the same object's `code` / `type` /
|
|
336
|
+
* `name`, objects are read outer first, and every structured code ranks ahead
|
|
337
|
+
* of HTTP status and message text. Its transport variants also supply
|
|
338
|
+
* `httpStatus`.
|
|
235
339
|
*/
|
|
236
340
|
export function diagnoseProviderFailure(input) {
|
|
237
341
|
const codes = [];
|
|
@@ -249,11 +353,15 @@ export function diagnoseProviderFailure(input) {
|
|
|
249
353
|
if (!value || typeof value !== 'object' || visited.has(value))
|
|
250
354
|
continue;
|
|
251
355
|
visited.add(value);
|
|
356
|
+
const codex = decodeCodexErrorInfo(read(value, 'codexErrorInfo') ?? read(value, 'codex_error_info'));
|
|
357
|
+
if (codex)
|
|
358
|
+
codes.push(codex);
|
|
252
359
|
for (const key of ['code', 'type', 'name'])
|
|
253
360
|
codes.push(text(read(value, key)));
|
|
254
361
|
messages.push(text(read(value, 'message')));
|
|
255
362
|
for (const key of ['status', 'statusCode', 'httpStatus', 'status_code'])
|
|
256
363
|
httpStatus ??= statusNumber(read(value, key));
|
|
364
|
+
httpStatus ??= codex?.httpStatusCode;
|
|
257
365
|
const headers = read(value, 'headers');
|
|
258
366
|
const header = typeof Headers !== 'undefined' && headers instanceof Headers
|
|
259
367
|
? headers.get('retry-after')
|
|
@@ -306,9 +414,20 @@ export function diagnoseProviderFailure(input) {
|
|
|
306
414
|
source: auth.source,
|
|
307
415
|
};
|
|
308
416
|
}
|
|
309
|
-
let diagnosis
|
|
310
|
-
|
|
311
|
-
|
|
417
|
+
let diagnosis;
|
|
418
|
+
let codexErrorInfo;
|
|
419
|
+
for (const code of codes) {
|
|
420
|
+
if (typeof code === 'string') {
|
|
421
|
+
diagnosis = classifyCode(code, input.credentialKind);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
diagnosis = classifyCodexErrorInfo(code, input.credentialKind);
|
|
425
|
+
if (diagnosis)
|
|
426
|
+
codexErrorInfo = code.kind;
|
|
427
|
+
}
|
|
428
|
+
if (diagnosis)
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
312
431
|
let source = 'structured-error';
|
|
313
432
|
if (!diagnosis && httpStatus !== undefined) {
|
|
314
433
|
diagnosis = classifyStatus(httpStatus, input.credentialKind);
|
|
@@ -344,6 +463,7 @@ export function diagnoseProviderFailure(input) {
|
|
|
344
463
|
credentialKind: input.credentialKind,
|
|
345
464
|
diagnosis,
|
|
346
465
|
recovery: recovery(diagnosis, input.credentialKind),
|
|
466
|
+
retryable: isRetryable(diagnosis, codexErrorInfo === 'usageLimitExceeded', retry),
|
|
347
467
|
evidence: diagnosis === 'unknown'
|
|
348
468
|
? []
|
|
349
469
|
: [
|
|
@@ -353,6 +473,7 @@ export function diagnoseProviderFailure(input) {
|
|
|
353
473
|
...(source === 'authentication-check'
|
|
354
474
|
? { checkedAt: authentication.checkedAt }
|
|
355
475
|
: {}),
|
|
476
|
+
...(codexErrorInfo ? { codexErrorInfo } : {}),
|
|
356
477
|
},
|
|
357
478
|
],
|
|
358
479
|
...(authentication ? { authentication } : {}),
|
|
@@ -18,7 +18,7 @@ import { CodexDatabase, spawnParentFromSource } from './codexDatabase';
|
|
|
18
18
|
import { cachedSubagentStats } from '../parsers/subagentStatsCache';
|
|
19
19
|
import { DEFAULT_ROLLOUT_WALK_MAX_DEPTH, extractRolloutSessionId as extractSessionId, isRolloutFile, walkRolloutFiles, } from './rolloutWalker';
|
|
20
20
|
import { getCodexSessionsDirs } from '../codexPaths';
|
|
21
|
-
import { cwdMatches, getSessionsDirs, listCodexSessionFilesAsync, normalizePath, readFirstLines, readSessionCwd, readSessionCwdAsync, readSessionMeta, } from './codexRollouts';
|
|
21
|
+
import { cwdMatches, findCodexRolloutFile, getSessionsDirs, listCodexSessionFilesAsync, normalizePath, readFirstLines, readSessionCwd, readSessionCwdAsync, readSessionMeta, } from './codexRollouts';
|
|
22
22
|
import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
|
|
23
23
|
import { ProviderDiagnosticTracker, diagnosticFromRuntimeStatus } from './diagnostics';
|
|
24
24
|
import { getModelContextWindowSize } from '../modelContext';
|
|
@@ -47,29 +47,7 @@ function hasRolloutFile(dir) {
|
|
|
47
47
|
function findRolloutFilesInConfiguredHomes(options = {}) {
|
|
48
48
|
return walkRolloutFiles(getSessionsDirs(), options);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
* Locates the rollout file for a Codex session id by walking
|
|
52
|
-
* `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
|
|
53
|
-
* `<home>/archived_sessions/`, where Codex moves the rollouts of archived
|
|
54
|
-
* threads.
|
|
55
|
-
*
|
|
56
|
-
* Filename-only match, case-insensitive on the id — no file contents are read
|
|
57
|
-
* and files are only stat'ed once their name matches, so the walk stays cheap
|
|
58
|
-
* even over a large history. When the same id exists in more than one home,
|
|
59
|
-
* the most recently modified copy wins. Returns `null` for an unknown, empty,
|
|
60
|
-
* or whitespace id. Consumers who want the database's answer instead can query
|
|
61
|
-
* the already-public `CodexDatabase` (requires the external `sqlite3` binary).
|
|
62
|
-
*/
|
|
63
|
-
export function findCodexRolloutFile(sessionId, options = {}) {
|
|
64
|
-
const normalizedId = sessionId.trim().toLowerCase();
|
|
65
|
-
if (!normalizedId)
|
|
66
|
-
return null;
|
|
67
|
-
const sessionsDirs = options.codexHome
|
|
68
|
-
? getCodexSessionsDirs([options.codexHome])
|
|
69
|
-
: getSessionsDirs();
|
|
70
|
-
// Name-only filter, one walk across the homes, newest modification first.
|
|
71
|
-
return walkRolloutFiles(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
|
|
72
|
-
}
|
|
50
|
+
export { findCodexRolloutFile };
|
|
73
51
|
/** Pick the best sessions directory for UI/search entrypoints. */
|
|
74
52
|
function getSessionsDir() {
|
|
75
53
|
const dirs = getCodexSessionsDirs(undefined, { includeArchived: false });
|
|
@@ -5,30 +5,14 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { execFile, execFileSync } from 'child_process';
|
|
8
|
+
import { codexSpawnParentFromSource as spawnParentFromSource } from '../parsers/codexRolloutOrigin';
|
|
8
9
|
/**
|
|
9
10
|
* Ids per inlined `IN (…)` query. Values are passed inside a single argv
|
|
10
11
|
* element to sqlite3, so an unbounded list would hit E2BIG on large stores.
|
|
11
12
|
*/
|
|
12
13
|
const SQL_ID_CHUNK_SIZE = 400;
|
|
13
|
-
/** Parent thread id
|
|
14
|
-
export
|
|
15
|
-
let parsed = source;
|
|
16
|
-
if (typeof source === 'string') {
|
|
17
|
-
if (!source.startsWith('{'))
|
|
18
|
-
return undefined;
|
|
19
|
-
try {
|
|
20
|
-
parsed = JSON.parse(source);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (!parsed || typeof parsed !== 'object')
|
|
27
|
-
return undefined;
|
|
28
|
-
const parent = parsed
|
|
29
|
-
.subagent?.thread_spawn?.parent_thread_id;
|
|
30
|
-
return typeof parent === 'string' ? parent : undefined;
|
|
31
|
-
}
|
|
14
|
+
/** Parent thread id of a spawned subagent's `source`; one rule shared with rollout parsing. */
|
|
15
|
+
export { spawnParentFromSource };
|
|
32
16
|
export class CodexDatabase {
|
|
33
17
|
dbPath;
|
|
34
18
|
sqlite3Available = null;
|
|
@@ -8,7 +8,7 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { getCodexSessionsDirs } from '../codexPaths';
|
|
10
10
|
import { DirectoryListingCache } from './directoryListingCache';
|
|
11
|
-
import { walkRolloutFilesAsync } from './rolloutWalker';
|
|
11
|
+
import { walkRolloutFiles, walkRolloutFilesAsync } from './rolloutWalker';
|
|
12
12
|
/** Every directory across the monitored homes that may hold rollouts, archived ones included. */
|
|
13
13
|
export function getSessionsDirs() {
|
|
14
14
|
return getCodexSessionsDirs();
|
|
@@ -213,3 +213,26 @@ export async function listCodexSessionFilesAsync(workspacePath, options = {}, ca
|
|
|
213
213
|
}
|
|
214
214
|
return results;
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Locates the rollout file for a Codex session id by walking
|
|
218
|
+
* `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`, then
|
|
219
|
+
* `<home>/archived_sessions/`, where Codex moves the rollouts of archived
|
|
220
|
+
* threads.
|
|
221
|
+
*
|
|
222
|
+
* Filename-only match, case-insensitive on the id — no file contents are read
|
|
223
|
+
* and files are only stat'ed once their name matches, so the walk stays cheap
|
|
224
|
+
* even over a large history. When the same id exists in more than one home,
|
|
225
|
+
* the most recently modified copy wins. Returns `null` for an unknown, empty,
|
|
226
|
+
* or whitespace id. Consumers who want the database's answer instead can query
|
|
227
|
+
* the already-public `CodexDatabase` (requires the external `sqlite3` binary).
|
|
228
|
+
*/
|
|
229
|
+
export function findCodexRolloutFile(sessionId, options = {}) {
|
|
230
|
+
const normalizedId = sessionId.trim().toLowerCase();
|
|
231
|
+
if (!normalizedId)
|
|
232
|
+
return null;
|
|
233
|
+
const sessionsDirs = options.codexHome
|
|
234
|
+
? getCodexSessionsDirs([options.codexHome])
|
|
235
|
+
: getSessionsDirs();
|
|
236
|
+
// Name-only filter, one walk across the homes, newest modification first.
|
|
237
|
+
return walkRolloutFiles(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
|
|
238
|
+
}
|
|
@@ -21,9 +21,8 @@ import * as path from 'path';
|
|
|
21
21
|
import { execSync } from 'child_process';
|
|
22
22
|
import { readSessionContextSnapshot } from '../context/sessionContext';
|
|
23
23
|
import { readSessionFileStats } from '../sessionStats';
|
|
24
|
-
import { convertOpenCodeMessage, parseDbMessageData, parseDbPartData, normalizeToolName, normalizeToolInput, } from '../parsers/openCodeParser';
|
|
24
|
+
import { convertOpenCodeMessage, parseDbMessageData, parseDbPartData, normalizeOpenCodeUsage, normalizeToolName, normalizeToolInput, } from '../parsers/openCodeParser';
|
|
25
25
|
import { OpenCodeDatabase } from './openCodeDatabase';
|
|
26
|
-
import { normalizeProviderUsage } from '../usageNormalization';
|
|
27
26
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
28
27
|
import { accumulateZaiUsage, inferZaiQuotaState, parseZaiQuotaError, ZAI_PROVIDER_IDS, } from '../zaiQuota';
|
|
29
28
|
import { ProviderDiagnosticTracker, diagnosticFromRuntimeStatus } from './diagnostics';
|
|
@@ -1456,21 +1455,14 @@ export class OpenCodeProvider {
|
|
|
1456
1455
|
if (tokens) {
|
|
1457
1456
|
// Same semantics as the main-session parser (openCodeParser).
|
|
1458
1457
|
const cache = tokens.cache;
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
inputTokens: tokens.input,
|
|
1468
|
-
outputTokens: tokens.output,
|
|
1469
|
-
cacheReadTokens: cache?.read,
|
|
1470
|
-
cacheWriteTokens: cache?.write,
|
|
1471
|
-
reasoningTokens: tokens.reasoning,
|
|
1472
|
-
reasoningIncludedInOutput: providerID === 'openai',
|
|
1473
|
-
}));
|
|
1458
|
+
addUsageToSubagent(tokenTotals, normalizeOpenCodeUsage({
|
|
1459
|
+
input: tokens.input,
|
|
1460
|
+
output: tokens.output,
|
|
1461
|
+
cacheRead: cache?.read,
|
|
1462
|
+
cacheWrite: cache?.write,
|
|
1463
|
+
reasoning: tokens.reasoning,
|
|
1464
|
+
total: tokens.total,
|
|
1465
|
+
}, msgData.providerID));
|
|
1474
1466
|
}
|
|
1475
1467
|
}
|
|
1476
1468
|
catch {
|
|
@@ -596,14 +596,51 @@ function renderContentBlock(block, includeThinking, includeToolDetail) {
|
|
|
596
596
|
return '';
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
|
-
|
|
599
|
+
/** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
|
|
600
|
+
const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
|
|
601
|
+
/**
|
|
602
|
+
* The tool's display identity and user-facing input. Entries built outside
|
|
603
|
+
* the canonical projection may still carry the parsers' `_sidekick*` keys in
|
|
604
|
+
* `toolInput`; those are hidden here and read as the raw identity.
|
|
605
|
+
*/
|
|
606
|
+
function toolDisplay(block) {
|
|
600
607
|
const name = block.toolName || 'unknown';
|
|
608
|
+
const raw = block.toolInput;
|
|
609
|
+
let input = raw;
|
|
610
|
+
let rawToolName = block.rawToolName;
|
|
611
|
+
let mcpServerName = block.mcpServerName;
|
|
612
|
+
if (raw && Object.keys(raw).some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))) {
|
|
613
|
+
const clean = {};
|
|
614
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
615
|
+
if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
|
|
616
|
+
clean[key] = value;
|
|
617
|
+
}
|
|
618
|
+
input = clean;
|
|
619
|
+
if (!rawToolName && typeof raw._sidekickRawToolName === 'string') {
|
|
620
|
+
rawToolName = raw._sidekickRawToolName;
|
|
621
|
+
}
|
|
622
|
+
if (!mcpServerName && typeof raw._sidekickMcpServerName === 'string') {
|
|
623
|
+
mcpServerName = raw._sidekickMcpServerName;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const parts = [];
|
|
627
|
+
if (rawToolName && rawToolName.trim() && rawToolName !== name)
|
|
628
|
+
parts.push(rawToolName);
|
|
629
|
+
if (mcpServerName && mcpServerName.trim())
|
|
630
|
+
parts.push(`MCP: ${mcpServerName}`);
|
|
631
|
+
const hint = parts.length
|
|
632
|
+
? ` <span class="tool-raw-name" style="color:var(--text-muted);font-size:11px">${escapeHtml(parts.join(' · '))}</span>`
|
|
633
|
+
: '';
|
|
634
|
+
return { name, hint, input };
|
|
635
|
+
}
|
|
636
|
+
function renderToolUse(block, includeDetail) {
|
|
637
|
+
const { name, hint, input } = toolDisplay(block);
|
|
601
638
|
if (!includeDetail) {
|
|
602
|
-
return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${escapeHtml(name)}</div></div>`;
|
|
639
|
+
return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${escapeHtml(name)}${hint}</div></div>`;
|
|
603
640
|
}
|
|
604
641
|
let inputHtml = '';
|
|
605
|
-
if (
|
|
606
|
-
const inputJson = JSON.stringify(
|
|
642
|
+
if (input && Object.keys(input).length > 0) {
|
|
643
|
+
const inputJson = JSON.stringify(input, null, 2);
|
|
607
644
|
const highlighted = highlightCodeBlock(inputJson, 'json');
|
|
608
645
|
inputHtml = `<div class="tool-io">
|
|
609
646
|
<div class="tool-label">Input</div>
|
|
@@ -611,7 +648,7 @@ function renderToolUse(block, includeDetail) {
|
|
|
611
648
|
</div>`;
|
|
612
649
|
}
|
|
613
650
|
return `<details class="tool-block" data-block-type="tool">
|
|
614
|
-
<summary>${escapeHtml(name)}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
|
|
651
|
+
<summary>${escapeHtml(name)}${hint}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
|
|
615
652
|
<div class="tool-content">
|
|
616
653
|
${inputHtml}
|
|
617
654
|
</div>
|
|
@@ -42,17 +42,19 @@ function toReportBlock(block) {
|
|
|
42
42
|
return block.text ? [{ type: 'text', text: block.text }] : [];
|
|
43
43
|
case 'thinking':
|
|
44
44
|
return block.text ? [{ type: 'thinking', text: block.text }] : [];
|
|
45
|
-
case 'tool_use':
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
|
|
45
|
+
case 'tool_use': {
|
|
46
|
+
const toolBlock = {
|
|
47
|
+
type: 'tool_use',
|
|
48
|
+
toolName: block.toolName ?? 'unknown',
|
|
49
|
+
toolInput: block.input && typeof block.input === 'object'
|
|
50
|
+
? block.input
|
|
51
|
+
: {},
|
|
52
|
+
toolUseId: block.toolUseId,
|
|
53
|
+
...(block.rawToolName ? { rawToolName: block.rawToolName } : {}),
|
|
54
|
+
...(block.mcpServerName ? { mcpServerName: block.mcpServerName } : {}),
|
|
55
|
+
};
|
|
56
|
+
return [toolBlock];
|
|
57
|
+
}
|
|
56
58
|
case 'tool_result':
|
|
57
59
|
return [
|
|
58
60
|
{
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { ACCOUNT_PROVIDER_IDS } from '../providerIds';
|
|
12
|
-
import { PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from '../providerFailure';
|
|
12
|
+
import { CODEX_ERROR_INFO_KINDS, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from '../providerFailure';
|
|
13
13
|
export const providerAuthenticationEvidenceSchema = z.object({
|
|
14
14
|
credentialKind: z.enum(PROVIDER_CREDENTIAL_KINDS),
|
|
15
15
|
state: z.enum(PROVIDER_AUTHENTICATION_STATES),
|
|
@@ -20,6 +20,7 @@ export const providerFailureEvidenceSchema = z.object({
|
|
|
20
20
|
source: z.enum(PROVIDER_FAILURE_EVIDENCE_SOURCES),
|
|
21
21
|
rule: z.enum(PROVIDER_FAILURE_CODES),
|
|
22
22
|
checkedAt: z.string().optional(),
|
|
23
|
+
codexErrorInfo: z.enum(CODEX_ERROR_INFO_KINDS).optional(),
|
|
23
24
|
});
|
|
24
25
|
export const providerRetryAfterSchema = z.discriminatedUnion('kind', [
|
|
25
26
|
z.object({ kind: z.literal('delay'), delayMs: z.number() }),
|
|
@@ -30,6 +31,7 @@ export const providerFailureDiagnosisSchema = z.object({
|
|
|
30
31
|
credentialKind: z.enum(PROVIDER_CREDENTIAL_KINDS),
|
|
31
32
|
diagnosis: z.enum(PROVIDER_FAILURE_CODES),
|
|
32
33
|
recovery: z.enum(PROVIDER_RECOVERY_ACTIONS),
|
|
34
|
+
retryable: z.boolean().optional(),
|
|
33
35
|
evidence: z.array(providerFailureEvidenceSchema),
|
|
34
36
|
authentication: providerAuthenticationEvidenceSchema.optional(),
|
|
35
37
|
httpStatus: z.number().optional(),
|