@yeaft/webchat-agent 0.1.1104 → 0.1.1107

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.
@@ -66,7 +66,7 @@ import { isHiddenConversationRow } from './conversation/internal-control.js';
66
66
  import { sliceLastNTurns } from './turn-utils.js';
67
67
  import { pairSanitize } from './pair-sanitize.js';
68
68
  import { filterSnapshotForVp } from './snapshot-filter.js';
69
- import { createVpStatusBroker } from './vp-status-broker.js';
69
+ import { createVpStatusBroker, isVpStatusRunning } from './vp-status-broker.js';
70
70
  import { classifyThread as defaultClassifyThread, fallbackTitle } from './vp/thread-classifier.js';
71
71
  import { listMcpServers, upsertMcpServer, removeMcpServer } from './config-api.js';
72
72
  import { buildMcpFlattenedTools } from './tools/mcp-tools.js';
@@ -433,6 +433,32 @@ function buildVpPromptPayload(vpId, envelope) {
433
433
  return { text, prompt, promptParts };
434
434
  }
435
435
 
436
+ function buildPendingRescueEnvelope({ sessionId, taskId = null, threadId = 'main', followUpId, leftover, replayText, replayParts = null }) {
437
+ const leftoverIsInternal = Boolean(leftover?.internal);
438
+ const leftoverInjectedBy = leftoverIsInternal && typeof leftover?.injectedBy === 'string'
439
+ ? leftover.injectedBy
440
+ : null;
441
+ return {
442
+ sessionId,
443
+ taskId,
444
+ trigger: 'pending_rescue',
445
+ msg: {
446
+ id: followUpId,
447
+ from: leftoverIsInternal && leftover.senderVpId ? leftover.senderVpId : 'user',
448
+ role: leftoverIsInternal ? 'assistant' : 'user',
449
+ text: replayText,
450
+ meta: {
451
+ rescuedFrom: 'pendingQueries',
452
+ threadId,
453
+ ...(leftoverInjectedBy ? { injectedBy: leftoverInjectedBy } : {}),
454
+ ...(leftoverIsInternal && leftover.senderVpId ? { senderVpId: leftover.senderVpId } : {}),
455
+ ...(leftoverIsInternal && leftover.sourceThreadId ? { sourceThreadId: leftover.sourceThreadId } : {}),
456
+ },
457
+ },
458
+ ...(Array.isArray(replayParts) && replayParts.length > 0 ? { _promptParts: replayParts } : {}),
459
+ };
460
+ }
461
+
436
462
  export function visibleInboundThreadId(envelope, fallbackThreadId = 'main') {
437
463
  const meta = envelope?.msg?.meta || {};
438
464
  if (
@@ -1217,6 +1243,7 @@ function formatTaskResultForVp(task) {
1217
1243
  `<task-result id="${task.id}" kind="${task.kind}" status="${task.status}">`,
1218
1244
  `title: ${task.title || task.kind || task.id}`,
1219
1245
  ];
1246
+ if (task?.runtime?.command) lines.push(`command: ${task.runtime.command}`);
1220
1247
  if (result.exitCode !== undefined && result.exitCode !== null) lines.push(`exitCode: ${result.exitCode}`);
1221
1248
  if (result.signal) lines.push(`signal: ${result.signal}`);
1222
1249
  if (result.error) lines.push(`error: ${result.error}`);
@@ -1349,6 +1376,9 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
1349
1376
  originalText: text,
1350
1377
  originalParts: Array.isArray(envelope?._promptParts) ? envelope._promptParts : null,
1351
1378
  internal: isInternalAppend,
1379
+ injectedBy: isInternalAppend ? injectedBy : null,
1380
+ senderVpId: isInternalAppend ? (envelope?.msg?.meta?.senderVpId || envelope?.msg?.from || null) : null,
1381
+ sourceThreadId: isInternalAppend ? visibleInboundThreadId(envelope, thread.threadId) : null,
1352
1382
  });
1353
1383
  persistInboundMessageOnceByMsgId({
1354
1384
  msgId: envelope?.msg?.id,
@@ -1533,18 +1563,15 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1533
1563
  : null;
1534
1564
  if (!replayText && !replayParts) continue;
1535
1565
  const followUpId = `followup_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
1536
- const followUpEnvelope = {
1566
+ const followUpEnvelope = buildPendingRescueEnvelope({
1537
1567
  sessionId,
1538
1568
  taskId: envelope?.taskId || null,
1539
- trigger: 'pending_rescue',
1540
- msg: {
1541
- id: followUpId,
1542
- from: 'user',
1543
- text: replayText,
1544
- meta: { rescuedFrom: 'pendingQueries', threadId: thread.threadId },
1545
- },
1546
- ...(replayParts ? { _promptParts: replayParts } : {}),
1547
- };
1569
+ threadId: thread.threadId,
1570
+ followUpId,
1571
+ leftover,
1572
+ replayText,
1573
+ replayParts,
1574
+ });
1548
1575
  const followUpTurnId = `${randomUUID().slice(0, 8)}:${vpId}`;
1549
1576
  inbox.push({ envelope: followUpEnvelope, turnId: followUpTurnId, thread });
1550
1577
  try {
@@ -1824,7 +1851,7 @@ function decorateSessionsWithRuntimeState(sessions) {
1824
1851
  const sessionId = status?.sessionId || status?.groupId || null;
1825
1852
  if (!sessionId) continue;
1826
1853
  const state = status.state || 'idle';
1827
- const running = !['idle', 'offline', 'completed', 'failed', 'aborted'].includes(state);
1854
+ const running = isVpStatusRunning(state);
1828
1855
  const updatedAt = status.updatedAt || status.since || Date.now();
1829
1856
  const prev = bySession.get(sessionId) || { running: false, runningVpCount: 0, latestActivityAt: 0 };
1830
1857
  if (running) prev.runningVpCount += 1;
@@ -2354,6 +2381,89 @@ function maybeTransitionVpStatus(hctx, state) {
2354
2381
  }
2355
2382
  }
2356
2383
 
2384
+ const STREAM_TEXT_BATCH_MAX_CHARS = 200;
2385
+ const STREAM_TEXT_BATCH_MAX_MS = 200;
2386
+
2387
+ function createStreamTextBatch() {
2388
+ return {
2389
+ parts: [],
2390
+ charCount: 0,
2391
+ timer: null,
2392
+ envelope: null,
2393
+ immediateNext: true,
2394
+ };
2395
+ }
2396
+
2397
+ function getStreamTextBatch(hctx) {
2398
+ if (!hctx) return null;
2399
+ if (!hctx.streamTextBatch) hctx.streamTextBatch = createStreamTextBatch();
2400
+ return hctx.streamTextBatch;
2401
+ }
2402
+
2403
+ function clearStreamTextBatchTimer(batch) {
2404
+ if (!batch?.timer) return;
2405
+ clearTimeout(batch.timer);
2406
+ batch.timer = null;
2407
+ }
2408
+
2409
+ function sendAssistantTextFrame(text, envelope) {
2410
+ if (!text) return;
2411
+ sendSessionOutputFrame({
2412
+ type: 'assistant',
2413
+ message: { content: [{ type: 'text', text }] },
2414
+ }, envelope);
2415
+ }
2416
+
2417
+ function flushStreamTextBatch(hctx, envelope, { resetImmediate = false } = {}) {
2418
+ const batch = hctx?.streamTextBatch;
2419
+ if (!batch) return false;
2420
+ clearStreamTextBatchTimer(batch);
2421
+ const text = batch.parts.join('');
2422
+ batch.parts = [];
2423
+ batch.charCount = 0;
2424
+ const flushEnvelope = envelope || batch.envelope;
2425
+ batch.envelope = flushEnvelope || null;
2426
+ if (resetImmediate) batch.immediateNext = true;
2427
+ if (!text) return false;
2428
+ sendAssistantTextFrame(text, flushEnvelope);
2429
+ return true;
2430
+ }
2431
+
2432
+ function scheduleStreamTextBatchFlush(hctx, batch) {
2433
+ if (!hctx || batch.timer) return;
2434
+ batch.timer = setTimeout(() => {
2435
+ batch.timer = null;
2436
+ flushStreamTextBatch(hctx, batch.envelope);
2437
+ }, STREAM_TEXT_BATCH_MAX_MS);
2438
+ if (batch.timer && typeof batch.timer.unref === 'function') {
2439
+ batch.timer.unref();
2440
+ }
2441
+ }
2442
+
2443
+ function queueStreamTextDelta(hctx, text, envelope) {
2444
+ if (typeof text !== 'string' || text.length === 0) return;
2445
+ const batch = getStreamTextBatch(hctx);
2446
+ if (!batch) {
2447
+ sendAssistantTextFrame(text, envelope);
2448
+ return;
2449
+ }
2450
+
2451
+ batch.envelope = envelope;
2452
+ if (batch.immediateNext) {
2453
+ batch.immediateNext = false;
2454
+ sendAssistantTextFrame(text, envelope);
2455
+ return;
2456
+ }
2457
+
2458
+ batch.parts.push(text);
2459
+ batch.charCount += text.length;
2460
+ if (batch.charCount >= STREAM_TEXT_BATCH_MAX_CHARS) {
2461
+ flushStreamTextBatch(hctx, envelope);
2462
+ return;
2463
+ }
2464
+ scheduleStreamTextBatchFlush(hctx, batch);
2465
+ }
2466
+
2357
2467
  /**
2358
2468
  * Handle a single engine event unwrapped from an `engine_event` envelope.
2359
2469
  * Stamps threadId on every outgoing frame so frontend grouping, tools,
@@ -2375,13 +2485,17 @@ function handleEngineEvent(event, hctx) {
2375
2485
  threadId: hctx.threadId || event.threadId,
2376
2486
  };
2377
2487
 
2488
+ if (event.type !== 'text_delta') {
2489
+ // Preserve wire order. Any boundary/metadata/tool event must see all text
2490
+ // accepted before it flushed first; otherwise the browser can render a tool
2491
+ // call or terminal result before the text that led to it.
2492
+ flushStreamTextBatch(hctx, envelope, { resetImmediate: true });
2493
+ }
2494
+
2378
2495
  switch (event.type) {
2379
2496
  case 'text_delta':
2380
2497
  hctx.assistantTextParts.push(event.text);
2381
- sendSessionOutputFrame({
2382
- type: 'assistant',
2383
- message: { content: [{ type: 'text', text: event.text }] },
2384
- }, envelope);
2498
+ queueStreamTextDelta(hctx, event.text, envelope);
2385
2499
  // vp-status: first text-delta of a (thinking|tool) phase flips
2386
2500
  // the row to 'streaming'. transition() is a no-op when already
2387
2501
  // streaming, so subsequent deltas are cheap.
@@ -3410,6 +3524,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3410
3524
  let turnEndReason = 'end_turn';
3411
3525
  let turnEndEmitted = false;
3412
3526
  let turnEndDetail = null;
3527
+ let handlerCtx = null;
3413
3528
  const markTurnEnd = (reason) => { turnEndEmitted = true; turnEndReason = reason; };
3414
3529
  const emitVpTurnEnd = (reason, detail = null) => {
3415
3530
  if (turnEndEmitted) return;
@@ -3483,7 +3598,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3483
3598
  vpEngine = getOrCreateVpEngine(sessionId, vpId, threadId);
3484
3599
  if (thread) thread.engine = vpEngine;
3485
3600
 
3486
- const handlerCtx = {
3601
+ handlerCtx = {
3487
3602
  assistantTextParts,
3488
3603
  toolCallsAccum,
3489
3604
  toolResultsAccum,
@@ -3529,6 +3644,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3529
3644
  handleEngineEvent(event, handlerCtx);
3530
3645
  }
3531
3646
 
3647
+ flushStreamTextBatch(handlerCtx, envelope, { resetImmediate: true });
3648
+
3532
3649
  // Turn completed — atomically append this VP's output to shared history.
3533
3650
  // route_forward handoff text is an internal trigger, already visible as
3534
3651
  // the source VP's tool action. Do not append it as a visible prompt for
@@ -3557,6 +3674,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3557
3674
  } catch (err) {
3558
3675
  const isAbort = err && (err.name === 'AbortError' || err.name === 'LLMAbortError');
3559
3676
  if (isAbort) {
3677
+ flushStreamTextBatch(handlerCtx, envelope, { resetImmediate: true });
3560
3678
  sendSessionOutputFrame({
3561
3679
  type: 'result',
3562
3680
  result_text: '',
@@ -3581,6 +3699,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3581
3699
  console.warn('[Yeaft] vp-status error transition failed:', brokerErr?.message || brokerErr);
3582
3700
  }
3583
3701
 
3702
+ flushStreamTextBatch(handlerCtx, envelope, { resetImmediate: true });
3703
+
3584
3704
  if (isPermissionErrorMsg(err.message)) {
3585
3705
  if (!_permissionDiagnosticSent) {
3586
3706
  _permissionDiagnosticSent = true;
@@ -4358,7 +4478,7 @@ export async function handleYeaftFetchToolStats(_msg = {}) {
4358
4478
  }
4359
4479
 
4360
4480
  /**
4361
- * Hydrate the YeaftDebugPanel from the persistent SQLite trace. The
4481
+ * Hydrate the YeaftDebugPanel from the persistent file-backed trace. The
4362
4482
  * panel state (`yeaftDebugLoops` / `yeaftDebugTurnsById`) is otherwise
4363
4483
  * built ONLY from in-flight `loop` / `turn_open` events on the wire,
4364
4484
  * so a panel opened after a turn has finished sees nothing for that
@@ -4366,28 +4486,36 @@ export async function handleYeaftFetchToolStats(_msg = {}) {
4366
4486
  * splices into place.
4367
4487
  *
4368
4488
  * Inputs (all optional):
4369
- * - `limit` max number of loops to return (1..500, default 100)
4370
- * - `sessionId` narrow by group
4371
- * - `threadId` narrow by thread
4489
+ * - `limit` request cap; bounded by the file trace store
4490
+ * - `indexOnly` list request summaries without loop/detail payloads
4491
+ * - `detailTurnId` fetch full loops/tools for one request
4492
+ * - `sessionId` — narrow by Session
4493
+ * - `threadId` — narrow by thread
4494
+ * - `search` — regex matched against bounded request summaries
4372
4495
  *
4373
4496
  * Sends:
4374
- * { type: 'yeaft_debug_history', loops: [...], turns: [...] }
4497
+ * { type: 'yeaft_debug_history', loops: [...], turns: [...], indexOnly, detailTurnId }
4375
4498
  *
4376
4499
  * Best-effort: if the session / trace isn't ready, sends an empty
4377
4500
  * snapshot so the panel renders a placeholder instead of spinning.
4378
4501
  */
4379
4502
  export async function handleYeaftFetchDebugHistory(msg = {}) {
4380
- const limit = Number.isFinite(msg?.limit) ? Number(msg.limit) : 100;
4503
+ const limit = Number.isFinite(msg?.limit) ? Number(msg.limit) : 10;
4381
4504
  const dreamLimit = Number.isFinite(msg?.dreamLimit) ? Number(msg.dreamLimit) : 5;
4382
4505
  const sessionId = typeof msg?.sessionId === 'string' && msg.sessionId ? msg.sessionId : null;
4383
4506
  const threadId = typeof msg?.threadId === 'string' && msg.threadId ? msg.threadId : null;
4507
+ const search = typeof msg?.search === 'string' ? msg.search.trim() : '';
4508
+ const requestId = typeof msg?.requestId === 'string' && msg.requestId ? msg.requestId : null;
4509
+ const requestKind = typeof msg?.requestKind === 'string' && msg.requestKind ? msg.requestKind : null;
4510
+ const indexOnly = !!msg?.indexOnly;
4511
+ const detailTurnId = typeof msg?.detailTurnId === 'string' && msg.detailTurnId ? msg.detailTurnId : null;
4384
4512
  let loops = [];
4385
4513
  let turns = [];
4386
4514
  let dreamEvents = [];
4387
4515
  let hasMore = false;
4388
4516
  try {
4389
4517
  if (session?.trace && typeof session.trace.fetchRecentDebugHistory === 'function') {
4390
- const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId });
4518
+ const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId, indexOnly, detailTurnId, search });
4391
4519
  loops = Array.isArray(out?.loops) ? out.loops : [];
4392
4520
  turns = Array.isArray(out?.turns) ? out.turns : [];
4393
4521
  dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
@@ -4399,6 +4527,14 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
4399
4527
  loops: [],
4400
4528
  turns: [],
4401
4529
  dreamEvents: [],
4530
+ requestId,
4531
+ requestKind,
4532
+ sessionId,
4533
+ threadId,
4534
+ search,
4535
+ limit,
4536
+ indexOnly,
4537
+ detailTurnId,
4402
4538
  error: err && err.message ? err.message : String(err),
4403
4539
  });
4404
4540
  return;
@@ -4408,10 +4544,15 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
4408
4544
  loops,
4409
4545
  turns,
4410
4546
  dreamEvents,
4547
+ requestId,
4548
+ requestKind,
4411
4549
  sessionId,
4412
4550
  threadId,
4551
+ search,
4413
4552
  hasMore,
4414
4553
  limit,
4554
+ indexOnly,
4555
+ detailTurnId,
4415
4556
  });
4416
4557
  }
4417
4558
 
@@ -4483,6 +4624,54 @@ export function handleYeaftSubAgentPrompt(msg) {
4483
4624
  }, { sessionId, vpId: task.ownerVpId || null, threadId: task.source?.threadId || null });
4484
4625
  }
4485
4626
 
4627
+ export function handleYeaftTaskCancel(msg) {
4628
+ const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
4629
+ const taskId = typeof msg?.taskId === 'string' ? msg.taskId.trim() : '';
4630
+ const clientRequestId = typeof msg?.clientRequestId === 'string' ? msg.clientRequestId.trim() : '';
4631
+ const fail = (error, task = null) => {
4632
+ sendSessionEvent({
4633
+ type: 'yeaft_task_cancel_result',
4634
+ success: false,
4635
+ taskId: taskId || null,
4636
+ clientRequestId: clientRequestId || null,
4637
+ error,
4638
+ ...(task ? { task } : {}),
4639
+ }, sessionId ? { sessionId, vpId: task?.ownerVpId || null, threadId: task?.source?.threadId || null } : undefined);
4640
+ };
4641
+
4642
+ if (!sessionId || !taskId) {
4643
+ fail('sessionId and taskId are required');
4644
+ return;
4645
+ }
4646
+ if (!session?.taskManager || typeof session.taskManager.cancelTask !== 'function') {
4647
+ fail('task manager unavailable');
4648
+ return;
4649
+ }
4650
+
4651
+ let result;
4652
+ try {
4653
+ result = session.taskManager.cancelTask(sessionId, taskId);
4654
+ } catch (err) {
4655
+ fail(err?.message || String(err));
4656
+ return;
4657
+ }
4658
+
4659
+ const task = result?.task || session.taskManager.getTask?.(sessionId, taskId) || null;
4660
+ if (!result?.ok) {
4661
+ fail(result?.error || 'Failed to cancel task', task);
4662
+ return;
4663
+ }
4664
+
4665
+ sendSessionEvent({
4666
+ type: 'yeaft_task_cancel_result',
4667
+ success: true,
4668
+ taskId,
4669
+ clientRequestId: clientRequestId || null,
4670
+ pending: !!result?.pending,
4671
+ task,
4672
+ }, { sessionId, vpId: task?.ownerVpId || null, threadId: task?.source?.threadId || null });
4673
+ }
4674
+
4486
4675
  /** Deprecated mode switch — Yeaft is single-mode. */
4487
4676
  export function handleYeaftModeSwitch(_msg) {
4488
4677
  console.warn('[Yeaft] yeaft_mode_switch is deprecated and ignored — Yeaft now runs in a single unified mode.');
@@ -5112,6 +5301,7 @@ export async function handleYeaftMcpReload(msg = {}) {
5112
5301
  export const __testHooks = {
5113
5302
  loadVisibleGroupHistoryPage,
5114
5303
  persistInboundMessageOnceByMsgId,
5304
+ buildPendingRescueEnvelope,
5115
5305
  setSessionForTest(nextSession) {
5116
5306
  session = nextSession || null;
5117
5307
  },
@@ -5121,6 +5311,13 @@ export const __testHooks = {
5121
5311
  vpAborts.clear();
5122
5312
  vpInboxes.clear();
5123
5313
  },
5314
+ resetVpStatusBroker() {
5315
+ if (vpStatusBroker) vpStatusBroker.reset();
5316
+ },
5317
+ seedVpStatus(status) {
5318
+ return getVpStatusBroker().transition(status);
5319
+ },
5320
+ decorateSessionsWithRuntimeState,
5124
5321
  seedQueuedVpTurn({ sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test' } = {}) {
5125
5322
  const key = threadKey(sessionId, vpId, threadId);
5126
5323
  const inbox = vpInboxes.get(key) || [];