@dzhechkov/harness-core 0.8.46 → 0.8.47
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/.dz-manifest.json +175 -163
- package/README.md +84 -2
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +32 -2
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-snapshot.d.ts +14 -1
- package/dist/agentdb-snapshot.d.ts.map +1 -1
- package/dist/agentdb-snapshot.js +100 -4
- package/dist/agentdb-snapshot.js.map +1 -1
- package/dist/apply-leg.d.ts +9 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +26 -4
- package/dist/apply-leg.js.map +1 -1
- package/dist/architecture.d.ts +0 -5
- package/dist/architecture.d.ts.map +1 -1
- package/dist/architecture.js +25 -2
- package/dist/architecture.js.map +1 -1
- package/dist/codex-rollouts.d.ts +35 -7
- package/dist/codex-rollouts.d.ts.map +1 -1
- package/dist/codex-rollouts.js +201 -113
- package/dist/codex-rollouts.js.map +1 -1
- package/dist/cost-ledger.d.ts +42 -3
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +478 -15
- package/dist/cost-ledger.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +16 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +21 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +7 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +48 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-plan.d.ts +4 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +4 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/mutation-gate.d.ts +36 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +109 -19
- package/dist/mutation-gate.js.map +1 -1
- package/dist/npm-homepage.d.ts +64 -0
- package/dist/npm-homepage.d.ts.map +1 -0
- package/dist/npm-homepage.js +109 -0
- package/dist/npm-homepage.js.map +1 -0
- package/dist/operations.d.ts +6 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +150 -24
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts.map +1 -1
- package/dist/pack-inventory.js +6 -1
- package/dist/pack-inventory.js.map +1 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +4 -1
- package/dist/parity.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +43 -1
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +269 -13
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +5 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +18 -1
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +63 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +82 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release-package-audit.d.ts +86 -0
- package/dist/release-package-audit.d.ts.map +1 -0
- package/dist/release-package-audit.js +272 -0
- package/dist/release-package-audit.js.map +1 -0
- package/dist/release.d.ts +8 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +58 -32
- package/dist/release.js.map +1 -1
- package/dist/round.d.ts +10 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +5 -1
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +11 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +124 -20
- package/dist/run-records.js.map +1 -1
- package/dist/stage-usage.d.ts +148 -0
- package/dist/stage-usage.d.ts.map +1 -0
- package/dist/stage-usage.js +261 -0
- package/dist/stage-usage.js.map +1 -0
- package/dist/statusline.d.ts +23 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +167 -1
- package/dist/statusline.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +21 -19
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +206 -95
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/dist/workflow-run.d.ts +12 -3
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +29 -2
- package/dist/workflow-run.js.map +1 -1
- package/package.json +3 -3
- package/sbom.json +264 -234
- package/src/agentdb-index.ts +31 -2
- package/src/agentdb-snapshot.ts +105 -4
- package/src/apply-leg.ts +26 -4
- package/src/architecture.ts +15 -2
- package/src/codex-rollouts.ts +155 -147
- package/src/cost-ledger.ts +308 -19
- package/src/feature-adr-routing.ts +22 -0
- package/src/guard.ts +52 -0
- package/src/index.ts +15 -3
- package/src/loop-plan.ts +8 -0
- package/src/mutation-gate.ts +153 -19
- package/src/npm-homepage.ts +142 -0
- package/src/operations.ts +177 -22
- package/src/pack-inventory.ts +6 -1
- package/src/parity.ts +4 -1
- package/src/publish-sibling-drift.ts +214 -11
- package/src/publish.ts +20 -1
- package/src/qe-bridge.ts +86 -1
- package/src/release-package-audit.ts +264 -0
- package/src/release.ts +55 -22
- package/src/round.ts +8 -0
- package/src/run-records.ts +101 -22
- package/src/stage-usage.ts +191 -0
- package/src/statusline.ts +149 -1
- package/src/workflow-run-dispatch.ts +148 -89
- package/src/workflow-run.ts +40 -4
- package/dist/ledger-cost-fill.d.ts +0 -58
- package/dist/ledger-cost-fill.d.ts.map +0 -1
- package/dist/ledger-cost-fill.js +0 -78
- package/dist/ledger-cost-fill.js.map +0 -1
- package/dist/retro.d.ts +0 -131
- package/dist/retro.d.ts.map +0 -1
- package/dist/retro.js +0 -207
- package/dist/retro.js.map +0 -1
- package/dist/sbom.d.ts +0 -42
- package/dist/sbom.d.ts.map +0 -1
- package/dist/sbom.js +0 -120
- package/dist/sbom.js.map +0 -1
package/src/codex-rollouts.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* @packageDocumentation
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { fnv1a64 } from './feature-adr-checkpoints.js';
|
|
35
|
+
|
|
34
36
|
interface RawRecord {
|
|
35
37
|
readonly type?: unknown;
|
|
36
38
|
readonly timestamp?: unknown;
|
|
@@ -46,8 +48,8 @@ function nonEmptyString(v: unknown): string | null {
|
|
|
46
48
|
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function finiteNonNegative(v: unknown): number {
|
|
50
|
-
return typeof v === 'number' && Number.
|
|
51
|
+
function finiteNonNegative(v: unknown): number | null {
|
|
52
|
+
return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 ? v : null;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/** Epoch ms from a record's own `timestamp` (current schema) or `ts` (legacy/defensive), or `null`. */
|
|
@@ -71,11 +73,12 @@ function isoOrNull(ms: number | null): string | null {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export interface CodexRolloutTotals {
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
readonly
|
|
77
|
-
readonly
|
|
78
|
-
readonly
|
|
76
|
+
readonly cachedWrite?: number | null;
|
|
77
|
+
readonly input: number | null;
|
|
78
|
+
readonly cachedInput: number | null;
|
|
79
|
+
readonly output: number | null;
|
|
80
|
+
readonly reasoning: number | null;
|
|
81
|
+
readonly total: number | null;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/**
|
|
@@ -86,6 +89,7 @@ export interface CodexRolloutTotals {
|
|
|
86
89
|
* such record fell inside this turn's interval (nothing to attribute to it).
|
|
87
90
|
*/
|
|
88
91
|
export interface CodexRolloutTurn {
|
|
92
|
+
readonly turnId?: string | null;
|
|
89
93
|
readonly model: string | null;
|
|
90
94
|
readonly cwd: string | null;
|
|
91
95
|
readonly startedAt: string | null;
|
|
@@ -94,6 +98,13 @@ export interface CodexRolloutTurn {
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
export interface CodexRollout {
|
|
101
|
+
readonly sourcePath?: string;
|
|
102
|
+
readonly turnId?: string | null;
|
|
103
|
+
readonly diagnostics?: readonly string[];
|
|
104
|
+
readonly cumulativeWitness?: CodexRolloutTotals | null;
|
|
105
|
+
/** Fresh validation of an observed cumulative prefix; compact association avoids copying key sets. */
|
|
106
|
+
readonly scopeDiagnostics?: readonly { sourceRecord: number; receiptCount: number; timestamp: string | null; diagnostics: readonly string[]; witness?: CodexRolloutTotals }[];
|
|
107
|
+
readonly receipts?: readonly { key: string; responseId: string | null; turnId: string | null; turnIndex?: number | null; timestamp: string | null; totals: CodexRolloutTotals; payloadDigest: string; source: string; sourceRecord?: number; diagnostics?: readonly string[] }[];
|
|
97
108
|
readonly id: string;
|
|
98
109
|
readonly cwd: string | null;
|
|
99
110
|
readonly model: string | null;
|
|
@@ -122,6 +133,7 @@ export interface CodexRolloutParseError {
|
|
|
122
133
|
* (both schemas use these five field names) out of a usage-bearing sub-object. */
|
|
123
134
|
function totalsFrom(usage: Record<string, unknown>): CodexRolloutTotals {
|
|
124
135
|
return {
|
|
136
|
+
cachedWrite: finiteNonNegative(usage['cache_write_input_tokens']),
|
|
125
137
|
input: finiteNonNegative(usage['input_tokens']),
|
|
126
138
|
cachedInput: finiteNonNegative(usage['cached_input_tokens']),
|
|
127
139
|
output: finiteNonNegative(usage['output_tokens']),
|
|
@@ -139,142 +151,117 @@ function totalsFrom(usage: Record<string, unknown>): CodexRolloutTotals {
|
|
|
139
151
|
* content.
|
|
140
152
|
*/
|
|
141
153
|
export function parseCodexRollout(text: string, fileName?: string): CodexRollout | CodexRolloutParseError {
|
|
142
|
-
if (typeof text !== 'string' || text.trim()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
interface OpenTurn { model: string | null; cwd: string | null; startedMs: number | null; baseline: CodexRolloutTotals | null; totals: CodexRolloutTotals | null }
|
|
169
|
-
const closedTurns: CodexRolloutTurn[] = [];
|
|
170
|
-
let open: OpenTurn | null = null;
|
|
171
|
-
|
|
172
|
-
let unmatchableTurns = 0;
|
|
173
|
-
const closeOpenTurn = (endMs: number | null): void => {
|
|
174
|
-
if (open === null) return;
|
|
175
|
-
if (open.startedMs === null || endMs === null) unmatchableTurns += 1;
|
|
176
|
-
closedTurns.push({
|
|
177
|
-
model: open.model,
|
|
178
|
-
cwd: open.cwd,
|
|
179
|
-
startedAt: open.startedMs === null ? null : isoOrNull(open.startedMs),
|
|
180
|
-
endedAt: endMs === null ? null : isoOrNull(endMs),
|
|
181
|
-
totals: open.totals,
|
|
182
|
-
});
|
|
154
|
+
if (typeof text !== 'string' || !text.trim()) return { error: 'empty rollout text' };
|
|
155
|
+
const diagnostics: string[] = [];
|
|
156
|
+
let id: string | null = null; let cwd: string | null = null; let model: string | null = null; let sessionCwd: string | null = null;
|
|
157
|
+
let firstMs: number | null = null; let lastMs: number | null = null;
|
|
158
|
+
let cumulative: CodexRolloutTotals | null = null; let witnessMs: number | null = null; let sawTurnModel = false;
|
|
159
|
+
type Receipt = NonNullable<CodexRollout['receipts']>[number];
|
|
160
|
+
const modern = new Map<string, Receipt>(); const conflicted = new Set<string>();
|
|
161
|
+
const legacy: Receipt[] = []; const scopeDiagnostics: NonNullable<CodexRollout['scopeDiagnostics']>[number][] = [];
|
|
162
|
+
const turns: Array<{ model: string | null; cwd: string | null; startedAt: string | null; endedAt: string | null;
|
|
163
|
+
turnId: string | null; totals: CodexRolloutTotals | null; baseline: CodexRolloutTotals | null }> = [];
|
|
164
|
+
let current: typeof turns[number] | undefined;
|
|
165
|
+
const empty = (): CodexRolloutTotals => ({ input: null, cachedInput: null, cachedWrite: null, output: null, reasoning: null, total: null });
|
|
166
|
+
const fields = ['input', 'cachedInput', 'cachedWrite', 'output', 'reasoning', 'total'] as const;
|
|
167
|
+
const sum = (values: readonly CodexRolloutTotals[]): CodexRolloutTotals => Object.fromEntries(fields.map((field) => {
|
|
168
|
+
const counts = values.map((v) => v[field]);
|
|
169
|
+
const total = counts.reduce<number>((n, v) => n + (v ?? 0), 0);
|
|
170
|
+
if (!Number.isSafeInteger(total)) diagnostics.push('aggregate-overflow:' + field);
|
|
171
|
+
return [field, counts.length > 0 && counts.every((v) => v != null) && Number.isSafeInteger(total) ? total : null];
|
|
172
|
+
})) as unknown as CodexRolloutTotals;
|
|
173
|
+
const readUsage = (usage: Record<string, unknown>, owned: string[] = []): CodexRolloutTotals => {
|
|
174
|
+
const t = totalsFrom(usage);
|
|
175
|
+
for (const [key, v] of Object.entries(usage)) if (key.endsWith('_tokens') && v != null && finiteNonNegative(v) === null) owned.push('invalid-counter:' + key);
|
|
176
|
+
if (t.input != null && ((t.cachedInput != null && t.cachedInput > t.input) || (t.cachedInput != null && t.cachedWrite != null && t.cachedInput + t.cachedWrite > t.input))) owned.push('cache-exceeds-input');
|
|
177
|
+
if (t.output != null && t.reasoning != null && t.reasoning > t.output) owned.push('reasoning-exceeds-output');
|
|
178
|
+
if (t.input != null && t.output != null && t.total != null && t.input + t.output !== t.total) owned.push('total-split-mismatch');
|
|
179
|
+
diagnostics.push(...owned); return t;
|
|
183
180
|
};
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
181
|
+
const signature = (r: Receipt): string => JSON.stringify([r.turnId, r.totals.input, r.totals.cachedInput, r.totals.output, r.totals.reasoning, r.totals.total]);
|
|
182
|
+
for (const [sourceRecord, line] of text.split('\n').entries()) {
|
|
183
|
+
if (!line.trim()) continue;
|
|
184
|
+
let raw: unknown;
|
|
185
|
+
try { raw = JSON.parse(line); } catch { diagnostics.push('malformed-record'); continue; }
|
|
186
|
+
if (!isRecord(raw)) continue;
|
|
187
|
+
const ms = recordTimeMs(raw); if (ms !== null) { firstMs = firstMs === null ? ms : Math.min(firstMs, ms); lastMs = lastMs === null ? ms : Math.max(lastMs, ms); }
|
|
188
|
+
const payload = isRecord(raw['payload']) ? raw['payload'] : null; if (!payload) continue;
|
|
189
|
+
if (raw['type'] === 'session_meta') { id ??= nonEmptyString(payload['session_id']) ?? nonEmptyString(payload['id']); cwd ??= nonEmptyString(payload['cwd']); sessionCwd ??= nonEmptyString(payload['cwd']); model ??= nonEmptyString(payload['model']); }
|
|
190
|
+
if (raw['type'] === 'turn_context') {
|
|
191
|
+
if (current) current.endedAt = isoOrNull(ms);
|
|
192
|
+
current = { model: nonEmptyString(payload['model']), cwd: nonEmptyString(payload['cwd']) ?? sessionCwd,
|
|
193
|
+
turnId: nonEmptyString(payload['turn_id']), startedAt: isoOrNull(ms), endedAt: null, totals: null, baseline: cumulative };
|
|
194
|
+
turns.push(current); if (!sawTurnModel && current.model !== null) { model = current.model; sawTurnModel = true; } cwd ??= current.cwd;
|
|
197
195
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const type = rec['type'];
|
|
208
|
-
const payload = isRecord(rec['payload']) ? rec['payload'] : null;
|
|
209
|
-
if (payload === null) continue;
|
|
210
|
-
|
|
211
|
-
if (type === 'session_meta') {
|
|
212
|
-
if (id === null) id = nonEmptyString(payload['session_id']) ?? nonEmptyString(payload['id']);
|
|
213
|
-
if (cwd === null) cwd = nonEmptyString(payload['cwd']);
|
|
214
|
-
// Step 0's documented (legacy, not observed live on this machine) shape put `model` directly on
|
|
215
|
-
// `session_meta` — accepted here too, but `turnContextModel` always wins at the end (below)
|
|
216
|
-
// since that is what the measured current schema actually carries.
|
|
217
|
-
if (sessionMetaModel === null) sessionMetaModel = nonEmptyString(payload['model']);
|
|
218
|
-
} else if (type === 'turn_context') {
|
|
219
|
-
if (turnContextModel === null) turnContextModel = nonEmptyString(payload['model']);
|
|
220
|
-
if (cwd === null) cwd = nonEmptyString(payload['cwd']);
|
|
221
|
-
// Close the previous open turn AT this boundary (even when the boundary has no timestamp —
|
|
222
|
-
// the previous turn must stop absorbing usage), then open the new one.
|
|
223
|
-
closeOpenTurn(ms);
|
|
224
|
-
open = { model: nonEmptyString(payload['model']), cwd: nonEmptyString(payload['cwd']) ?? cwd, startedMs: ms, baseline: lastTotals, totals: null };
|
|
196
|
+
const modernUsage = raw['type'] === 'token_usage_record' && isRecord(payload['usage']) ? payload['usage'] : null;
|
|
197
|
+
const info = isRecord(payload['info']) ? payload['info'] : null;
|
|
198
|
+
const countEvent = raw['type'] === 'token_count' || (raw['type'] === 'event_msg' && payload['type'] === 'token_count');
|
|
199
|
+
const recordDiagnostics: string[] = []; let recordWitness: CodexRolloutTotals | undefined;
|
|
200
|
+
const witness = modernUsage ? payload['thread_token_usage'] : countEvent ? info?.['total_token_usage'] : null;
|
|
201
|
+
if (isRecord(witness) && (witnessMs === null || ms === null || ms >= witnessMs)) {
|
|
202
|
+
const next = readUsage(witness, recordDiagnostics); recordWitness = next;
|
|
203
|
+
if (cumulative?.total != null && next.total != null && next.total < cumulative.total) { diagnostics.push('cumulative-reset'); recordDiagnostics.push('cumulative-reset'); }
|
|
204
|
+
cumulative = next; witnessMs = ms;
|
|
225
205
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
206
|
+
if (modernUsage) {
|
|
207
|
+
const owned: string[] = []; const totals = readUsage(modernUsage, owned);
|
|
208
|
+
const responseId = nonEmptyString(payload['response_id']);
|
|
209
|
+
const turnId = nonEmptyString(payload['turn_id']) ?? current?.turnId ?? null;
|
|
210
|
+
const key = responseId ? 'response:' + responseId : 'record:' + fnv1a64(JSON.stringify([id, turnId, isoOrNull(ms), totals]));
|
|
211
|
+
if (!responseId) diagnostics.push('response-id-unavailable');
|
|
212
|
+
if (payload['session_id'] != null && id != null && payload['session_id'] !== id) { diagnostics.push('foreign-session'); owned.push('foreign-session'); }
|
|
213
|
+
const receipt: Receipt = { key, responseId, turnId, turnIndex: current ? turns.indexOf(current) : null, timestamp: isoOrNull(ms), totals, sourceRecord, diagnostics: owned,
|
|
214
|
+
payloadDigest: fnv1a64(JSON.stringify([payload['session_id'] ?? id, payload['thread_id'] ?? id, turnId, current?.model ?? model, current?.cwd ?? cwd, totals])), source: 'modern-response' };
|
|
215
|
+
if (current?.turnId != null && turnId !== current.turnId) { diagnostics.push('turn-association-conflict'); owned.push('turn-association-conflict'); }
|
|
216
|
+
const previous = modern.get(key);
|
|
217
|
+
if (previous && previous.payloadDigest !== receipt.payloadDigest) { conflicted.add(key); diagnostics.push('conflicting-response:' + key); }
|
|
218
|
+
else modern.set(key, previous ? { ...previous, diagnostics: [...new Set([...(previous.diagnostics ?? []), ...owned])] } : receipt);
|
|
219
|
+
} else if (countEvent && info && isRecord(info['last_token_usage'])) {
|
|
220
|
+
const owned: string[] = []; const totals = readUsage(info['last_token_usage'], owned); recordDiagnostics.push(...owned);
|
|
221
|
+
legacy.push({ key: 'legacy:' + fnv1a64(JSON.stringify([id, current?.turnId, isoOrNull(ms), witness, totals])), responseId: null,
|
|
222
|
+
turnId: current?.turnId ?? null, turnIndex: current ? turns.indexOf(current) : null, timestamp: isoOrNull(ms), totals, sourceRecord, diagnostics: owned, payloadDigest: fnv1a64(JSON.stringify([current?.turnId, totals])), source: 'legacy-last' });
|
|
223
|
+
} else if (countEvent && cumulative && current) {
|
|
224
|
+
const baseline = current.baseline;
|
|
225
|
+
current.totals = baseline === null ? cumulative : Object.fromEntries(fields.map((field) => {
|
|
226
|
+
const now = cumulative?.[field]; const base = baseline[field]; const delta = now != null && base != null ? now - base : null;
|
|
227
|
+
if (delta != null && delta < 0) diagnostics.push('cumulative-reset:' + field);
|
|
228
|
+
return [field, delta != null && delta >= 0 ? delta : null];
|
|
229
|
+
})) as unknown as CodexRolloutTotals;
|
|
237
230
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (open !== null) open.totals = deltaTotals(t, open.baseline);
|
|
245
|
-
lastTotals = t;
|
|
231
|
+
if (recordWitness !== undefined || recordDiagnostics.length > 0) {
|
|
232
|
+
let receiptCount = modern.size - conflicted.size;
|
|
233
|
+
if (legacy.length) {
|
|
234
|
+
const modernReceipts = [...modern.values()].filter((r) => !conflicted.has(r.key));
|
|
235
|
+
const legacyKeys = new Set(legacy.filter((r) => !modernReceipts.some((m) => signature(m) === signature(r) && m.timestamp != null && r.timestamp != null && Math.abs(Date.parse(m.timestamp) - Date.parse(r.timestamp)) <= 5000)).map((r) => r.key));
|
|
236
|
+
receiptCount += legacyKeys.size;
|
|
246
237
|
}
|
|
238
|
+
scopeDiagnostics.push({ sourceRecord, receiptCount, timestamp: isoOrNull(ms), diagnostics: recordDiagnostics,
|
|
239
|
+
...(recordWitness !== undefined ? { witness: recordWitness } : {}) });
|
|
247
240
|
}
|
|
248
241
|
}
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
if (!sawAnyRecord) return { error: 'no parseable JSON lines in rollout text' };
|
|
252
|
-
if (id === null) {
|
|
253
|
-
// Last resort: the uuid embedded in `rollout-<ts>-<uuid>.jsonl` — never invented, only read back.
|
|
254
|
-
// A plain "greedy dash" regex would stop at the uuid's OWN internal dashes (its 8-4-4-4-12 hex
|
|
255
|
-
// groups), so this matches the canonical uuid shape explicitly rather than "everything after the
|
|
256
|
-
// last dash".
|
|
257
|
-
const m = typeof fileName === 'string'
|
|
258
|
-
? /([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/.exec(fileName)
|
|
259
|
-
: null;
|
|
260
|
-
id = m !== null ? (m[1] ?? null) : null;
|
|
261
|
-
}
|
|
242
|
+
if (current) current.endedAt = isoOrNull(lastMs);
|
|
243
|
+
if (id === null && fileName) id = /([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/.exec(fileName)?.[1] ?? null;
|
|
262
244
|
if (id === null) return { error: 'no session_meta record and no id in fileName — cannot identify this rollout' };
|
|
263
|
-
|
|
264
|
-
|
|
245
|
+
const seenTurnIds = new Set<string>();
|
|
246
|
+
for (const turn of turns) if (turn.turnId !== null) { if (seenTurnIds.has(turn.turnId)) diagnostics.push('turn-association-conflict'); seenTurnIds.add(turn.turnId); }
|
|
247
|
+
const responses = [...modern.values()].filter((r) => !conflicted.has(r.key));
|
|
248
|
+
const seenLegacy = new Set<string>();
|
|
249
|
+
for (const receipt of legacy) {
|
|
250
|
+
const mirror = responses.some((r) => signature(r) === signature(receipt) && r.timestamp != null && receipt.timestamp != null
|
|
251
|
+
&& Math.abs(Date.parse(r.timestamp) - Date.parse(receipt.timestamp)) <= 5000);
|
|
252
|
+
if (!mirror && !seenLegacy.has(receipt.key)) { responses.push(receipt); seenLegacy.add(receipt.key); }
|
|
265
253
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
granularity:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
};
|
|
254
|
+
if (responses.length === 0 && cumulative === null && conflicted.size === 0) return { error: 'no token_count or token_usage_record entry — nothing to attribute' };
|
|
255
|
+
for (const turn of turns) {
|
|
256
|
+
const selected = responses.filter((r) => r.turnIndex === turns.indexOf(turn));
|
|
257
|
+
if (selected.length > 0) turn.totals = sum(selected.map((r) => r.totals));
|
|
258
|
+
}
|
|
259
|
+
const totals = conflicted.size > 0 ? empty() : responses.length > 0 ? sum(responses.map((r) => r.totals)) : cumulative!;
|
|
260
|
+
if (responses.length > 0 && cumulative?.total != null && totals.total !== cumulative.total) diagnostics.push('cumulative-witness-mismatch');
|
|
261
|
+
return { id, cwd, model, startedAt: isoOrNull(firstMs), endedAt: isoOrNull(lastMs), totals,
|
|
262
|
+
granularity: turns.length ? 'turn' : 'session', unmatchableTurns: turns.filter((t) => t.startedAt === null || t.endedAt === null).length,
|
|
263
|
+
turns: turns.map(({ baseline: _baseline, ...turn }) => turn), receipts: responses, diagnostics: [...new Set(diagnostics)],
|
|
264
|
+
cumulativeWitness: cumulative, scopeDiagnostics, ...(fileName ? { sourcePath: fileName } : {}) };
|
|
278
265
|
}
|
|
279
266
|
|
|
280
267
|
export type CodexRolloutMatch =
|
|
@@ -284,13 +271,15 @@ export type CodexRolloutMatch =
|
|
|
284
271
|
|
|
285
272
|
export interface CodexRolloutMatchWindow {
|
|
286
273
|
/** ISO instant — the window's lower bound. */
|
|
287
|
-
readonly from
|
|
274
|
+
readonly from?: string;
|
|
288
275
|
/** ISO instant — the window's upper bound. */
|
|
289
|
-
readonly to
|
|
276
|
+
readonly to?: string;
|
|
290
277
|
/** Exact match against {@link CodexRollout.cwd}, when given. */
|
|
291
278
|
readonly cwd?: string;
|
|
292
279
|
/** Exact match against {@link CodexRollout.model}, when given. */
|
|
293
280
|
readonly model?: string;
|
|
281
|
+
readonly rolloutId?: string;
|
|
282
|
+
readonly turnId?: string;
|
|
294
283
|
}
|
|
295
284
|
|
|
296
285
|
/**
|
|
@@ -313,12 +302,27 @@ export interface CodexRolloutMatchWindow {
|
|
|
313
302
|
* silently resolved to one of them).
|
|
314
303
|
*/
|
|
315
304
|
function candidateViewsOf(r: CodexRollout): readonly CodexRollout[] {
|
|
316
|
-
if (r.granularity === 'session') return [r];
|
|
317
|
-
const out: CodexRollout[] = [];
|
|
318
|
-
for (const turn of r.turns)
|
|
305
|
+
if (r.granularity === 'session') return r.turns.length === 0 ? [r] : [];
|
|
306
|
+
const out: CodexRollout[] = []; const turnIdCounts = new Map<string, number>();
|
|
307
|
+
for (const turn of r.turns) if (turn.turnId != null) turnIdCounts.set(turn.turnId, (turnIdCounts.get(turn.turnId) ?? 0) + 1);
|
|
308
|
+
for (const [turnIndex, turn] of r.turns.entries()) {
|
|
309
|
+
if (turn.turnId != null && turnIdCounts.get(turn.turnId) !== 1) continue;
|
|
319
310
|
if (turn.totals === null) continue; // nothing was ever attributed to this turn — not a candidate
|
|
311
|
+
// Original turn position owns membership, even when adjacent boundaries share a timestamp.
|
|
312
|
+
const receipts = r.receipts?.filter((receipt) => {
|
|
313
|
+
if (receipt.turnIndex != null) {
|
|
314
|
+
if (!Number.isSafeInteger(receipt.turnIndex) || receipt.turnIndex < 0 || receipt.turnIndex >= r.turns.length || receipt.turnIndex !== turnIndex) return false;
|
|
315
|
+
return receipt.turnId === null || turn.turnId == null || receipt.turnId === turn.turnId;
|
|
316
|
+
}
|
|
317
|
+
return receipt.turnId !== null && turn.turnId === receipt.turnId && turnIdCounts.get(receipt.turnId) === 1;
|
|
318
|
+
});
|
|
319
|
+
const contradicts = r.receipts?.some((receipt) => receipt.turnIndex === turnIndex && receipt.turnId !== null && turn.turnId != null && receipt.turnId !== turn.turnId);
|
|
320
|
+
if (contradicts || (r.receipts && r.receipts.length > 0 && receipts?.length === 0)) continue;
|
|
320
321
|
out.push({
|
|
322
|
+
...r,
|
|
321
323
|
id: r.id,
|
|
324
|
+
turnId: turn.turnId ?? null,
|
|
325
|
+
receipts: receipts ?? [],
|
|
322
326
|
unmatchableTurns: r.unmatchableTurns,
|
|
323
327
|
cwd: turn.cwd,
|
|
324
328
|
model: turn.model,
|
|
@@ -348,20 +352,24 @@ export function matchCodexRollouts(
|
|
|
348
352
|
rollouts: readonly CodexRollout[],
|
|
349
353
|
window: CodexRolloutMatchWindow,
|
|
350
354
|
): CodexRolloutMatch {
|
|
351
|
-
const fromMs = Date.parse(window.from);
|
|
352
|
-
const toMs = Date.parse(window.to);
|
|
353
|
-
|
|
355
|
+
const fromMs = Date.parse(window.from ?? '');
|
|
356
|
+
const toMs = Date.parse(window.to ?? '');
|
|
357
|
+
const hasWindow = window.from !== undefined || window.to !== undefined;
|
|
358
|
+
const exact = window.rolloutId !== undefined || window.turnId !== undefined;
|
|
359
|
+
if ((hasWindow && (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs > toMs)) || (!hasWindow && !exact)) return { status: 'none' };
|
|
354
360
|
|
|
355
361
|
const candidates: CodexRollout[] = [];
|
|
356
362
|
for (const r of rollouts) {
|
|
357
363
|
for (const view of candidateViewsOf(r)) {
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
if (window.rolloutId !== undefined && view.id !== window.rolloutId) continue;
|
|
365
|
+
if (window.turnId !== undefined && view.turnId !== window.turnId) continue;
|
|
366
|
+
if (hasWindow && (view.startedAt === null || view.endedAt === null)) continue;
|
|
367
|
+
const startMs = Date.parse(view.startedAt ?? '');
|
|
368
|
+
const endMs = Date.parse(view.endedAt ?? '');
|
|
369
|
+
if (hasWindow && (!Number.isFinite(startMs) || !Number.isFinite(endMs))) continue;
|
|
362
370
|
// Lead delta after Codex r2 (#5): the turn must START inside the window — a turn that merely
|
|
363
371
|
// brushes the window's edge (any-overlap) is exactly how a neighbouring dispatch's turn leaks in.
|
|
364
|
-
if (startMs < fromMs || startMs > toMs) continue;
|
|
372
|
+
if (hasWindow && (startMs < fromMs || startMs > toMs)) continue;
|
|
365
373
|
if (window.cwd !== undefined && view.cwd !== window.cwd) continue;
|
|
366
374
|
if (window.model !== undefined && view.model !== window.model) continue;
|
|
367
375
|
candidates.push(view);
|