@yeaft/webchat-agent 1.0.247 → 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.
@@ -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);
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);
357
382
  }
358
383
 
359
- function actionMessages(action, runs, events) {
360
- return messagesForGeneration(action, runs, events, actionGeneration(action?.generation));
361
- }
362
-
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]);
@@ -732,6 +742,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
732
742
  omittedActionCount: originalCount,
733
743
  createdAt: count(workItem.createdAt),
734
744
  updatedAt: count(workItem.updatedAt),
745
+ coordinatorRevision: count(workItem.coordinatorRevision),
735
746
  };
736
747
  if (options.event === true) dto.workItem = minimalWorkItem;
737
748
  else return minimalWorkItem;
@@ -837,14 +848,20 @@ function workItemFailureReason(detail) {
837
848
  * Authenticated browser detail DTO. Raw execution records stay Agent-local;
838
849
  * the browser receives only aggregate execution stats plus the explicit user-facing response.
839
850
  */
840
- export function projectWorkItemDetail(detail) {
851
+ export function projectWorkItemDetail(detail, options = {}) {
841
852
  if (!detail) return null;
842
853
  const liveActionId = bodyActionId(detail);
854
+ const bodyActionEvents = Array.isArray(options.bodyActionEvents)
855
+ ? options.bodyActionEvents
856
+ : detail.events;
843
857
  const mainline = projectMainlineBrowser(detail);
844
858
  const mainlineActionById = new Map((mainline?.actions || []).map(action => [action.id, action]));
845
859
  const projected = {
846
860
  id: detail.id,
847
861
  revision: detail.revision,
862
+ planRevision: count(detail.planRevision),
863
+ ledgerRevision: count(detail.ledgerRevision),
864
+ coordinatorRevision: count(detail.coordinatorRevision),
848
865
  title: detail.title,
849
866
  goal: detail.goal,
850
867
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
@@ -870,8 +887,21 @@ export function projectWorkItemDetail(detail) {
870
887
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
871
888
  messages: (Array.isArray(detail.messages) ? detail.messages : []).slice(-100).map(message => ({
872
889
  id: String(message.id || ''),
890
+ turnId: String(message.turnId || message.id || ''),
891
+ role: message.role === 'assistant' ? 'assistant' : message.role === 'legacy_instruction' ? 'legacy_instruction' : 'user',
873
892
  text: truncateUtf8(message.text || '', MAX_ACTION_MESSAGE_CHARS),
893
+ status: ['thinking', 'completed', 'failed'].includes(message.status) ? message.status : 'completed',
894
+ error: truncateUtf8(message.error || '', MAX_ACTION_DIAGNOSTIC_CHARS) || null,
895
+ decision: message.decision && typeof message.decision === 'object' ? {
896
+ kind: ['answer', 'guide_actions', 'replan'].includes(message.decision.kind)
897
+ ? message.decision.kind : null,
898
+ reason: truncateUtf8(message.decision.reason || '', MAX_ACTION_DIAGNOSTIC_CHARS),
899
+ changedContract: message.decision.changedContract === true,
900
+ affectedActionIds: Array.isArray(message.decision.affectedActionIds)
901
+ ? message.decision.affectedActionIds.map(id => String(id)).slice(0, 8) : [],
902
+ } : null,
874
903
  createdAt: count(message.createdAt),
904
+ updatedAt: count(message.updatedAt || message.createdAt),
875
905
  })),
876
906
  attachments: projectAttachments(detail.attachments),
877
907
  createdAt: detail.createdAt,
@@ -882,7 +912,12 @@ export function projectWorkItemDetail(detail) {
882
912
  : String(detail.actionSummary || ''),
883
913
  actions: Array.isArray(detail.actions)
884
914
  ? detail.actions.map(action => ({
885
- ...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
+ ),
886
921
  ...(mainlineActionById.get(action.id) || {}),
887
922
  }))
888
923
  : [],
@@ -897,9 +932,12 @@ export function projectWorkItemDetail(detail) {
897
932
  export function projectWorkItemSummary(detail) {
898
933
  if (!detail) return null;
899
934
  if (!Array.isArray(detail.actions)) {
900
- return {
935
+ return enforceWorkItemBrowserDtoBudget({
901
936
  id: detail.id,
902
937
  revision: detail.revision,
938
+ planRevision: count(detail.planRevision),
939
+ ledgerRevision: count(detail.ledgerRevision),
940
+ coordinatorRevision: count(detail.coordinatorRevision),
903
941
  title: detail.title,
904
942
  goal: detail.goal,
905
943
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
@@ -911,6 +949,8 @@ export function projectWorkItemSummary(detail) {
911
949
  attentionActionIds: Array.isArray(detail.attentionActionIds) ? detail.attentionActionIds : undefined,
912
950
  currentActionId: detail.currentActionId || null,
913
951
  currentAction: projectCurrentActionSummary(detail.currentAction),
952
+ actionStats: Array.isArray(detail.actionStats)
953
+ ? detail.actionStats.map(action => ({ ...action })) : [],
914
954
  actionCount: count(detail.actionCount),
915
955
  completedActionCount: count(detail.completedActionCount),
916
956
  executionStats: executionStats(detail.executionStats),
@@ -924,13 +964,16 @@ export function projectWorkItemSummary(detail) {
924
964
  attentionAction: detail.attentionAction || null,
925
965
  activeAction: detail.activeAction || null,
926
966
  executors: Array.isArray(detail.executors) ? detail.executors : [],
927
- };
967
+ }, { keepActionId: detail.currentActionId || null });
928
968
  }
929
969
  const action = currentAction(detail);
930
970
  const projectedAction = action ? projectAction(action, detail.runs, detail.events, false) : null;
931
- return {
971
+ return enforceWorkItemBrowserDtoBudget({
932
972
  id: detail.id,
933
973
  revision: detail.revision,
974
+ planRevision: count(detail.planRevision),
975
+ ledgerRevision: count(detail.ledgerRevision),
976
+ coordinatorRevision: count(detail.coordinatorRevision),
934
977
  title: detail.title,
935
978
  goal: detail.goal,
936
979
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
@@ -950,13 +993,14 @@ export function projectWorkItemSummary(detail) {
950
993
  failureReason: workItemFailureReason(detail),
951
994
 
952
995
  currentAction: projectCurrentActionSummary(action, projectedAction),
996
+ actionStats: projectActionStats(detail, null),
953
997
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
954
998
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
955
999
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
956
1000
  createdAt: detail.createdAt,
957
1001
  updatedAt: detail.updatedAt,
958
1002
  ...boardFields(detail),
959
- };
1003
+ }, { keepActionId: action?.id || null });
960
1004
  }
961
1005
 
962
1006
  export function projectWorkCenterEvent(event) {
@@ -970,12 +1014,18 @@ export function projectWorkCenterEvent(event) {
970
1014
  },
971
1015
  };
972
1016
  }
973
- 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);
974
1022
  return enforceWorkItemBrowserDtoBudget({
975
1023
  type,
1024
+ ...(eventActionId ? { actionId: eventActionId } : {}),
1025
+ ...(typeof event?.runId === 'string' && event.runId ? { runId: event.runId } : {}),
976
1026
  workItem: {
977
1027
  ...projectWorkItemSummary(event?.workItem),
978
- actionStats: projectActionStats(event?.workItem),
1028
+ actionStats: projectActionStats(event?.workItem, liveActionId),
979
1029
  },
980
1030
  }, { event: true, keepActionId: liveActionId });
981
1031
  }
@@ -992,7 +1042,7 @@ function projectDebugUsage(value) {
992
1042
  }
993
1043
 
994
1044
  export function projectActionMessagePage(action, runs, events, options = {}) {
995
- const messages = actionMessages(action, runs, events);
1045
+ const messages = actionConversationMessages(action, runs, events);
996
1046
  const requestedCursor = options.cursor == null ? messages.length : Number(options.cursor);
997
1047
  const end = Number.isFinite(requestedCursor)
998
1048
  ? Math.max(0, Math.min(messages.length, Math.floor(requestedCursor)))
@@ -1009,12 +1059,12 @@ export function projectActionMessagePage(action, runs, events, options = {}) {
1009
1059
  }
1010
1060
 
1011
1061
  export function actionThreadIncludesRun(action, runs, runId) {
1012
- return threadRuns(action, runs).some(run => run.id === runId);
1062
+ return conversationRuns(action, runs).some(run => run.id === runId);
1013
1063
  }
1014
1064
 
1015
1065
  export function projectActionRequestIndex(action, entries) {
1016
1066
  const source = Array.isArray(entries) ? entries : [];
1017
- 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));
1018
1068
  return {
1019
1069
  actionId: action.id,
1020
1070
  generation: Math.max(1, count(action.generation) || 1),
@@ -578,7 +578,7 @@ function completionContract(action, workItem) {
578
578
  "acceptanceChecks": ${JSON.stringify(acceptanceChecks)},
579
579
  "waitingReason": null,
580
580
  "error": null${reviewField}${triageField}${planField}
581
- }\nFor completed, provide at least one concrete evidence item and exactly one acceptanceChecks entry for every current acceptance criterion, in the same order, with status passed, deferred, or not_applicable and a non-empty evidence reference. Triage must use its proposed criteria when submitting a contractPatch. Test, approved review, and deliver require every criterion to be passed; if a criterion is not applicable, triage must remove or rewrite it through contractPatch before verification. This is a deterministic submission gate, not independent proof: later test, review, and deliver Actions must verify the claims. A model turn ending is not completion. Use waiting when user or external input is required. Use retryable only for a transient failure. Do not start background jobs or delegate this Action.`;
581
+ }\nFor completed, provide at least one concrete evidence item and exactly one acceptanceChecks entry for every current acceptance criterion, in the same order, with status passed, deferred, or not_applicable and a non-empty evidence reference. Triage must use its proposed criteria when submitting a contractPatch. An intermediate Action may defer criteria outside its task-specific expected result; the final deliver Action, and an approved review with no downstream work, require every criterion to pass. If a criterion is no longer applicable, ask the WorkItem Coordinator to revise the contract instead of pretending it passed. This is a deterministic submission gate, not independent proof: later verification and delivery Actions must verify the claims. A model turn ending is not completion. Use waiting when user or external input is required. Use retryable only for a transient failure. Do not start background jobs or delegate this Action.`;
582
582
  }
583
583
 
584
584
  function safeCheckpointUrl(value) {
@@ -105,6 +105,7 @@ export class WorkCenterService {
105
105
  this.controller = options.controller || new WorkflowController(this.store, {
106
106
  listAvailableVpIds: options.listAvailableVpIds,
107
107
  });
108
+ this.coordinator = options.coordinator || null;
108
109
  this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : () => {};
109
110
  this.watcher = new WorkItemWatcher({
110
111
  store: this.store,
@@ -119,6 +120,20 @@ export class WorkCenterService {
119
120
  this.store.recoverInterruptedRuns(this.ownerBootId);
120
121
  }
121
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
+
122
137
  async handle(op, payload = {}, requestContext = {}) {
123
138
  switch (op) {
124
139
  case 'list': {
@@ -271,14 +286,22 @@ export class WorkCenterService {
271
286
  return { id, deleted: true, cleanupWarning };
272
287
  }
273
288
  case 'work_item_message': {
289
+ if (!this.coordinator) throw new Error('Work Center Coordinator is unavailable');
274
290
  const id = requiredString(payload.id, 'id');
275
- const detail = this.controller.message(id, {
291
+ const turn = this.coordinator.message(id, {
276
292
  text: typeof payload.text === 'string' ? payload.text : '',
277
293
  revision: payload.revision,
294
+ planRevision: payload.planRevision,
295
+ ledgerRevision: payload.ledgerRevision,
296
+ coordinatorRevision: payload.coordinatorRevision,
297
+ }, {
298
+ onUpdate: (type, workItem) => {
299
+ this.watcher.abortInvalidWorkItemRuns(id);
300
+ this.#emit({ type, workItem });
301
+ },
278
302
  });
279
- this.watcher.notifyWorkItemInput(id);
280
- this.#emit({ type: 'work_item.message_added', workItem: detail });
281
- return detail;
303
+ turn.task.catch(() => {});
304
+ return { accepted: true, turnId: turn.detail.messages?.at(-1)?.turnId || null };
282
305
  }
283
306
  case 'retry_action': {
284
307
  const id = requiredString(payload.id, 'id');
@@ -296,6 +319,10 @@ export class WorkCenterService {
296
319
  }
297
320
  case 'action_input': {
298
321
  const id = requiredString(payload.id, 'id');
322
+ const generation = Number(payload.generation);
323
+ if (!Number.isInteger(generation) || generation < 1) {
324
+ throw new Error('generation must be a positive integer');
325
+ }
299
326
  const workItem = this.#requiredItem(id);
300
327
  let addedAttachments = [];
301
328
  let detail;
@@ -308,7 +335,7 @@ export class WorkCenterService {
308
335
  text: typeof payload.text === 'string' ? payload.text : '',
309
336
  actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
310
337
  revision: payload.revision,
311
- generation: payload.generation,
338
+ generation,
312
339
  addedAttachmentCount: addedAttachments.length,
313
340
  addedAttachments,
314
341
  attachments: [...(workItem.attachments || []), ...addedAttachments],
@@ -429,6 +456,7 @@ export class WorkCenterService {
429
456
  }
430
457
 
431
458
  async shutdown() {
459
+ await this.coordinator?.shutdown?.();
432
460
  await this.watcher.stop();
433
461
  try { await this.watcher.runner?.shutdown?.(); } catch {}
434
462
  try { await this.watcher.runner?.trace?.close?.(); } catch {}