@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/yeaft/session.js CHANGED
@@ -17,7 +17,7 @@ import { initYeaftDir, DEFAULT_YEAFT_DIR, isWritable } from './init.js';
17
17
  import { loadConfig, loadMCPConfig } from './config.js';
18
18
  import { createTrace } from './debug-trace.js';
19
19
  import { createLLMAdapter } from './llm/adapter.js';
20
- import { ConversationStore } from './conversation/persist.js';
20
+ import { ConversationStore, setDefaultRecentTurnsLimit } from './conversation/persist.js';
21
21
  import { SkillManager, createSkillManager } from './skills.js';
22
22
  import { MCPManager } from './mcp.js';
23
23
  import { createFullRegistry } from './tools/index.js';
@@ -156,6 +156,15 @@ export async function loadSession(options = {}) {
156
156
  // unref it (CLI / tests). Non-persisted — set per-session by caller.
157
157
  if (serverMode) config.serverMode = true;
158
158
 
159
+ // Propagate the (clamped) cold-start replay window to the conversation
160
+ // store. The default is 20 turns; a user wanting more recall after a
161
+ // fresh boot sets `yeaft.recentTurnsLimit` in ~/.yeaft/config.json.
162
+ // Called once per session boot — subsequent boots overwrite the
163
+ // module-level default safely (single-process model).
164
+ if (config?.yeaft?.recentTurnsLimit) {
165
+ setDefaultRecentTurnsLimit(config.yeaft.recentTurnsLimit);
166
+ }
167
+
159
168
  // ─── 2.1 Migration state check (task-334i) ────────────
160
169
  // If the group-chat feature flag is on but migration has not
161
170
  // completed, warn the user. Do NOT auto-run migration: that is
@@ -0,0 +1,88 @@
1
+ /**
2
+ * snapshot-filter.js — VP-isolated view of the in-memory group history.
3
+ *
4
+ * Mirrors the disk-replay rules in
5
+ * `agent/yeaft/conversation/persist.js#loadSessionHistoryForVp`:
6
+ *
7
+ * - User rows (no `speakerVpId`): KEEP — every VP sees the prompt.
8
+ * - This VP's own assistant rows + their paired tool rows: KEEP.
9
+ * - OTHER VPs' assistant rows: KEEP TEXT ONLY (strip `toolCalls` AND
10
+ * `thinkingBlocks` — thinking is VP-private per Anthropic's signed-
11
+ * block contract; tool_use ids belong to that VP's own tool arc).
12
+ * - OTHER VPs' tool result rows (role:'tool'): DROP — they pair with
13
+ * stripped tool_use ids and would orphan on the LLM request.
14
+ * - Rows with `_reflection` / `internal` / `systemOnly`: DROP — engine-
15
+ * private; never enter another VP's context.
16
+ *
17
+ * Why this exists separately from the disk replay path:
18
+ * `web-bridge.js` builds an in-memory `baseSnapshot` for every running
19
+ * VP turn. Before this filter the snapshot was only `threadId`-scoped
20
+ * and leaked other VPs' tool calls + thinking blocks into the next
21
+ * turn's messages, producing orphan `tool_use` ids and Anthropic 422s.
22
+ * The disk path uses `ConversationStore.loadSessionHistoryForVp` —
23
+ * the same rules implemented here, but reading from `messages/*.md`
24
+ * instead of the in-memory tape. We intentionally duplicate the rule
25
+ * set rather than calling into persist.js so both call sites stay
26
+ * independently audit-able.
27
+ *
28
+ * Pure function; does not mutate inputs.
29
+ *
30
+ * @param {object[]} snapshot — entries from getOrCreateSessionHistory(sessionId)
31
+ * (already threadId-filtered by the caller)
32
+ * @param {string} vpId — the VP we're about to send a turn for
33
+ * @returns {object[]}
34
+ */
35
+ export function filterSnapshotForVp(snapshot, vpId) {
36
+ if (!Array.isArray(snapshot) || snapshot.length === 0) return [];
37
+ const out = [];
38
+ for (const m of snapshot) {
39
+ if (!m || typeof m !== 'object') continue;
40
+ // Reflection / engine-private rows are dropped regardless of vpId.
41
+ // Even the "no vpId, give me everything" code path must not leak
42
+ // these into a snapshot the caller will hand to an LLM — they were
43
+ // never meant to enter another VP's context, nor any VP's.
44
+ if (m._reflection || m.internal || m.systemOnly || m.systemOnlyMessage) continue;
45
+ if (!vpId) {
46
+ // No VP scope known: treat every row as "own". This is the
47
+ // fallback used by callers that don't (yet) have a vpId; the
48
+ // reflection filter above still applies.
49
+ out.push(m);
50
+ continue;
51
+ }
52
+ if (m.role === 'user') {
53
+ out.push(m);
54
+ continue;
55
+ }
56
+ if (m.role === 'assistant') {
57
+ if (!m.speakerVpId || m.speakerVpId === vpId) {
58
+ // Own assistant turn OR un-attributed (pre-rename) row — keep
59
+ // intact so this VP's tool arcs survive untouched.
60
+ out.push(m);
61
+ } else {
62
+ // Other VP's assistant turn — keep the visible text only.
63
+ // Stripping toolCalls is what makes the paired 'tool' rows
64
+ // below safe to drop without leaving orphan tool_use ids in
65
+ // the LLM payload. Stripping thinkingBlocks is required by
66
+ // Anthropic — signatures are VP-private and would fail
67
+ // server-side verification if echoed by a different VP.
68
+ const copy = { ...m };
69
+ delete copy.toolCalls;
70
+ delete copy.thinkingBlocks;
71
+ out.push(copy);
72
+ }
73
+ continue;
74
+ }
75
+ if (m.role === 'tool') {
76
+ // Tool results belong to the assistant turn that emitted the
77
+ // tool_use. We only keep ours; the other VPs' results paired
78
+ // with `toolCalls` we just stripped above, so they'd be
79
+ // orphans now anyway.
80
+ if (!m.speakerVpId || m.speakerVpId === vpId) out.push(m);
81
+ continue;
82
+ }
83
+ // Unknown role — keep as-is so future schema additions don't get
84
+ // silently dropped.
85
+ out.push(m);
86
+ }
87
+ return out;
88
+ }