@f5-sales-demo/xcsh-stats 19.105.7 → 20.0.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh-stats",
4
- "version": "19.105.7",
4
+ "version": "20.0.1",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -33,12 +33,13 @@
33
33
  "check": "biome check . && bun run check:types",
34
34
  "check:types": "tsgo -p tsconfig.json --noEmit && tsgo -p tsconfig.client.json --noEmit",
35
35
  "lint": "biome lint .",
36
+ "test": "bun test",
36
37
  "fix": "biome check --write --unsafe .",
37
38
  "fmt": "biome format --write ."
38
39
  },
39
40
  "dependencies": {
40
- "@f5-sales-demo/pi-ai": "19.105.7",
41
- "@f5-sales-demo/pi-utils": "19.105.7",
41
+ "@f5-sales-demo/pi-ai": "20.0.1",
42
+ "@f5-sales-demo/pi-utils": "20.0.1",
42
43
  "@tailwindcss/node": "^4.2",
43
44
  "chart.js": "^4.5",
44
45
  "date-fns": "~4.4.0",
package/src/aggregator.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  insertMessageStats,
16
16
  setFileOffset,
17
17
  } from "./db";
18
- import { getSessionEntry, listAllSessionFiles, parseSessionFile } from "./parser";
18
+ import { getSessionMessageChain, listAllSessionFiles, parseSessionFile } from "./parser";
19
19
  import type { DashboardStats, MessageStats, RequestDetails } from "./types";
20
20
 
21
21
  /**
@@ -106,17 +106,14 @@ export async function getRequestDetails(id: number): Promise<RequestDetails | nu
106
106
  const msg = getMessageById(id);
107
107
  if (!msg) return null;
108
108
 
109
- const entry = await getSessionEntry(msg.sessionFile, msg.entryId);
110
- if (entry?.type !== "message") return null;
111
-
112
- // TODO: Get parent/context messages?
113
- // For now we return the single entry which contains the assistant response.
114
- // The user prompt is likely the parent.
109
+ const entries = await getSessionMessageChain(msg.sessionFile, msg.entryId);
110
+ const entry = entries.at(-1);
111
+ if (entry?.message.role !== "assistant") return null;
115
112
 
116
113
  return {
117
114
  ...msg,
118
- messages: [entry],
119
- output: (entry as any).message,
115
+ messages: entries.map(item => item.message),
116
+ output: entry.message,
120
117
  };
121
118
  }
122
119
 
package/src/parser.ts CHANGED
@@ -167,3 +167,41 @@ export async function getSessionEntry(sessionPath: string, entryId: string): Pro
167
167
  }
168
168
  return null;
169
169
  }
170
+
171
+ /**
172
+ * Reconstruct the message ancestry ending at a session entry.
173
+ *
174
+ * Non-message tree entries are traversed but omitted from the returned conversation.
175
+ * Missing parents and malformed cycles terminate the walk without hiding the messages
176
+ * that were recovered successfully.
177
+ */
178
+ export async function getSessionMessageChain(sessionPath: string, entryId: string): Promise<SessionMessageEntry[]> {
179
+ let bytes: Uint8Array;
180
+ try {
181
+ bytes = await Bun.file(sessionPath).bytes();
182
+ } catch (err) {
183
+ if (isEnoent(err)) return [];
184
+ throw err;
185
+ }
186
+
187
+ const { entries } = parseSessionEntriesLenient(bytes);
188
+ const byId = new Map<string, SessionMessageEntry | (SessionEntry & { id: string; parentId: string | null })>();
189
+ for (const entry of entries) {
190
+ if ("id" in entry && "parentId" in entry && typeof entry.id === "string") {
191
+ byId.set(entry.id, entry);
192
+ }
193
+ }
194
+
195
+ const messages: SessionMessageEntry[] = [];
196
+ const visited = new Set<string>();
197
+ let current = byId.get(entryId);
198
+ while (current && !visited.has(current.id)) {
199
+ visited.add(current.id);
200
+ if (current.type === "message" && "message" in current) {
201
+ messages.push(current as SessionMessageEntry);
202
+ }
203
+ current = current.parentId ? byId.get(current.parentId) : undefined;
204
+ }
205
+
206
+ return messages.reverse();
207
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage, StopReason, Usage } from "@f5-sales-demo/pi-ai";
1
+ import type { AssistantMessage, Message, StopReason, Usage } from "@f5-sales-demo/pi-ai";
2
2
 
3
3
  /**
4
4
  * Extracted stats from an assistant message.
@@ -36,8 +36,8 @@ export interface MessageStats {
36
36
  * Full details of a request, including content.
37
37
  */
38
38
  export interface RequestDetails extends MessageStats {
39
- messages: any[]; // The full conversation history or just the last turn
40
- output: any; // The model's response
39
+ messages: Message[];
40
+ output: AssistantMessage;
41
41
  }
42
42
 
43
43
  /**
@@ -169,7 +169,14 @@ export interface SessionMessageEntry {
169
169
  id: string;
170
170
  parentId: string | null;
171
171
  timestamp: string;
172
- message: AssistantMessage | { role: "user" | "toolResult" };
172
+ message: Message;
173
173
  }
174
174
 
175
- export type SessionEntry = SessionHeader | SessionMessageEntry | { type: string };
175
+ export interface SessionTreeEntry {
176
+ type: string;
177
+ id: string;
178
+ parentId: string | null;
179
+ [key: string]: unknown;
180
+ }
181
+
182
+ export type SessionEntry = SessionHeader | SessionMessageEntry | SessionTreeEntry | { type: string };