@yeaft/webchat-agent 1.0.447 → 1.0.449
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/Dockerfile +7 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +1 -1
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/archive/turn-archive.js +1 -1
- package/yeaft/cli.js +15 -17
- package/yeaft/config.js +2 -3
- package/yeaft/conversation/internal-control.js +2 -0
- package/yeaft/conversation/persist.js +22 -216
- package/yeaft/conversation/visible-entry.js +1 -1
- package/yeaft/effort.js +0 -2
- package/yeaft/engine.js +31 -376
- package/yeaft/history-window.js +570 -0
- package/yeaft/llm/adapter.js +1 -1
- package/yeaft/llm/anthropic.js +1 -1
- package/yeaft/llm/models-dev.js +2 -1
- package/yeaft/llm/openai-responses.js +1 -1
- package/yeaft/llm/router.js +2 -3
- package/yeaft/llm/usage-accounting.js +2 -2
- package/yeaft/pair-sanitize.js +3 -3
- package/yeaft/prompts.js +3 -4
- package/yeaft/session.js +0 -51
- package/yeaft/stdio-protocol.js +0 -1
- package/yeaft/stop-hooks.js +5 -6
- package/yeaft/turn-utils.js +5 -5
- package/yeaft/web-bridge.js +57 -135
- package/yeaft/compact/compactor.js +0 -283
- package/yeaft/compact/orchestrator.js +0 -141
- package/yeaft/compact/partition.js +0 -94
- package/yeaft/compact/triggers.js +0 -54
- package/yeaft/compact/turn-group.js +0 -85
- package/yeaft/history-compact.js +0 -764
package/yeaft/session.js
CHANGED
|
@@ -27,8 +27,6 @@ import { resolveMcpPluginPolicy } from './plugins.js';
|
|
|
27
27
|
import { createFullRegistry } from './tools/index.js';
|
|
28
28
|
import { buildMcpFlattenedTools } from './tools/mcp-tools.js';
|
|
29
29
|
import { Engine } from './engine.js';
|
|
30
|
-
import { Compactor } from './compact/compactor.js';
|
|
31
|
-
import { resolveContextWindow } from './models.js';
|
|
32
30
|
import { ToolUsageStats } from './stats/tool-usage.js';
|
|
33
31
|
import { TaskManager } from './tasks/manager.js';
|
|
34
32
|
// H2.f.5 removed the old user-facing thread pipeline/dispatcher. The base
|
|
@@ -59,18 +57,6 @@ import { openAmsRegistry } from './memory/ams-registry.js';
|
|
|
59
57
|
import { join } from 'path';
|
|
60
58
|
import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, mkdirSync as mkdirSyncSafe } from 'fs';
|
|
61
59
|
|
|
62
|
-
/**
|
|
63
|
-
* Application-wide default for `Compactor`'s trigger ratio (the
|
|
64
|
-
* "fraction of model context" gate). The user-stated requirement is
|
|
65
|
-
* "model context 的 70%"; this is the canonical literal for it. Lives
|
|
66
|
-
* in session.js (not in compactor.js) because `Compactor` is also used
|
|
67
|
-
* by test fixtures that intentionally skip the ratio injector to
|
|
68
|
-
* exercise the library default (`history-compact.js#DEFAULT_TOKEN_FRACTION`).
|
|
69
|
-
* The two defaults are kept separate on purpose — see the Compactor
|
|
70
|
-
* constructor JSDoc for the boundary.
|
|
71
|
-
*/
|
|
72
|
-
const DEFAULT_COMPACT_TRIGGER_RATIO = 0.7;
|
|
73
|
-
|
|
74
60
|
/**
|
|
75
61
|
* @typedef {Object} SessionOptions
|
|
76
62
|
* @property {string} [dir] — Yeaft data directory override (default: ~/.yeaft)
|
|
@@ -88,7 +74,6 @@ const DEFAULT_COMPACT_TRIGGER_RATIO = 0.7;
|
|
|
88
74
|
/**
|
|
89
75
|
* @typedef {Object} Session
|
|
90
76
|
* @property {Engine} engine — The wired engine, ready for .query()
|
|
91
|
-
* @property {import('./compact/compactor.js').Compactor} compactor — Per-group history compactor
|
|
92
77
|
* @property {import('./llm/adapter.js').LLMAdapter} adapter — The LLM adapter
|
|
93
78
|
* @property {object} config — Resolved configuration
|
|
94
79
|
* @property {ConversationStore} conversationStore — Conversation persistence
|
|
@@ -481,41 +466,6 @@ export async function loadSession(options = {}) {
|
|
|
481
466
|
managedCliReady: managedCliInstall,
|
|
482
467
|
});
|
|
483
468
|
|
|
484
|
-
// ─── 9a-pre. Create per-group history Compactor ────────
|
|
485
|
-
// Owns the post-turn single-flight + race-guarded compactor that used
|
|
486
|
-
// to live inline in web-bridge.js. Bridge keeps history ownership and
|
|
487
|
-
// wires the WS sink (`yeaft_history_compacted`) via
|
|
488
|
-
// `compactor.setOnCompacted` from `installYeaftRuntimeBridge`.
|
|
489
|
-
const compactor = new Compactor({
|
|
490
|
-
summarize: ({ system, prompt, maxTokens } = {}) =>
|
|
491
|
-
engine.summarizeForCompact({ system, prompt, maxTokens }),
|
|
492
|
-
// Resolve the model's true context window (GPT-5 256K vs Claude 200K
|
|
493
|
-
// etc.) instead of pinning to a flat `config.maxContextTokens`.
|
|
494
|
-
// The 70% threshold then floats with the model in use — the
|
|
495
|
-
// user-stated requirement ("超过 model context 70% 这一个约束").
|
|
496
|
-
getMaxContextTokens: () =>
|
|
497
|
-
resolveContextWindow(
|
|
498
|
-
typeof config.model === 'string' && config.model
|
|
499
|
-
? config.model
|
|
500
|
-
: (config.primaryModel || ''),
|
|
501
|
-
config
|
|
502
|
-
),
|
|
503
|
-
// Trigger ratio knob. Defaults to DEFAULT_COMPACT_TRIGGER_RATIO (0.7)
|
|
504
|
-
// per the user directive; a finite number in (0, 1) wins. Anything
|
|
505
|
-
// else (NaN, ≤0, ≥1, missing) falls back to the default so a typo in
|
|
506
|
-
// config.json can't disable compact.
|
|
507
|
-
getTriggerRatio: () => {
|
|
508
|
-
const r = Number(config?.compactTriggerRatio);
|
|
509
|
-
return Number.isFinite(r) && r > 0 && r < 1 ? r : DEFAULT_COMPACT_TRIGGER_RATIO;
|
|
510
|
-
},
|
|
511
|
-
// Live-read: `config.language` is mutated in place by
|
|
512
|
-
// `engine.setLanguage()` (which broadcastLanguageChange fans out to
|
|
513
|
-
// every per-VP engine). The compactor must see the post-broadcast
|
|
514
|
-
// value, not a boot-time snapshot, so the summary prompt + the
|
|
515
|
-
// "session continued" wrapper render in the user's current locale.
|
|
516
|
-
getLanguage: () =>
|
|
517
|
-
typeof config.language === 'string' ? config.language : undefined,
|
|
518
|
-
});
|
|
519
469
|
|
|
520
470
|
// ─── 9a. Create dream scheduler ────────────
|
|
521
471
|
// The legacy R6 dream-scheduler was retired alongside recall-r6;
|
|
@@ -626,7 +576,6 @@ export async function loadSession(options = {}) {
|
|
|
626
576
|
config,
|
|
627
577
|
conversationStore,
|
|
628
578
|
dreamScheduler,
|
|
629
|
-
compactor,
|
|
630
579
|
skillManager,
|
|
631
580
|
mcpManager,
|
|
632
581
|
toolRegistry,
|
package/yeaft/stdio-protocol.js
CHANGED
package/yeaft/stop-hooks.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* Runs after each query loop completes:
|
|
5
5
|
* 1. Persist messages to conversation/messages/
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* entries-store extraction was retired in the H2-AMS rip.
|
|
7
|
+
* Conversation persistence is handled incrementally by the engine. There is
|
|
8
|
+
* no post-turn LLM conversation-summary maintenance step.
|
|
10
9
|
*
|
|
11
10
|
* Dream V2 owns all background memory maintenance (scope summaries +
|
|
12
11
|
* memory writes via dream/session-wiring.js → createV2DreamScheduler).
|
|
@@ -195,9 +194,9 @@ export async function runStopHooks(context) {
|
|
|
195
194
|
}
|
|
196
195
|
}
|
|
197
196
|
|
|
198
|
-
// 2.
|
|
199
|
-
//
|
|
200
|
-
//
|
|
197
|
+
// 2. Dream V2 owns background scope-memory maintenance via the session
|
|
198
|
+
// dream scheduler (createV2DreamScheduler). Transcript persistence above
|
|
199
|
+
// is the only end-of-turn storage work here.
|
|
201
200
|
|
|
202
201
|
return result;
|
|
203
202
|
}
|
package/yeaft/turn-utils.js
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Two concerns this module unifies:
|
|
9
9
|
*
|
|
10
|
-
* 1.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* invariant work.
|
|
10
|
+
* 1. Tool turns are grouped atomically — `[user, assistant, tool…]`
|
|
11
|
+
* triples must move as a unit so we don't split tool_use/tool_result
|
|
12
|
+
* pairs. That's storage-invariant work.
|
|
14
13
|
*
|
|
15
14
|
* 2. THIS module does turn IDENTITY — "is the next user-role message
|
|
16
15
|
* the start of a new conversational turn, or is it just another
|
|
@@ -87,7 +86,8 @@ export function countTurns(messages) {
|
|
|
87
86
|
* Find the index of the message that opens the (n)-th-from-end turn.
|
|
88
87
|
* Returns -1 if there aren't n turns in the history.
|
|
89
88
|
*
|
|
90
|
-
* Algorithm
|
|
89
|
+
* Algorithm: walk user-role boundaries from the end while counting
|
|
90
|
+
* distinct canonical turns.
|
|
91
91
|
* - Walk user-role messages from END backwards, counting DISTINCT
|
|
92
92
|
* turns by canonical text.
|
|
93
93
|
* - When `turnsFromEnd === n`, record the index. Keep walking — the
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -65,9 +65,7 @@ import { loadSessionConfig, normalizeSessionConfig, resolveSessionConfig, Sessio
|
|
|
65
65
|
import { updateSessionConfig } from './sessions/session-crud.js';
|
|
66
66
|
import { createCoordinator } from './sessions/coordinator.js';
|
|
67
67
|
import { seedDefaultSession } from './sessions/seed-default.js';
|
|
68
|
-
import {
|
|
69
|
-
trimSnapshotForBudget,
|
|
70
|
-
} from './history-compact.js';
|
|
68
|
+
import { trimHistoryCacheForRuntime, trimSnapshotForBudget } from './history-window.js';
|
|
71
69
|
import { persistYeaftAttachments, attachmentsForPersistence, persistedAttachmentPreviewPayload } from './attachments.js';
|
|
72
70
|
import { normalizeSessionMessageQuote, sessionMessageQuotePrompt } from './session-message-quote.js';
|
|
73
71
|
import { ConversationStore, parseSeqFromId, projectVisibleSessionMessages } from './conversation/persist.js';
|
|
@@ -1249,11 +1247,12 @@ let _vpUnsubscribe = null;
|
|
|
1249
1247
|
* of which group it belonged to. Disk was group-tagged correctly, but
|
|
1250
1248
|
* the in-memory tape was unified.
|
|
1251
1249
|
*
|
|
1252
|
-
* Post-refactor: each
|
|
1253
|
-
* hydrated from `conversationStore.loadRecentBySession(sessionId)` on
|
|
1254
|
-
* access. Group-A and group-B are isolated
|
|
1250
|
+
* Post-refactor: each Session context owns its own bounded `history` cache,
|
|
1251
|
+
* lazily hydrated from `conversationStore.loadRecentBySession(sessionId)` on
|
|
1252
|
+
* first access. Group-A and group-B are isolated; the durable transcript is
|
|
1253
|
+
* never replaced by the cache trim.
|
|
1255
1254
|
*
|
|
1256
|
-
* @typedef {Array<{role:'user'|'assistant'|'tool', content:string|Array, toolCalls?:Array, toolCallId?:string, isError?:boolean}>} GroupHistory
|
|
1255
|
+
* @typedef {Array<{role:'user'|'assistant'|'tool', content:string|Array, toolCalls?:Array, thinkingBlocks?:Array, toolCallId?:string, isError?:boolean}>} GroupHistory
|
|
1257
1256
|
*/
|
|
1258
1257
|
|
|
1259
1258
|
/**
|
|
@@ -1261,10 +1260,10 @@ let _vpUnsubscribe = null;
|
|
|
1261
1260
|
* @property {object|null} coord — group coordinator (lazily built by getOrCreateSessionContext)
|
|
1262
1261
|
* @property {object|null} router — message router (lazily built by getOrCreateSessionContext)
|
|
1263
1262
|
* @property {object|null} sessionHandle — opened group handle (lazily built by getOrCreateSessionContext)
|
|
1264
|
-
* @property {GroupHistory} history — per-
|
|
1263
|
+
* @property {GroupHistory} history — per-Session bounded runtime history cache
|
|
1265
1264
|
* @property {boolean} historyHydrated — true once history has been loaded
|
|
1266
1265
|
* from disk (or explicitly assigned). The flag is required because an
|
|
1267
|
-
* empty array is legitimate post-
|
|
1266
|
+
* empty array is legitimate post-clear state and
|
|
1268
1267
|
* MUST NOT trigger a re-hydrate. Without the flag, a partial entry
|
|
1269
1268
|
* would short-circuit `getOrCreateSessionHistory` on truthy `[]` and skip
|
|
1270
1269
|
* the disk load.
|
|
@@ -1366,6 +1365,24 @@ function projectPersistedToHistoryEntry(m, { includeReflections = false } = {})
|
|
|
1366
1365
|
input: tc.input,
|
|
1367
1366
|
}));
|
|
1368
1367
|
}
|
|
1368
|
+
// Anthropic thinking blocks carry provider-signed replay state. The durable
|
|
1369
|
+
// parser already validates them; preserve the complete payload + signature
|
|
1370
|
+
// in the runtime history owner so a restart does not create a tool arc with
|
|
1371
|
+
// its required thinking prefix missing. `filterSnapshotForVp` strips these
|
|
1372
|
+
// blocks from other VPs before any provider request.
|
|
1373
|
+
if (Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length > 0) {
|
|
1374
|
+
const thinkingBlocks = m.thinkingBlocks
|
|
1375
|
+
.filter(tb => tb
|
|
1376
|
+
&& typeof tb.signature === 'string'
|
|
1377
|
+
&& tb.signature
|
|
1378
|
+
&& (tb.redacted === true
|
|
1379
|
+
? typeof tb.data === 'string'
|
|
1380
|
+
: typeof tb.thinking === 'string'))
|
|
1381
|
+
.map(tb => tb.redacted === true
|
|
1382
|
+
? { redacted: true, data: tb.data, signature: tb.signature }
|
|
1383
|
+
: { thinking: tb.thinking, signature: tb.signature });
|
|
1384
|
+
if (thinkingBlocks.length > 0) entry.thinkingBlocks = thinkingBlocks;
|
|
1385
|
+
}
|
|
1369
1386
|
if (m.isError) entry.isError = true;
|
|
1370
1387
|
if (m.ts) entry.ts = m.ts;
|
|
1371
1388
|
else if (m.time) entry.ts = m.time;
|
|
@@ -1373,7 +1390,7 @@ function projectPersistedToHistoryEntry(m, { includeReflections = false } = {})
|
|
|
1373
1390
|
if (Array.isArray(m.attachments) && m.attachments.length > 0) entry.attachments = m.attachments;
|
|
1374
1391
|
if (m.quote && typeof m.quote === 'object') entry.quote = m.quote;
|
|
1375
1392
|
if (Array.isArray(m.todos)) entry.todos = m.todos;
|
|
1376
|
-
if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments && !entry.images && !entry.toolCalls && !entry.todos && !entry.askUserResults) return null;
|
|
1393
|
+
if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments && !entry.images && !entry.toolCalls && !entry.thinkingBlocks && !entry.todos && !entry.askUserResults) return null;
|
|
1377
1394
|
return entry;
|
|
1378
1395
|
}
|
|
1379
1396
|
|
|
@@ -1639,6 +1656,12 @@ function hydrateGroupHistory(sessionId) {
|
|
|
1639
1656
|
return out;
|
|
1640
1657
|
}
|
|
1641
1658
|
|
|
1659
|
+
function boundRuntimeSessionHistory(history) {
|
|
1660
|
+
return trimHistoryCacheForRuntime(history, {
|
|
1661
|
+
language: session?.config?.language,
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1642
1665
|
/**
|
|
1643
1666
|
* Get-or-create the per-group history array. Used everywhere the bridge
|
|
1644
1667
|
* needs to read/append/snapshot a group's conversation tape. Lazily
|
|
@@ -1648,9 +1671,8 @@ function hydrateGroupHistory(sessionId) {
|
|
|
1648
1671
|
* opened the group can still read history).
|
|
1649
1672
|
*
|
|
1650
1673
|
* Returns the SAME array reference across calls within the same
|
|
1651
|
-
* lifecycle, so consumers can mutate-in-place. Reassigned
|
|
1652
|
-
*
|
|
1653
|
-
* events, and session reset.
|
|
1674
|
+
* lifecycle, so consumers can mutate-in-place. Reassigned by session
|
|
1675
|
+
* hydration/reset paths when the canonical array must be replaced.
|
|
1654
1676
|
*
|
|
1655
1677
|
* @param {string} sessionId
|
|
1656
1678
|
* @returns {GroupHistory}
|
|
@@ -1659,7 +1681,7 @@ function getOrCreateSessionHistory(sessionId) {
|
|
|
1659
1681
|
if (!sessionId) return [];
|
|
1660
1682
|
let entry = sessionContexts.get(sessionId);
|
|
1661
1683
|
// Use `historyHydrated` rather than truthiness on `history` itself —
|
|
1662
|
-
// an empty array (post-
|
|
1684
|
+
// an empty array (post-clear or a partial entry
|
|
1663
1685
|
// seeded by an early `getOrCreateSessionContext` call before data was
|
|
1664
1686
|
// loaded) is legitimate state that does NOT mean "needs hydration"...
|
|
1665
1687
|
// unless we never loaded from disk in the first place. The flag
|
|
@@ -1669,19 +1691,19 @@ function getOrCreateSessionHistory(sessionId) {
|
|
|
1669
1691
|
entry = makeGroupContextStub();
|
|
1670
1692
|
sessionContexts.set(sessionId, entry);
|
|
1671
1693
|
}
|
|
1672
|
-
entry.history = hydrateGroupHistory(sessionId);
|
|
1694
|
+
entry.history = boundRuntimeSessionHistory(hydrateGroupHistory(sessionId));
|
|
1673
1695
|
entry.historyHydrated = true;
|
|
1674
1696
|
return entry.history;
|
|
1675
1697
|
}
|
|
1676
1698
|
|
|
1677
1699
|
/**
|
|
1678
|
-
* Reassign a group's history reference. Used by
|
|
1679
|
-
*
|
|
1700
|
+
* Reassign a group's history reference. Used by session reset and clear
|
|
1701
|
+
* paths that need to swap the array (not just mutate it). Returns
|
|
1680
1702
|
* the new reference. Idempotent if the entry doesn't exist (creates one).
|
|
1681
1703
|
*
|
|
1682
1704
|
* Sets `historyHydrated = true` because an explicit assignment is itself
|
|
1683
|
-
* a hydration — even setting `[]` after
|
|
1684
|
-
*
|
|
1705
|
+
* a hydration — even setting `[]` after clear means "this is the canonical
|
|
1706
|
+
* state right now, don't re-load from disk".
|
|
1685
1707
|
*
|
|
1686
1708
|
* @param {string} sessionId
|
|
1687
1709
|
* @param {GroupHistory} next
|
|
@@ -1693,7 +1715,7 @@ function setGroupHistory(sessionId, next) {
|
|
|
1693
1715
|
entry = makeGroupContextStub();
|
|
1694
1716
|
sessionContexts.set(sessionId, entry);
|
|
1695
1717
|
}
|
|
1696
|
-
entry.history = next;
|
|
1718
|
+
entry.history = boundRuntimeSessionHistory(Array.isArray(next) ? next : []);
|
|
1697
1719
|
entry.historyHydrated = true;
|
|
1698
1720
|
}
|
|
1699
1721
|
|
|
@@ -1876,10 +1898,8 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1876
1898
|
// are silently dropped.
|
|
1877
1899
|
toolStats: session.toolStats || null,
|
|
1878
1900
|
taskManager: session.taskManager || null,
|
|
1879
|
-
// Per-VP fan-out: bind the engine to its (sessionId, vpId)
|
|
1880
|
-
//
|
|
1881
|
-
// compact.md (which every VP would otherwise share, producing
|
|
1882
|
-
// identical, ever-growing summaries across groups).
|
|
1901
|
+
// Per-VP fan-out: bind the engine to its (sessionId, vpId) for scoped
|
|
1902
|
+
// history, memory, and tool ownership.
|
|
1883
1903
|
sessionId,
|
|
1884
1904
|
vpId,
|
|
1885
1905
|
});
|
|
@@ -1941,7 +1961,7 @@ function getOrCreateSessionContext(sessionId, sessionHandle) {
|
|
|
1941
1961
|
// having gone through `getOrCreateSessionHistory` first: a partial entry
|
|
1942
1962
|
// could exist with `historyHydrated:false`, so do the load now.
|
|
1943
1963
|
if (!entry.historyHydrated) {
|
|
1944
|
-
entry.history = hydrateGroupHistory(sessionId);
|
|
1964
|
+
entry.history = boundRuntimeSessionHistory(hydrateGroupHistory(sessionId));
|
|
1945
1965
|
entry.historyHydrated = true;
|
|
1946
1966
|
}
|
|
1947
1967
|
return entry;
|
|
@@ -2428,12 +2448,6 @@ export async function __testResetVpState() {
|
|
|
2428
2448
|
}
|
|
2429
2449
|
yeaftConversationId = null;
|
|
2430
2450
|
lastYeaftSlashCommandSnapshot = null;
|
|
2431
|
-
// Per-group compact in-flight + pending state lives on the session's
|
|
2432
|
-
// Compactor. Clear it so a follow-on test doesn't see ghost in-flight
|
|
2433
|
-
// promises from a prior run.
|
|
2434
|
-
if (session?.compactor && typeof session.compactor.__testReset === 'function') {
|
|
2435
|
-
session.compactor.__testReset();
|
|
2436
|
-
}
|
|
2437
2451
|
}
|
|
2438
2452
|
|
|
2439
2453
|
|
|
@@ -3920,28 +3934,6 @@ export function installYeaftRuntimeBridge(s) {
|
|
|
3920
3934
|
}
|
|
3921
3935
|
};
|
|
3922
3936
|
|
|
3923
|
-
// Wire the post-compact WS sink. Compactor is constructed in
|
|
3924
|
-
// session.js with a no-op sink; bridge owns `sendSessionEvent` /
|
|
3925
|
-
// `yeaftConversationId`, so the sink is wired here once a session is
|
|
3926
|
-
// present. Per-group `yeaft_history_compacted` events fire after a
|
|
3927
|
-
// successful summarize+swap.
|
|
3928
|
-
if (s.compactor && typeof s.compactor.setOnCompacted === 'function') {
|
|
3929
|
-
s.compactor.setOnCompacted((sessionId, result) => {
|
|
3930
|
-
try {
|
|
3931
|
-
sendSessionEvent({
|
|
3932
|
-
type: 'yeaft_history_compacted',
|
|
3933
|
-
reason: result?.reason ?? null,
|
|
3934
|
-
beforeTurns: result?.beforeTurns,
|
|
3935
|
-
afterTurns: result?.afterTurns,
|
|
3936
|
-
beforeTokens: result?.beforeTokens,
|
|
3937
|
-
afterTokens: result?.afterTokens,
|
|
3938
|
-
archivedCount: result?.archivedCount,
|
|
3939
|
-
ts: Date.now(),
|
|
3940
|
-
}, { sessionId });
|
|
3941
|
-
} catch { /* WS pipeline failure must not crash compact */ }
|
|
3942
|
-
});
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
3937
|
ctx.yeaftRuntimeSettings = {
|
|
3946
3938
|
// No multi-thread settings to surface anymore. Stub for back-compat
|
|
3947
3939
|
// with message-router's update_yeaft_settings branch — assignments are
|
|
@@ -4365,17 +4357,6 @@ function handleEngineEvent(event, hctx) {
|
|
|
4365
4357
|
}, envelope);
|
|
4366
4358
|
break;
|
|
4367
4359
|
|
|
4368
|
-
case 'consolidate':
|
|
4369
|
-
// Engine compressed the context — clear THIS group's accumulated
|
|
4370
|
-
// history. Other groups' histories stay intact.
|
|
4371
|
-
if (hctx.sessionId) setGroupHistory(hctx.sessionId, []);
|
|
4372
|
-
sendSessionEvent({
|
|
4373
|
-
type: 'consolidate',
|
|
4374
|
-
archivedCount: event.archivedCount,
|
|
4375
|
-
extractedCount: event.extractedCount,
|
|
4376
|
-
}, envelope);
|
|
4377
|
-
break;
|
|
4378
|
-
|
|
4379
4360
|
case 'fallback':
|
|
4380
4361
|
sendSessionEvent({
|
|
4381
4362
|
type: 'fallback',
|
|
@@ -4703,20 +4684,6 @@ async function runYeaftSessionSend(msg) {
|
|
|
4703
4684
|
},
|
|
4704
4685
|
});
|
|
4705
4686
|
|
|
4706
|
-
// Entry gate: if a compact is in flight from the previous turn IN
|
|
4707
|
-
// THIS GROUP, wait for it to finish before reading the group's
|
|
4708
|
-
// history. Compact runs at turn END (post-fanout) so it does not
|
|
4709
|
-
// block the user's current message latency, but a fast double-send
|
|
4710
|
-
// from the user must not race with the swap. Other groups' compacts
|
|
4711
|
-
// never block this gate. Compactor is created in session.js — until
|
|
4712
|
-
// a session has loaded (or in test paths that never call
|
|
4713
|
-
// `ensureSessionLoaded`) it may be unavailable; skip gracefully.
|
|
4714
|
-
if (session?.compactor) {
|
|
4715
|
-
const compactWaitStart = perfNowMs();
|
|
4716
|
-
await session.compactor.awaitInFlight(sessionId);
|
|
4717
|
-
traceDuration('session_send.await_compactor', compactWaitStart);
|
|
4718
|
-
}
|
|
4719
|
-
|
|
4720
4687
|
// yeaftDir is a hard prerequisite for both session boot and group seeding;
|
|
4721
4688
|
// validate BEFORE booting so a misconfigured agent doesn't leave a zombie
|
|
4722
4689
|
// session lying around.
|
|
@@ -4970,34 +4937,6 @@ async function runYeaftSessionSend(msg) {
|
|
|
4970
4937
|
ok: true,
|
|
4971
4938
|
});
|
|
4972
4939
|
|
|
4973
|
-
// Post-turn compaction. Fire-and-forget — does NOT block the response
|
|
4974
|
-
// path. The Compactor's own precheck (`shouldCompactHistory`) decides
|
|
4975
|
-
// whether to engage the LLM, using the trigger ratio wired in
|
|
4976
|
-
// `session.js` (default 70% of model context, knob:
|
|
4977
|
-
// `config.compactTriggerRatio`). The single-flight + anti-starvation
|
|
4978
|
-
// logic inside Compactor handles concurrent turns; the entry-gate
|
|
4979
|
-
// `awaitInFlight` at the top of `handleYeaftSessionSend` (~:2291)
|
|
4980
|
-
// ensures a follow-up turn never reads a half-mutated history.
|
|
4981
|
-
//
|
|
4982
|
-
// Why a per-call historyHandle: Compactor must NEVER close over a
|
|
4983
|
-
// frozen snapshot — the array reference can be swapped by
|
|
4984
|
-
// `consolidate`, session reset, or `route_forward` bursts. The handle
|
|
4985
|
-
// re-resolves on each `get` via the same sessionId-keyed helpers used
|
|
4986
|
-
// everywhere else in the bridge.
|
|
4987
|
-
//
|
|
4988
|
-
// Naming asymmetry note: `getOrCreateSessionHistory` and
|
|
4989
|
-
// `setGroupHistory` are intentionally NOT renamed to match. Both are
|
|
4990
|
-
// session-keyed today (the `set` helper's name is a historical alias
|
|
4991
|
-
// from the pre-VP-thread era), but per CLAUDE.md's "不要为了改名而批量
|
|
4992
|
-
// 重命名" guardrail we leave the wire-compat names alone and rely on
|
|
4993
|
-
// co-location to make the symmetry obvious. The source-pinning test
|
|
4994
|
-
// `web-bridge-post-turn-compact-wiring.test.js` matches both names.
|
|
4995
|
-
if (session?.compactor && sessionId) {
|
|
4996
|
-
session.compactor.scheduleAfterTurn(sessionId, {
|
|
4997
|
-
get: () => getOrCreateSessionHistory(sessionId),
|
|
4998
|
-
set: (next) => setGroupHistory(sessionId, next),
|
|
4999
|
-
});
|
|
5000
|
-
}
|
|
5001
4940
|
}
|
|
5002
4941
|
|
|
5003
4942
|
/**
|
|
@@ -5625,10 +5564,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5625
5564
|
markEngineTerminal,
|
|
5626
5565
|
markTurnEnd,
|
|
5627
5566
|
};
|
|
5628
|
-
// Always trim the snapshot before passing to engine.query. This is
|
|
5629
|
-
//
|
|
5630
|
-
//
|
|
5631
|
-
// array). See `trimSnapshotForBudget` doc-block for policy.
|
|
5567
|
+
// Always trim the snapshot before passing to engine.query. This is a
|
|
5568
|
+
// deterministic provider-request window; it never calls an LLM or
|
|
5569
|
+
// changes the persisted transcript. See `trimSnapshotForBudget`.
|
|
5632
5570
|
const trimStart = perfNowMs();
|
|
5633
5571
|
const trimmedMessages = trimSnapshotForBudget(baseSnapshot, {
|
|
5634
5572
|
messageTokenBudget: session?.config?.messageTokenBudget,
|
|
@@ -5930,11 +5868,12 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5930
5868
|
* raw user prompt(s) + the per-VP assistant text + tool results. Related
|
|
5931
5869
|
* appends consumed by Engine loop-boundary hooks are passed in the prompt list
|
|
5932
5870
|
* exactly once, with the same threadId as the running thread. The
|
|
5933
|
-
* engine's own `conversationMessages` (with T1/T2
|
|
5871
|
+
* engine's own `conversationMessages` (with T1/T2 tool-arc folding applied)
|
|
5934
5872
|
* is persisted to disk via stop-hooks, so the next turn's history is
|
|
5935
5873
|
* read from disk via `loadRecentBySession` on next session boot. Within
|
|
5936
|
-
* a session, this in-memory tape
|
|
5937
|
-
*
|
|
5874
|
+
* a session, this in-memory tape is a bounded deterministic cache; the
|
|
5875
|
+
* complete transcript remains in ConversationStore and each provider request
|
|
5876
|
+
* applies its own pair-safe history window.
|
|
5938
5877
|
*/
|
|
5939
5878
|
function buildTurnHistoryEntries(threadId, vpId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum, opts = {}) {
|
|
5940
5879
|
const entries = [];
|
|
@@ -6019,6 +5958,11 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
|
|
|
6019
5958
|
} else {
|
|
6020
5959
|
history.push(...nextEntries);
|
|
6021
5960
|
}
|
|
5961
|
+
// The runtime tape is a disposable bounded cache. ConversationStore already
|
|
5962
|
+
// owns the complete transcript, so replacing this reference cannot lose
|
|
5963
|
+
// durable history. Keep the cache bounded after every partial/final append,
|
|
5964
|
+
// including large tool outputs from a long-running turn.
|
|
5965
|
+
setGroupHistory(sessionId, history);
|
|
6022
5966
|
}
|
|
6023
5967
|
|
|
6024
5968
|
/**
|
|
@@ -6135,14 +6079,6 @@ function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = '
|
|
|
6135
6079
|
*/
|
|
6136
6080
|
const _persistedUserMsgIds = new Set();
|
|
6137
6081
|
|
|
6138
|
-
// Per-group post-turn compaction lives on `session.compactor`
|
|
6139
|
-
// (`agent/yeaft/compact/compactor.js`), constructed in `session.js`.
|
|
6140
|
-
// Bridge passes a per-call `historyHandle = { get, set }` and wires the
|
|
6141
|
-
// `yeaft_history_compacted` WS sink via `compactor.setOnCompacted` from
|
|
6142
|
-
// `installYeaftRuntimeBridge`. The bridge keeps history ownership; the
|
|
6143
|
-
// Compactor owns single-flight, anti-starvation, race-guard, and the
|
|
6144
|
-
// LLM summarize call.
|
|
6145
|
-
|
|
6146
6082
|
/**
|
|
6147
6083
|
* Abort every in-flight VP turn and clear all queued envelopes across
|
|
6148
6084
|
* every group/thread runtime. Shared by `handleYeaftAbortThread` when no
|
|
@@ -7102,10 +7038,6 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
7102
7038
|
? loadVisibleGroupHistoryPage(session.conversationStore, sessionId, limit)
|
|
7103
7039
|
: { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
|
|
7104
7040
|
traceDuration('history.store_load_recent', loadStart, { detail: { rawCount: visiblePage.messages?.length || 0, limit } });
|
|
7105
|
-
// Legacy compact.md is a non-group fallback only. For group replay, reading
|
|
7106
|
-
// it makes every group show "has compact" once any legacy/non-scoped compact
|
|
7107
|
-
// exists, even when this group has no scoped summary.
|
|
7108
|
-
const compactSummary = sessionId ? '' : session.conversationStore.readCompactSummary();
|
|
7109
7041
|
const replayEntries = sessionId
|
|
7110
7042
|
? visiblePage.messages
|
|
7111
7043
|
: visiblePage.messages
|
|
@@ -7152,20 +7084,10 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
7152
7084
|
oldestSeq = visiblePage.oldestSeq;
|
|
7153
7085
|
}
|
|
7154
7086
|
|
|
7155
|
-
// hasCompactSummary used to read a single session-global file, so it was
|
|
7156
|
-
// always true once ANY group/VP in the session had compacted. For group
|
|
7157
|
-
// replay, only scoped per-(group, vp) summaries count; legacy compact.md is
|
|
7158
|
-
// reserved for non-group / pre-scoped 1:1 callers.
|
|
7159
|
-
let hasCompactSummaryFlag = !!compactSummary;
|
|
7160
|
-
if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForSession === 'function') {
|
|
7161
|
-
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForSession(sessionId);
|
|
7162
|
-
}
|
|
7163
|
-
|
|
7164
7087
|
sendSessionEvent({
|
|
7165
7088
|
type: 'history_loaded',
|
|
7166
7089
|
mode: 'recent',
|
|
7167
7090
|
count: replayEntries.length,
|
|
7168
|
-
hasCompactSummary: hasCompactSummaryFlag,
|
|
7169
7091
|
sessionId,
|
|
7170
7092
|
requestId,
|
|
7171
7093
|
hasMore,
|