@yeaft/webchat-agent 1.0.248 → 1.0.249

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.248",
3
+ "version": "1.0.249",
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",
@@ -6,7 +6,7 @@ import { scanVpLibrary } from '../vp/vp-store.js';
6
6
  import { WorkCenterService } from './service.js';
7
7
  import { WorkItemRunner } from './runner.js';
8
8
  import { WorkItemCoordinator } from './coordinator.js';
9
- import { projectWorkCenterEvent, projectWorkItemDetail } from './projection.js';
9
+ import { projectWorkCenterEvent } from './projection.js';
10
10
  import { previewWorkCenterPlan } from './planner.js';
11
11
  import { readWorkCenterSettings, writeWorkCenterSettings } from './settings.js';
12
12
  import { defaultWorkCenterStageInstructions } from './workflow.js';
@@ -188,6 +188,7 @@ export async function handleWorkCenterRequest(msg) {
188
188
  const op = typeof msg.op === 'string' ? msg.op : '';
189
189
  try {
190
190
  let data;
191
+ let workCenter = null;
191
192
  if (op === 'get_settings') {
192
193
  data = await readSettingsResponse();
193
194
  } else if (op === 'update_settings') {
@@ -210,13 +211,13 @@ export async function handleWorkCenterRequest(msg) {
210
211
  await resetYeaftSession();
211
212
  data = await readSettingsResponse();
212
213
  } else {
213
- const workCenter = await ensureWorkCenter();
214
+ workCenter = await ensureWorkCenter();
214
215
  const payload = Object.hasOwn(BROWSER_FILE_FIELDS, op)
215
216
  ? browserFilePayload(op, msg.payload)
216
217
  : (BROWSER_ACTION_DEBUG_OPS.has(op) ? browserFilePayload(op, msg.payload) : (msg.payload || {}));
217
218
  data = await workCenter.handle(op, payload);
218
219
  }
219
- if (BROWSER_DETAIL_OPS.has(op)) data = projectWorkItemDetail(data);
220
+ if (BROWSER_DETAIL_OPS.has(op)) data = workCenter.projectBrowserDetail(data);
220
221
  send({
221
222
  type: 'work_center_response',
222
223
  requestId,
@@ -6,7 +6,7 @@ import {
6
6
  sanitizeDebugValue,
7
7
  sanitizeDiagnosticText,
8
8
  } from './debug-projection.js';
9
- import { eventMatchesActionGeneration, runMatchesActionIdentity } from './action-identity.js';
9
+ import { runMatchesActionIdentity } from './action-identity.js';
10
10
  import { taskSpecificActionBrief } from './workflow.js';
11
11
  import { buildMainlineProjection } from './mainline-projection.js';
12
12
 
@@ -175,17 +175,30 @@ function runSpecHash(run) {
175
175
  : '';
176
176
  }
177
177
 
178
- function threadRuns(action, runs) {
179
- const source = (Array.isArray(runs) ? runs : []).filter(run => run?.actionId === action?.id);
180
- const selectedSpecByGeneration = new Map((Array.isArray(action?.identityHistory) ? action.identityHistory : [])
181
- .filter(identity => typeof identity?.specHash === 'string' && identity.specHash)
182
- .map(identity => [actionGeneration(identity.generation), identity.specHash]));
178
+ function conversationIdentitySelection(action) {
183
179
  const currentGeneration = actionGeneration(action?.generation);
180
+ const generations = new Set([currentGeneration]);
181
+ const selectedSpecByGeneration = new Map();
182
+ for (const identity of Array.isArray(action?.identityHistory) ? action.identityHistory : []) {
183
+ const generation = Number(identity?.generation);
184
+ if (!Number.isInteger(generation) || generation <= 0 || generation > currentGeneration) continue;
185
+ const specHash = typeof identity?.specHash === 'string' ? identity.specHash : '';
186
+ if (!specHash && generation !== 1) continue;
187
+ generations.add(generation);
188
+ if (specHash) selectedSpecByGeneration.set(generation, specHash);
189
+ }
184
190
  if (typeof action?.specHash === 'string' && action.specHash) {
185
191
  selectedSpecByGeneration.set(currentGeneration, action.specHash);
186
192
  }
193
+ return { generations, selectedSpecByGeneration };
194
+ }
195
+
196
+ function conversationRuns(action, runs) {
197
+ const source = (Array.isArray(runs) ? runs : []).filter(run => run?.actionId === action?.id);
198
+ const { generations, selectedSpecByGeneration } = conversationIdentitySelection(action);
187
199
  return source.filter(run => {
188
200
  const generation = runGeneration(run);
201
+ if (!generations.has(generation)) return false;
189
202
  const selectedSpec = selectedSpecByGeneration.get(generation) || '';
190
203
  const spec = runSpecHash(run);
191
204
  return selectedSpec ? spec === selectedSpec : generation === 1 && !spec;
@@ -231,7 +244,13 @@ function compareProjectedMessages(left, right) {
231
244
  if (leftEventId != null && rightEventId != null) {
232
245
  return compareEventIds(leftEventId, rightEventId);
233
246
  }
234
- return (left?.role === 'user' ? -1 : 1)
247
+ const leftRole = left?.role === 'user' ? 0 : 1;
248
+ const rightRole = right?.role === 'user' ? 0 : 1;
249
+ if (leftRole !== rightRole) return leftRole - rightRole;
250
+ const generationOrder = count(left?.generation) - count(right?.generation);
251
+ if (generationOrder) return generationOrder;
252
+ const attemptOrder = count(left?.attempt) - count(right?.attempt);
253
+ return attemptOrder
235
254
  || String(left?.id || '').localeCompare(String(right?.id || ''));
236
255
  }
237
256
 
@@ -260,7 +279,7 @@ function normalizeProjectedMessage(message) {
260
279
  };
261
280
  }
262
281
 
263
- function actionInputMessages(action, events, generation = actionGeneration(action?.generation), includeThreadIdentity = false) {
282
+ function actionInputMessages(action, events, generation = actionGeneration(action?.generation)) {
264
283
  return (Array.isArray(events) ? events : [])
265
284
  .filter(event => event?.actionId === action?.id
266
285
  && actionGeneration(event.actionGeneration) === generation
@@ -273,29 +292,30 @@ function actionInputMessages(action, events, generation = actionGeneration(actio
273
292
  text: event.data?.text || event.data?.guidance || '',
274
293
  attachments: event.data?.attachments,
275
294
  createdAt: event.createdAt,
276
- ...(includeThreadIdentity ? { generation } : {}),
277
295
  }))
278
296
  .filter(Boolean);
279
297
  }
280
298
 
281
- function runResponseMessage(run, includeThreadIdentity = false) {
299
+ function runResponseMessage(run) {
300
+ const response = typeof run?.response === 'string' ? run.response : '';
301
+ const text = response.trim() || (run?.status === 'failed' ? failedRunMessageText(run) : '');
282
302
  return normalizeProjectedMessage({
283
303
  id: `run:${run.id}`,
284
304
  role: 'assistant',
285
305
  kind: 'response',
286
306
  status: run.status || 'running',
287
- text: typeof run.response === 'string' ? run.response : '',
288
- createdAt: count(run.startedAt),
307
+ text,
308
+ createdAt: count(run.endedAt || run.startedAt),
289
309
  updatedAt: count(run.endedAt || run.startedAt),
290
310
  progressRevision: count(run.progressRevision),
291
- generation: includeThreadIdentity ? runGeneration(run) : null,
292
- attempt: includeThreadIdentity ? run.actionAttempt : null,
311
+ generation: runGeneration(run),
312
+ attempt: Math.max(1, count(run.actionAttempt) || 1),
293
313
  runId: run.id,
294
314
  speaker: run.vpSnapshot,
295
315
  });
296
316
  }
297
317
 
298
- function loopOutputMessages(action, events, matchingRuns, generation = actionGeneration(action?.generation), includeThreadIdentity = false) {
318
+ function loopOutputMessages(action, events, matchingRuns, generation = actionGeneration(action?.generation)) {
299
319
  const runById = new Map(matchingRuns.map(run => [run.id, run]));
300
320
  const projected = [];
301
321
  let previousTranscriptEvent = null;
@@ -320,9 +340,9 @@ function loopOutputMessages(action, events, matchingRuns, generation = actionGen
320
340
  kind: 'response',
321
341
  status: 'completed',
322
342
  text: event.data?.response || '',
323
- createdAt: event.createdAt,
324
- generation: includeThreadIdentity ? event.actionGeneration ?? event.data?.actionGeneration : null,
325
- attempt: includeThreadIdentity ? event.data?.actionAttempt : null,
343
+ createdAt: count(run.endedAt || event.createdAt),
344
+ generation: runGeneration(run),
345
+ attempt: Math.max(1, count(run.actionAttempt) || 1),
326
346
  runId: event.runId,
327
347
  speaker: run.vpSnapshot,
328
348
  });
@@ -336,66 +356,44 @@ function loopOutputMessages(action, events, matchingRuns, generation = actionGen
336
356
  return projected;
337
357
  }
338
358
 
339
- function messagesForGeneration(action, runs, events, generation, includeThreadIdentity = false) {
340
- const matchingRuns = threadRuns(action, runs).filter(run => runGeneration(run) === generation);
341
- const matchingRunIds = new Set(matchingRuns.map(run => run.id));
342
- const runsWithLoopOutput = new Set((Array.isArray(events) ? events : [])
343
- .filter(event => event?.actionId === action?.id
344
- && actionGeneration(event.actionGeneration ?? event.data?.actionGeneration) === generation
345
- && matchingRunIds.has(event.runId)
346
- && event.type === 'run.loop_output')
347
- .map(event => event.runId));
359
+ function messagesForGeneration(action, runs, events, generation) {
360
+ const matchingRuns = conversationRuns(action, runs).filter(run => (
361
+ runGeneration(run) === generation && run?.status !== 'running'
362
+ ));
363
+ const failedWithoutResponse = new Set(matchingRuns
364
+ .filter(run => run?.status === 'failed' && !String(run?.response || '').trim())
365
+ .map(run => run.id));
366
+ const loopMessages = loopOutputMessages(
367
+ action,
368
+ events,
369
+ matchingRuns.filter(run => !failedWithoutResponse.has(run.id)),
370
+ generation,
371
+ );
372
+ const runsWithLoopOutput = new Set(loopMessages.map(message => message.runId).filter(Boolean));
348
373
  return [
349
- ...actionInputMessages(action, events, generation, includeThreadIdentity),
350
- ...loopOutputMessages(action, events, matchingRuns, generation, includeThreadIdentity),
374
+ ...actionInputMessages(action, events, generation),
375
+ ...loopMessages,
351
376
  ...matchingRuns
352
- .sort((left, right) => count(left.startedAt) - count(right.startedAt))
353
- .filter(run => !runsWithLoopOutput.has(run.id))
354
- .map(run => runResponseMessage(run, includeThreadIdentity))
355
- .filter(Boolean)]
356
- .sort(compareProjectedMessages);
357
- }
358
-
359
- function actionMessages(action, runs, events) {
360
- return messagesForGeneration(action, runs, events, actionGeneration(action?.generation));
377
+ .sort((left, right) => count(left.startedAt) - count(right.startedAt))
378
+ .filter(run => !runsWithLoopOutput.has(run.id))
379
+ .map(run => runResponseMessage(run))
380
+ .filter(Boolean),
381
+ ].sort(compareProjectedMessages);
361
382
  }
362
383
 
363
- function projectActionThread(action, runs, events) {
364
- const allRuns = threadRuns(action, runs);
365
- const generations = new Set([
366
- actionGeneration(action?.generation),
367
- ...allRuns.map(runGeneration),
368
- ...(Array.isArray(events) ? events : [])
369
- .filter(event => event?.actionId === action?.id
370
- && ['action.guidance_added', 'action.input_added', 'run.loop_output'].includes(event.type))
371
- .map(event => actionGeneration(event.actionGeneration ?? event.data?.actionGeneration)),
372
- ]);
373
- return [...generations].sort((left, right) => left - right).map(generation => {
374
- const canonical = generation === actionGeneration(action?.generation);
375
- return {
376
- generation,
377
- canonical,
378
- messages: canonical ? [] : messagesForGeneration(action, allRuns, events, generation, true).slice(-MAX_ACTION_MESSAGES),
379
- runs: allRuns.filter(run => runGeneration(run) === generation)
380
- .sort((left, right) => count(left.startedAt) - count(right.startedAt) || String(left.id).localeCompare(String(right.id)))
381
- .map(run => ({
382
- id: run.id,
383
- attempt: Math.max(1, count(run.actionAttempt) || 1),
384
- status: run.status || 'running',
385
- startedAt: count(run.startedAt),
386
- endedAt: count(run.endedAt),
387
- progressRevision: count(run.progressRevision),
388
- loopCount: count(run.loopCount),
389
- toolCount: count(run.toolCount),
390
- })),
391
- };
392
- }).filter(entry => entry.messages.length > 0 || entry.runs.length > 0 || entry.canonical);
384
+ function actionConversationMessages(action, runs, events) {
385
+ const allRuns = conversationRuns(action, runs);
386
+ const { generations } = conversationIdentitySelection(action);
387
+ return [...generations]
388
+ .flatMap(generation => messagesForGeneration(action, allRuns, events, generation))
389
+ .sort(compareProjectedMessages);
393
390
  }
394
391
 
395
392
 
396
393
 
397
394
  const MAX_FAILURE_REASON_LENGTH = 2_000;
398
395
  const MAX_FAILURE_INSPECTION_LENGTH = 16_000;
396
+ const DEFAULT_FAILED_RUN_MESSAGE = 'The Action failed.';
399
397
  const SAFE_FAILURE_FALLBACK = 'The Action failed. Sensitive details were omitted.';
400
398
  const CREDENTIAL_ASSIGNMENT_PATTERN = /\b(?:[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)|api[_-]?key|access[_-]?token|authorization|password|secret|token)\s*[:=]/i;
401
399
  const PROVIDER_TOKEN_PATTERN = /\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{12,}|github_pat_[A-Za-z0-9_]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|AKIA[A-Z0-9]{12,})\b/i;
@@ -429,6 +427,14 @@ function sanitizeFailureDiagnostic(value) {
429
427
  return sanitizeDiagnosticText(raw, MAX_ACTION_DIAGNOSTIC_CHARS);
430
428
  }
431
429
 
430
+ function failedRunMessageText(run) {
431
+ const diagnostics = [
432
+ sanitizeFailureDiagnostic(run?.summary),
433
+ sanitizeFailureDiagnostic(run?.error),
434
+ ].filter(Boolean);
435
+ return [...new Set(diagnostics)].join('\n\n') || DEFAULT_FAILED_RUN_MESSAGE;
436
+ }
437
+
432
438
  function sanitizeFailureReason(value) {
433
439
  const raw = typeof value === 'string'
434
440
  ? value.trim().slice(0, MAX_FAILURE_INSPECTION_LENGTH)
@@ -446,21 +452,25 @@ function sanitizeFailureReason(value) {
446
452
 
447
453
 
448
454
  function actionExecution(action, runs, events, includeBody = true) {
455
+ let conversationMessages = Array.isArray(runs)
456
+ ? actionConversationMessages(action, runs, events)
457
+ : [];
458
+ if (conversationMessages.length === 0 && Array.isArray(action?.messages)) {
459
+ conversationMessages = action.messages.map(normalizeProjectedMessage).filter(Boolean);
460
+ }
461
+ const messageCount = Array.isArray(runs)
462
+ ? conversationMessages.length
463
+ : count(action?.messageCount) || conversationMessages.length;
464
+ const messages = includeBody ? conversationMessages.slice(-MAX_ACTION_MESSAGES) : [];
465
+ const messageCursor = Array.isArray(runs)
466
+ ? (includeBody
467
+ ? (messageCount > messages.length ? String(messageCount - messages.length) : null)
468
+ : (messageCount > 0 ? String(messageCount) : null))
469
+ : action?.messageCursor == null ? null : String(action.messageCursor);
449
470
  const matchingRuns = Array.isArray(runs)
450
471
  ? runs.filter(run => run?.actionId === action?.id && runMatchesActionIdentity(run, action))
451
472
  : [];
452
473
  if (matchingRuns.length === 0) {
453
- const inputMessageCount = Array.isArray(events) ? events.filter(event => (
454
- event?.actionId === action?.id
455
- && eventMatchesActionGeneration(event, action)
456
- && ['action.guidance_added', 'action.input_added'].includes(event.type)
457
- )).length : 0;
458
- const messages = includeBody
459
- ? (Array.isArray(action?.messages)
460
- ? action.messages.slice(-MAX_ACTION_MESSAGES).map(normalizeProjectedMessage).filter(Boolean)
461
- : actionInputMessages(action, events))
462
- : [];
463
- const messageCount = count(action?.messageCount) || (includeBody ? messages.length : inputMessageCount);
464
474
  return {
465
475
  ...executionStats(action?.executionStats || action),
466
476
  response: includeBody && typeof action?.response === 'string' ? action.response : '',
@@ -479,11 +489,8 @@ function actionExecution(action, runs, events, includeBody = true) {
479
489
  messages,
480
490
  liveMessage: includeBody ? normalizeProjectedMessage(action?.liveMessage) : null,
481
491
  messageCount,
482
- messageCursor: action?.messageCursor == null
483
- ? (!includeBody && messageCount > 0 ? String(messageCount) : null)
484
- : String(action.messageCursor),
492
+ messageCursor,
485
493
  failureReason: sanitizeFailureReason(action?.failureReason),
486
-
487
494
  };
488
495
  }
489
496
  const stats = sumExecutionStats(matchingRuns);
@@ -496,16 +503,6 @@ function actionExecution(action, runs, events, includeBody = true) {
496
503
  .filter(run => run?.status === 'failed')
497
504
  .sort((left, right) => count(right.endedAt || right.startedAt) - count(left.endedAt || left.startedAt))[0]
498
505
  : null;
499
- const inputMessageCount = includeBody
500
- ? 0
501
- : (Array.isArray(events) ? events : []).filter(event => (
502
- event?.actionId === action?.id
503
- && eventMatchesActionGeneration(event, action)
504
- && ['action.guidance_added', 'action.input_added'].includes(event.type)
505
- )).length;
506
- const allMessages = includeBody ? actionMessages(action, matchingRuns, events) : [];
507
- const totalMessageCount = includeBody ? allMessages.length : matchingRuns.length + inputMessageCount;
508
- const messages = allMessages.slice(-MAX_ACTION_MESSAGES);
509
506
  const liveMessage = includeBody ? runResponseMessage(latest) : null;
510
507
  return {
511
508
  ...stats,
@@ -520,14 +517,11 @@ function actionExecution(action, runs, events, includeBody = true) {
520
517
  failedAt: count(latestFailure.endedAt || latestFailure.startedAt),
521
518
  } : null,
522
519
  failureReason: sanitizeFailureReason(latestFailure?.error),
523
-
524
520
  progressRevision: count(latest?.progressRevision),
525
521
  messages,
526
522
  liveMessage,
527
- messageCount: totalMessageCount,
528
- messageCursor: includeBody
529
- ? (totalMessageCount > messages.length ? String(totalMessageCount - messages.length) : null)
530
- : (totalMessageCount > 0 ? String(totalMessageCount) : null),
523
+ messageCount,
524
+ messageCursor,
531
525
  };
532
526
  }
533
527
 
@@ -607,7 +601,6 @@ function projectAction(action, runs, events, includeBody = true) {
607
601
  response: execution.response,
608
602
  failure: execution.failure,
609
603
  messages: execution.messages,
610
- thread: Array.isArray(runs) ? projectActionThread(action, runs, events) : (action.thread || []),
611
604
  liveMessage: execution.liveMessage,
612
605
  } : {}),
613
606
  };
@@ -641,10 +634,14 @@ function stripActionBody(action, keepFailure = false) {
641
634
  return projected;
642
635
  }
643
636
 
644
- function projectActionStats(detail) {
645
- if (!Array.isArray(detail?.actions)) return [];
646
- const liveActionId = bodyActionId(detail);
647
- return detail.actions.map(action => {
637
+ function canonicalActions(detail) {
638
+ return Array.isArray(detail?.actions)
639
+ ? detail.actions.filter(action => !['superseded', 'cancelled'].includes(action?.status))
640
+ : [];
641
+ }
642
+
643
+ function projectActionStats(detail, liveActionId = bodyActionId(detail)) {
644
+ return canonicalActions(detail).map(action => {
648
645
  const projected = projectAction(
649
646
  action,
650
647
  detail.runs,
@@ -661,6 +658,7 @@ function projectActionStats(detail) {
661
658
  loopCount: projected.loopCount,
662
659
  toolCount: projected.toolCount,
663
660
  progressRevision: projected.progressRevision,
661
+ attempt: Math.max(0, count(action?.attempt)),
664
662
  };
665
663
  if (projected.failureReason) stats.failureReason = projected.failureReason;
666
664
  if (projected.id === liveActionId) {
@@ -676,12 +674,24 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
676
674
  if (!value || jsonByteLength(value) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return value;
677
675
  const dto = value;
678
676
  const workItem = options.event === true ? dto.workItem : dto;
679
- const actions = Array.isArray(workItem?.actions)
677
+ let actions = Array.isArray(workItem?.actions)
680
678
  ? workItem.actions
681
679
  : Array.isArray(workItem?.actionStats) ? workItem.actionStats : [];
682
680
  const keepId = options.keepActionId || workItem?.currentActionId || actions.at(-1)?.id || null;
683
681
  workItem.truncated = true;
684
682
 
683
+ if (!Array.isArray(workItem.actions) && Array.isArray(workItem.actionStats)) {
684
+ workItem.actionStats = actions.map(action => ({
685
+ id: action?.id,
686
+ generation: actionGeneration(action?.generation),
687
+ status: action?.status,
688
+ progressRevision: count(action?.progressRevision),
689
+ attempt: Math.max(0, count(action?.attempt)),
690
+ }));
691
+ actions = workItem.actionStats;
692
+ if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
693
+ }
694
+
685
695
  for (let index = 0; index < actions.length; index += 1) {
686
696
  if (actions[index]?.id === keepId) continue;
687
697
  actions[index] = stripActionBody(actions[index]);
@@ -838,9 +848,12 @@ function workItemFailureReason(detail) {
838
848
  * Authenticated browser detail DTO. Raw execution records stay Agent-local;
839
849
  * the browser receives only aggregate execution stats plus the explicit user-facing response.
840
850
  */
841
- export function projectWorkItemDetail(detail) {
851
+ export function projectWorkItemDetail(detail, options = {}) {
842
852
  if (!detail) return null;
843
853
  const liveActionId = bodyActionId(detail);
854
+ const bodyActionEvents = Array.isArray(options.bodyActionEvents)
855
+ ? options.bodyActionEvents
856
+ : detail.events;
844
857
  const mainline = projectMainlineBrowser(detail);
845
858
  const mainlineActionById = new Map((mainline?.actions || []).map(action => [action.id, action]));
846
859
  const projected = {
@@ -899,7 +912,12 @@ export function projectWorkItemDetail(detail) {
899
912
  : String(detail.actionSummary || ''),
900
913
  actions: Array.isArray(detail.actions)
901
914
  ? detail.actions.map(action => ({
902
- ...projectAction(action, detail.runs, detail.events, action?.id === liveActionId),
915
+ ...projectAction(
916
+ action,
917
+ detail.runs,
918
+ action?.id === liveActionId ? bodyActionEvents : detail.events,
919
+ action?.id === liveActionId,
920
+ ),
903
921
  ...(mainlineActionById.get(action.id) || {}),
904
922
  }))
905
923
  : [],
@@ -914,7 +932,7 @@ export function projectWorkItemDetail(detail) {
914
932
  export function projectWorkItemSummary(detail) {
915
933
  if (!detail) return null;
916
934
  if (!Array.isArray(detail.actions)) {
917
- return {
935
+ return enforceWorkItemBrowserDtoBudget({
918
936
  id: detail.id,
919
937
  revision: detail.revision,
920
938
  planRevision: count(detail.planRevision),
@@ -931,6 +949,8 @@ export function projectWorkItemSummary(detail) {
931
949
  attentionActionIds: Array.isArray(detail.attentionActionIds) ? detail.attentionActionIds : undefined,
932
950
  currentActionId: detail.currentActionId || null,
933
951
  currentAction: projectCurrentActionSummary(detail.currentAction),
952
+ actionStats: Array.isArray(detail.actionStats)
953
+ ? detail.actionStats.map(action => ({ ...action })) : [],
934
954
  actionCount: count(detail.actionCount),
935
955
  completedActionCount: count(detail.completedActionCount),
936
956
  executionStats: executionStats(detail.executionStats),
@@ -944,11 +964,11 @@ export function projectWorkItemSummary(detail) {
944
964
  attentionAction: detail.attentionAction || null,
945
965
  activeAction: detail.activeAction || null,
946
966
  executors: Array.isArray(detail.executors) ? detail.executors : [],
947
- };
967
+ }, { keepActionId: detail.currentActionId || null });
948
968
  }
949
969
  const action = currentAction(detail);
950
970
  const projectedAction = action ? projectAction(action, detail.runs, detail.events, false) : null;
951
- return {
971
+ return enforceWorkItemBrowserDtoBudget({
952
972
  id: detail.id,
953
973
  revision: detail.revision,
954
974
  planRevision: count(detail.planRevision),
@@ -973,13 +993,14 @@ export function projectWorkItemSummary(detail) {
973
993
  failureReason: workItemFailureReason(detail),
974
994
 
975
995
  currentAction: projectCurrentActionSummary(action, projectedAction),
996
+ actionStats: projectActionStats(detail, null),
976
997
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
977
998
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
978
999
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
979
1000
  createdAt: detail.createdAt,
980
1001
  updatedAt: detail.updatedAt,
981
1002
  ...boardFields(detail),
982
- };
1003
+ }, { keepActionId: action?.id || null });
983
1004
  }
984
1005
 
985
1006
  export function projectWorkCenterEvent(event) {
@@ -993,12 +1014,18 @@ export function projectWorkCenterEvent(event) {
993
1014
  },
994
1015
  };
995
1016
  }
996
- const liveActionId = bodyActionId(event?.workItem);
1017
+ const eventActionId = typeof event?.actionId === 'string'
1018
+ && event.workItem?.actions?.some(action => action?.id === event.actionId)
1019
+ ? event.actionId
1020
+ : null;
1021
+ const liveActionId = eventActionId || bodyActionId(event?.workItem);
997
1022
  return enforceWorkItemBrowserDtoBudget({
998
1023
  type,
1024
+ ...(eventActionId ? { actionId: eventActionId } : {}),
1025
+ ...(typeof event?.runId === 'string' && event.runId ? { runId: event.runId } : {}),
999
1026
  workItem: {
1000
1027
  ...projectWorkItemSummary(event?.workItem),
1001
- actionStats: projectActionStats(event?.workItem),
1028
+ actionStats: projectActionStats(event?.workItem, liveActionId),
1002
1029
  },
1003
1030
  }, { event: true, keepActionId: liveActionId });
1004
1031
  }
@@ -1015,7 +1042,7 @@ function projectDebugUsage(value) {
1015
1042
  }
1016
1043
 
1017
1044
  export function projectActionMessagePage(action, runs, events, options = {}) {
1018
- const messages = actionMessages(action, runs, events);
1045
+ const messages = actionConversationMessages(action, runs, events);
1019
1046
  const requestedCursor = options.cursor == null ? messages.length : Number(options.cursor);
1020
1047
  const end = Number.isFinite(requestedCursor)
1021
1048
  ? Math.max(0, Math.min(messages.length, Math.floor(requestedCursor)))
@@ -1032,12 +1059,12 @@ export function projectActionMessagePage(action, runs, events, options = {}) {
1032
1059
  }
1033
1060
 
1034
1061
  export function actionThreadIncludesRun(action, runs, runId) {
1035
- return threadRuns(action, runs).some(run => run.id === runId);
1062
+ return conversationRuns(action, runs).some(run => run.id === runId);
1036
1063
  }
1037
1064
 
1038
1065
  export function projectActionRequestIndex(action, entries) {
1039
1066
  const source = Array.isArray(entries) ? entries : [];
1040
- const allowedRunIds = new Set(threadRuns(action, source.map(({ run }) => run)).map(run => run.id));
1067
+ const allowedRunIds = new Set(conversationRuns(action, source.map(({ run }) => run)).map(run => run.id));
1041
1068
  return {
1042
1069
  actionId: action.id,
1043
1070
  generation: Math.max(1, count(action.generation) || 1),
@@ -120,6 +120,20 @@ export class WorkCenterService {
120
120
  this.store.recoverInterruptedRuns(this.ownerBootId);
121
121
  }
122
122
 
123
+ projectBrowserDetail(detail) {
124
+ if (!detail) return null;
125
+ const actions = Array.isArray(detail.actions) ? detail.actions : [];
126
+ const actionId = detail.currentActionId && actions.some(action => action?.id === detail.currentActionId)
127
+ ? detail.currentActionId
128
+ : [...actions].sort((left, right) => (
129
+ Number(right?.sequence || 0) - Number(left?.sequence || 0)
130
+ || String(right?.id || '').localeCompare(String(left?.id || ''))
131
+ ))[0]?.id || null;
132
+ return projectWorkItemDetail(detail, {
133
+ bodyActionEvents: actionId ? this.store.listActionEvents(actionId) : detail.events,
134
+ });
135
+ }
136
+
123
137
  async handle(op, payload = {}, requestContext = {}) {
124
138
  switch (op) {
125
139
  case 'list': {
@@ -2081,6 +2081,7 @@ export class WorkItemStore {
2081
2081
  const ids = workItems.map(item => item.id);
2082
2082
  const actionsByWorkItem = new Map(ids.map(id => [id, []]));
2083
2083
  for (const row of this.db.prepare(`SELECT * FROM actions WHERE work_item_id IN (${placeholders})
2084
+ AND status NOT IN ('superseded', 'cancelled')
2084
2085
  ORDER BY work_item_id, sequence`).all(...ids)) {
2085
2086
  actionsByWorkItem.get(row.work_item_id).push(mapAction(row));
2086
2087
  }
@@ -166,7 +166,12 @@ export class WorkItemWatcher {
166
166
  response: '', summary: '', evidence: [],
167
167
  error: error?.message || String(error),
168
168
  });
169
- this.onEvent({ type: 'run.finished', workItem: this.store.getWorkItemDetail(claim.workItem.id) });
169
+ this.onEvent({
170
+ type: 'run.finished',
171
+ actionId: claim.action.id,
172
+ runId: claim.run.id,
173
+ workItem: this.store.getWorkItemDetail(claim.workItem.id),
174
+ });
170
175
  continue;
171
176
  }
172
177
  this.#startClaim(claim);
@@ -212,7 +217,12 @@ export class WorkItemWatcher {
212
217
  if (this.lifecycle === 'running') queueMicrotask(() => { this.tick().catch(() => {}); });
213
218
  });
214
219
  this.activeRuns.set(key, entry);
215
- this.onEvent({ type: 'run.started', workItem: this.store.getWorkItemDetail(claim.workItem.id) });
220
+ this.onEvent({
221
+ type: 'run.started',
222
+ actionId: claim.action.id,
223
+ runId: claim.run.id,
224
+ workItem: this.store.getWorkItemDetail(claim.workItem.id),
225
+ });
216
226
  }
217
227
 
218
228
  async #execute(claim, signal, registerProgressReader, registerInputWake) {
@@ -232,7 +242,14 @@ export class WorkItemWatcher {
232
242
  claim.run.leaseEpoch,
233
243
  progress,
234
244
  );
235
- if (detail) this.onEvent({ type: 'run.progress', workItem: detail });
245
+ if (detail) {
246
+ this.onEvent({
247
+ type: 'run.progress',
248
+ actionId: claim.action.id,
249
+ runId: claim.run.id,
250
+ workItem: detail,
251
+ });
252
+ }
236
253
  return !!detail;
237
254
  },
238
255
  });
@@ -265,7 +282,12 @@ export class WorkItemWatcher {
265
282
  claim.run.leaseEpoch,
266
283
  result,
267
284
  );
268
- this.onEvent({ type: 'run.finished', workItem });
285
+ this.onEvent({
286
+ type: 'run.finished',
287
+ actionId: claim.action.id,
288
+ runId: claim.run.id,
289
+ workItem,
290
+ });
269
291
  } catch (err) {
270
292
  if (!/stale|cancelled|already finished/i.test(err?.message || '')) throw err;
271
293
  }