@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
@@ -0,0 +1,153 @@
1
+ /**
2
+ * DX-4508 — a LOCAL live view of a session's sub-agents, for the plugin's plan pane.
3
+ *
4
+ * The pane shows one card per sub-agent: what it is, what it is doing, and what it has spent. Its dashboard path
5
+ * (`GET /api/plan-sessions/:id/subagents`) lags about 80s behind the transcript (the relay's 5s poll, its 15s
6
+ * heartbeat flush, the pane's 60s read), so the same facts are also computed here, on the operator's own machine, from
7
+ * the transcript files themselves, and streamed by `platform-mcp subagents-live` within a poll of being written.
8
+ *
9
+ * ## One place computes each number
10
+ *
11
+ * Nothing here counts or prices on its own. It consumes the SAME two watcher feeds the relay wires
12
+ * (`onSourceDiscovered`, `onUsageLine`) into the SAME producers:
13
+ *
14
+ * | Snapshot field | Computed by | As the dashboard read derives it from |
15
+ * |---|---|---|
16
+ * | `description`, `agentType`, `parentId`, `effort`, `currentActivity`, `finishedAt`, `endStatus`, `cardId` | {@link createActivityObserver} (`cardId` is its `briefIssueId`) | the `dispatch_activities` row the same observer feeds |
17
+ * | `tokensTotal` | {@link UsageLedgerCounter} (per-reply per-field maximum) via {@link ledgerLineFromRaw} | `usage_by_source[<id>]`, all five fields summed |
18
+ * | `costUsd` | {@link priceLedgerCredit} over each reply | `usage_by_source[<id>].costUsd` (the ledger writer prices each credit with it) |
19
+ * | `toolCallCount` | the counter's distinct `tool_use` ids per reply | `SUM(cardinality(tool_use_ids))` |
20
+ * | `model` | the counter's newest reply whose model is not {@link SYNTHETIC_MODEL} | the newest non-synthetic `dispatch_message_usage.model` |
21
+ *
22
+ * Cost is priced per reply over the reply's whole usage, where the server prices each CREDIT (the excess a resend
23
+ * adds); pricing is linear, so the two agree whenever a reply keeps one model and speed, which Claude Code always writes.
24
+ *
25
+ * ## Where the live view differs from the observer's own stamps
26
+ *
27
+ * `startedAt` and `lastActivityAt` come from the sub-agent's own transcript lines (its first and newest `assistant` /
28
+ * `user` line). The observer stamps a start at the moment its watcher DISCOVERED the file, which is right for the relay
29
+ * (its first discovery is the real start, and the server keeps the earliest), but this view reads every file from byte
30
+ * 0 each time it starts, so a discovery stamp would show every finished sub-agent as having started, and been active,
31
+ * just now. The discovery stamp is used only for a sub-agent that has written no line yet.
32
+ *
33
+ * ## Memory
34
+ *
35
+ * Per sub-agent: the observer's one small state record, and the counter's one small entry per reply. No transcript
36
+ * text is held; the watcher reads each file's new bytes and drops them once parsed.
37
+ */
38
+ import { createActivityObserver } from "./activity-observer.js";
39
+ import { SYNTHETIC_MODEL, priceLedgerCredit } from "./pricing.js";
40
+ import { MAIN_SOURCE, REPORT_NO_COMPACTIONS, UsageLedgerCounter, ledgerLineFromRaw, totalTokensOfUsage } from "./usage-ledger.js";
41
+ /** The line's own timestamp in epoch ms, or null when it carries none that parses. */
42
+ function lineTime(raw) {
43
+ if (typeof raw.timestamp !== "string")
44
+ return null;
45
+ const ms = Date.parse(raw.timestamp);
46
+ return Number.isNaN(ms) ? null : ms;
47
+ }
48
+ /** `priceOf` is the caller's declared prices (the package holds none, DX-5611): the SAME lookup the server prices a credit with. */
49
+ export function createSubagentLiveView(priceOf) {
50
+ const sources = new Map();
51
+ const dirty = new Set();
52
+ // DX-5333: no `recordMessages`, so the observer's end-of-poll confirm (`onPollComplete`) of a held report has nothing to store and this
53
+ // view never forwards it; the observer's maps stay capped.
54
+ const observer = createActivityObserver({
55
+ // The observer reports a source's FULL current state on every change.
56
+ recordActivities: (changes) => {
57
+ for (const change of changes) {
58
+ const state = sources.get(change.activityId);
59
+ if (state === undefined) {
60
+ sources.set(change.activityId, {
61
+ activity: change,
62
+ counter: new UsageLedgerCounter(),
63
+ firstLineAt: null,
64
+ newestLineAt: null,
65
+ lastSent: null,
66
+ });
67
+ }
68
+ else {
69
+ state.activity = change;
70
+ }
71
+ dirty.add(change.activityId);
72
+ }
73
+ },
74
+ });
75
+ const snapshot = (state) => {
76
+ let costUsd = 0;
77
+ let toolCallCount = 0;
78
+ let model = null;
79
+ let modelAt = -Infinity;
80
+ for (const entry of state.counter.all()) {
81
+ costUsd += priceLedgerCredit(entry.model, entry.speed, entry.usage, priceOf).costUsd;
82
+ toolCallCount += entry.toolUseIds.length;
83
+ if (entry.model !== null && entry.model !== SYNTHETIC_MODEL && entry.repliedAt >= modelAt) {
84
+ model = entry.model;
85
+ modelAt = entry.repliedAt;
86
+ }
87
+ }
88
+ const a = state.activity;
89
+ const startedAt = state.firstLineAt ?? a.startedAt;
90
+ return {
91
+ id: a.activityId,
92
+ parentId: a.parentActivityId,
93
+ description: a.description,
94
+ agentType: a.agentType,
95
+ model,
96
+ effort: a.effort,
97
+ startedAt,
98
+ lastActivityAt: state.newestLineAt ?? startedAt,
99
+ finishedAt: a.finishedAt,
100
+ endStatus: a.endStatus,
101
+ tokensTotal: totalTokensOfUsage(state.counter.totals()),
102
+ costUsd,
103
+ toolCallCount,
104
+ currentActivity: a.currentActivity,
105
+ cardId: a.briefIssueId,
106
+ };
107
+ };
108
+ return {
109
+ onSourceDiscovered: observer.onSourceDiscovered,
110
+ onLine(raw, meta) {
111
+ // The observer first: it reads the main transcript's task notifications, and a line the ledger refuses (no
112
+ // timestamp) must not withhold them.
113
+ observer.onLine(raw, meta);
114
+ if (meta.source === MAIN_SOURCE)
115
+ return;
116
+ const state = sources.get(meta.source);
117
+ if (state === undefined)
118
+ return;
119
+ if (raw.type === "assistant" || raw.type === "user") {
120
+ const at = lineTime(raw);
121
+ if (at !== null) {
122
+ if (state.firstLineAt === null || at < state.firstLineAt) {
123
+ state.firstLineAt = at;
124
+ dirty.add(meta.source);
125
+ }
126
+ if (state.newestLineAt === null || at > state.newestLineAt) {
127
+ state.newestLineAt = at;
128
+ dirty.add(meta.source);
129
+ }
130
+ }
131
+ }
132
+ const line = ledgerLineFromRaw(raw, meta.source, meta.byteOffset, REPORT_NO_COMPACTIONS);
133
+ if (line !== null && state.counter.observeLine(line))
134
+ dirty.add(meta.source);
135
+ },
136
+ takeChanged() {
137
+ const out = [];
138
+ for (const source of dirty) {
139
+ const state = sources.get(source);
140
+ if (state === undefined)
141
+ continue;
142
+ const snap = snapshot(state);
143
+ const json = JSON.stringify(snap);
144
+ if (json === state.lastSent)
145
+ continue;
146
+ state.lastSent = json;
147
+ out.push(snap);
148
+ }
149
+ dirty.clear();
150
+ return out;
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * DX-2962: moved from danxbot root `src/agent/tool-summary.ts` unchanged —
3
+ * used only by `session-log-watcher.ts` in the root repo, so it moved
4
+ * wholesale rather than being duplicated or injected.
5
+ *
6
+ * Summarizes a tool's input into a short human-readable string.
7
+ */
8
+ export declare function summarizeToolInput(name: string, input: Record<string, unknown>): string;
9
+ export declare function truncStr(s: string, max: number): string;
10
+ /**
11
+ * Builds a human-readable summary for an assistant message from its content blocks.
12
+ * Returns "Tools: Read(...), Bash(...)" for tool calls, or "Text: <preview>" for text-only.
13
+ */
14
+ export declare function buildAssistantSummary(content: Record<string, unknown>[]): string;
15
+ /**
16
+ * Builds a human-readable summary for a user message containing tool results.
17
+ * Returns "Tool results: toolu_01, toolu_02".
18
+ */
19
+ export declare function buildToolResultSummary(content: Record<string, unknown>[]): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * DX-2962: moved from danxbot root `src/agent/tool-summary.ts` unchanged —
3
+ * used only by `session-log-watcher.ts` in the root repo, so it moved
4
+ * wholesale rather than being duplicated or injected.
5
+ *
6
+ * Summarizes a tool's input into a short human-readable string.
7
+ */
8
+ export function summarizeToolInput(name, input) {
9
+ switch (name) {
10
+ case "Read":
11
+ return truncStr(String(input.file_path || ""), 80);
12
+ case "Grep":
13
+ return truncStr(String(input.pattern || ""), 60);
14
+ case "Glob":
15
+ return truncStr(String(input.pattern || ""), 60);
16
+ case "Bash":
17
+ return truncStr(String(input.command || ""), 100);
18
+ default:
19
+ return "";
20
+ }
21
+ }
22
+ export function truncStr(s, max) {
23
+ return s.length > max ? s.slice(0, max) + "..." : s;
24
+ }
25
+ /**
26
+ * Builds a human-readable summary for an assistant message from its content blocks.
27
+ * Returns "Tools: Read(...), Bash(...)" for tool calls, or "Text: <preview>" for text-only.
28
+ */
29
+ export function buildAssistantSummary(content) {
30
+ const toolUses = content.filter((b) => b.type === "tool_use");
31
+ const textBlocks = content.filter((b) => b.type === "text");
32
+ const toolNames = toolUses
33
+ .map((t) => {
34
+ const name = t.name || "unknown";
35
+ const input = (t.input ?? {});
36
+ const detail = summarizeToolInput(name, input);
37
+ return detail ? `${name}(${detail})` : name;
38
+ })
39
+ .join(", ");
40
+ const textPreview = textBlocks
41
+ .map((b) => b.text)
42
+ .join(" ")
43
+ .slice(0, 200);
44
+ return toolNames
45
+ ? `Tools: ${toolNames}`
46
+ : `Text: ${textPreview || "(empty)"}`;
47
+ }
48
+ /**
49
+ * Builds a human-readable summary for a user message containing tool results.
50
+ * Returns "Tool results: toolu_01, toolu_02".
51
+ */
52
+ export function buildToolResultSummary(content) {
53
+ const toolResults = content.filter((r) => r.type === "tool_result");
54
+ const ids = toolResults
55
+ .map((r) => r.tool_use_id || "result")
56
+ .join(", ");
57
+ return `Tool results: ${ids}`;
58
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * DX-3964 - the per-source reducer that turns a transcript's lines into each reply's
3
+ * API time, agent time and lines added/removed, exactly per the `## Definitions` DX-3975
4
+ * measured on 18 real transcripts. Pure arithmetic (no `node:` imports), so the
5
+ * browser-safe entry exports it too.
6
+ *
7
+ * One reducer per SOURCE (one transcript file). It is fed every assistant, user,
8
+ * attachment and system line of that file in FILE ORDER, and reports, per reply, values
9
+ * that only ever grow as lines are appended (max semantics), so the ledger's
10
+ * `excess = max - credited` makes a resend, resume or restart credit nothing twice.
11
+ *
12
+ * - Never sort by timestamp: `hwm` (the largest timestamp seen) is a running maximum
13
+ * and a line older than it yields a 0 gap and never moves it back.
14
+ * - A reducer that starts mid-file anchors `hwm` on the first assistant or tool_result
15
+ * line it sees (a stale queued attachment at a restart must not be a gap), and
16
+ * credits only what it derived from lines in its own window.
17
+ */
18
+ import type { CompactionBoundary, LedgerLine } from "./usage-ledger.js";
19
+ import { type LedgerTiming } from "./ledger-timing.js";
20
+ /** A single gap above this is a suspend or an unanswered prompt, never work. */
21
+ export declare const MAX_CREDITED_GAP_MS = 600000;
22
+ export interface TimingToolUse {
23
+ id: string;
24
+ name: string;
25
+ input: Record<string, unknown> | null;
26
+ }
27
+ export interface PatchHunk {
28
+ lines: readonly string[];
29
+ }
30
+ export interface TimingToolResult {
31
+ toolUseId: string;
32
+ isError: boolean;
33
+ /** The result's text (a string content, or its text blocks joined). */
34
+ text: string;
35
+ /** `toolUseResult.structuredPatch` of the line carrying exactly this one result; absent from sub-agent transcripts. */
36
+ structuredPatch: readonly PatchHunk[] | null;
37
+ }
38
+ /** One transcript line of the fed set (assistant, user, attachment, system), parsed. */
39
+ export type TranscriptLine = {
40
+ kind: "assistant";
41
+ line: LedgerLine;
42
+ /** The counter's own key for the reply (`message.id`, or the synthetic per-offset key). */
43
+ replyKey: string;
44
+ byteOffset: number;
45
+ /** `message.model` is `<synthetic>` (API error text): it measures nothing. */
46
+ synthetic: boolean;
47
+ toolUses: readonly TimingToolUse[];
48
+ } | {
49
+ kind: "user";
50
+ source: string;
51
+ timestamp: number;
52
+ byteOffset: number;
53
+ /** Empty for a human prompt (string content, an array with no tool_result, an `isMeta` line). */
54
+ results: readonly TimingToolResult[];
55
+ /**
56
+ * DX-4939 - the length in characters of the text of a compaction's summary, when this is the `isCompactSummary` line Claude Code
57
+ * writes right after a `compact_boundary`; `null` for every other user line.
58
+ */
59
+ compactSummaryChars: number | null;
60
+ } | {
61
+ kind: "attachment" | "system";
62
+ source: string;
63
+ timestamp: number;
64
+ byteOffset: number;
65
+ }
66
+ /** DX-4922 - a `system` line whose subtype is `compact_boundary`; it feeds the timing reducer exactly as any system line does. */
67
+ | {
68
+ kind: "compaction";
69
+ source: string;
70
+ timestamp: number;
71
+ byteOffset: number;
72
+ boundary: CompactionBoundary;
73
+ };
74
+ export declare function timestampOf(t: TranscriptLine): number;
75
+ export declare class SourceTimingReducer {
76
+ /** `null` until anchored. */
77
+ private hwm;
78
+ private candidates;
79
+ /** Gap time ending at an attachment or system line: owned by the next reply to start. */
80
+ private carried;
81
+ private readonly replies;
82
+ private readonly tools;
83
+ private readonly pendingEdits;
84
+ /** Feed one line; returns the current values of every reply it touched. */
85
+ feed(t: TranscriptLine): Map<string, LedgerTiming>;
86
+ private feedAssistant;
87
+ private feedUser;
88
+ /** `S`: the largest request-side timestamp since the previous assistant line, clamped to the reply's first line. */
89
+ private sendTimeFor;
90
+ }
91
+ /** Number of lines in `s` (a trailing newline does not start another line). */
92
+ export declare function countLines(s: string): number;
93
+ /**
94
+ * The line diff of one Edit's `old_string` against its `new_string`: split on newline, common prefix and suffix trimmed,
95
+ * then the longest common subsequence. Equals Claude Code's own `structuredPatch` on 908 of 916 measured Edits.
96
+ */
97
+ export declare function lineDiffCounts(oldText: string, newText: string): {
98
+ added: number;
99
+ removed: number;
100
+ };
@@ -0,0 +1,231 @@
1
+ import { zeroTiming } from "./ledger-timing.js";
2
+ /** A single gap above this is a suspend or an unanswered prompt, never work. */
3
+ export const MAX_CREDITED_GAP_MS = 600_000;
4
+ /** Tools whose result is a parent WAITING on a child that carries that time in its own source. */
5
+ const WAITING_TOOLS = new Set(["Agent", "Task"]);
6
+ export function timestampOf(t) {
7
+ return t.kind === "assistant" ? t.line.timestamp : t.timestamp;
8
+ }
9
+ function snapshot(r) {
10
+ if (r.synthetic)
11
+ return zeroTiming();
12
+ return {
13
+ apiMs: Math.max(0, r.lastTs - r.sendTime),
14
+ agentMs: r.agentMs,
15
+ linesAdded: r.linesAdded,
16
+ linesRemoved: r.linesRemoved,
17
+ };
18
+ }
19
+ export class SourceTimingReducer {
20
+ /** `null` until anchored. */
21
+ hwm = null;
22
+ candidates = [];
23
+ /** Gap time ending at an attachment or system line: owned by the next reply to start. */
24
+ carried = 0;
25
+ replies = new Map();
26
+ tools = new Map();
27
+ pendingEdits = new Map();
28
+ /** Feed one line; returns the current values of every reply it touched. */
29
+ feed(t) {
30
+ const touched = new Set();
31
+ const ts = timestampOf(t);
32
+ let gap = 0;
33
+ if (this.hwm === null) {
34
+ // A reducer that starts mid-file anchors on its first assistant or tool_result line; a fresh file's own
35
+ // first line (offset 0) anchors it too. Before the anchor nothing is credited and nothing is remembered.
36
+ const anchors = t.byteOffset === 0 || t.kind === "assistant" || (t.kind === "user" && t.results.length > 0);
37
+ if (!anchors)
38
+ return new Map();
39
+ this.hwm = ts;
40
+ }
41
+ else {
42
+ gap = Math.max(0, ts - this.hwm);
43
+ if (gap > MAX_CREDITED_GAP_MS)
44
+ gap = 0;
45
+ this.hwm = Math.max(this.hwm, ts);
46
+ }
47
+ switch (t.kind) {
48
+ case "assistant":
49
+ this.feedAssistant(t, ts, gap, touched);
50
+ break;
51
+ case "user":
52
+ this.feedUser(t, gap, touched);
53
+ this.candidates.push({ ts, attachment: false });
54
+ break;
55
+ case "attachment":
56
+ this.carried += gap;
57
+ this.candidates.push({ ts, attachment: true });
58
+ break;
59
+ case "system":
60
+ case "compaction":
61
+ this.carried += gap;
62
+ break;
63
+ }
64
+ const out = new Map();
65
+ for (const key of touched)
66
+ out.set(key, snapshot(this.replies.get(key)));
67
+ return out;
68
+ }
69
+ feedAssistant(t, ts, gap, touched) {
70
+ let reply = this.replies.get(t.replyKey);
71
+ if (!reply) {
72
+ reply = {
73
+ sendTime: this.sendTimeFor(ts),
74
+ lastTs: ts,
75
+ synthetic: t.synthetic,
76
+ agentMs: this.carried,
77
+ linesAdded: 0,
78
+ linesRemoved: 0,
79
+ settled: new Set(),
80
+ };
81
+ this.carried = 0;
82
+ this.replies.set(t.replyKey, reply);
83
+ }
84
+ reply.lastTs = Math.max(reply.lastTs, ts);
85
+ reply.agentMs += gap;
86
+ touched.add(t.replyKey);
87
+ // Request-send candidates are the lines AFTER the previous assistant line, so every assistant line resets them.
88
+ this.candidates = [];
89
+ for (const use of t.toolUses) {
90
+ this.tools.set(use.id, { replyKey: t.replyKey, name: use.name });
91
+ if ((use.name === "Edit" || use.name === "Write") && use.input !== null && !reply.settled.has(use.id)) {
92
+ this.pendingEdits.set(use.id, { replyKey: t.replyKey, name: use.name, input: use.input });
93
+ }
94
+ }
95
+ }
96
+ feedUser(t, gap, touched) {
97
+ if (t.results.length === 0)
98
+ return; // a human prompt: the gap ending here is user or idle time
99
+ const owner = this.tools.get(t.results[0].toolUseId);
100
+ // A gap ending at an Agent/Task result is a parent waiting on a child whose own source carries that time.
101
+ if (owner && !WAITING_TOOLS.has(owner.name)) {
102
+ const reply = this.replies.get(owner.replyKey);
103
+ if (reply) {
104
+ reply.agentMs += gap;
105
+ touched.add(owner.replyKey);
106
+ }
107
+ }
108
+ for (const result of t.results) {
109
+ const pending = this.pendingEdits.get(result.toolUseId);
110
+ if (!pending)
111
+ continue;
112
+ this.pendingEdits.delete(result.toolUseId);
113
+ const reply = this.replies.get(pending.replyKey);
114
+ if (!reply)
115
+ continue;
116
+ reply.settled.add(result.toolUseId);
117
+ if (result.isError)
118
+ continue;
119
+ const lines = linesOf(pending, result);
120
+ reply.linesAdded += lines.added;
121
+ reply.linesRemoved += lines.removed;
122
+ touched.add(pending.replyKey);
123
+ }
124
+ }
125
+ /** `S`: the largest request-side timestamp since the previous assistant line, clamped to the reply's first line. */
126
+ sendTimeFor(firstTs) {
127
+ let best = null;
128
+ for (const c of this.candidates) {
129
+ // A `deferred_tools_record` attachment is written AFTER the response starts.
130
+ if (c.attachment && c.ts >= firstTs)
131
+ continue;
132
+ if (best === null || c.ts > best)
133
+ best = c.ts;
134
+ }
135
+ return best === null ? firstTs : Math.min(firstTs, best);
136
+ }
137
+ }
138
+ /** Number of lines in `s` (a trailing newline does not start another line). */
139
+ export function countLines(s) {
140
+ if (s === "")
141
+ return 0;
142
+ const n = s.split("\n").length;
143
+ return s.endsWith("\n") ? n - 1 : n;
144
+ }
145
+ function patchCounts(patch) {
146
+ let added = 0;
147
+ let removed = 0;
148
+ for (const hunk of patch) {
149
+ for (const line of hunk.lines) {
150
+ if (line.startsWith("+"))
151
+ added++;
152
+ else if (line.startsWith("-"))
153
+ removed++;
154
+ }
155
+ }
156
+ return { added, removed };
157
+ }
158
+ function splitLines(s) {
159
+ return s === "" ? [] : s.split("\n");
160
+ }
161
+ /** Myers' shortest edit script length (insertions + deletions) between two line lists. */
162
+ function editDistance(a, b) {
163
+ const n = a.length;
164
+ const m = b.length;
165
+ const max = n + m;
166
+ if (max === 0)
167
+ return 0;
168
+ const off = max;
169
+ const v = new Int32Array(2 * max + 2);
170
+ for (let d = 0; d <= max; d++) {
171
+ for (let k = -d; k <= d; k += 2) {
172
+ let x;
173
+ if (k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]))
174
+ x = v[off + k + 1];
175
+ else
176
+ x = v[off + k - 1] + 1;
177
+ let y = x - k;
178
+ while (x < n && y < m && a[x] === b[y]) {
179
+ x++;
180
+ y++;
181
+ }
182
+ v[off + k] = x;
183
+ if (x >= n && y >= m)
184
+ return d;
185
+ }
186
+ }
187
+ throw new Error("editDistance: unreachable, the edit script is at most n + m long");
188
+ }
189
+ /**
190
+ * The line diff of one Edit's `old_string` against its `new_string`: split on newline, common prefix and suffix trimmed,
191
+ * then the longest common subsequence. Equals Claude Code's own `structuredPatch` on 908 of 916 measured Edits.
192
+ */
193
+ export function lineDiffCounts(oldText, newText) {
194
+ const a = splitLines(oldText);
195
+ const b = splitLines(newText);
196
+ let start = 0;
197
+ while (start < a.length && start < b.length && a[start] === b[start])
198
+ start++;
199
+ let endA = a.length;
200
+ let endB = b.length;
201
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
202
+ endA--;
203
+ endB--;
204
+ }
205
+ const ta = a.slice(start, endA);
206
+ const tb = b.slice(start, endB);
207
+ const d = editDistance(ta, tb);
208
+ const lcs = (ta.length + tb.length - d) / 2;
209
+ return { added: tb.length - lcs, removed: ta.length - lcs };
210
+ }
211
+ function stringInput(input, key) {
212
+ const v = input[key];
213
+ return typeof v === "string" ? v : "";
214
+ }
215
+ /** The lines one successful Edit or Write result credits. Any other tool never reaches here. */
216
+ function linesOf(pending, result) {
217
+ if (pending.name === "Edit") {
218
+ if (pending.input.replace_all === true && result.structuredPatch !== null) {
219
+ return patchCounts(result.structuredPatch);
220
+ }
221
+ return lineDiffCounts(stringInput(pending.input, "old_string"), stringInput(pending.input, "new_string"));
222
+ }
223
+ if (result.text.includes("File created successfully")) {
224
+ return { added: countLines(stringInput(pending.input, "content")), removed: 0 };
225
+ }
226
+ // An overwrite: the previous content is not in the input, so only a patch (main and worker transcripts) can say.
227
+ if (result.text.includes("has been updated successfully") && result.structuredPatch !== null) {
228
+ return patchCounts(result.structuredPatch);
229
+ }
230
+ return { added: 0, removed: 0 };
231
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DX-2962: moved from danxbot root `src/types.ts` — this package is the
3
+ * canonical source now. Root `src/types.ts` re-exports these two names
4
+ * verbatim so every existing root import (`from "../types.js"` /
5
+ * `from "./types.js"`) keeps working unchanged (Core Principle 1 — single
6
+ * canonical schema, one home).
7
+ */
8
+ export interface AgentLogEntry {
9
+ timestamp: number;
10
+ type: string;
11
+ subtype?: string;
12
+ summary: string;
13
+ data: Record<string, unknown>;
14
+ }
15
+ /**
16
+ * Sub-agent lineage carried on AgentLogEntry.data (and propagated into every
17
+ * emitted EventPayload.data) so gpt-manager can attribute usage to the
18
+ * originating sub-agent within the same dispatch. Set by SessionLogWatcher for
19
+ * entries read from `<parent-session>/subagents/*.jsonl`; absent on parent entries.
20
+ */
21
+ export interface AgentLineage {
22
+ subagent_id: string;
23
+ parent_session_id: string | null;
24
+ agent_type: string | undefined;
25
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};