@yeaft/webchat-agent 1.0.295 → 1.0.298

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.
@@ -829,7 +829,14 @@ function projectMainlineBrowser(detail) {
829
829
 
830
830
  function waitingReason(detail) {
831
831
  if (typeof detail?.waitingReason === 'string') return detail.waitingReason;
832
- if (detail?.status !== 'waiting' || !Array.isArray(detail.runs)) return '';
832
+ if (detail?.status !== 'waiting') return '';
833
+ const waitingEvent = Array.isArray(detail?.events)
834
+ ? detail.events.find(event => event?.type === 'action.waiting'
835
+ && event?.actionId === detail.currentActionId
836
+ && typeof event?.data?.reason === 'string')
837
+ : null;
838
+ if (waitingEvent) return waitingEvent.data.reason;
839
+ if (!Array.isArray(detail.runs)) return '';
833
840
  return detail.runs.find(run => (
834
841
  run?.actionId === detail.currentActionId && typeof run.waitingReason === 'string'
835
842
  ))?.waitingReason || '';
@@ -1029,6 +1036,8 @@ export function projectWorkCenterEvent(event) {
1029
1036
  type,
1030
1037
  ...(eventActionId ? { actionId: eventActionId } : {}),
1031
1038
  ...(typeof event?.runId === 'string' && event.runId ? { runId: event.runId } : {}),
1039
+ ...(typeof event?.clientMessageId === 'string' && event.clientMessageId
1040
+ ? { clientMessageId: truncateUtf8(event.clientMessageId, 256) } : {}),
1032
1041
  workItem: {
1033
1042
  ...projectWorkItemSummary(event?.workItem),
1034
1043
  actionStats: projectActionStats(event?.workItem, liveActionId),
@@ -1,5 +1,5 @@
1
1
  import { Engine } from '../engine.js';
2
- import { ToolRegistry } from '../tools/registry.js';
2
+ import { ToolRegistry, isToolErrorOutput, toolErrorEffect } from '../tools/registry.js';
3
3
  import { defineTool } from '../tools/types.js';
4
4
  import { allTools } from '../tools/index.js';
5
5
  import { parsePatch } from '../tools/apply-patch.js';
@@ -265,7 +265,7 @@ export function workItemToolPolicySnapshot(workDir, attachmentRefs = [], mcpTool
265
265
  };
266
266
  }
267
267
 
268
- function wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive) {
268
+ function wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle = null) {
269
269
  return {
270
270
  ...tool,
271
271
  async execute(input, ctx) {
@@ -273,12 +273,28 @@ function wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunAct
273
273
  const checkedInput = tool.name.startsWith('mcp__')
274
274
  ? input
275
275
  : assertToolInput(tool.name, input, canonicalDir, canonicalAttachmentFiles);
276
- const output = await tool.execute(checkedInput, {
277
- ...ctx,
278
- cwd: canonicalDir,
279
- workDir: canonicalDir,
280
- imageAllowlist: canonicalAttachmentFiles.map(file => file.root),
281
- });
276
+ const trackOperation = typeof operationLifecycle === 'function'
277
+ && tool.sideEffectScope !== 'run'
278
+ && tool.isReadOnly?.(checkedInput) !== true;
279
+ const operation = trackOperation ? operationLifecycle(tool.name, checkedInput) : null;
280
+ let output;
281
+ try {
282
+ output = await tool.execute(checkedInput, {
283
+ ...ctx,
284
+ cwd: canonicalDir,
285
+ workDir: canonicalDir,
286
+ imageAllowlist: canonicalAttachmentFiles.map(file => file.root),
287
+ });
288
+ } catch (error) {
289
+ operation?.complete('unknown', { error: String(error?.message || error) });
290
+ throw error;
291
+ }
292
+ const outputHash = hashMainlineSnapshot({ output: String(output || '') });
293
+ const returnedError = tool.errorOutput === 'json-error-envelope' && isToolErrorOutput(output);
294
+ const effectStatus = returnedError
295
+ ? toolErrorEffect(output) === 'none' ? 'failed_no_effect' : 'unknown'
296
+ : 'applied';
297
+ operation?.complete(effectStatus, { outputHash });
282
298
  if (!isRunActive()) throw new Error('Work Center Run lease was lost during tool execution');
283
299
  if (['FileRead', 'ViewImage'].includes(tool.name) && typeof output === 'string') {
284
300
  const withoutFilePaths = canonicalAttachmentFiles.reduce(
@@ -376,6 +392,7 @@ export function createSubmitWorkItemPlanTool({
376
392
  },
377
393
  isConcurrencySafe: () => false,
378
394
  isReadOnly: () => false,
395
+ sideEffectScope: 'run',
379
396
  });
380
397
  }
381
398
 
@@ -434,6 +451,7 @@ export function createProposeWorkItemActionsTool({
434
451
  ctx.requestEndTurn?.({ kind: 'work_item_actions_proposed', proposalId: input.proposalId });
435
452
  return JSON.stringify({ submitted: true, proposalId: input.proposalId, actionCount: input.actions.length });
436
453
  },
454
+ sideEffectScope: 'run',
437
455
  });
438
456
  }
439
457
 
@@ -455,6 +473,7 @@ export function createRequestWorkItemReplanTool({ workItem, collector, isRunActi
455
473
  ctx.requestEndTurn?.({ kind: 'work_item_replan_requested', proposalId: input.proposalId });
456
474
  return JSON.stringify({ submitted: true, proposalId: input.proposalId });
457
475
  },
476
+ sideEffectScope: 'run',
458
477
  });
459
478
  }
460
479
 
@@ -509,10 +528,13 @@ export function createSubmitWorkItemReplanTool({ vps, workItem, action, actions,
509
528
  },
510
529
  isConcurrencySafe: () => false,
511
530
  isReadOnly: () => false,
531
+ sideEffectScope: 'run',
512
532
  });
513
533
  }
514
534
 
515
- export function createWorkItemToolRegistry({ workDir, attachmentFiles = [], isRunActive, mcpTools = [], runTools = [] }) {
535
+ export function createWorkItemToolRegistry({
536
+ workDir, attachmentFiles = [], isRunActive, mcpTools = [], runTools = [], operationLifecycle = null,
537
+ }) {
516
538
  const canonicalDir = canonicalWorkDir(path.resolve(workDir));
517
539
  const canonicalAttachmentFiles = attachmentFiles.map(file => ({
518
540
  ...file,
@@ -522,14 +544,20 @@ export function createWorkItemToolRegistry({ workDir, attachmentFiles = [], isRu
522
544
  const hasAttachments = canonicalAttachmentFiles.length > 0;
523
545
  for (const tool of allTools) {
524
546
  if (!WORK_ITEM_TOOL_ALLOWLIST.has(tool.name) || (hasAttachments && tool.name === 'Bash')) continue;
525
- registry.register(wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive));
547
+ registry.register(wrapWorkItemTool(
548
+ tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
549
+ ));
526
550
  }
527
551
  for (const tool of mcpTools) {
528
552
  if (!tool?.name?.startsWith('mcp__')) continue;
529
- registry.register(wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive));
553
+ registry.register(wrapWorkItemTool(
554
+ tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
555
+ ));
530
556
  }
531
557
  for (const tool of runTools) {
532
- registry.register(wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive));
558
+ registry.register(wrapWorkItemTool(
559
+ tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
560
+ ));
533
561
  }
534
562
  return registry;
535
563
  }
@@ -1016,12 +1044,37 @@ export class WorkItemRunner {
1016
1044
  attachmentContext.files.map(file => file.ref),
1017
1045
  [...mcpToolNames, ...runToolNames],
1018
1046
  );
1047
+ let operationOrdinal = 0;
1048
+ const operationLifecycle = (toolName, input) => {
1049
+ operationOrdinal += 1;
1050
+ const idempotencyKey = `${run.id}:tool:${operationOrdinal}`;
1051
+ const claimed = this.store.createAndClaimOperation({
1052
+ workItemId: workItem.id,
1053
+ actionId: action.id,
1054
+ runId: run.id,
1055
+ operationType: toolName,
1056
+ idempotencyKey,
1057
+ replayPolicy: 'never_automatic',
1058
+ payload: { inputHash: hashMainlineSnapshot(input) },
1059
+ }, ownerBootId, run.leaseEpoch, false);
1060
+ if (!claimed) throw new Error(`Work Center could not claim Operation ${idempotencyKey}`);
1061
+ return {
1062
+ complete: (effectStatus, result) => {
1063
+ if (!this.store.completeOperation(
1064
+ idempotencyKey, ownerBootId, run.leaseEpoch, effectStatus, result,
1065
+ )) {
1066
+ throw new Error(`Work Center Operation ${idempotencyKey} lost its execution fence`);
1067
+ }
1068
+ },
1069
+ };
1070
+ };
1019
1071
  const toolRegistry = createWorkItemToolRegistry({
1020
1072
  workDir,
1021
1073
  attachmentFiles: attachmentContext.files,
1022
1074
  isRunActive,
1023
1075
  mcpTools: workspaceRuntime.mcpTools,
1024
1076
  runTools,
1077
+ operationLifecycle,
1025
1078
  });
1026
1079
  const config = {
1027
1080
  ...runtime.config,
@@ -1136,6 +1189,7 @@ export class WorkItemRunner {
1136
1189
  if (typeof registerInputWake === 'function') {
1137
1190
  registerInputWake(() => engine.wakeForPendingUserMessage?.());
1138
1191
  }
1192
+ const pendingEntriesById = new Map();
1139
1193
  const drainPendingUserMessages = () => {
1140
1194
  const pending = this.store.listPendingActionInputs?.(
1141
1195
  action.id, run.id, ownerBootId, run.leaseEpoch,
@@ -1149,13 +1203,49 @@ export class WorkItemRunner {
1149
1203
  const content = [item.text, attachmentLines.length > 0
1150
1204
  ? `Additional WorkItem attachments:\n${attachmentLines.join('\n')}` : '']
1151
1205
  .filter(Boolean).join('\n\n');
1152
- if (!content || !this.store.acknowledgeActionInput?.(
1153
- item.id, action.id, run.id, ownerBootId, run.leaseEpoch,
1154
- )) continue;
1155
- accepted.push({ content, preview: item.text || '[attachments]' });
1206
+ if (!content) continue;
1207
+ pendingEntriesById.set(String(item.id), item);
1208
+ accepted.push({
1209
+ content,
1210
+ preview: item.text || '[attachments]',
1211
+ durableInputId: String(item.id),
1212
+ });
1156
1213
  }
1157
1214
  return accepted;
1158
1215
  };
1216
+ const prepareProviderRequest = ({ entries, system, messages, model }) => {
1217
+ const durableEntries = entries
1218
+ .filter(entry => entry?.durableInputId)
1219
+ .map(entry => pendingEntriesById.get(String(entry.durableInputId)))
1220
+ .filter(Boolean);
1221
+ const requestBody = { model, system, messages };
1222
+ const turn = this.store.prepareEngineTurn?.(
1223
+ action.id, run.id, ownerBootId, run.leaseEpoch, durableEntries,
1224
+ { requestBody, dispatchCapability: 'unknown' },
1225
+ );
1226
+ if (!turn) throw new Error('Work Center could not persist the next provider turn');
1227
+ return turn;
1228
+ };
1229
+ const startProviderRequest = turn => {
1230
+ if (!turn) return;
1231
+ const claimed = this.store.claimEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch);
1232
+ if (!claimed) throw new Error('Work Center provider turn lost its Run lease before dispatch');
1233
+ };
1234
+ const finishProviderRequest = (turn, result) => {
1235
+ if (!turn) return;
1236
+ if (!this.store.consumeEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch, result)) {
1237
+ throw new Error('Work Center provider response lost its EngineTurn fence');
1238
+ }
1239
+ };
1240
+ const failProviderRequest = (turn, error) => {
1241
+ if (!turn) return;
1242
+ const failure = this.store.failEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch, error);
1243
+ if (failure && failure.allowRetry === false) {
1244
+ error.retryable = false;
1245
+ error.workItemFailureKind = 'provider_dispatch_unknown';
1246
+ error.workItemFailureCode = 'engine_turn_dispatch_unknown';
1247
+ }
1248
+ };
1159
1249
  try {
1160
1250
  const prompt = v2Execution
1161
1251
  ? `${renderMainlineContextSnapshot(mainline.contextSnapshot)}${fixedPromptSuffix}`
@@ -1180,9 +1270,14 @@ export class WorkItemRunner {
1180
1270
  workDir,
1181
1271
  userAlreadyPersisted: true,
1182
1272
  drainPendingUserMessages,
1273
+ prepareProviderRequest,
1274
+ startProviderRequest,
1275
+ finishProviderRequest,
1276
+ failProviderRequest,
1183
1277
  closePendingUserInput: () => this.store.closeRunInput(
1184
1278
  run.id, ownerBootId, run.leaseEpoch,
1185
1279
  ),
1280
+
1186
1281
  collabToolPolicy: 'single-vp',
1187
1282
  })) {
1188
1283
  if (event?.type === 'loop') {
@@ -60,6 +60,14 @@ function parseBoardCursor(value) {
60
60
  }
61
61
  }
62
62
 
63
+ function removeUnpersistedAttachmentFiles(root, workItemId, addedAttachments, detail) {
64
+ if (!Array.isArray(addedAttachments) || addedAttachments.length === 0) return;
65
+ const persistedIds = new Set((Array.isArray(detail?.attachments) ? detail.attachments : [])
66
+ .map(attachment => attachment?.id).filter(Boolean));
67
+ const unpersisted = addedAttachments.filter(attachment => !persistedIds.has(attachment?.id));
68
+ if (unpersisted.length > 0) removeWorkItemAttachmentFiles(root, workItemId, unpersisted);
69
+ }
70
+
63
71
  function listBoardItems(store, payload) {
64
72
  const limit = Math.min(Math.max(Number(payload.limit) || 100, 1), 200);
65
73
  const cursor = parseBoardCursor(payload.cursor);
@@ -106,6 +114,7 @@ export class WorkCenterService {
106
114
  listAvailableVpIds: options.listAvailableVpIds,
107
115
  });
108
116
  this.coordinator = options.coordinator || null;
117
+ if (this.coordinator) this.coordinator.ownerBootId = this.ownerBootId;
109
118
  this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : () => {};
110
119
  this.recoveryTasks = new Map();
111
120
  this.recoveryQueue = new Map();
@@ -315,6 +324,52 @@ export class WorkCenterService {
315
324
  this.#emit({ type: 'work_item.deleted', workItem: { id, revision: deleted.revision } });
316
325
  return { id, deleted: true, cleanupWarning };
317
326
  }
327
+ case 'post_work_item_message': {
328
+ const clientMessageId = requiredString(payload.clientMessageId, 'clientMessageId');
329
+ const target = payload.target && typeof payload.target === 'object' ? payload.target : {};
330
+ if (target.kind === 'coordinator') {
331
+ const receipt = this.store.getCoordinatorClientMessageReceipt(payload.id, clientMessageId);
332
+ if (receipt) return { accepted: true, turnId: receipt.turnId || null, duplicate: true };
333
+ return this.handle('work_item_message', { ...payload, clientMessageId }, requestContext);
334
+ }
335
+ if (target.kind === 'action') {
336
+ if (this.store.hasActionInputClientMessage(payload.id, target.actionId, clientMessageId)) {
337
+ return this.store.getWorkItemDetail(payload.id);
338
+ }
339
+ return this.handle('action_input', {
340
+ ...payload,
341
+ clientMessageId,
342
+ actionId: target.actionId,
343
+ generation: target.generation,
344
+ }, requestContext);
345
+ }
346
+ if (target.kind === 'request') {
347
+ const id = requiredString(payload.id, 'id');
348
+ const workItem = this.#requiredItem(id);
349
+ const action = this.#requiredAction(workItem, target.actionId);
350
+ if (action.status === 'failed' && !String(payload.text || '').trim()
351
+ && (!Array.isArray(payload.files) || payload.files.length === 0)) {
352
+ const detail = this.controller.retry(id, {
353
+ expected: {
354
+ actionId: action.id,
355
+ revision: payload.revision,
356
+ generation: target.generation,
357
+ statuses: ['failed'],
358
+ },
359
+ });
360
+ this.watcher.abortInvalidWorkItemRuns(id);
361
+ this.#emit({ type: 'action.retried', workItem: detail });
362
+ return detail;
363
+ }
364
+ return this.handle('action_input', {
365
+ ...payload,
366
+ clientMessageId,
367
+ actionId: target.actionId,
368
+ generation: target.generation,
369
+ }, requestContext);
370
+ }
371
+ throw new Error('WorkItem message target is invalid');
372
+ }
318
373
  case 'work_item_message': {
319
374
  if (!this.coordinator) throw new Error('Work Center Coordinator is unavailable');
320
375
  const id = requiredString(payload.id, 'id');
@@ -332,12 +387,18 @@ export class WorkCenterService {
332
387
  planRevision: payload.planRevision,
333
388
  ledgerRevision: payload.ledgerRevision,
334
389
  coordinatorRevision: payload.coordinatorRevision,
390
+ clientMessageId: typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null,
335
391
  addedAttachments,
336
392
  attachments: [...(workItem.attachments || []), ...addedAttachments],
337
393
  }, {
338
394
  onUpdate: (type, nextWorkItem) => {
339
395
  this.watcher.abortInvalidWorkItemRuns(id);
340
- this.#emit({ type, workItem: nextWorkItem });
396
+ this.#emit({
397
+ type,
398
+ clientMessageId: typeof payload.clientMessageId === 'string'
399
+ ? payload.clientMessageId : null,
400
+ workItem: nextWorkItem,
401
+ });
341
402
  },
342
403
  });
343
404
  } catch (error) {
@@ -350,6 +411,7 @@ export class WorkCenterService {
350
411
  } catch {}
351
412
  throw error;
352
413
  }
414
+ removeUnpersistedAttachmentFiles(this.attachmentRoot, id, addedAttachments, turn.detail);
353
415
  turn.task.catch(() => {});
354
416
  return { accepted: true, turnId: turn.detail.messages?.at(-1)?.turnId || null };
355
417
  }
@@ -387,6 +449,7 @@ export class WorkCenterService {
387
449
  actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
388
450
  revision: payload.revision,
389
451
  generation,
452
+ clientMessageId: typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null,
390
453
  addedAttachmentCount: addedAttachments.length,
391
454
  addedAttachments,
392
455
  attachments: [...(workItem.attachments || []), ...addedAttachments],
@@ -401,9 +464,15 @@ export class WorkCenterService {
401
464
  } catch {}
402
465
  throw error;
403
466
  }
467
+ removeUnpersistedAttachmentFiles(this.attachmentRoot, id, addedAttachments, detail);
404
468
  this.watcher.abortInvalidWorkItemRuns(id);
405
469
  this.watcher.notifyActionInput(id, payload.actionId);
406
- this.#emit({ type: 'action.input_added', workItem: detail });
470
+ this.#emit({
471
+ type: 'action.input_added',
472
+ actionId: payload.actionId,
473
+ clientMessageId: typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null,
474
+ workItem: detail,
475
+ });
407
476
  return detail;
408
477
  }
409
478
  case 'guide': {
@@ -523,6 +592,35 @@ export class WorkCenterService {
523
592
  this.#drainFailureRecoveryQueue();
524
593
  }
525
594
 
595
+ #scanCoordinatorProviderRecoveries() {
596
+ if (this.shuttingDown || !this.coordinator) return;
597
+ this.store.recoverCoordinatorProviderTurns();
598
+ this.store.recoverCoordinatorMailbox();
599
+ for (const recoverable of this.store.getRecoverableCoordinatorTurns?.() || []) {
600
+ const claim = this.store.claimCoordinatorTurn(
601
+ recoverable.workItemId, recoverable.turnId, this.ownerBootId,
602
+ );
603
+ if (!claim) continue;
604
+ const started = this.store.resumeCoordinatorTurn(
605
+ recoverable.workItemId, recoverable.turnId, claim,
606
+ );
607
+ if (!started) continue;
608
+ const turn = this.coordinator.resume(started, {
609
+ text: recoverable.text || '',
610
+ recovery: Boolean(recoverable.recovery),
611
+ addedAttachments: Array.isArray(recoverable.addedAttachments)
612
+ ? recoverable.addedAttachments : [],
613
+ onUpdate: (type, detail) => this.#emit({ type, workItem: detail }),
614
+ });
615
+ turn?.task?.catch?.(() => {});
616
+ }
617
+ }
618
+
619
+ #scanRecoveries() {
620
+ this.#scanCoordinatorProviderRecoveries();
621
+ this.#scanFailureRecoveries();
622
+ }
623
+
526
624
  #scanFailureRecoveries() {
527
625
  if (this.shuttingDown || !this.coordinator) return;
528
626
  const now = Date.now();
@@ -584,10 +682,10 @@ export class WorkCenterService {
584
682
  }
585
683
 
586
684
  start() {
587
- this.#scanFailureRecoveries();
685
+ this.#scanRecoveries();
588
686
  if (!this.recoveryTimer) {
589
687
  this.recoveryTimer = setInterval(
590
- () => this.#scanFailureRecoveries(),
688
+ () => this.#scanRecoveries(),
591
689
  this.recoveryPollIntervalMs,
592
690
  );
593
691
  this.recoveryTimer.unref?.();