@yeaft/webchat-agent 0.1.680 → 0.1.683
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 +1 -1
- package/unify/conversation/persist.js +36 -0
- package/unify/tool-folding/index.js +38 -8
- package/unify/web-bridge.js +18 -2
package/package.json
CHANGED
|
@@ -442,6 +442,42 @@ export class ConversationStore {
|
|
|
442
442
|
return this.#loadFromDir(this.#msgDir, Infinity);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Load recent hot messages stamped with `groupId`, sorted chronologically.
|
|
447
|
+
* Group-history-isolation (Bug 7): a message lives in exactly one group.
|
|
448
|
+
* Messages without a `groupId` frontmatter (legacy / pre-grouping) are
|
|
449
|
+
* NOT returned — they would otherwise leak into every group's stream.
|
|
450
|
+
*
|
|
451
|
+
* Implementation note: filters AFTER reading the most recent N files
|
|
452
|
+
* because the on-disk order is global by sequence id. We over-read by
|
|
453
|
+
* loading all hot files and slicing the tail of the filtered set so
|
|
454
|
+
* `limit` reflects "N most recent messages in this group", not "N most
|
|
455
|
+
* recent messages on disk that happen to be in this group". For typical
|
|
456
|
+
* inboxes (≤ a few thousand hot messages) this is cheap; if it ever
|
|
457
|
+
* becomes a hot path we add a per-group on-disk index.
|
|
458
|
+
*
|
|
459
|
+
* @param {string} groupId — required; null/empty returns []
|
|
460
|
+
* @param {number} [limit=50]
|
|
461
|
+
* @returns {object[]}
|
|
462
|
+
*/
|
|
463
|
+
loadRecentByGroup(groupId, limit = 50) {
|
|
464
|
+
if (!groupId) return [];
|
|
465
|
+
const all = this.#loadFromDir(this.#msgDir, Infinity);
|
|
466
|
+
const filtered = all.filter(m => m && m.groupId === groupId);
|
|
467
|
+
if (limit === Infinity || limit < 0) return filtered;
|
|
468
|
+
return filtered.slice(-limit);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Load every hot message stamped with `groupId`.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} groupId
|
|
475
|
+
* @returns {object[]}
|
|
476
|
+
*/
|
|
477
|
+
loadAllByGroup(groupId) {
|
|
478
|
+
return this.loadRecentByGroup(groupId, Infinity);
|
|
479
|
+
}
|
|
480
|
+
|
|
445
481
|
/**
|
|
446
482
|
* Count hot messages.
|
|
447
483
|
*
|
|
@@ -23,12 +23,27 @@ export { buildFallbackStub } from './fallback-stub.js';
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Collapse messages[startIdx..endIdx] (inclusive) into a single
|
|
26
|
-
* `{ role: '
|
|
27
|
-
* mutate the input.
|
|
26
|
+
* `{ role: 'user', content }` reflection message. Returns a NEW array; does
|
|
27
|
+
* not mutate the input.
|
|
28
28
|
*
|
|
29
|
-
* The original assistant+tool sequence (the action arc) is replaced by
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* The original assistant+tool sequence (the action arc) is replaced by ONE
|
|
30
|
+
* synthetic user message carrying the reflection summary. User messages
|
|
31
|
+
* that happened to appear inside the range stay put (defensive — the caller
|
|
32
|
+
* normally passes a range that contains only assistant+tool).
|
|
33
|
+
*
|
|
34
|
+
* Why role='user' (not 'assistant'):
|
|
35
|
+
* The Anthropic Messages API requires the messages array to end with a
|
|
36
|
+
* user message before the next assistant turn. If we collapsed into an
|
|
37
|
+
* assistant message and the reflection happened to land at the tail
|
|
38
|
+
* (e.g. immediately before #applyPendingT2Reflections fires its next
|
|
39
|
+
* query), the API rejects the request with "model does not support
|
|
40
|
+
* assistant message prefill".
|
|
41
|
+
*
|
|
42
|
+
* Following Claude Code's compact pattern, we wrap the reflection as a
|
|
43
|
+
* synthetic user message — the model treats it as a context-recovery
|
|
44
|
+
* directive and continues from there. The opening line ("The previous N
|
|
45
|
+
* tool calls have been folded ...") plus the closing "Continue from
|
|
46
|
+
* here." make it unambiguous that this is not a fresh user prompt.
|
|
32
47
|
*
|
|
33
48
|
* @param {Array} messages
|
|
34
49
|
* @param {number} startIdx
|
|
@@ -43,13 +58,28 @@ export function collapseRangeToReflection(messages, startIdx, endIdx, reflection
|
|
|
43
58
|
const collapsed = messages.slice(startIdx, endIdx + 1);
|
|
44
59
|
const after = messages.slice(endIdx + 1);
|
|
45
60
|
|
|
61
|
+
// Count tool_use occurrences inside the collapsed range so the wrapper
|
|
62
|
+
// text can name how many calls were folded. Falls back to "previous"
|
|
63
|
+
// wording when no tool calls are detected.
|
|
64
|
+
let toolCount = 0;
|
|
65
|
+
for (const m of collapsed) {
|
|
66
|
+
if (m && m.role === 'assistant' && Array.isArray(m.toolCalls)) {
|
|
67
|
+
toolCount += m.toolCalls.length;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
// Preserve any user messages that happened to appear inside the range
|
|
47
72
|
// (not expected per V7 spec, but defensive). Everything else (assistant +
|
|
48
|
-
// tool) is replaced by ONE
|
|
73
|
+
// tool) is replaced by ONE synthetic user reflection message.
|
|
49
74
|
const preservedUsers = collapsed.filter(m => m && m.role === 'user');
|
|
75
|
+
const header = toolCount > 0
|
|
76
|
+
? `The previous ${toolCount} tool call${toolCount === 1 ? '' : 's'} have been folded for context efficiency.`
|
|
77
|
+
: 'The previous tool calls have been folded for context efficiency.';
|
|
78
|
+
const wrappedContent =
|
|
79
|
+
`${header}\n\nSummary:\n${reflectionContent}\n\nContinue from here.`;
|
|
50
80
|
const reflectionMsg = {
|
|
51
|
-
role: '
|
|
52
|
-
content:
|
|
81
|
+
role: 'user',
|
|
82
|
+
content: wrappedContent,
|
|
53
83
|
_reflection: true,
|
|
54
84
|
};
|
|
55
85
|
return [...before, ...preservedUsers, reflectionMsg, ...after];
|
package/unify/web-bridge.js
CHANGED
|
@@ -1384,8 +1384,18 @@ export function handleUnifyModelSwitch(msg) {
|
|
|
1384
1384
|
/**
|
|
1385
1385
|
* Handle history load request. Loads recent messages from ConversationStore
|
|
1386
1386
|
* and replays them through the standard claude_output pipeline.
|
|
1387
|
+
*
|
|
1388
|
+
* Group-history-isolation (Bug 7): when `msg.groupId` is provided the
|
|
1389
|
+
* replay AND the engine's bootstrap context are filtered to that group.
|
|
1390
|
+
* Messages tagged with another groupId — and legacy messages with no
|
|
1391
|
+
* groupId at all — are excluded so a stale `grp_default` (or any other
|
|
1392
|
+
* group) never bleeds into the active group's pane.
|
|
1387
1393
|
*/
|
|
1388
1394
|
export async function handleUnifyLoadHistory(msg) {
|
|
1395
|
+
const groupId = (msg && typeof msg.groupId === 'string' && msg.groupId) || null;
|
|
1396
|
+
const pickRecent = (store, lim) =>
|
|
1397
|
+
groupId ? store.loadRecentByGroup(groupId, lim) : store.loadRecent(lim);
|
|
1398
|
+
|
|
1389
1399
|
if (!session) {
|
|
1390
1400
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1391
1401
|
session = await loadSession({
|
|
@@ -1397,7 +1407,12 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
1397
1407
|
|
|
1398
1408
|
unifyConversationId = `unify-${Date.now()}`;
|
|
1399
1409
|
|
|
1400
|
-
restoreHistoryFromRecent(session.conversationStore
|
|
1410
|
+
restoreHistoryFromRecent(pickRecent(session.conversationStore, 50));
|
|
1411
|
+
} else if (groupId) {
|
|
1412
|
+
// Re-entering an existing session with a (possibly new) group filter:
|
|
1413
|
+
// re-seed the engine's flat history so it doesn't carry messages from
|
|
1414
|
+
// another group into the next turn's context.
|
|
1415
|
+
restoreHistoryFromRecent(pickRecent(session.conversationStore, 50));
|
|
1401
1416
|
}
|
|
1402
1417
|
|
|
1403
1418
|
// Always replay session_ready so refresh / reconnect rebuilds UI state.
|
|
@@ -1413,7 +1428,7 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
1413
1428
|
sendGroupSnapshotBroadcast();
|
|
1414
1429
|
|
|
1415
1430
|
const limit = (typeof msg.limit === 'number') ? msg.limit : 50;
|
|
1416
|
-
const messages = limit > 0 ? session.conversationStore
|
|
1431
|
+
const messages = limit > 0 ? pickRecent(session.conversationStore, limit) : [];
|
|
1417
1432
|
const compactSummary = session.conversationStore.readCompactSummary();
|
|
1418
1433
|
|
|
1419
1434
|
for (const m of messages) {
|
|
@@ -1434,6 +1449,7 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
1434
1449
|
hasCompactSummary: !!compactSummary,
|
|
1435
1450
|
totalHot: session.conversationStore.countHot(),
|
|
1436
1451
|
totalCold: session.conversationStore.countCold(),
|
|
1452
|
+
groupId,
|
|
1437
1453
|
});
|
|
1438
1454
|
}
|
|
1439
1455
|
|