@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/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,
@@ -421,7 +421,6 @@ export function createStreamProjector({
421
421
  case 'llm_retry':
422
422
  case 'memory_used':
423
423
  case 'recall':
424
- case 'consolidate':
425
424
  case 'reflection':
426
425
  case 'tool_result_update':
427
426
  case 'user_append':
@@ -4,9 +4,8 @@
4
4
  * Runs after each query loop completes:
5
5
  * 1. Persist messages to conversation/messages/
6
6
  *
7
- * Consolidation (compact orchestrator) is driven by the engine itself
8
- * via `#maybeConsolidate`; the legacy LLM-driven `consolidate()` plus
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. Consolidation is owned by the engine (#maybeConsolidate → compact
199
- // orchestrator). Dream V2 owns background scope-memory maintenance
200
- // via the session dream scheduler (createV2DreamScheduler).
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
  }
@@ -7,10 +7,9 @@
7
7
  *
8
8
  * Two concerns this module unifies:
9
9
  *
10
- * 1. `compact/turn-group.js` already does atomicity grouping —
11
- * `[user, assistant, tool…]` triples that must move as a unit
12
- * so we don't split tool_use/tool_result pairs. That's storage-
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 (mirrors `history-compact.js#findCutIndex`):
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
@@ -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 GroupContext owns its own `history`, lazily
1253
- * hydrated from `conversationStore.loadRecentBySession(sessionId)` on first
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-group conversation tape
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-consolidate / post-clear state and
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 only by
1652
- * compact (race guard checks reference equality), `consolidate`
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-consolidate, post-clear, or a partial entry
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 compact + consolidate +
1679
- * clear paths that need to swap the array (not just mutate it). Returns
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 `consolidate` means "this is the
1684
- * canonical state right now, don't re-load from disk".
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) so post-turn
1880
- // compact reads/writes a scoped summary instead of the legacy global
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
- // the second-line defense (history-compact only fires above 30K
5630
- // tokens — small chats with many turns still bloat the messages
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 collapse applied)
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 carries the un-collapsed form — which
5937
- * is fine because each VP turn's `engine.query` re-collapses on the fly.
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,