@yeaft/webchat-agent 1.0.151 → 1.0.153

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.151",
3
+ "version": "1.0.153",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -240,7 +240,7 @@ export class AnthropicAdapter extends LLMAdapter {
240
240
  * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', effortContext?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
241
241
  * @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
242
242
  */
243
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange }) {
243
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange, onRequestStart }) {
244
244
  if (signal?.aborted) throw new LLMAbortError();
245
245
 
246
246
  const body = {
@@ -272,6 +272,7 @@ export class AnthropicAdapter extends LLMAdapter {
272
272
 
273
273
  let response;
274
274
  try {
275
+ onRequestStart?.();
275
276
  response = await fetch(url, {
276
277
  method: 'POST',
277
278
  headers,
@@ -328,6 +329,7 @@ export class AnthropicAdapter extends LLMAdapter {
328
329
  const responseStatus = response.status;
329
330
  let sawStop = false;
330
331
  let sawMessageStart = false;
332
+ let cumulativeOutputTokens = 0;
331
333
 
332
334
  try {
333
335
  while (true) {
@@ -454,12 +456,17 @@ export class AnthropicAdapter extends LLMAdapter {
454
456
  stopReason: this.#mapStopReason(stopReason),
455
457
  };
456
458
  }
457
- // Usage from message_delta
459
+ // Anthropic message_delta usage is cumulative across the response.
460
+ // Expose only the newly consumed output tokens so shared accounting
461
+ // can safely add events from message_start and multiple deltas.
458
462
  if (event.usage) {
463
+ const nextOutputTokens = Math.max(0, Number(event.usage.output_tokens) || 0);
464
+ const outputTokens = Math.max(0, nextOutputTokens - cumulativeOutputTokens);
465
+ cumulativeOutputTokens = Math.max(cumulativeOutputTokens, nextOutputTokens);
459
466
  yield {
460
467
  type: 'usage',
461
468
  inputTokens: 0, // Only in message_start
462
- outputTokens: event.usage.output_tokens || 0,
469
+ outputTokens,
463
470
  };
464
471
  }
465
472
  } else if (type === 'message_stop') {
@@ -468,10 +475,14 @@ export class AnthropicAdapter extends LLMAdapter {
468
475
  sawMessageStart = true;
469
476
  // Usage from message_start
470
477
  if (event.message?.usage) {
478
+ cumulativeOutputTokens = Math.max(
479
+ cumulativeOutputTokens,
480
+ Math.max(0, Number(event.message.usage.output_tokens) || 0),
481
+ );
471
482
  yield {
472
483
  type: 'usage',
473
484
  inputTokens: event.message.usage.input_tokens || 0,
474
- outputTokens: event.message.usage.output_tokens || 0,
485
+ outputTokens: cumulativeOutputTokens,
475
486
  cacheReadTokens: event.message.usage.cache_read_input_tokens || 0,
476
487
  cacheWriteTokens: event.message.usage.cache_creation_input_tokens || 0,
477
488
  };
@@ -518,7 +529,7 @@ export class AnthropicAdapter extends LLMAdapter {
518
529
  * models silently drop the param. max_tokens auto-widens to budget+1024
519
530
  * when needed.
520
531
  */
521
- async call({ model, system, messages, maxTokens = 4096, effort, effortSource, effortContext, signal }) {
532
+ async call({ model, system, messages, maxTokens = 4096, effort, effortSource, effortContext, signal, onRequestStart }) {
522
533
  if (signal?.aborted) throw new LLMAbortError();
523
534
 
524
535
  const body = {
@@ -536,6 +547,7 @@ export class AnthropicAdapter extends LLMAdapter {
536
547
 
537
548
  let response;
538
549
  try {
550
+ onRequestStart?.();
539
551
  response = await fetch(`${this.#baseUrl}/v1/messages`, {
540
552
  method: 'POST',
541
553
  headers: this.#headers(),
@@ -257,7 +257,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
257
257
  * `api-key` headers are auto-redacted (see `redactRawRequest` in
258
258
  * `adapter.js`); request-body fields are caller-controlled.
259
259
  */
260
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange }) {
260
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange, onRequestStart }) {
261
261
  if (signal?.aborted) throw new LLMAbortError();
262
262
 
263
263
  const body = {
@@ -297,6 +297,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
297
297
 
298
298
  let response;
299
299
  try {
300
+ onRequestStart?.();
300
301
  response = await fetch(url, {
301
302
  method: 'POST',
302
303
  headers,
@@ -516,7 +517,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
516
517
  * expose them, mirror the stream() instrumentation. Parity with
517
518
  * anthropic.js's `call()`.
518
519
  */
519
- async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal }) {
520
+ async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal, onRequestStart }) {
520
521
  if (signal?.aborted) throw new LLMAbortError();
521
522
 
522
523
  const body = {
@@ -540,6 +541,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
540
541
 
541
542
  let response;
542
543
  try {
544
+ onRequestStart?.();
543
545
  response = await fetch(`${this.#baseUrl}/responses`, {
544
546
  method: 'POST',
545
547
  headers: {
@@ -43,14 +43,10 @@ function addUsage(total, usage) {
43
43
  total.totalTokens += normalized.totalTokens;
44
44
  }
45
45
 
46
- function hasUsage(usage) {
47
- return Object.values(usage).some(value => value > 0);
48
- }
49
-
50
46
  /**
51
47
  * Wrap the shared adapter at the provider-call boundary. Every stream request
52
- * reports once after its event stream finishes (or aborts after returning
53
- * usage), and every non-streaming side call reports once after success.
48
+ * reports once after its event stream finishes or aborts, and every
49
+ * non-streaming side call reports once whether it succeeds or fails.
54
50
  *
55
51
  * Parent VP engines, sub-agent engines, Dream, compact, reflection, AMS, and
56
52
  * classifiers all reuse this adapter, so none need their own accounting hook.
@@ -58,15 +54,35 @@ function hasUsage(usage) {
58
54
  export class UsageAccountingAdapter extends LLMAdapter {
59
55
  #adapter;
60
56
  #onUsage;
57
+ #onRequest;
61
58
 
62
- constructor(adapter, onUsage) {
59
+ constructor(adapter, onUsage, onRequest = null) {
63
60
  super(adapter?.config || {});
64
61
  this.#adapter = adapter;
65
62
  this.#onUsage = onUsage;
63
+ this.#onRequest = onRequest;
64
+ }
65
+
66
+ #requestParams(params) {
67
+ if (typeof this.#onRequest !== 'function') return params;
68
+ const upstream = params?.onRequestStart;
69
+ return {
70
+ ...params,
71
+ onRequestStart: () => {
72
+ try {
73
+ upstream?.();
74
+ } finally {
75
+ try {
76
+ this.#onRequest();
77
+ } catch (error) {
78
+ console.warn(`[llm-usage] request callback failed: ${error?.message || error}`);
79
+ }
80
+ }
81
+ },
82
+ };
66
83
  }
67
84
 
68
85
  #report(usage) {
69
- if (!hasUsage(usage)) return;
70
86
  try {
71
87
  this.#onUsage(usage);
72
88
  } catch (error) {
@@ -77,7 +93,7 @@ export class UsageAccountingAdapter extends LLMAdapter {
77
93
  async *stream(params) {
78
94
  const total = normalizeTokenUsage();
79
95
  try {
80
- for await (const event of this.#adapter.stream(params)) {
96
+ for await (const event of this.#adapter.stream(this.#requestParams(params))) {
81
97
  if (event?.type === 'usage') addUsage(total, event);
82
98
  yield event;
83
99
  }
@@ -87,9 +103,14 @@ export class UsageAccountingAdapter extends LLMAdapter {
87
103
  }
88
104
 
89
105
  async call(params) {
90
- const result = await this.#adapter.call(params);
91
- this.#report(normalizeTokenUsage(result?.usage || {}));
92
- return result;
106
+ let usage = normalizeTokenUsage();
107
+ try {
108
+ const result = await this.#adapter.call(this.#requestParams(params));
109
+ usage = normalizeTokenUsage(result?.usage || {});
110
+ return result;
111
+ } finally {
112
+ this.#report(usage);
113
+ }
93
114
  }
94
115
 
95
116
  refreshProviders(providers) {
@@ -105,8 +126,8 @@ export class UsageAccountingAdapter extends LLMAdapter {
105
126
  }
106
127
  }
107
128
 
108
- export function withUsageAccounting(adapter, onUsage) {
129
+ export function withUsageAccounting(adapter, onUsage, onRequest = null) {
109
130
  return typeof onUsage === 'function'
110
- ? new UsageAccountingAdapter(adapter, onUsage)
131
+ ? new UsageAccountingAdapter(adapter, onUsage, onRequest)
111
132
  : adapter;
112
133
  }
@@ -82,6 +82,12 @@ function normalizeTerminalResult(result, action) {
82
82
  : null,
83
83
  loopCount: Math.max(0, Number(result.loopCount) || 0),
84
84
  toolCount: Math.max(0, Number(result.toolCount) || 0),
85
+ llmRequestCount: Math.max(0, Number(result.llmRequestCount) || 0),
86
+ inputTokens: Math.max(0, Number(result.inputTokens) || 0),
87
+ outputTokens: Math.max(0, Number(result.outputTokens) || 0),
88
+ cacheReadTokens: Math.max(0, Number(result.cacheReadTokens) || 0),
89
+ cacheWriteTokens: Math.max(0, Number(result.cacheWriteTokens) || 0),
90
+ totalTokens: Math.max(0, Number(result.totalTokens) || 0),
85
91
  acceptanceChecks: Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : [],
86
92
  checkpoint: result.checkpoint && typeof result.checkpoint === 'object'
87
93
  ? result.checkpoint : null,
@@ -7,22 +7,45 @@ function count(value) {
7
7
  return Math.max(0, Number(value) || 0);
8
8
  }
9
9
 
10
+ function emptyExecutionStats() {
11
+ return {
12
+ llmRequestCount: 0,
13
+ loopCount: 0,
14
+ toolCount: 0,
15
+ inputTokens: 0,
16
+ outputTokens: 0,
17
+ cacheReadTokens: 0,
18
+ cacheWriteTokens: 0,
19
+ totalTokens: 0,
20
+ };
21
+ }
22
+
23
+ function executionStats(value) {
24
+ const stats = emptyExecutionStats();
25
+ for (const key of Object.keys(stats)) stats[key] = count(value?.[key]);
26
+ return stats;
27
+ }
28
+
29
+ function sumExecutionStats(values) {
30
+ return values.reduce((total, value) => {
31
+ const next = executionStats(value);
32
+ for (const key of Object.keys(total)) total[key] += next[key];
33
+ return total;
34
+ }, emptyExecutionStats());
35
+ }
36
+
10
37
  function actionExecution(action, runs) {
11
38
  const matchingRuns = Array.isArray(runs)
12
39
  ? runs.filter(run => run?.actionId === action?.id)
13
40
  : [];
14
41
  if (matchingRuns.length === 0) {
15
42
  return {
16
- loopCount: count(action?.loopCount),
17
- toolCount: count(action?.toolCount),
43
+ ...executionStats(action),
18
44
  response: '',
19
45
  progressRevision: 0,
20
46
  };
21
47
  }
22
- const stats = matchingRuns.reduce((total, run) => ({
23
- loopCount: total.loopCount + count(run.loopCount),
24
- toolCount: total.toolCount + count(run.toolCount),
25
- }), { loopCount: 0, toolCount: 0 });
48
+ const stats = sumExecutionStats(matchingRuns);
26
49
  const latest = [...matchingRuns].sort((left, right) => (
27
50
  count(right.progressRevision) - count(left.progressRevision)
28
51
  || count(right.startedAt) - count(left.startedAt)
@@ -65,6 +88,7 @@ function projectAction(action, runs) {
65
88
  assignmentPolicy: projectAssignmentPolicy(action.assignmentPolicy),
66
89
  requiredRole: action.requiredRole || '',
67
90
  status: action.status,
91
+ executionStats: executionStats(execution),
68
92
  loopCount: execution.loopCount,
69
93
  toolCount: execution.toolCount,
70
94
  response: execution.response,
@@ -79,6 +103,7 @@ function projectActionStats(detail) {
79
103
  return {
80
104
  id: projected.id,
81
105
  status: projected.status,
106
+ executionStats: projected.executionStats,
82
107
  loopCount: projected.loopCount,
83
108
  toolCount: projected.toolCount,
84
109
  response: projected.response,
@@ -97,7 +122,7 @@ function waitingReason(detail) {
97
122
 
98
123
  /**
99
124
  * Authenticated browser detail DTO. Raw execution records stay Agent-local;
100
- * the browser receives only Action status/counts plus the explicit user-facing response.
125
+ * the browser receives only aggregate execution stats plus the explicit user-facing response.
101
126
  */
102
127
  export function projectWorkItemDetail(detail) {
103
128
  if (!detail) return null;
@@ -112,6 +137,7 @@ export function projectWorkItemDetail(detail) {
112
137
  planningMode: detail.workflowSnapshot?.planningMode || 'static',
113
138
  status: detail.status,
114
139
  currentActionId: detail.currentActionId || null,
140
+ executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
115
141
  reuseMemory: detail.reuseMemory !== false,
116
142
  waitingReason: waitingReason(detail),
117
143
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
@@ -142,6 +168,7 @@ export function projectWorkItemSummary(detail) {
142
168
  status: detail.status,
143
169
  currentActionId: detail.currentActionId || null,
144
170
  currentAction: null,
171
+ executionStats: executionStats(detail.executionStats),
145
172
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
146
173
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
147
174
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
@@ -160,6 +187,7 @@ export function projectWorkItemSummary(detail) {
160
187
  planningMode: detail.workflowSnapshot?.planningMode || 'static',
161
188
  status: detail.status,
162
189
  currentActionId: detail.currentActionId || null,
190
+ executionStats: sumExecutionStats(Array.isArray(detail.runs) ? detail.runs : []),
163
191
  currentAction: projectedAction ? {
164
192
  id: projectedAction.id,
165
193
  type: projectedAction.type,
@@ -12,6 +12,7 @@ import { formatPickedForInjection } from '../sessions/pre-flow.js';
12
12
  import { existsSync, lstatSync, realpathSync } from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { buildWorkItemAttachmentContext } from './attachments.js';
15
+ import { withUsageAccounting } from '../llm/usage-accounting.js';
15
16
  import {
16
17
  appendCheckpointToolEvent,
17
18
  renderActionResumeBlock,
@@ -499,34 +500,24 @@ export class WorkItemRunner {
499
500
  );
500
501
  if (!snapshotsWritten) throw new Error('Work Center Run lost its lease before execution');
501
502
 
502
- const engine = new Engine({
503
- adapter: runtime.adapter,
504
- trace: new NullTrace(),
505
- config,
506
- conversationStore: null,
507
- memoryIndex: null,
508
- amsRegistry: null,
509
- toolRegistry,
510
- skillManager: null,
511
- mcpManager: null,
512
- // WorkItem execution has its own durable Run/Event record. Do not let
513
- // the Engine create Session exec logs, archives, or shared tool stats.
514
- yeaftDir: null,
515
- toolStats: null,
516
- taskManager: null,
517
- vpId: vp.id,
518
- });
519
-
520
503
  let text = '';
521
504
  let loopCount = 0;
522
505
  let toolCount = 0;
523
506
  let checkpoint = null;
524
507
  const toolInputs = new Map();
508
+ const usageStats = {
509
+ llmRequestCount: 0,
510
+ inputTokens: 0,
511
+ outputTokens: 0,
512
+ cacheReadTokens: 0,
513
+ cacheWriteTokens: 0,
514
+ totalTokens: 0,
515
+ };
525
516
  let lastProgressAt = 0;
517
+ const executionStats = () => ({ loopCount, toolCount, ...usageStats });
526
518
  const currentProgress = () => ({
527
519
  response: publicWorkItemResponse(text),
528
- loopCount,
529
- toolCount,
520
+ ...executionStats(),
530
521
  checkpoint,
531
522
  });
532
523
  const reportProgress = (force = false) => {
@@ -537,6 +528,34 @@ export class WorkItemRunner {
537
528
  return onProgress(currentProgress());
538
529
  };
539
530
  if (typeof registerProgressReader === 'function') registerProgressReader(currentProgress);
531
+ const adapter = withUsageAccounting(runtime.adapter, usage => {
532
+ usageStats.inputTokens += usage.inputTokens;
533
+ usageStats.outputTokens += usage.outputTokens;
534
+ usageStats.cacheReadTokens += usage.cacheReadTokens;
535
+ usageStats.cacheWriteTokens += usage.cacheWriteTokens;
536
+ usageStats.totalTokens += usage.totalTokens;
537
+ reportProgress(true);
538
+ }, () => {
539
+ usageStats.llmRequestCount += 1;
540
+ reportProgress(true);
541
+ });
542
+ const engine = new Engine({
543
+ adapter,
544
+ trace: new NullTrace(),
545
+ config,
546
+ conversationStore: null,
547
+ memoryIndex: null,
548
+ amsRegistry: null,
549
+ toolRegistry,
550
+ skillManager: null,
551
+ mcpManager: null,
552
+ // WorkItem execution has its own durable Run/Event record. Do not let
553
+ // the Engine create Session exec logs, archives, or shared tool stats.
554
+ yeaftDir: null,
555
+ toolStats: null,
556
+ taskManager: null,
557
+ vpId: vp.id,
558
+ });
540
559
  try {
541
560
  const prompt = `${executionAction.instruction}${resumeBlock}${attachmentContext.promptBlock}${memoryBlock}${completionContract(executionAction, workItem)}`;
542
561
  const promptParts = attachmentContext.promptParts.length > 0
@@ -567,15 +586,10 @@ export class WorkItemRunner {
567
586
  });
568
587
  }
569
588
  if (event?.type === 'text_delta' && typeof event.text === 'string') text += event.text;
570
- reportProgress();
589
+ reportProgress(event?.type === 'loop');
571
590
  }
572
591
  } catch (error) {
573
- error.workItemExecutionStats = {
574
- response: publicWorkItemResponse(text),
575
- loopCount,
576
- toolCount,
577
- checkpoint,
578
- };
592
+ error.workItemExecutionStats = currentProgress();
579
593
  throw error;
580
594
  } finally {
581
595
  try { engine.abort?.('work_item_run_finished'); } catch {}
@@ -585,8 +599,7 @@ export class WorkItemRunner {
585
599
  return {
586
600
  ...parseStructuredResult(text, executionAction.type),
587
601
  response,
588
- loopCount,
589
- toolCount,
602
+ ...executionStats(),
590
603
  checkpoint,
591
604
  };
592
605
  }
@@ -48,6 +48,16 @@ function mapWorkItem(row) {
48
48
  origin: parseJson(row.origin, null),
49
49
  linkedSessionIds: parseJson(row.linked_session_ids, []),
50
50
  attachments: parseJson(row.attachments, []),
51
+ executionStats: {
52
+ llmRequestCount: Math.max(0, Number(row.usage_llm_request_count) || 0),
53
+ loopCount: Math.max(0, Number(row.usage_loop_count) || 0),
54
+ toolCount: Math.max(0, Number(row.usage_tool_count) || 0),
55
+ inputTokens: Math.max(0, Number(row.usage_input_tokens) || 0),
56
+ outputTokens: Math.max(0, Number(row.usage_output_tokens) || 0),
57
+ cacheReadTokens: Math.max(0, Number(row.usage_cache_read_tokens) || 0),
58
+ cacheWriteTokens: Math.max(0, Number(row.usage_cache_write_tokens) || 0),
59
+ totalTokens: Math.max(0, Number(row.usage_total_tokens) || 0),
60
+ },
51
61
  createdAt: row.created_at,
52
62
  updatedAt: row.updated_at,
53
63
  };
@@ -102,6 +112,12 @@ function mapRun(row) {
102
112
  contractPatch: parseJson(row.contract_patch, null),
103
113
  loopCount: Math.max(0, Number(row.loop_count) || 0),
104
114
  toolCount: Math.max(0, Number(row.tool_count) || 0),
115
+ llmRequestCount: Math.max(0, Number(row.llm_request_count) || 0),
116
+ inputTokens: Math.max(0, Number(row.input_tokens) || 0),
117
+ outputTokens: Math.max(0, Number(row.output_tokens) || 0),
118
+ cacheReadTokens: Math.max(0, Number(row.cache_read_tokens) || 0),
119
+ cacheWriteTokens: Math.max(0, Number(row.cache_write_tokens) || 0),
120
+ totalTokens: Math.max(0, Number(row.total_tokens) || 0),
105
121
  progressRevision: Math.max(0, Number(row.progress_revision) || 0),
106
122
  checkpoint: normalizeActionCheckpoint(parseJson(row.checkpoint, null)),
107
123
  };
@@ -220,6 +236,12 @@ export class WorkItemStore {
220
236
  response TEXT NOT NULL DEFAULT '',
221
237
  loop_count INTEGER NOT NULL DEFAULT 0,
222
238
  tool_count INTEGER NOT NULL DEFAULT 0,
239
+ llm_request_count INTEGER NOT NULL DEFAULT 0,
240
+ input_tokens INTEGER NOT NULL DEFAULT 0,
241
+ output_tokens INTEGER NOT NULL DEFAULT 0,
242
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
243
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
244
+ total_tokens INTEGER NOT NULL DEFAULT 0,
223
245
  progress_revision INTEGER NOT NULL DEFAULT 0,
224
246
  checkpoint TEXT
225
247
  );
@@ -274,6 +296,18 @@ export class WorkItemStore {
274
296
  if (!hasColumn(this.db, 'runs', 'tool_count')) {
275
297
  this.db.exec('ALTER TABLE runs ADD COLUMN tool_count INTEGER NOT NULL DEFAULT 0');
276
298
  }
299
+ for (const column of [
300
+ 'llm_request_count',
301
+ 'input_tokens',
302
+ 'output_tokens',
303
+ 'cache_read_tokens',
304
+ 'cache_write_tokens',
305
+ 'total_tokens',
306
+ ]) {
307
+ if (!hasColumn(this.db, 'runs', column)) {
308
+ this.db.exec(`ALTER TABLE runs ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`);
309
+ }
310
+ }
277
311
  if (!hasColumn(this.db, 'runs', 'response')) {
278
312
  this.db.exec("ALTER TABLE runs ADD COLUMN response TEXT NOT NULL DEFAULT ''");
279
313
  }
@@ -437,22 +471,32 @@ export class WorkItemStore {
437
471
  const where = [];
438
472
  const values = [];
439
473
  if (typeof filters.status === 'string' && filters.status) {
440
- where.push('status = ?');
474
+ where.push('w.status = ?');
441
475
  values.push(filters.status);
442
476
  }
443
477
  if (typeof filters.sessionId === 'string' && filters.sessionId.trim()) {
444
478
  const sessionId = filters.sessionId.trim();
445
- where.push('(instr(origin, ?) > 0 OR instr(linked_session_ids, ?) > 0)');
479
+ where.push('(instr(w.origin, ?) > 0 OR instr(w.linked_session_ids, ?) > 0)');
446
480
  values.push(`\"sessionId\":${JSON.stringify(sessionId)}`, JSON.stringify(sessionId));
447
481
  }
448
482
  if (typeof filters.search === 'string' && filters.search.trim()) {
449
- where.push('(title LIKE ? OR goal LIKE ?)');
483
+ where.push('(w.title LIKE ? OR w.goal LIKE ?)');
450
484
  const query = `%${filters.search.trim()}%`;
451
485
  values.push(query, query);
452
486
  }
453
487
  const limit = Math.min(Math.max(Number(filters.limit) || 100, 1), 500);
454
- const sql = `SELECT * FROM work_items ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
455
- ORDER BY updated_at DESC LIMIT ?`;
488
+ const sql = `SELECT w.*,
489
+ COALESCE(SUM(r.llm_request_count), 0) AS usage_llm_request_count,
490
+ COALESCE(SUM(r.loop_count), 0) AS usage_loop_count,
491
+ COALESCE(SUM(r.tool_count), 0) AS usage_tool_count,
492
+ COALESCE(SUM(r.input_tokens), 0) AS usage_input_tokens,
493
+ COALESCE(SUM(r.output_tokens), 0) AS usage_output_tokens,
494
+ COALESCE(SUM(r.cache_read_tokens), 0) AS usage_cache_read_tokens,
495
+ COALESCE(SUM(r.cache_write_tokens), 0) AS usage_cache_write_tokens,
496
+ COALESCE(SUM(r.total_tokens), 0) AS usage_total_tokens
497
+ FROM work_items w LEFT JOIN runs r ON r.work_item_id = w.id
498
+ ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
499
+ GROUP BY w.id ORDER BY w.updated_at DESC LIMIT ?`;
456
500
  return this.db.prepare(sql).all(...values, limit).map(mapWorkItem);
457
501
  }
458
502
 
@@ -762,7 +806,8 @@ export class WorkItemStore {
762
806
  const hasFinalProgress = finalProgress && typeof finalProgress === 'object';
763
807
  const runChanged = hasFinalProgress
764
808
  ? this.db.prepare(`UPDATE runs SET status = 'interrupted', ended_at = ?, error = ?,
765
- response = ?, loop_count = ?, tool_count = ?, checkpoint = ?,
809
+ response = ?, loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?,
810
+ output_tokens = ?, cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?, checkpoint = ?,
766
811
  progress_revision = progress_revision + 1
767
812
  WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'`).run(
768
813
  now,
@@ -770,6 +815,12 @@ export class WorkItemStore {
770
815
  normalizeRunResponse(finalProgress.response),
771
816
  Math.max(0, Number(finalProgress.loopCount) || 0),
772
817
  Math.max(0, Number(finalProgress.toolCount) || 0),
818
+ Math.max(0, Number(finalProgress.llmRequestCount) || 0),
819
+ Math.max(0, Number(finalProgress.inputTokens) || 0),
820
+ Math.max(0, Number(finalProgress.outputTokens) || 0),
821
+ Math.max(0, Number(finalProgress.cacheReadTokens) || 0),
822
+ Math.max(0, Number(finalProgress.cacheWriteTokens) || 0),
823
+ Math.max(0, Number(finalProgress.totalTokens) || 0),
773
824
  stringify(normalizeActionCheckpoint(finalProgress.checkpoint)),
774
825
  runId,
775
826
  ownerBootId,
@@ -835,12 +886,19 @@ export class WorkItemStore {
835
886
  const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
836
887
  if (!active) return null;
837
888
  const checkpoint = normalizeActionCheckpoint(progress.checkpoint);
838
- const result = this.db.prepare(`UPDATE runs SET response = ?, loop_count = ?, tool_count = ?, checkpoint = ?,
839
- progress_revision = progress_revision + 1 WHERE id = ? AND owner_boot_id = ?
840
- AND lease_epoch = ? AND status = 'running'`).run(
889
+ const result = this.db.prepare(`UPDATE runs SET response = ?, loop_count = ?, tool_count = ?,
890
+ llm_request_count = ?, input_tokens = ?, output_tokens = ?, cache_read_tokens = ?,
891
+ cache_write_tokens = ?, total_tokens = ?, checkpoint = ?, progress_revision = progress_revision + 1
892
+ WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'`).run(
841
893
  normalizeRunResponse(progress.response),
842
894
  Math.max(0, Number(progress.loopCount) || 0),
843
895
  Math.max(0, Number(progress.toolCount) || 0),
896
+ Math.max(0, Number(progress.llmRequestCount) || 0),
897
+ Math.max(0, Number(progress.inputTokens) || 0),
898
+ Math.max(0, Number(progress.outputTokens) || 0),
899
+ Math.max(0, Number(progress.cacheReadTokens) || 0),
900
+ Math.max(0, Number(progress.cacheWriteTokens) || 0),
901
+ Math.max(0, Number(progress.totalTokens) || 0),
844
902
  stringify(checkpoint),
845
903
  runId,
846
904
  ownerBootId,
@@ -886,7 +944,9 @@ export class WorkItemStore {
886
944
  const now = this.now();
887
945
  this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, response = ?, summary = ?, evidence = ?,
888
946
  waiting_reason = ?, error = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
889
- loop_count = ?, tool_count = ?, progress_revision = progress_revision + 1 WHERE id = ?`).run(
947
+ loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?, output_tokens = ?,
948
+ cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?,
949
+ progress_revision = progress_revision + 1 WHERE id = ?`).run(
890
950
  result.outcome,
891
951
  now,
892
952
  normalizeRunResponse(result.response),
@@ -899,6 +959,12 @@ export class WorkItemStore {
899
959
  stringify(normalizeActionCheckpoint(result.checkpoint)),
900
960
  Math.max(0, Number(result.loopCount) || 0),
901
961
  Math.max(0, Number(result.toolCount) || 0),
962
+ Math.max(0, Number(result.llmRequestCount) || 0),
963
+ Math.max(0, Number(result.inputTokens) || 0),
964
+ Math.max(0, Number(result.outputTokens) || 0),
965
+ Math.max(0, Number(result.cacheReadTokens) || 0),
966
+ Math.max(0, Number(result.cacheWriteTokens) || 0),
967
+ Math.max(0, Number(result.totalTokens) || 0),
902
968
  runId,
903
969
  );
904
970
  this.onTransitionStep?.('after_run_update');
@@ -36,6 +36,8 @@ export class WorkItemWatcher {
36
36
  if (this.timer) clearInterval(this.timer);
37
37
  this.timer = null;
38
38
  const active = Array.from(this.activeRuns.values());
39
+ for (const entry of active) entry.abortController.abort('watcher_stopped');
40
+ await Promise.allSettled(active.map(entry => entry.promise));
39
41
  const failures = [];
40
42
  for (const entry of active) {
41
43
  try {
@@ -50,11 +52,8 @@ export class WorkItemWatcher {
50
52
  } catch (error) {
51
53
  entry.interrupted = false;
52
54
  failures.push(error);
53
- } finally {
54
- entry.abortController.abort('watcher_stopped');
55
55
  }
56
56
  }
57
- await Promise.allSettled(active.map(entry => entry.promise));
58
57
  if (failures.length > 0) {
59
58
  throw new AggregateError(failures, 'Could not persist one or more Work Center interruptions');
60
59
  }
@@ -140,6 +139,12 @@ export class WorkItemWatcher {
140
139
  error: err?.message || String(err),
141
140
  loopCount: err?.workItemExecutionStats?.loopCount || 0,
142
141
  toolCount: err?.workItemExecutionStats?.toolCount || 0,
142
+ llmRequestCount: err?.workItemExecutionStats?.llmRequestCount || 0,
143
+ inputTokens: err?.workItemExecutionStats?.inputTokens || 0,
144
+ outputTokens: err?.workItemExecutionStats?.outputTokens || 0,
145
+ cacheReadTokens: err?.workItemExecutionStats?.cacheReadTokens || 0,
146
+ cacheWriteTokens: err?.workItemExecutionStats?.cacheWriteTokens || 0,
147
+ totalTokens: err?.workItemExecutionStats?.totalTokens || 0,
143
148
  checkpoint: err?.workItemExecutionStats?.checkpoint || null,
144
149
  };
145
150
  }