@titan-design/session-analytics 0.5.0 → 0.8.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/README.md CHANGED
@@ -6,7 +6,9 @@ reads a session graph through a read-only connection and never writes it; `write
6
6
  the one function here that writes.
7
7
 
8
8
  Tier 2 of the titan-platform DAG. It depends on `@titan-design/session-graph` for table
9
- names and on `@titan-design/store-sqlite` for the `Db` type. `zod` is a peer dependency.
9
+ names, on `@titan-design/store-sqlite` for the `Db` type, and on `@titan-design/session-read`
10
+ and `@titan-design/agent-protocol` for the observations the session timeline folds. `zod` is a
11
+ peer dependency.
10
12
 
11
13
  ```ts
12
14
  import { classifySession, contextBand, priceRequest } from "@titan-design/session-analytics";
@@ -63,6 +65,9 @@ priceRequest(
63
65
  segmentation, written through session-graph's `replaceEpisodes`.
64
66
  - `initiativeFromCwd(cwd)`, `sessionInitiative(tasks, cwd)` — a session's initiative from its
65
67
  task edges, falling back to the `cf_analyze.py` cwd rule.
68
+ - `buildSessionTimeline(observations, options?)`, `SessionTimelineAccumulator`,
69
+ `countAtOrBefore(sortedMs, targetMs)` and the `SessionTimeline` types — the read model behind
70
+ a session view; see "Session timeline" below.
66
71
 
67
72
  ```ts
68
73
  import { openDatabase } from "@titan-design/store-sqlite";
@@ -72,6 +77,57 @@ const report = costReport(openDatabase(graphPath, { readonly: true }), { days: 7
72
77
  process.stdout.write(renderCostReportText(report));
73
78
  ```
74
79
 
80
+ ## Session timeline
81
+
82
+ `buildSessionTimeline(observations, { gapMinMs, maxTextChars, prices })` is a pure fold over
83
+ session-read's normalized observations, so it reads Claude Code and Codex sessions alike and
84
+ needs no graph. It returns one `SessionTimeline`, plain JSON that a daemon can send as it is:
85
+
86
+ ```ts
87
+ import { claudeSourceFromPath, readSessionObservations } from "@titan-design/session-read";
88
+ import { SessionTimelineAccumulator, countAtOrBefore } from "@titan-design/session-analytics";
89
+
90
+ const accumulator = new SessionTimelineAccumulator();
91
+ for await (const observation of readSessionObservations(claudeSourceFromPath(file, namespace))) {
92
+ accumulator.add(observation);
93
+ }
94
+ const timeline = accumulator.result();
95
+ countAtOrBefore(timeline.tools.atMs, scrubbedMs); // tool calls made by that time
96
+ ```
97
+
98
+ | Field | What it holds |
99
+ |---|---|
100
+ | `turns` | `TimelineTurn[]`. A user message opens a turn. Each has `user`, `assistant` messages, `toolCalls`, `errorCount`, `tokens`, `costUsd` and `gapBeforeMs`. Sort a turn's messages and tool calls by `seq` to interleave them. |
101
+ | `buckets` | `TimelineMinuteBucket[]`, one per clock minute that held activity, with event, message, tool call and error counts, output tokens and cost. |
102
+ | `gaps` | `TimelineGap[]`: each idle stretch of `TIMELINE_GAP_MIN_MS` (10 minutes) or more. The bucket and the turn after a gap carry `gapBeforeMs`. The wait on a tool call that later returned is not idle and is never a gap. |
103
+ | `tokens` | `TokenTimeline`: one `TimelineTokenPoint` per API request (prompt size, output, cost, running totals, `afterCompaction`), the `CompactionMark`s and the models used. |
104
+ | `tools`, `files`, `errors`, `agents` | Calls by name and by session-read tool family, first touch of each file by access, failed calls, and subagent dispatch spans. Each carries ascending `atMs` arrays for `countAtOrBefore`. |
105
+ | `totals` | Counts, the four disjoint token classes and the cost. |
106
+
107
+ Every `*Ms` field is epoch milliseconds. The model holds no time zone, so a session that
108
+ crosses midnight is one unbroken run of buckets and the renderer picks the zone. Text is capped
109
+ at `TIMELINE_TEXT_CAP` characters with `truncated` set, and each message and tool call keeps the
110
+ `byteOffset` of its transcript line for reading the full record. `SESSION_TIMELINE_VERSION`
111
+ changes when a field is removed or changes meaning.
112
+
113
+ A turn's `origin` is `prompt`, `injected` (a harness block such as a channel message or a task
114
+ notification, named in `injectedMarker`), `compaction` (a continuation summary) or `none`
115
+ (activity before any user message). It is read from the head of the opening text with
116
+ session-read's injected markers, so a typed prompt that the harness prefixed with a reminder
117
+ block reads as `injected`.
118
+
119
+ Cost comes from `priceRequest` over `PRICE_TABLE`. The normalized usage has no 5m and 1h split
120
+ of cache writes, so every cache write is priced at the 5m rate and the figure under-reads a
121
+ session that wrote 1h caches. A source that reports only running totals (`basis: "snapshot"`)
122
+ gets token totals and no points, and nothing is priced: `totals.costUsd` and every turn's
123
+ `costUsd` are 0, and `totals.requests` is null.
124
+
125
+ Claude Code writes one compaction as a boundary line and then a summary line. The timeline
126
+ reports it as one `CompactionMark`, at the boundary's time, with the summary.
127
+
128
+ `result()` returns a fresh copy each time, so a result is safe to keep while more observations
129
+ are added. `ToolFamily`, the type of a tool call's `family`, is re-exported from session-read.
130
+
75
131
  ## Wake episodes
76
132
 
77
133
  An episode is one arrival that wakes a session and the requests after it, up to the next