@yeaft/webchat-agent 0.1.903 → 0.1.905
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/connection/message-router.js +10 -9
- package/package.json +1 -1
- package/yeaft/compact/compactor.js +2 -2
- package/yeaft/config-api.js +16 -6
- package/yeaft/config.js +15 -2
- package/yeaft/conversation/persist.js +173 -60
- package/yeaft/engine.js +3 -3
- package/yeaft/llm/router.js +1 -1
- package/yeaft/memory/ams-registry.js +18 -19
- package/yeaft/pair-sanitize.js +1 -1
- package/yeaft/session.js +10 -1
- package/yeaft/snapshot-filter.js +88 -0
- package/yeaft/web-bridge.js +169 -140
package/yeaft/web-bridge.js
CHANGED
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
import { persistYeaftAttachments, attachmentsForPersistence, persistedAttachmentPreviewPayload } from './attachments.js';
|
|
56
56
|
import { parseSeqFromId } from './conversation/persist.js';
|
|
57
57
|
import { sliceLastNTurns } from './turn-utils.js';
|
|
58
|
+
import { pairSanitize } from './pair-sanitize.js';
|
|
59
|
+
import { filterSnapshotForVp } from './snapshot-filter.js';
|
|
58
60
|
import { createVpStatusBroker } from './vp-status-broker.js';
|
|
59
61
|
import { classifyThread as defaultClassifyThread, fallbackTitle } from './vp/thread-classifier.js';
|
|
60
62
|
|
|
@@ -221,7 +223,7 @@ const vpCurrentTodos = new Map();
|
|
|
221
223
|
* router: ReturnType<typeof createRouter>,
|
|
222
224
|
* sessionHandle: object }>}
|
|
223
225
|
*/
|
|
224
|
-
const
|
|
226
|
+
const sessionContexts = new Map();
|
|
225
227
|
|
|
226
228
|
function vpKey(sessionId, vpId) {
|
|
227
229
|
return `${sessionId}::${vpId}`;
|
|
@@ -356,7 +358,7 @@ async function waitForRoutePromises(msgId) {
|
|
|
356
358
|
*/
|
|
357
359
|
function invalidateGroupContext(sessionId) {
|
|
358
360
|
if (!sessionId) return;
|
|
359
|
-
|
|
361
|
+
sessionContexts.delete(sessionId);
|
|
360
362
|
const prefix = `${sessionId}::`;
|
|
361
363
|
for (const [k, ctrl] of vpAborts) {
|
|
362
364
|
if (!k.startsWith(prefix)) continue;
|
|
@@ -458,7 +460,7 @@ let _vpUnsubscribe = null;
|
|
|
458
460
|
|
|
459
461
|
/**
|
|
460
462
|
* Per-group conversation history lives on the GroupContext entry
|
|
461
|
-
* (`
|
|
463
|
+
* (`sessionContexts.get(sessionId).history`). The pre-refactor module-level
|
|
462
464
|
* `conversationMessages` was a single array shared across every group —
|
|
463
465
|
* a user prompt in group-A would leak into group-B's next-turn snapshot
|
|
464
466
|
* because the bridge appended every turn to the same array regardless
|
|
@@ -466,7 +468,7 @@ let _vpUnsubscribe = null;
|
|
|
466
468
|
* the in-memory tape was unified.
|
|
467
469
|
*
|
|
468
470
|
* Post-refactor: each GroupContext owns its own `history`, lazily
|
|
469
|
-
* hydrated from `conversationStore.
|
|
471
|
+
* hydrated from `conversationStore.loadRecentBySession(sessionId)` on first
|
|
470
472
|
* access. Group-A and group-B are isolated.
|
|
471
473
|
*
|
|
472
474
|
* @typedef {Array<{role:'user'|'assistant'|'tool', content:string|Array, toolCalls?:Array, toolCallId?:string, isError?:boolean}>} GroupHistory
|
|
@@ -482,7 +484,7 @@ let _vpUnsubscribe = null;
|
|
|
482
484
|
* from disk (or explicitly assigned). The flag is required because an
|
|
483
485
|
* empty array is legitimate post-consolidate / post-clear state and
|
|
484
486
|
* MUST NOT trigger a re-hydrate. Without the flag, a partial entry
|
|
485
|
-
* would short-circuit `
|
|
487
|
+
* would short-circuit `getOrCreateSessionHistory` on truthy `[]` and skip
|
|
486
488
|
* the disk load.
|
|
487
489
|
*/
|
|
488
490
|
|
|
@@ -602,26 +604,26 @@ function loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq = null)
|
|
|
602
604
|
|
|
603
605
|
let rows = [];
|
|
604
606
|
try {
|
|
605
|
-
if (typeof store.
|
|
606
|
-
const page = store.
|
|
607
|
+
if (typeof store.loadVisibleBySession === 'function') {
|
|
608
|
+
const page = store.loadVisibleBySession(sessionId, beforeSeq, limit);
|
|
607
609
|
return {
|
|
608
610
|
messages: (page.messages || []).map(projectPersistedToVisibleHistoryEntry).filter(Boolean),
|
|
609
611
|
oldestSeq: (typeof page.oldestSeq === 'number') ? page.oldestSeq : null,
|
|
610
612
|
hasMore: !!page.hasMore,
|
|
611
613
|
};
|
|
612
|
-
} else if (typeof store.
|
|
614
|
+
} else if (typeof store.loadOlderBySession === 'function') {
|
|
613
615
|
// Compatibility fallback for older test doubles: use an unbounded raw
|
|
614
616
|
// prefix, then project/slice visible rows below.
|
|
615
|
-
rows = store.
|
|
617
|
+
rows = store.loadOlderBySession(sessionId, beforeSeq, Infinity).messages || [];
|
|
616
618
|
} else if (Number.isFinite(beforeSeq)) {
|
|
617
|
-
const all = typeof store.
|
|
618
|
-
? store.
|
|
619
|
-
: store.
|
|
619
|
+
const all = typeof store.loadAllBySession === 'function'
|
|
620
|
+
? store.loadAllBySession(sessionId)
|
|
621
|
+
: store.loadRecentBySession(sessionId, Infinity);
|
|
620
622
|
rows = all.filter(m => parseSeqFromId(m?.id) < beforeSeq);
|
|
621
|
-
} else if (typeof store.
|
|
622
|
-
rows = store.
|
|
623
|
+
} else if (typeof store.loadAllBySession === 'function') {
|
|
624
|
+
rows = store.loadAllBySession(sessionId);
|
|
623
625
|
} else {
|
|
624
|
-
rows = store.
|
|
626
|
+
rows = store.loadRecentBySession(sessionId, Infinity);
|
|
625
627
|
}
|
|
626
628
|
} catch (err) {
|
|
627
629
|
console.error('[Yeaft] visible history page load failed:', err?.message || err);
|
|
@@ -658,7 +660,7 @@ function hydrateGroupHistory(sessionId) {
|
|
|
658
660
|
if (!session?.conversationStore || !sessionId) return [];
|
|
659
661
|
let recent;
|
|
660
662
|
try {
|
|
661
|
-
recent = session.conversationStore.
|
|
663
|
+
recent = session.conversationStore.loadRecentBySession(sessionId);
|
|
662
664
|
} catch (err) {
|
|
663
665
|
console.warn('[Yeaft] hydrateGroupHistory failed (sessionId=%s):', sessionId, err?.message || err);
|
|
664
666
|
return [];
|
|
@@ -674,7 +676,7 @@ function hydrateGroupHistory(sessionId) {
|
|
|
674
676
|
/**
|
|
675
677
|
* Get-or-create the per-group history array. Used everywhere the bridge
|
|
676
678
|
* needs to read/append/snapshot a group's conversation tape. Lazily
|
|
677
|
-
* inserts an entry into `
|
|
679
|
+
* inserts an entry into `sessionContexts` on first access — no
|
|
678
680
|
* `sessionHandle` required (history is independent of coord/router
|
|
679
681
|
* lifecycle, so a sub-agent / route_forward path that hasn't yet
|
|
680
682
|
* opened the group can still read history).
|
|
@@ -687,9 +689,9 @@ function hydrateGroupHistory(sessionId) {
|
|
|
687
689
|
* @param {string} sessionId
|
|
688
690
|
* @returns {GroupHistory}
|
|
689
691
|
*/
|
|
690
|
-
function
|
|
692
|
+
function getOrCreateSessionHistory(sessionId) {
|
|
691
693
|
if (!sessionId) return [];
|
|
692
|
-
let entry =
|
|
694
|
+
let entry = sessionContexts.get(sessionId);
|
|
693
695
|
// Use `historyHydrated` rather than truthiness on `history` itself —
|
|
694
696
|
// an empty array (post-consolidate, post-clear, or a partial entry
|
|
695
697
|
// seeded by an early `getOrCreateSessionContext` call before data was
|
|
@@ -699,7 +701,7 @@ function getOrCreateGroupHistory(sessionId) {
|
|
|
699
701
|
if (entry && entry.historyHydrated) return entry.history;
|
|
700
702
|
if (!entry) {
|
|
701
703
|
entry = makeGroupContextStub();
|
|
702
|
-
|
|
704
|
+
sessionContexts.set(sessionId, entry);
|
|
703
705
|
}
|
|
704
706
|
entry.history = hydrateGroupHistory(sessionId);
|
|
705
707
|
entry.historyHydrated = true;
|
|
@@ -720,10 +722,10 @@ function getOrCreateGroupHistory(sessionId) {
|
|
|
720
722
|
*/
|
|
721
723
|
function setGroupHistory(sessionId, next) {
|
|
722
724
|
if (!sessionId) return;
|
|
723
|
-
let entry =
|
|
725
|
+
let entry = sessionContexts.get(sessionId);
|
|
724
726
|
if (!entry) {
|
|
725
727
|
entry = makeGroupContextStub();
|
|
726
|
-
|
|
728
|
+
sessionContexts.set(sessionId, entry);
|
|
727
729
|
}
|
|
728
730
|
entry.history = next;
|
|
729
731
|
entry.historyHydrated = true;
|
|
@@ -737,7 +739,7 @@ function setGroupHistory(sessionId, next) {
|
|
|
737
739
|
* @param {string} sessionId
|
|
738
740
|
*/
|
|
739
741
|
export function __testGroupHistory(sessionId) {
|
|
740
|
-
return
|
|
742
|
+
return getOrCreateSessionHistory(sessionId);
|
|
741
743
|
}
|
|
742
744
|
|
|
743
745
|
/**
|
|
@@ -758,12 +760,12 @@ export function __testSetSession(sessionLike) {
|
|
|
758
760
|
/**
|
|
759
761
|
* Test-only: peek at the GroupContext entry for a group (or undefined
|
|
760
762
|
* if never seeded). Lets tests assert the `historyHydrated` flag without
|
|
761
|
-
* exporting the entire `
|
|
763
|
+
* exporting the entire `sessionContexts` Map.
|
|
762
764
|
*
|
|
763
765
|
* @param {string} sessionId
|
|
764
766
|
*/
|
|
765
767
|
export function __testGroupContextEntry(sessionId) {
|
|
766
|
-
return
|
|
768
|
+
return sessionContexts.get(sessionId);
|
|
767
769
|
}
|
|
768
770
|
|
|
769
771
|
/**
|
|
@@ -887,9 +889,9 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
887
889
|
* @returns {{ coord: object, router: object, sessionHandle: object }}
|
|
888
890
|
*/
|
|
889
891
|
function getOrCreateSessionContext(sessionId, sessionHandle) {
|
|
890
|
-
let entry =
|
|
892
|
+
let entry = sessionContexts.get(sessionId);
|
|
891
893
|
if (entry && entry.coord && entry.router) return entry;
|
|
892
|
-
// Either no entry, or a partial entry seeded by `
|
|
894
|
+
// Either no entry, or a partial entry seeded by `getOrCreateSessionHistory`
|
|
893
895
|
// (no coord/router yet). Build the coord/router and merge into the
|
|
894
896
|
// existing record so the per-group history reference and hydration
|
|
895
897
|
// flag are preserved.
|
|
@@ -899,13 +901,13 @@ function getOrCreateSessionContext(sessionId, sessionHandle) {
|
|
|
899
901
|
const router = createRouter({ coordinator: coord });
|
|
900
902
|
if (!entry) {
|
|
901
903
|
entry = makeGroupContextStub();
|
|
902
|
-
|
|
904
|
+
sessionContexts.set(sessionId, entry);
|
|
903
905
|
}
|
|
904
906
|
entry.coord = coord;
|
|
905
907
|
entry.router = router;
|
|
906
908
|
entry.sessionHandle = sessionHandle;
|
|
907
909
|
// Defend against a future caller that builds a coord/router without
|
|
908
|
-
// having gone through `
|
|
910
|
+
// having gone through `getOrCreateSessionHistory` first: a partial entry
|
|
909
911
|
// could exist with `historyHydrated:false`, so do the load now.
|
|
910
912
|
if (!entry.historyHydrated) {
|
|
911
913
|
entry.history = hydrateGroupHistory(sessionId);
|
|
@@ -1048,8 +1050,13 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
1048
1050
|
vpAborts.set(key, vpAbort);
|
|
1049
1051
|
turnAbortCtrls.set(turnId, vpAbort);
|
|
1050
1052
|
turnAbortMeta.set(turnId, { sessionId, vpId, threadId: thread.threadId, key });
|
|
1051
|
-
const baseSnapshot =
|
|
1052
|
-
|
|
1053
|
+
const baseSnapshot = pairSanitize(
|
|
1054
|
+
filterSnapshotForVp(
|
|
1055
|
+
getOrCreateSessionHistory(sessionId)
|
|
1056
|
+
.filter((m) => !m.threadId || m.threadId === 'main' || m.threadId === thread.threadId),
|
|
1057
|
+
vpId,
|
|
1058
|
+
),
|
|
1059
|
+
);
|
|
1053
1060
|
const trigger = envelope?.trigger || 'fallback';
|
|
1054
1061
|
const { text, prompt, promptParts } = buildVpPromptPayload(vpId, envelope);
|
|
1055
1062
|
|
|
@@ -1104,7 +1111,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
1104
1111
|
try {
|
|
1105
1112
|
if (text && envelope?.msg) {
|
|
1106
1113
|
sendYeaftEvent({
|
|
1107
|
-
type: '
|
|
1114
|
+
type: 'session_message',
|
|
1108
1115
|
sessionId,
|
|
1109
1116
|
vpId,
|
|
1110
1117
|
threadId: thread.threadId,
|
|
@@ -1226,7 +1233,7 @@ export async function __testResetVpState() {
|
|
|
1226
1233
|
vpDrivers.clear();
|
|
1227
1234
|
vpEngines.clear();
|
|
1228
1235
|
vpAborts.clear();
|
|
1229
|
-
|
|
1236
|
+
sessionContexts.clear();
|
|
1230
1237
|
vpCurrentTodos.clear();
|
|
1231
1238
|
threadClassifier = defaultClassifyThread;
|
|
1232
1239
|
// Per-group compact in-flight + pending state lives on the session's
|
|
@@ -1413,31 +1420,20 @@ export function handleYeaftVpRead(msg) {
|
|
|
1413
1420
|
}
|
|
1414
1421
|
|
|
1415
1422
|
/**
|
|
1416
|
-
*
|
|
1423
|
+
* Session CRUD wired to WS events.
|
|
1417
1424
|
*/
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
if (out.sessionId != null && out.groupId == null) out.groupId = out.sessionId;
|
|
1424
|
-
if (out.groupId != null && out.sessionId == null) out.sessionId = out.groupId;
|
|
1425
|
-
sendYeaftEvent({ type: 'group_crud_result', ...out });
|
|
1426
|
-
sendYeaftEvent({ type: 'session_crud_result', ...out });
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
function sendGroupSnapshotBroadcast() {
|
|
1425
|
+
function sendSessionCrudResult(payload) {
|
|
1426
|
+
sendYeaftEvent({ type: 'session_crud_result', ...payload });
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function sendSessionSnapshotBroadcast() {
|
|
1430
1430
|
try {
|
|
1431
1431
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1432
1432
|
if (!yeaftDir) return;
|
|
1433
1433
|
const sessions = snapshotSessions(yeaftDir);
|
|
1434
|
-
// Wire-compat: emit both legacy `group_list_updated` (groups field)
|
|
1435
|
-
// and new `session_list_updated` (sessions field). Old web bundles
|
|
1436
|
-
// listen on the former.
|
|
1437
|
-
sendYeaftEvent({ type: 'group_list_updated', groups: sessions });
|
|
1438
1434
|
sendYeaftEvent({ type: 'session_list_updated', sessions });
|
|
1439
1435
|
} catch (err) {
|
|
1440
|
-
console.warn('[Yeaft]
|
|
1436
|
+
console.warn('[Yeaft] sendSessionSnapshotBroadcast failed:', err?.message || err);
|
|
1441
1437
|
}
|
|
1442
1438
|
}
|
|
1443
1439
|
|
|
@@ -1451,28 +1447,26 @@ function sendGroupSnapshotBroadcast() {
|
|
|
1451
1447
|
*
|
|
1452
1448
|
* fix-yeaft-session-per-agent: previously, Agent B's sessions were
|
|
1453
1449
|
* invisible in the unified sidebar until the user clicked into B's
|
|
1454
|
-
* Yeaft view, because `
|
|
1450
|
+
* Yeaft view, because `sendSessionSnapshotBroadcast` only fired from
|
|
1455
1451
|
* `ensureSessionLoaded`. That made the cross-agent list look broken
|
|
1456
1452
|
* ("I see A but not B even though B is online") and was a major
|
|
1457
1453
|
* contributor to the "session list disappears on switch" symptom.
|
|
1458
1454
|
*/
|
|
1459
|
-
export {
|
|
1455
|
+
export { sendSessionSnapshotBroadcast as broadcastYeaftSessionSnapshotEager };
|
|
1460
1456
|
|
|
1461
|
-
function
|
|
1457
|
+
function sendSessionRosterChanged(session) {
|
|
1462
1458
|
if (!session) return;
|
|
1463
1459
|
const payload = {
|
|
1464
1460
|
sessionId: session.id,
|
|
1465
|
-
groupId: session.id, // wire-compat for old web bundles
|
|
1466
1461
|
name: session.name,
|
|
1467
1462
|
roster: session.roster,
|
|
1468
1463
|
defaultVpId: session.defaultVpId,
|
|
1469
1464
|
workDir: session.workDir || '',
|
|
1470
1465
|
};
|
|
1471
|
-
sendYeaftEvent({ type: 'group_roster_changed', ...payload });
|
|
1472
1466
|
sendYeaftEvent({ type: 'session_roster_changed', ...payload });
|
|
1473
1467
|
}
|
|
1474
1468
|
|
|
1475
|
-
function
|
|
1469
|
+
function sessionErrorPayload(err) {
|
|
1476
1470
|
let code = 'unknown';
|
|
1477
1471
|
if (err instanceof SessionCrudError) code = err.code;
|
|
1478
1472
|
else if (err instanceof SessionConfigError) code = err.code;
|
|
@@ -1488,9 +1482,9 @@ export function handleYeaftListSessions(msg) {
|
|
|
1488
1482
|
try {
|
|
1489
1483
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1490
1484
|
const groups = snapshotSessions(yeaftDir);
|
|
1491
|
-
|
|
1485
|
+
sendSessionCrudResult({ op: 'list', requestId, ok: true, sessions: groups });
|
|
1492
1486
|
} catch (err) {
|
|
1493
|
-
|
|
1487
|
+
sendSessionCrudResult({ op: 'list', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1494
1488
|
}
|
|
1495
1489
|
}
|
|
1496
1490
|
|
|
@@ -1500,25 +1494,34 @@ export function handleYeaftCreateSession(msg) {
|
|
|
1500
1494
|
try {
|
|
1501
1495
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1502
1496
|
const group = createSessionFromSpec(yeaftDir, payload);
|
|
1503
|
-
|
|
1504
|
-
|
|
1497
|
+
sendSessionCrudResult({ op: 'create', requestId, ok: true, session: group });
|
|
1498
|
+
sendSessionSnapshotBroadcast();
|
|
1505
1499
|
} catch (err) {
|
|
1506
|
-
|
|
1500
|
+
sendSessionCrudResult({ op: 'create', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1507
1501
|
}
|
|
1508
1502
|
}
|
|
1509
1503
|
|
|
1510
1504
|
export function handleYeaftRenameSession(msg) {
|
|
1511
1505
|
const requestId = msg && msg.requestId;
|
|
1512
|
-
|
|
1506
|
+
// fix-yeaft-delete-and-agent-revert: accept legacy `groupId` in
|
|
1507
|
+
// addition to `sessionId`. The contract documented in
|
|
1508
|
+
// `web/stores/sessions.js` header ("Inbound payloads may carry
|
|
1509
|
+
// either sessionId (new) or groupId (legacy); both are accepted,
|
|
1510
|
+
// prefer sessionId") was only honored on web-side reads; the agent
|
|
1511
|
+
// handlers were silently rejecting the older wire shape that today's
|
|
1512
|
+
// SessionSettingsModal still sends. That's what made delete/rename/
|
|
1513
|
+
// archive/update_config/add_member/remove_member/set_default_vp
|
|
1514
|
+
// all throw `not_found` on undefined ids.
|
|
1515
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1513
1516
|
const name = msg && msg.name;
|
|
1514
1517
|
try {
|
|
1515
1518
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1516
1519
|
const group = renameSession(yeaftDir, sessionId, name);
|
|
1517
1520
|
invalidateGroupContext(sessionId);
|
|
1518
|
-
|
|
1519
|
-
|
|
1521
|
+
sendSessionCrudResult({ op: 'rename', requestId, ok: true, session: group });
|
|
1522
|
+
sendSessionSnapshotBroadcast();
|
|
1520
1523
|
} catch (err) {
|
|
1521
|
-
|
|
1524
|
+
sendSessionCrudResult({ op: 'rename', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1522
1525
|
}
|
|
1523
1526
|
}
|
|
1524
1527
|
|
|
@@ -1538,7 +1541,8 @@ export function handleYeaftRenameSession(msg) {
|
|
|
1538
1541
|
*/
|
|
1539
1542
|
export function handleYeaftUpdateSession(msg) {
|
|
1540
1543
|
const requestId = msg && msg.requestId;
|
|
1541
|
-
|
|
1544
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1545
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1542
1546
|
const patch = (msg && msg.patch && typeof msg.patch === 'object') ? msg.patch : null;
|
|
1543
1547
|
try {
|
|
1544
1548
|
const hasName = patch && typeof patch.name === 'string' && patch.name.trim().length > 0;
|
|
@@ -1555,10 +1559,10 @@ export function handleYeaftUpdateSession(msg) {
|
|
|
1555
1559
|
group = updateSessionAnnouncement(yeaftDir, sessionId, patch.announcement);
|
|
1556
1560
|
}
|
|
1557
1561
|
invalidateGroupContext(sessionId);
|
|
1558
|
-
|
|
1559
|
-
|
|
1562
|
+
sendSessionCrudResult({ op: 'update', requestId, ok: true, session: group });
|
|
1563
|
+
sendSessionSnapshotBroadcast();
|
|
1560
1564
|
} catch (err) {
|
|
1561
|
-
|
|
1565
|
+
sendSessionCrudResult({ op: 'update', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1562
1566
|
}
|
|
1563
1567
|
}
|
|
1564
1568
|
|
|
@@ -1572,7 +1576,8 @@ export function handleYeaftUpdateSession(msg) {
|
|
|
1572
1576
|
*/
|
|
1573
1577
|
export function handleYeaftUpdateSessionConfig(msg) {
|
|
1574
1578
|
const requestId = msg && msg.requestId;
|
|
1575
|
-
|
|
1579
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1580
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1576
1581
|
const partial = (msg && msg.config && typeof msg.config === 'object') ? msg.config : null;
|
|
1577
1582
|
try {
|
|
1578
1583
|
if (!sessionId) throw new SessionConfigError('missing_group_id', 'sessionId required');
|
|
@@ -1585,30 +1590,32 @@ export function handleYeaftUpdateSessionConfig(msg) {
|
|
|
1585
1590
|
if (k.startsWith(prefix)) vpEngines.delete(k);
|
|
1586
1591
|
}
|
|
1587
1592
|
invalidateGroupContext(sessionId);
|
|
1588
|
-
|
|
1589
|
-
|
|
1593
|
+
sendSessionCrudResult({ op: 'update_config', requestId, ok: true, sessionId, config: savedConfig });
|
|
1594
|
+
sendSessionSnapshotBroadcast();
|
|
1590
1595
|
} catch (err) {
|
|
1591
|
-
|
|
1596
|
+
sendSessionCrudResult({ op: 'update_config', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1592
1597
|
}
|
|
1593
1598
|
}
|
|
1594
1599
|
|
|
1595
1600
|
export function handleYeaftArchiveSession(msg) {
|
|
1596
1601
|
const requestId = msg && msg.requestId;
|
|
1597
|
-
|
|
1602
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1603
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1598
1604
|
try {
|
|
1599
1605
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1600
1606
|
const result = archiveSession(yeaftDir, sessionId);
|
|
1601
1607
|
invalidateGroupContext(sessionId);
|
|
1602
|
-
|
|
1603
|
-
|
|
1608
|
+
sendSessionCrudResult({ op: 'archive', requestId, ok: true, sessionId: result.sessionId });
|
|
1609
|
+
sendSessionSnapshotBroadcast();
|
|
1604
1610
|
} catch (err) {
|
|
1605
|
-
|
|
1611
|
+
sendSessionCrudResult({ op: 'archive', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1606
1612
|
}
|
|
1607
1613
|
}
|
|
1608
1614
|
|
|
1609
1615
|
export function handleYeaftDeleteSession(msg) {
|
|
1610
1616
|
const requestId = msg && msg.requestId;
|
|
1611
|
-
|
|
1617
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1618
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1612
1619
|
try {
|
|
1613
1620
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1614
1621
|
const result = deleteSession(yeaftDir, sessionId);
|
|
@@ -1633,37 +1640,39 @@ export function handleYeaftDeleteSession(msg) {
|
|
|
1633
1640
|
for (const k of Array.from(vpEngines.keys())) {
|
|
1634
1641
|
if (k.startsWith(prefix)) vpEngines.delete(k);
|
|
1635
1642
|
}
|
|
1636
|
-
|
|
1643
|
+
sendSessionCrudResult({
|
|
1637
1644
|
op: 'delete',
|
|
1638
1645
|
requestId,
|
|
1639
1646
|
ok: true,
|
|
1640
1647
|
sessionId: result.sessionId,
|
|
1641
1648
|
messagesRemoved,
|
|
1642
1649
|
});
|
|
1643
|
-
|
|
1650
|
+
sendSessionSnapshotBroadcast();
|
|
1644
1651
|
} catch (err) {
|
|
1645
|
-
|
|
1652
|
+
sendSessionCrudResult({ op: 'delete', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1646
1653
|
}
|
|
1647
1654
|
}
|
|
1648
1655
|
|
|
1649
1656
|
export function handleYeaftSessionAddMember(msg) {
|
|
1650
1657
|
const requestId = msg && msg.requestId;
|
|
1651
|
-
|
|
1658
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1659
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1652
1660
|
const vpId = msg && msg.vpId;
|
|
1653
1661
|
try {
|
|
1654
1662
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1655
1663
|
const group = addMember(yeaftDir, sessionId, vpId);
|
|
1656
1664
|
invalidateGroupContext(sessionId);
|
|
1657
|
-
|
|
1658
|
-
|
|
1665
|
+
sendSessionCrudResult({ op: 'add_member', requestId, ok: true, session: group });
|
|
1666
|
+
sendSessionRosterChanged(group);
|
|
1659
1667
|
} catch (err) {
|
|
1660
|
-
|
|
1668
|
+
sendSessionCrudResult({ op: 'add_member', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1661
1669
|
}
|
|
1662
1670
|
}
|
|
1663
1671
|
|
|
1664
1672
|
export function handleYeaftSessionRemoveMember(msg) {
|
|
1665
1673
|
const requestId = msg && msg.requestId;
|
|
1666
|
-
|
|
1674
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1675
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1667
1676
|
const vpId = msg && msg.vpId;
|
|
1668
1677
|
try {
|
|
1669
1678
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
@@ -1675,25 +1684,26 @@ export function handleYeaftSessionRemoveMember(msg) {
|
|
|
1675
1684
|
for (const key of Array.from(vpEngines.keys())) {
|
|
1676
1685
|
if (key.startsWith(removedPrefix)) vpEngines.delete(key);
|
|
1677
1686
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1687
|
+
sendSessionCrudResult({ op: 'remove_member', requestId, ok: true, session: group });
|
|
1688
|
+
sendSessionRosterChanged(group);
|
|
1680
1689
|
} catch (err) {
|
|
1681
|
-
|
|
1690
|
+
sendSessionCrudResult({ op: 'remove_member', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1682
1691
|
}
|
|
1683
1692
|
}
|
|
1684
1693
|
|
|
1685
1694
|
export function handleYeaftSessionSetDefaultVp(msg) {
|
|
1686
1695
|
const requestId = msg && msg.requestId;
|
|
1687
|
-
|
|
1696
|
+
// wire-compat: accept legacy `groupId` (see handleYeaftRenameSession).
|
|
1697
|
+
const sessionId = (msg && (msg.sessionId || msg.groupId)) || null;
|
|
1688
1698
|
const vpId = msg && msg.vpId;
|
|
1689
1699
|
try {
|
|
1690
1700
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1691
1701
|
const group = setSessionDefaultVp(yeaftDir, sessionId, vpId);
|
|
1692
1702
|
invalidateGroupContext(sessionId);
|
|
1693
|
-
|
|
1694
|
-
|
|
1703
|
+
sendSessionCrudResult({ op: 'set_default_vp', requestId, ok: true, session: group });
|
|
1704
|
+
sendSessionRosterChanged(group);
|
|
1695
1705
|
} catch (err) {
|
|
1696
|
-
|
|
1706
|
+
sendSessionCrudResult({ op: 'set_default_vp', requestId, ok: false, error: sessionErrorPayload(err) });
|
|
1697
1707
|
}
|
|
1698
1708
|
}
|
|
1699
1709
|
|
|
@@ -2170,20 +2180,21 @@ function handleEngineEvent(event, hctx) {
|
|
|
2170
2180
|
}
|
|
2171
2181
|
|
|
2172
2182
|
/**
|
|
2173
|
-
* Handle a
|
|
2183
|
+
* Handle a yeaft_session_chat message from the web UI — the SOLE Yeaft
|
|
2174
2184
|
* conversation entry point.
|
|
2175
2185
|
*
|
|
2176
2186
|
* Contract (post-consolidation, was previously split between handleYeaftChat
|
|
2177
2187
|
* and handleYeaftSessionSend):
|
|
2178
|
-
* - Frontend ALWAYS sends `
|
|
2188
|
+
* - Frontend ALWAYS sends `yeaft_session_chat`. There is no `yeaft_chat`.
|
|
2179
2189
|
* - `sessionId` defaults to `'grp_default'` if missing — Yeaft is a single
|
|
2180
|
-
* conversation backed by the default
|
|
2181
|
-
* a
|
|
2182
|
-
* -
|
|
2183
|
-
*
|
|
2190
|
+
* conversation backed by the default session; the user is never "outside"
|
|
2191
|
+
* a session.
|
|
2192
|
+
* - Sessions are created up-front via `handleYeaftCreateSession`; this
|
|
2193
|
+
* handler does NOT seed missing sessions on the fly. An unknown
|
|
2194
|
+
* sessionId surfaces a clear "session not found" error to the UI.
|
|
2184
2195
|
* - Coordinator is MANDATORY (this is what guarantees ctx.router is wired
|
|
2185
2196
|
* so the `route_forward` tool can never trip `router_unavailable`).
|
|
2186
|
-
* - No legacy "no-
|
|
2197
|
+
* - No legacy "no-session" fallback paths — they were the source of the
|
|
2187
2198
|
* router_unavailable bug fixed in v0.1.671.
|
|
2188
2199
|
*/
|
|
2189
2200
|
export async function handleYeaftSessionSend(msg) {
|
|
@@ -2228,12 +2239,11 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2228
2239
|
|
|
2229
2240
|
await ensureSessionLoaded();
|
|
2230
2241
|
|
|
2231
|
-
// Open the
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2242
|
+
// Open the session. The default `grp_default` no longer self-seeds
|
|
2243
|
+
// here — session creation happens up-front via `handleYeaftCreateSession`,
|
|
2244
|
+
// so a missing dir surfaces a clear error rather than masking it.
|
|
2234
2245
|
let sessionHandle = null;
|
|
2235
2246
|
let sessionRoot = null;
|
|
2236
|
-
let seedFailed = false;
|
|
2237
2247
|
try {
|
|
2238
2248
|
const groupYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
|
|
2239
2249
|
sessionRoot = sessionsRoot(groupYeaftDir);
|
|
@@ -2247,19 +2257,16 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2247
2257
|
// default-group row across agents. Now we surface the not-found
|
|
2248
2258
|
// case so the web can show an "agent offline / session missing"
|
|
2249
2259
|
// hint instead of silently creating a different session.
|
|
2250
|
-
console.warn('[Yeaft]
|
|
2260
|
+
console.warn('[Yeaft] yeaft_session_chat: sessionId %s not found', sessionId);
|
|
2251
2261
|
}
|
|
2252
2262
|
} catch (err) {
|
|
2253
|
-
console.warn('[Yeaft]
|
|
2263
|
+
console.warn('[Yeaft] yeaft_session_chat: session open failed', err?.message || err);
|
|
2254
2264
|
}
|
|
2255
2265
|
|
|
2256
2266
|
if (!sessionHandle) {
|
|
2257
|
-
const errText = seedFailed
|
|
2258
|
-
? `⚠️ Failed to seed default group ${sessionId} — check group .yeaft permissions.`
|
|
2259
|
-
: `⚠️ Group ${sessionId} not found.`;
|
|
2260
2267
|
sendYeaftOutput({
|
|
2261
2268
|
type: 'assistant',
|
|
2262
|
-
message: { content: [{ type: 'text', text:
|
|
2269
|
+
message: { content: [{ type: 'text', text: `⚠️ Session ${sessionId} not found.` }] },
|
|
2263
2270
|
}, { sessionId });
|
|
2264
2271
|
sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
|
|
2265
2272
|
return;
|
|
@@ -2284,7 +2291,7 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2284
2291
|
if (rosterMutated) {
|
|
2285
2292
|
try { sessionHandle.close && sessionHandle.close(); } catch { /* best-effort */ }
|
|
2286
2293
|
sessionHandle = openSession(sessionRoot, sessionId);
|
|
2287
|
-
|
|
2294
|
+
sendSessionRosterChanged(sessionHandle.getMeta());
|
|
2288
2295
|
}
|
|
2289
2296
|
}
|
|
2290
2297
|
const meta2 = sessionHandle.getMeta();
|
|
@@ -2293,12 +2300,12 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2293
2300
|
setSessionDefaultVp(yeaftDir, sessionId, meta2.roster[0]);
|
|
2294
2301
|
try { sessionHandle.close && sessionHandle.close(); } catch { /* best-effort */ }
|
|
2295
2302
|
sessionHandle = openSession(sessionRoot, sessionId);
|
|
2296
|
-
|
|
2303
|
+
sendSessionRosterChanged(sessionHandle.getMeta());
|
|
2297
2304
|
rosterMutated = true;
|
|
2298
2305
|
} catch { /* best-effort */ }
|
|
2299
2306
|
}
|
|
2300
2307
|
} catch (err) {
|
|
2301
|
-
console.warn('[Yeaft]
|
|
2308
|
+
console.warn('[Yeaft] yeaft_session_chat: auto-roster heal failed', err?.message || err);
|
|
2302
2309
|
}
|
|
2303
2310
|
|
|
2304
2311
|
// task-707: per-group persistent coordinator/router. Created once per
|
|
@@ -2307,7 +2314,7 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2307
2314
|
// we replace the cached coord so it points at the freshly-opened
|
|
2308
2315
|
// sessionHandle.
|
|
2309
2316
|
if (rosterMutated) {
|
|
2310
|
-
|
|
2317
|
+
sessionContexts.delete(sessionId);
|
|
2311
2318
|
}
|
|
2312
2319
|
const sessionCtx = getOrCreateSessionContext(sessionId, sessionHandle);
|
|
2313
2320
|
const coord = sessionCtx.coord;
|
|
@@ -2331,7 +2338,7 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2331
2338
|
try {
|
|
2332
2339
|
attachmentBundle = persistYeaftAttachments(inboundFiles, { subdir: sessionId });
|
|
2333
2340
|
} catch (err) {
|
|
2334
|
-
console.warn('[Yeaft]
|
|
2341
|
+
console.warn('[Yeaft] yeaft_session_chat: attachment persist failed', err?.message || err);
|
|
2335
2342
|
}
|
|
2336
2343
|
}
|
|
2337
2344
|
// Surface partial / total upload failures to the user. We don't abort
|
|
@@ -2370,10 +2377,10 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2370
2377
|
_promptSuffix: attachmentBundle.promptSuffix,
|
|
2371
2378
|
});
|
|
2372
2379
|
} catch (err) {
|
|
2373
|
-
console.warn('[Yeaft]
|
|
2380
|
+
console.warn('[Yeaft] yeaft_session_chat: coord.ingest failed', err?.message || err);
|
|
2374
2381
|
sendYeaftOutput({
|
|
2375
2382
|
type: 'assistant',
|
|
2376
|
-
message: { content: [{ type: 'text', text: `⚠️
|
|
2383
|
+
message: { content: [{ type: 'text', text: `⚠️ Session dispatch error: ${err?.message || err}` }] },
|
|
2377
2384
|
}, { sessionId });
|
|
2378
2385
|
sendYeaftOutput({ type: 'result', result_text: '' }, { sessionId });
|
|
2379
2386
|
return;
|
|
@@ -2574,7 +2581,7 @@ async function ensureSessionLoaded() {
|
|
|
2574
2581
|
|
|
2575
2582
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
2576
2583
|
|
|
2577
|
-
// Per-group history is hydrated lazily on first `
|
|
2584
|
+
// Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
|
|
2578
2585
|
// — there's no global "all conversations" tape any more.
|
|
2579
2586
|
|
|
2580
2587
|
sendYeaftEvent({
|
|
@@ -2587,7 +2594,7 @@ async function ensureSessionLoaded() {
|
|
|
2587
2594
|
tools: session.status.tools,
|
|
2588
2595
|
yeaftDir: ctx.CONFIG?.yeaftDir || null,
|
|
2589
2596
|
});
|
|
2590
|
-
|
|
2597
|
+
sendSessionSnapshotBroadcast();
|
|
2591
2598
|
// vp-status: rebuild frontend status table from authoritative agent
|
|
2592
2599
|
// memory. Sent unconditionally so reconnect/refresh paths get the same
|
|
2593
2600
|
// bootstrap as first-load (the broker dedup logic makes a redundant
|
|
@@ -2713,7 +2720,7 @@ async function raceWithEscalation(inner, { deadlineMs, onEscalate }) {
|
|
|
2713
2720
|
* appended to `conversationMessages`.
|
|
2714
2721
|
*
|
|
2715
2722
|
* task-707: takes a coordinator `envelope` rather than the coordinator
|
|
2716
|
-
* itself; the persistent coord lives in `
|
|
2723
|
+
* itself; the persistent coord lives in `sessionContexts[sessionId]`. Uses
|
|
2717
2724
|
* `getOrCreateVpEngine(sessionId, vpId)` so each VP runs against its own
|
|
2718
2725
|
* Engine instance — private state (`#currentAbortCtrl`, `#__queryCounter`,
|
|
2719
2726
|
* `#pendingT2`, `#abortReason`, `#adjustRanByGroup`, `#execLog`) does not
|
|
@@ -2794,10 +2801,10 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2794
2801
|
|
|
2795
2802
|
// task-707: per-VP engine + persistent group coord. The coord is
|
|
2796
2803
|
// created in handleYeaftSessionSend via getOrCreateSessionContext and
|
|
2797
|
-
// cached on `
|
|
2804
|
+
// cached on `sessionContexts`; we pull it here so route_forward
|
|
2798
2805
|
// (router built from this same coord) lands envelopes back on the
|
|
2799
2806
|
// right inbox set.
|
|
2800
|
-
const sessionCtx =
|
|
2807
|
+
const sessionCtx = sessionContexts.get(sessionId);
|
|
2801
2808
|
const sessionCoordinator = sessionCtx?.coord || null;
|
|
2802
2809
|
const queryOpts = buildVpQueryOpts({
|
|
2803
2810
|
vpId,
|
|
@@ -2856,7 +2863,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2856
2863
|
}
|
|
2857
2864
|
|
|
2858
2865
|
// Turn completed — atomically append this VP's output to shared history.
|
|
2859
|
-
|
|
2866
|
+
appendTurnToSessionHistory(sessionId, threadId, vpId, [prompt, ...appendedUserPrompts], assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
|
|
2860
2867
|
|
|
2861
2868
|
sendYeaftOutput({
|
|
2862
2869
|
type: 'assistant',
|
|
@@ -2974,23 +2981,31 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2974
2981
|
* exactly once, with the same threadId as the running thread. The
|
|
2975
2982
|
* engine's own `conversationMessages` (with T1/T2 collapse applied)
|
|
2976
2983
|
* is persisted to disk via stop-hooks, so the next turn's history is
|
|
2977
|
-
* read from disk via `
|
|
2984
|
+
* read from disk via `loadRecentBySession` on next session boot. Within
|
|
2978
2985
|
* a session, this in-memory tape carries the un-collapsed form — which
|
|
2979
2986
|
* is fine because each VP turn's `engine.query` re-collapses on the fly.
|
|
2980
2987
|
*/
|
|
2981
|
-
function
|
|
2988
|
+
function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum) {
|
|
2982
2989
|
if (!sessionId) return;
|
|
2983
|
-
const history =
|
|
2990
|
+
const history = getOrCreateSessionHistory(sessionId);
|
|
2984
2991
|
const promptList = Array.isArray(prompts) ? prompts : [prompts];
|
|
2985
2992
|
for (const prompt of promptList) {
|
|
2986
2993
|
if (typeof prompt === 'string' && prompt.trim()) {
|
|
2994
|
+
// user rows intentionally carry NO speakerVpId — every VP in the
|
|
2995
|
+
// session should see the prompt in their history.
|
|
2987
2996
|
history.push({ role: 'user', content: prompt, threadId: threadId || 'main' });
|
|
2988
2997
|
}
|
|
2989
2998
|
}
|
|
2990
2999
|
|
|
2991
3000
|
const fullText = assistantTextParts.join('');
|
|
2992
3001
|
if (fullText || toolCallsAccum.length > 0) {
|
|
3002
|
+
// Stamp speakerVpId on assistant + tool rows so the in-memory
|
|
3003
|
+
// baseSnapshot filter (filterSnapshotForVp) can mirror the disk
|
|
3004
|
+
// replay's per-VP isolation rules. Without this stamp, the next
|
|
3005
|
+
// VP turn would inherit the previous VP's tool_use ids without
|
|
3006
|
+
// matching tool_result rows → Anthropic API 422.
|
|
2993
3007
|
const assistantMsg = { role: 'assistant', content: fullText, threadId: threadId || 'main' };
|
|
3008
|
+
if (vpId) assistantMsg.speakerVpId = vpId;
|
|
2994
3009
|
if (toolCallsAccum.length > 0) {
|
|
2995
3010
|
assistantMsg.toolCalls = toolCallsAccum.map(tc => ({
|
|
2996
3011
|
id: tc.id,
|
|
@@ -3002,7 +3017,9 @@ function appendTurnToGroupHistory(sessionId, threadId, prompts, assistantTextPar
|
|
|
3002
3017
|
// requires us to echo them back on the next request or the API
|
|
3003
3018
|
// returns "content[].thinking in the thinking mode must be passed
|
|
3004
3019
|
// back to the API". The signature is server-private — it stays in
|
|
3005
|
-
// this in-memory history and in agent-side persistence only.
|
|
3020
|
+
// this in-memory history and in agent-side persistence only. The
|
|
3021
|
+
// signature is also VP-private; filterSnapshotForVp drops it from
|
|
3022
|
+
// OTHER VPs' rows before each turn's payload is built.
|
|
3006
3023
|
if (Array.isArray(thinkingBlocksAccum) && thinkingBlocksAccum.length > 0) {
|
|
3007
3024
|
assistantMsg.thinkingBlocks = thinkingBlocksAccum.map(tb => (
|
|
3008
3025
|
tb.redacted
|
|
@@ -3013,13 +3030,15 @@ function appendTurnToGroupHistory(sessionId, threadId, prompts, assistantTextPar
|
|
|
3013
3030
|
history.push(assistantMsg);
|
|
3014
3031
|
|
|
3015
3032
|
for (const tr of toolResultsAccum) {
|
|
3016
|
-
|
|
3033
|
+
const toolMsg = {
|
|
3017
3034
|
role: 'tool',
|
|
3018
3035
|
toolCallId: tr.toolCallId,
|
|
3019
3036
|
content: tr.content,
|
|
3020
3037
|
isError: tr.isError,
|
|
3021
3038
|
threadId: threadId || 'main',
|
|
3022
|
-
}
|
|
3039
|
+
};
|
|
3040
|
+
if (vpId) toolMsg.speakerVpId = vpId;
|
|
3041
|
+
history.push(toolMsg);
|
|
3023
3042
|
}
|
|
3024
3043
|
}
|
|
3025
3044
|
}
|
|
@@ -3303,6 +3322,16 @@ export function __testGetRegisteredThreadIds() {
|
|
|
3303
3322
|
*/
|
|
3304
3323
|
export const __testRaceWithEscalation = raceWithEscalation;
|
|
3305
3324
|
|
|
3325
|
+
/**
|
|
3326
|
+
* Test-only: invoke `appendTurnToSessionHistory` directly. Lets the VP
|
|
3327
|
+
* stamp contract (`speakerVpId` on assistant + tool rows, none on user
|
|
3328
|
+
* rows) be pinned with a table-driven test instead of booting a full
|
|
3329
|
+
* session. See `test/agent/yeaft/web-bridge-append-turn-vp-stamp.test.js`.
|
|
3330
|
+
*/
|
|
3331
|
+
export function __testAppendTurnToSessionHistory(...args) {
|
|
3332
|
+
return appendTurnToSessionHistory(...args);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3306
3335
|
/**
|
|
3307
3336
|
* Manual dream trigger.
|
|
3308
3337
|
*
|
|
@@ -3628,11 +3657,11 @@ export function handleYeaftModelSwitch(msg) {
|
|
|
3628
3657
|
export async function handleYeaftLoadHistory(msg) {
|
|
3629
3658
|
const sessionId = (msg && typeof msg.sessionId === 'string' && msg.sessionId) || null;
|
|
3630
3659
|
// `lim` is now expressed in TURNS, not raw messages. `loadRecent` and
|
|
3631
|
-
// `
|
|
3660
|
+
// `loadRecentBySession` use turn-based slicing so the cut never lands
|
|
3632
3661
|
// mid-tool-arc. Pass `undefined` to use the persistence-layer default
|
|
3633
3662
|
// (DEFAULT_RECENT_TURNS = 20 turns).
|
|
3634
3663
|
const pickRecent = (store, lim) =>
|
|
3635
|
-
sessionId ? store.
|
|
3664
|
+
sessionId ? store.loadRecentBySession(sessionId, lim) : store.loadRecent(lim);
|
|
3636
3665
|
|
|
3637
3666
|
if (!session) {
|
|
3638
3667
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
@@ -3646,7 +3675,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3646
3675
|
|
|
3647
3676
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
3648
3677
|
|
|
3649
|
-
// Per-group history hydrates lazily via
|
|
3678
|
+
// Per-group history hydrates lazily via getOrCreateSessionHistory.
|
|
3650
3679
|
// When the load-history call carries a sessionId, force-refresh THAT
|
|
3651
3680
|
// group's tape so the next user message sees on-disk state. When
|
|
3652
3681
|
// it doesn't (legacy callers), do nothing — the per-group lazy
|
|
@@ -3670,7 +3699,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3670
3699
|
tools: session.status.tools,
|
|
3671
3700
|
yeaftDir: ctx.CONFIG?.yeaftDir || null,
|
|
3672
3701
|
});
|
|
3673
|
-
|
|
3702
|
+
sendSessionSnapshotBroadcast();
|
|
3674
3703
|
// vp-status: replay the authoritative table on reconnect so a refreshed
|
|
3675
3704
|
// frontend doesn't have to wait for the next transition to learn each
|
|
3676
3705
|
// VP's current state.
|
|
@@ -3795,8 +3824,8 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3795
3824
|
// replay, only scoped per-(group, vp) summaries count; legacy compact.md is
|
|
3796
3825
|
// reserved for non-group / pre-scoped 1:1 callers.
|
|
3797
3826
|
let hasCompactSummaryFlag = !!compactSummary;
|
|
3798
|
-
if (sessionId && typeof session.conversationStore.
|
|
3799
|
-
hasCompactSummaryFlag = session.conversationStore.
|
|
3827
|
+
if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForSession === 'function') {
|
|
3828
|
+
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForSession(sessionId);
|
|
3800
3829
|
}
|
|
3801
3830
|
|
|
3802
3831
|
// Latest seq cursor in the recent-mode reply lets the frontend stamp its
|
|
@@ -3856,7 +3885,7 @@ export async function handleYeaftLoadMoreHistory(msg) {
|
|
|
3856
3885
|
try {
|
|
3857
3886
|
result = loadVisibleGroupHistoryPage(session.conversationStore, sessionId, turns, beforeSeq);
|
|
3858
3887
|
} catch (err) {
|
|
3859
|
-
console.error('[Yeaft]
|
|
3888
|
+
console.error('[Yeaft] loadOlderBySession failed:', err.message);
|
|
3860
3889
|
result = { messages: [], oldestSeq: null, hasMore: false };
|
|
3861
3890
|
}
|
|
3862
3891
|
|
|
@@ -3902,7 +3931,7 @@ export async function resetYeaftSession() {
|
|
|
3902
3931
|
session = null;
|
|
3903
3932
|
}
|
|
3904
3933
|
yeaftConversationId = null;
|
|
3905
|
-
// Per-group histories live on
|
|
3934
|
+
// Per-group histories live on sessionContexts entries — clearing the
|
|
3906
3935
|
// map (a few lines below) drops every group's history with it. No
|
|
3907
3936
|
// separate global tape to clear.
|
|
3908
3937
|
// Re-arm the permission warning. The user might have fixed the
|
|
@@ -3922,7 +3951,7 @@ export async function resetYeaftSession() {
|
|
|
3922
3951
|
vpInboxes.clear();
|
|
3923
3952
|
vpDrivers.clear();
|
|
3924
3953
|
vpEngines.clear();
|
|
3925
|
-
|
|
3954
|
+
sessionContexts.clear();
|
|
3926
3955
|
vpCurrentTodos.clear();
|
|
3927
3956
|
threadClassifier = defaultClassifyThread;
|
|
3928
3957
|
// History-dedup cache is keyed by per-session coordinator msg ids;
|
|
@@ -3951,7 +3980,7 @@ export async function resetYeaftSession() {
|
|
|
3951
3980
|
|
|
3952
3981
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
3953
3982
|
|
|
3954
|
-
// Per-group history hydrates lazily via
|
|
3983
|
+
// Per-group history hydrates lazily via getOrCreateSessionHistory on
|
|
3955
3984
|
// first read. Nothing to seed here.
|
|
3956
3985
|
|
|
3957
3986
|
sendYeaftEvent({
|