crewly 1.20.73 → 1.20.75
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/dist/backend/backend/src/constants.d.ts +64 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +64 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +49 -0
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +119 -0
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.js +8 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +55 -5
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts +61 -0
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.js +111 -2
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts +261 -0
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-session.service.js +432 -0
- package/dist/backend/backend/src/services/browser/browser-session.service.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.d.ts +221 -0
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.js +484 -0
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/model-pricing.d.ts +97 -0
- package/dist/backend/backend/src/services/monitoring/model-pricing.d.ts.map +1 -0
- package/dist/backend/backend/src/services/monitoring/model-pricing.js +120 -0
- package/dist/backend/backend/src/services/monitoring/model-pricing.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +8 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +64 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +64 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts +61 -0
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.js +111 -2
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +8 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/frontend/dist/assets/{index-26a16b91.css → index-a6480a0f.css} +1 -1
- package/frontend/dist/assets/{index-320b73bf.js → index-cc910352.js} +259 -259
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Transcript Sync Service
|
|
3
|
+
*
|
|
4
|
+
* Keeps the Usage dashboard honest for `claude-code` agents by reading the
|
|
5
|
+
* exact per-turn `usage` block that Claude Code writes into its own session
|
|
6
|
+
* transcript, and feeding it into {@link TokenUsageService}.
|
|
7
|
+
*
|
|
8
|
+
* This replaces a boot-time sync that never recorded anything. That version
|
|
9
|
+
* had four defects, each of which this module exists to avoid:
|
|
10
|
+
*
|
|
11
|
+
* 1. It derived the transcript directory from `CREWLY_HOME`, but agents run
|
|
12
|
+
* with their project as the working directory, so it always read an empty
|
|
13
|
+
* or absent folder. We take each session's real `cwd` from
|
|
14
|
+
* {@link SessionStatePersistence} instead.
|
|
15
|
+
* 2. It ran once at startup, so nothing an agent did afterwards was counted.
|
|
16
|
+
* We poll on an interval and read only the bytes appended since last time.
|
|
17
|
+
* 3. It keyed records on the Claude conversation UUID and re-added the whole
|
|
18
|
+
* running total on every call, so a restart double-counted the entire
|
|
19
|
+
* history. We key on the Crewly session name and keep a byte cursor plus a
|
|
20
|
+
* recent-message-id set so a turn is counted exactly once.
|
|
21
|
+
* 4. Its price table stopped at an older model generation, so current models
|
|
22
|
+
* silently fell through to the sonnet default. Pricing now lives in
|
|
23
|
+
* {@link module:services/monitoring/model-pricing} and reports whether a
|
|
24
|
+
* rate was an exact match.
|
|
25
|
+
*
|
|
26
|
+
* As a side effect the sync knows each agent's true context size — the sum of
|
|
27
|
+
* fresh, cached and cache-written input on its most recent turn — which is the
|
|
28
|
+
* only reliable source for it, since Claude Code's TUI context readout is not
|
|
29
|
+
* capturable from PTY scrollback. That number is handed to an observer so the
|
|
30
|
+
* context-window monitor can act on real figures.
|
|
31
|
+
*
|
|
32
|
+
* @module services/monitoring/claude-transcript-sync
|
|
33
|
+
*/
|
|
34
|
+
import { promises as fs } from 'fs';
|
|
35
|
+
import * as path from 'path';
|
|
36
|
+
import * as os from 'os';
|
|
37
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
38
|
+
import { getSessionStatePersistence } from '../session/session-state-persistence.js';
|
|
39
|
+
import { TokenUsageService } from './token-usage.service.js';
|
|
40
|
+
import { calculateCost } from './model-pricing.js';
|
|
41
|
+
import { CLAUDE_TRANSCRIPT_SYNC_CONSTANTS } from '../../constants.js';
|
|
42
|
+
import { encodeProjectSlug } from './claude-session-tokens.service.js';
|
|
43
|
+
/**
|
|
44
|
+
* Reads Claude Code transcripts on an interval and records exact token usage.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const sync = ClaudeTranscriptSyncService.getInstance();
|
|
49
|
+
* sync.onContextReading((r) => monitor.updateContextTokens(r.sessionName, r.contextTokens));
|
|
50
|
+
* await sync.start();
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class ClaudeTranscriptSyncService {
|
|
54
|
+
static instance = null;
|
|
55
|
+
logger;
|
|
56
|
+
cursorFile;
|
|
57
|
+
/**
|
|
58
|
+
* Home directory that `~/.claude/projects` is resolved against.
|
|
59
|
+
*
|
|
60
|
+
* Injectable because `os.homedir()` does not reliably follow a `$HOME`
|
|
61
|
+
* override once the process has already resolved it, which makes it
|
|
62
|
+
* untestable against a temp tree.
|
|
63
|
+
*/
|
|
64
|
+
homeDir;
|
|
65
|
+
cursors = new Map();
|
|
66
|
+
timer = null;
|
|
67
|
+
observers = [];
|
|
68
|
+
loaded = false;
|
|
69
|
+
/** Guards against a slow pass overlapping the next tick. */
|
|
70
|
+
running = false;
|
|
71
|
+
/**
|
|
72
|
+
* @param cursorFile - Override the cursor persistence path (tests)
|
|
73
|
+
* @param homeDir - Override the home directory transcripts live under (tests)
|
|
74
|
+
*/
|
|
75
|
+
constructor(cursorFile, homeDir) {
|
|
76
|
+
this.logger = LoggerService.getInstance().createComponentLogger('ClaudeTranscriptSync');
|
|
77
|
+
this.homeDir = homeDir ?? os.homedir();
|
|
78
|
+
this.cursorFile =
|
|
79
|
+
cursorFile ?? path.join(this.homeDir, '.crewly', CLAUDE_TRANSCRIPT_SYNC_CONSTANTS.CURSOR_FILE);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the shared instance.
|
|
83
|
+
*
|
|
84
|
+
* @returns The singleton service
|
|
85
|
+
*/
|
|
86
|
+
static getInstance() {
|
|
87
|
+
if (!ClaudeTranscriptSyncService.instance) {
|
|
88
|
+
ClaudeTranscriptSyncService.instance = new ClaudeTranscriptSyncService();
|
|
89
|
+
}
|
|
90
|
+
return ClaudeTranscriptSyncService.instance;
|
|
91
|
+
}
|
|
92
|
+
/** Drops the singleton and stops its timer (tests). */
|
|
93
|
+
static resetInstance() {
|
|
94
|
+
ClaudeTranscriptSyncService.instance?.stop();
|
|
95
|
+
ClaudeTranscriptSyncService.instance = null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Register a callback to receive each session's context size per pass.
|
|
99
|
+
*
|
|
100
|
+
* @param observer - Invoked once per session that produced a new turn
|
|
101
|
+
*/
|
|
102
|
+
onContextReading(observer) {
|
|
103
|
+
this.observers.push(observer);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Load cursors, run one pass immediately, then poll.
|
|
107
|
+
*
|
|
108
|
+
* Safe to call twice; the second call is a no-op.
|
|
109
|
+
*/
|
|
110
|
+
async start() {
|
|
111
|
+
if (this.timer)
|
|
112
|
+
return;
|
|
113
|
+
await this.loadCursors();
|
|
114
|
+
await this.sync();
|
|
115
|
+
this.timer = setInterval(() => {
|
|
116
|
+
void this.sync();
|
|
117
|
+
}, CLAUDE_TRANSCRIPT_SYNC_CONSTANTS.SYNC_INTERVAL_MS);
|
|
118
|
+
// Never hold the event loop open for a metrics poll.
|
|
119
|
+
this.timer.unref?.();
|
|
120
|
+
this.logger.info('Claude transcript sync started', {
|
|
121
|
+
intervalMs: CLAUDE_TRANSCRIPT_SYNC_CONSTANTS.SYNC_INTERVAL_MS,
|
|
122
|
+
cursorFile: this.cursorFile,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Stops polling. Cursors already on disk are left alone. */
|
|
126
|
+
stop() {
|
|
127
|
+
if (this.timer) {
|
|
128
|
+
clearInterval(this.timer);
|
|
129
|
+
this.timer = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Run one sync pass across every registered claude-code session.
|
|
134
|
+
*
|
|
135
|
+
* Overlapping calls are dropped rather than queued — a pass that outruns
|
|
136
|
+
* the interval means the machine is busy, and piling up reads of
|
|
137
|
+
* multi-megabyte transcripts would make that worse.
|
|
138
|
+
*
|
|
139
|
+
* @returns What the pass counted
|
|
140
|
+
*/
|
|
141
|
+
async sync() {
|
|
142
|
+
const result = {
|
|
143
|
+
sessionsUpdated: 0,
|
|
144
|
+
turnsCounted: 0,
|
|
145
|
+
costAdded: 0,
|
|
146
|
+
sessionsWithoutTranscript: 0,
|
|
147
|
+
};
|
|
148
|
+
if (this.running)
|
|
149
|
+
return result;
|
|
150
|
+
this.running = true;
|
|
151
|
+
try {
|
|
152
|
+
if (!this.loaded)
|
|
153
|
+
await this.loadCursors();
|
|
154
|
+
const sessions = getSessionStatePersistence().getRegisteredSessionsMap();
|
|
155
|
+
// Several agents commonly work in one repo. Count them up front so
|
|
156
|
+
// transcript resolution knows when a directory cannot identify a
|
|
157
|
+
// single agent.
|
|
158
|
+
const perCwd = new Map();
|
|
159
|
+
for (const info of sessions.values()) {
|
|
160
|
+
if (info.runtimeType !== 'claude-code' || !info.cwd)
|
|
161
|
+
continue;
|
|
162
|
+
perCwd.set(info.cwd, (perCwd.get(info.cwd) ?? 0) + 1);
|
|
163
|
+
}
|
|
164
|
+
for (const [sessionName, info] of sessions) {
|
|
165
|
+
// Only Claude Code writes the transcripts this service reads.
|
|
166
|
+
if (info.runtimeType !== 'claude-code')
|
|
167
|
+
continue;
|
|
168
|
+
if (!info.cwd)
|
|
169
|
+
continue;
|
|
170
|
+
const filePath = await this.resolveTranscript(info.cwd, info.claudeSessionId, (perCwd.get(info.cwd) ?? 0) > 1);
|
|
171
|
+
if (!filePath) {
|
|
172
|
+
result.sessionsWithoutTranscript += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const counted = await this.syncOne(sessionName, filePath);
|
|
176
|
+
if (counted.turns > 0) {
|
|
177
|
+
result.sessionsUpdated += 1;
|
|
178
|
+
result.turnsCounted += counted.turns;
|
|
179
|
+
result.costAdded += counted.cost;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// No new turns, but the agent is still carrying whatever it
|
|
183
|
+
// was carrying. Re-announce it so a monitor that started
|
|
184
|
+
// after the first reading still learns the figure.
|
|
185
|
+
const known = this.cursors.get(sessionName)?.lastContextTokens;
|
|
186
|
+
if (known !== undefined) {
|
|
187
|
+
this.emitContext({ sessionName, contextTokens: known, model: '' });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (result.sessionsUpdated > 0) {
|
|
192
|
+
await this.saveCursors();
|
|
193
|
+
this.logger.info('Synced Claude transcripts', result);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
// A metrics poll must never take the server down.
|
|
198
|
+
this.logger.warn('Claude transcript sync pass failed', {
|
|
199
|
+
error: err instanceof Error ? err.message : String(err),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
this.running = false;
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Locate a session's transcript file.
|
|
209
|
+
*
|
|
210
|
+
* Prefers the conversation id Crewly recorded when it launched the agent.
|
|
211
|
+
* Falls back to the newest transcript in the project's slug directory,
|
|
212
|
+
* which covers an agent whose id has not been captured yet.
|
|
213
|
+
*
|
|
214
|
+
* @param cwd - The agent's working directory
|
|
215
|
+
* @param claudeSessionId - Conversation id, when known
|
|
216
|
+
* @param cwdIsShared - Whether another registered session works in the
|
|
217
|
+
* same directory, which makes newest-wins unsafe
|
|
218
|
+
* @returns Absolute path, or null when nothing readable exists
|
|
219
|
+
*/
|
|
220
|
+
async resolveTranscript(cwd, claudeSessionId, cwdIsShared) {
|
|
221
|
+
const dir = path.join(this.homeDir, '.claude', 'projects', encodeProjectSlug(cwd));
|
|
222
|
+
if (claudeSessionId) {
|
|
223
|
+
const direct = path.join(dir, `${claudeSessionId}.jsonl`);
|
|
224
|
+
try {
|
|
225
|
+
await fs.access(direct);
|
|
226
|
+
return direct;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Recorded id has no file — fall through to newest-wins.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Newest transcript in the project directory. Covers an agent whose
|
|
233
|
+
// conversation id Crewly has not captured yet.
|
|
234
|
+
//
|
|
235
|
+
// Only safe when this agent is the sole one working in this directory.
|
|
236
|
+
// Several agents in one repo share a slug directory, so newest-wins
|
|
237
|
+
// would hand all of them the same transcript and attribute one busy
|
|
238
|
+
// agent's turns — and its context size — to every one of its
|
|
239
|
+
// colleagues. Observed exactly that: four agents each reported the
|
|
240
|
+
// same 726,475 tokens, which was one agent's figure. Waiting for the
|
|
241
|
+
// conversation id costs a little early data; guessing costs the
|
|
242
|
+
// dashboard its meaning.
|
|
243
|
+
if (cwdIsShared)
|
|
244
|
+
return null;
|
|
245
|
+
let files;
|
|
246
|
+
try {
|
|
247
|
+
files = await fs.readdir(dir);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
let newest = '';
|
|
253
|
+
let newestMtime = -1;
|
|
254
|
+
for (const file of files) {
|
|
255
|
+
if (!file.endsWith('.jsonl'))
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
const { mtimeMs } = await fs.stat(path.join(dir, file));
|
|
259
|
+
if (mtimeMs > newestMtime) {
|
|
260
|
+
newestMtime = mtimeMs;
|
|
261
|
+
newest = file;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return newest ? path.join(dir, newest) : null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Consume the unread tail of one transcript and record what it holds.
|
|
272
|
+
*
|
|
273
|
+
* @param sessionName - Crewly session name, used as the usage key
|
|
274
|
+
* @param filePath - Transcript to read
|
|
275
|
+
* @returns Turns counted and cost added for this session
|
|
276
|
+
*/
|
|
277
|
+
async syncOne(sessionName, filePath) {
|
|
278
|
+
let cursor = this.cursors.get(sessionName);
|
|
279
|
+
// A different transcript means the agent was given a new conversation
|
|
280
|
+
// (a fresh session id, not a resume). Start its cursor over, but keep
|
|
281
|
+
// the cost so the dashboard shows the agent's lifetime spend.
|
|
282
|
+
if (!cursor || cursor.filePath !== filePath) {
|
|
283
|
+
cursor = { filePath, offset: 0, seenMessageIds: [], cost: cursor?.cost ?? 0 };
|
|
284
|
+
this.cursors.set(sessionName, cursor);
|
|
285
|
+
}
|
|
286
|
+
let size;
|
|
287
|
+
try {
|
|
288
|
+
size = (await fs.stat(filePath)).size;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return { turns: 0, cost: 0 };
|
|
292
|
+
}
|
|
293
|
+
// Truncated or rotated underneath us — re-read from the top rather
|
|
294
|
+
// than seeking past the end and silently counting nothing forever.
|
|
295
|
+
if (size < cursor.offset) {
|
|
296
|
+
cursor.offset = 0;
|
|
297
|
+
cursor.seenMessageIds = [];
|
|
298
|
+
}
|
|
299
|
+
if (size === cursor.offset)
|
|
300
|
+
return { turns: 0, cost: 0 };
|
|
301
|
+
const tail = await this.readFrom(filePath, cursor.offset, size);
|
|
302
|
+
// Only advance past whole lines; a partial trailing line is re-read next pass.
|
|
303
|
+
const lastNewline = tail.lastIndexOf('\n');
|
|
304
|
+
if (lastNewline < 0)
|
|
305
|
+
return { turns: 0, cost: 0 };
|
|
306
|
+
const complete = tail.slice(0, lastNewline);
|
|
307
|
+
const consumedBytes = Buffer.byteLength(complete, 'utf-8') + 1;
|
|
308
|
+
const seen = new Set(cursor.seenMessageIds);
|
|
309
|
+
const turns = this.parseTurns(complete, seen);
|
|
310
|
+
cursor.offset += consumedBytes;
|
|
311
|
+
if (turns.length === 0)
|
|
312
|
+
return { turns: 0, cost: 0 };
|
|
313
|
+
const tokenSvc = TokenUsageService.getInstance();
|
|
314
|
+
let costAdded = 0;
|
|
315
|
+
let latest = null;
|
|
316
|
+
for (const turn of turns) {
|
|
317
|
+
const { cost } = calculateCost({ input: turn.input, output: turn.output, cacheRead: turn.cacheRead, cacheWrite: turn.cacheWrite }, turn.model);
|
|
318
|
+
costAdded += cost;
|
|
319
|
+
// `input` stays the fresh-token count so the dashboard's input
|
|
320
|
+
// column means what it says; cached tokens ride along in `detail`
|
|
321
|
+
// and are what make the cost figure meaningful.
|
|
322
|
+
tokenSvc.recordUsage(sessionName, sessionName, turn.input, turn.output, turn.model, undefined, {
|
|
323
|
+
cachedInput: turn.cacheRead + turn.cacheWrite,
|
|
324
|
+
timestamp: turn.timestamp,
|
|
325
|
+
});
|
|
326
|
+
seen.add(turn.messageId);
|
|
327
|
+
latest = turn;
|
|
328
|
+
}
|
|
329
|
+
cursor.cost += costAdded;
|
|
330
|
+
tokenSvc.overrideSessionCost(sessionName, cursor.cost);
|
|
331
|
+
// Keep the dedupe set bounded — only the newest ids can collide with
|
|
332
|
+
// a line Claude Code rewrites.
|
|
333
|
+
cursor.seenMessageIds = Array.from(seen).slice(-CLAUDE_TRANSCRIPT_SYNC_CONSTANTS.MAX_DEDUPE_IDS);
|
|
334
|
+
if (latest) {
|
|
335
|
+
const contextTokens = latest.input + latest.cacheRead + latest.cacheWrite;
|
|
336
|
+
cursor.lastContextTokens = contextTokens;
|
|
337
|
+
this.emitContext({ sessionName, contextTokens, model: latest.model });
|
|
338
|
+
}
|
|
339
|
+
return { turns: turns.length, cost: costAdded };
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Read a byte range of a file as UTF-8.
|
|
343
|
+
*
|
|
344
|
+
* Reads only the new tail; these transcripts reach tens of megabytes and
|
|
345
|
+
* loading one whole on every poll is what makes a naive version of this
|
|
346
|
+
* service more expensive than the usage it measures.
|
|
347
|
+
*
|
|
348
|
+
* @param filePath - File to read
|
|
349
|
+
* @param start - First byte to read
|
|
350
|
+
* @param end - Exclusive end byte
|
|
351
|
+
* @returns The decoded slice
|
|
352
|
+
*/
|
|
353
|
+
async readFrom(filePath, start, end) {
|
|
354
|
+
const handle = await fs.open(filePath, 'r');
|
|
355
|
+
try {
|
|
356
|
+
const length = end - start;
|
|
357
|
+
const buf = Buffer.alloc(length);
|
|
358
|
+
await handle.read(buf, 0, length, start);
|
|
359
|
+
return buf.toString('utf-8');
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
await handle.close();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Extract the assistant turns from a slice of transcript.
|
|
367
|
+
*
|
|
368
|
+
* @param text - Whole lines of JSONL
|
|
369
|
+
* @param seen - Message ids already counted; matches are skipped
|
|
370
|
+
* @returns Turns in file order
|
|
371
|
+
*/
|
|
372
|
+
parseTurns(text, seen) {
|
|
373
|
+
const turns = [];
|
|
374
|
+
for (const line of text.split('\n')) {
|
|
375
|
+
if (!line.trim())
|
|
376
|
+
continue;
|
|
377
|
+
let entry;
|
|
378
|
+
try {
|
|
379
|
+
entry = JSON.parse(line);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (entry.type !== 'assistant')
|
|
385
|
+
continue;
|
|
386
|
+
const msg = entry.message;
|
|
387
|
+
const usage = msg?.usage;
|
|
388
|
+
if (!msg || !usage)
|
|
389
|
+
continue;
|
|
390
|
+
const input = usage.input_tokens || 0;
|
|
391
|
+
const output = usage.output_tokens || 0;
|
|
392
|
+
const cacheRead = usage.cache_read_input_tokens || 0;
|
|
393
|
+
const cacheWrite = usage.cache_creation_input_tokens || 0;
|
|
394
|
+
// Claude Code writes `<synthetic>` entries — cancellations, tool
|
|
395
|
+
// bookkeeping — with an all-zero usage block. They are not model
|
|
396
|
+
// round-trips. Counting them inflates the turn count, and worse,
|
|
397
|
+
// one landing last makes the agent look like it is carrying no
|
|
398
|
+
// context at all: Atlas sat behind three of them reporting 0
|
|
399
|
+
// while actually holding 726k tokens.
|
|
400
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0)
|
|
401
|
+
continue;
|
|
402
|
+
const messageId = msg.id || `${entry.timestamp}`;
|
|
403
|
+
if (seen.has(messageId))
|
|
404
|
+
continue;
|
|
405
|
+
seen.add(messageId);
|
|
406
|
+
turns.push({
|
|
407
|
+
messageId,
|
|
408
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
409
|
+
model: msg.model || '',
|
|
410
|
+
input,
|
|
411
|
+
output,
|
|
412
|
+
cacheRead,
|
|
413
|
+
cacheWrite,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return turns;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Hand a context reading to every observer, isolating their failures.
|
|
420
|
+
*
|
|
421
|
+
* @param reading - The measurement to publish
|
|
422
|
+
*/
|
|
423
|
+
emitContext(reading) {
|
|
424
|
+
for (const observer of this.observers) {
|
|
425
|
+
try {
|
|
426
|
+
observer(reading);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
this.logger.warn('Context observer threw', {
|
|
430
|
+
sessionName: reading.sessionName,
|
|
431
|
+
error: err instanceof Error ? err.message : String(err),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/** Reads persisted cursors; a missing or corrupt file starts fresh. */
|
|
437
|
+
async loadCursors() {
|
|
438
|
+
this.loaded = true;
|
|
439
|
+
try {
|
|
440
|
+
const raw = await fs.readFile(this.cursorFile, 'utf-8');
|
|
441
|
+
const parsed = JSON.parse(raw);
|
|
442
|
+
this.cursors = new Map(Object.entries(parsed));
|
|
443
|
+
this.logger.debug('Loaded transcript cursors', { sessions: this.cursors.size });
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
this.cursors = new Map();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** Writes cursors atomically so a crash mid-write cannot corrupt them. */
|
|
450
|
+
async saveCursors() {
|
|
451
|
+
const obj = {};
|
|
452
|
+
for (const [k, v] of this.cursors)
|
|
453
|
+
obj[k] = v;
|
|
454
|
+
const tmp = `${this.cursorFile}.tmp`;
|
|
455
|
+
try {
|
|
456
|
+
await fs.mkdir(path.dirname(this.cursorFile), { recursive: true });
|
|
457
|
+
await fs.writeFile(tmp, JSON.stringify(obj, null, 2), 'utf-8');
|
|
458
|
+
await fs.rename(tmp, this.cursorFile);
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
this.logger.warn('Failed to persist transcript cursors', {
|
|
462
|
+
error: err instanceof Error ? err.message : String(err),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Current cursor for a session (tests and diagnostics).
|
|
468
|
+
*
|
|
469
|
+
* @param sessionName - Crewly session name
|
|
470
|
+
* @returns The cursor, or undefined if the session has never synced
|
|
471
|
+
*/
|
|
472
|
+
getCursor(sessionName) {
|
|
473
|
+
return this.cursors.get(sessionName);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Convenience accessor for the shared sync service.
|
|
478
|
+
*
|
|
479
|
+
* @returns The singleton instance
|
|
480
|
+
*/
|
|
481
|
+
export function getClaudeTranscriptSync() {
|
|
482
|
+
return ClaudeTranscriptSyncService.getInstance();
|
|
483
|
+
}
|
|
484
|
+
//# sourceMappingURL=claude-transcript-sync.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-transcript-sync.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/monitoring/claude-transcript-sync.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Pricing Table
|
|
3
|
+
*
|
|
4
|
+
* List prices for the runtimes Crewly drives, used to turn raw token counts
|
|
5
|
+
* into a cost figure for the Usage dashboard.
|
|
6
|
+
*
|
|
7
|
+
* Two things to keep in mind when reading a cost produced from this table:
|
|
8
|
+
*
|
|
9
|
+
* 1. These are pay-as-you-go API list prices. An agent running under a Max
|
|
10
|
+
* subscription does not generate a bill at these rates — the number is an
|
|
11
|
+
* API-equivalent, useful for comparing agents against each other and for
|
|
12
|
+
* spotting runaway context, not for reconciling an invoice.
|
|
13
|
+
* 2. Cache reads are far cheaper than fresh input and cache writes are dearer,
|
|
14
|
+
* so a cost computed without splitting them out is wrong by an order of
|
|
15
|
+
* magnitude for a long-lived agent. Every rate here is cache-aware and
|
|
16
|
+
* {@link calculateCost} requires the split.
|
|
17
|
+
*
|
|
18
|
+
* When a model is not listed by name we fall back to its family (the `opus` /
|
|
19
|
+
* `sonnet` / `haiku` substring) and finally to the sonnet tier. The resolved
|
|
20
|
+
* rate carries a `source` so callers can mark an estimated cost as estimated
|
|
21
|
+
* rather than presenting a guess as a measurement.
|
|
22
|
+
*
|
|
23
|
+
* @module services/monitoring/model-pricing
|
|
24
|
+
*/
|
|
25
|
+
/** Per-token USD rates for one model, split by how the tokens were billed. */
|
|
26
|
+
export interface ModelRate {
|
|
27
|
+
/** USD per fresh (uncached) input token */
|
|
28
|
+
input: number;
|
|
29
|
+
/** USD per output token */
|
|
30
|
+
output: number;
|
|
31
|
+
/** USD per token read from the prompt cache */
|
|
32
|
+
cacheRead: number;
|
|
33
|
+
/** USD per token written to the prompt cache */
|
|
34
|
+
cacheWrite: number;
|
|
35
|
+
}
|
|
36
|
+
/** A {@link ModelRate} plus how confidently it was matched to the model. */
|
|
37
|
+
export interface ResolvedModelRate extends ModelRate {
|
|
38
|
+
/**
|
|
39
|
+
* `exact` — the model id is listed by name.
|
|
40
|
+
* `family` — matched on an `opus`/`sonnet`/`haiku`/`fable` substring.
|
|
41
|
+
* `default` — nothing matched; the sonnet tier was assumed.
|
|
42
|
+
*
|
|
43
|
+
* Anything other than `exact` means the cost is an estimate.
|
|
44
|
+
*/
|
|
45
|
+
source: 'exact' | 'family' | 'default';
|
|
46
|
+
}
|
|
47
|
+
/** Opus-class list price (USD per 1M tokens). */
|
|
48
|
+
export declare const OPUS_TIER: ModelRate;
|
|
49
|
+
/** Sonnet-class list price (USD per 1M tokens). */
|
|
50
|
+
export declare const SONNET_TIER: ModelRate;
|
|
51
|
+
/** Haiku-class list price (USD per 1M tokens), 4.5 generation. */
|
|
52
|
+
export declare const HAIKU_TIER: ModelRate;
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the billing rate for a model id.
|
|
55
|
+
*
|
|
56
|
+
* @param model - Model id as the runtime reported it (e.g. `claude-opus-5`)
|
|
57
|
+
* @returns The rate plus how it was matched; never throws, never returns null
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const rate = resolveRate('claude-opus-5'); // source: 'exact'
|
|
62
|
+
* const guess = resolveRate('claude-fable-5-1'); // source: 'family'
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveRate(model: string): ResolvedModelRate;
|
|
66
|
+
/** The four token counts that make up one model round-trip. */
|
|
67
|
+
export interface TokenSplit {
|
|
68
|
+
/** Fresh input tokens (not served from cache) */
|
|
69
|
+
input: number;
|
|
70
|
+
/** Output tokens */
|
|
71
|
+
output: number;
|
|
72
|
+
/** Input tokens served from the prompt cache */
|
|
73
|
+
cacheRead: number;
|
|
74
|
+
/** Input tokens written into the prompt cache */
|
|
75
|
+
cacheWrite: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Computes the cache-aware cost of one or more model round-trips.
|
|
79
|
+
*
|
|
80
|
+
* @param split - The four token counts; pass 0 for any the runtime omits
|
|
81
|
+
* @param model - Model id as the runtime reported it
|
|
82
|
+
* @returns Cost in USD, and whether the rate was an exact match
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const { cost, estimated } = calculateCost(
|
|
87
|
+
* { input: 500, output: 200, cacheRead: 690_000, cacheWrite: 0 },
|
|
88
|
+
* 'claude-opus-5',
|
|
89
|
+
* );
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function calculateCost(split: TokenSplit, model: string): {
|
|
93
|
+
cost: number;
|
|
94
|
+
estimated: boolean;
|
|
95
|
+
rateSource: ResolvedModelRate['source'];
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=model-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-pricing.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/monitoring/model-pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACzB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IACnD;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;CACvC;AAYD,iDAAiD;AACjD,eAAO,MAAM,SAAS,WAAiC,CAAC;AACxD,mDAAmD;AACnD,eAAO,MAAM,WAAW,WAA+B,CAAC;AACxD,kEAAkE;AAClE,eAAO,MAAM,UAAU,WAA8B,CAAC;AAsCtD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAW5D;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IAC1B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAA;CAAE,CAQ/E"}
|