@bli-cockpit/telemetry-core 0.1.46 → 0.1.48
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/dist/evidence-upload.d.ts +66 -0
- package/dist/evidence-upload.js +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/memory-hook-stats.d.ts +1 -1
- package/dist/session-facts-text.d.ts +16 -0
- package/dist/session-facts-text.js +62 -0
- package/dist/session-facts.d.ts +177 -0
- package/dist/session-facts.js +356 -0
- package/package.json +5 -3
|
@@ -528,6 +528,39 @@ export declare const CodexSessionAttributionSchema: z.ZodObject<{
|
|
|
528
528
|
signals: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
529
529
|
session_file_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
530
530
|
session_file_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
531
|
+
session_facts: z.ZodOptional<z.ZodObject<{
|
|
532
|
+
extractor_version: z.ZodString;
|
|
533
|
+
source: z.ZodEnum<{
|
|
534
|
+
codex: "codex";
|
|
535
|
+
claude_code: "claude_code";
|
|
536
|
+
}>;
|
|
537
|
+
usage_by_model: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
538
|
+
input_tokens: z.ZodNumber;
|
|
539
|
+
output_tokens: z.ZodNumber;
|
|
540
|
+
cache_read_input_tokens: z.ZodNumber;
|
|
541
|
+
cache_creation_input_tokens: z.ZodNumber;
|
|
542
|
+
}, z.core.$strict>>;
|
|
543
|
+
models: z.ZodArray<z.ZodString>;
|
|
544
|
+
input_tokens: z.ZodNumber;
|
|
545
|
+
output_tokens: z.ZodNumber;
|
|
546
|
+
cache_read_tokens: z.ZodNumber;
|
|
547
|
+
cache_creation_tokens: z.ZodNumber;
|
|
548
|
+
total_tokens: z.ZodNumber;
|
|
549
|
+
record_count: z.ZodNumber;
|
|
550
|
+
turn_count: z.ZodNumber;
|
|
551
|
+
user_prompt_count: z.ZodNumber;
|
|
552
|
+
assistant_response_count: z.ZodNumber;
|
|
553
|
+
tool_calls_by_name: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
554
|
+
tool_call_count: z.ZodNumber;
|
|
555
|
+
first_timestamp: z.ZodNullable<z.ZodString>;
|
|
556
|
+
last_timestamp: z.ZodNullable<z.ZodString>;
|
|
557
|
+
wall_duration_seconds: z.ZodNullable<z.ZodNumber>;
|
|
558
|
+
transcript_bytes: z.ZodNumber;
|
|
559
|
+
transcript_lines: z.ZodNumber;
|
|
560
|
+
transcript_files: z.ZodNumber;
|
|
561
|
+
truncated_models: z.ZodNumber;
|
|
562
|
+
truncated_tool_names: z.ZodNumber;
|
|
563
|
+
}, z.core.$strict>>;
|
|
531
564
|
session_file_mtime: z.ZodOptional<z.ZodString>;
|
|
532
565
|
repo_fingerprint: z.ZodOptional<z.ZodString>;
|
|
533
566
|
worktree_fingerprint: z.ZodOptional<z.ZodString>;
|
|
@@ -599,6 +632,39 @@ export declare const CodexSessionAttributionReportRequestSchema: z.ZodObject<{
|
|
|
599
632
|
signals: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
600
633
|
session_file_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
601
634
|
session_file_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
635
|
+
session_facts: z.ZodOptional<z.ZodObject<{
|
|
636
|
+
extractor_version: z.ZodString;
|
|
637
|
+
source: z.ZodEnum<{
|
|
638
|
+
codex: "codex";
|
|
639
|
+
claude_code: "claude_code";
|
|
640
|
+
}>;
|
|
641
|
+
usage_by_model: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
642
|
+
input_tokens: z.ZodNumber;
|
|
643
|
+
output_tokens: z.ZodNumber;
|
|
644
|
+
cache_read_input_tokens: z.ZodNumber;
|
|
645
|
+
cache_creation_input_tokens: z.ZodNumber;
|
|
646
|
+
}, z.core.$strict>>;
|
|
647
|
+
models: z.ZodArray<z.ZodString>;
|
|
648
|
+
input_tokens: z.ZodNumber;
|
|
649
|
+
output_tokens: z.ZodNumber;
|
|
650
|
+
cache_read_tokens: z.ZodNumber;
|
|
651
|
+
cache_creation_tokens: z.ZodNumber;
|
|
652
|
+
total_tokens: z.ZodNumber;
|
|
653
|
+
record_count: z.ZodNumber;
|
|
654
|
+
turn_count: z.ZodNumber;
|
|
655
|
+
user_prompt_count: z.ZodNumber;
|
|
656
|
+
assistant_response_count: z.ZodNumber;
|
|
657
|
+
tool_calls_by_name: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
658
|
+
tool_call_count: z.ZodNumber;
|
|
659
|
+
first_timestamp: z.ZodNullable<z.ZodString>;
|
|
660
|
+
last_timestamp: z.ZodNullable<z.ZodString>;
|
|
661
|
+
wall_duration_seconds: z.ZodNullable<z.ZodNumber>;
|
|
662
|
+
transcript_bytes: z.ZodNumber;
|
|
663
|
+
transcript_lines: z.ZodNumber;
|
|
664
|
+
transcript_files: z.ZodNumber;
|
|
665
|
+
truncated_models: z.ZodNumber;
|
|
666
|
+
truncated_tool_names: z.ZodNumber;
|
|
667
|
+
}, z.core.$strict>>;
|
|
602
668
|
session_file_mtime: z.ZodOptional<z.ZodString>;
|
|
603
669
|
repo_fingerprint: z.ZodOptional<z.ZodString>;
|
|
604
670
|
worktree_fingerprint: z.ZodOptional<z.ZodString>;
|
package/dist/evidence-upload.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { CaptureProvenanceSchema, IsoDateTimeSchema, NonEmptyStringSchema, Sha256Schema, } from "./common.js";
|
|
3
3
|
import { RawEvidencePointerSchema } from "./privacy.js";
|
|
4
4
|
import { RawEvidenceRedactionMetadataSchema } from "./secret-guards.js";
|
|
5
|
+
import { SessionFactsSchema } from "./session-facts.js";
|
|
5
6
|
/**
|
|
6
7
|
* Chunked raw-evidence upload wire contract.
|
|
7
8
|
*
|
|
@@ -285,6 +286,16 @@ export const CodexSessionAttributionSchema = z
|
|
|
285
286
|
signals: z.array(SafeLabelSchema).max(20).default([]),
|
|
286
287
|
session_file_hash_sha256: Sha256Schema.optional(),
|
|
287
288
|
session_file_byte_size: z.number().int().nonnegative().optional(),
|
|
289
|
+
/**
|
|
290
|
+
* Everything deterministic about the transcript, counted by the collector
|
|
291
|
+
* while the file was in its hands (BLI-4341). Token totals per model, turn
|
|
292
|
+
* and tool counts, first/last timestamps, size. Metadata only.
|
|
293
|
+
*
|
|
294
|
+
* Optional because a CLI published before this change sends none, and its
|
|
295
|
+
* uploads must keep validating; the server extractor is the safety net for
|
|
296
|
+
* exactly those.
|
|
297
|
+
*/
|
|
298
|
+
session_facts: SessionFactsSchema.optional(),
|
|
288
299
|
session_file_mtime: IsoDateTimeSchema.optional(),
|
|
289
300
|
repo_fingerprint: SafeLabelSchema.optional(),
|
|
290
301
|
worktree_fingerprint: SafeLabelSchema.optional(),
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./paths.js";
|
|
|
19
19
|
export * from "./privacy.js";
|
|
20
20
|
export * from "./risk-flags.js";
|
|
21
21
|
export * from "./secret-guards.js";
|
|
22
|
+
export * from "./session-facts.js";
|
|
22
23
|
export * from "./source-adapter.js";
|
|
23
24
|
export * from "./ticket-id.js";
|
|
24
25
|
export * from "./trace-sink.js";
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./paths.js";
|
|
|
19
19
|
export * from "./privacy.js";
|
|
20
20
|
export * from "./risk-flags.js";
|
|
21
21
|
export * from "./secret-guards.js";
|
|
22
|
+
export * from "./session-facts.js";
|
|
22
23
|
export * from "./source-adapter.js";
|
|
23
24
|
export * from "./ticket-id.js";
|
|
24
25
|
export * from "./trace-sink.js";
|
|
@@ -93,8 +93,8 @@ export declare const MEMORY_HOOK_BUCKET_PATTERN: RegExp;
|
|
|
93
93
|
export declare const MemoryHookStatsFileSchema: z.ZodObject<{
|
|
94
94
|
schema_version: z.ZodLiteral<"memory-hook-stats.v1">;
|
|
95
95
|
buckets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodEnum<{
|
|
96
|
-
"session-start": "session-start";
|
|
97
96
|
prompt: "prompt";
|
|
97
|
+
"session-start": "session-start";
|
|
98
98
|
stop: "stop";
|
|
99
99
|
}> & z.core.$partial, z.ZodObject<{
|
|
100
100
|
runs: z.ZodNumber;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text reads of one transcript record.
|
|
3
|
+
*
|
|
4
|
+
* Moved here from `@bli-cockpit/harvest-analysis` `extract/turn-text.ts`
|
|
5
|
+
* (BLI-4341) so the collector and the server ask the same question of a record
|
|
6
|
+
* ("did this turn carry any text?") with one implementation. The extractor now
|
|
7
|
+
* re-exports these; there is no second copy to drift.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here leaves the process: callers use the text to decide a COUNT.
|
|
10
|
+
*/
|
|
11
|
+
export declare function claudeUserTurnText(record: Record<string, unknown>): string | null;
|
|
12
|
+
export declare function codexUserTurnText(payload: Record<string, unknown>): string | null;
|
|
13
|
+
export declare function claudeAssistantText(record: Record<string, unknown>): string | null;
|
|
14
|
+
export declare function codexAssistantText(payload: Record<string, unknown>): string | null;
|
|
15
|
+
export declare function textFromPlainTextBlocks(value: unknown): string | null;
|
|
16
|
+
export declare function normalizeTurnText(value: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text reads of one transcript record.
|
|
3
|
+
*
|
|
4
|
+
* Moved here from `@bli-cockpit/harvest-analysis` `extract/turn-text.ts`
|
|
5
|
+
* (BLI-4341) so the collector and the server ask the same question of a record
|
|
6
|
+
* ("did this turn carry any text?") with one implementation. The extractor now
|
|
7
|
+
* re-exports these; there is no second copy to drift.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here leaves the process: callers use the text to decide a COUNT.
|
|
10
|
+
*/
|
|
11
|
+
const TEXT_BLOCK_TYPES = new Set(["text", "input_text", "output_text"]);
|
|
12
|
+
const NON_TEXT_BLOCK_TYPES = new Set([
|
|
13
|
+
"tool_result",
|
|
14
|
+
"tool_use",
|
|
15
|
+
"function_call",
|
|
16
|
+
"function_call_output",
|
|
17
|
+
]);
|
|
18
|
+
export function claudeUserTurnText(record) {
|
|
19
|
+
const message = record["message"];
|
|
20
|
+
return (textFromPlainTextBlocks(message) ??
|
|
21
|
+
textFromPlainTextBlocks(record["content"]) ??
|
|
22
|
+
textFromPlainTextBlocks(record["text"]) ??
|
|
23
|
+
textFromPlainTextBlocks(record["prompt"]));
|
|
24
|
+
}
|
|
25
|
+
export function codexUserTurnText(payload) {
|
|
26
|
+
return (textFromPlainTextBlocks(payload["content"]) ??
|
|
27
|
+
textFromPlainTextBlocks(payload["text"]) ??
|
|
28
|
+
textFromPlainTextBlocks(payload["message"]));
|
|
29
|
+
}
|
|
30
|
+
export function claudeAssistantText(record) {
|
|
31
|
+
const message = record["message"];
|
|
32
|
+
return textFromPlainTextBlocks(message);
|
|
33
|
+
}
|
|
34
|
+
export function codexAssistantText(payload) {
|
|
35
|
+
return (textFromPlainTextBlocks(payload["content"]) ??
|
|
36
|
+
textFromPlainTextBlocks(payload["text"]) ??
|
|
37
|
+
textFromPlainTextBlocks(payload["message"]));
|
|
38
|
+
}
|
|
39
|
+
export function textFromPlainTextBlocks(value) {
|
|
40
|
+
if (typeof value === "string")
|
|
41
|
+
return value;
|
|
42
|
+
if (!value || typeof value !== "object")
|
|
43
|
+
return null;
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
const text = value
|
|
46
|
+
.map((item) => textFromPlainTextBlocks(item))
|
|
47
|
+
.filter((item) => Boolean(item))
|
|
48
|
+
.join("\n");
|
|
49
|
+
return text || null;
|
|
50
|
+
}
|
|
51
|
+
const record = value;
|
|
52
|
+
const blockType = typeof record["type"] === "string" ? record["type"] : null;
|
|
53
|
+
if (blockType && NON_TEXT_BLOCK_TYPES.has(blockType))
|
|
54
|
+
return null;
|
|
55
|
+
if (blockType && !TEXT_BLOCK_TYPES.has(blockType))
|
|
56
|
+
return null;
|
|
57
|
+
const direct = textFromPlainTextBlocks(record["text"]) ?? textFromPlainTextBlocks(record["content"]);
|
|
58
|
+
return direct;
|
|
59
|
+
}
|
|
60
|
+
export function normalizeTurnText(value) {
|
|
61
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
62
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session facts: everything deterministic about one transcript, counted once,
|
|
3
|
+
* where the file already is.
|
|
4
|
+
*
|
|
5
|
+
* BLI-4341 part A. Until now the only place a session's token totals were
|
|
6
|
+
* computed was the server: `@bli-cockpit/harvest-analysis` downloaded the
|
|
7
|
+
* transcript back out of Storage and summed it (`det.v5`). The collector holds
|
|
8
|
+
* the same bytes at harvest time and had already read every line, so the
|
|
9
|
+
* download was pure re-work, and a session with no analysis run had no token
|
|
10
|
+
* count at all.
|
|
11
|
+
*
|
|
12
|
+
* This module is the ONE implementation of those rules. The collector feeds it
|
|
13
|
+
* at upload; the server extractor feeds it the same records during its own
|
|
14
|
+
* pass, so both sides count identically by construction rather than by review.
|
|
15
|
+
* `det.v6` is `det.v5`'s rules with one owner.
|
|
16
|
+
*
|
|
17
|
+
* Contract:
|
|
18
|
+
* - Pure. No filesystem, no network, no clock. Content in, numbers out.
|
|
19
|
+
* - METADATA ONLY on the way out: token counts, turn counts, tool NAMES and
|
|
20
|
+
* their counts, timestamps, byte/line sizes. Text is read in-process to
|
|
21
|
+
* decide whether a turn counted; it is never returned.
|
|
22
|
+
* - Two feeds, one rulebook: `observeSessionFactsRecord` for a caller that
|
|
23
|
+
* is already parsing the JSONL (the server extractor), and
|
|
24
|
+
* `computeSessionFacts` for a caller holding whole file contents (the
|
|
25
|
+
* collector).
|
|
26
|
+
*
|
|
27
|
+
* Counting rules, both verified against real transcripts:
|
|
28
|
+
* - Claude Code writes PER-TURN usage on each assistant record, repeated
|
|
29
|
+
* across the several records of one API request, so usage is summed with a
|
|
30
|
+
* dedupe on `message.id` (fallback `requestId`).
|
|
31
|
+
* - Codex writes a CUMULATIVE `total_token_usage` on every `token_count`
|
|
32
|
+
* event, so the last one wins, attributed to the `turn_context` model. Its
|
|
33
|
+
* `input_tokens` includes cached input and its `output_tokens` already
|
|
34
|
+
* includes reasoning, so input is normalized to fresh-only, matching the
|
|
35
|
+
* Anthropic-shaped usage the price table expects.
|
|
36
|
+
*/
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
/** Bump when a counting rule changes, never when a field is added. */
|
|
39
|
+
export declare const SESSION_FACTS_VERSION = "det.v6";
|
|
40
|
+
/** How many distinct models and tool names one session may report. */
|
|
41
|
+
export declare const SESSION_FACTS_MAX_MODELS = 50;
|
|
42
|
+
export declare const SESSION_FACTS_MAX_TOOL_NAMES = 200;
|
|
43
|
+
export type SessionFactsSource = "codex" | "claude_code";
|
|
44
|
+
export interface SessionFactsUsage {
|
|
45
|
+
input_tokens: number;
|
|
46
|
+
output_tokens: number;
|
|
47
|
+
cache_read_input_tokens: number;
|
|
48
|
+
cache_creation_input_tokens: number;
|
|
49
|
+
}
|
|
50
|
+
export interface SessionFacts {
|
|
51
|
+
/** The rules that produced these numbers. */
|
|
52
|
+
extractor_version: string;
|
|
53
|
+
source: SessionFactsSource;
|
|
54
|
+
usage_by_model: Record<string, SessionFactsUsage>;
|
|
55
|
+
models: string[];
|
|
56
|
+
input_tokens: number;
|
|
57
|
+
output_tokens: number;
|
|
58
|
+
cache_read_tokens: number;
|
|
59
|
+
cache_creation_tokens: number;
|
|
60
|
+
total_tokens: number;
|
|
61
|
+
/** JSONL records that parsed. Unreadable lines are counted in `transcript_lines` only. */
|
|
62
|
+
record_count: number;
|
|
63
|
+
/** User prompts plus assistant responses; the two are also reported apart. */
|
|
64
|
+
turn_count: number;
|
|
65
|
+
user_prompt_count: number;
|
|
66
|
+
assistant_response_count: number;
|
|
67
|
+
tool_calls_by_name: Record<string, number>;
|
|
68
|
+
tool_call_count: number;
|
|
69
|
+
first_timestamp: string | null;
|
|
70
|
+
last_timestamp: string | null;
|
|
71
|
+
wall_duration_seconds: number | null;
|
|
72
|
+
transcript_bytes: number;
|
|
73
|
+
transcript_lines: number;
|
|
74
|
+
/** Main plus any sidecars the caller fed in. */
|
|
75
|
+
transcript_files: number;
|
|
76
|
+
/** Distinct models dropped by the cap. A truncation that says nothing is a silent failure. */
|
|
77
|
+
truncated_models: number;
|
|
78
|
+
/** Distinct tool names dropped by the cap. */
|
|
79
|
+
truncated_tool_names: number;
|
|
80
|
+
}
|
|
81
|
+
export interface SessionFactsAccumulator {
|
|
82
|
+
source: SessionFactsSource;
|
|
83
|
+
usage_by_model: Record<string, SessionFactsUsage>;
|
|
84
|
+
seen_usage_ids: Set<string>;
|
|
85
|
+
record_count: number;
|
|
86
|
+
user_prompt_count: number;
|
|
87
|
+
assistant_response_count: number;
|
|
88
|
+
tool_calls_by_name: Record<string, number>;
|
|
89
|
+
first_timestamp_ms: number | null;
|
|
90
|
+
last_timestamp_ms: number | null;
|
|
91
|
+
transcript_bytes: number;
|
|
92
|
+
transcript_lines: number;
|
|
93
|
+
transcript_files: number;
|
|
94
|
+
/** Codex only: the newest cumulative total and the model to attribute it to. */
|
|
95
|
+
codex_total_usage: Record<string, unknown> | null;
|
|
96
|
+
codex_model_hint: string | null;
|
|
97
|
+
}
|
|
98
|
+
export declare function newSessionFactsAccumulator(source: SessionFactsSource): SessionFactsAccumulator;
|
|
99
|
+
/** One transcript file's size, for callers that never hold its whole text. */
|
|
100
|
+
export declare function observeSessionFactsFile(acc: SessionFactsAccumulator, file: {
|
|
101
|
+
bytes: number;
|
|
102
|
+
lines: number;
|
|
103
|
+
}): void;
|
|
104
|
+
/** One parsed JSONL record. The caller owns parsing; this owns the rules. */
|
|
105
|
+
export declare function observeSessionFactsRecord(acc: SessionFactsAccumulator, record: Record<string, unknown>): void;
|
|
106
|
+
/**
|
|
107
|
+
* OpenAI semantics differ from the Anthropic-shaped usage the price table
|
|
108
|
+
* assumes: `input_tokens` INCLUDES `cached_input_tokens` and `output_tokens`
|
|
109
|
+
* already INCLUDES `reasoning_output_tokens`. Normalizing input to fresh-only
|
|
110
|
+
* is what keeps the list-price equivalent from inflating several-fold.
|
|
111
|
+
*/
|
|
112
|
+
export declare function codexUsageFromCumulativeTotal(total: Record<string, unknown>): SessionFactsUsage;
|
|
113
|
+
export declare function finalizeSessionFacts(acc: SessionFactsAccumulator): SessionFacts;
|
|
114
|
+
/**
|
|
115
|
+
* One whole file into an existing accumulator, for a caller that has the text
|
|
116
|
+
* but is not already walking its records (a Claude subagent sidecar, say).
|
|
117
|
+
* Returns the line total so the caller can report what it read.
|
|
118
|
+
*/
|
|
119
|
+
export declare function observeSessionFactsContent(acc: SessionFactsAccumulator, content: string): {
|
|
120
|
+
lines: number;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* The whole-file door: give it every file of one session (the Claude main plus
|
|
124
|
+
* its subagent sidecars, or the one Codex rollout) and it returns the facts.
|
|
125
|
+
* Unparseable lines are counted in `transcript_lines` and skipped, exactly as
|
|
126
|
+
* the server extractor skips them.
|
|
127
|
+
*/
|
|
128
|
+
export declare function computeSessionFacts(options: {
|
|
129
|
+
source: SessionFactsSource;
|
|
130
|
+
contents: readonly string[];
|
|
131
|
+
}): SessionFacts;
|
|
132
|
+
export declare const SessionFactsUsageSchema: z.ZodObject<{
|
|
133
|
+
input_tokens: z.ZodNumber;
|
|
134
|
+
output_tokens: z.ZodNumber;
|
|
135
|
+
cache_read_input_tokens: z.ZodNumber;
|
|
136
|
+
cache_creation_input_tokens: z.ZodNumber;
|
|
137
|
+
}, z.core.$strict>;
|
|
138
|
+
/**
|
|
139
|
+
* The wire shape. Optional wherever it rides an envelope: a CLI published
|
|
140
|
+
* before this change sends no block at all, and its uploads must still
|
|
141
|
+
* validate.
|
|
142
|
+
*/
|
|
143
|
+
export declare const SessionFactsSchema: z.ZodObject<{
|
|
144
|
+
extractor_version: z.ZodString;
|
|
145
|
+
source: z.ZodEnum<{
|
|
146
|
+
codex: "codex";
|
|
147
|
+
claude_code: "claude_code";
|
|
148
|
+
}>;
|
|
149
|
+
usage_by_model: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
150
|
+
input_tokens: z.ZodNumber;
|
|
151
|
+
output_tokens: z.ZodNumber;
|
|
152
|
+
cache_read_input_tokens: z.ZodNumber;
|
|
153
|
+
cache_creation_input_tokens: z.ZodNumber;
|
|
154
|
+
}, z.core.$strict>>;
|
|
155
|
+
models: z.ZodArray<z.ZodString>;
|
|
156
|
+
input_tokens: z.ZodNumber;
|
|
157
|
+
output_tokens: z.ZodNumber;
|
|
158
|
+
cache_read_tokens: z.ZodNumber;
|
|
159
|
+
cache_creation_tokens: z.ZodNumber;
|
|
160
|
+
total_tokens: z.ZodNumber;
|
|
161
|
+
record_count: z.ZodNumber;
|
|
162
|
+
turn_count: z.ZodNumber;
|
|
163
|
+
user_prompt_count: z.ZodNumber;
|
|
164
|
+
assistant_response_count: z.ZodNumber;
|
|
165
|
+
tool_calls_by_name: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
166
|
+
tool_call_count: z.ZodNumber;
|
|
167
|
+
first_timestamp: z.ZodNullable<z.ZodString>;
|
|
168
|
+
last_timestamp: z.ZodNullable<z.ZodString>;
|
|
169
|
+
wall_duration_seconds: z.ZodNullable<z.ZodNumber>;
|
|
170
|
+
transcript_bytes: z.ZodNumber;
|
|
171
|
+
transcript_lines: z.ZodNumber;
|
|
172
|
+
transcript_files: z.ZodNumber;
|
|
173
|
+
truncated_models: z.ZodNumber;
|
|
174
|
+
truncated_tool_names: z.ZodNumber;
|
|
175
|
+
}, z.core.$strict>;
|
|
176
|
+
export type SessionFactsWire = z.infer<typeof SessionFactsSchema>;
|
|
177
|
+
export { claudeAssistantText, claudeUserTurnText, codexAssistantText, codexUserTurnText, normalizeTurnText, textFromPlainTextBlocks, } from "./session-facts-text.js";
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session facts: everything deterministic about one transcript, counted once,
|
|
3
|
+
* where the file already is.
|
|
4
|
+
*
|
|
5
|
+
* BLI-4341 part A. Until now the only place a session's token totals were
|
|
6
|
+
* computed was the server: `@bli-cockpit/harvest-analysis` downloaded the
|
|
7
|
+
* transcript back out of Storage and summed it (`det.v5`). The collector holds
|
|
8
|
+
* the same bytes at harvest time and had already read every line, so the
|
|
9
|
+
* download was pure re-work, and a session with no analysis run had no token
|
|
10
|
+
* count at all.
|
|
11
|
+
*
|
|
12
|
+
* This module is the ONE implementation of those rules. The collector feeds it
|
|
13
|
+
* at upload; the server extractor feeds it the same records during its own
|
|
14
|
+
* pass, so both sides count identically by construction rather than by review.
|
|
15
|
+
* `det.v6` is `det.v5`'s rules with one owner.
|
|
16
|
+
*
|
|
17
|
+
* Contract:
|
|
18
|
+
* - Pure. No filesystem, no network, no clock. Content in, numbers out.
|
|
19
|
+
* - METADATA ONLY on the way out: token counts, turn counts, tool NAMES and
|
|
20
|
+
* their counts, timestamps, byte/line sizes. Text is read in-process to
|
|
21
|
+
* decide whether a turn counted; it is never returned.
|
|
22
|
+
* - Two feeds, one rulebook: `observeSessionFactsRecord` for a caller that
|
|
23
|
+
* is already parsing the JSONL (the server extractor), and
|
|
24
|
+
* `computeSessionFacts` for a caller holding whole file contents (the
|
|
25
|
+
* collector).
|
|
26
|
+
*
|
|
27
|
+
* Counting rules, both verified against real transcripts:
|
|
28
|
+
* - Claude Code writes PER-TURN usage on each assistant record, repeated
|
|
29
|
+
* across the several records of one API request, so usage is summed with a
|
|
30
|
+
* dedupe on `message.id` (fallback `requestId`).
|
|
31
|
+
* - Codex writes a CUMULATIVE `total_token_usage` on every `token_count`
|
|
32
|
+
* event, so the last one wins, attributed to the `turn_context` model. Its
|
|
33
|
+
* `input_tokens` includes cached input and its `output_tokens` already
|
|
34
|
+
* includes reasoning, so input is normalized to fresh-only, matching the
|
|
35
|
+
* Anthropic-shaped usage the price table expects.
|
|
36
|
+
*/
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
import { claudeAssistantText, claudeUserTurnText, codexAssistantText, codexUserTurnText, } from "./session-facts-text.js";
|
|
39
|
+
/** Bump when a counting rule changes, never when a field is added. */
|
|
40
|
+
export const SESSION_FACTS_VERSION = "det.v6";
|
|
41
|
+
/** How many distinct models and tool names one session may report. */
|
|
42
|
+
export const SESSION_FACTS_MAX_MODELS = 50;
|
|
43
|
+
export const SESSION_FACTS_MAX_TOOL_NAMES = 200;
|
|
44
|
+
const MAX_MODEL_NAME_CHARS = 200;
|
|
45
|
+
const MAX_TOOL_NAME_CHARS = 120;
|
|
46
|
+
export function newSessionFactsAccumulator(source) {
|
|
47
|
+
return {
|
|
48
|
+
source,
|
|
49
|
+
usage_by_model: {},
|
|
50
|
+
seen_usage_ids: new Set(),
|
|
51
|
+
record_count: 0,
|
|
52
|
+
user_prompt_count: 0,
|
|
53
|
+
assistant_response_count: 0,
|
|
54
|
+
tool_calls_by_name: {},
|
|
55
|
+
first_timestamp_ms: null,
|
|
56
|
+
last_timestamp_ms: null,
|
|
57
|
+
transcript_bytes: 0,
|
|
58
|
+
transcript_lines: 0,
|
|
59
|
+
transcript_files: 0,
|
|
60
|
+
codex_total_usage: null,
|
|
61
|
+
codex_model_hint: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** One transcript file's size, for callers that never hold its whole text. */
|
|
65
|
+
export function observeSessionFactsFile(acc, file) {
|
|
66
|
+
acc.transcript_files += 1;
|
|
67
|
+
acc.transcript_bytes += Math.max(0, Math.trunc(file.bytes));
|
|
68
|
+
acc.transcript_lines += Math.max(0, Math.trunc(file.lines));
|
|
69
|
+
}
|
|
70
|
+
/** One parsed JSONL record. The caller owns parsing; this owns the rules. */
|
|
71
|
+
export function observeSessionFactsRecord(acc, record) {
|
|
72
|
+
acc.record_count += 1;
|
|
73
|
+
const ms = parseTimestampMs(record["timestamp"]);
|
|
74
|
+
if (ms != null) {
|
|
75
|
+
acc.first_timestamp_ms =
|
|
76
|
+
acc.first_timestamp_ms == null ? ms : Math.min(acc.first_timestamp_ms, ms);
|
|
77
|
+
acc.last_timestamp_ms =
|
|
78
|
+
acc.last_timestamp_ms == null ? ms : Math.max(acc.last_timestamp_ms, ms);
|
|
79
|
+
}
|
|
80
|
+
if (acc.source === "claude_code")
|
|
81
|
+
observeClaudeRecord(acc, record);
|
|
82
|
+
else
|
|
83
|
+
observeCodexRecord(acc, record);
|
|
84
|
+
}
|
|
85
|
+
function observeClaudeRecord(acc, record) {
|
|
86
|
+
const type = typeof record["type"] === "string" ? record["type"] : "";
|
|
87
|
+
if (type === "user") {
|
|
88
|
+
if (record["isMeta"] === true)
|
|
89
|
+
return;
|
|
90
|
+
const text = claudeUserTurnText(record);
|
|
91
|
+
if (text?.trim())
|
|
92
|
+
acc.user_prompt_count += 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (type !== "assistant")
|
|
96
|
+
return;
|
|
97
|
+
const message = record["message"];
|
|
98
|
+
if (!message || typeof message !== "object")
|
|
99
|
+
return;
|
|
100
|
+
const msg = message;
|
|
101
|
+
const model = typeof msg["model"] === "string" ? msg["model"] : null;
|
|
102
|
+
const usage = msg["usage"];
|
|
103
|
+
// One API request emits several records repeating the same usage; the id is
|
|
104
|
+
// what makes summing them safe.
|
|
105
|
+
const usageId = (typeof msg["id"] === "string" && msg["id"]) ||
|
|
106
|
+
(typeof record["requestId"] === "string" && record["requestId"]) ||
|
|
107
|
+
null;
|
|
108
|
+
if (model && usage && typeof usage === "object" && usageId && !acc.seen_usage_ids.has(usageId)) {
|
|
109
|
+
acc.seen_usage_ids.add(usageId);
|
|
110
|
+
addClaudeUsage(acc, model, usage);
|
|
111
|
+
}
|
|
112
|
+
if (claudeAssistantText(record)?.trim())
|
|
113
|
+
acc.assistant_response_count += 1;
|
|
114
|
+
const content = msg["content"];
|
|
115
|
+
if (!Array.isArray(content))
|
|
116
|
+
return;
|
|
117
|
+
for (const block of content) {
|
|
118
|
+
if (!block || typeof block !== "object")
|
|
119
|
+
continue;
|
|
120
|
+
const b = block;
|
|
121
|
+
if (b["type"] !== "tool_use")
|
|
122
|
+
continue;
|
|
123
|
+
countTool(acc, typeof b["name"] === "string" ? b["name"] : "unknown");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function observeCodexRecord(acc, record) {
|
|
127
|
+
const type = String(record["type"] ?? "");
|
|
128
|
+
const payload = record["payload"];
|
|
129
|
+
const p = payload && typeof payload === "object" ? payload : {};
|
|
130
|
+
if (type === "turn_context") {
|
|
131
|
+
if (typeof p["model"] === "string")
|
|
132
|
+
acc.codex_model_hint = p["model"];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (type === "response_item") {
|
|
136
|
+
const itemType = String(p["type"] ?? "");
|
|
137
|
+
if ((itemType === "message" || itemType === "user_message") && p["role"] === "user") {
|
|
138
|
+
if (codexUserTurnText(p)?.trim())
|
|
139
|
+
acc.user_prompt_count += 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (itemType === "message" && p["role"] === "assistant") {
|
|
143
|
+
if (codexAssistantText(p)?.trim())
|
|
144
|
+
acc.assistant_response_count += 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (itemType === "function_call") {
|
|
148
|
+
countTool(acc, typeof p["name"] === "string" ? p["name"] : "unknown");
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (type !== "event_msg")
|
|
153
|
+
return;
|
|
154
|
+
if (String(p["type"] ?? "") !== "token_count")
|
|
155
|
+
return;
|
|
156
|
+
const info = p["info"];
|
|
157
|
+
if (!info || typeof info !== "object")
|
|
158
|
+
return;
|
|
159
|
+
const total = info["total_token_usage"];
|
|
160
|
+
// Cumulative: the last event of the session is the session total.
|
|
161
|
+
if (total && typeof total === "object") {
|
|
162
|
+
acc.codex_total_usage = total;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function countTool(acc, name) {
|
|
166
|
+
const key = name.slice(0, MAX_TOOL_NAME_CHARS) || "unknown";
|
|
167
|
+
acc.tool_calls_by_name[key] = (acc.tool_calls_by_name[key] ?? 0) + 1;
|
|
168
|
+
}
|
|
169
|
+
function addClaudeUsage(acc, model, usage) {
|
|
170
|
+
const key = model.slice(0, MAX_MODEL_NAME_CHARS);
|
|
171
|
+
const current = acc.usage_by_model[key] ?? emptyUsage();
|
|
172
|
+
current.input_tokens += num(usage["input_tokens"]);
|
|
173
|
+
current.output_tokens += num(usage["output_tokens"]);
|
|
174
|
+
current.cache_read_input_tokens += num(usage["cache_read_input_tokens"]);
|
|
175
|
+
current.cache_creation_input_tokens += num(usage["cache_creation_input_tokens"]);
|
|
176
|
+
acc.usage_by_model[key] = current;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* OpenAI semantics differ from the Anthropic-shaped usage the price table
|
|
180
|
+
* assumes: `input_tokens` INCLUDES `cached_input_tokens` and `output_tokens`
|
|
181
|
+
* already INCLUDES `reasoning_output_tokens`. Normalizing input to fresh-only
|
|
182
|
+
* is what keeps the list-price equivalent from inflating several-fold.
|
|
183
|
+
*/
|
|
184
|
+
export function codexUsageFromCumulativeTotal(total) {
|
|
185
|
+
const input = num(total["input_tokens"]);
|
|
186
|
+
const cached = num(total["cached_input_tokens"]);
|
|
187
|
+
return {
|
|
188
|
+
input_tokens: Math.max(0, input - cached),
|
|
189
|
+
output_tokens: num(total["output_tokens"]),
|
|
190
|
+
cache_read_input_tokens: cached,
|
|
191
|
+
cache_creation_input_tokens: 0,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function finalizeSessionFacts(acc) {
|
|
195
|
+
// A Codex session whose usage never named a model is left uncounted rather
|
|
196
|
+
// than filed under a guess: det.v5 did the same, and a token total on the
|
|
197
|
+
// wrong model is money attributed to the wrong place.
|
|
198
|
+
if (acc.codex_total_usage && acc.codex_model_hint) {
|
|
199
|
+
acc.usage_by_model[acc.codex_model_hint.slice(0, MAX_MODEL_NAME_CHARS)] =
|
|
200
|
+
codexUsageFromCumulativeTotal(acc.codex_total_usage);
|
|
201
|
+
}
|
|
202
|
+
const usageEntries = Object.entries(acc.usage_by_model).sort((a, b) => usageTotal(b[1]) - usageTotal(a[1]) || a[0].localeCompare(b[0]));
|
|
203
|
+
const keptUsage = usageEntries.slice(0, SESSION_FACTS_MAX_MODELS);
|
|
204
|
+
const toolEntries = Object.entries(acc.tool_calls_by_name).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
205
|
+
const keptTools = toolEntries.slice(0, SESSION_FACTS_MAX_TOOL_NAMES);
|
|
206
|
+
const usage_by_model = Object.fromEntries(keptUsage);
|
|
207
|
+
const tool_calls_by_name = Object.fromEntries(keptTools);
|
|
208
|
+
const totals = keptUsage.reduce((sum, [, usage]) => ({
|
|
209
|
+
input_tokens: sum.input_tokens + usage.input_tokens,
|
|
210
|
+
output_tokens: sum.output_tokens + usage.output_tokens,
|
|
211
|
+
cache_read_input_tokens: sum.cache_read_input_tokens + usage.cache_read_input_tokens,
|
|
212
|
+
cache_creation_input_tokens: sum.cache_creation_input_tokens + usage.cache_creation_input_tokens,
|
|
213
|
+
}), emptyUsage());
|
|
214
|
+
const first = acc.first_timestamp_ms;
|
|
215
|
+
const last = acc.last_timestamp_ms;
|
|
216
|
+
return {
|
|
217
|
+
extractor_version: SESSION_FACTS_VERSION,
|
|
218
|
+
source: acc.source,
|
|
219
|
+
usage_by_model,
|
|
220
|
+
models: keptUsage.map(([model]) => model).sort((a, b) => a.localeCompare(b)),
|
|
221
|
+
input_tokens: totals.input_tokens,
|
|
222
|
+
output_tokens: totals.output_tokens,
|
|
223
|
+
cache_read_tokens: totals.cache_read_input_tokens,
|
|
224
|
+
cache_creation_tokens: totals.cache_creation_input_tokens,
|
|
225
|
+
total_tokens: usageTotal(totals),
|
|
226
|
+
record_count: acc.record_count,
|
|
227
|
+
turn_count: acc.user_prompt_count + acc.assistant_response_count,
|
|
228
|
+
user_prompt_count: acc.user_prompt_count,
|
|
229
|
+
assistant_response_count: acc.assistant_response_count,
|
|
230
|
+
tool_calls_by_name,
|
|
231
|
+
tool_call_count: keptTools.reduce((sum, [, count]) => sum + count, 0),
|
|
232
|
+
first_timestamp: first == null ? null : new Date(first).toISOString(),
|
|
233
|
+
last_timestamp: last == null ? null : new Date(last).toISOString(),
|
|
234
|
+
wall_duration_seconds: first == null || last == null ? null : Math.max(0, Math.round((last - first) / 1000)),
|
|
235
|
+
transcript_bytes: acc.transcript_bytes,
|
|
236
|
+
transcript_lines: acc.transcript_lines,
|
|
237
|
+
transcript_files: acc.transcript_files,
|
|
238
|
+
truncated_models: usageEntries.length - keptUsage.length,
|
|
239
|
+
truncated_tool_names: toolEntries.length - keptTools.length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* One whole file into an existing accumulator, for a caller that has the text
|
|
244
|
+
* but is not already walking its records (a Claude subagent sidecar, say).
|
|
245
|
+
* Returns the line total so the caller can report what it read.
|
|
246
|
+
*/
|
|
247
|
+
export function observeSessionFactsContent(acc, content) {
|
|
248
|
+
let lines = 0;
|
|
249
|
+
for (const line of content.split("\n")) {
|
|
250
|
+
if (!line.trim())
|
|
251
|
+
continue;
|
|
252
|
+
lines += 1;
|
|
253
|
+
let record;
|
|
254
|
+
try {
|
|
255
|
+
record = JSON.parse(line);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Silent by design: the parser's message quotes the offending line,
|
|
259
|
+
// which is transcript content. The line still counts toward the file's
|
|
260
|
+
// line total, so the loss is visible as a number.
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
264
|
+
continue;
|
|
265
|
+
observeSessionFactsRecord(acc, record);
|
|
266
|
+
}
|
|
267
|
+
observeSessionFactsFile(acc, { bytes: byteLength(content), lines });
|
|
268
|
+
return { lines };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The whole-file door: give it every file of one session (the Claude main plus
|
|
272
|
+
* its subagent sidecars, or the one Codex rollout) and it returns the facts.
|
|
273
|
+
* Unparseable lines are counted in `transcript_lines` and skipped, exactly as
|
|
274
|
+
* the server extractor skips them.
|
|
275
|
+
*/
|
|
276
|
+
export function computeSessionFacts(options) {
|
|
277
|
+
const acc = newSessionFactsAccumulator(options.source);
|
|
278
|
+
for (const content of options.contents) {
|
|
279
|
+
observeSessionFactsContent(acc, content);
|
|
280
|
+
}
|
|
281
|
+
return finalizeSessionFacts(acc);
|
|
282
|
+
}
|
|
283
|
+
function usageTotal(usage) {
|
|
284
|
+
return (usage.input_tokens +
|
|
285
|
+
usage.output_tokens +
|
|
286
|
+
usage.cache_read_input_tokens +
|
|
287
|
+
usage.cache_creation_input_tokens);
|
|
288
|
+
}
|
|
289
|
+
function emptyUsage() {
|
|
290
|
+
return {
|
|
291
|
+
input_tokens: 0,
|
|
292
|
+
output_tokens: 0,
|
|
293
|
+
cache_read_input_tokens: 0,
|
|
294
|
+
cache_creation_input_tokens: 0,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function byteLength(value) {
|
|
298
|
+
return typeof Buffer === "undefined"
|
|
299
|
+
? new TextEncoder().encode(value).length
|
|
300
|
+
: Buffer.byteLength(value, "utf8");
|
|
301
|
+
}
|
|
302
|
+
function parseTimestampMs(value) {
|
|
303
|
+
if (typeof value !== "string")
|
|
304
|
+
return null;
|
|
305
|
+
const ms = Date.parse(value);
|
|
306
|
+
return Number.isFinite(ms) ? ms : null;
|
|
307
|
+
}
|
|
308
|
+
function num(value) {
|
|
309
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
310
|
+
}
|
|
311
|
+
const TokenCountSchema = z.number().int().nonnegative().max(1_000_000_000_000);
|
|
312
|
+
export const SessionFactsUsageSchema = z
|
|
313
|
+
.object({
|
|
314
|
+
input_tokens: TokenCountSchema,
|
|
315
|
+
output_tokens: TokenCountSchema,
|
|
316
|
+
cache_read_input_tokens: TokenCountSchema,
|
|
317
|
+
cache_creation_input_tokens: TokenCountSchema,
|
|
318
|
+
})
|
|
319
|
+
.strict();
|
|
320
|
+
/**
|
|
321
|
+
* The wire shape. Optional wherever it rides an envelope: a CLI published
|
|
322
|
+
* before this change sends no block at all, and its uploads must still
|
|
323
|
+
* validate.
|
|
324
|
+
*/
|
|
325
|
+
export const SessionFactsSchema = z
|
|
326
|
+
.object({
|
|
327
|
+
extractor_version: z.string().trim().min(1).max(40),
|
|
328
|
+
source: z.enum(["codex", "claude_code"]),
|
|
329
|
+
usage_by_model: z
|
|
330
|
+
.record(z.string().min(1).max(MAX_MODEL_NAME_CHARS), SessionFactsUsageSchema)
|
|
331
|
+
.refine((value) => Object.keys(value).length <= SESSION_FACTS_MAX_MODELS, { message: "too many models" }),
|
|
332
|
+
models: z.array(z.string().min(1).max(MAX_MODEL_NAME_CHARS)).max(SESSION_FACTS_MAX_MODELS),
|
|
333
|
+
input_tokens: TokenCountSchema,
|
|
334
|
+
output_tokens: TokenCountSchema,
|
|
335
|
+
cache_read_tokens: TokenCountSchema,
|
|
336
|
+
cache_creation_tokens: TokenCountSchema,
|
|
337
|
+
total_tokens: TokenCountSchema,
|
|
338
|
+
record_count: z.number().int().nonnegative(),
|
|
339
|
+
turn_count: z.number().int().nonnegative(),
|
|
340
|
+
user_prompt_count: z.number().int().nonnegative(),
|
|
341
|
+
assistant_response_count: z.number().int().nonnegative(),
|
|
342
|
+
tool_calls_by_name: z
|
|
343
|
+
.record(z.string().min(1).max(MAX_TOOL_NAME_CHARS), z.number().int().nonnegative())
|
|
344
|
+
.refine((value) => Object.keys(value).length <= SESSION_FACTS_MAX_TOOL_NAMES, { message: "too many tool names" }),
|
|
345
|
+
tool_call_count: z.number().int().nonnegative(),
|
|
346
|
+
first_timestamp: z.string().datetime({ offset: true }).nullable(),
|
|
347
|
+
last_timestamp: z.string().datetime({ offset: true }).nullable(),
|
|
348
|
+
wall_duration_seconds: z.number().int().nonnegative().nullable(),
|
|
349
|
+
transcript_bytes: z.number().int().nonnegative(),
|
|
350
|
+
transcript_lines: z.number().int().nonnegative(),
|
|
351
|
+
transcript_files: z.number().int().nonnegative(),
|
|
352
|
+
truncated_models: z.number().int().nonnegative(),
|
|
353
|
+
truncated_tool_names: z.number().int().nonnegative(),
|
|
354
|
+
})
|
|
355
|
+
.strict();
|
|
356
|
+
export { claudeAssistantText, claudeUserTurnText, codexAssistantText, codexUserTurnText, normalizeTurnText, textFromPlainTextBlocks, } from "./session-facts-text.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/telemetry-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
},
|
|
18
18
|
"./memory-experience": {
|
|
19
19
|
"types": "./dist/memory-experience.d.ts",
|
|
20
|
-
"import": "./dist/memory-experience.js"
|
|
20
|
+
"import": "./dist/memory-experience.js",
|
|
21
|
+
"default": "./dist/memory-experience.js"
|
|
21
22
|
},
|
|
22
23
|
"./evidence-storage-encoding": {
|
|
23
24
|
"types": "./dist/evidence-storage-encoding.d.ts",
|
|
24
|
-
"import": "./dist/evidence-storage-encoding.js"
|
|
25
|
+
"import": "./dist/evidence-storage-encoding.js",
|
|
26
|
+
"default": "./dist/evidence-storage-encoding.js"
|
|
25
27
|
}
|
|
26
28
|
},
|
|
27
29
|
"publishConfig": {
|