@yeaft/webchat-agent 1.0.198 → 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.198",
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
@@ -61,7 +61,7 @@ import {
61
61
  trimSnapshotForBudget,
62
62
  } from './history-compact.js';
63
63
  import { persistYeaftAttachments, attachmentsForPersistence, persistedAttachmentPreviewPayload } from './attachments.js';
64
- import { ConversationStore, parseSeqFromId } from './conversation/persist.js';
64
+ import { ConversationStore, parseSeqFromId, projectVisibleSessionMessages } from './conversation/persist.js';
65
65
  import { isHiddenConversationRow } from './conversation/internal-control.js';
66
66
  import { imageMetadataForPersistence } from './image-assets.js';
67
67
  import { sliceLastNTurns } from './turn-utils.js';
@@ -94,6 +94,7 @@ const SKILL_RELOAD_INTERVAL_MS = 2_000;
94
94
  * vpId:string,
95
95
  * threadId:string,
96
96
  * turnId:string,
97
+ * toolCallId:string,
97
98
  * question:string,
98
99
  * options:Array<string>,
99
100
  * createdAt:number,
@@ -1188,6 +1189,9 @@ function projectPersistedToHistoryEntry(m) {
1188
1189
  if (m.clientMessageId) entry.clientMessageId = m.clientMessageId;
1189
1190
  if (m.speakerVpId) entry.speakerVpId = m.speakerVpId;
1190
1191
  if (m.toolCallId) entry.toolCallId = m.toolCallId;
1192
+ if (Array.isArray(m.askUserResults) && m.askUserResults.length > 0) {
1193
+ entry.askUserResults = m.askUserResults;
1194
+ }
1191
1195
  if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
1192
1196
  entry.toolCalls = m.toolCalls.map(tc => ({
1193
1197
  id: tc.id,
@@ -1203,7 +1207,7 @@ function projectPersistedToHistoryEntry(m) {
1203
1207
  else if (m.time) entry.ts = m.time;
1204
1208
  if (Array.isArray(m.images) && m.images.length > 0) entry.images = m.images;
1205
1209
  if (Array.isArray(m.attachments) && m.attachments.length > 0) entry.attachments = m.attachments;
1206
- if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments && !entry.images && !entry.toolCalls && !entry.toolSummaryCount) return null;
1210
+ if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments && !entry.images && !entry.toolCalls && !entry.toolSummaryCount && !entry.askUserResults) return null;
1207
1211
  return entry;
1208
1212
  }
1209
1213
 
@@ -1253,7 +1257,7 @@ function loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq = null)
1253
1257
  return { messages: [], oldestSeq: null, hasMore: false };
1254
1258
  }
1255
1259
 
1256
- const visible = rows
1260
+ const visible = projectVisibleSessionMessages(rows)
1257
1261
  .map(projectPersistedToVisibleHistoryEntry)
1258
1262
  .filter(Boolean);
1259
1263
  const messages = sliceLastNTurns(visible, limit);
@@ -1280,7 +1284,7 @@ function ensureYeaftConversationId() {
1280
1284
  }
1281
1285
 
1282
1286
  function projectVisibleHistoryChunkMessages(messages = []) {
1283
- return (messages || [])
1287
+ return projectVisibleSessionMessages(messages)
1284
1288
  .map(projectPersistedToVisibleHistoryEntry)
1285
1289
  .filter(Boolean)
1286
1290
  .map(m => ({
@@ -1296,6 +1300,7 @@ function projectVisibleHistoryChunkMessages(messages = []) {
1296
1300
  ...(Array.isArray(m.attachments) && m.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(m.attachments) } : {}),
1297
1301
  ...(Array.isArray(m.images) && m.images.length > 0 ? { images: m.images } : {}),
1298
1302
  ...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
1303
+ ...(Array.isArray(m.askUserResults) && m.askUserResults.length > 0 ? { askUserResults: m.askUserResults } : {}),
1299
1304
  ...(Number.isFinite(m.toolSummaryCount) && m.toolSummaryCount > 0
1300
1305
  ? { toolSummaryCount: m.toolSummaryCount }
1301
1306
  : (Array.isArray(m.toolCalls) && m.toolCalls.length > 0 ? { toolSummaryCount: m.toolCalls.length } : {})),
@@ -1304,9 +1309,9 @@ function projectVisibleHistoryChunkMessages(messages = []) {
1304
1309
 
1305
1310
  function emitHistoryChunk({ sessionId, messages, mode = 'older', oldestSeq = null, hasMore = false, latestSeq = null, afterSeq = null, turns = null, perfTraceId = null }) {
1306
1311
  const projectedMessages = projectVisibleHistoryChunkMessages(messages);
1307
- if (mode === 'delta' && projectedMessages.length === 0) {
1308
- return projectedMessages;
1309
- }
1312
+ // Empty deltas still carry the authoritative safe cursor and clear the
1313
+ // browser's syncingAfterSeq fence. Dropping this envelope leaves Session
1314
+ // switching stuck after hidden-only or pair-unsafe rows.
1310
1315
  sendToServer({
1311
1316
  type: 'yeaft_history_chunk',
1312
1317
  conversationId: yeaftConversationId,
@@ -2719,6 +2724,7 @@ function pendingUserPromptEvent(requestId, pending, extra = {}) {
2719
2724
  return {
2720
2725
  type: 'ask_user_question',
2721
2726
  requestId,
2727
+ toolCallId: pending.toolCallId || null,
2722
2728
  questions: [{
2723
2729
  question: pending.question,
2724
2730
  options: pending.options.map(label => ({ label, description: '' })),
@@ -2758,6 +2764,7 @@ function settlePendingUserPrompt(requestId, pending, { answers = null, timedOut
2758
2764
  sendSessionEvent({
2759
2765
  type: timedOut ? 'ask_user_expired' : 'ask_user_answered',
2760
2766
  requestId,
2767
+ toolCallId: pending.toolCallId || null,
2761
2768
  ...(timedOut ? { expiredAt: Date.now() } : { answers: answers || {} }),
2762
2769
  }, {
2763
2770
  sessionId: pending.sessionId,
@@ -5025,7 +5032,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5025
5032
  // each write a copy of the user message, and history replay
5026
5033
  // would render the user's prompt N times.
5027
5034
  userAlreadyPersisted: true,
5028
- askUser: ({ question, options }) => new Promise((resolve, reject) => {
5035
+ askUser: ({ question, options }, toolCall = null) => new Promise((resolve, reject) => {
5029
5036
  const requestId = `ask_${randomUUID()}`;
5030
5037
  const signal = vpAbort.signal;
5031
5038
  const createdAt = Date.now();
@@ -5043,6 +5050,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5043
5050
  vpId,
5044
5051
  threadId,
5045
5052
  turnId,
5053
+ toolCallId: typeof toolCall?.id === 'string' ? toolCall.id : '',
5046
5054
  question,
5047
5055
  options: Array.isArray(options) ? options.filter(label => typeof label === 'string') : [],
5048
5056
  createdAt,
@@ -6110,6 +6118,7 @@ export function handleYeaftAskUserAnswer(msg) {
6110
6118
  if (msg.vpId && msg.vpId !== pending.vpId) return false;
6111
6119
  if (msg.turnId && msg.turnId !== pending.turnId) return false;
6112
6120
  if (msg.threadId && msg.threadId !== pending.threadId) return false;
6121
+ if (msg.toolCallId && msg.toolCallId !== pending.toolCallId) return false;
6113
6122
 
6114
6123
  return settlePendingUserPrompt(requestId, pending, { answers: msg.answers || {} });
6115
6124
  }
@@ -7066,10 +7075,10 @@ export const __testHooks = {
7066
7075
  vpAborts.clear();
7067
7076
  vpInboxes.clear();
7068
7077
  },
7069
- seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test', question = 'Continue?', options = [], createdAt = Date.now(), expiresAt = Date.now() + ASK_USER_TIMEOUT_MS } = {}) {
7078
+ seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test', toolCallId = 'call-test', question = 'Continue?', options = [], createdAt = Date.now(), expiresAt = Date.now() + ASK_USER_TIMEOUT_MS } = {}) {
7070
7079
  let resolved;
7071
7080
  const promise = new Promise(resolve => { resolved = resolve; });
7072
- pendingUserPrompts.set(requestId, { resolve: resolved, sessionId, vpId, threadId, turnId, question, options, createdAt, expiresAt, timer: null });
7081
+ pendingUserPrompts.set(requestId, { resolve: resolved, sessionId, vpId, threadId, turnId, toolCallId, question, options, createdAt, expiresAt, timer: null });
7073
7082
  return promise;
7074
7083
  },
7075
7084
  replayPendingUserPrompts,