@yeaft/webchat-agent 1.0.211 → 1.0.213

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.211",
3
+ "version": "1.0.213",
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",
@@ -42,6 +42,77 @@ function projectCurrentActionSummary(action, projectedAction = action) {
42
42
  assignmentMode: projectedAction.assignmentPolicy?.mode || (projectedAction.requiredRole ? 'fixed' : null),
43
43
  status: projectedAction.status,
44
44
  objective: truncateUtf8(action?.brief?.objective, 1_000) || null,
45
+ ...(projectedAction.assignedVp ? { assignedVp: projectedAction.assignedVp } : {}),
46
+ };
47
+ }
48
+
49
+ const BOARD_ACTION_STATUSES = ['completed', 'running', 'ready', 'waiting', 'failed'];
50
+
51
+ function boardActionCounts(actions) {
52
+ const counts = Object.fromEntries(BOARD_ACTION_STATUSES.map(status => [status, 0]));
53
+ for (const action of Array.isArray(actions) ? actions : []) {
54
+ if (Object.hasOwn(counts, action?.status)) counts[action.status] += 1;
55
+ }
56
+ return counts;
57
+ }
58
+
59
+ export function workItemBoardLane(detail) {
60
+ const actions = (Array.isArray(detail?.actions) ? detail.actions : [])
61
+ .filter(action => !['superseded', 'cancelled'].includes(action?.status));
62
+ if (['done', 'cancelled'].includes(detail?.status)) return 'closed';
63
+ if (['draft', 'waiting', 'needs_attention'].includes(detail?.status)
64
+ || actions.some(action => ['waiting', 'failed'].includes(action?.status))) {
65
+ return 'needs_attention';
66
+ }
67
+ return 'active';
68
+ }
69
+
70
+ function boardActionSummary(action, runs, events) {
71
+ if (!action) return null;
72
+ const projected = projectAction(action, runs, events, false);
73
+ return {
74
+ id: projected.id,
75
+ type: projected.type,
76
+ stageId: projected.stageId,
77
+ status: projected.status,
78
+ objective: truncateUtf8(action?.brief?.objective, 1_000) || null,
79
+ assignedVp: projected.assignedVp || null,
80
+ };
81
+ }
82
+
83
+ function boardFields(detail) {
84
+ const actions = (Array.isArray(detail?.actions) ? detail.actions : [])
85
+ .filter(action => !['superseded', 'cancelled'].includes(action?.status));
86
+ const runs = Array.isArray(detail?.runs) ? detail.runs : [];
87
+ const events = Array.isArray(detail?.events) ? detail.events : [];
88
+ const bySequence = (left, right) => count(left?.sequence) - count(right?.sequence)
89
+ || String(left?.id || '').localeCompare(String(right?.id || ''));
90
+ const attentionAction = [...actions]
91
+ .filter(action => ['waiting', 'failed'].includes(action?.status))
92
+ .sort((left, right) => {
93
+ const priority = { waiting: 0, failed: 1 };
94
+ return priority[left.status] - priority[right.status] || bySequence(left, right);
95
+ })[0] || null;
96
+ const activeAction = [...actions]
97
+ .filter(action => ['running', 'ready'].includes(action?.status))
98
+ .sort((left, right) => {
99
+ const priority = { running: 0, ready: 1 };
100
+ return priority[left.status] - priority[right.status] || bySequence(left, right);
101
+ })[0] || null;
102
+ const executors = [];
103
+ const seenExecutors = new Set();
104
+ for (const action of actions) {
105
+ const assignedVp = projectAction(action, runs, events, false).assignedVp;
106
+ if (!assignedVp?.id || seenExecutors.has(assignedVp.id)) continue;
107
+ seenExecutors.add(assignedVp.id);
108
+ executors.push(assignedVp);
109
+ }
110
+ return {
111
+ boardLane: workItemBoardLane({ ...detail, actions }),
112
+ actionCounts: boardActionCounts(actions),
113
+ attentionAction: boardActionSummary(attentionAction, runs, events),
114
+ activeAction: boardActionSummary(activeAction, runs, events),
115
+ executors,
45
116
  };
46
117
  }
47
118
 
@@ -673,6 +744,11 @@ export function projectWorkItemSummary(detail) {
673
744
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
674
745
  createdAt: detail.createdAt,
675
746
  updatedAt: detail.updatedAt,
747
+ boardLane: detail.boardLane || workItemBoardLane(detail),
748
+ actionCounts: detail.actionCounts || boardActionCounts([]),
749
+ attentionAction: detail.attentionAction || null,
750
+ activeAction: detail.activeAction || null,
751
+ executors: Array.isArray(detail.executors) ? detail.executors : [],
676
752
  };
677
753
  }
678
754
  const action = currentAction(detail);
@@ -704,6 +780,7 @@ export function projectWorkItemSummary(detail) {
704
780
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
705
781
  createdAt: detail.createdAt,
706
782
  updatedAt: detail.updatedAt,
783
+ ...boardFields(detail),
707
784
  };
708
785
  }
709
786
 
@@ -44,6 +44,44 @@ function requiredWorkDir(value) {
44
44
  return canonical;
45
45
  }
46
46
 
47
+ function boardCursor(item) {
48
+ return Buffer.from(JSON.stringify([Number(item.updatedAt) || 0, String(item.id || '')]), 'utf8')
49
+ .toString('base64url');
50
+ }
51
+
52
+ function parseBoardCursor(value) {
53
+ if (typeof value !== 'string' || !value) return null;
54
+ try {
55
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
56
+ if (!Array.isArray(parsed) || parsed.length !== 2 || !Number.isFinite(Number(parsed[0]))) return null;
57
+ return [Number(parsed[0]), String(parsed[1] || '')];
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function listBoardItems(store, payload) {
64
+ const limit = Math.min(Math.max(Number(payload.limit) || 100, 1), 200);
65
+ const cursor = parseBoardCursor(payload.cursor);
66
+ const lane = ['needs_attention', 'active', 'closed'].includes(payload.lane) ? payload.lane : null;
67
+ const vpId = typeof payload.vpId === 'string' ? payload.vpId.trim() : '';
68
+ const workItemType = typeof payload.workItemType === 'string' ? payload.workItemType.trim() : '';
69
+ const projected = store.listWorkItems({
70
+ ...payload,
71
+ lane,
72
+ vpId,
73
+ workItemType,
74
+ cursorUpdatedAt: cursor?.[0],
75
+ cursorId: cursor?.[1],
76
+ limit: limit + 1,
77
+ }).map(projectWorkItemSummary);
78
+ const items = projected.slice(0, limit);
79
+ return {
80
+ items,
81
+ nextCursor: projected.length > limit && items.length > 0 ? boardCursor(items.at(-1)) : null,
82
+ };
83
+ }
84
+
47
85
  export class WorkCenterService {
48
86
  constructor(options) {
49
87
  const yeaftDir = requiredString(options?.yeaftDir, 'yeaftDir');
@@ -83,11 +121,13 @@ export class WorkCenterService {
83
121
 
84
122
  async handle(op, payload = {}, requestContext = {}) {
85
123
  switch (op) {
86
- case 'list':
124
+ case 'list': {
125
+ const page = listBoardItems(this.store, payload);
87
126
  return {
88
- items: this.store.listWorkItems(payload).map(projectWorkItemSummary),
127
+ ...page,
89
128
  watcher: this.watcher.status(),
90
129
  };
130
+ }
91
131
  case 'get':
92
132
  return this.#requiredItem(payload.id);
93
133
  case 'get_action_messages': {
@@ -1192,11 +1192,55 @@ export class WorkItemStore {
1192
1192
  where.push('(instr(w.origin, ?) > 0 OR instr(w.linked_session_ids, ?) > 0)');
1193
1193
  values.push(`\"sessionId\":${JSON.stringify(sessionId)}`, JSON.stringify(sessionId));
1194
1194
  }
1195
- if (typeof filters.search === 'string' && filters.search.trim()) {
1195
+ const keyword = typeof filters.keyword === 'string' ? filters.keyword.trim()
1196
+ : typeof filters.search === 'string' ? filters.search.trim() : '';
1197
+ if (keyword) {
1196
1198
  where.push('(w.title LIKE ? OR w.goal LIKE ?)');
1197
- const query = `%${filters.search.trim()}%`;
1199
+ const query = `%${keyword}%`;
1198
1200
  values.push(query, query);
1199
1201
  }
1202
+ const createdFrom = Number(filters.createdFrom);
1203
+ const createdTo = Number(filters.createdTo);
1204
+ const updatedFrom = Number(filters.updatedFrom);
1205
+ const updatedTo = Number(filters.updatedTo);
1206
+ if (Number.isFinite(createdFrom) && createdFrom > 0) { where.push('w.created_at >= ?'); values.push(createdFrom); }
1207
+ if (Number.isFinite(createdTo) && createdTo > 0) { where.push('w.created_at <= ?'); values.push(createdTo); }
1208
+ if (Number.isFinite(updatedFrom) && updatedFrom > 0) { where.push('w.updated_at >= ?'); values.push(updatedFrom); }
1209
+ if (Number.isFinite(updatedTo) && updatedTo > 0) { where.push('w.updated_at <= ?'); values.push(updatedTo); }
1210
+ if (typeof filters.workItemType === 'string' && filters.workItemType.trim()) {
1211
+ where.push('instr(w.workflow_snapshot, ?) > 0');
1212
+ values.push(`\"workItemType\":${JSON.stringify(filters.workItemType.trim())}`);
1213
+ }
1214
+ if (typeof filters.vpId === 'string' && filters.vpId.trim()) {
1215
+ where.push(`EXISTS (SELECT 1 FROM actions executor_action
1216
+ JOIN runs executor_run ON executor_run.id = (
1217
+ SELECT latest_executor_run.id FROM runs latest_executor_run
1218
+ WHERE latest_executor_run.action_id = executor_action.id
1219
+ ORDER BY latest_executor_run.started_at DESC, latest_executor_run.progress_revision DESC
1220
+ LIMIT 1)
1221
+ WHERE executor_action.work_item_id = w.id
1222
+ AND executor_action.status NOT IN ('superseded', 'cancelled')
1223
+ AND instr(executor_run.vp_snapshot, ?) > 0)`);
1224
+ values.push(`\"id\":${JSON.stringify(filters.vpId.trim())}`);
1225
+ }
1226
+ if (filters.lane === 'closed') {
1227
+ where.push("w.status IN ('done', 'cancelled')");
1228
+ } else if (filters.lane === 'needs_attention') {
1229
+ where.push(`w.status NOT IN ('done', 'cancelled') AND (
1230
+ w.status IN ('draft', 'waiting', 'needs_attention') OR EXISTS (
1231
+ SELECT 1 FROM actions attention_action WHERE attention_action.work_item_id = w.id
1232
+ AND attention_action.status IN ('waiting', 'failed')))`);
1233
+ } else if (filters.lane === 'active') {
1234
+ where.push(`w.status NOT IN ('done', 'cancelled', 'draft', 'waiting', 'needs_attention')
1235
+ AND NOT EXISTS (SELECT 1 FROM actions attention_action WHERE attention_action.work_item_id = w.id
1236
+ AND attention_action.status IN ('waiting', 'failed'))`);
1237
+ }
1238
+ const cursorUpdatedAt = Number(filters.cursorUpdatedAt);
1239
+ const cursorId = typeof filters.cursorId === 'string' ? filters.cursorId : '';
1240
+ if (Number.isFinite(cursorUpdatedAt) && cursorUpdatedAt >= 0 && cursorId) {
1241
+ where.push('(w.updated_at < ? OR (w.updated_at = ? AND w.id < ?))');
1242
+ values.push(cursorUpdatedAt, cursorUpdatedAt, cursorId);
1243
+ }
1200
1244
  const limit = Math.min(Math.max(Number(filters.limit) || 100, 1), 500);
1201
1245
  const sql = `SELECT w.*,
1202
1246
  current_action.type AS current_action_type,
@@ -1219,12 +1263,24 @@ export class WorkItemStore {
1219
1263
  LEFT JOIN actions current_action ON current_action.id = w.current_action_id
1220
1264
  LEFT JOIN runs r ON r.work_item_id = w.id
1221
1265
  ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
1222
- GROUP BY w.id ORDER BY w.updated_at DESC LIMIT ?`;
1223
- return this.db.prepare(sql).all(...values, limit).map(mapWorkItem).map(workItem => {
1224
- if (!isGraphWorkItem(workItem)) return workItem;
1225
- const actions = this.db.prepare('SELECT * FROM actions WHERE work_item_id = ? ORDER BY sequence')
1226
- .all(workItem.id).map(mapAction);
1227
- return graphExecutionState(workItem, actions);
1266
+ GROUP BY w.id ORDER BY w.updated_at DESC, w.id DESC LIMIT ?`;
1267
+ const workItems = this.db.prepare(sql).all(...values, limit).map(mapWorkItem);
1268
+ if (workItems.length === 0) return [];
1269
+ const placeholders = workItems.map(() => '?').join(',');
1270
+ const ids = workItems.map(item => item.id);
1271
+ const actionsByWorkItem = new Map(ids.map(id => [id, []]));
1272
+ for (const row of this.db.prepare(`SELECT * FROM actions WHERE work_item_id IN (${placeholders})
1273
+ ORDER BY work_item_id, sequence`).all(...ids)) {
1274
+ actionsByWorkItem.get(row.work_item_id).push(mapAction(row));
1275
+ }
1276
+ const runsByWorkItem = new Map(ids.map(id => [id, []]));
1277
+ for (const row of this.db.prepare(`SELECT * FROM runs WHERE work_item_id IN (${placeholders})
1278
+ ORDER BY work_item_id, started_at DESC`).all(...ids)) {
1279
+ runsByWorkItem.get(row.work_item_id).push(mapRun(row));
1280
+ }
1281
+ return workItems.map(workItem => {
1282
+ const actions = actionsByWorkItem.get(workItem.id) || [];
1283
+ return graphExecutionState({ ...workItem, actions, runs: runsByWorkItem.get(workItem.id) || [] }, actions);
1228
1284
  });
1229
1285
  }
1230
1286