codesesh 1.0.6 → 1.0.8

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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-COZSWVCH.js → chunk-AGUAXTKC.js} +2 -2
  3. package/dist/{chunk-XQDYKKRC.js → chunk-DHQPM5FF.js} +98 -14
  4. package/dist/chunk-DHQPM5FF.js.map +1 -0
  5. package/dist/{chunk-YZ6XCSVQ.js → chunk-KVTLNX7V.js} +3 -3
  6. package/dist/chunk-KVTLNX7V.js.map +1 -0
  7. package/dist/{chunk-A3MT6L6Z.js → chunk-L4G3LE7I.js} +25 -26
  8. package/dist/chunk-L4G3LE7I.js.map +1 -0
  9. package/dist/{chunk-L3DDWTYV.js → chunk-RBCCNCDO.js} +12 -10
  10. package/dist/chunk-RBCCNCDO.js.map +1 -0
  11. package/dist/{chunk-4QFNZCBR.js → chunk-V3HKVTIO.js} +3 -2
  12. package/dist/chunk-V3HKVTIO.js.map +1 -0
  13. package/dist/{chunk-4ND6AT6W.js → chunk-YLXHGL4S.js} +6 -6
  14. package/dist/chunk-YLXHGL4S.js.map +1 -0
  15. package/dist/{discovery-ADSQARBV.js → discovery-KEZQU6RL.js} +7 -5
  16. package/dist/index.js +42 -16
  17. package/dist/index.js.map +1 -1
  18. package/dist/project-identity-worker.js +4 -4
  19. package/dist/scan-refresh-worker.js +6 -6
  20. package/dist/search-index-worker.js +6 -6
  21. package/dist/session-detail-worker.js +6 -6
  22. package/dist/smart-tag-worker.js +3 -3
  23. package/dist/web/assets/CustomTimeWindowDialog-z5xtqkBF.js +1 -0
  24. package/dist/web/assets/DetailLanding-CpV0jLmT.js +1 -0
  25. package/dist/web/assets/DialogTitle-B8zPwJB8.js +1 -0
  26. package/dist/web/assets/InteractiveReceipt-Bsj37HtN.js +1 -0
  27. package/dist/web/assets/OverviewScreen-B9qZqEQA.js +1 -0
  28. package/dist/web/assets/PrismHighlighter-DWS6Nx_X.js +6 -0
  29. package/dist/web/assets/Projects-oY3KCgS4.js +1 -0
  30. package/dist/web/assets/SearchResultsPanel-BAManMmt.js +1 -0
  31. package/dist/web/assets/SessionDetail-KSgkmLV7.js +53 -0
  32. package/dist/web/assets/SmartTagChips-BMP9pPRw.js +1 -0
  33. package/dist/web/assets/api-CtZGdxlN.js +1 -0
  34. package/dist/web/assets/index-BSLHxV0w.css +1 -0
  35. package/dist/web/assets/index-DNw59Qsn.js +1423 -0
  36. package/dist/web/assets/jsx-runtime-D3jfb0Ew.js +1 -0
  37. package/dist/web/assets/panel-DcOuALYX.js +1 -0
  38. package/dist/web/assets/session-indexes-DKukxoe1.js +4 -0
  39. package/dist/web/assets/translate-CLoII3EO.js +2 -0
  40. package/dist/web/assets/utils-Dk_44zMo.js +1 -0
  41. package/dist/web/index.html +9 -8
  42. package/package.json +2 -2
  43. package/dist/chunk-4ND6AT6W.js.map +0 -1
  44. package/dist/chunk-4QFNZCBR.js.map +0 -1
  45. package/dist/chunk-A3MT6L6Z.js.map +0 -1
  46. package/dist/chunk-L3DDWTYV.js.map +0 -1
  47. package/dist/chunk-XQDYKKRC.js.map +0 -1
  48. package/dist/chunk-YZ6XCSVQ.js.map +0 -1
  49. package/dist/web/assets/CustomTimeWindowDialog-D2bx7uDv.js +0 -1
  50. package/dist/web/assets/InteractiveReceipt-BFpIdno8.js +0 -1
  51. package/dist/web/assets/OverviewScreen-D_KXfiJi.js +0 -1
  52. package/dist/web/assets/PrismHighlighter-BgVkYu_S.js +0 -6
  53. package/dist/web/assets/Projects-CN44UpuR.js +0 -1
  54. package/dist/web/assets/SearchResultsPanel-CWD01v-a.js +0 -1
  55. package/dist/web/assets/SessionDetail-D8taQ6Tl.js +0 -53
  56. package/dist/web/assets/api-B9lJtVbO.js +0 -1
  57. package/dist/web/assets/index-XpolWMY4.js +0 -1423
  58. package/dist/web/assets/index-xbEEMszv.css +0 -1
  59. package/dist/web/assets/jsx-runtime-B-hcVAMW.js +0 -1
  60. package/dist/web/assets/session-indexes-fib48ZKn.js +0 -4
  61. package/dist/web/assets/translate-Dmym1ZDl.js +0 -2
  62. package/dist/web/assets/utils-Bk66u8b3.js +0 -1
  63. /package/dist/{chunk-COZSWVCH.js.map → chunk-AGUAXTKC.js.map} +0 -0
  64. /package/dist/{discovery-ADSQARBV.js.map → discovery-KEZQU6RL.js.map} +0 -0
@@ -4,9 +4,9 @@ import {
4
4
  getProjectIdentityKey,
5
5
  getSessionAgentKey,
6
6
  matchesProjectIdentity
7
- } from "./chunk-4ND6AT6W.js";
7
+ } from "./chunk-YLXHGL4S.js";
8
8
 
9
- // ../core/dist/chunk-2AZEUUPW.mjs
9
+ // ../core/dist/chunk-O3HCRG2L.mjs
10
10
  import * as os from "os";
11
11
  import * as path from "path";
12
12
  import { createHash } from "crypto";
@@ -324,4 +324,4 @@ export {
324
324
  normalizeProjectScopePath,
325
325
  buildProjectGroups
326
326
  };
327
- //# sourceMappingURL=chunk-YZ6XCSVQ.js.map
327
+ //# sourceMappingURL=chunk-KVTLNX7V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/projects/identity.ts","../../core/src/projects/display-name.ts","../../core/src/projects/fs.ts","../../core/src/projects/scope.ts","../../core/src/projects/groups.ts"],"sourcesContent":["import * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport type { ProjectIdentity, ProjectIdentityKind } from \"../types/index.js\";\nimport { fallbackDisplayName } from \"./display-name.js\";\nimport { realFs, type IdentityFs } from \"./fs.js\";\n\nexport type { IdentityFs } from \"./fs.js\";\n\nexport const PROJECT_IDENTITY_RESOLVER_REVISION = \"project-identity-v2\";\n\nexport interface ProjectIdentityProjection {\n identity: ProjectIdentity;\n resolverRevision: string;\n inputSignature: string;\n}\n\nconst MANIFESTS = [\n \"package.json\",\n \"Cargo.toml\",\n \"pyproject.toml\",\n \"go.mod\",\n \"Gemfile\",\n \"pom.xml\",\n \"build.gradle\",\n] as const;\n\nconst PARSEABLE_MANIFESTS = [\"package.json\", \"Cargo.toml\", \"pyproject.toml\"] as const;\n\nconst LOOSE_DIRS = new Set([\"/tmp\", \"/private/tmp\"]);\nconst LOOSE_HOME_DIRS = [\"Desktop\", \"Downloads\", \"Documents\"];\n\ntype PathOps = Pick<\n typeof path.posix,\n \"dirname\" | \"isAbsolute\" | \"join\" | \"relative\" | \"resolve\" | \"sep\"\n>;\n\n// Identity semantics live in the contract; this module only discovers them\n// from the filesystem and git.\nexport {\n getProjectIdentityKey,\n isProjectIdentityKind,\n matchesProjectIdentity,\n} from \"../contract/project-identity.js\";\n\nexport function normalizeGitRemote(url: string): string | null {\n if (!url) return null;\n let value = url.trim();\n if (/^[a-z][a-z\\d+.-]*:\\/\\//i.test(value)) {\n try {\n const parsed = new URL(value);\n value = `${parsed.hostname}${parsed.pathname}`;\n } catch {\n return null;\n }\n } else {\n const sshMatch = value.match(/^[^@]+@([^:]+):(.+)$/);\n if (sshMatch) value = `${sshMatch[1]}/${sshMatch[2]}`;\n }\n value = value.replace(/\\.git$/, \"\");\n if (!value.includes(\"/\")) return null;\n return value.toLowerCase();\n}\n\n// Process-lifetime identity cache, keyed by directory. Only used for the real\n// filesystem (fs === realFs); tests inject fake IdentityFs implementations and\n// must bypass the cache to stay isolated from each other. git remotes change\n// rarely, so a coarse TTL is enough — no file-watch invalidation needed.\nconst IDENTITY_CACHE_TTL_MS = 10 * 60 * 1000;\nexport const IDENTITY_CACHE_MAX_ENTRIES = 512;\n\ninterface IdentityCacheEntry {\n projection: ProjectIdentityProjection;\n resolvedAt: number;\n}\n\nconst identityCache = new Map<string, IdentityCacheEntry>();\n\n/** Clears the process-lifetime identity cache. For tests and explicit invalidation. */\nexport function clearIdentityCache(): void {\n identityCache.clear();\n}\n\nexport function computeIdentity(cwd: string | null | undefined, fs: IdentityFs): ProjectIdentity {\n return computeIdentityProjection(cwd, fs).identity;\n}\n\nexport function normalizeProjectDirectory(cwd: string | null | undefined): string {\n if (!cwd) return \"\";\n return getPathOps(cwd).resolve(cwd);\n}\n\nexport function computeIdentityProjection(\n cwd: string | null | undefined,\n fs: IdentityFs = realFs,\n resolverRevision = PROJECT_IDENTITY_RESOLVER_REVISION,\n): ProjectIdentityProjection {\n if (fs !== realFs) return resolveIdentityProjection(cwd, fs, resolverRevision);\n\n const key = normalizeProjectDirectory(cwd);\n const cached = identityCache.get(key);\n if (cached) {\n if (\n cached.projection.resolverRevision === resolverRevision &&\n Date.now() - cached.resolvedAt < IDENTITY_CACHE_TTL_MS\n ) {\n identityCache.delete(key);\n identityCache.set(key, cached);\n return cached.projection;\n }\n identityCache.delete(key);\n }\n const projection = resolveIdentityProjection(cwd, fs, resolverRevision);\n identityCache.set(key, { projection, resolvedAt: Date.now() });\n while (identityCache.size > IDENTITY_CACHE_MAX_ENTRIES) {\n const oldestKey = identityCache.keys().next().value;\n if (oldestKey == null) break;\n identityCache.delete(oldestKey);\n }\n return projection;\n}\n\nfunction resolveIdentityProjection(\n cwd: string | null | undefined,\n fs: IdentityFs,\n resolverRevision: string,\n): ProjectIdentityProjection {\n if (!cwd) return projectIdentityProjection(loose(), resolverRevision, [\"loose\", \"missing\"]);\n\n const pathOps = getPathOps(cwd);\n const absoluteCwd = pathOps.resolve(cwd);\n const homeDir = os.homedir();\n const homePathOps = getPathOps(homeDir);\n const home = homePathOps === pathOps ? pathOps.resolve(homeDir) : homeDir;\n if (absoluteCwd === home || LOOSE_DIRS.has(absoluteCwd)) {\n return projectIdentityProjection(loose(), resolverRevision, [\"loose\", absoluteCwd]);\n }\n if (\n homePathOps === pathOps &&\n LOOSE_HOME_DIRS.some((dir) => absoluteCwd === pathOps.join(home, dir))\n ) {\n return projectIdentityProjection(loose(), resolverRevision, [\"loose\", absoluteCwd]);\n }\n\n const gitRoot = findGitRoot(absoluteCwd, fs, pathOps);\n if (gitRoot) {\n const remote = fs.spawn(\"git\", [\"config\", \"--get\", \"remote.origin.url\"], { cwd: gitRoot });\n if (remote.exitCode === 0) {\n const normalized = normalizeGitRemote(remote.stdout.trim());\n if (normalized) {\n const identity: ProjectIdentity = {\n kind: \"git_remote\",\n key: normalized,\n displayName: deriveDisplayName({ kind: \"git_remote\", key: normalized, gitRoot, fs }),\n };\n return projectIdentityProjection(identity, resolverRevision, [\n \"git_remote\",\n gitRoot,\n normalized,\n identity.displayName,\n ]);\n }\n }\n\n const common = fs.spawn(\"git\", [\"rev-parse\", \"--git-common-dir\"], { cwd: gitRoot });\n if (common.exitCode === 0) {\n const raw = common.stdout.trim();\n if (raw) {\n const key = pathOps.isAbsolute(raw) ? raw : pathOps.resolve(gitRoot, raw);\n const identity: ProjectIdentity = {\n kind: \"git_common_dir\",\n key,\n displayName: deriveDisplayName({ kind: \"git_common_dir\", key, gitRoot, fs }),\n };\n return projectIdentityProjection(identity, resolverRevision, [\n \"git_common_dir\",\n gitRoot,\n key,\n identity.displayName,\n ]);\n }\n }\n }\n\n const manifestDir = findManifestDir(absoluteCwd, fs, pathOps);\n if (manifestDir) {\n const identity: ProjectIdentity = {\n kind: \"manifest_path\",\n key: manifestDir,\n displayName: deriveDisplayName({ kind: \"manifest_path\", key: manifestDir, fs }),\n };\n return projectIdentityProjection(identity, resolverRevision, [\n \"manifest_path\",\n manifestDir,\n identity.displayName,\n ]);\n }\n\n if (homePathOps === pathOps) {\n const synthetic = synthesizeCodexScratchIdentity(absoluteCwd, home, pathOps);\n if (synthetic) {\n return projectIdentityProjection(synthetic, resolverRevision, [\"synthetic\", synthetic.key]);\n }\n }\n\n const identity: ProjectIdentity = {\n kind: \"path\",\n key: absoluteCwd,\n displayName: fallbackDisplayName(absoluteCwd),\n };\n return projectIdentityProjection(identity, resolverRevision, [\"path\", absoluteCwd]);\n}\n\nfunction projectIdentityProjection(\n identity: ProjectIdentity,\n resolverRevision: string,\n inputs: readonly string[],\n): ProjectIdentityProjection {\n return {\n identity,\n resolverRevision,\n inputSignature: createHash(\"sha256\").update(JSON.stringify(inputs)).digest(\"hex\"),\n };\n}\n\nfunction loose(): ProjectIdentity {\n return { kind: \"loose\", key: \"loose\", displayName: \"Loose\" };\n}\n\nfunction synthesizeCodexScratchIdentity(\n absoluteCwd: string,\n home: string,\n pathOps: PathOps,\n): ProjectIdentity | null {\n const root = pathOps.resolve(pathOps.join(home, \"Documents\", \"Codex\"));\n const child = pathOps.relative(root, absoluteCwd);\n if (\n !child ||\n child === \"..\" ||\n child.startsWith(`..${pathOps.sep}`) ||\n pathOps.isAbsolute(child)\n ) {\n return null;\n }\n return { kind: \"synthetic\", key: \"codex:scratch\", displayName: \"Chats\" };\n}\n\nfunction getPathOps(input: string): PathOps {\n if (/^[a-zA-Z]:[\\\\/]/.test(input) || input.startsWith(\"\\\\\\\\\")) {\n return path.win32;\n }\n if (input.startsWith(\"/\")) return path.posix;\n return path;\n}\n\nfunction findGitRoot(start: string, fs: IdentityFs, pathOps: PathOps): string | null {\n let current = start;\n while (current) {\n if (fs.exists(pathOps.join(current, \".git\"))) return current;\n const parent = pathOps.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n return null;\n}\n\nfunction findManifestDir(start: string, fs: IdentityFs, pathOps: PathOps): string | null {\n let current = start;\n while (current) {\n for (const manifest of MANIFESTS) {\n if (fs.exists(pathOps.join(current, manifest))) return current;\n }\n const parent = pathOps.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n return null;\n}\n\ninterface DisplayNameInput {\n kind: ProjectIdentityKind;\n key: string;\n gitRoot?: string;\n fs: IdentityFs;\n}\n\nfunction deriveDisplayName(input: DisplayNameInput): string {\n const pathOps = getPathOps(input.gitRoot ?? input.key);\n const dir = input.gitRoot ?? (input.kind === \"manifest_path\" ? input.key : null);\n if (dir) {\n for (const manifest of PARSEABLE_MANIFESTS) {\n const manifestPath = pathOps.join(dir, manifest);\n if (input.fs.exists(manifestPath)) {\n const name = parseManifestName(manifest, input.fs.readText(manifestPath) ?? \"\");\n if (name) return name;\n }\n }\n }\n\n if (input.kind === \"git_remote\") {\n return input.key.split(\"/\").at(-1) || input.key;\n }\n if (input.gitRoot) return fallbackDisplayName(input.gitRoot);\n return fallbackDisplayName(input.key);\n}\n\nfunction parseManifestName(file: string, text: string): string | null {\n if (!text) return null;\n if (file === \"package.json\" || file === \"Cargo.toml\" || file === \"pyproject.toml\") {\n const match = text.match(/\"name\"\\s*:\\s*\"([^\"]+)\"/) || text.match(/^\\s*name\\s*=\\s*\"([^\"]+)\"/m);\n if (match?.[1]) return match[1];\n }\n return null;\n}\n","export function fallbackDisplayName(input: string): string {\n if (input === \"/\") return \"(root)\";\n const trimmed = input.replace(/[/\\\\]+$/, \"\");\n const parts = trimmed.split(/[/\\\\]+/).filter(Boolean);\n return parts.at(-1) ?? trimmed;\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\n\nexport interface IdentityFs {\n exists(path: string): boolean;\n readText(path: string): string | null;\n spawn(cmd: string, args: string[], opts: { cwd: string }): { stdout: string; exitCode: number };\n}\n\nexport const realFs: IdentityFs = {\n exists(path) {\n return existsSync(path);\n },\n readText(path) {\n try {\n return readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n },\n spawn(cmd, args, opts) {\n const result = spawnSync(cmd, args, { cwd: opts.cwd, encoding: \"utf8\", timeout: 1000 });\n return {\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n exitCode: result.status ?? 1,\n };\n },\n};\n","import type { ProjectIdentityRef, SessionHead } from \"../types/index.js\";\nimport {\n computeIdentity,\n matchesProjectIdentity,\n normalizeProjectDirectory,\n type IdentityFs,\n} from \"./identity.js\";\nimport { realFs } from \"./fs.js\";\n\nexport interface ProjectScopeMatcher {\n identity: ProjectIdentityRef;\n path: string;\n}\n\nexport function createProjectScopeMatcher(\n queryPath: string,\n fs: IdentityFs = realFs,\n): ProjectScopeMatcher {\n const identity = computeIdentity(queryPath, fs);\n return createProjectScopeMatcherFromIdentity(queryPath, identity);\n}\n\nexport function createProjectScopeMatcherFromIdentity(\n queryPath: string,\n identity: ProjectIdentityRef,\n): ProjectScopeMatcher {\n return {\n identity: { kind: identity.kind, key: identity.key },\n path: normalizeProjectScopePath(queryPath),\n };\n}\n\nexport function matchesProjectScope(session: SessionHead, scope: ProjectScopeMatcher): boolean {\n if (matchesProjectIdentity(session.project_identity, scope.identity)) return true;\n return session.directory ? isPathScopeMatch(scope.path, session.directory) : false;\n}\n\nexport function filterSessionsByProjectScope<T extends SessionHead>(\n sessions: T[],\n queryPath: string,\n fs?: IdentityFs,\n): T[] {\n const scope = createProjectScopeMatcher(queryPath, fs);\n return sessions.filter((session) => matchesProjectScope(session, scope));\n}\n\nfunction isPathScopeMatch(queryPath: string, sessionPath: string): boolean {\n const session = normalizeProjectScopePath(sessionPath);\n return (\n session === queryPath ||\n session.startsWith(queryPath + \"/\") ||\n queryPath.startsWith(session + \"/\")\n );\n}\n\nexport function normalizeProjectScopePath(path: string): string {\n return normalizeProjectDirectory(path).replaceAll(\"\\\\\", \"/\");\n}\n","import type { ProjectGroup, ProjectIdentity, SessionHead } from \"../types/index.js\";\nimport { buildSessionTree, getSessionAgentKey } from \"../contract/index.js\";\nimport { getProjectIdentityKey } from \"./identity.js\";\n\nexport function buildProjectGroups(sessions: SessionHead[]): ProjectGroup[] {\n const groups = new Map<\n string,\n { identity: ProjectIdentity; sources: Set<string>; sessionCount: number; lastActivity: number }\n >();\n\n const tree = buildSessionTree(sessions);\n for (const session of sessions) {\n if (tree.mountStateOf(session) === \"mounted-child\") continue;\n const identity = session.project_identity;\n if (!identity) continue;\n const activity = session.time_updated ?? session.time_created;\n const groupKey = getProjectIdentityKey(identity);\n const current = groups.get(groupKey);\n if (current) {\n current.sources.add(getSessionAgentKey(session));\n current.sessionCount += 1;\n current.lastActivity = Math.max(current.lastActivity, activity);\n } else {\n groups.set(groupKey, {\n identity,\n sources: new Set([getSessionAgentKey(session)]),\n sessionCount: 1,\n lastActivity: activity,\n });\n }\n }\n\n return [...groups.values()]\n .map((group) => ({\n identityKind: group.identity.kind,\n identityKey: group.identity.key,\n displayName: group.identity.displayName,\n sources: [...group.sources].sort(),\n sessionCount: group.sessionCount,\n lastActivity: group.lastActivity || null,\n }))\n .sort((a, b) => {\n if (a.identityKind === \"loose\" && b.identityKind !== \"loose\") return 1;\n if (b.identityKind === \"loose\" && a.identityKind !== \"loose\") return -1;\n return (b.lastActivity ?? 0) - (a.lastActivity ?? 0);\n });\n}\n"],"mappings":"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","names":["path","identity"]}
@@ -8,9 +8,9 @@ import {
8
8
  getSessionAgentKey,
9
9
  getSessionReferenceKey,
10
10
  toCalendarDayKey
11
- } from "./chunk-4ND6AT6W.js";
11
+ } from "./chunk-YLXHGL4S.js";
12
12
 
13
- // ../core/dist/chunk-FILZDGK6.mjs
13
+ // ../core/dist/chunk-XNDXGBK6.mjs
14
14
  var COST_ABSOLUTE_TOLERANCE = 1e-8;
15
15
  var COST_RELATIVE_TOLERANCE = 1e-6;
16
16
  function indexFacts(facts) {
@@ -147,7 +147,8 @@ function visitAttributedUsage(tree, options, visit) {
147
147
  inputTokens: detailedTokens ? message.inputTokens : 0,
148
148
  outputTokens,
149
149
  cacheReadTokens: detailedTokens ? message.cacheReadTokens : 0,
150
- cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0
150
+ cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0,
151
+ modelUsage: detailedTokens && message.model ? { [message.model]: message.inputTokens + outputTokens } : void 0
151
152
  });
152
153
  }
153
154
  }
@@ -167,15 +168,22 @@ function visitAttributedUsage(tree, options, visit) {
167
168
  if (!detailedTokens) {
168
169
  const inputTokens = nonNegative(session.stats.total_input_tokens);
169
170
  const outputTokens = nonNegative(session.stats.total_output_tokens);
171
+ const totalTokens = nonNegative(session.stats.total_tokens ?? inputTokens + outputTokens);
172
+ const modelTotal = Object.values(session.model_usage ?? {}).reduce(
173
+ (sum, tokens) => sum + tokens,
174
+ 0
175
+ );
170
176
  visit({
171
177
  entry,
172
178
  time: fallbackTime,
173
179
  messages: 0,
174
- totalTokens: nonNegative(session.stats.total_tokens ?? inputTokens + outputTokens),
180
+ totalTokens,
175
181
  inputTokens,
176
182
  outputTokens,
177
183
  cacheReadTokens: nonNegative(session.stats.total_cache_read_tokens),
178
- cacheCreateTokens: nonNegative(session.stats.total_cache_create_tokens)
184
+ cacheCreateTokens: nonNegative(session.stats.total_cache_create_tokens),
185
+ // Unreconciled model totals stay unassigned rather than inflating their shares.
186
+ modelUsage: modelTotal <= totalTokens ? session.model_usage : void 0
179
187
  });
180
188
  }
181
189
  }
@@ -246,14 +254,6 @@ function createAccumulator(byAgentNames, scope, to) {
246
254
  recent: []
247
255
  };
248
256
  }
249
- function foldModelUsage(node, into) {
250
- if (node.session.model_usage) {
251
- for (const [model, tokens] of Object.entries(node.session.model_usage)) {
252
- into.set(model, (into.get(model) ?? 0) + tokens);
253
- }
254
- }
255
- for (const child of node.children) foldModelUsage(child, into);
256
- }
257
257
  function getOrCreateProject(session, acc) {
258
258
  const identity = session.project_identity;
259
259
  if (!identity) return void 0;
@@ -301,17 +301,6 @@ function accumulateActivity(node, acc) {
301
301
  acc.daily.set(dayKey, bucket);
302
302
  }
303
303
  bucket.sessions += 1;
304
- const usage = /* @__PURE__ */ new Map();
305
- foldModelUsage(node, usage);
306
- for (const [model, tokens] of usage) {
307
- const entry = acc.models.get(model);
308
- if (entry) {
309
- entry.tokens += tokens;
310
- entry.sessions += 1;
311
- } else {
312
- acc.models.set(model, { tokens, sessions: 1 });
313
- }
314
- }
315
304
  trackProjectActivity(node, acc, agentKey);
316
305
  let recentIndex = acc.recent.length;
317
306
  for (let i = 0; i < acc.recent.length; i += 1) {
@@ -333,6 +322,16 @@ function addUsage(acc, entry, time, usage) {
333
322
  acc.messages += messages;
334
323
  acc.tokens += totalTokens;
335
324
  acc.cacheReadTokens += cacheReadTokens;
325
+ for (const [model, tokens] of Object.entries(usage.modelUsage ?? {})) {
326
+ if (tokens <= 0) continue;
327
+ const current = acc.models.get(model);
328
+ if (current) {
329
+ current.tokens += tokens;
330
+ current.sessions.add(entry);
331
+ } else {
332
+ acc.models.set(model, { tokens, sessions: /* @__PURE__ */ new Set([entry]) });
333
+ }
334
+ }
336
335
  const agentKey = getSessionAgentName(entry.session);
337
336
  let metric = acc.agents.get(agentKey);
338
337
  if (!metric) {
@@ -478,7 +477,7 @@ function buildDashboard(sessions, options) {
478
477
  };
479
478
  }).filter((item) => item.sessions > 0 || item.messages > 0 || item.tokens > 0 || item.cost > 0).sort((a, b) => b.sessions - a.sessions || b.cost - a.cost);
480
479
  const dailyActivity = [...acc.daily.values()].sort((a, b) => a.date.localeCompare(b.date));
481
- const modelDistribution = [...acc.models.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
480
+ const modelDistribution = [...acc.models.entries()].map(([model, { tokens, sessions: sessions2 }]) => ({ model, tokens, sessions: sessions2.size })).sort((a, b) => b.tokens - a.tokens);
482
481
  const modelCost = costFactsAvailable ? [...acc.modelCosts.values()].sort((a, b) => b.cost - a.cost).slice(0, MODEL_COST_LIMIT) : null;
483
482
  const rankedProjects = [...acc.projects.values()].sort((a, b) => b.cost - a.cost);
484
483
  const perProject = rankedProjects.slice(0, DASHBOARD_PROJECT_LIMIT).map(toProjectStat);
@@ -667,4 +666,4 @@ export {
667
666
  summarizeProjects,
668
667
  attachProjectMetricsFromTree
669
668
  };
670
- //# sourceMappingURL=chunk-A3MT6L6Z.js.map
669
+ //# sourceMappingURL=chunk-L4G3LE7I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/analytics/cost-attribution.ts","../../core/src/analytics/dashboard.ts","../../core/src/analytics/projects.ts"],"sourcesContent":["import type { CostSource, SessionHead } from \"../types/session.js\";\nimport {\n getSessionReferenceKey,\n type SessionTree,\n type SessionTreeNode,\n} from \"../contract/index.js\";\nimport type {\n DashboardCostFacts,\n MessageCostFact,\n SessionCostSummary,\n SessionModelCostFact,\n} from \"./cost-facts.js\";\n\nconst COST_ABSOLUTE_TOLERANCE = 1e-8;\nconst COST_RELATIVE_TOLERANCE = 1e-6;\n\ninterface MetricFactIndex {\n messagesBySession: Map<string, MessageCostFact[]>;\n summariesBySession: Map<string, SessionCostSummary>;\n}\n\nexport interface AttributedCost {\n entry: SessionTreeNode;\n time: number;\n cost: number;\n source: CostSource;\n modelCosts: SessionModelCostFact[];\n}\n\nexport interface CostAttributionOptions {\n from?: number;\n to: number;\n facts?: DashboardCostFacts | null;\n matchesEntry?: (session: SessionHead) => boolean;\n}\n\nexport interface AttributedUsage {\n entry: SessionTreeNode;\n time: number;\n messages: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheCreateTokens: number;\n modelUsage?: Readonly<Record<string, number>>;\n}\n\nexport type UsageAttributionOptions = CostAttributionOptions;\n\nfunction indexFacts(facts: DashboardCostFacts | null | undefined): MetricFactIndex {\n const messagesBySession = new Map<string, MessageCostFact[]>();\n const summariesBySession = new Map<string, SessionCostSummary>();\n if (!facts) return { messagesBySession, summariesBySession };\n\n for (const summary of facts.sessions) {\n summariesBySession.set(getSessionReferenceKey(summary.reference), summary);\n }\n for (const message of facts.messages) {\n const key = getSessionReferenceKey(message.reference);\n const messages = messagesBySession.get(key);\n if (messages) messages.push(message);\n else messagesBySession.set(key, [message]);\n }\n return { messagesBySession, summariesBySession };\n}\n\nfunction nonNegative(value: number | undefined): number {\n return Math.max(0, Number(value ?? 0));\n}\n\nfunction costsReconcile(left: number, right: number): boolean {\n const tolerance = Math.max(\n COST_ABSOLUTE_TOLERANCE,\n Math.max(Math.abs(left), Math.abs(right)) * COST_RELATIVE_TOLERANCE,\n );\n return Math.abs(left - right) <= tolerance;\n}\n\nfunction hasDetailedCost(session: SessionHead, summary: SessionCostSummary | undefined): boolean {\n const totalCost = Math.max(0, session.stats.total_cost);\n return (\n totalCost > 0 &&\n summary != null &&\n summary.untimedMessageCost <= COST_ABSOLUTE_TOLERANCE &&\n costsReconcile(summary.messageCost, totalCost)\n );\n}\n\nfunction hasDetailedMessages(\n session: SessionHead,\n summary: SessionCostSummary | undefined,\n): boolean {\n return (\n summary != null &&\n summary.untimedMessageCount === 0 &&\n summary.messageCount === nonNegative(session.stats.message_count)\n );\n}\n\n// Provider payloads disagree on whether reasoning is already included in output.\nfunction detailedOutputIncludesReasoning(\n session: SessionHead,\n summary: SessionCostSummary | undefined,\n): boolean | null {\n if (!summary) return null;\n const stats = session.stats;\n const sessionInput = nonNegative(stats.total_input_tokens);\n const sessionOutput = nonNegative(stats.total_output_tokens);\n const sessionCacheRead = nonNegative(stats.total_cache_read_tokens);\n const sessionCacheCreate = nonNegative(stats.total_cache_create_tokens);\n const sessionTotal = nonNegative(stats.total_tokens ?? sessionInput + sessionOutput);\n const outputIncludesReasoning = summary.outputTokens + summary.reasoningTokens;\n const outputMatches = summary.outputTokens === sessionOutput;\n const outputWithReasoningMatches = outputIncludesReasoning === sessionOutput;\n const effectiveOutput = outputMatches\n ? summary.outputTokens\n : outputWithReasoningMatches\n ? outputIncludesReasoning\n : null;\n\n if (\n effectiveOutput == null ||\n summary.inputTokens !== sessionInput ||\n summary.cacheReadTokens !== sessionCacheRead ||\n summary.cacheCreateTokens !== sessionCacheCreate ||\n summary.inputTokens + effectiveOutput !== sessionTotal ||\n summary.untimedInputTokens > 0 ||\n summary.untimedOutputTokens > 0 ||\n summary.untimedReasoningTokens > 0 ||\n summary.untimedCacheReadTokens > 0 ||\n summary.untimedCacheCreateTokens > 0\n ) {\n return null;\n }\n return !outputMatches && outputWithReasoningMatches;\n}\n\nfunction isInWindow(time: number, from: number | undefined, to: number): boolean {\n return (from == null || time >= from) && time <= to;\n}\n\nfunction resolvedCostSource(session: SessionHead, source?: CostSource): CostSource {\n return source ?? session.stats.cost_source ?? \"recorded\";\n}\n\nfunction fallbackModelCosts(\n summary: SessionCostSummary | undefined,\n totalCost: number,\n): SessionModelCostFact[] {\n if (!summary) return [];\n const modelTotal = summary.modelCosts.reduce((sum, model) => sum + model.cost, 0);\n return modelTotal > totalCost && !costsReconcile(modelTotal, totalCost) ? [] : summary.modelCosts;\n}\n\nexport function visitAttributedCosts(\n tree: SessionTree,\n options: CostAttributionOptions,\n visit: (attributed: AttributedCost) => void,\n): void {\n const factIndex = indexFacts(options.facts);\n const factsAvailable = options.facts != null;\n\n for (const entry of tree.entries) {\n if (options.matchesEntry && !options.matchesEntry(entry.session)) continue;\n const fallbackTime = entry.session.time_updated ?? entry.session.time_created;\n const pending = [entry];\n\n while (pending.length > 0) {\n const node = pending.pop()!;\n for (const child of node.children) pending.push(child);\n\n const session = node.session;\n const totalCost = Math.max(0, session.stats.total_cost);\n if (totalCost <= 0) continue;\n const key = getSessionReferenceKey(session.reference);\n const summary = factIndex.summariesBySession.get(key);\n\n if (factsAvailable && hasDetailedCost(session, summary)) {\n for (const message of factIndex.messagesBySession.get(key) ?? []) {\n if (!isInWindow(message.time, options.from, options.to) || message.cost <= 0) continue;\n const source = resolvedCostSource(session, message.costSource);\n visit({\n entry,\n time: message.time,\n cost: message.cost,\n source,\n modelCosts: message.model\n ? [\n {\n model: message.model,\n cost: message.cost,\n costRecorded: source === \"recorded\" ? message.cost : 0,\n },\n ]\n : [],\n });\n }\n continue;\n }\n\n if (!isInWindow(fallbackTime, options.from, options.to)) continue;\n visit({\n entry,\n time: fallbackTime,\n cost: totalCost,\n source: resolvedCostSource(session),\n modelCosts: factsAvailable ? fallbackModelCosts(summary, totalCost) : [],\n });\n }\n }\n}\n\nexport function visitAttributedUsage(\n tree: SessionTree,\n options: UsageAttributionOptions,\n visit: (attributed: AttributedUsage) => void,\n): void {\n const factIndex = indexFacts(options.facts);\n const factsAvailable = options.facts != null;\n\n for (const entry of tree.entries) {\n if (options.matchesEntry && !options.matchesEntry(entry.session)) continue;\n const fallbackTime = entry.session.time_updated ?? entry.session.time_created;\n const pending = [entry];\n\n while (pending.length > 0) {\n const node = pending.pop()!;\n for (const child of node.children) pending.push(child);\n\n const session = node.session;\n const key = getSessionReferenceKey(session.reference);\n const summary = factIndex.summariesBySession.get(key);\n const detailedMessages = factsAvailable && hasDetailedMessages(session, summary);\n const outputIncludesReasoning = factsAvailable\n ? detailedOutputIncludesReasoning(session, summary)\n : null;\n const detailedTokens = outputIncludesReasoning != null;\n\n if (detailedMessages || detailedTokens) {\n for (const message of factIndex.messagesBySession.get(key) ?? []) {\n if (!isInWindow(message.time, options.from, options.to)) continue;\n const outputTokens = detailedTokens\n ? message.outputTokens + (outputIncludesReasoning ? message.reasoningTokens : 0)\n : 0;\n visit({\n entry,\n time: message.time,\n messages: detailedMessages ? 1 : 0,\n totalTokens: detailedTokens ? message.inputTokens + outputTokens : 0,\n inputTokens: detailedTokens ? message.inputTokens : 0,\n outputTokens,\n cacheReadTokens: detailedTokens ? message.cacheReadTokens : 0,\n cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0,\n modelUsage:\n detailedTokens && message.model\n ? { [message.model]: message.inputTokens + outputTokens }\n : undefined,\n });\n }\n }\n\n if (!isInWindow(fallbackTime, options.from, options.to)) continue;\n if (!detailedMessages) {\n visit({\n entry,\n time: fallbackTime,\n messages: nonNegative(session.stats.message_count),\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheCreateTokens: 0,\n });\n }\n if (!detailedTokens) {\n const inputTokens = nonNegative(session.stats.total_input_tokens);\n const outputTokens = nonNegative(session.stats.total_output_tokens);\n const totalTokens = nonNegative(session.stats.total_tokens ?? inputTokens + outputTokens);\n const modelTotal = Object.values(session.model_usage ?? {}).reduce(\n (sum, tokens) => sum + tokens,\n 0,\n );\n visit({\n entry,\n time: fallbackTime,\n messages: 0,\n totalTokens,\n inputTokens,\n outputTokens,\n cacheReadTokens: nonNegative(session.stats.total_cache_read_tokens),\n cacheCreateTokens: nonNegative(session.stats.total_cache_create_tokens),\n // Unreconciled model totals stay unassigned rather than inflating their shares.\n modelUsage: modelTotal <= totalTokens ? session.model_usage : undefined,\n });\n }\n }\n }\n}\n","/**\n * Dashboard analytics — pure aggregation over SessionHead[] with no HTTP or DB\n * coupling. The handler parses request params and supplies file-activity; this\n * module owns the domain math: per-agent metrics, daily buckets, model\n * distribution, project ranking and the recent-sessions window.\n *\n * Metrics are *inclusive*: the unit of work is a top-level node (root or\n * orphan) and every descendant's stats roll into it. See contract/session-tree.\n */\nimport type { AgentInfo } from \"../types/index.js\";\nimport type { CostSource, ProjectIdentityKind, SessionHead } from \"../types/session.js\";\nimport type {\n DashboardAgentStat,\n DashboardAggregate,\n DashboardDailyBucket,\n DashboardData,\n DashboardPreviousTotals,\n DashboardProjectRollup,\n DashboardProjectStat,\n DashboardRecentSession,\n DashboardTotals,\n ModelCostEntry,\n ModelDistributionEntry,\n SessionTree,\n SessionTreeNode,\n} from \"../contract/index.js\";\nimport {\n addCalendarDays,\n buildSessionTree,\n countCalendarDays,\n filterSessionTreeEntriesByActivityWindow,\n getProjectIdentityKey,\n getSessionAgentKey,\n toCalendarDayKey,\n} from \"../contract/index.js\";\nimport type { DashboardCostFacts } from \"./cost-facts.js\";\nimport {\n visitAttributedCosts,\n visitAttributedUsage,\n type AttributedUsage,\n} from \"./cost-attribution.js\";\n\nexport type {\n DashboardAgentStat,\n DashboardAggregate,\n DashboardDailyBucket,\n DashboardData,\n DashboardPreviousTotals,\n DashboardProjectRollup,\n DashboardProjectStat,\n DashboardRecentSession,\n DashboardTotals,\n ModelDistributionEntry,\n};\n\nexport const DASHBOARD_RECENT_LIMIT = 10;\nexport const DASHBOARD_PROJECT_LIMIT = 12;\nexport const PROJECT_SPARKLINE_DAYS = 14;\nconst MODEL_COST_LIMIT = 20;\n\nexport interface DashboardScope {\n agent?: string;\n projectKind?: ProjectIdentityKind;\n projectKey?: string;\n}\n\ninterface DashboardAgentAggregate {\n name: string;\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n}\n\ninterface DashboardProjectAggregate {\n identityKind: ProjectIdentityKind;\n identityKey: string;\n displayName: string;\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n hasEstimatedCost: boolean;\n agentSessions: Map<string, number>;\n sparkline: number[];\n}\n\ninterface DashboardRecentCandidate {\n session: SessionHead;\n activity: number;\n}\n\ninterface DashboardAccumulator {\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n costRecorded: number;\n costEstimated: number;\n cacheReadTokens: number;\n hasEstimatedCost: boolean;\n agents: Map<string, DashboardAgentAggregate>;\n agentKeys: Set<string>;\n daily: Map<string, DashboardDailyBucket>;\n models: Map<string, { tokens: number; sessions: Set<SessionTreeNode> }>;\n modelCosts: Map<string, ModelCostEntry>;\n projects: Map<string, DashboardProjectAggregate>;\n projectKeys: Set<string>;\n /** Calendar day key → sparkline slot; anchored at the window's `to`. */\n sparklineSlots: Map<string, number>;\n /** Activity-desc, capped at DASHBOARD_RECENT_LIMIT; index 0 is the latest entry. */\n recent: DashboardRecentCandidate[];\n}\n\nexport interface DashboardOptions {\n byAgentNames: string[];\n scope: DashboardScope;\n from?: number;\n to: number;\n agentInfoMap?: Map<string, AgentInfo>;\n /** Immediately preceding window of equal length; drives `totals.previous`. */\n compare?: { from: number; to: number };\n /** Message-level cost and usage facts; null/undefined keeps session-activity fallback behavior. */\n costFacts?: DashboardCostFacts | null;\n}\n\n// --- SessionHead domain helpers (shared with other handlers via re-export) ---\n\nexport function getTotalTokens(stats: SessionHead[\"stats\"]): number {\n return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;\n}\n\nexport function getSessionAgentName(session: SessionHead): string {\n return getSessionAgentKey(session);\n}\n\nexport function getSessionActivityTime(session: SessionHead): number {\n return session.time_updated ?? session.time_created;\n}\n\nfunction matchesScope(session: SessionHead, scope: DashboardScope): boolean {\n if (scope.agent && getSessionAgentName(session) !== scope.agent) return false;\n if (scope.projectKind == null && scope.projectKey == null) return true;\n const identity = session.project_identity;\n // A half-specified scope matches nothing: identity fields are never undefined.\n return (\n identity != null && identity.kind === scope.projectKind && identity.key === scope.projectKey\n );\n}\n\n/** Top-level nodes (roots ∪ orphans) inside the window that match the scope. */\nfunction scopedEntries(\n tree: SessionTree,\n scope: DashboardScope,\n from: number | undefined,\n to: number,\n): SessionTreeNode[] {\n return filterSessionTreeEntriesByActivityWindow(tree, from, to).filter((node) =>\n matchesScope(node.session, scope),\n );\n}\n\nfunction emptyDailyBucket(date: string): DashboardDailyBucket {\n return {\n date,\n sessions: 0,\n messages: 0,\n cost: 0,\n input: 0,\n output: 0,\n cache_read: 0,\n cache_create: 0,\n };\n}\n\nfunction createAccumulator(\n byAgentNames: string[],\n scope: DashboardScope,\n to: number,\n): DashboardAccumulator {\n const agents = new Map<string, DashboardAgentAggregate>();\n for (const name of byAgentNames) {\n const key = name.toLowerCase();\n if (scope.agent && key !== scope.agent) continue;\n agents.set(key, { name, sessions: 0, messages: 0, tokens: 0, cost: 0 });\n }\n\n const sparklineSlots = new Map<string, number>();\n for (let slot = 0; slot < PROJECT_SPARKLINE_DAYS; slot += 1) {\n const day = addCalendarDays(to, slot - (PROJECT_SPARKLINE_DAYS - 1));\n sparklineSlots.set(toCalendarDayKey(day), slot);\n }\n\n return {\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost: 0,\n costRecorded: 0,\n costEstimated: 0,\n cacheReadTokens: 0,\n hasEstimatedCost: false,\n agents,\n agentKeys: new Set(),\n daily: new Map(),\n models: new Map(),\n modelCosts: new Map(),\n projects: new Map(),\n projectKeys: new Set(),\n sparklineSlots,\n recent: [],\n };\n}\n\nfunction getOrCreateProject(\n session: SessionHead,\n acc: DashboardAccumulator,\n): DashboardProjectAggregate | undefined {\n const identity = session.project_identity;\n if (!identity) return undefined;\n\n const key = getProjectIdentityKey(identity);\n let project = acc.projects.get(key);\n if (!project) {\n project = {\n identityKind: identity.kind,\n identityKey: identity.key,\n displayName: identity.displayName,\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost: 0,\n hasEstimatedCost: false,\n agentSessions: new Map(),\n sparkline: Array.from({ length: PROJECT_SPARKLINE_DAYS }, () => 0),\n };\n acc.projects.set(key, project);\n }\n return project;\n}\n\nfunction trackProjectActivity(\n node: SessionTreeNode,\n acc: DashboardAccumulator,\n agentKey: string,\n): void {\n const identity = node.session.project_identity;\n const project = getOrCreateProject(node.session, acc);\n if (!identity || !project) return;\n\n project.sessions += 1;\n project.agentSessions.set(agentKey, (project.agentSessions.get(agentKey) ?? 0) + 1);\n acc.projectKeys.add(getProjectIdentityKey(identity));\n}\n\n/** Fold activity metrics for one top-level node and its inclusive subtree. */\nfunction accumulateActivity(node: SessionTreeNode, acc: DashboardAccumulator): void {\n const session = node.session;\n const activity = getSessionActivityTime(session);\n const agentKey = getSessionAgentName(session);\n\n acc.sessions += 1;\n acc.agentKeys.add(agentKey);\n\n const metric = acc.agents.get(agentKey);\n if (metric) {\n metric.sessions += 1;\n }\n\n const dayKey = toCalendarDayKey(activity);\n let bucket = acc.daily.get(dayKey);\n if (!bucket) {\n bucket = emptyDailyBucket(dayKey);\n acc.daily.set(dayKey, bucket);\n }\n bucket.sessions += 1;\n\n trackProjectActivity(node, acc, agentKey);\n\n let recentIndex = acc.recent.length;\n for (let i = 0; i < acc.recent.length; i += 1) {\n if (activity > acc.recent[i]!.activity) {\n recentIndex = i;\n break;\n }\n }\n if (recentIndex < DASHBOARD_RECENT_LIMIT) {\n acc.recent.splice(recentIndex, 0, { session, activity });\n if (acc.recent.length > DASHBOARD_RECENT_LIMIT) acc.recent.pop();\n }\n}\n\nfunction addUsage(\n acc: DashboardAccumulator,\n entry: SessionTreeNode,\n time: number,\n usage: Omit<AttributedUsage, \"entry\" | \"time\">,\n): void {\n const { messages, totalTokens, inputTokens, outputTokens, cacheReadTokens, cacheCreateTokens } =\n usage;\n if (\n messages <= 0 &&\n totalTokens <= 0 &&\n inputTokens <= 0 &&\n outputTokens <= 0 &&\n cacheReadTokens <= 0 &&\n cacheCreateTokens <= 0\n ) {\n return;\n }\n\n acc.messages += messages;\n acc.tokens += totalTokens;\n acc.cacheReadTokens += cacheReadTokens;\n for (const [model, tokens] of Object.entries(usage.modelUsage ?? {})) {\n if (tokens <= 0) continue;\n const current = acc.models.get(model);\n if (current) {\n current.tokens += tokens;\n current.sessions.add(entry);\n } else {\n acc.models.set(model, { tokens, sessions: new Set([entry]) });\n }\n }\n\n const agentKey = getSessionAgentName(entry.session);\n let metric = acc.agents.get(agentKey);\n if (!metric) {\n metric = { name: agentKey, sessions: 0, messages: 0, tokens: 0, cost: 0 };\n acc.agents.set(agentKey, metric);\n }\n metric.messages += messages;\n metric.tokens += totalTokens;\n\n const dayKey = toCalendarDayKey(time);\n let bucket = acc.daily.get(dayKey);\n if (!bucket) {\n bucket = emptyDailyBucket(dayKey);\n acc.daily.set(dayKey, bucket);\n }\n bucket.messages += messages;\n bucket.input += Math.max(0, inputTokens - cacheReadTokens - cacheCreateTokens);\n bucket.output += outputTokens;\n bucket.cache_read += cacheReadTokens;\n bucket.cache_create += cacheCreateTokens;\n\n const project = getOrCreateProject(entry.session, acc);\n if (!project) return;\n project.messages += messages;\n project.tokens += totalTokens;\n}\n\nfunction accumulateUsage(\n tree: SessionTree,\n scope: DashboardScope,\n acc: DashboardAccumulator,\n from: number | undefined,\n to: number,\n costFacts: DashboardCostFacts | null | undefined,\n): void {\n visitAttributedUsage(\n tree,\n { from, to, facts: costFacts, matchesEntry: (session) => matchesScope(session, scope) },\n ({ entry, time, ...usage }) => addUsage(acc, entry, time, usage),\n );\n}\n\nfunction addModelCost(\n acc: DashboardAccumulator,\n model: string | undefined,\n cost: number,\n recordedCost: number,\n): void {\n if (!model || cost <= 0) return;\n const recorded = Math.max(0, Math.min(cost, recordedCost));\n const current = acc.modelCosts.get(model);\n if (current) {\n current.cost += cost;\n current.costRecorded += recorded;\n current.costEstimated += cost - recorded;\n return;\n }\n acc.modelCosts.set(model, {\n model,\n cost,\n costRecorded: recorded,\n costEstimated: cost - recorded,\n });\n}\n\nfunction addCost(\n acc: DashboardAccumulator,\n entry: SessionTreeNode,\n time: number,\n cost: number,\n source: CostSource,\n): void {\n if (cost <= 0) return;\n acc.cost += cost;\n if (source === \"recorded\") acc.costRecorded += cost;\n else {\n acc.costEstimated += cost;\n acc.hasEstimatedCost = true;\n }\n\n const agentKey = getSessionAgentName(entry.session);\n let metric = acc.agents.get(agentKey);\n if (!metric) {\n metric = { name: agentKey, sessions: 0, messages: 0, tokens: 0, cost: 0 };\n acc.agents.set(agentKey, metric);\n }\n metric.cost += cost;\n\n const dayKey = toCalendarDayKey(time);\n let bucket = acc.daily.get(dayKey);\n if (!bucket) {\n bucket = emptyDailyBucket(dayKey);\n acc.daily.set(dayKey, bucket);\n }\n bucket.cost += cost;\n\n const project = getOrCreateProject(entry.session, acc);\n if (!project) return;\n project.cost += cost;\n if (source === \"estimated\") project.hasEstimatedCost = true;\n const slot = acc.sparklineSlots.get(dayKey);\n if (slot != null) project.sparkline[slot]! += cost;\n}\n\nfunction accumulateCosts(\n tree: SessionTree,\n scope: DashboardScope,\n acc: DashboardAccumulator,\n from: number | undefined,\n to: number,\n costFacts: DashboardCostFacts | null | undefined,\n): void {\n visitAttributedCosts(\n tree,\n { from, to, facts: costFacts, matchesEntry: (session) => matchesScope(session, scope) },\n ({ entry, time, cost, source, modelCosts }) => {\n addCost(acc, entry, time, cost, source);\n for (const model of modelCosts) {\n addModelCost(acc, model.model, model.cost, model.costRecorded);\n }\n },\n );\n}\n\nfunction toPreviousTotals(acc: DashboardAccumulator): DashboardPreviousTotals {\n return { sessions: acc.sessions, messages: acc.messages, tokens: acc.tokens, cost: acc.cost };\n}\n\nfunction toProjectStat(project: DashboardProjectAggregate): DashboardProjectStat {\n return {\n identityKind: project.identityKind,\n identityKey: project.identityKey,\n displayName: project.displayName,\n sessions: project.sessions,\n messages: project.messages,\n tokens: project.tokens,\n cost: project.cost,\n cost_source:\n project.cost > 0 ? (project.hasEstimatedCost ? \"estimated\" : \"recorded\") : undefined,\n agents: [...project.agentSessions.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([agentKey]) => agentKey),\n sparkline: project.sparkline,\n };\n}\n\n/**\n * Aggregate sessions into dashboard metrics, daily buckets, model distribution,\n * project ranking and the recent-sessions window. Pure — no HTTP, no DB.\n */\nexport function buildDashboard(\n sessions: SessionHead[],\n options: DashboardOptions,\n): DashboardAggregate {\n const { byAgentNames, scope, from, to, agentInfoMap, compare, costFacts } = options;\n const tree = buildSessionTree(sessions);\n const costFactsAvailable = costFacts != null;\n\n const acc = createAccumulator(byAgentNames, scope, to);\n if (from != null) {\n const bucketDays = countCalendarDays(from, to);\n for (let i = 0; i < bucketDays; i += 1) {\n const key = toCalendarDayKey(addCalendarDays(from, i));\n acc.daily.set(key, emptyDailyBucket(key));\n }\n }\n for (const node of scopedEntries(tree, scope, from, to)) accumulateActivity(node, acc);\n accumulateUsage(tree, scope, acc, from, to, costFacts);\n accumulateCosts(tree, scope, acc, from, to, costFacts);\n\n let previous: DashboardPreviousTotals | undefined;\n if (compare) {\n const compareAcc = createAccumulator(byAgentNames, scope, compare.to);\n for (const node of scopedEntries(tree, scope, compare.from, compare.to)) {\n accumulateActivity(node, compareAcc);\n }\n accumulateUsage(tree, scope, compareAcc, compare.from, compare.to, costFacts);\n accumulateCosts(tree, scope, compareAcc, compare.from, compare.to, costFacts);\n previous = toPreviousTotals(compareAcc);\n }\n\n const perAgent: DashboardAgentStat[] = [...acc.agents.values()]\n .map((metrics) => {\n const info = agentInfoMap?.get(metrics.name);\n return {\n name: metrics.name,\n displayName: info?.displayName ?? metrics.name,\n icon: info?.icon ?? \"\",\n iconColored: info?.iconColored,\n sessions: metrics.sessions,\n messages: metrics.messages,\n tokens: metrics.tokens,\n cost: metrics.cost,\n };\n })\n .filter((item) => item.sessions > 0 || item.messages > 0 || item.tokens > 0 || item.cost > 0)\n .sort((a, b) => b.sessions - a.sessions || b.cost - a.cost);\n\n const dailyActivity = [...acc.daily.values()].sort((a, b) => a.date.localeCompare(b.date));\n const modelDistribution: ModelDistributionEntry[] = [...acc.models.entries()]\n .map(([model, { tokens, sessions }]) => ({ model, tokens, sessions: sessions.size }))\n .sort((a, b) => b.tokens - a.tokens);\n const modelCost = costFactsAvailable\n ? [...acc.modelCosts.values()].sort((a, b) => b.cost - a.cost).slice(0, MODEL_COST_LIMIT)\n : null;\n\n const rankedProjects = [...acc.projects.values()].sort((a, b) => b.cost - a.cost);\n const perProject = rankedProjects.slice(0, DASHBOARD_PROJECT_LIMIT).map(toProjectStat);\n const projectRollup = rankedProjects\n .slice(DASHBOARD_PROJECT_LIMIT)\n .reduce<DashboardProjectRollup>(\n (rollup, project) => ({\n projects: rollup.projects + 1,\n sessions: rollup.sessions + project.sessions,\n tokens: rollup.tokens + project.tokens,\n cost: rollup.cost + project.cost,\n }),\n { projects: 0, sessions: 0, tokens: 0, cost: 0 },\n );\n\n const recentSessions: DashboardRecentSession[] = acc.recent.map(({ session }) => ({\n reference: session.reference,\n session,\n }));\n\n const latest = acc.recent[0];\n return {\n totals: {\n sessions: acc.sessions,\n messages: acc.messages,\n tokens: acc.tokens,\n cost: acc.cost,\n costRecorded: acc.costRecorded,\n costEstimated: acc.costEstimated,\n cacheReadTokens: acc.cacheReadTokens,\n cost_source: acc.cost > 0 ? (acc.hasEstimatedCost ? \"estimated\" : \"recorded\") : undefined,\n latestActivity: latest?.activity || undefined,\n latestActivityProject: latest?.session.project_identity?.displayName,\n latestActivityAgent: latest ? getSessionAgentName(latest.session) : undefined,\n previous,\n },\n scopeCounts: { projects: acc.projectKeys.size, agents: acc.agentKeys.size },\n perAgent,\n dailyActivity,\n modelDistribution,\n modelCost,\n perProject,\n projectRollup,\n recentSessions,\n };\n}\n","/**\n * Project analytics — pure aggregation over SessionHead[], the project-level\n * counterpart to dashboard.ts. No HTTP, no DB: the caller supplies the groups\n * and the sessions, this module owns the arithmetic.\n *\n * Metrics are inclusive of sub-sessions and `sessionCount` is recomputed from\n * the sessions actually handed in, so a windowed caller gets windowed counts\n * instead of the cache-wide count carried by ProjectGroup.\n */\nimport type { ProjectGroup, SessionHead } from \"../types/index.js\";\nimport type {\n ApiProjectAgentStat,\n ApiProjectGroup,\n ApiProjectSummary,\n SessionTree,\n SessionTreeNode,\n} from \"../contract/index.js\";\nimport { getProjectIdentityKey } from \"../contract/project-identity.js\";\nimport {\n buildSessionTree,\n filterSessionTreeEntriesByActivityWindow,\n} from \"../contract/session-tree.js\";\nimport { getSessionAgentName } from \"./dashboard.js\";\nimport type { DashboardCostFacts } from \"./cost-facts.js\";\nimport { visitAttributedCosts, visitAttributedUsage } from \"./cost-attribution.js\";\n\ninterface ProjectMetrics {\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n hasEstimatedCost: boolean;\n agentStats: Map<string, ApiProjectAgentStat>;\n}\n\nexport function summarizeProjects(projects: readonly ApiProjectGroup[]): ApiProjectSummary {\n const summary: ApiProjectSummary = {\n projects: projects.length,\n sessions: 0,\n tokens: 0,\n cost: 0,\n latestActivity: null,\n };\n\n for (const project of projects) {\n summary.sessions += project.sessionCount;\n summary.tokens += project.tokens;\n summary.cost += project.cost;\n if (project.lastActivity != null) {\n summary.latestActivity =\n summary.latestActivity == null\n ? project.lastActivity\n : Math.max(summary.latestActivity, project.lastActivity);\n }\n }\n\n return summary;\n}\n\nfunction emptyMetrics(): ProjectMetrics {\n return {\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost: 0,\n hasEstimatedCost: false,\n agentStats: new Map(),\n };\n}\n\n/**\n * Fold per-session totals into the project groups they belong to. Sessions with\n * no resolved project identity contribute to nothing.\n */\nexport function attachProjectMetrics(\n projects: ProjectGroup[],\n sessions: SessionHead[],\n): ApiProjectGroup[] {\n const tree = buildSessionTree(sessions);\n return attachProjectMetricsToEntries(projects, tree, tree.entries);\n}\n\nexport function attachProjectMetricsFromTree(\n projects: ProjectGroup[],\n tree: SessionTree,\n from?: number,\n to?: number,\n costFacts?: DashboardCostFacts | null,\n): ApiProjectGroup[] {\n return attachProjectMetricsToEntries(\n projects,\n tree,\n filterSessionTreeEntriesByActivityWindow(tree, from, to),\n from,\n to,\n costFacts,\n );\n}\n\nfunction attachProjectMetricsToEntries(\n projects: ProjectGroup[],\n tree: SessionTree,\n entries: SessionTreeNode[],\n from?: number,\n to?: number,\n costFacts?: DashboardCostFacts | null,\n): ApiProjectGroup[] {\n const metrics = new Map<string, ProjectMetrics>();\n\n for (const node of entries) {\n const identity = node.session.project_identity;\n if (!identity) continue;\n\n const key = getProjectIdentityKey(identity);\n let current = metrics.get(key);\n if (!current) {\n current = emptyMetrics();\n metrics.set(key, current);\n }\n\n current.sessions += 1;\n\n const agentName = getSessionAgentName(node.session);\n const agent = current.agentStats.get(agentName);\n if (agent) {\n agent.sessions += 1;\n } else {\n current.agentStats.set(agentName, {\n name: agentName,\n sessions: 1,\n messages: 0,\n tokens: 0,\n cost: 0,\n });\n }\n }\n\n visitAttributedUsage(\n tree,\n { from, to: to ?? Number.POSITIVE_INFINITY, facts: costFacts },\n ({ entry, messages, totalTokens }) => {\n const identity = entry.session.project_identity;\n if (!identity) return;\n const key = getProjectIdentityKey(identity);\n let current = metrics.get(key);\n if (!current) {\n current = emptyMetrics();\n metrics.set(key, current);\n }\n current.messages += messages;\n current.tokens += totalTokens;\n\n const agentName = getSessionAgentName(entry.session);\n const agent = current.agentStats.get(agentName);\n if (agent) {\n agent.messages += messages;\n agent.tokens += totalTokens;\n } else {\n current.agentStats.set(agentName, {\n name: agentName,\n sessions: 0,\n messages,\n tokens: totalTokens,\n cost: 0,\n });\n }\n },\n );\n\n visitAttributedCosts(\n tree,\n { from, to: to ?? Number.POSITIVE_INFINITY, facts: costFacts },\n ({ entry, cost, source }) => {\n const identity = entry.session.project_identity;\n if (!identity) return;\n const key = getProjectIdentityKey(identity);\n let current = metrics.get(key);\n if (!current) {\n current = emptyMetrics();\n metrics.set(key, current);\n }\n current.cost += cost;\n if (source === \"estimated\") current.hasEstimatedCost = true;\n\n const agentName = getSessionAgentName(entry.session);\n const agent = current.agentStats.get(agentName);\n if (agent) agent.cost += cost;\n else {\n current.agentStats.set(agentName, {\n name: agentName,\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost,\n });\n }\n },\n );\n\n return projects.map((project) => {\n const metric = metrics.get(\n getProjectIdentityKey({ kind: project.identityKind, key: project.identityKey }),\n );\n return {\n ...project,\n sessionCount: metric?.sessions ?? 0,\n messages: metric?.messages ?? 0,\n tokens: metric?.tokens ?? 0,\n cost: metric?.cost ?? 0,\n cost_source:\n metric && metric.cost > 0\n ? metric.hasEstimatedCost\n ? \"estimated\"\n : \"recorded\"\n : undefined,\n agentStats: [...(metric?.agentStats.values() ?? [])].sort((a, b) => b.sessions - a.sessions),\n };\n });\n}\n"],"mappings":"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","names":["sessions"]}
@@ -32,9 +32,9 @@ import {
32
32
  reportFieldMismatch,
33
33
  resolveSessionTitle,
34
34
  safeParseJsonRecord
35
- } from "./chunk-4ND6AT6W.js";
35
+ } from "./chunk-YLXHGL4S.js";
36
36
 
37
- // ../core/dist/chunk-PGD63YEI.mjs
37
+ // ../core/dist/chunk-V6GZEKLJ.mjs
38
38
  import { statSync } from "fs";
39
39
  import { readdirSync, statSync as statSync2 } from "fs";
40
40
  import { join } from "path";
@@ -1288,7 +1288,8 @@ var TranscriptBuilder = class {
1288
1288
  cost_source: input.costSource,
1289
1289
  parts: (input.parts ?? []).map((part) => this.retainPart(part)),
1290
1290
  subagent_id: input.subagentId,
1291
- nickname: input.nickname
1291
+ nickname: input.nickname,
1292
+ ...input.automated ? { automated: true } : {}
1292
1293
  };
1293
1294
  }
1294
1295
  registerToolCall(part) {
@@ -1386,8 +1387,7 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
1386
1387
  name = AGENT_METADATA.name;
1387
1388
  displayName = AGENT_METADATA.displayName;
1388
1389
  basePath = this.configuredSourceRoot;
1389
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1390
- sessionsIndexCache = {};
1390
+ sessionsIndexCache = /* @__PURE__ */ new Map();
1391
1391
  sessionsIndexMtime = {};
1392
1392
  childContextsBySource = /* @__PURE__ */ new Map();
1393
1393
  childContextCache = /* @__PURE__ */ new Map();
@@ -1689,8 +1689,9 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
1689
1689
  const cacheKey = basename(projectDir);
1690
1690
  const indexPath = this.getSessionsIndexPath(projectDir);
1691
1691
  const mtime = this.readFileMtimeMs(indexPath);
1692
- if (cacheKey in this.sessionsIndexCache && this.sessionsIndexMtime[cacheKey] === mtime) {
1693
- return this.sessionsIndexCache[cacheKey];
1692
+ const cached = this.sessionsIndexCache.get(cacheKey);
1693
+ if (cached && this.sessionsIndexMtime[cacheKey] === mtime) {
1694
+ return cached;
1694
1695
  }
1695
1696
  const map = /* @__PURE__ */ new Map();
1696
1697
  if (existsSync2(indexPath)) {
@@ -1706,7 +1707,7 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
1706
1707
  } catch {
1707
1708
  }
1708
1709
  }
1709
- this.sessionsIndexCache[cacheKey] = map;
1710
+ this.sessionsIndexCache.set(cacheKey, map);
1710
1711
  this.sessionsIndexMtime[cacheKey] = mtime;
1711
1712
  return map;
1712
1713
  }
@@ -6158,7 +6159,7 @@ var PiAgent = class extends SingleFileSessionSource {
6158
6159
  if (role === "custom" && message["display"] === true) {
6159
6160
  const parts = normalizeTextParts(message["content"], timestampMs);
6160
6161
  if (parts.length === 0) return null;
6161
- return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
6162
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts, automated: true });
6162
6163
  }
6163
6164
  if (role === "branchSummary" || role === "compactionSummary") {
6164
6165
  const summary = String(message["summary"] ?? "").trim();
@@ -6260,6 +6261,7 @@ var PiAgent = class extends SingleFileSessionSource {
6260
6261
  return {
6261
6262
  id: narrowPiField("summary.id", entry["id"], asString) ?? "",
6262
6263
  role: type === "custom_message" ? "user" : "assistant",
6264
+ automated: type === "custom_message",
6263
6265
  agent: type === "custom_message" ? void 0 : "pi",
6264
6266
  timestampMs,
6265
6267
  parts: [{ type: "text", text, time_created: timestampMs }]
@@ -8385,4 +8387,4 @@ export {
8385
8387
  createRegisteredAgents,
8386
8388
  getAgentInfoMap
8387
8389
  };
8388
- //# sourceMappingURL=chunk-L3DDWTYV.js.map
8390
+ //# sourceMappingURL=chunk-RBCCNCDO.js.map