@50arms/session-telemetry 0.0.0-stage → 0.1.66

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 (45) hide show
  1. package/README.md +68 -2
  2. package/dist/.source-hash +1 -0
  3. package/dist/activity-observer.d.ts +186 -0
  4. package/dist/activity-observer.js +917 -0
  5. package/dist/api-tool-names.d.ts +12 -0
  6. package/dist/api-tool-names.js +19 -0
  7. package/dist/browser.d.ts +29 -0
  8. package/dist/browser.js +10 -0
  9. package/dist/catalog-prices.d.ts +27 -0
  10. package/dist/catalog-prices.js +31 -0
  11. package/dist/claim-tracker.d.ts +53 -0
  12. package/dist/claim-tracker.js +213 -0
  13. package/dist/dispatch-usage.d.ts +321 -0
  14. package/dist/dispatch-usage.js +657 -0
  15. package/dist/git-head.d.ts +1 -0
  16. package/dist/git-head.js +19 -0
  17. package/dist/index.d.ts +35 -0
  18. package/dist/index.js +49 -0
  19. package/dist/internal-logger.d.ts +16 -0
  20. package/dist/internal-logger.js +27 -0
  21. package/dist/ledger-timing.d.ts +26 -0
  22. package/dist/ledger-timing.js +31 -0
  23. package/dist/pricing.d.ts +93 -0
  24. package/dist/pricing.js +77 -0
  25. package/dist/real-work.d.ts +13 -0
  26. package/dist/real-work.js +57 -0
  27. package/dist/session-log-watcher.d.ts +616 -0
  28. package/dist/session-log-watcher.js +1285 -0
  29. package/dist/session-title-marks.d.ts +2 -0
  30. package/dist/session-title-marks.js +16 -0
  31. package/dist/stats-wire.d.ts +345 -0
  32. package/dist/stats-wire.js +636 -0
  33. package/dist/subagent-live.d.ts +36 -0
  34. package/dist/subagent-live.js +153 -0
  35. package/dist/tool-summary.d.ts +19 -0
  36. package/dist/tool-summary.js +58 -0
  37. package/dist/transcript-timing.d.ts +100 -0
  38. package/dist/transcript-timing.js +231 -0
  39. package/dist/types.d.ts +25 -0
  40. package/dist/types.js +1 -0
  41. package/dist/usage-ledger.d.ts +256 -0
  42. package/dist/usage-ledger.js +581 -0
  43. package/dist/write-queue.d.ts +9 -0
  44. package/dist/write-queue.js +16 -0
  45. package/package.json +43 -4
package/README.md CHANGED
@@ -1,3 +1,69 @@
1
- # Temporary Holding Version
1
+ # `@50arms/session-telemetry`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DX-2962 — the shared Claude Code transcript collector, extracted from
4
+ danxbot root's `src/agent/session-log-watcher.ts` so the danxbot worker and
5
+ the `platform-mcp` MCP server's plan event relay (DX-2954) run **identical** collector
6
+ code, instead of the worker's copy silently diverging from a re-implementation
7
+ inside the MCP package.
8
+
9
+ ## What's here
10
+
11
+ - `session-log-watcher.ts` — `SessionLogWatcher`: incremental tailing of a
12
+ Claude Code session's main + sub-agent JSONL transcripts. No whole-transcript
13
+ buffer — small per-reply bookkeeping only, and only for the life of the
14
+ process (see its own header for the DX-2962 "unbounded entries list
15
+ deleted" note).
16
+ - `types.ts` — `AgentLogEntry` / `AgentLineage`, the collector's emitted
17
+ shape. Canonical home now; danxbot root's `src/types.ts` re-exports both
18
+ names verbatim.
19
+ - `tool-summary.ts` — human-readable summary builders the collector uses.
20
+ - `subagent-live.ts` — `createSubagentLiveView` (DX-4508): per-sub-agent snapshots
21
+ (tokens, cost, tool calls, model, current activity, card) computed by the same
22
+ activity observer, ledger counter and pricing rule the dashboard's numbers come
23
+ from; streamed by `platform-mcp subagents-live`.
24
+ - `internal-logger.ts` — this package's own tiny logging shim (see its
25
+ header for why it does not reuse root's `src/logger.ts`).
26
+ - `index.ts` — the main entry (Node-only; imports `node:fs`/`node:path`/`node:os`).
27
+ - `browser.ts` — the `./browser` subpath. Zero `node:` imports, enforced by
28
+ `src/__tests__/no-node-imports.test.ts`. Today it re-exports only the two
29
+ types (`AgentLogEntry`/`AgentLineage`); DX-2963 adds the first runtime
30
+ export (the usage counter + pricing function).
31
+
32
+ ## Isolation
33
+
34
+ This package **never imports from the danxbot root repo**. Everything it
35
+ needs (logging, tool-summary formatting, the entry/lineage types) is either
36
+ moved in wholesale (when the root file had no other consumer) or
37
+ re-implemented minimally as this package's own dependency (when the root
38
+ file — like `src/logger.ts`, with ~300 unrelated consumers — was never a
39
+ candidate to move). See `session-log-watcher.ts` and `internal-logger.ts`'s
40
+ own header comments for the per-file reasoning.
41
+
42
+ ## How consumers resolve this package (DX-2962 AC3)
43
+
44
+ | Consumer | Mechanism | Why |
45
+ |---|---|---|
46
+ | **danxbot worker** (root repo, `tsx src/index.ts`) | npm **workspace** link (`"workspaces": ["packages/*"]` in root `package.json`, `"@50arms/session-telemetry"` in root `dependencies`) | The worker and this package always ship from the SAME repo commit — a workspace link means the worker runs whatever source the current commit carries, with zero drift risk and no dependency on registry propagation timing. The Docker image builds `dist/` for it in a dedicated builder stage (alongside `frontend-builder` in `Dockerfile`) since the final image's `npm ci --omit=dev` never installs this package's own `devDependencies` (`typescript`). |
47
+ | **`platform-mcp` MCP server** (separate npm-published package, DX-2954) | Ordinary npm dependency, **pinned to the exact published version** the worker's commit ships | The MCP server is a genuinely separate installable artifact (`npx -y @50arms/platform-mcp`), so it cannot workspace-link into danxbot's own checkout — it needs the real published bytes. Pinning exact (not `^`/`~`) is what makes "worker and MCP run identical collector code for any given release" true: MCP's own version bump is the only thing that ever changes what collector code it runs. |
48
+ | **Dashboard server** (`src/dashboard/*`) | Same npm dependency shape as the worker (the dashboard server's `src/` is root-repo code, so it resolves through the SAME root workspace link as the worker) | An npm dependency resolves through ordinary node_modules lookup, so no `@backend`-style alias is needed. |
49
+
50
+ ## Release order (DX-2962 AC3)
51
+
52
+ 1. Publish this package (`make publish-session-telemetry`).
53
+ 2. Publish `platform-mcp` (only once it depends on this package — DX-2954).
54
+ 3. Deploy the danxbot backend.
55
+
56
+ Publishing comes before deploying because the publish-drift gate
57
+ (`src/dispatch/publish-drift-gate.ts`) refuses a finalize whose bumped
58
+ `packages/*/package.json` version is not yet live on npm.
59
+
60
+ ## Boot check
61
+
62
+ `npm run verify:boot` (wired into `make publish-session-telemetry`,
63
+ mirroring `packages/danx-dashboard-mcp/scripts/verify-boot.sh`) packs the
64
+ freshly-built package with `npm pack`, installs the tarball into a clean
65
+ temp directory (the way an `npm install` consumer actually would — not a
66
+ relative `require` back into this checkout), and imports both the main
67
+ entry and the `./browser` subpath, calling one exported function from each.
68
+ A broken `exports` map, a missing `dist/` file, or an accidental `node:`
69
+ leak into `browser.ts` fails the publish before anything reaches npm.
@@ -0,0 +1 @@
1
+ 2ba7b19c6a4f26c9d1b0a2798a82b2e55e3f5aac580245587742c3332dcf6c25
@@ -0,0 +1,186 @@
1
+ /**
2
+ * DX-2954 — the TRANSCRIPT-side producer of a dispatch's running set.
3
+ *
4
+ * `dispatch_activities` answers "what is this dispatch running RIGHT NOW" — one
5
+ * row per sub-agent or background script, with `finished_at` NULL while it is
6
+ * still going. This module produces the sub-agent half of that set from the one
7
+ * signal danxbot owns end-to-end: the session-log watcher discovering a
8
+ * sub-agent's transcript file and then tailing it.
9
+ *
10
+ * ## Why the transcript, and where the hooks come in
11
+ *
12
+ * Claude Code's `SubagentStart` / `SubagentStop` hooks carry an `agent_type`
13
+ * before the sub-agent has written anything and a FINISH for every sub-agent,
14
+ * foreground or background. They ship from the 50arms plugin in the
15
+ * claude-plugins repo (DX-3284; DX-4235 moved them into the plugin's hooks module), which calls
16
+ * `50arms_api`, and reach the server
17
+ * through `POST /api/plan-sessions/me/activity` — which writes the SAME
18
+ * `dispatch_activities` rows through the same one server writer.
19
+ *
20
+ * The transcript stays the producer because it needs no hook to be installed: a
21
+ * session with no plugin, and every WORKER dispatch, still gets a row for each
22
+ * sub-agent from here. The hooks are an ENRICHMENT layered on the same rows:
23
+ *
24
+ * | Signal | Sub-agent start | Sub-agent finish | Background script |
25
+ * |---|---|---|---|
26
+ * | Transcript (here) | file discovered | a foreground sub-agent's own `tool_result` in the spawner's transcript (DX-5576); a background sub-agent's task notification (DX-4498); else the dispatch's terminal write (`finishOpenActivities`) as the fallback | not visible |
27
+ * | Hooks (DX-3284, operator sessions) | `SubagentStart` | `SubagentStop` | `PostToolUse` start; end at dispatch terminal (no documented completion event) |
28
+ *
29
+ * ## What it reports about each sub-agent (DX-4498)
30
+ *
31
+ * All of it read from what Claude Code itself writes, with no cooperation from
32
+ * the sub-agent:
33
+ *
34
+ * | Field | Read from |
35
+ * |---|---|
36
+ * | `agentType`, `description`, `parentActivityId` | the sub-agent's `meta.json` sidecar (via `SourceDiscoveredInfo`); the description through {@link redactLabel} |
37
+ * | `lastActivityAt` | the timestamp of its latest `assistant` / `user` line |
38
+ * | `effort` | its latest `assistant` line's top-level `effort` |
39
+ * | `currentActivity` | its latest `tool_use` block, through {@link describeToolCall} |
40
+ * | `briefProfile` | DX-5898: the leading word of its brief (the root `user` line) when shaped like a profile name, else null; the server matches it against the team's profiles |
41
+ * | `briefIssueId` | the card its spawning `Agent` call names: the first card id in its `description` (redacted), else the first anywhere in its brief (the root `user` line) — see {@link firstIssueId} (DX-4508) |
42
+ * | `finishedAt`, `endStatus` | what Claude Code writes into the SPAWNER's transcript: a foreground spawn's `tool_result` (DX-5576) or a background spawn's task notification (DX-4498) |
43
+ *
44
+ * Model, tokens, cost, tool-call count and the claimed card are NOT here: the
45
+ * usage ledger already records them per reply, keyed by the same source, and
46
+ * the read derives them from it (two places recording one number is how they
47
+ * drift).
48
+ *
49
+ * ## Why the two producers cannot double-count
50
+ *
51
+ * Every row is an upsert keyed `(dispatch_id, kind, activity_id)`, and BOTH
52
+ * producers derive `activity_id` from the same thing: the sub-agent's transcript
53
+ * file basename, `agent-<agent_id>`. The watcher has it directly; a hook has the
54
+ * `agent_id` and builds the same basename from it: `agent-` plus the id with one leading `agent-` stripped (the hooks docs
55
+ * show the prefix on some `agent_id`s), so both forms reach this key. That rule lives in the 50arms plugin's hooks module
56
+ * (claude-plugins repo). A hook's `transcript_path` is NOT the
57
+ * sub-agent's file — on these events it is the MAIN session's transcript, and only
58
+ * `SubagentStop` also carries the sub-agent's own as `agent_transcript_path`,
59
+ * which the plugin uses to refuse a key that disagrees. That basename is also
60
+ * the key the ledger credits the sub-agent's tokens under, which is how
61
+ * per-sub-agent tokens join to the running set without this table carrying a
62
+ * token column. A task notification names the sub-agent by its bare id
63
+ * (`<task-id>`), which is mapped onto the same `agent-<id>` key.
64
+ *
65
+ * ## A finish is reported only when Claude Code says so
66
+ *
67
+ * A sub-agent's OWN transcript gives no "this sub-agent is done" signal — a
68
+ * finished sub-agent's file simply stops growing, which is indistinguishable
69
+ * from a slow one. Guessing a finish from an idle gap would mark a
70
+ * long-thinking sub-agent as complete, so this module never does. The finishes it
71
+ * reports are EXPLICIT, and a row ends one of three ways:
72
+ *
73
+ * 1. A FOREGROUND spawn's `tool_result` in the spawner's transcript (DX-5576), written when the sub-agent is done: its status is
74
+ * `completed`, `failed` for an errored result, `stopped` for an interrupted or rejected one ({@link foregroundEndStatus}).
75
+ * 2. A BACKGROUND spawn's `<task-notification>` (DX-4498), which Claude Code delivers to the spawner when the task ends, carrying
76
+ * its `<status>` (`completed`, `failed`, `killed`, `stopped`). A foreground spawn gets no notification.
77
+ * 3. The fallback, for a row neither signal reached: the `SubagentStop` hook (operator sessions) or the dispatch's own terminal
78
+ * write, which sets `finishedAt` but names no status.
79
+ *
80
+ * Whichever lands first sets `finishedAt`; a later signal only fills the status, unless it is a later end (DX-6815: a sub-agent resumed after its finish ends again), which replaces the finish.
81
+ */
82
+ import { type ActivityChange, type ActivityEndStatus, type ActivityMessage, type ActivityProblemLink } from "./stats-wire.js";
83
+ import type { EntryMeta, SourceDiscoveredInfo } from "./session-log-watcher.js";
84
+ /** What the observer needs from the dispatch's usage accumulator. */
85
+ export interface ActivityObserverDeps {
86
+ /**
87
+ * Queue activity changes onto the next stats write. Called with the full
88
+ * current state of each changed activity — the server's upsert is idempotent,
89
+ * so re-reporting an unchanged field is free and a dropped batch is
90
+ * self-healing on the next write.
91
+ */
92
+ recordActivities: (changes: ActivityChange[]) => void;
93
+ /**
94
+ * DX-2954 — ignore every sub-agent file that already existed when the watcher
95
+ * attached. Set for a dispatch that continues an earlier one's session
96
+ * (`StartCursorPlan.inheritsPredecessorSubagents`): those files are the
97
+ * predecessor's sub-agents, which ended with its process, and reporting them
98
+ * would open a never-closing "running" row per predecessor sub-agent on the
99
+ * new dispatch — inflating `subagent_count` / `running_activity_count` and
100
+ * tripping the silent-too-long warning ten minutes later.
101
+ */
102
+ ignorePreexisting?: boolean;
103
+ /**
104
+ * DX-5333 — queue the messages read from sub-agent transcripts (see {@link ActivityMessage}). Absent for a consumer that
105
+ * stores nothing (the local live view).
106
+ */
107
+ recordMessages?: (messages: ActivityMessage[]) => void;
108
+ /** DX-5333 — queue the card problems a sub-agent created (see {@link ActivityProblemLink}). Absent like {@link ActivityObserverDeps.recordMessages}. */
109
+ recordProblemLinks?: (links: ActivityProblemLink[]) => void;
110
+ /**
111
+ * DX-5333 — `git rev-parse HEAD` in `cwd`: the full object id, or null when `cwd` is not a git checkout (or git cannot be
112
+ * run). Absent when the consumer stamps no start commit. Called at most once per distinct `cwd` per sub-agent, and only for
113
+ * a sub-agent the collector saw START (never one already running when it attached, whose checkout has moved on since).
114
+ */
115
+ readHeadSha?: (cwd: string) => Promise<string | null>;
116
+ }
117
+ export interface ActivityObserver {
118
+ /** Wire to the watcher's `onSourceDiscovered`. */
119
+ onSourceDiscovered: (info: SourceDiscoveredInfo) => void;
120
+ /** DX-4498 — wire to the watcher's `onUsageLine` (the raw line feed: the brief and task notifications are not entries). */
121
+ onLine: (raw: Record<string, unknown>, meta: EntryMeta) => void;
122
+ /**
123
+ * DX-5333 - wire to the watcher's `onPollComplete`. A sub-agent whose end was reported in an EARLIER poll (its spawn's result, or the
124
+ * task-notification) has all its lines read since (DX-5413: a signal read this poll may precede its child's last lines), so its held reply is confirmed here as its final.
125
+ */
126
+ onPollComplete: () => void;
127
+ }
128
+ /**
129
+ * DX-4498 — a task notification's `(task id, end status)`, or null for a line that is not one, or one whose status is
130
+ * not an end (`running`). Only the two delivery shapes Claude Code itself writes are read: a `user` line whose
131
+ * `origin.kind` is `task-notification`, and a `queued_command` attachment with that `commandMode`. A sub-agent's
132
+ * reply that merely QUOTES a notification carries neither marker, so it can never end anything.
133
+ */
134
+ export declare function parseTaskNotification(raw: Record<string, unknown>): {
135
+ taskId: string;
136
+ endStatus: ActivityEndStatus;
137
+ } | null;
138
+ /** Punctuation a token may be wrapped in, which never stops it from being read as opaque. */
139
+ export declare const WRAPPING_PUNCTUATION: RegExp;
140
+ /**
141
+ * DX-4498 — an agent-written label as the stats wire may carry it: on one line, with every path-shaped token (any token
142
+ * holding a `/` or `\`) read as `[path]`, and every secret-shaped token (an `ENV_NAME=value` value, or an opaque run of
143
+ * 20+ letters and digits such as a key, token or full SHA) read as `[redacted]`. A label is free text the agent wrote
144
+ * for a person to read (real shell descriptions name absolute paths), and it reaches every reader of the team, so this
145
+ * is the one way any label leaves a transcript. Empty for a blank label.
146
+ */
147
+ export declare function redactLabel(label: string): string;
148
+ /**
149
+ * DX-5333 — a message as the stats wire may carry it: every credential read as `[redacted]` — a URL's password, an
150
+ * `Authorization`/bearer value, the value of a credential-named assignment, JSON key or `--flag` (`password: x`,
151
+ * `"password": "x"`, `--password x`), and every opaque run of 20+ characters (a key, a token, a base64 secret that may hold
152
+ * `/` or end in `=`; a git commit id is kept) — with the text's own whitespace, line breaks, paths and prose kept: a brief is
153
+ * read to replay the work, and its paths are the work. NUL characters are removed (Postgres refuses them, which would wedge
154
+ * the write that carries the message). Cut to {@link ACTIVITY_MESSAGE_MAX_CHARS}. The server applies the same function again
155
+ * on receipt.
156
+ *
157
+ * The order of the chain matters: NUL goes first, the PEM block next (its body lines would otherwise be read token by token), then
158
+ * the structured patterns (URL credentials, headers, named values, flags), and the per-token classifier last, over what is
159
+ * left. The server re-runs the whole chain on text already masked here, so every step must leave `[redacted]` alone (idempotent).
160
+ */
161
+ export declare function redactSecrets(text: string): string;
162
+ /** DX-4498 — a transcript value read as a label: {@link redactLabel}'s text, or null for a non-string or blank one. */
163
+ export declare function labelOf(value: unknown): string | null;
164
+ /**
165
+ * DX-4498 — a sub-agent's current activity, from one `tool_use` block: the tool's name, plus a label only where the
166
+ * agent wrote one AS a label (a shell call's `description`, an `Agent` call's `description`, a `Skill` call's skill
167
+ * name), through {@link labelOf}. Never a command, a file path, a secret or a prompt: the stats wire carries ids,
168
+ * numbers and redacted labels, never free-form transcript content.
169
+ */
170
+ export declare function describeToolCall(name: string, input: Record<string, unknown>): string;
171
+ /**
172
+ * DX-4508 — the first card-id-shaped token in a text (`DX-4498`, also `DX-4498:`, `(DX-4498)`), or null when it has
173
+ * none. A sub-agent's card is the one its spawning `Agent` call names: its `description` ("Review DX-4498 code gates")
174
+ * first, else its brief ("Quality gate for card DX-4421 ..."), where the first-word rule DX-4498 shipped missed most
175
+ * real briefs. Only the SHAPE is checked: the server keeps the id only when it is a real card of the team, so a plan ref
176
+ * (`PLAN-2`) or rule ref (`R-45`) never reaches a reader.
177
+ */
178
+ export declare function firstIssueId(text: string): string | null;
179
+ /**
180
+ * DX-5898 - the leading word of a sub-agent's brief when it has a profile name's shape (`gate-designer DX-5875` -> `gate-designer`), else
181
+ * null. An orchestrator briefs a profile sub-agent as `<profile> <CARD-ID>` (DX-5899); the collector cannot know the team's profiles, so it
182
+ * keeps only the raw word and the server matches it against the profiles the team may see when it reads the row. A brief that opens with an
183
+ * ordinary word equal to a profile's name therefore gives a false match: the shape check and that server match are the only filters.
184
+ */
185
+ export declare function leadingProfileWord(text: string): string | null;
186
+ export declare function createActivityObserver(deps: ActivityObserverDeps): ActivityObserver;