@yeaft/webchat-agent 0.1.797 → 0.1.799

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": "0.1.797",
3
+ "version": "0.1.799",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -50,7 +50,11 @@ export function createDreamScheduler({ run, intervalMs = DEFAULT_INTERVAL_MS, ke
50
50
  async function fire(opts) {
51
51
  if (inflight) {
52
52
  log.warn?.('[dream] tick dropped — previous run still in progress');
53
- return inflight;
53
+ return {
54
+ skipped: true,
55
+ skippedReason: 'already-running',
56
+ trigger: opts?.manual ? 'manual' : 'auto',
57
+ };
54
58
  }
55
59
  inflight = (async () => {
56
60
  try {
@@ -52,6 +52,7 @@ import {
52
52
  } from './history-compact.js';
53
53
  import { persistUnifyAttachments, attachmentsForPersistence } from './attachments.js';
54
54
  import { parseSeqFromId } from './conversation/persist.js';
55
+ import { sliceLastNTurns } from './turn-utils.js';
55
56
  import { createVpStatusBroker } from './vp-status-broker.js';
56
57
  import { classifyThread as defaultClassifyThread, fallbackTitle } from './vp/thread-classifier.js';
57
58
 
@@ -76,8 +77,8 @@ export function __testSetThreadClassifier(fn) {
76
77
  * the first's inflight promise and dropped its own scope filter. So
77
78
  * "B during A's run" doesn't actually produce a separate scoped pass
78
79
  * for B — letting B install a second sink wrapper would only mis-stamp
79
- * A's events with B's groupId. Rejecting B with an explicit error is
80
- * the honest answer; the user can re-click after A settles.
80
+ * A's events with B's groupId. Reporting B as an explicit skipped
81
+ * result is the honest answer; the user can re-click after A settles.
81
82
  * @type {Set<string>}
82
83
  */
83
84
  const inflightScopedDreamGroups = new Set();
@@ -523,6 +524,54 @@ function projectPersistedToHistoryEntry(m) {
523
524
  return entry;
524
525
  }
525
526
 
527
+ function projectPersistedToVisibleHistoryEntry(m) {
528
+ const entry = projectPersistedToHistoryEntry(m);
529
+ return entry && (entry.role === 'user' || entry.role === 'assistant') ? entry : null;
530
+ }
531
+
532
+ function loadVisibleGroupHistoryPage(store, groupId, limit, beforeSeq = null) {
533
+ if (!store || !groupId || !(limit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
534
+
535
+ let rows = [];
536
+ try {
537
+ if (typeof store.loadOlderByGroup === 'function') {
538
+ // Use an unbounded raw prefix, then project/slice visible rows below.
539
+ // This preserves loadOlderByGroup's hot+cold scan without letting raw
540
+ // reflection/internal rows consume the UI-visible page window.
541
+ rows = store.loadOlderByGroup(groupId, beforeSeq, Infinity).messages || [];
542
+ } else if (Number.isFinite(beforeSeq)) {
543
+ const all = typeof store.loadAllByGroup === 'function'
544
+ ? store.loadAllByGroup(groupId)
545
+ : store.loadRecentByGroup(groupId, Infinity);
546
+ rows = all.filter(m => parseSeqFromId(m?.id) < beforeSeq);
547
+ } else if (typeof store.loadAllByGroup === 'function') {
548
+ rows = store.loadAllByGroup(groupId);
549
+ } else {
550
+ rows = store.loadRecentByGroup(groupId, Infinity);
551
+ }
552
+ } catch (err) {
553
+ console.error('[Unify] visible history page load failed:', err?.message || err);
554
+ return { messages: [], oldestSeq: null, hasMore: false };
555
+ }
556
+
557
+ const visible = rows
558
+ .map(projectPersistedToVisibleHistoryEntry)
559
+ .filter(Boolean);
560
+ const messages = sliceLastNTurns(visible, limit);
561
+ const oldestSeq = messages.length ? parseSeqFromId(messages[0].id) : null;
562
+ const firstVisibleSeq = visible.length ? parseSeqFromId(visible[0].id) : null;
563
+ const hasMore = messages.length > 0
564
+ && Number.isFinite(oldestSeq)
565
+ && Number.isFinite(firstVisibleSeq)
566
+ && oldestSeq > firstVisibleSeq;
567
+
568
+ return {
569
+ messages,
570
+ oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
571
+ hasMore,
572
+ };
573
+ }
574
+
526
575
  /**
527
576
  * Hydrate a freshly-created GroupContext's history from the on-disk
528
577
  * conversation store. Returns an empty array if the session isn't
@@ -2955,10 +3004,12 @@ export function normalizeDreamResult(result) {
2955
3004
  .filter(t => t && t.status === 'error')
2956
3005
  .map(t => ({ target: t.target || null, error: t.error || 'unknown' }));
2957
3006
  const hardError = result?.error || null;
2958
- const skipped = !hardError && groupsProcessed === 0 && targetsApplied === 0;
3007
+ const explicitSkipped = result?.skipped === true;
3008
+ const skipped = !hardError && (explicitSkipped || (groupsProcessed === 0 && targetsApplied === 0));
2959
3009
  const skippedReason = skipped
2960
- ? (skippedGroups[0]?.reason || 'no-targets-applied')
3010
+ ? (result?.skippedReason || skippedGroups[0]?.reason || 'no-targets-applied')
2961
3011
  : null;
3012
+ const trigger = result?.trigger || null;
2962
3013
  const success = !hardError && targetErrors.length === 0 && !skipped && targetsApplied > 0;
2963
3014
 
2964
3015
  return {
@@ -2971,6 +3022,7 @@ export function normalizeDreamResult(result) {
2971
3022
  targetErrors,
2972
3023
  entriesCreated: targetsApplied,
2973
3024
  lastDreamAt: result?.startedAt || new Date().toISOString(),
3025
+ trigger,
2974
3026
  error: hardError || (targetErrors[0]?.error || null),
2975
3027
  };
2976
3028
  }
@@ -3007,11 +3059,16 @@ export async function handleUnifyDreamTrigger(msg = {}) {
3007
3059
  // dream-v2/schedule.js inflight reuse), so the user-facing semantics
3008
3060
  // are unchanged ("you already asked").
3009
3061
  if (groupId && inflightScopedDreamGroups.size > 0) {
3010
- const error = 'A dream pass is already running.';
3062
+ const skippedResult = {
3063
+ skipped: true,
3064
+ skippedReason: 'already-running',
3065
+ trigger: msg.manual === false ? 'auto' : 'manual',
3066
+ };
3011
3067
  sendToServer({
3012
3068
  type: 'unify_dream_result',
3013
3069
  ...tag,
3014
- ...normalizeDreamResult({ error }),
3070
+ ...skippedResult,
3071
+ ...normalizeDreamResult(skippedResult),
3015
3072
  });
3016
3073
  return;
3017
3074
  }
@@ -3283,12 +3340,17 @@ export async function handleUnifyLoadHistory(msg) {
3283
3340
  // ~20–25 turns; in the turn-count world 50 turns of UI scrollback is
3284
3341
  // still cheap and matches what the frontend already passes through.
3285
3342
  const limit = (typeof msg.limit === 'number') ? msg.limit : 50;
3286
- const messages = limit > 0 ? pickRecent(session.conversationStore, limit) : [];
3343
+ const visiblePage = groupId
3344
+ ? loadVisibleGroupHistoryPage(session.conversationStore, groupId, limit)
3345
+ : { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
3287
3346
  const compactSummary = session.conversationStore.readCompactSummary();
3347
+ const replayEntries = groupId
3348
+ ? visiblePage.messages
3349
+ : visiblePage.messages
3350
+ .map(projectPersistedToVisibleHistoryEntry)
3351
+ .filter(Boolean);
3288
3352
 
3289
- for (const m of messages) {
3290
- const entry = projectPersistedToHistoryEntry(m);
3291
- if (!entry) continue;
3353
+ for (const entry of replayEntries) {
3292
3354
  if (entry.role === 'user') {
3293
3355
  sendUnifyOutput({ type: 'user', message: { content: entry.content, id: entry.id || null } }, { groupId: entry.groupId || null });
3294
3356
  } else if (entry.role === 'assistant') {
@@ -3310,33 +3372,19 @@ export async function handleUnifyLoadHistory(msg) {
3310
3372
 
3311
3373
  // Compute the pagination cursor for the bootstrap load so the frontend
3312
3374
  // knows whether a "Load older messages" hint should be shown and where
3313
- // to start the next page. The cursor is the seq of the oldest replayed
3314
- // message; `hasMore` is true iff there's an earlier message in the
3315
- // group that we did NOT replay.
3375
+ // to start the next page. For group history, this is computed from the
3376
+ // visible projected page, not raw persisted rows, so reflection/internal
3377
+ // tail rows cannot consume the bootstrap window or create false hasMore.
3316
3378
  let hasMore = false;
3317
3379
  let oldestSeq = null;
3318
- if (groupId && messages.length > 0) {
3319
- const firstId = messages[0].id;
3320
- const seq = parseSeqFromId(firstId);
3321
- // Defend against malformed ids: a NaN cursor would round-trip back as
3322
- // a poison `beforeSeq` and degrade subsequent paginations to "give me
3323
- // the newest page again". Surface as null instead.
3324
- oldestSeq = Number.isFinite(seq) ? seq : null;
3325
- if (oldestSeq != null) {
3326
- // Consult the store for whether anything older exists in the same
3327
- // group. Cheap: a single extra `loadOlderByGroup` with turns=1.
3328
- try {
3329
- const probe = session.conversationStore.loadOlderByGroup(groupId, oldestSeq, 1);
3330
- hasMore = probe.messages.length > 0;
3331
- } catch (err) {
3332
- console.error('[Unify] history-load probe failed:', err.message);
3333
- }
3334
- }
3380
+ if (groupId) {
3381
+ hasMore = visiblePage.hasMore;
3382
+ oldestSeq = visiblePage.oldestSeq;
3335
3383
  }
3336
3384
 
3337
3385
  sendUnifyEvent({
3338
3386
  type: 'history_loaded',
3339
- count: messages.length,
3387
+ count: replayEntries.length,
3340
3388
  hasCompactSummary: !!compactSummary,
3341
3389
  totalHot: session.conversationStore.countHot(),
3342
3390
  totalCold: session.conversationStore.countCold(),
@@ -3378,7 +3426,7 @@ export async function handleUnifyLoadMoreHistory(msg) {
3378
3426
 
3379
3427
  let result;
3380
3428
  try {
3381
- result = session.conversationStore.loadOlderByGroup(groupId, beforeSeq, turns);
3429
+ result = loadVisibleGroupHistoryPage(session.conversationStore, groupId, turns, beforeSeq);
3382
3430
  } catch (err) {
3383
3431
  console.error('[Unify] loadOlderByGroup failed:', err.message);
3384
3432
  result = { messages: [], oldestSeq: null, hasMore: false };
@@ -3389,8 +3437,6 @@ export async function handleUnifyLoadMoreHistory(msg) {
3389
3437
  // server-side, and stable ids + speaker attribution ride with each row
3390
3438
  // so older-history prepend renders exactly like refresh replay.
3391
3439
  const projected = (result.messages || [])
3392
- .map(projectPersistedToHistoryEntry)
3393
- .filter(m => m && (m.role === 'user' || m.role === 'assistant'))
3394
3440
  .map(m => ({
3395
3441
  ...(m.id ? { id: m.id } : {}),
3396
3442
  role: m.role,