@yeaft/webchat-agent 1.0.197 → 1.0.199

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.
Binary file
@@ -16,6 +16,6 @@
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
19
- <script type="module" src="app.bundle.js?v=aa7816be"></script>
19
+ <script type="module" src="app.bundle.js?v=b986f0f0"></script>
20
20
  </body>
21
21
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.197",
3
+ "version": "1.0.199",
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",
@@ -57,6 +57,10 @@ let DEFAULT_RECENT_TURNS = 20;
57
57
  const RECENT_SESSION_SCAN_BASE_CAP = 64;
58
58
  const RECENT_SESSION_SCAN_PER_TURN_CAP = 4;
59
59
  const RECENT_SESSION_SCAN_MAX_CAP = 256;
60
+ // A normal Engine loop executes at most 30 tools, but parallel VPs can append
61
+ // many rows between a call and its result. One extra normal delta page keeps the
62
+ // scan bounded while leaving enough room to close a legitimate interleaved arc.
63
+ const DELTA_TOOL_PAIR_EXTENSION_CAP = 500;
60
64
 
61
65
 
62
66
  const SEGMENT_INDEX_FILE = 'index.json';
@@ -235,6 +239,90 @@ function recentSessionScanCap(turnsLimit) {
235
239
  return Math.min(RECENT_SESSION_SCAN_MAX_CAP, RECENT_SESSION_SCAN_BASE_CAP + turns * RECENT_SESSION_SCAN_PER_TURN_CAP);
236
240
  }
237
241
 
242
+ function askUserToolIdentity(row, toolCallId) {
243
+ if (!row || typeof toolCallId !== 'string' || !toolCallId) return null;
244
+ return [
245
+ row.sessionId || '',
246
+ row.speakerVpId || '',
247
+ row.turnId || '',
248
+ row.threadId || 'main',
249
+ toolCallId,
250
+ ].join('\u0000');
251
+ }
252
+
253
+ function parseAskUserResult(toolCall, toolResult) {
254
+ if (!toolCall || toolCall.name !== 'AskUser' || typeof toolCall.id !== 'string' || !toolCall.id
255
+ || !toolResult || toolResult.isError) return null;
256
+ let payload = toolResult.content;
257
+ if (typeof payload === 'string') {
258
+ try { payload = JSON.parse(payload); } catch { return null; }
259
+ }
260
+ if (!payload || typeof payload !== 'object') return null;
261
+
262
+ const question = typeof toolCall.input?.question === 'string'
263
+ ? toolCall.input.question
264
+ : (typeof payload.question === 'string' ? payload.question : '');
265
+ const options = Array.isArray(toolCall.input?.options)
266
+ ? toolCall.input.options.filter(option => typeof option === 'string')
267
+ : [];
268
+ if (payload.timedOut === true) {
269
+ return {
270
+ toolCallId: toolCall.id,
271
+ status: 'expired',
272
+ question,
273
+ options,
274
+ };
275
+ }
276
+ if (!payload.answers || typeof payload.answers !== 'object' || Array.isArray(payload.answers)) return null;
277
+ return {
278
+ toolCallId: toolCall.id,
279
+ status: 'answered',
280
+ question,
281
+ options,
282
+ answers: payload.answers,
283
+ };
284
+ }
285
+
286
+ export function projectVisibleSessionMessages(messages) {
287
+ const rows = Array.isArray(messages) ? messages : [];
288
+ const toolResults = new Map();
289
+ for (const row of rows) {
290
+ if (row?.role !== 'tool') continue;
291
+ const identity = askUserToolIdentity(row, row.toolCallId);
292
+ if (identity) toolResults.set(identity, row);
293
+ }
294
+
295
+ const visible = [];
296
+ for (const row of rows) {
297
+ if (!row || (row.role !== 'user' && row.role !== 'assistant')) continue;
298
+ if (row.role !== 'assistant' || !Array.isArray(row.toolCalls) || row.toolCalls.length === 0) {
299
+ if (row.role === 'assistant' && !row.content && !row.attachments && !row.images
300
+ && !row.toolSummaryCount && !row.askUserResults) continue;
301
+ visible.push(row);
302
+ continue;
303
+ }
304
+
305
+ const askUserResults = [];
306
+ let omittedToolCount = 0;
307
+ for (const toolCall of row.toolCalls) {
308
+ const identity = askUserToolIdentity(row, toolCall?.id);
309
+ const result = parseAskUserResult(toolCall, identity ? toolResults.get(identity) : null);
310
+ if (result) askUserResults.push(result);
311
+ else omittedToolCount += 1;
312
+ }
313
+ const { toolCalls, ...rest } = row;
314
+ const projected = {
315
+ ...rest,
316
+ ...(omittedToolCount > 0 ? { toolSummaryCount: omittedToolCount } : {}),
317
+ ...(askUserResults.length > 0 ? { askUserResults } : {}),
318
+ };
319
+ if (!projected.content && !projected.attachments && !projected.images
320
+ && !projected.toolSummaryCount && !projected.askUserResults) continue;
321
+ visible.push(projected);
322
+ }
323
+ return visible;
324
+ }
325
+
238
326
  // ─── Frontmatter helpers ─────────────────────────────────────
239
327
 
240
328
  /**
@@ -1346,14 +1434,15 @@ export class ConversationStore {
1346
1434
  const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
1347
1435
  const page = this.#loadRecentSessionWindow(sessionId, turnsLimit, {
1348
1436
  beforeSeq: cutoff,
1349
- roles: new Set(['user', 'assistant']),
1350
- stripAssistantToolCalls: true,
1437
+ roles: null,
1438
+ stripAssistantToolCalls: false,
1351
1439
  });
1352
- if (page.messages.length === 0) return { messages: [], oldestSeq: null, hasMore: page.truncated };
1440
+ const messages = projectVisibleSessionMessages(page.messages);
1441
+ if (messages.length === 0) return { messages: [], oldestSeq: null, hasMore: page.truncated };
1353
1442
 
1354
- const oldestSeq = page.messages.length ? parseSeqFromId(page.messages[0].id) : null;
1443
+ const oldestSeq = messages.length ? parseSeqFromId(messages[0].id) : null;
1355
1444
  return {
1356
- messages: page.messages,
1445
+ messages,
1357
1446
  oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
1358
1447
  hasMore: page.truncated,
1359
1448
  };
@@ -1368,8 +1457,8 @@ export class ConversationStore {
1368
1457
  * cursor. When no visible rows changed after `afterSeq`, keep the cursor at
1369
1458
  * least at `afterSeq` so an empty delta still completes the in-flight sync
1370
1459
  * without downgrading the client to a cursor-less loaded state. Hidden rows
1371
- * are also allowed to advance this cursor because they were scanned and
1372
- * intentionally omitted from UI replay.
1460
+ * advance the cursor only at pair-safe boundaries; a cursor must never cross
1461
+ * an assistant tool call before all of that call's result rows are included.
1373
1462
  *
1374
1463
  * @param {string} sessionId
1375
1464
  * @param {number|null} afterSeq — exclusive lower bound
@@ -1382,19 +1471,81 @@ export class ConversationStore {
1382
1471
  const cutoff = Number.isFinite(afterSeq) && afterSeq >= 0 ? afterSeq : null;
1383
1472
  if (cutoff === null) return { messages: [], latestSeq: null };
1384
1473
  const after = [];
1385
- let newestScannedSeq = cutoff;
1474
+ const pendingToolResultIds = new Set();
1475
+ const completedBeforeCursor = new Set();
1476
+ const boundaryAssistants = [];
1477
+ let boundaryLookbackRows = 0;
1478
+ for (const previous of this.#iterateSessionRows(sessionId, { beforeSeq: cutoff + 1, desc: true })) {
1479
+ if (!previous || previous.sessionId !== sessionId) continue;
1480
+ boundaryLookbackRows += 1;
1481
+ if (boundaryLookbackRows > DELTA_TOOL_PAIR_EXTENSION_CAP) break;
1482
+ if (isHiddenConversationRow(previous)) continue;
1483
+ if (previous.role === 'tool' && typeof previous.toolCallId === 'string') {
1484
+ completedBeforeCursor.add(previous.toolCallId);
1485
+ continue;
1486
+ }
1487
+ if (previous.role === 'assistant' && Array.isArray(previous.toolCalls) && previous.toolCalls.length > 0) {
1488
+ const pendingIds = previous.toolCalls
1489
+ .map(toolCall => toolCall?.id)
1490
+ .filter(toolCallId => typeof toolCallId === 'string'
1491
+ && toolCallId
1492
+ && !completedBeforeCursor.has(toolCallId));
1493
+ if (pendingIds.length > 0) boundaryAssistants.push({ message: previous, pendingIds });
1494
+ continue;
1495
+ }
1496
+ if (previous.role === 'user') break;
1497
+ }
1498
+ boundaryAssistants.sort((a, b) => compareMessagesBySeq(a.message, b.message));
1499
+ for (const boundary of boundaryAssistants) {
1500
+ after.push(boundary.message);
1501
+ for (const toolCallId of boundary.pendingIds) pendingToolResultIds.add(toolCallId);
1502
+ }
1503
+ const earliestBoundarySeq = boundaryAssistants.length > 0
1504
+ ? parseSeqFromId(boundaryAssistants[0].message.id)
1505
+ : null;
1506
+ let safeCursorSeq = Number.isFinite(earliestBoundarySeq)
1507
+ ? Math.max(0, earliestBoundarySeq - 1)
1508
+ : cutoff;
1509
+ let visibleRows = after.length;
1510
+ let extensionRows = 0;
1386
1511
  for (const m of this.#iterateSessionRows(sessionId, { afterSeq: cutoff, desc: false })) {
1387
1512
  if (!m || m.sessionId !== sessionId) continue;
1388
1513
  const seq = parseSeqFromId(m.id);
1389
- if (Number.isFinite(seq) && seq > newestScannedSeq) newestScannedSeq = seq;
1390
- if (isHiddenConversationRow(m)) continue;
1391
- after.push(m);
1392
- if (after.length >= limit) break;
1393
- }
1394
- const sliced = pairSanitize(after.slice(0, limit));
1395
- const lastSeq = sliced.length ? parseSeqFromId(sliced[sliced.length - 1].id) : null;
1396
- const latestSeq = Number.isFinite(lastSeq) ? Math.max(lastSeq, newestScannedSeq) : newestScannedSeq;
1397
- return { messages: sliced, latestSeq };
1514
+ const hidden = isHiddenConversationRow(m);
1515
+ if (!hidden) {
1516
+ // Keep every outstanding call open across interleaved VP rows. Session
1517
+ // persistence is globally sequenced, so a sibling VP may append visible
1518
+ // messages between an assistant call and that call's result.
1519
+ after.push(m);
1520
+ visibleRows += 1;
1521
+ if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
1522
+ for (const toolCall of m.toolCalls) {
1523
+ if (typeof toolCall?.id === 'string' && toolCall.id) pendingToolResultIds.add(toolCall.id);
1524
+ }
1525
+ } else if (m.role === 'tool' && typeof m.toolCallId === 'string') {
1526
+ pendingToolResultIds.delete(m.toolCallId);
1527
+ }
1528
+ }
1529
+ if (pendingToolResultIds.size === 0 && Number.isFinite(seq)) safeCursorSeq = seq;
1530
+ if (visibleRows < limit) continue;
1531
+ if (pendingToolResultIds.size === 0) break;
1532
+ extensionRows += 1;
1533
+ if (extensionRows >= DELTA_TOOL_PAIR_EXTENSION_CAP) break;
1534
+ }
1535
+ // If the extension cap stopped inside a malformed arc, return only the
1536
+ // prefix covered by the safe cursor. Returning later rows with an earlier
1537
+ // cursor would make the next delta repeat visible messages unnecessarily.
1538
+ const pairSafeRows = pendingToolResultIds.size === 0
1539
+ ? after
1540
+ : after.filter(message => {
1541
+ const seq = parseSeqFromId(message?.id);
1542
+ return Number.isFinite(seq) && seq <= safeCursorSeq;
1543
+ });
1544
+ const sliced = pairSanitize(pairSafeRows);
1545
+ // Never advance past a row the sanitizer had to drop. A malformed or
1546
+ // over-cap tool arc must be retried from its assistant call rather than
1547
+ // turning the following result into a permanent orphan on the next page.
1548
+ return { messages: projectVisibleSessionMessages(sliced), latestSeq: safeCursorSeq };
1398
1549
  }
1399
1550
 
1400
1551
  /**
@@ -1480,31 +1631,33 @@ export class ConversationStore {
1480
1631
 
1481
1632
  const beforeTurns = Math.min(10, Math.max(1, Number.isFinite(opts.beforeTurns) ? Math.floor(opts.beforeTurns) : 3));
1482
1633
  const afterTurns = Math.min(10, Math.max(1, Number.isFinite(opts.afterTurns) ? Math.floor(opts.afterTurns) : 3));
1483
- const before = this.loadVisibleBySession(sessionId, anchorSeq + 1, beforeTurns + 1);
1484
- const messages = before.messages.slice();
1634
+ const beforeRaw = this.#loadRecentSessionWindow(sessionId, beforeTurns + 1, {
1635
+ beforeSeq: anchorSeq + 1,
1636
+ roles: null,
1637
+ stripAssistantToolCalls: false,
1638
+ });
1639
+ const messages = beforeRaw.messages.slice();
1485
1640
  const seen = new Set(messages.map(message => message?.id).filter(Boolean));
1486
1641
  let followingUserTurns = 0;
1487
1642
 
1488
1643
  for (const message of this.#iterateSessionRows(sessionId, { afterSeq: anchorSeq, desc: false })) {
1489
1644
  if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
1490
- if (message.role !== 'user' && message.role !== 'assistant') continue;
1491
1645
  if (message.role === 'user') {
1492
1646
  followingUserTurns += 1;
1493
1647
  if (followingUserTurns > afterTurns) break;
1494
1648
  }
1495
1649
  if (message.id && seen.has(message.id)) continue;
1496
- const projected = this.#projectVisibleMessage(message);
1497
- if (!projected) continue;
1498
- if (projected.id) seen.add(projected.id);
1499
- messages.push(projected);
1650
+ if (message.id) seen.add(message.id);
1651
+ messages.push(message);
1500
1652
  }
1501
1653
 
1502
1654
  messages.sort(compareMessagesBySeq);
1503
- const oldestSeq = messages.length > 0 ? parseSeqFromId(messages[0].id) : null;
1655
+ const visibleMessages = projectVisibleSessionMessages(messages);
1656
+ const oldestSeq = visibleMessages.length > 0 ? parseSeqFromId(visibleMessages[0].id) : null;
1504
1657
  return {
1505
- messages,
1658
+ messages: visibleMessages,
1506
1659
  oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
1507
- hasMoreBefore: before.hasMore,
1660
+ hasMoreBefore: beforeRaw.truncated,
1508
1661
  };
1509
1662
  }
1510
1663
 
@@ -2246,15 +2399,6 @@ export class ConversationStore {
2246
2399
  return `${start > 0 ? '…' : ''}${text.slice(start, end)}${end < text.length ? '…' : ''}`;
2247
2400
  }
2248
2401
 
2249
- #projectVisibleMessage(message) {
2250
- if (!message || (message.role !== 'user' && message.role !== 'assistant')) return null;
2251
- if (message.role !== 'assistant' || !Array.isArray(message.toolCalls) || message.toolCalls.length === 0) {
2252
- return message;
2253
- }
2254
- const { toolCalls, ...rest } = message;
2255
- return { ...rest, toolSummaryCount: toolCalls.length };
2256
- }
2257
-
2258
2402
  #readSegmentRows(conversationDir, opts = {}) {
2259
2403
  return this.#segmentStoreForConversationDir(conversationDir).readAll(opts);
2260
2404
  }
package/yeaft/engine.js CHANGED
@@ -1242,7 +1242,9 @@ export class Engine {
1242
1242
  // Null in non-VP / test contexts — tools tolerate missing slots.
1243
1243
  getCurrentTodos: vpCtx?.getCurrentTodos || null,
1244
1244
  setCurrentTodos: vpCtx?.setCurrentTodos || null,
1245
- askUser: vpCtx?.askUser || null,
1245
+ askUser: typeof vpCtx?.askUser === 'function'
1246
+ ? input => vpCtx.askUser(input, typeof vpCtx?.currentToolCall === 'function' ? vpCtx.currentToolCall() : null)
1247
+ : null,
1246
1248
  // task-707: tool-callable end-turn signal. The engine threads this
1247
1249
  // setter when constructing toolCtx so a tool (e.g. route_forward)
1248
1250
  // can mark "after this batch, end the turn — do NOT call adapter
@@ -32,7 +32,7 @@
32
32
  * @property {number} [contextWindow] — current model's context window in
33
33
  * tokens (used by ToolRegistry.execute to cap a single tool result at a
34
34
  * fraction of the window so one runaway grep can't blow the wire).
35
- * @property {(input: {question:string, options?:string[]}) => Promise<object>} [askUser]
35
+ * @property {(input: {question:string, options?:string[]}, toolCall?: {id?:string, name?:string}|null) => Promise<object>} [askUser]
36
36
  * — host-provided interactive prompt. Resolves only after the user answers.
37
37
  * @property {(reason?: string|object) => void} [requestEndTurn]
38
38
  * — tool-callable signal that the current engine turn should end after