@thehammer/danx-session-telemetry 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/browser.d.ts +22 -0
- package/dist/browser.js +3 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/internal-logger.d.ts +16 -0
- package/dist/internal-logger.js +24 -0
- package/dist/session-log-watcher.d.ts +310 -0
- package/dist/session-log-watcher.js +846 -0
- package/dist/tool-summary.d.ts +19 -0
- package/dist/tool-summary.js +58 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# `@thehammer/danx-session-telemetry`
|
|
2
|
+
|
|
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 `danx-dashboard-mcp` desktop bridge (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
|
+
- `internal-logger.ts` — this package's own tiny logging shim (see its
|
|
21
|
+
header for why it does not reuse root's `src/logger.ts`).
|
|
22
|
+
- `index.ts` — the main entry (Node-only; imports `node:fs`/`node:path`/`node:os`).
|
|
23
|
+
- `browser.ts` — the `./browser` subpath. Zero `node:` imports, enforced by
|
|
24
|
+
`src/__tests__/no-node-imports.test.ts`. Today it re-exports only the two
|
|
25
|
+
types (`AgentLogEntry`/`AgentLineage`); DX-2963 adds the first runtime
|
|
26
|
+
export (the usage counter + pricing function).
|
|
27
|
+
|
|
28
|
+
## Isolation
|
|
29
|
+
|
|
30
|
+
This package **never imports from the danxbot root repo**. Everything it
|
|
31
|
+
needs (logging, tool-summary formatting, the entry/lineage types) is either
|
|
32
|
+
moved in wholesale (when the root file had no other consumer) or
|
|
33
|
+
re-implemented minimally as this package's own dependency (when the root
|
|
34
|
+
file — like `src/logger.ts`, with ~300 unrelated consumers — was never a
|
|
35
|
+
candidate to move). See `session-log-watcher.ts` and `internal-logger.ts`'s
|
|
36
|
+
own header comments for the per-file reasoning.
|
|
37
|
+
|
|
38
|
+
## How consumers resolve this package (DX-2962 AC3)
|
|
39
|
+
|
|
40
|
+
| Consumer | Mechanism | Why |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **danxbot worker** (root repo, `tsx src/index.ts`) | npm **workspace** link (`"workspaces": ["packages/*"]` in root `package.json`, `"@thehammer/danx-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 (mirroring `dashboard-builder`/`frontend-builder` in `Dockerfile`) since the final image's `npm ci --omit=dev` never installs this package's own `devDependencies` (`typescript`). |
|
|
43
|
+
| **`danx-dashboard-mcp` desktop bridge** (separate npm-published package, DX-2954) | Ordinary npm dependency, **pinned to the exact published version** the worker's commit ships | The bridge is a genuinely separate installable artifact (`npx -y @thehammer/danx-dashboard-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. |
|
|
44
|
+
| **Dashboard server / dashboard SPA** (`src/dashboard/*`, `dashboard/*`) | Same npm dependency shape as the worker (dashboard's `src/` is root-repo code, so it resolves through the SAME root workspace link as the worker; `dashboard/` is its own separate npm package — DX-2963, the first real consumer there, adds an ordinary `dashboard/package.json` dependency, resolved by Vite/Vitest/`vue-tsc` through plain node_modules resolution, no alias needed) | No `@backend`-style alias is needed for an npm dependency the way one is needed for `dashboard/`'s reach into root `src/` — that alias exists because `dashboard/` is a SEPARATE package with no other way to reach root source; a real npm dependency resolves through ordinary node_modules lookup in every one of Vite's two configs, Vitest, `vue-tsc`, and the `dashboard-builder` Dockerfile stage (which already runs `cd dashboard && npm ci` AFTER the root repo — including `packages/`— is copied in, so the dependency is resolvable with zero Dockerfile change). `dashboard/` is FROZEN (`.claude/rules/ui-deprecation.md`) — this card adds no dependency there since nothing consumes it yet; DX-2963 adds the one line when it wires the real counter into `DispatchDetail.vue`/`DispatchList.vue`. |
|
|
45
|
+
|
|
46
|
+
## Release order (DX-2962 AC3)
|
|
47
|
+
|
|
48
|
+
1. Publish this package (`make publish-danx-session-telemetry`).
|
|
49
|
+
2. Publish `danx-dashboard-mcp` (only once it depends on this package — DX-2954).
|
|
50
|
+
3. Deploy the danxbot backend.
|
|
51
|
+
4. Move the plugin's bridge-version pin.
|
|
52
|
+
|
|
53
|
+
Publishing comes before deploying because the publish-drift gate
|
|
54
|
+
(`src/dispatch/publish-drift-gate.ts`) refuses a finalize whose bumped
|
|
55
|
+
`packages/*/package.json` version is not yet live on npm.
|
|
56
|
+
|
|
57
|
+
## Boot check
|
|
58
|
+
|
|
59
|
+
`npm run verify:boot` (wired into `make publish-danx-session-telemetry`,
|
|
60
|
+
mirroring `packages/danx-dashboard-mcp/scripts/verify-boot.sh`) packs the
|
|
61
|
+
freshly-built package with `npm pack`, installs the tarball into a clean
|
|
62
|
+
temp directory (the way an `npm install` consumer actually would — not a
|
|
63
|
+
relative `require` back into this checkout), and imports both the main
|
|
64
|
+
entry and the `./browser` subpath, calling one exported function from each.
|
|
65
|
+
A broken `exports` map, a missing `dist/` file, or an accidental `node:`
|
|
66
|
+
leak into `browser.ts` fails the publish before anything reaches npm.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962 — browser-safe subpath (`@thehammer/danx-session-telemetry/browser`).
|
|
3
|
+
*
|
|
4
|
+
* This file MUST NEVER import a `node:` builtin, directly or transitively —
|
|
5
|
+
* enforced by `src/__tests__/no-node-imports.test.ts`, which greps every
|
|
6
|
+
* file this module reaches. `session-log-watcher.ts` (the main entry's
|
|
7
|
+
* collector) imports `node:fs`/`node:fs/promises`/`node:os`/`node:path`, so
|
|
8
|
+
* it — and `internal-logger.ts` and `tool-summary.ts`, which it alone pulls
|
|
9
|
+
* in — are deliberately NOT re-exported here.
|
|
10
|
+
*
|
|
11
|
+
* Only the two type-only exports are browser-safe today: `AgentLogEntry` /
|
|
12
|
+
* `AgentLineage` carry no runtime code at all, so any consumer that needs
|
|
13
|
+
* to describe a transcript entry's SHAPE (the dashboard server, the
|
|
14
|
+
* dashboard SPA, a future browser-side usage display) can depend on this
|
|
15
|
+
* subpath with zero risk of pulling in a Node builtin.
|
|
16
|
+
*
|
|
17
|
+
* DX-2963 adds the first real runtime export here: the one usage counter
|
|
18
|
+
* and the pricing function, both pure arithmetic with no I/O. This card
|
|
19
|
+
* (DX-2962) only creates the subpath and its no-`node:`-imports guarantee.
|
|
20
|
+
*/
|
|
21
|
+
export type { AgentLogEntry, AgentLineage } from "./types.js";
|
|
22
|
+
export { truncStr } from "./tool-summary.js";
|
package/dist/browser.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962 — `@thehammer/danx-session-telemetry` main entry point.
|
|
3
|
+
*
|
|
4
|
+
* The shared Claude Code transcript collector: incremental tailing of the
|
|
5
|
+
* main and sub-agent JSONL transcripts, with no whole-transcript buffer
|
|
6
|
+
* (small per-reply bookkeeping only, kept in memory for the life of the
|
|
7
|
+
* process — see `session-log-watcher.ts`'s header). Consumed by the
|
|
8
|
+
* danxbot worker (Node, workspace-linked — see the README) and, from
|
|
9
|
+
* DX-2954, the `danx-dashboard-mcp` desktop bridge (Node, npm-installed at
|
|
10
|
+
* a pinned exact version).
|
|
11
|
+
*
|
|
12
|
+
* A browser-safe subset with NO `node:` imports lives at the `./browser`
|
|
13
|
+
* subpath (see `browser.ts`) for consumers that cannot import Node builtins
|
|
14
|
+
* — the dashboard server, the dashboard SPA, and any future browser-side
|
|
15
|
+
* usage display.
|
|
16
|
+
*/
|
|
17
|
+
export { SessionLogWatcher, DISPATCH_TAG_PREFIX, encodeClaudeProjectsCwd, deriveSessionDir, sessionUuidExistsInCleanRoom, findNewestJsonlFile, findSessionFileByDispatchId, convertJsonlEntry, } from "./session-log-watcher.js";
|
|
18
|
+
export type { EntryConsumer, SessionLogWatcherOptions, } from "./session-log-watcher.js";
|
|
19
|
+
export type { AgentLogEntry, AgentLineage } from "./types.js";
|
|
20
|
+
export { truncStr, summarizeToolInput, buildAssistantSummary, buildToolResultSummary, } from "./tool-summary.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962 — `@thehammer/danx-session-telemetry` main entry point.
|
|
3
|
+
*
|
|
4
|
+
* The shared Claude Code transcript collector: incremental tailing of the
|
|
5
|
+
* main and sub-agent JSONL transcripts, with no whole-transcript buffer
|
|
6
|
+
* (small per-reply bookkeeping only, kept in memory for the life of the
|
|
7
|
+
* process — see `session-log-watcher.ts`'s header). Consumed by the
|
|
8
|
+
* danxbot worker (Node, workspace-linked — see the README) and, from
|
|
9
|
+
* DX-2954, the `danx-dashboard-mcp` desktop bridge (Node, npm-installed at
|
|
10
|
+
* a pinned exact version).
|
|
11
|
+
*
|
|
12
|
+
* A browser-safe subset with NO `node:` imports lives at the `./browser`
|
|
13
|
+
* subpath (see `browser.ts`) for consumers that cannot import Node builtins
|
|
14
|
+
* — the dashboard server, the dashboard SPA, and any future browser-side
|
|
15
|
+
* usage display.
|
|
16
|
+
*/
|
|
17
|
+
export { SessionLogWatcher, DISPATCH_TAG_PREFIX, encodeClaudeProjectsCwd, deriveSessionDir, sessionUuidExistsInCleanRoom, findNewestJsonlFile, findSessionFileByDispatchId, convertJsonlEntry, } from "./session-log-watcher.js";
|
|
18
|
+
// `truncStr` is a generic, dependency-free string helper that moved here
|
|
19
|
+
// alongside the rest of `tool-summary.ts` (it has no other home now that
|
|
20
|
+
// file is gone from root) — re-exported for danxbot root consumers that
|
|
21
|
+
// truncate a string for display but have nothing else to do with the
|
|
22
|
+
// transcript collector, e.g. `src/dashboard/dispatch-scout.ts`.
|
|
23
|
+
export { truncStr, summarizeToolInput, buildAssistantSummary, buildToolResultSummary, } from "./tool-summary.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962: minimal package-local logging shim. The collector previously
|
|
3
|
+
* imported danxbot root's `src/logger.ts` (`createLogger`), but that file
|
|
4
|
+
* is root-repo-only and this package must never reach back into the root
|
|
5
|
+
* repo (see the package README "Isolation"). `createLogger`'s own
|
|
6
|
+
* consumers span ~300+ unrelated root files, so it is not something this
|
|
7
|
+
* extraction "moves" — only the three log levels the collector actually
|
|
8
|
+
* calls (`info`/`warn`/`error`) are reproduced here, deliberately smaller
|
|
9
|
+
* than the root abstraction it replaces for this package's own use.
|
|
10
|
+
*/
|
|
11
|
+
export interface Logger {
|
|
12
|
+
info(message: string): void;
|
|
13
|
+
warn(message: string, error?: unknown): void;
|
|
14
|
+
error(message: string, error?: unknown): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createLogger(component: string): Logger;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function emit(level, component, message, error) {
|
|
2
|
+
const entry = {
|
|
3
|
+
timestamp: new Date().toISOString(),
|
|
4
|
+
level,
|
|
5
|
+
component,
|
|
6
|
+
message,
|
|
7
|
+
};
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
entry.error = error.message;
|
|
10
|
+
entry.stack = error.stack;
|
|
11
|
+
}
|
|
12
|
+
else if (error !== undefined) {
|
|
13
|
+
entry.error = String(error);
|
|
14
|
+
}
|
|
15
|
+
const writer = level === "info" ? console.log : console.error;
|
|
16
|
+
writer(JSON.stringify(entry));
|
|
17
|
+
}
|
|
18
|
+
export function createLogger(component) {
|
|
19
|
+
return {
|
|
20
|
+
info: (message) => emit("info", component, message),
|
|
21
|
+
warn: (message, error) => emit("warn", component, message, error),
|
|
22
|
+
error: (message, error) => emit("error", component, message, error),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionLogWatcher — Polls Claude Code JSONL session files for new entries.
|
|
3
|
+
*
|
|
4
|
+
* DX-2962: moved unchanged from danxbot root `src/agent/session-log-watcher.ts`
|
|
5
|
+
* into this shared published package, so the danxbot worker and the
|
|
6
|
+
* `danx-dashboard-mcp` desktop bridge (DX-2954) run identical collector
|
|
7
|
+
* code. Only the imports changed — `../logger.js` -> `./internal-logger.js`
|
|
8
|
+
* (this package's own tiny logging shim, since root's `logger.ts` has ~300
|
|
9
|
+
* unrelated consumers and does not move), `../types.js` -> `./types.js`
|
|
10
|
+
* (moved — see that file's own header).
|
|
11
|
+
*
|
|
12
|
+
* Claude Code writes:
|
|
13
|
+
* - Parent session: ~/.claude/projects/<cwd-path>/<session-uuid>.jsonl
|
|
14
|
+
* - Sub-agent sessions: ~/.claude/projects/<cwd-path>/<session-uuid>/subagents/agent-<hash>.jsonl
|
|
15
|
+
* + agent-<hash>.meta.json sidecar with {agentType, description}
|
|
16
|
+
*
|
|
17
|
+
* The watcher tails the parent file and all sub-agent files in parallel, parses
|
|
18
|
+
* new lines, converts them to AgentLogEntry, and emits to registered consumers.
|
|
19
|
+
* Sub-agent entries are decorated with lineage metadata (subagent_id,
|
|
20
|
+
* parent_session_id, agent_type) inside `data` so downstream consumers can
|
|
21
|
+
* attribute usage correctly — they flow under the SAME dispatch, not a separate session.
|
|
22
|
+
*/
|
|
23
|
+
import type { AgentLogEntry } from "./types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Tag prefix injected into the piped prompt by the launcher. The watcher scans
|
|
26
|
+
* JSONL files for this tag to deterministically find the correct session file
|
|
27
|
+
* for a given dispatch, avoiding mtime-based heuristics that can pick up the
|
|
28
|
+
* wrong file (e.g., an active human session).
|
|
29
|
+
*/
|
|
30
|
+
export declare const DISPATCH_TAG_PREFIX = "<!-- danxbot-dispatch:";
|
|
31
|
+
export type EntryConsumer = (entry: AgentLogEntry) => void;
|
|
32
|
+
export interface SessionLogWatcherOptions {
|
|
33
|
+
/** Working directory the agent was launched in. Used to derive the session directory. */
|
|
34
|
+
cwd: string;
|
|
35
|
+
/** How often to poll for new entries in milliseconds. Default: 5000. */
|
|
36
|
+
pollIntervalMs?: number;
|
|
37
|
+
/** Specific session ID to watch. If omitted, watches the newest .jsonl file. */
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
/** Override the session directory path (for testing). */
|
|
40
|
+
sessionDir?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Dispatch job ID to search for in JSONL file content.
|
|
43
|
+
* When set, the watcher scans files for the `<!-- danxbot-dispatch:<id> -->` tag
|
|
44
|
+
* instead of picking the newest file by mtime. This prevents picking up the wrong
|
|
45
|
+
* session file (e.g., an active human session) when the agent's file doesn't exist yet.
|
|
46
|
+
*/
|
|
47
|
+
dispatchId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Skip every byte on disk at attach time and tail strictly forward from
|
|
50
|
+
* EOF. Set whenever the bytes already on disk belong to a PRIOR dispatch's
|
|
51
|
+
* accounting, so replaying them would double-count that dispatch's usage /
|
|
52
|
+
* tool calls against this one:
|
|
53
|
+
* - the Phase 2c (DX-209) reattach pass — a prior worker incarnation
|
|
54
|
+
* already accumulated the JSONL into the row's counters; and
|
|
55
|
+
* - a `claude --resume <id>` resume (DX-2583) — claude appends this
|
|
56
|
+
* dispatch's turns to the SAME session JSONL the prior dispatch(es)
|
|
57
|
+
* already filled, so tailing from byte 0 re-sums the whole session's
|
|
58
|
+
* history onto a fresh row (`attach-monitoring-stack.ts` sets this via
|
|
59
|
+
* `seedFromEof` when `resumeSessionId` is present).
|
|
60
|
+
* Default `false` — a genuinely fresh spawn writes a brand-new file whose
|
|
61
|
+
* EOF is 0, so it ingests its own full file from the start.
|
|
62
|
+
*/
|
|
63
|
+
fromEof?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Encode a working directory to the subdir name Claude Code writes JSONL
|
|
67
|
+
* under. Replaces `/`, `.`, AND `_` with `-`. Verified empirically against
|
|
68
|
+
* on-disk entries: a dispatched-agent cwd like
|
|
69
|
+
* `/var/tmp/danxbot-clean-room/<hash>/repo__board__work__dani__DX-1` encodes to
|
|
70
|
+
* `-var-tmp-danxbot-clean-room-<hash>-repo--board--work--dani--DX-1` — each
|
|
71
|
+
* `__` key delimiter collapses to `--`.
|
|
72
|
+
*
|
|
73
|
+
* `realpathSync` runs first so the encoding matches what claude's own
|
|
74
|
+
* resolver produces — the agent's cwd is realpath'd by the kernel before
|
|
75
|
+
* claude reads `process.cwd()`. ENOENT falls back to the normalized path
|
|
76
|
+
* so callers can pre-compute encodings before the dir exists (e.g. unit
|
|
77
|
+
* tests that exercise the pure encoding contract).
|
|
78
|
+
*
|
|
79
|
+
* The `.`/`_` encoding matters: a hidden-dir segment (leading `.`) or a
|
|
80
|
+
* clean-room key's `__` delimiter that the encoder does NOT collapse produces
|
|
81
|
+
* a directory Claude Code never writes to — silently breaking
|
|
82
|
+
* SessionLogWatcher attachment for every host-mode dispatch. The
|
|
83
|
+
* separator-encoding regression tests below pin this.
|
|
84
|
+
*
|
|
85
|
+
* Host mode (claude 2.1.104+, GitHub #51488 — symlinks under
|
|
86
|
+
* `~/.claude/projects/` are no longer followed) lets claude write its
|
|
87
|
+
* transcript to this exact realpath-encoded dir as a REAL directory; the
|
|
88
|
+
* watcher reads the same path, so they converge with no symlink. Docker
|
|
89
|
+
* binds `<repo>/claude-projects/` at `/home/danxbot/.claude/projects/` and
|
|
90
|
+
* relies on the same encoding for the bound subdir name. Keep the encoding
|
|
91
|
+
* logic centralized here so both runtimes stay in lockstep.
|
|
92
|
+
*/
|
|
93
|
+
export declare function encodeClaudeProjectsCwd(cwd: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Derives the Claude Code session directory from a working directory path.
|
|
96
|
+
* Claude Code stores sessions at ~/.claude/projects/<encoded-cwd>/. See
|
|
97
|
+
* `encodeClaudeProjectsCwd` for the encoding contract.
|
|
98
|
+
*/
|
|
99
|
+
export declare function deriveSessionDir(cwd: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* On-disk existence gate for a stored, resumable Claude session UUID
|
|
102
|
+
* (migration 061). Given the clean-room `cwd` a dispatch ran in and a
|
|
103
|
+
* session `uuid`, returns `true` iff `<uuid>.jsonl` lives under that cwd's
|
|
104
|
+
* encoded `~/.claude/projects/<encoded>/` dir.
|
|
105
|
+
*
|
|
106
|
+
* This is the deterministic precondition the issue-bound resume gates check
|
|
107
|
+
* BEFORE passing `--resume <uuid>`: a session pinned in cwd A is only
|
|
108
|
+
* resumable from cwd A (Claude Code keys JSONLs by encoded-cwd), so a uuid
|
|
109
|
+
* stored on a card but orphaned by a cwd / agent move resolves here to a
|
|
110
|
+
* different encoded dir, is not found, and the caller fails SOFT to a fresh
|
|
111
|
+
* launch. This is NOT a swallowed error (which fail-loud forbids) — it is a
|
|
112
|
+
* checked precondition: a genuinely-absent session legitimately means
|
|
113
|
+
* "start fresh", and the per-agent isolation DX-1071 enforced falls out for
|
|
114
|
+
* free (agent B's cwd never contains agent A's session file).
|
|
115
|
+
*/
|
|
116
|
+
export declare function sessionUuidExistsInCleanRoom(cwd: string, uuid: string): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Finds the newest .jsonl file in a directory.
|
|
119
|
+
* Returns the full path, or null if no .jsonl files exist.
|
|
120
|
+
*/
|
|
121
|
+
export declare function findNewestJsonlFile(dir: string, sessionId?: string): Promise<string | null>;
|
|
122
|
+
/**
|
|
123
|
+
* Finds a JSONL session file that contains the dispatch tag for a specific job ID.
|
|
124
|
+
*
|
|
125
|
+
* Scans each `.jsonl` file for the dispatch tag
|
|
126
|
+
* `<!-- danxbot-dispatch:<dispatchId> -->`, injected into the piped prompt
|
|
127
|
+
* by the launcher.
|
|
128
|
+
*
|
|
129
|
+
* The tag's position depends on whether this is a fresh spawn or a
|
|
130
|
+
* `claude --resume`:
|
|
131
|
+
* - **Fresh** — the tag is in the FIRST `user` entry, near the head of a
|
|
132
|
+
* brand-new file.
|
|
133
|
+
* - **Resume (DX-1106)** — claude appends the new dispatch's tagged
|
|
134
|
+
* prompt to the END of the prior session's already-multi-MB JSONL, so
|
|
135
|
+
* the tag sits in the TAIL, far past any fixed head window. A
|
|
136
|
+
* head-only scan never matches a resumed session → the watcher gives
|
|
137
|
+
* up after 180s → no session_uuid stamp, no heartbeat, no stall
|
|
138
|
+
* detection for that dispatch.
|
|
139
|
+
*
|
|
140
|
+
* So we scan BOTH a head window and a tail window (they overlap, covering
|
|
141
|
+
* the whole file, for any file ≤ 2×`SCAN_BYTES`). A resumed tag always
|
|
142
|
+
* lands in the freshly-appended tail; a fresh tag always lands in the head.
|
|
143
|
+
*
|
|
144
|
+
* Returns the full path to the matching file, or null if no file matches.
|
|
145
|
+
*/
|
|
146
|
+
export declare function findSessionFileByDispatchId(dir: string, dispatchId: string): Promise<string | null>;
|
|
147
|
+
/**
|
|
148
|
+
* Converts a raw JSONL entry from Claude Code's session log into an AgentLogEntry
|
|
149
|
+
* used by downstream consumers (stall detection, event forwarding).
|
|
150
|
+
*/
|
|
151
|
+
export declare function convertJsonlEntry(raw: Record<string, unknown>, lastTimestamp: number): {
|
|
152
|
+
entry: AgentLogEntry;
|
|
153
|
+
timestamp: number;
|
|
154
|
+
} | null;
|
|
155
|
+
export declare class SessionLogWatcher {
|
|
156
|
+
private consumers;
|
|
157
|
+
private pollTimer;
|
|
158
|
+
private parentTail;
|
|
159
|
+
private subagentsDir;
|
|
160
|
+
private subagentTails;
|
|
161
|
+
private parentSessionId;
|
|
162
|
+
private sessionDir;
|
|
163
|
+
private sessionId;
|
|
164
|
+
private dispatchId;
|
|
165
|
+
private pollIntervalMs;
|
|
166
|
+
private fromEof;
|
|
167
|
+
private running;
|
|
168
|
+
private polling;
|
|
169
|
+
private initSynthesized;
|
|
170
|
+
constructor(options: SessionLogWatcherOptions);
|
|
171
|
+
/** Register a consumer callback that receives each new AgentLogEntry. */
|
|
172
|
+
onEntry(consumer: EntryConsumer): void;
|
|
173
|
+
/** Returns the discovered parent session file path, or null if not yet found. */
|
|
174
|
+
getSessionFilePath(): string | null;
|
|
175
|
+
/**
|
|
176
|
+
* Resolve this dispatch's JSONL session file by a one-shot disk scan,
|
|
177
|
+
* independent of the live tail loop (DX-1201).
|
|
178
|
+
*
|
|
179
|
+
* The live watcher records `session_uuid` lazily from the FIRST emitted
|
|
180
|
+
* entry. If discovery ever fails to attach within the dispatch's lifetime —
|
|
181
|
+
* a transient `open`/`stat`/`read` error on a busy shared session dir, or a
|
|
182
|
+
* timing window where the spawn finalizes before the first poll observed an
|
|
183
|
+
* entry — that lazy path never fires and the row's `session_uuid` stays
|
|
184
|
+
* NULL, breaking same-thread Slack resume (the dispatch is not resumable).
|
|
185
|
+
*
|
|
186
|
+
* This method gives the finalize backstop a deterministic, cause-agnostic
|
|
187
|
+
* way to resolve the file: by the time a dispatch finalizes, the agent's
|
|
188
|
+
* `danxbot_complete` tool_use is already written to the JSONL, so the file
|
|
189
|
+
* is guaranteed present on disk. It scans the watcher's OWN `sessionDir`
|
|
190
|
+
* (the dir derived from `agentCwd` at construction — the same dir claude
|
|
191
|
+
* wrote to) for the dispatch tag. Returns the file path, or null when no
|
|
192
|
+
* dispatchId is set or no file carries the tag.
|
|
193
|
+
*
|
|
194
|
+
* Prefers the already-discovered `parentTail.filePath` when the live loop
|
|
195
|
+
* did attach — no redundant scan on the happy path.
|
|
196
|
+
*/
|
|
197
|
+
resolveSessionFileNow(): Promise<string | null>;
|
|
198
|
+
/** Start polling for session log entries. */
|
|
199
|
+
start(): Promise<void>;
|
|
200
|
+
/** Stop polling and clean up. */
|
|
201
|
+
stop(): void;
|
|
202
|
+
/**
|
|
203
|
+
* Read any JSONL bytes appended since the last scheduled poll tick. Used by
|
|
204
|
+
* the launcher's cleanup path to capture the agent's final assistant entry
|
|
205
|
+
* (the one carrying the closing `usage` block + the `tool_use` for
|
|
206
|
+
* `danxbot_complete`) before stop() halts polling. Without this, the row
|
|
207
|
+
* finalized by `DispatchTracker.finalize` snapshots stale `job.usage` and
|
|
208
|
+
* the dispatches table undercounts every token + counter field by exactly
|
|
209
|
+
* what the JSONL grew between ticks.
|
|
210
|
+
*
|
|
211
|
+
* Race contract: if a scheduled `poll()` is already in flight when drain
|
|
212
|
+
* is called, the first poll() invoked here would early-return at the
|
|
213
|
+
* `this.polling` guard and silently miss the tail. To avoid that silent
|
|
214
|
+
* skip we yield until the in-flight poll completes, then run our own
|
|
215
|
+
* poll. The combined effect is "every byte written before drain returns
|
|
216
|
+
* has been observed" — exactly the guarantee finalize depends on.
|
|
217
|
+
*
|
|
218
|
+
* No-op when the parent file has not been discovered (drain before
|
|
219
|
+
* start, or start aborted) — there is nothing to read. Safe to call
|
|
220
|
+
* after `stop()`; returns immediately.
|
|
221
|
+
*/
|
|
222
|
+
drain(): Promise<void>;
|
|
223
|
+
/**
|
|
224
|
+
* Discover the JSONL session file. Polls until found or max attempts reached.
|
|
225
|
+
* For dispatched agents, the file appears shortly after the process starts.
|
|
226
|
+
*/
|
|
227
|
+
private discoverSessionFile;
|
|
228
|
+
/**
|
|
229
|
+
* Adopt a parent session file and derive the sub-agent directory.
|
|
230
|
+
*
|
|
231
|
+
* When `this.fromEof` is set (Phase 2c reattach OR a DX-2583 resume — see
|
|
232
|
+
* the option's doc), seed `byteOffset` to the file's current size so the
|
|
233
|
+
* next poll reads strictly forward — historical entries are NOT re-emitted.
|
|
234
|
+
* A fresh `stat` failure (race
|
|
235
|
+
* with a delete that happens between discover and adopt) falls back to
|
|
236
|
+
* 0 so the watcher tails from the start; that is preferable to throwing
|
|
237
|
+
* during boot. `fromEof` does NOT skip sub-agent files: any sub-agent
|
|
238
|
+
* file claude writes AFTER reattach is genuinely new and should be
|
|
239
|
+
* tailed normally.
|
|
240
|
+
*/
|
|
241
|
+
private setParentFile;
|
|
242
|
+
/**
|
|
243
|
+
* Poll all known files: the parent session, then re-enumerate + tail sub-agent files.
|
|
244
|
+
* Guarded against concurrent polls and against stop() racing an in-flight poll.
|
|
245
|
+
*/
|
|
246
|
+
private poll;
|
|
247
|
+
/** Read any new bytes from `state.filePath` and emit resulting entries. */
|
|
248
|
+
private pollFile;
|
|
249
|
+
/**
|
|
250
|
+
* Re-enumerate the sub-agent directory and add tail states for any
|
|
251
|
+
* `agent-<hash>.jsonl` files not already being tailed.
|
|
252
|
+
*/
|
|
253
|
+
private discoverNewSubagentFiles;
|
|
254
|
+
/** Centralized lineage refresh — see `refreshLineage` for the per-state body. */
|
|
255
|
+
private refreshPendingAgentTypes;
|
|
256
|
+
/**
|
|
257
|
+
* Refresh `state.lineage` by re-reading the meta sidecar and back-filling
|
|
258
|
+
* parent_session_id from the most recent parent observation. Idempotent:
|
|
259
|
+
* once `agent_type` is set it stays set (never read meta twice). Used by
|
|
260
|
+
* both the centralized `refreshPendingAgentTypes` and the inline call from
|
|
261
|
+
* `pollFile` — the inline call is the defense-in-depth that closes the race
|
|
262
|
+
* where bytes appear before `refreshPendingAgentTypes` has a chance to run.
|
|
263
|
+
*/
|
|
264
|
+
private refreshLineage;
|
|
265
|
+
/**
|
|
266
|
+
* Read agentType from the `<subagentId>.meta.json` sidecar. Returns
|
|
267
|
+
* undefined only for ENOENT (file not written yet). Malformed JSON or
|
|
268
|
+
* permission errors are logged at WARN and return undefined — the watcher
|
|
269
|
+
* must not crash on bad sidecar files, but the failure is visible in logs.
|
|
270
|
+
*/
|
|
271
|
+
private readAgentType;
|
|
272
|
+
/** Process raw text from a file tail, handling partial lines across poll ticks. */
|
|
273
|
+
private processText;
|
|
274
|
+
/**
|
|
275
|
+
* Convert a raw JSONL entry and emit to consumers.
|
|
276
|
+
*
|
|
277
|
+
* Init synthesis (parent stream only): the persisted JSONL transcript never
|
|
278
|
+
* carries a `system/init` line in ANY runtime — Claude Code (2.1.201) only
|
|
279
|
+
* streams that event over stdout (`--output-format stream-json`), which the
|
|
280
|
+
* docker spawn path intentionally never reads (`spawn-docker-mode.ts`), and
|
|
281
|
+
* host's interactive JSONL never wrote one either. So we ALWAYS synthesize
|
|
282
|
+
* one from the first assistant message's sessionId and model, on host AND
|
|
283
|
+
* docker alike. Sub-agent streams never synthesize — their entries flow
|
|
284
|
+
* under the parent dispatch's existing session.
|
|
285
|
+
*
|
|
286
|
+
* IMPORTANT (DX-651): synthesized init events carry `tools: []` and
|
|
287
|
+
* `data.synthesized: true` — this watcher never observes the real
|
|
288
|
+
* MCP-registered tool / skill / agent list for EITHER runtime. Any consumer
|
|
289
|
+
* that wants to check what actually loaded (tools, skills, agents) MUST
|
|
290
|
+
* skip an entry carrying `data.synthesized === true` — treating it as a
|
|
291
|
+
* truthful docker `system/init` (as `declared-loaded-observer.ts` used to,
|
|
292
|
+
* gating on runtime alone) manufactures a guaranteed false
|
|
293
|
+
* declared-not-loaded drift for every dispatch with a non-empty declared
|
|
294
|
+
* skill/agent set. The pre-launch probe in `mcp-server-probe.ts` is the
|
|
295
|
+
* tool-registration check that works across both runtimes.
|
|
296
|
+
*/
|
|
297
|
+
private handleRawEntry;
|
|
298
|
+
/**
|
|
299
|
+
* Emit an entry to registered consumers. For sub-agent streams,
|
|
300
|
+
* `state.lineage` decorates `entry.data` so the emitted EventPayload
|
|
301
|
+
* (via mapEntryToEvents) inherits subagent_id/parent_session_id/agent_type.
|
|
302
|
+
*
|
|
303
|
+
* DX-2962: no longer also pushes onto an internal `this.entries` array —
|
|
304
|
+
* the pre-extraction watcher kept every entry alive for the life of the
|
|
305
|
+
* process purely so `getEntries()` (deleted) could hand it back to three
|
|
306
|
+
* tests; nothing else read it. Every real consumer subscribes via
|
|
307
|
+
* `onEntry` instead.
|
|
308
|
+
*/
|
|
309
|
+
private emitEntry;
|
|
310
|
+
}
|