@yeaft/webchat-agent 1.0.256 → 1.0.258

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.256",
3
+ "version": "1.0.258",
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",
@@ -8,23 +8,31 @@ export function eventMatchesActionGeneration(event, action) {
8
8
  }
9
9
 
10
10
  export function currentActionInputEventIds(events, action) {
11
- const inputEvents = new Map((Array.isArray(events) ? events : [])
12
- .filter(event => event?.type === 'action.input_added' && event.actionId === action?.id)
11
+ const actionEvents = (Array.isArray(events) ? events : [])
12
+ .filter(event => event?.actionId === action?.id);
13
+ const inputEvents = new Map(actionEvents
14
+ .filter(event => event.type === 'action.input_added')
13
15
  .map(event => [String(event.id), event]));
14
16
  const valid = new Set();
17
+ const superseded = new Set();
15
18
  for (const event of inputEvents.values()) {
16
19
  if (eventMatchesActionGeneration(event, action)) valid.add(String(event.id));
17
20
  }
18
- for (const event of Array.isArray(events) ? events : []) {
19
- if (event?.type !== 'action.input_rebound' || event.actionId !== action?.id
20
- || !eventMatchesActionGeneration(event, action)) continue;
21
+ for (const event of actionEvents) {
21
22
  const sourceEventIds = [
22
23
  event.data?.sourceEventId,
23
24
  ...(Array.isArray(event.data?.sourceEventIds) ? event.data.sourceEventIds : []),
24
- ].filter(value => value != null);
25
+ ].filter(value => value != null).map(String);
26
+ if (event.type === 'action.input_superseded') {
27
+ for (const eventId of sourceEventIds) {
28
+ superseded.add(eventId);
29
+ valid.delete(eventId);
30
+ }
31
+ continue;
32
+ }
33
+ if (event.type !== 'action.input_rebound' || !eventMatchesActionGeneration(event, action)) continue;
25
34
  for (const eventId of sourceEventIds) {
26
- const key = String(eventId);
27
- if (inputEvents.has(key)) valid.add(key);
35
+ if (inputEvents.has(eventId) && !superseded.has(eventId)) valid.add(eventId);
28
36
  }
29
37
  }
30
38
  return valid;
@@ -20,7 +20,7 @@ let shutdownPromise = null;
20
20
  let serviceFactory = null;
21
21
 
22
22
  const BROWSER_DETAIL_OPS = new Set([
23
- 'get', 'create', 'update', 'start', 'cancel', 'action_input', 'retry_action', 'guide', 'retry',
23
+ 'get', 'create', 'update', 'start', 'cancel', 'resume', 'action_input', 'retry_action', 'guide', 'retry',
24
24
  ]);
25
25
  const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
26
26
  // `files` is an internal server-to-Agent field. The browser relay rejects any
@@ -34,6 +34,7 @@ const BROWSER_FILE_FIELDS = Object.freeze({
34
34
  ],
35
35
  action_input: ['id', 'text', 'actionId', 'revision', 'generation', 'files'],
36
36
  retry_action: ['id', 'actionId', 'revision', 'generation'],
37
+ resume: ['id', 'revision'],
37
38
  delete: ['id', 'revision'],
38
39
  guide: ['id', 'guidance', 'actionId', 'revision', 'generation', 'files'],
39
40
  get_action_messages: ['id', 'actionId', 'generation', 'cursor', 'limit'],
@@ -173,6 +173,30 @@ export class WorkflowController {
173
173
  return this.store.getWorkItemDetail(id);
174
174
  }
175
175
 
176
+ resume(id, input = {}) {
177
+ const revision = Number(input.revision);
178
+ if (!Number.isInteger(revision) || revision < 1) {
179
+ throw new Error('revision is required to resume a WorkItem');
180
+ }
181
+ const detail = this.store.resumeWorkItemAtomic(id, revision, workItem => {
182
+ const action = initialActionFor(workItem);
183
+ if (workItem.reuseMemory === false) return action;
184
+ const context = this.store.getReusableContext(workItem.workDir, workItem.id);
185
+ return {
186
+ ...action,
187
+ context,
188
+ instruction: actionInstruction(
189
+ action,
190
+ workItem,
191
+ context,
192
+ renderSessionContextSnapshot(workItem.sessionContext),
193
+ ),
194
+ };
195
+ });
196
+ if (!detail) throw new Error(`WorkItem not found: ${id}`);
197
+ return detail;
198
+ }
199
+
176
200
  guide(id, input = {}) {
177
201
  const guidance = typeof input.guidance === 'string' ? input.guidance.trim().slice(0, 8_000) : '';
178
202
  const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
@@ -307,6 +307,13 @@ export class WorkCenterService {
307
307
  this.#emit({ type: 'work_item.cancelled', workItem: detail });
308
308
  return detail;
309
309
  }
310
+ case 'resume': {
311
+ const id = requiredString(payload.id, 'id');
312
+ const detail = this.controller.resume(id, { revision: payload.revision });
313
+ this.watcher.abortInvalidWorkItemRuns(id);
314
+ this.#emit({ type: 'work_item.resumed', workItem: detail });
315
+ return detail;
316
+ }
310
317
  case 'delete': {
311
318
  const id = requiredString(payload.id, 'id');
312
319
  const deleted = this.store.deleteWorkItemAtomic(id, Number(payload.revision));
@@ -8,7 +8,7 @@ import { currentActionInputEventIds, runMatchesActionIdentity } from './action-i
8
8
  import { canonicalActionInstruction, withoutActionInputContext } from './workflow.js';
9
9
 
10
10
  const SCHEMA_VERSION = 22;
11
- const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
11
+ const UNFINISHED_ACTION_STATUSES = "'ready','running','waiting','failed'";
12
12
  const MAX_REUSABLE_CONTEXT_ITEMS = 12;
13
13
  const MAX_RUN_RESPONSE_CHARS = 65_536;
14
14
 
@@ -2643,16 +2643,32 @@ export class WorkItemStore {
2643
2643
  }
2644
2644
 
2645
2645
  #invalidateExecution(workItem, actionStatus, runStatus, reason, now) {
2646
- const openActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
2647
- AND status IN (${OPEN_ACTION_STATUSES})`).all(workItem.id).map(mapAction);
2648
- this.#assertNoIntegrationReservation(openActions, now);
2649
- this.#supersedePendingActionInputs(openActions, reason, now);
2650
- if (workItem.currentRunId) {
2651
- this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, error = ?
2652
- WHERE id = ? AND status = 'running'`).run(runStatus, now, reason, workItem.currentRunId);
2646
+ const unfinishedActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
2647
+ AND status IN (${UNFINISHED_ACTION_STATUSES})`).all(workItem.id).map(mapAction);
2648
+ this.#assertNoIntegrationReservation(unfinishedActions, now);
2649
+ this.#supersedePendingActionInputs(unfinishedActions, reason, now);
2650
+ const actionIds = unfinishedActions.map(action => action.id);
2651
+ if (actionIds.length === 0) return;
2652
+ const placeholders = actionIds.map(() => '?').join(',');
2653
+ this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, error = ?, accepting_input = 0
2654
+ WHERE work_item_id = ? AND action_id IN (${placeholders}) AND status = 'running'`).run(
2655
+ runStatus,
2656
+ now,
2657
+ reason,
2658
+ workItem.id,
2659
+ ...actionIds,
2660
+ );
2661
+ const changed = this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL,
2662
+ lease_epoch = lease_epoch + 1, updated_at = ? WHERE work_item_id = ?
2663
+ AND id IN (${placeholders}) AND status IN (${UNFINISHED_ACTION_STATUSES})`).run(
2664
+ actionStatus,
2665
+ now,
2666
+ workItem.id,
2667
+ ...actionIds,
2668
+ );
2669
+ if (Number(changed.changes) !== actionIds.length) {
2670
+ throw new Error('Work Center execution changed while it was invalidated');
2653
2671
  }
2654
- this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL, updated_at = ?
2655
- WHERE work_item_id = ? AND status IN (${OPEN_ACTION_STATUSES})`).run(actionStatus, now, workItem.id);
2656
2672
  }
2657
2673
 
2658
2674
  updateWorkItemAtomic(id, patch, makeInitialAction) {
@@ -2732,6 +2748,93 @@ export class WorkItemStore {
2732
2748
  });
2733
2749
  }
2734
2750
 
2751
+ resumeWorkItemAtomic(id, expectedRevision, makeInitialAction) {
2752
+ return withTransaction(this.db, () => {
2753
+ const workItem = this.getWorkItem(id);
2754
+ if (!workItem) return null;
2755
+ if (!Number.isInteger(expectedRevision) || workItem.revision !== expectedRevision) {
2756
+ throw new Error('WorkItem changed before it was resumed; refresh and try again');
2757
+ }
2758
+ if (workItem.status !== 'cancelled') {
2759
+ throw new Error(`WorkItem in ${workItem.status} cannot be resumed`);
2760
+ }
2761
+
2762
+ const now = this.now();
2763
+ const cancelledActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
2764
+ AND status = 'cancelled' ORDER BY sequence`).all(id).map(mapAction);
2765
+ this.#assertNoIntegrationReservation(cancelledActions, now);
2766
+ this.#supersedePendingActionInputs(
2767
+ cancelledActions,
2768
+ 'WorkItem resume started a new Action generation',
2769
+ now,
2770
+ );
2771
+ const resumedActions = [];
2772
+ for (const action of cancelledActions) {
2773
+ const context = carryCurrentActionInputContext(this.db, action);
2774
+ const candidate = {
2775
+ ...action,
2776
+ context,
2777
+ status: 'ready',
2778
+ attempt: 0,
2779
+ currentRunId: null,
2780
+ resultRunId: null,
2781
+ workspace: null,
2782
+ generation: action.generation + 1,
2783
+ };
2784
+ candidate.instruction = canonicalActionInstruction(workItem, candidate, context);
2785
+ candidate.specHash = actionSpecHash(candidate);
2786
+ const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
2787
+ current_run_id = NULL, lease_epoch = lease_epoch + 1, generation = generation + 1,
2788
+ context = ?, instruction = ?, spec_hash = ?, identity_history = ?, result_run_id = NULL,
2789
+ workspace = NULL, updated_at = ? WHERE id = ? AND work_item_id = ?
2790
+ AND status = 'cancelled' AND generation = ? AND spec_hash = ?`).run(
2791
+ stringify(context),
2792
+ candidate.instruction,
2793
+ candidate.specHash,
2794
+ stringify(actionIdentityHistory(action, candidate.generation, candidate.specHash)),
2795
+ now,
2796
+ action.id,
2797
+ id,
2798
+ action.generation,
2799
+ action.specHash,
2800
+ );
2801
+ if (Number(changed.changes) !== 1) {
2802
+ throw new Error('WorkItem Action changed before it was resumed');
2803
+ }
2804
+ resumedActions.push(this.getAction(action.id));
2805
+ }
2806
+
2807
+ if (resumedActions.length === 0) {
2808
+ const initialAction = makeInitialAction(workItem);
2809
+ resumedActions.push(this.#insertAction(id, {
2810
+ ...initialAction,
2811
+ contractRevision: workItem.revision,
2812
+ }, this.#nextSequence(id), now));
2813
+ }
2814
+
2815
+ const currentAction = resumedActions
2816
+ .find(action => action.dependsOnStageIds.length === 0) || resumedActions[0];
2817
+ const changedWorkItem = this.db.prepare(`UPDATE work_items SET status = 'ready',
2818
+ current_action_id = ?, current_run_id = NULL, updated_at = ?
2819
+ WHERE id = ? AND status = 'cancelled' AND revision = ?`).run(
2820
+ currentAction.id,
2821
+ now,
2822
+ id,
2823
+ expectedRevision,
2824
+ );
2825
+ if (Number(changedWorkItem.changes) !== 1) {
2826
+ throw new Error('WorkItem changed before it was resumed; refresh and try again');
2827
+ }
2828
+ this.appendEvent(id, 'work_item.resumed', {
2829
+ resumedActionIds: resumedActions.map(action => action.id),
2830
+ }, {
2831
+ actionId: currentAction.id,
2832
+ actionGeneration: currentAction.generation,
2833
+ });
2834
+ return this.getWorkItemDetail(id);
2835
+ });
2836
+ }
2837
+
2735
2838
  startWorkItemAtomic(id, makeInitialAction) {
2736
2839
  return withTransaction(this.db, () => {
2737
2840
  const workItem = this.getWorkItem(id);