@yeaft/webchat-agent 1.0.510 → 1.0.512

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.510",
3
+ "version": "1.0.512",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/cli.js CHANGED
@@ -380,7 +380,7 @@ async function runREPL(config, args) {
380
380
 
381
381
  // Load persisted conversation as initial messages. `loadRecent` is now
382
382
  // turn-based (one user round-trip = one turn; multi-VP fan-out collapses
383
- // into one turn). 20 turns is the bootstrap window. Provider requests
383
+ // into one turn). 10 turns is the bootstrap window. Provider requests
384
384
  // apply deterministic history-window trimming; persisted history stays
385
385
  // authoritative and no LLM conversation summary is generated.
386
386
  let conversationMessages = conversationStore.loadRecent().map(m => ({
package/yeaft/config.js CHANGED
@@ -53,7 +53,7 @@ const DEFAULTS = {
53
53
  // ConversationStore.loadRecentBySession / loadSessionHistoryForVp
54
54
  // bring back after boot or reconnect. Older transcript remains available
55
55
  // through history pagination/search. Range: 1–500.
56
- yeaftRecentTurnsLimit: 20,
56
+ yeaftRecentTurnsLimit: 10,
57
57
  // Same-Session related Q&A turns, selected by deterministic full-text rules.
58
58
  // Range: 0–5; 0 disables related recall without changing recent history.
59
59
  yeaftRelatedTurnsLimit: 5,
@@ -42,7 +42,7 @@ import { markConversationDirty } from './history-index-state.js';
42
42
  * Chat-Completions adapter. Turn-based slicing always cuts at a user-
43
43
  * message boundary, which is pair-safe by construction.
44
44
  *
45
- * 20 turns is the bootstrap window the user signed off on (2026-05-01).
45
+ * 10 turns is the default bootstrap window.
46
46
  * It is the cold-start replay window after a fresh boot or reconnect. Runtime
47
47
  * provider requests apply a separate deterministic history-window transform;
48
48
  * no LLM summary is required for recovery.
@@ -57,7 +57,7 @@ import { markConversationDirty } from './history-index-state.js';
57
57
  * at module load (`const cap = DEFAULT_RECENT_TURNS`) would not see
58
58
  * runtime overrides. The reader function makes that always-correct.
59
59
  */
60
- let DEFAULT_RECENT_TURNS = 20;
60
+ let DEFAULT_RECENT_TURNS = 10;
61
61
 
62
62
  // Circuit breaker for newest-to-oldest session scans. This bounds event-loop
63
63
  // starvation when the newest transcript tail is dense with hidden/internal or
package/yeaft/engine.js CHANGED
@@ -2132,7 +2132,7 @@ export class Engine {
2132
2132
  // const useDreamMemory = scenario === 'work-item' || !!vpPersona?.subAgent
2133
2133
  // || (!runtimeSessionId && !internalTrigger);
2134
2134
  const useDreamMemory = false;
2135
- const recentTurnCap = Math.max(20, this.#config.yeaft?.recentTurnsLimit ?? 20);
2135
+ const recentTurnCap = Math.max(10, this.#config.yeaft?.recentTurnsLimit ?? 10);
2136
2136
  const relatedTurnCap = Math.min(5, this.#config.yeaft?.relatedTurnsLimit ?? 5);
2137
2137
  let relatedHistoryTurns = [];
2138
2138
  let historyRecallMeta = { source: 'messages', status: 'disabled' };
@@ -2915,7 +2915,7 @@ export class Engine {
2915
2915
  const buckets = useMessageHistory ? buildHistoryBuckets(conversationMessages, {
2916
2916
  prompt,
2917
2917
  relatedTurns: relatedHistoryTurns,
2918
- recentTurnCap: Math.max(20, requestConfig.yeaft?.recentTurnsLimit ?? 20),
2918
+ recentTurnCap: Math.max(10, requestConfig.yeaft?.recentTurnsLimit ?? 10),
2919
2919
  relatedTurnCap: Math.min(5, requestConfig.yeaft?.relatedTurnsLimit ?? 5),
2920
2920
  messageTokenBudget: historyBudget,
2921
2921
  currentTurnStartIndex: turnStartIdx,
@@ -27,6 +27,8 @@ export const DEFAULT_RUNTIME_CACHE_TURN_CAP = 25;
27
27
  export const DEFAULT_RUNTIME_CACHE_TOKEN_BUDGET = 32768;
28
28
  export const DEFAULT_RUNTIME_CACHE_MESSAGE_CAP = 256;
29
29
 
30
+ const DEFAULT_PROVIDER_RECENT_TURNS = 10;
31
+ const MINIMUM_RECENT_PROVIDER_TURNS = 3;
30
32
  const IMAGE_PART_TOKEN_COST = 1024;
31
33
  const DOCUMENT_PART_TOKEN_COST = 2048;
32
34
  const CONTENT_PART_FRAME_TOKENS = 2;
@@ -893,16 +895,56 @@ function describeBucket(turns, messages = turns.flatMap(turn => turn.text)) {
893
895
  };
894
896
  }
895
897
 
898
+ /**
899
+ * Emergency projection for the three-turn floor. It reuses the existing
900
+ * deterministic message fitter against a disposable provider copy, while
901
+ * sharing rows/tokens across the newest three human boundaries. The durable
902
+ * transcript is never rewritten.
903
+ */
904
+ function compressRecentTurns(turns, tokenBudget, messageCap) {
905
+ const fitted = [];
906
+ let tokens = tokenBudget;
907
+ let rows = messageCap;
908
+ for (let index = 0; index < turns.length; index += 1) {
909
+ const turn = turns[index];
910
+ const remaining = turns.length - index;
911
+ let turnTokens = Math.floor(tokens / remaining);
912
+ const minimumRowTokens = Array.isArray(turn.text[0]?.content) ? 5 : 3;
913
+ const turnRows = Math.min(Math.floor(rows / remaining), Math.floor(turnTokens / minimumRowTokens));
914
+ if (turnTokens < minimumRowTokens || turnRows < 1) continue;
915
+ const selected = turn.text.length <= turnRows ? turn.text
916
+ : [turn.text[0], ...(turnRows > 1 ? turn.text.slice(-(turnRows - 1)) : [])];
917
+ const text = [];
918
+ for (let row = 0; row < selected.length; row += 1) {
919
+ const allowance = Math.floor(turnTokens / (selected.length - row));
920
+ const message = shrinkMessageToBudget(selected[row], allowance);
921
+ const cost = estimateMessageTokens(message);
922
+ if (cost <= allowance && hasProviderContent(message.content)) {
923
+ text.push(message);
924
+ turnTokens -= cost;
925
+ } else if (row === 0) break;
926
+ }
927
+ if (!text.some(bucketUserBoundary)) continue;
928
+ const cost = estimateMessagesTokens(text);
929
+ fitted.push({ ...turn, text, tokens: cost });
930
+ tokens -= cost;
931
+ rows -= text.length;
932
+ }
933
+ return fitted;
934
+ }
935
+
896
936
  /**
897
937
  * Recompute provider history from untrimmed candidates; never mutate/cache the
898
938
  * result in the transcript. Past human turn boundaries are retained when the
899
939
  * configured recent window fits; tools are optional enrichment, newest first.
900
940
  *
901
941
  * The active turn is outside both buckets and outside the history budget. Its
902
- * opening user row is protected by the later whole-request fitter. Recent text
903
- * stays complete when it fits, but a configured turn count is never a hard
904
- * request-success floor. Related recall only uses remaining history budget and
905
- * stays optional and complete. External recall must have
942
+ * opening user row is protected by the later whole-request fitter. Prefer ten
943
+ * complete recent turns, reducing the oldest end down to three. If that floor
944
+ * cannot fit, omit recall and compress only those three turns; historical tool
945
+ * replay then narrows from the normal newest three turns to the newest one.
946
+ * Related recall otherwise uses remaining history budget and stays optional and
947
+ * complete. External recall must have
906
948
  * comparable userSeq/source identities to establish
907
949
  * that it predates recent/current history; unknown chronology fails closed.
908
950
  *
@@ -917,7 +959,7 @@ export function buildHistoryBuckets(snapshot, options = {}) {
917
959
  const source = Array.isArray(snapshot) ? snapshot : [];
918
960
  const tokenBudget = bucketCap(options.messageTokenBudget, DEFAULT_MESSAGE_TOKEN_BUDGET);
919
961
  const messageCap = bucketCap(options.maxMessageCount, DEFAULT_RUNTIME_CACHE_MESSAGE_CAP);
920
- const recentCap = bucketCap(options.recentTurnCap, 20);
962
+ const recentCap = bucketCap(options.recentTurnCap, DEFAULT_PROVIDER_RECENT_TURNS);
921
963
  const relatedCap = bucketCap(options.relatedTurnCap, 5, 5);
922
964
  const keepToolTurns = bucketCap(options.keepToolTurns, DEFAULT_KEEP_TOOL_TURNS);
923
965
  const allTurns = splitBucketTurns(source);
@@ -997,11 +1039,12 @@ export function buildHistoryBuckets(snapshot, options = {}) {
997
1039
  // past-turn boundary is still a safe fence; never guess from text/time.
998
1040
  || (turn.index == null && currentIdentity.userSeq == null && past.length > 0
999
1041
  && bucketBefore(turn, past.at(-1)))));
1000
- // Recent text has first claim on history budget. Drop whole oldest turns,
1001
- // never reserve space for recall or truncate text to manufacture turn counts.
1002
- const recent = [];
1042
+ // Recent text has first claim on history budget. Drop whole oldest turns down
1043
+ // to the three-turn floor before projecting that floor more aggressively.
1044
+ let recent = [];
1003
1045
  let recentTokens = 0;
1004
1046
  let recentRows = 0;
1047
+ let compressedRecentFloor = false;
1005
1048
  const recentCandidates = recentCap > 0 ? past.slice(-recentCap) : [];
1006
1049
  for (let index = recentCandidates.length - 1; index >= 0; index -= 1) {
1007
1050
  const turn = recentCandidates[index];
@@ -1011,10 +1054,15 @@ export function buildHistoryBuckets(snapshot, options = {}) {
1011
1054
  recentTokens += turn.tokens;
1012
1055
  recentRows += turn.text.length;
1013
1056
  }
1057
+ const recentFloor = Math.min(MINIMUM_RECENT_PROVIDER_TURNS, recentCandidates.length);
1058
+ if (recent.length < recentFloor) {
1059
+ compressedRecentFloor = true;
1060
+ recent = compressRecentTurns(recentCandidates.slice(-recentFloor), availableTokens, availableRows);
1061
+ }
1014
1062
  let remainingTokens = availableTokens - recent.reduce((total, turn) => total + turn.tokens, 0);
1015
1063
  let remainingRows = availableRows - recent.reduce((total, turn) => total + turn.text.length, 0);
1016
1064
  const related = [];
1017
- const ranked = eligible.slice().sort((a, b) => b.score - a.score
1065
+ const ranked = compressedRecentFloor ? [] : eligible.slice().sort((a, b) => b.score - a.score
1018
1066
  || (a.userSeq ?? a.index ?? 0) - (b.userSeq ?? b.index ?? 0));
1019
1067
  for (const turn of ranked) {
1020
1068
  if (related.length >= relatedCap) break;
@@ -1035,8 +1083,9 @@ export function buildHistoryBuckets(snapshot, options = {}) {
1035
1083
  // Enrich only after both complete-text buckets and the active turn are paid.
1036
1084
  // Tool protocol is useful only for immediate continuity; unlike visible text,
1037
1085
  // it never reaches farther back than the configured recent tool window.
1038
- const recentToolSource = keepToolTurns > 0
1039
- ? recent.slice(-keepToolTurns).flatMap(turn => turn.messages).filter(isVisibleConversationRow)
1086
+ const effectiveKeepToolTurns = compressedRecentFloor ? Math.min(1, keepToolTurns) : keepToolTurns;
1087
+ const recentToolSource = effectiveKeepToolTurns > 0
1088
+ ? recent.slice(-effectiveKeepToolTurns).flatMap(turn => turn.messages).filter(isVisibleConversationRow)
1040
1089
  : [];
1041
1090
  const recentMessages = withoutHistorySourceIndexes(addOptionalRecentToolPairs(
1042
1091
  recentBaseline,
@@ -1065,7 +1114,9 @@ export function buildHistoryBuckets(snapshot, options = {}) {
1065
1114
  budget: {
1066
1115
  messageTokenBudget: tokenBudget, maxMessageCount: messageCap,
1067
1116
  recentTurnCap: recentCap, relatedTurnCap: relatedCap,
1068
- minimumRecentTurns: 0,
1117
+ minimumRecentTurns: recentFloor,
1118
+ compressedRecentFloor,
1119
+ effectiveKeepToolTurns,
1069
1120
  relatedReservedTokens: 0, availableHistoryTokens: availableTokens,
1070
1121
  usedTokens: estimateMessagesTokens([...relatedMessages, ...recentMessages]),
1071
1122
  usedMessages: relatedMessages.length + recentMessages.length,
package/yeaft/session.js CHANGED
@@ -180,7 +180,7 @@ export async function loadSession(options = {}) {
180
180
  if (typeof dreamEnabled === 'boolean') config.dream.enabled = dreamEnabled;
181
181
 
182
182
  // Propagate the (clamped) cold-start replay window to the conversation
183
- // store. The default is 20 turns; a user wanting more recall after a
183
+ // store. The default is 10 turns; a user wanting more recall after a
184
184
  // fresh boot sets `yeaft.recentTurnsLimit` in ~/.yeaft/config.json.
185
185
  // Called once per session boot — subsequent boots overwrite the
186
186
  // module-level default safely (single-process model).
@@ -7174,7 +7174,7 @@ export async function handleYeaftLoadHistory(msg) {
7174
7174
  // `lim` is now expressed in TURNS, not raw messages. `loadRecent` and
7175
7175
  // `loadRecentBySession` use turn-based slicing so the cut never lands
7176
7176
  // mid-tool-arc. Pass `undefined` to use the persistence-layer default
7177
- // (DEFAULT_RECENT_TURNS = 20 turns).
7177
+ // (DEFAULT_RECENT_TURNS = 10 turns).
7178
7178
  const pickRecent = (store, lim) =>
7179
7179
  sessionId ? store.loadRecentBySession(sessionId, lim) : store.loadRecent(lim);
7180
7180
  let historyAlreadyReplayed = false;