@yeaft/webchat-agent 1.0.207 → 1.0.209

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.207",
3
+ "version": "1.0.209",
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",
@@ -1577,40 +1577,72 @@ export class ConversationStore {
1577
1577
  const limit = Math.min(50, Math.max(1, Number.isFinite(opts.limit) ? Math.floor(opts.limit) : 20));
1578
1578
  const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
1579
1579
  const results = [];
1580
- const seen = new Set();
1581
1580
  let hasMore = false;
1582
1581
 
1583
- for (const message of this.#iterateSessionRows(sessionId, { beforeSeq, desc: true })) {
1584
- if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
1585
- if (message.role !== 'user' && message.role !== 'assistant') continue;
1586
- if (!message.id || seen.has(message.id)) continue;
1587
- seen.add(message.id);
1588
-
1589
- const text = this.#visibleSearchText(message.content);
1582
+ for (const entry of this.#iterateVisibleResponseEntries(sessionId, { beforeSeq })) {
1583
+ const text = entry.textParts.join(' ');
1590
1584
  const matchIndex = text.toLocaleLowerCase().indexOf(needle);
1591
1585
  if (matchIndex < 0) continue;
1592
1586
  if (results.length >= limit) {
1593
1587
  hasMore = true;
1594
1588
  break;
1595
1589
  }
1596
-
1597
- const seq = parseSeqFromId(message.id);
1598
- if (!Number.isFinite(seq)) continue;
1599
1590
  results.push({
1600
- messageId: message.id,
1601
- turnId: message.turnId || message.threadId || message.id,
1602
- seq,
1603
- role: message.role,
1604
- speakerVpId: message.speakerVpId || null,
1605
- timestamp: message.ts || message.time || null,
1591
+ ...this.#projectVisibleResponseEntry(entry),
1606
1592
  snippet: this.#searchSnippet(text, matchIndex, needle.length),
1607
1593
  });
1608
1594
  }
1609
1595
 
1596
+ const lastResult = results[results.length - 1] || null;
1597
+ return {
1598
+ results: results.map(({ _beforeSeq, ...result }) => result),
1599
+ hasMore,
1600
+ nextBeforeSeq: hasMore && lastResult ? lastResult._beforeSeq : null,
1601
+ };
1602
+ }
1603
+
1604
+ /**
1605
+ * Load a lightweight outline page for one Session. Only user and assistant
1606
+ * text metadata is projected; tool payloads, attachments and full message
1607
+ * bodies never leave the Agent through this API.
1608
+ *
1609
+ * @param {string} sessionId
1610
+ * @param {{ limit?: number, beforeSeq?: number|null, includeTotal?: boolean }} [opts]
1611
+ * @returns {{ results: object[], hasMore: boolean, nextBeforeSeq: number|null, totalCount: number|null }}
1612
+ */
1613
+ loadVisibleOutlineBySession(sessionId, opts = {}) {
1614
+ if (!sessionId) return { results: [], hasMore: false, nextBeforeSeq: null, totalCount: 0 };
1615
+
1616
+ const limit = Math.min(100, Math.max(1, Number.isFinite(opts.limit) ? Math.floor(opts.limit) : 50));
1617
+ const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
1618
+ const newestFirst = [];
1619
+ let hasMore = false;
1620
+
1621
+ for (const entry of this.#iterateVisibleResponseEntries(sessionId, { beforeSeq })) {
1622
+ if (newestFirst.length >= limit) {
1623
+ hasMore = true;
1624
+ break;
1625
+ }
1626
+ const projected = this.#projectVisibleResponseEntry(entry);
1627
+ newestFirst.push({
1628
+ ...projected,
1629
+ snippet: this.#outlineSnippet(entry.textParts.join(' ')),
1630
+ });
1631
+ }
1632
+
1633
+ let totalCount = null;
1634
+ if (opts.includeTotal !== false) {
1635
+ totalCount = 0;
1636
+ for (const _entry of this.#iterateVisibleResponseEntries(sessionId)) totalCount += 1;
1637
+ }
1638
+
1639
+ const oldestEntry = newestFirst[newestFirst.length - 1] || null;
1640
+ const results = newestFirst.reverse().map(({ _beforeSeq, ...entry }) => entry);
1610
1641
  return {
1611
1642
  results,
1612
1643
  hasMore,
1613
- nextBeforeSeq: hasMore && results.length > 0 ? results[results.length - 1].seq : null,
1644
+ nextBeforeSeq: hasMore && oldestEntry ? oldestEntry._beforeSeq : null,
1645
+ totalCount,
1614
1646
  };
1615
1647
  }
1616
1648
 
@@ -2381,6 +2413,75 @@ export class ConversationStore {
2381
2413
  };
2382
2414
  }
2383
2415
 
2416
+ *#iterateVisibleResponseEntries(sessionId, opts = {}) {
2417
+ const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
2418
+ const seen = new Set();
2419
+ let current = null;
2420
+
2421
+ const visibleRow = (message) => {
2422
+ if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) return null;
2423
+ if (message.role !== 'user' && message.role !== 'assistant') return null;
2424
+ if (!message.id || seen.has(message.id)) return null;
2425
+ seen.add(message.id);
2426
+ const seq = parseSeqFromId(message.id);
2427
+ if (!Number.isFinite(seq)) return null;
2428
+ const text = this.#visibleSearchText(message.content);
2429
+ const speakerVpId = message.speakerVpId || null;
2430
+ return {
2431
+ message,
2432
+ seq,
2433
+ text,
2434
+ speakerVpId,
2435
+ groupKey: message.role === 'assistant'
2436
+ ? `assistant:${message.turnId || message.id}:${speakerVpId || ''}`
2437
+ : `user:${message.id}`,
2438
+ };
2439
+ };
2440
+ const startEntry = (row) => ({
2441
+ groupKey: row.groupKey,
2442
+ role: row.message.role,
2443
+ turnId: row.message.turnId || row.message.threadId || row.message.id,
2444
+ speakerVpId: row.speakerVpId,
2445
+ oldestSeq: row.seq,
2446
+ anchor: row,
2447
+ anchorHasText: !!row.text,
2448
+ textParts: row.text ? [row.text] : [],
2449
+ });
2450
+ const mergeRow = (entry, row) => {
2451
+ entry.oldestSeq = Math.min(entry.oldestSeq, row.seq);
2452
+ if (row.text) entry.textParts.unshift(row.text);
2453
+ if (!entry.anchorHasText && row.text) {
2454
+ entry.anchor = row;
2455
+ entry.anchorHasText = true;
2456
+ }
2457
+ };
2458
+
2459
+ for (const message of this.#iterateSessionRows(sessionId, { beforeSeq, desc: true })) {
2460
+ const row = visibleRow(message);
2461
+ if (!row) continue;
2462
+ if (current && current.groupKey === row.groupKey) {
2463
+ mergeRow(current, row);
2464
+ continue;
2465
+ }
2466
+ if (current) yield current;
2467
+ current = startEntry(row);
2468
+ }
2469
+ if (current) yield current;
2470
+ }
2471
+
2472
+ #projectVisibleResponseEntry(entry) {
2473
+ return {
2474
+ messageId: entry.anchor.message.id,
2475
+ ...(entry.anchor.message.clientMessageId ? { clientMessageId: entry.anchor.message.clientMessageId } : {}),
2476
+ turnId: entry.turnId,
2477
+ seq: entry.anchor.seq,
2478
+ role: entry.role,
2479
+ speakerVpId: entry.speakerVpId,
2480
+ timestamp: entry.anchor.message.ts || entry.anchor.message.time || null,
2481
+ _beforeSeq: entry.oldestSeq,
2482
+ };
2483
+ }
2484
+
2384
2485
  #visibleSearchText(content) {
2385
2486
  if (typeof content === 'string') return content.replace(/\s+/g, ' ').trim();
2386
2487
  if (!Array.isArray(content)) return '';
@@ -2399,6 +2500,11 @@ export class ConversationStore {
2399
2500
  return `${start > 0 ? '…' : ''}${text.slice(start, end)}${end < text.length ? '…' : ''}`;
2400
2501
  }
2401
2502
 
2503
+ #outlineSnippet(text) {
2504
+ const limit = 180;
2505
+ return text.length > limit ? `${text.slice(0, limit).trimEnd()}…` : text;
2506
+ }
2507
+
2402
2508
  #readSegmentRows(conversationDir, opts = {}) {
2403
2509
  return this.#segmentStoreForConversationDir(conversationDir).readAll(opts);
2404
2510
  }
@@ -6537,18 +6537,50 @@ export async function handleYeaftLoadHistory(msg) {
6537
6537
  }
6538
6538
 
6539
6539
  /**
6540
- * Handle a "load older messages" pagination request. Reads `turns` more
6541
- * turns of history strictly older than `beforeSeq` for `sessionId`, and
6542
- * emits them in a single `yeaft_history_chunk` envelope (NOT a
6543
- * `yeaft_output` that pipeline appends, but the frontend needs to
6544
- * PREPEND these older messages above what it already has).
6540
+ * Load one lightweight Conversation Outline page. The response contains only
6541
+ * visible user/assistant metadata and bounded snippets; full message bodies and
6542
+ * tool payloads stay on the Agent. `beforeSeq` is an exclusive older-page
6543
+ * cursor, and `includeTotal` avoids recounting after the first page.
6545
6544
  *
6546
- * Tool replay is NOT included in this PR same projection as
6547
- * `handleYeaftLoadHistory` (user / assistant text only). On any internal
6548
- * failure we still emit an empty chunk so the spinner clears.
6549
- *
6550
- * @param {object} msg — { sessionId, beforeSeq, turns }
6545
+ * @param {object} msg { sessionId, beforeSeq, limit, includeTotal }
6551
6546
  */
6547
+ export async function handleYeaftLoadHistoryOutline(msg) {
6548
+ const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
6549
+ const requestId = typeof msg?.requestId === 'string' ? msg.requestId : null;
6550
+ const beforeSeq = Number.isFinite(msg?.beforeSeq) ? msg.beforeSeq : null;
6551
+ const limit = Math.min(100, Math.max(1, Number.isFinite(msg?.limit) ? Math.floor(msg.limit) : 50));
6552
+ const response = {
6553
+ type: 'yeaft_history_outline',
6554
+ requestId,
6555
+ sessionId: sessionId || null,
6556
+ results: [],
6557
+ hasMore: false,
6558
+ nextBeforeSeq: null,
6559
+ totalCount: null,
6560
+ _requestClientId: msg?._requestClientId || null,
6561
+ };
6562
+
6563
+ if (!sessionId) {
6564
+ sendToServer({ ...response, error: 'invalid_session' });
6565
+ return;
6566
+ }
6567
+
6568
+ try {
6569
+ const defaultYeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
6570
+ const storeDir = resolveSessionYeaftDir(defaultYeaftDir, sessionId);
6571
+ const store = new ConversationStore(storeDir);
6572
+ const result = store.loadVisibleOutlineBySession(sessionId, {
6573
+ limit,
6574
+ beforeSeq,
6575
+ includeTotal: msg?.includeTotal !== false,
6576
+ });
6577
+ sendToServer({ ...response, ...result });
6578
+ } catch (err) {
6579
+ console.error('[Yeaft] Session history outline failed:', err?.message || err);
6580
+ sendToServer({ ...response, error: 'outline_failed' });
6581
+ }
6582
+ }
6583
+
6552
6584
  export async function handleYeaftSearchHistory(msg) {
6553
6585
  const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
6554
6586
  const query = typeof msg?.query === 'string' ? msg.query.trim().slice(0, 500) : '';
@@ -33,6 +33,18 @@ function currentAction(detail) {
33
33
  return detail.actions.find(action => action.id === detail.currentActionId) || null;
34
34
  }
35
35
 
36
+ function projectCurrentActionSummary(action, projectedAction = action) {
37
+ if (!projectedAction?.id) return null;
38
+ return {
39
+ id: projectedAction.id,
40
+ type: projectedAction.type,
41
+ stageId: projectedAction.stageId,
42
+ assignmentMode: projectedAction.assignmentPolicy?.mode || (projectedAction.requiredRole ? 'fixed' : null),
43
+ status: projectedAction.status,
44
+ objective: truncateUtf8(action?.brief?.objective, 1_000) || null,
45
+ };
46
+ }
47
+
36
48
  function count(value) {
37
49
  return Math.max(0, Number(value) || 0);
38
50
  }
@@ -652,7 +664,9 @@ export function projectWorkItemSummary(detail) {
652
664
  activeActionIds: Array.isArray(detail.activeActionIds) ? detail.activeActionIds : undefined,
653
665
  attentionActionIds: Array.isArray(detail.attentionActionIds) ? detail.attentionActionIds : undefined,
654
666
  currentActionId: detail.currentActionId || null,
655
- currentAction: null,
667
+ currentAction: projectCurrentActionSummary(detail.currentAction),
668
+ actionCount: count(detail.actionCount),
669
+ completedActionCount: count(detail.completedActionCount),
656
670
  executionStats: executionStats(detail.executionStats),
657
671
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
658
672
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
@@ -677,18 +691,14 @@ export function projectWorkItemSummary(detail) {
677
691
  activeActionIds: Array.isArray(detail.activeActionIds) ? detail.activeActionIds : undefined,
678
692
  attentionActionIds: Array.isArray(detail.attentionActionIds) ? detail.attentionActionIds : undefined,
679
693
  currentActionId: detail.currentActionId || null,
694
+ actionCount: detail.actions.filter(item => !['superseded', 'cancelled'].includes(item?.status)).length,
695
+ completedActionCount: detail.actions.filter(item => item?.status === 'completed').length,
680
696
  executionStats: Array.isArray(detail.runs)
681
697
  ? sumExecutionStats(detail.runs)
682
698
  : executionStats(detail.executionStats),
683
699
  failureReason: workItemFailureReason(detail),
684
700
 
685
- currentAction: projectedAction ? {
686
- id: projectedAction.id,
687
- type: projectedAction.type,
688
- stageId: projectedAction.stageId,
689
- assignmentMode: projectedAction.assignmentPolicy?.mode || (projectedAction.requiredRole ? 'fixed' : null),
690
- status: projectedAction.status,
691
- } : null,
701
+ currentAction: projectCurrentActionSummary(action, projectedAction),
692
702
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
693
703
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
694
704
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
@@ -70,12 +70,21 @@ function mapWorkItem(row) {
70
70
  workflowSnapshot: parseJson(row.workflow_snapshot, null),
71
71
  status: row.status,
72
72
  currentActionId: row.current_action_id || null,
73
+ currentAction: row.current_action_type ? {
74
+ id: row.current_action_id,
75
+ type: row.current_action_type,
76
+ stageId: row.current_action_stage_id || row.current_action_type,
77
+ status: row.current_action_status || null,
78
+ brief: parseJson(row.current_action_brief, null),
79
+ } : null,
73
80
  currentRunId: row.current_run_id || null,
74
81
  workDir: row.work_dir || '',
75
82
  workspaceKey: row.workspace_key || '',
76
83
  reuseMemory: row.reuse_memory !== 0,
77
84
  origin: parseJson(row.origin, null),
78
85
  linkedSessionIds: parseJson(row.linked_session_ids, []),
86
+ actionCount: Math.max(0, Number(row.action_count) || 0),
87
+ completedActionCount: Math.max(0, Number(row.completed_action_count) || 0),
79
88
  sessionContext: parseJson(row.session_context, []),
80
89
  messages: parseJson(row.messages, []),
81
90
  attachments: parseJson(row.attachments, []),
@@ -1190,6 +1199,14 @@ export class WorkItemStore {
1190
1199
  }
1191
1200
  const limit = Math.min(Math.max(Number(filters.limit) || 100, 1), 500);
1192
1201
  const sql = `SELECT w.*,
1202
+ current_action.type AS current_action_type,
1203
+ current_action.stage_id AS current_action_stage_id,
1204
+ current_action.status AS current_action_status,
1205
+ current_action.brief AS current_action_brief,
1206
+ (SELECT COUNT(*) FROM actions a WHERE a.work_item_id = w.id
1207
+ AND a.status NOT IN ('superseded', 'cancelled')) AS action_count,
1208
+ (SELECT COUNT(*) FROM actions a WHERE a.work_item_id = w.id
1209
+ AND a.status = 'completed') AS completed_action_count,
1193
1210
  COALESCE(SUM(r.llm_request_count), 0) AS usage_llm_request_count,
1194
1211
  COALESCE(SUM(r.loop_count), 0) AS usage_loop_count,
1195
1212
  COALESCE(SUM(r.tool_count), 0) AS usage_tool_count,
@@ -1198,7 +1215,9 @@ export class WorkItemStore {
1198
1215
  COALESCE(SUM(r.cache_read_tokens), 0) AS usage_cache_read_tokens,
1199
1216
  COALESCE(SUM(r.cache_write_tokens), 0) AS usage_cache_write_tokens,
1200
1217
  COALESCE(SUM(r.total_tokens), 0) AS usage_total_tokens
1201
- FROM work_items w LEFT JOIN runs r ON r.work_item_id = w.id
1218
+ FROM work_items w
1219
+ LEFT JOIN actions current_action ON current_action.id = w.current_action_id
1220
+ LEFT JOIN runs r ON r.work_item_id = w.id
1202
1221
  ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
1203
1222
  GROUP BY w.id ORDER BY w.updated_at DESC LIMIT ?`;
1204
1223
  return this.db.prepare(sql).all(...values, limit).map(mapWorkItem).map(workItem => {