@yeaft/webchat-agent 0.1.852 → 0.1.854

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": "0.1.852",
3
+ "version": "0.1.854",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -450,20 +450,22 @@ export class ConversationStore {
450
450
  }
451
451
 
452
452
  /**
453
- * Update the compact summary (cumulative).
453
+ * Rewrite the compact summary in place.
454
454
  *
455
- * @param {string} summary new summary to append
455
+ * Compact's semantics are "the running summary of everything older than
456
+ * the hot window". Each compact pass already received the previous
457
+ * summary text as input and produced a *new* cumulative summary — so
458
+ * we overwrite, we never append. Appending was the original behaviour
459
+ * (kept around as a diary), but the engine reads the whole file back
460
+ * into `<conversation_summary>` on every turn, so appending grows the
461
+ * per-turn prompt without bound until it defeats compaction itself.
462
+ *
463
+ * @param {string} summary — the new, complete summary to persist
456
464
  */
457
- updateCompactSummary(summary) {
458
- let existing = '';
459
- if (existsSync(this.#compactPath)) {
460
- existing = readFileSync(this.#compactPath, 'utf8');
461
- }
462
-
463
- const date = new Date().toISOString().split('T')[0];
464
- const entry = `\n## ${date}\n\n${summary}\n`;
465
+ replaceCompactSummary(summary) {
466
+ if (typeof summary !== 'string' || !summary) return;
465
467
  try {
466
- writeFileSync(this.#compactPath, existing + entry, { encoding: 'utf8', mode: 0o644 });
468
+ writeFileSync(this.#compactPath, summary, { encoding: 'utf8', mode: 0o644 });
467
469
  } catch (err) {
468
470
  if (isPermissionError(err)) {
469
471
  if (!_permissionWarned) {
@@ -539,26 +541,19 @@ export class ConversationStore {
539
541
  }
540
542
 
541
543
  /**
542
- * Append a per-(groupId, vpId) compact summary entry. Same append-only
543
- * "## YYYY-MM-DD ..." structure as the legacy `updateCompactSummary`,
544
- * but isolated to one file per (group, vp).
544
+ * Rewrite a per-(groupId, vpId) compact summary in place. See
545
+ * `replaceCompactSummary` for the rationale same reason, scoped file.
545
546
  *
546
547
  * @param {string} groupId
547
548
  * @param {string} vpId
548
549
  * @param {string} summary
549
550
  */
550
- updateCompactSummaryFor(groupId, vpId, summary) {
551
+ replaceCompactSummaryFor(groupId, vpId, summary) {
552
+ if (typeof summary !== 'string' || !summary) return;
551
553
  const path = this.#scopedCompactPath(groupId, vpId);
552
554
  if (!path) return;
553
- let existing = '';
554
- if (existsSync(path)) {
555
- try { existing = readFileSync(path, 'utf8'); }
556
- catch { existing = ''; }
557
- }
558
- const date = new Date().toISOString().split('T')[0];
559
- const entry = `\n## ${date}\n\n${summary}\n`;
560
555
  try {
561
- writeFileSync(path, existing + entry, { encoding: 'utf8', mode: 0o644 });
556
+ writeFileSync(path, summary, { encoding: 'utf8', mode: 0o644 });
562
557
  } catch (err) {
563
558
  if (isPermissionError(err)) {
564
559
  if (!_permissionWarned) {
package/unify/engine.js CHANGED
@@ -991,14 +991,12 @@ export class Engine {
991
991
  #getCompactSummary() {
992
992
  if (!this.#conversationStore) return '';
993
993
  // Per-(group, vp) scoping: when this engine is bound to a fan-out VP,
994
- // read its own summary file. On a miss (empty scoped file) we fall
995
- // through to the legacy global file so pre-PR sessions whose only
996
- // summary lives in compact.md still surface their context — matches
997
- // the OR-fallback in web-bridge's `hasCompactSummary` flag.
994
+ // read ONLY its own summary file. Falling back to legacy compact.md here
995
+ // leaks another group/VP's summary into every new group turn after one
996
+ // post-turn compact writes the session-global file.
998
997
  if (this.#groupId && this.#vpId
999
998
  && typeof this.#conversationStore.readCompactSummaryFor === 'function') {
1000
- const scoped = this.#conversationStore.readCompactSummaryFor(this.#groupId, this.#vpId);
1001
- if (scoped) return scoped;
999
+ return this.#conversationStore.readCompactSummaryFor(this.#groupId, this.#vpId);
1002
1000
  }
1003
1001
  return this.#conversationStore.readCompactSummary();
1004
1002
  }
@@ -1149,18 +1147,32 @@ export class Engine {
1149
1147
  // a jarring locale flip mid-context.
1150
1148
  const isZh = String(this.#config.language || '').toLowerCase().startsWith('zh');
1151
1149
  const summariserSystem = isZh
1152
- ? '你是对话摘要器。请用中文写出 2–3 段简明摘要,保留决策、事实与上下文。'
1153
- : 'You are a conversation summarizer. Summarize concisely in 2–3 paragraphs, preserving decisions, facts, and context.';
1150
+ ? '你是对话摘要器。下面包含「先前累计摘要」(可能为空)与「新待压缩对话」。请融合两者,输出一份「重写后的累计摘要」——不要分段罗列日期、不要保留 "## 2026-..." 等历史分节,直接产出一份连贯、可被下一轮直接重新注入 prompt 的摘要。保留关键决策、事实、上下文与人物意图。'
1151
+ : 'You are a conversation summarizer. The input contains a "previous cumulative summary" (may be empty) plus a "new conversation to absorb". Merge them into ONE rewritten cumulative summary — do NOT keep dated section headers or any historical log structure. Output a single coherent summary suitable to be re-injected into the next turn\'s prompt as-is. Preserve key decisions, facts, context, and intent.';
1154
1152
  const summariserPromptPrefix = isZh ? '请概括:\n\n' : 'Summarize:\n\n';
1155
1153
 
1156
1154
  const hooks = {
1157
1155
  summarise: async () => {
1158
1156
  try {
1157
+ // Read prior summary at call time, not at orchestrator setup,
1158
+ // so the merge always sees the freshest on-disk state even if
1159
+ // future orchestrator changes invoke summarise more than once.
1160
+ const priorSummary = this.#getCompactSummary() || '';
1161
+ const priorBlock = priorSummary
1162
+ ? (isZh
1163
+ ? `【先前累计摘要】\n${priorSummary}\n\n【新待压缩对话】\n`
1164
+ : `[Previous cumulative summary]\n${priorSummary}\n\n[New conversation to absorb]\n`)
1165
+ : '';
1159
1166
  const result = await adapter.call({
1160
1167
  model: fastConfig.model,
1161
1168
  system: summariserSystem,
1162
- messages: [{ role: 'user', content: `${summariserPromptPrefix}${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
1163
- maxTokens: 1024,
1169
+ messages: [{ role: 'user', content: `${summariserPromptPrefix}${priorBlock}${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
1170
+ // 10k output budget: the running summary is the engine's
1171
+ // long-term memory of cold turns, so it deserves room to
1172
+ // actually preserve detail. We rewrite-in-place each round,
1173
+ // so size stays bounded by maxTokens regardless of how many
1174
+ // compact passes have run.
1175
+ maxTokens: 10240,
1164
1176
  });
1165
1177
  return (result.text || '').trim();
1166
1178
  } catch {
@@ -1204,10 +1216,10 @@ export class Engine {
1204
1216
  conversationStore.moveToColdBatch(archiveIds);
1205
1217
  }
1206
1218
  if (out.compactSummary) {
1207
- if (scoped && typeof conversationStore.updateCompactSummaryFor === 'function') {
1208
- conversationStore.updateCompactSummaryFor(this.#groupId, this.#vpId, out.compactSummary);
1219
+ if (scoped && typeof conversationStore.replaceCompactSummaryFor === 'function') {
1220
+ conversationStore.replaceCompactSummaryFor(this.#groupId, this.#vpId, out.compactSummary);
1209
1221
  } else {
1210
- conversationStore.updateCompactSummary(out.compactSummary);
1222
+ conversationStore.replaceCompactSummary(out.compactSummary);
1211
1223
  }
1212
1224
  }
1213
1225
  // Index update only makes sense for the legacy path that actually
@@ -3595,7 +3595,10 @@ export async function handleUnifyLoadHistory(msg) {
3595
3595
  const visiblePage = groupId
3596
3596
  ? loadVisibleGroupHistoryPage(session.conversationStore, groupId, limit)
3597
3597
  : { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
3598
- const compactSummary = session.conversationStore.readCompactSummary();
3598
+ // Legacy compact.md is a non-group fallback only. For group replay, reading
3599
+ // it makes every group show "has compact" once any legacy/non-scoped compact
3600
+ // exists, even when this group has no scoped summary.
3601
+ const compactSummary = groupId ? '' : session.conversationStore.readCompactSummary();
3599
3602
  const replayEntries = groupId
3600
3603
  ? visiblePage.messages
3601
3604
  : visiblePage.messages
@@ -3645,15 +3648,13 @@ export async function handleUnifyLoadHistory(msg) {
3645
3648
  oldestSeq = visiblePage.oldestSeq;
3646
3649
  }
3647
3650
 
3648
- // hasCompactSummary used to read a single session-global file, so it
3649
- // was always true once ANY group/VP in the session had compacted.
3650
- // Now we check (a) the scoped dir for any per-VP summary file in this
3651
- // group, falling back to (b) the legacy global file for sessions that
3652
- // pre-date the per-(group, vp) split.
3651
+ // hasCompactSummary used to read a single session-global file, so it was
3652
+ // always true once ANY group/VP in the session had compacted. For group
3653
+ // replay, only scoped per-(group, vp) summaries count; legacy compact.md is
3654
+ // reserved for non-group / pre-scoped 1:1 callers.
3653
3655
  let hasCompactSummaryFlag = !!compactSummary;
3654
3656
  if (groupId && typeof session.conversationStore.hasAnyCompactSummaryForGroup === 'function') {
3655
- hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(groupId)
3656
- || hasCompactSummaryFlag;
3657
+ hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(groupId);
3657
3658
  }
3658
3659
 
3659
3660
  sendUnifyEvent({