crewly 1.20.72 → 1.20.74
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 +44 -5
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts +41 -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 +52 -1
- 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 +209 -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 +440 -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/backend/src/services/v3/team-trigger-reconciler.service.d.ts +14 -0
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js +35 -4
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/types/v2/index.d.ts +2 -2
- package/dist/backend/backend/src/types/v2/index.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/index.js.map +1 -1
- package/dist/backend/backend/src/types/v2/trigger.types.d.ts +37 -0
- package/dist/backend/backend/src/types/v2/trigger.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/trigger.types.js +19 -1
- package/dist/backend/backend/src/types/v2/trigger.types.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 +41 -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 +52 -1
- 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/dist/cli/backend/src/types/v2/index.d.ts +2 -2
- package/dist/cli/backend/src/types/v2/index.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/index.js.map +1 -1
- package/dist/cli/backend/src/types/v2/trigger.types.d.ts +37 -0
- package/dist/cli/backend/src/types/v2/trigger.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/trigger.types.js +19 -1
- package/dist/cli/backend/src/types/v2/trigger.types.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,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Session Service
|
|
3
|
+
*
|
|
4
|
+
* Makes an agent's browser work watchable. Every `/api/browser/*` action an
|
|
5
|
+
* agent takes is folded into a per-agent {@link BrowserSession} — what it is
|
|
6
|
+
* doing, where, and a recent picture of the page — so the owner can follow
|
|
7
|
+
* along instead of reading a transcript of tool calls after the fact.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists as its own object rather than a log line: an agent driving
|
|
10
|
+
* a browser is the one activity where the interesting state lives outside
|
|
11
|
+
* Crewly entirely. Without a picture, an agent that cannot see what it is
|
|
12
|
+
* doing will describe the page confidently and wrongly, and the owner has no
|
|
13
|
+
* way to catch it. The most damaging version of that already happened here —
|
|
14
|
+
* an agent that could not attach an image to Slack published a screenshot of
|
|
15
|
+
* a half-filled account-transfer form to a public URL instead.
|
|
16
|
+
*
|
|
17
|
+
* Which leads to the rule this module enforces by construction:
|
|
18
|
+
*
|
|
19
|
+
* **Frames are owner-surface only.** They live in memory, are never written
|
|
20
|
+
* to disk, never attached to a chat message, and never posted to Slack or
|
|
21
|
+
* any other multi-party surface. The only way to read one is an authenticated
|
|
22
|
+
* fetch of {@link BrowserSessionService.getFrame} by someone already looking
|
|
23
|
+
* at this Crewly instance. A screenshot of whatever the owner happened to be
|
|
24
|
+
* logged into is exactly the kind of thing that must not be broadcast.
|
|
25
|
+
*
|
|
26
|
+
* Capture cadence is driven by whether anyone is actually looking. A session
|
|
27
|
+
* nobody is watching refreshes rarely and only after the agent did something;
|
|
28
|
+
* a session being watched refreshes at roughly a frame a second. There is no
|
|
29
|
+
* subscribe/unsubscribe protocol to leak — a viewer is simply someone who
|
|
30
|
+
* fetched a frame recently, which self-expires.
|
|
31
|
+
*
|
|
32
|
+
* @module services/browser/browser-session.service
|
|
33
|
+
*/
|
|
34
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
35
|
+
import { BROWSER_SESSION_CONSTANTS } from '../../constants.js';
|
|
36
|
+
/** Tool names grouped by the activity they represent. */
|
|
37
|
+
const NAVIGATION_TOOLS = new Set(['navigate']);
|
|
38
|
+
const READ_TOOLS = new Set([
|
|
39
|
+
'readText',
|
|
40
|
+
'screenshot',
|
|
41
|
+
'fullPageScreenshot',
|
|
42
|
+
'getElement',
|
|
43
|
+
'getInteractiveElements',
|
|
44
|
+
'searchText',
|
|
45
|
+
'listOptions',
|
|
46
|
+
'getCookies',
|
|
47
|
+
'getLocalStorage',
|
|
48
|
+
'getConsoleMessages',
|
|
49
|
+
'waitForSelector',
|
|
50
|
+
'getTabs',
|
|
51
|
+
]);
|
|
52
|
+
const TERMINAL_TOOLS = new Set(['unbindTab']);
|
|
53
|
+
/**
|
|
54
|
+
* Turns a tool call into a sentence a person can read.
|
|
55
|
+
*
|
|
56
|
+
* @param tool - Tool name as dispatched to the extension
|
|
57
|
+
* @param params - Params sent with it
|
|
58
|
+
* @returns A short description, e.g. `Clicked “Sign in”`
|
|
59
|
+
*/
|
|
60
|
+
export function describeAction(tool, params) {
|
|
61
|
+
const p = params ?? {};
|
|
62
|
+
const selector = typeof p.selector === 'string' ? p.selector : undefined;
|
|
63
|
+
const text = typeof p.text === 'string' ? p.text : undefined;
|
|
64
|
+
switch (tool) {
|
|
65
|
+
case 'navigate':
|
|
66
|
+
return typeof p.url === 'string' ? `Opening ${p.url}` : 'Opening a page';
|
|
67
|
+
case 'click':
|
|
68
|
+
return selector ? `Clicked ${selector}` : 'Clicked the page';
|
|
69
|
+
case 'fill':
|
|
70
|
+
case 'type':
|
|
71
|
+
case 'insertText':
|
|
72
|
+
// Never echo what was typed — it is frequently a credential.
|
|
73
|
+
return selector ? `Typed into ${selector}` : 'Typed into the page';
|
|
74
|
+
case 'selectOption':
|
|
75
|
+
return selector ? `Chose an option in ${selector}` : 'Chose an option';
|
|
76
|
+
case 'setFileInput':
|
|
77
|
+
return 'Attached a file';
|
|
78
|
+
case 'pressKey':
|
|
79
|
+
return typeof p.key === 'string' ? `Pressed ${p.key}` : 'Pressed a key';
|
|
80
|
+
case 'scroll':
|
|
81
|
+
case 'scrollInElement':
|
|
82
|
+
return 'Scrolled the page';
|
|
83
|
+
case 'hover':
|
|
84
|
+
return selector ? `Hovered ${selector}` : 'Hovered the page';
|
|
85
|
+
case 'readText':
|
|
86
|
+
return 'Reading page';
|
|
87
|
+
case 'searchText':
|
|
88
|
+
return text ? `Looking for “${text}”` : 'Searching the page';
|
|
89
|
+
case 'screenshot':
|
|
90
|
+
case 'fullPageScreenshot':
|
|
91
|
+
return 'Taking a screenshot';
|
|
92
|
+
case 'getInteractiveElements':
|
|
93
|
+
return 'Looking at what is on the page';
|
|
94
|
+
case 'waitForSelector':
|
|
95
|
+
return selector ? `Waiting for ${selector}` : 'Waiting for the page';
|
|
96
|
+
case 'executeJs':
|
|
97
|
+
case 'executeScript':
|
|
98
|
+
return 'Running a script on the page';
|
|
99
|
+
case 'unbindTab':
|
|
100
|
+
return 'Finished with this tab';
|
|
101
|
+
case 'bindTab':
|
|
102
|
+
return 'Opening a browser tab';
|
|
103
|
+
default:
|
|
104
|
+
return tool;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Maps a tool to the status it puts the session into.
|
|
109
|
+
*
|
|
110
|
+
* @param tool - Tool name
|
|
111
|
+
* @returns The status to show
|
|
112
|
+
*/
|
|
113
|
+
export function statusForTool(tool) {
|
|
114
|
+
if (TERMINAL_TOOLS.has(tool))
|
|
115
|
+
return 'done';
|
|
116
|
+
if (NAVIGATION_TOOLS.has(tool))
|
|
117
|
+
return 'navigating';
|
|
118
|
+
if (READ_TOOLS.has(tool))
|
|
119
|
+
return 'reading';
|
|
120
|
+
return 'acting';
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Tracks what each agent is doing in the browser and keeps a recent frame.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const sessions = BrowserSessionService.getInstance();
|
|
128
|
+
* sessions.setCapturer(capture);
|
|
129
|
+
* sessions.noteAction({ agentSession: 'pia', tool: 'navigate', params: { url } });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export class BrowserSessionService {
|
|
133
|
+
static instance = null;
|
|
134
|
+
logger;
|
|
135
|
+
sessions = new Map();
|
|
136
|
+
/** Frame bytes, held apart from the session so metadata stays cheap to copy. */
|
|
137
|
+
frames = new Map();
|
|
138
|
+
/** Last time someone fetched this session's frame (epoch ms). */
|
|
139
|
+
lastViewedAt = new Map();
|
|
140
|
+
/** Sessions that acted since their last capture. */
|
|
141
|
+
dirty = new Set();
|
|
142
|
+
/** Sessions with a capture in flight, so ticks cannot pile up on a slow page. */
|
|
143
|
+
capturing = new Set();
|
|
144
|
+
capturer = null;
|
|
145
|
+
timer = null;
|
|
146
|
+
constructor() {
|
|
147
|
+
this.logger = LoggerService.getInstance().createComponentLogger('BrowserSession');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the shared instance.
|
|
151
|
+
*
|
|
152
|
+
* @returns The singleton service
|
|
153
|
+
*/
|
|
154
|
+
static getInstance() {
|
|
155
|
+
if (!BrowserSessionService.instance) {
|
|
156
|
+
BrowserSessionService.instance = new BrowserSessionService();
|
|
157
|
+
}
|
|
158
|
+
return BrowserSessionService.instance;
|
|
159
|
+
}
|
|
160
|
+
/** Drops the singleton and stops its timer (tests). */
|
|
161
|
+
static resetInstance() {
|
|
162
|
+
BrowserSessionService.instance?.stop();
|
|
163
|
+
BrowserSessionService.instance = null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Provide the function used to capture a frame.
|
|
167
|
+
*
|
|
168
|
+
* Until this is set the service still tracks actions; it simply has no
|
|
169
|
+
* pictures, which is the correct behaviour on an install with no browser
|
|
170
|
+
* extension connected.
|
|
171
|
+
*
|
|
172
|
+
* @param capturer - How to capture, or null to disable capture
|
|
173
|
+
*/
|
|
174
|
+
setCapturer(capturer) {
|
|
175
|
+
this.capturer = capturer;
|
|
176
|
+
}
|
|
177
|
+
/** Starts the capture loop. Safe to call twice. */
|
|
178
|
+
start() {
|
|
179
|
+
if (this.timer)
|
|
180
|
+
return;
|
|
181
|
+
this.timer = setInterval(() => {
|
|
182
|
+
void this.tick();
|
|
183
|
+
}, BROWSER_SESSION_CONSTANTS.TICK_INTERVAL_MS);
|
|
184
|
+
this.timer.unref?.();
|
|
185
|
+
this.logger.info('Browser session tracking started', {
|
|
186
|
+
tickMs: BROWSER_SESSION_CONSTANTS.TICK_INTERVAL_MS,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Stops the capture loop. Sessions and frames are left in place. */
|
|
190
|
+
stop() {
|
|
191
|
+
if (this.timer) {
|
|
192
|
+
clearInterval(this.timer);
|
|
193
|
+
this.timer = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Record that an agent performed a browser action.
|
|
198
|
+
*
|
|
199
|
+
* Called from the single dispatch choke point in the browser controller,
|
|
200
|
+
* so every tool is covered without each handler having to remember.
|
|
201
|
+
*
|
|
202
|
+
* @param input - The action that just succeeded
|
|
203
|
+
*/
|
|
204
|
+
noteAction(input) {
|
|
205
|
+
const { agentSession, tool, params } = input;
|
|
206
|
+
if (!agentSession)
|
|
207
|
+
return;
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
let session = this.sessions.get(agentSession);
|
|
210
|
+
if (!session) {
|
|
211
|
+
session = {
|
|
212
|
+
id: agentSession,
|
|
213
|
+
agentSession,
|
|
214
|
+
status: 'reading',
|
|
215
|
+
lastAction: '',
|
|
216
|
+
lastActionAt: now,
|
|
217
|
+
startedAt: now,
|
|
218
|
+
};
|
|
219
|
+
this.sessions.set(agentSession, session);
|
|
220
|
+
}
|
|
221
|
+
// A finished session that acts again is a new piece of work, not a
|
|
222
|
+
// resurrection of the old one — reset its clock and clear the stale
|
|
223
|
+
// picture so the UI never shows a frame from a previous task.
|
|
224
|
+
if (session.status === 'done' || session.status === 'stopped') {
|
|
225
|
+
session.startedAt = now;
|
|
226
|
+
delete session.endedAt;
|
|
227
|
+
this.frames.delete(agentSession);
|
|
228
|
+
delete session.frameAt;
|
|
229
|
+
}
|
|
230
|
+
session.status = statusForTool(tool);
|
|
231
|
+
session.lastAction = describeAction(tool, params);
|
|
232
|
+
session.lastActionAt = now;
|
|
233
|
+
if (input.agentName)
|
|
234
|
+
session.agentName = input.agentName;
|
|
235
|
+
if (input.goal)
|
|
236
|
+
session.goal = input.goal;
|
|
237
|
+
if (typeof input.tabId === 'number')
|
|
238
|
+
session.tabId = input.tabId;
|
|
239
|
+
if (typeof params?.url === 'string')
|
|
240
|
+
session.url = params.url;
|
|
241
|
+
if (session.status === 'done')
|
|
242
|
+
session.endedAt = now;
|
|
243
|
+
this.dirty.add(agentSession);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Mark a session as finished.
|
|
247
|
+
*
|
|
248
|
+
* @param agentSession - The agent whose session ended
|
|
249
|
+
* @param reason - `done` when the agent finished, `stopped` when a person
|
|
250
|
+
* ended it
|
|
251
|
+
*/
|
|
252
|
+
endSession(agentSession, reason = 'done') {
|
|
253
|
+
const session = this.sessions.get(agentSession);
|
|
254
|
+
if (!session)
|
|
255
|
+
return;
|
|
256
|
+
session.status = reason;
|
|
257
|
+
session.endedAt = Date.now();
|
|
258
|
+
this.dirty.delete(agentSession);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* All sessions, newest activity first.
|
|
262
|
+
*
|
|
263
|
+
* @param includeFinished - Include sessions that are done or stopped
|
|
264
|
+
* @returns Copies of the session records; frames are not included
|
|
265
|
+
*/
|
|
266
|
+
listSessions(includeFinished = true) {
|
|
267
|
+
const all = Array.from(this.sessions.values())
|
|
268
|
+
.filter((s) => includeFinished || (s.status !== 'done' && s.status !== 'stopped'))
|
|
269
|
+
.sort((a, b) => b.lastActionAt - a.lastActionAt);
|
|
270
|
+
return all.map((s) => ({ ...s }));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* One session by id.
|
|
274
|
+
*
|
|
275
|
+
* @param id - Session id (the agent session name)
|
|
276
|
+
* @returns A copy, or undefined
|
|
277
|
+
*/
|
|
278
|
+
getSession(id) {
|
|
279
|
+
const s = this.sessions.get(id);
|
|
280
|
+
return s ? { ...s } : undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read the current frame, registering the caller as a viewer.
|
|
284
|
+
*
|
|
285
|
+
* Fetching a frame is what marks a session as watched, which is what
|
|
286
|
+
* raises its capture rate. There is no separate subscribe call to get out
|
|
287
|
+
* of sync, and interest expires on its own when nobody fetches.
|
|
288
|
+
*
|
|
289
|
+
* @param id - Session id
|
|
290
|
+
* @returns The frame, or undefined when none has been captured
|
|
291
|
+
*/
|
|
292
|
+
getFrame(id) {
|
|
293
|
+
this.lastViewedAt.set(id, Date.now());
|
|
294
|
+
return this.frames.get(id);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Whether anyone has looked at this session recently.
|
|
298
|
+
*
|
|
299
|
+
* @param id - Session id
|
|
300
|
+
* @param now - Clock override for tests
|
|
301
|
+
* @returns True when a frame was fetched inside the watch window
|
|
302
|
+
*/
|
|
303
|
+
isWatched(id, now = Date.now()) {
|
|
304
|
+
const seen = this.lastViewedAt.get(id);
|
|
305
|
+
return seen !== undefined && now - seen <= BROWSER_SESSION_CONSTANTS.WATCH_WINDOW_MS;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Decide whether a session is due for a capture.
|
|
309
|
+
*
|
|
310
|
+
* Watched sessions refresh on a short interval whether or not the agent
|
|
311
|
+
* did anything, because the page can change on its own. Unwatched ones
|
|
312
|
+
* refresh only after an action, and slowly — a picture nobody is looking
|
|
313
|
+
* at is pure cost on the agent's browser.
|
|
314
|
+
*
|
|
315
|
+
* @param id - Session id
|
|
316
|
+
* @param now - Clock override for tests
|
|
317
|
+
* @returns True when a frame should be captured now
|
|
318
|
+
*/
|
|
319
|
+
shouldCapture(id, now = Date.now()) {
|
|
320
|
+
const session = this.sessions.get(id);
|
|
321
|
+
if (!session)
|
|
322
|
+
return false;
|
|
323
|
+
if (session.status === 'done' || session.status === 'stopped')
|
|
324
|
+
return false;
|
|
325
|
+
if (this.capturing.has(id))
|
|
326
|
+
return false;
|
|
327
|
+
const age = now - (session.frameAt ?? 0);
|
|
328
|
+
if (this.isWatched(id, now)) {
|
|
329
|
+
return age >= BROWSER_SESSION_CONSTANTS.WATCHED_FRAME_INTERVAL_MS;
|
|
330
|
+
}
|
|
331
|
+
return this.dirty.has(id) && age >= BROWSER_SESSION_CONSTANTS.IDLE_FRAME_INTERVAL_MS;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* One pass of the capture loop.
|
|
335
|
+
*
|
|
336
|
+
* @returns How many sessions were captured
|
|
337
|
+
*/
|
|
338
|
+
async tick() {
|
|
339
|
+
if (!this.capturer)
|
|
340
|
+
return 0;
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const due = Array.from(this.sessions.keys()).filter((id) => this.shouldCapture(id, now));
|
|
343
|
+
if (due.length === 0)
|
|
344
|
+
return 0;
|
|
345
|
+
await Promise.all(due.map((id) => this.captureFrame(id)));
|
|
346
|
+
return due.length;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Capture one session's frame.
|
|
350
|
+
*
|
|
351
|
+
* A failure is recorded on the session and otherwise swallowed: a page
|
|
352
|
+
* that cannot be captured (a restricted URL, a tab the user closed) must
|
|
353
|
+
* not stop the agent or the loop.
|
|
354
|
+
*
|
|
355
|
+
* @param id - Session id
|
|
356
|
+
* @returns True when a new frame was stored
|
|
357
|
+
*/
|
|
358
|
+
async captureFrame(id) {
|
|
359
|
+
const session = this.sessions.get(id);
|
|
360
|
+
if (!session || !this.capturer || this.capturing.has(id))
|
|
361
|
+
return false;
|
|
362
|
+
this.capturing.add(id);
|
|
363
|
+
try {
|
|
364
|
+
const shot = await this.capturer(session.agentSession, {
|
|
365
|
+
format: BROWSER_SESSION_CONSTANTS.FRAME_FORMAT,
|
|
366
|
+
quality: BROWSER_SESSION_CONSTANTS.FRAME_QUALITY,
|
|
367
|
+
scale: BROWSER_SESSION_CONSTANTS.FRAME_SCALE,
|
|
368
|
+
});
|
|
369
|
+
if (!shot?.base64) {
|
|
370
|
+
session.frameError = 'No image returned';
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
this.frames.set(id, {
|
|
374
|
+
base64: shot.base64,
|
|
375
|
+
mimeType: shot.format === 'jpeg' ? 'image/jpeg' : 'image/png',
|
|
376
|
+
capturedAt: Date.now(),
|
|
377
|
+
...(shot.devicePixelRatio !== undefined ? { devicePixelRatio: shot.devicePixelRatio } : {}),
|
|
378
|
+
});
|
|
379
|
+
session.frameAt = Date.now();
|
|
380
|
+
delete session.frameError;
|
|
381
|
+
this.dirty.delete(id);
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
session.frameError = err instanceof Error ? err.message : String(err);
|
|
386
|
+
// Do not retry immediately — clearing dirty stops a broken page
|
|
387
|
+
// from being hammered once per tick.
|
|
388
|
+
this.dirty.delete(id);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
this.capturing.delete(id);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Forget sessions that finished a while ago, and their frames.
|
|
397
|
+
*
|
|
398
|
+
* @param now - Clock override for tests
|
|
399
|
+
* @returns How many sessions were dropped
|
|
400
|
+
*/
|
|
401
|
+
prune(now = Date.now()) {
|
|
402
|
+
let dropped = 0;
|
|
403
|
+
for (const [id, session] of this.sessions) {
|
|
404
|
+
const ended = session.endedAt;
|
|
405
|
+
if (ended !== undefined && now - ended > BROWSER_SESSION_CONSTANTS.RETAIN_FINISHED_MS) {
|
|
406
|
+
this.sessions.delete(id);
|
|
407
|
+
this.frames.delete(id);
|
|
408
|
+
this.lastViewedAt.delete(id);
|
|
409
|
+
this.dirty.delete(id);
|
|
410
|
+
dropped += 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return dropped;
|
|
414
|
+
}
|
|
415
|
+
/** Drops all state (tests). */
|
|
416
|
+
clear() {
|
|
417
|
+
this.sessions.clear();
|
|
418
|
+
this.frames.clear();
|
|
419
|
+
this.lastViewedAt.clear();
|
|
420
|
+
this.dirty.clear();
|
|
421
|
+
this.capturing.clear();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Convenience accessor for the shared browser session service.
|
|
426
|
+
*
|
|
427
|
+
* @returns The singleton instance
|
|
428
|
+
*/
|
|
429
|
+
export function getBrowserSessions() {
|
|
430
|
+
return BrowserSessionService.getInstance();
|
|
431
|
+
}
|
|
432
|
+
//# sourceMappingURL=browser-session.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-session.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/browser/browser-session.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
/**
|
|
35
|
+
* How far a single session's transcript has been consumed.
|
|
36
|
+
*
|
|
37
|
+
* Persisted so a backend restart resumes where it left off instead of
|
|
38
|
+
* re-counting a transcript that may hold hundreds of dollars of history.
|
|
39
|
+
*/
|
|
40
|
+
export interface TranscriptCursor {
|
|
41
|
+
/** Absolute path of the transcript this cursor belongs to */
|
|
42
|
+
filePath: string;
|
|
43
|
+
/** Byte offset already consumed */
|
|
44
|
+
offset: number;
|
|
45
|
+
/**
|
|
46
|
+
* Ids of the most recently counted assistant messages.
|
|
47
|
+
*
|
|
48
|
+
* Claude Code can rewrite a line for the same `message.id` (for instance
|
|
49
|
+
* when a turn is streamed and then finalised), so an offset alone is not
|
|
50
|
+
* enough to guarantee exactly-once counting. Bounded to
|
|
51
|
+
* `MAX_DEDUPE_IDS` entries.
|
|
52
|
+
*/
|
|
53
|
+
seenMessageIds: string[];
|
|
54
|
+
/** Cumulative USD cost attributed to this session so far */
|
|
55
|
+
cost: number;
|
|
56
|
+
}
|
|
57
|
+
/** One agent's context size, as measured from its latest transcript turn. */
|
|
58
|
+
export interface ContextReading {
|
|
59
|
+
/** Crewly session name (e.g. `think-tank-atlas-b4e166f6`) */
|
|
60
|
+
sessionName: string;
|
|
61
|
+
/** Fresh + cache-read + cache-written input tokens on the latest turn */
|
|
62
|
+
contextTokens: number;
|
|
63
|
+
/** Model id that produced the turn */
|
|
64
|
+
model: string;
|
|
65
|
+
}
|
|
66
|
+
/** Callback invoked once per sync for every session with a fresh reading. */
|
|
67
|
+
export type ContextObserver = (reading: ContextReading) => void;
|
|
68
|
+
/** What one sync pass did, returned for logging and tests. */
|
|
69
|
+
export interface SyncResult {
|
|
70
|
+
/** Sessions that had at least one new turn */
|
|
71
|
+
sessionsUpdated: number;
|
|
72
|
+
/** New assistant turns counted across all sessions */
|
|
73
|
+
turnsCounted: number;
|
|
74
|
+
/** USD attributed in this pass */
|
|
75
|
+
costAdded: number;
|
|
76
|
+
/** Sessions skipped because no transcript could be located */
|
|
77
|
+
sessionsWithoutTranscript: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reads Claude Code transcripts on an interval and records exact token usage.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const sync = ClaudeTranscriptSyncService.getInstance();
|
|
85
|
+
* sync.onContextReading((r) => monitor.updateContextTokens(r.sessionName, r.contextTokens));
|
|
86
|
+
* await sync.start();
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare class ClaudeTranscriptSyncService {
|
|
90
|
+
private static instance;
|
|
91
|
+
private readonly logger;
|
|
92
|
+
private readonly cursorFile;
|
|
93
|
+
/**
|
|
94
|
+
* Home directory that `~/.claude/projects` is resolved against.
|
|
95
|
+
*
|
|
96
|
+
* Injectable because `os.homedir()` does not reliably follow a `$HOME`
|
|
97
|
+
* override once the process has already resolved it, which makes it
|
|
98
|
+
* untestable against a temp tree.
|
|
99
|
+
*/
|
|
100
|
+
private readonly homeDir;
|
|
101
|
+
private cursors;
|
|
102
|
+
private timer;
|
|
103
|
+
private observers;
|
|
104
|
+
private loaded;
|
|
105
|
+
/** Guards against a slow pass overlapping the next tick. */
|
|
106
|
+
private running;
|
|
107
|
+
/**
|
|
108
|
+
* @param cursorFile - Override the cursor persistence path (tests)
|
|
109
|
+
* @param homeDir - Override the home directory transcripts live under (tests)
|
|
110
|
+
*/
|
|
111
|
+
constructor(cursorFile?: string, homeDir?: string);
|
|
112
|
+
/**
|
|
113
|
+
* Get the shared instance.
|
|
114
|
+
*
|
|
115
|
+
* @returns The singleton service
|
|
116
|
+
*/
|
|
117
|
+
static getInstance(): ClaudeTranscriptSyncService;
|
|
118
|
+
/** Drops the singleton and stops its timer (tests). */
|
|
119
|
+
static resetInstance(): void;
|
|
120
|
+
/**
|
|
121
|
+
* Register a callback to receive each session's context size per pass.
|
|
122
|
+
*
|
|
123
|
+
* @param observer - Invoked once per session that produced a new turn
|
|
124
|
+
*/
|
|
125
|
+
onContextReading(observer: ContextObserver): void;
|
|
126
|
+
/**
|
|
127
|
+
* Load cursors, run one pass immediately, then poll.
|
|
128
|
+
*
|
|
129
|
+
* Safe to call twice; the second call is a no-op.
|
|
130
|
+
*/
|
|
131
|
+
start(): Promise<void>;
|
|
132
|
+
/** Stops polling. Cursors already on disk are left alone. */
|
|
133
|
+
stop(): void;
|
|
134
|
+
/**
|
|
135
|
+
* Run one sync pass across every registered claude-code session.
|
|
136
|
+
*
|
|
137
|
+
* Overlapping calls are dropped rather than queued — a pass that outruns
|
|
138
|
+
* the interval means the machine is busy, and piling up reads of
|
|
139
|
+
* multi-megabyte transcripts would make that worse.
|
|
140
|
+
*
|
|
141
|
+
* @returns What the pass counted
|
|
142
|
+
*/
|
|
143
|
+
sync(): Promise<SyncResult>;
|
|
144
|
+
/**
|
|
145
|
+
* Locate a session's transcript file.
|
|
146
|
+
*
|
|
147
|
+
* Prefers the conversation id Crewly recorded when it launched the agent.
|
|
148
|
+
* Falls back to the newest transcript in the project's slug directory,
|
|
149
|
+
* which covers an agent whose id has not been captured yet.
|
|
150
|
+
*
|
|
151
|
+
* @param cwd - The agent's working directory
|
|
152
|
+
* @param claudeSessionId - Conversation id, when known
|
|
153
|
+
* @returns Absolute path, or null when nothing readable exists
|
|
154
|
+
*/
|
|
155
|
+
private resolveTranscript;
|
|
156
|
+
/**
|
|
157
|
+
* Consume the unread tail of one transcript and record what it holds.
|
|
158
|
+
*
|
|
159
|
+
* @param sessionName - Crewly session name, used as the usage key
|
|
160
|
+
* @param filePath - Transcript to read
|
|
161
|
+
* @returns Turns counted and cost added for this session
|
|
162
|
+
*/
|
|
163
|
+
private syncOne;
|
|
164
|
+
/**
|
|
165
|
+
* Read a byte range of a file as UTF-8.
|
|
166
|
+
*
|
|
167
|
+
* Reads only the new tail; these transcripts reach tens of megabytes and
|
|
168
|
+
* loading one whole on every poll is what makes a naive version of this
|
|
169
|
+
* service more expensive than the usage it measures.
|
|
170
|
+
*
|
|
171
|
+
* @param filePath - File to read
|
|
172
|
+
* @param start - First byte to read
|
|
173
|
+
* @param end - Exclusive end byte
|
|
174
|
+
* @returns The decoded slice
|
|
175
|
+
*/
|
|
176
|
+
private readFrom;
|
|
177
|
+
/**
|
|
178
|
+
* Extract the assistant turns from a slice of transcript.
|
|
179
|
+
*
|
|
180
|
+
* @param text - Whole lines of JSONL
|
|
181
|
+
* @param seen - Message ids already counted; matches are skipped
|
|
182
|
+
* @returns Turns in file order
|
|
183
|
+
*/
|
|
184
|
+
private parseTurns;
|
|
185
|
+
/**
|
|
186
|
+
* Hand a context reading to every observer, isolating their failures.
|
|
187
|
+
*
|
|
188
|
+
* @param reading - The measurement to publish
|
|
189
|
+
*/
|
|
190
|
+
private emitContext;
|
|
191
|
+
/** Reads persisted cursors; a missing or corrupt file starts fresh. */
|
|
192
|
+
private loadCursors;
|
|
193
|
+
/** Writes cursors atomically so a crash mid-write cannot corrupt them. */
|
|
194
|
+
private saveCursors;
|
|
195
|
+
/**
|
|
196
|
+
* Current cursor for a session (tests and diagnostics).
|
|
197
|
+
*
|
|
198
|
+
* @param sessionName - Crewly session name
|
|
199
|
+
* @returns The cursor, or undefined if the session has never synced
|
|
200
|
+
*/
|
|
201
|
+
getCursor(sessionName: string): TranscriptCursor | undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Convenience accessor for the shared sync service.
|
|
205
|
+
*
|
|
206
|
+
* @returns The singleton instance
|
|
207
|
+
*/
|
|
208
|
+
export declare function getClaudeTranscriptSync(): ClaudeTranscriptSyncService;
|
|
209
|
+
//# sourceMappingURL=claude-transcript-sync.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-transcript-sync.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/monitoring/claude-transcript-sync.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAYH;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC9B,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;AAEhE,8DAA8D;AAC9D,MAAM,WAAW,UAAU;IAC1B,8CAA8C;IAC9C,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,YAAY,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,yBAAyB,EAAE,MAAM,CAAC;CAClC;AAaD;;;;;;;;;GASG;AACH,qBAAa,2BAA2B;IACvC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA4C;IAEnE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,MAAM,CAAS;IACvB,4DAA4D;IAC5D,OAAO,CAAC,OAAO,CAAS;IAExB;;;OAGG;gBACS,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;IAOjD;;;;OAIG;IACH,MAAM,CAAC,WAAW,IAAI,2BAA2B;IAOjD,uDAAuD;IACvD,MAAM,CAAC,aAAa,IAAI,IAAI;IAK5B;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAIjD;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B,6DAA6D;IAC7D,IAAI,IAAI,IAAI;IAOZ;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IAiDjC;;;;;;;;;;OAUG;YACW,iBAAiB;IAwC/B;;;;;;OAMG;YACW,OAAO;IA8ErB;;;;;;;;;;;OAWG;YACW,QAAQ;IAYtB;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;IAoClB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAanB,uEAAuE;YACzD,WAAW;IAYzB,0EAA0E;YAC5D,WAAW;IAezB;;;;;OAKG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAG5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,2BAA2B,CAErE"}
|