@yeaft/webchat-agent 1.0.446 → 1.0.448
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/local-runtime/server/context.js +3 -22
- package/local-runtime/server/database.js +1 -0
- package/local-runtime/server/db/agent-inventory-db.js +122 -0
- package/local-runtime/server/db/connection.js +59 -0
- package/local-runtime/server/handlers/agent-sync.js +12 -1
- package/local-runtime/server/handlers/client-conversation.js +1 -0
- package/local-runtime/server/routes/admin-routes.js +78 -15
- package/local-runtime/server/ws-agent.js +41 -3
- package/local-runtime/server/ws-client.js +1 -7
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +208 -233
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- 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
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* archive/turn-archive.js — DESIGN.md §4.2 + §4.4.
|
|
3
3
|
*
|
|
4
|
-
* When
|
|
4
|
+
* When a durable archive operation stores a turn-group, the full content is
|
|
5
5
|
* written to `…/archive/<turnId>.md` so a worker can later replay it via
|
|
6
6
|
* `message_trace({turnId})`. Each archived turn is one markdown file
|
|
7
7
|
* with a YAML-ish header followed by JSON-encoded message bodies. We
|
package/yeaft/cli.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* /history — Show conversation history
|
|
18
18
|
* /history [n] — Show last N messages from conversation
|
|
19
19
|
* /search <keyword> — Search conversation history
|
|
20
|
-
* /compact —
|
|
20
|
+
* /compact — Retired for native Yeaft; inspect history or Memory instead
|
|
21
21
|
* /tools — List registered tools
|
|
22
22
|
* /skills — List loaded skills
|
|
23
23
|
* Conversation persistence across REPL sessions
|
|
@@ -380,13 +380,17 @@ 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
|
|
384
|
-
//
|
|
383
|
+
// into one turn). 20 turns is the bootstrap window. Provider requests
|
|
384
|
+
// apply deterministic history-window trimming; persisted history stays
|
|
385
|
+
// authoritative and no LLM conversation summary is generated.
|
|
385
386
|
let conversationMessages = conversationStore.loadRecent().map(m => ({
|
|
386
387
|
role: m.role,
|
|
387
388
|
content: m.content,
|
|
388
389
|
...(m.toolCallId && { toolCallId: m.toolCallId }),
|
|
389
390
|
...(m.toolCalls && { toolCalls: m.toolCalls }),
|
|
391
|
+
...(Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length > 0
|
|
392
|
+
? { thinkingBlocks: m.thinkingBlocks.map(block => ({ ...block })) }
|
|
393
|
+
: {}),
|
|
390
394
|
}));
|
|
391
395
|
|
|
392
396
|
const hotCount = conversationStore.countHot();
|
|
@@ -445,7 +449,7 @@ async function runREPL(config, args) {
|
|
|
445
449
|
console.log(' /trace <stats|recent> — Query debug trace');
|
|
446
450
|
console.log(' /history [n] — Show last N messages');
|
|
447
451
|
console.log(' /search <keyword> — Search conversation history');
|
|
448
|
-
console.log(' /compact —
|
|
452
|
+
console.log(' /compact — Retired; native Yeaft keeps transcript + Memory');
|
|
449
453
|
console.log(' /context — Show context info');
|
|
450
454
|
console.log(' /dry-run — Toggle dry-run mode');
|
|
451
455
|
console.log(' /stats — Show session stats');
|
|
@@ -516,7 +520,7 @@ async function runREPL(config, args) {
|
|
|
516
520
|
}
|
|
517
521
|
|
|
518
522
|
case 'compact': {
|
|
519
|
-
console.log('The /compact REPL command is retired.
|
|
523
|
+
console.log('The /compact REPL command is retired. Native Yeaft keeps the transcript authoritative and uses Memory plus a deterministic provider history window.');
|
|
520
524
|
break;
|
|
521
525
|
}
|
|
522
526
|
|
|
@@ -756,13 +760,6 @@ async function runREPL(config, args) {
|
|
|
756
760
|
process.stderr.write(`[recall] ${event.entryCount} entries${event.cached ? ' (cached)' : ''}\n`);
|
|
757
761
|
}
|
|
758
762
|
break;
|
|
759
|
-
case 'consolidate':
|
|
760
|
-
if (session.config.debug) {
|
|
761
|
-
process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
|
|
762
|
-
} else {
|
|
763
|
-
process.stderr.write(`[compact] Memory consolidated\n`);
|
|
764
|
-
}
|
|
765
|
-
break;
|
|
766
763
|
case 'fallback':
|
|
767
764
|
process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
|
|
768
765
|
break;
|
|
@@ -911,6 +908,9 @@ async function runStreamJson(config, args) {
|
|
|
911
908
|
content: message.content,
|
|
912
909
|
...(message.toolCallId && { toolCallId: message.toolCallId }),
|
|
913
910
|
...(message.toolCalls && { toolCalls: message.toolCalls }),
|
|
911
|
+
...(Array.isArray(message.thinkingBlocks) && message.thinkingBlocks.length > 0
|
|
912
|
+
? { thinkingBlocks: message.thinkingBlocks.map(block => ({ ...block })) }
|
|
913
|
+
: {}),
|
|
914
914
|
}));
|
|
915
915
|
|
|
916
916
|
// An ad-hoc stream-json conversation has exactly one Engine and no VP roster.
|
|
@@ -1263,6 +1263,9 @@ async function runOnce(config, args) {
|
|
|
1263
1263
|
content: m.content,
|
|
1264
1264
|
...(m.toolCallId && { toolCallId: m.toolCallId }),
|
|
1265
1265
|
...(m.toolCalls && { toolCalls: m.toolCalls }),
|
|
1266
|
+
...(Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length > 0
|
|
1267
|
+
? { thinkingBlocks: m.thinkingBlocks.map(block => ({ ...block })) }
|
|
1268
|
+
: {}),
|
|
1266
1269
|
}));
|
|
1267
1270
|
|
|
1268
1271
|
let terminalEngineError = null;
|
|
@@ -1292,11 +1295,6 @@ async function runOnce(config, args) {
|
|
|
1292
1295
|
process.stderr.write(`[recall] ${event.entryCount} entries${event.cached ? ' (cached)' : ''}\n`);
|
|
1293
1296
|
}
|
|
1294
1297
|
break;
|
|
1295
|
-
case 'consolidate':
|
|
1296
|
-
if (args.verbose || args.debug) {
|
|
1297
|
-
process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
|
|
1298
|
-
}
|
|
1299
|
-
break;
|
|
1300
1298
|
case 'fallback':
|
|
1301
1299
|
process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
|
|
1302
1300
|
break;
|
package/yeaft/config.js
CHANGED
|
@@ -51,9 +51,8 @@ const DEFAULTS = {
|
|
|
51
51
|
yeaftAutoArchiveIdleDays: 30,
|
|
52
52
|
// Cold-start replay window: how many of the most recent turns
|
|
53
53
|
// ConversationStore.loadRecentBySession / loadSessionHistoryForVp
|
|
54
|
-
// bring back
|
|
55
|
-
//
|
|
56
|
-
// before compaction fires. Range: 1–500.
|
|
54
|
+
// bring back after boot or reconnect. Older transcript remains available
|
|
55
|
+
// through history pagination/search. Range: 1–500.
|
|
57
56
|
yeaftRecentTurnsLimit: 20,
|
|
58
57
|
// CLAUDE.md / AGENTS.md project-doc cap, in bytes. Mirrors Codex's
|
|
59
58
|
// `project_doc_max_bytes`. 0 disables the feature (no project-doc
|
|
@@ -18,6 +18,8 @@ export function isInternalControlContent(content) {
|
|
|
18
18
|
export function isHiddenConversationRow(row) {
|
|
19
19
|
if (!row) return true;
|
|
20
20
|
if (row._reflection || row.internal || row.systemOnly || row.systemOnlyMessage) return true;
|
|
21
|
+
// Legacy compact-control rows remain hidden when reading old stores. The
|
|
22
|
+
// current runtime never creates or injects them.
|
|
21
23
|
if (row.kind === 'compact_summary' || row._compactSummary) return true;
|
|
22
24
|
return isInternalControlContent(row.content);
|
|
23
25
|
}
|
|
@@ -43,9 +43,9 @@ import { markConversationDirty } from './history-index-state.js';
|
|
|
43
43
|
* message boundary, which is pair-safe by construction.
|
|
44
44
|
*
|
|
45
45
|
* 20 turns is the bootstrap window the user signed off on (2026-05-01).
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* It is the cold-start replay window after a fresh boot or reconnect. Runtime
|
|
47
|
+
* provider requests apply a separate deterministic history-window transform;
|
|
48
|
+
* no LLM summary is required for recovery.
|
|
49
49
|
*
|
|
50
50
|
* Configurable via `~/.yeaft/config.json` → `yeaft.recentTurnsLimit`.
|
|
51
51
|
* Session boot calls `setDefaultRecentTurnsLimit()` once with the
|
|
@@ -212,25 +212,23 @@ export function __resetTruncationWarned() {
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* Warn (once per session per process) when the cold-start replay window
|
|
215
|
-
*
|
|
216
|
-
*
|
|
215
|
+
* truncates history. The persisted transcript remains available through
|
|
216
|
+
* pagination/search; this warning only describes the model bootstrap window.
|
|
217
217
|
*
|
|
218
218
|
* @param {string} sessionId
|
|
219
219
|
* @param {string} storeDir
|
|
220
220
|
* @param {number} recentTurnsLimit — configured recent-turn window
|
|
221
|
-
* @param {boolean} hasCompactSummary
|
|
222
221
|
*/
|
|
223
|
-
function maybeWarnHistoryTruncated(sessionId, storeDir, recentTurnsLimit
|
|
222
|
+
function maybeWarnHistoryTruncated(sessionId, storeDir, recentTurnsLimit) {
|
|
224
223
|
if (!sessionId || !storeDir) return;
|
|
225
|
-
if (hasCompactSummary) return;
|
|
226
224
|
const key = `${storeDir}::${sessionId}`;
|
|
227
225
|
if (_truncationWarned.has(key)) return;
|
|
228
226
|
_truncationWarned.add(key);
|
|
229
227
|
// eslint-disable-next-line no-console
|
|
230
228
|
console.warn(
|
|
231
|
-
`[Yeaft] history for session ${sessionId}
|
|
232
|
-
`
|
|
233
|
-
`Raise yeaft.recentTurnsLimit in ~/.yeaft/config.json
|
|
229
|
+
`[Yeaft] history for session ${sessionId} bootstrapped with ${recentTurnsLimit} recent turns ` +
|
|
230
|
+
`(recentTurnsLimit=${DEFAULT_RECENT_TURNS}); older transcript remains available through history pagination/search. ` +
|
|
231
|
+
`Raise yeaft.recentTurnsLimit in ~/.yeaft/config.json to send more recent context after boot.`
|
|
234
232
|
);
|
|
235
233
|
}
|
|
236
234
|
|
|
@@ -1116,12 +1114,10 @@ class SegmentStore {
|
|
|
1116
1114
|
* chat/ — one-to-one chat mode history
|
|
1117
1115
|
* index.json
|
|
1118
1116
|
* segments/
|
|
1119
|
-
* compact.md
|
|
1120
1117
|
* blobs/
|
|
1121
1118
|
* sessions/<sessionId>/conversation/
|
|
1122
1119
|
* index.json
|
|
1123
1120
|
* segments/
|
|
1124
|
-
* compact/
|
|
1125
1121
|
* blobs/
|
|
1126
1122
|
*
|
|
1127
1123
|
* Legacy compatibility: ~/.yeaft/conversation is read as an old mixed store,
|
|
@@ -1139,9 +1135,6 @@ export class ConversationStore {
|
|
|
1139
1135
|
#msgDir; // default hot messages dir: ~/.yeaft/chat/messages
|
|
1140
1136
|
#coldDir; // default cold messages dir: ~/.yeaft/chat/cold
|
|
1141
1137
|
#indexPath; // ~/.yeaft/chat/index.md
|
|
1142
|
-
#compactPath; // ~/.yeaft/chat/compact.md
|
|
1143
|
-
#legacyCompactPath;
|
|
1144
|
-
#legacyCompactScopedDir;
|
|
1145
1138
|
#chatMsgDir;
|
|
1146
1139
|
#chatColdDir;
|
|
1147
1140
|
#legacyMsgDir;
|
|
@@ -1163,18 +1156,12 @@ export class ConversationStore {
|
|
|
1163
1156
|
this.#msgDir = join(this.#chatDir, 'messages');
|
|
1164
1157
|
this.#coldDir = join(this.#chatDir, 'cold');
|
|
1165
1158
|
this.#indexPath = join(this.#chatDir, 'index.md');
|
|
1166
|
-
this.#compactPath = join(this.#chatDir, 'compact.md');
|
|
1167
|
-
this.#legacyCompactPath = join(this.#legacyConvDir, 'compact.md');
|
|
1168
1159
|
|
|
1169
1160
|
this.#chatMsgDir = this.#msgDir;
|
|
1170
1161
|
this.#chatColdDir = this.#coldDir;
|
|
1171
1162
|
this.#legacyMsgDir = join(this.#legacyConvDir, 'messages');
|
|
1172
1163
|
this.#legacyColdDir = join(this.#legacyConvDir, 'cold');
|
|
1173
1164
|
|
|
1174
|
-
// Per-(sessionId, vpId) compact summary files live under that session's
|
|
1175
|
-
// conversation directory. The legacy ~/.yeaft/conversation/compact directory
|
|
1176
|
-
// is read for compatibility.
|
|
1177
|
-
this.#legacyCompactScopedDir = join(this.#legacyConvDir, 'compact');
|
|
1178
1165
|
this.#nextSeq = null;
|
|
1179
1166
|
this.#nextSeqByThread = new Map();
|
|
1180
1167
|
|
|
@@ -1390,50 +1377,8 @@ export class ConversationStore {
|
|
|
1390
1377
|
}
|
|
1391
1378
|
|
|
1392
1379
|
/**
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
* Compact's semantics are "the running summary of everything older than
|
|
1396
|
-
* the hot window". Each compact pass already received the previous
|
|
1397
|
-
* summary text as input and produced a *new* cumulative summary — so
|
|
1398
|
-
* we overwrite, we never append. Appending was the original behaviour
|
|
1399
|
-
* (kept around as a diary), but the engine reads the whole file back
|
|
1400
|
-
* into `<conversation_summary>` on every turn, so appending grows the
|
|
1401
|
-
* per-turn prompt without bound until it defeats compaction itself.
|
|
1402
|
-
*
|
|
1403
|
-
* @param {string} summary — the new, complete summary to persist
|
|
1404
|
-
*/
|
|
1405
|
-
replaceCompactSummary(summary) {
|
|
1406
|
-
if (typeof summary !== 'string' || !summary) return;
|
|
1407
|
-
try {
|
|
1408
|
-
writeFileSync(this.#compactPath, summary, { encoding: 'utf8', mode: 0o644 });
|
|
1409
|
-
} catch (err) {
|
|
1410
|
-
if (isPermissionError(err)) {
|
|
1411
|
-
if (!_permissionWarned) {
|
|
1412
|
-
console.warn(`[Yeaft] Cannot write compact summary: ${err.code}`);
|
|
1413
|
-
_permissionWarned = true;
|
|
1414
|
-
}
|
|
1415
|
-
} else {
|
|
1416
|
-
throw err;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
/**
|
|
1422
|
-
* Read the compact summary.
|
|
1423
|
-
*
|
|
1424
|
-
* @returns {string}
|
|
1425
|
-
*/
|
|
1426
|
-
readCompactSummary() {
|
|
1427
|
-
if (existsSync(this.#compactPath)) return readFileSync(this.#compactPath, 'utf8');
|
|
1428
|
-
if (existsSync(this.#legacyCompactPath)) return readFileSync(this.#legacyCompactPath, 'utf8');
|
|
1429
|
-
return '';
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
/**
|
|
1433
|
-
* Sanitize one id (sessionId or vpId) into a safe filename component.
|
|
1380
|
+
* Sanitize one id into a safe directory component.
|
|
1434
1381
|
* Anything outside `[A-Za-z0-9._-]` collapses to `_`; max 120 chars.
|
|
1435
|
-
* For directory path components, use `#safeDirComponent` instead; this
|
|
1436
|
-
* helper intentionally preserves historical compact-summary filenames.
|
|
1437
1382
|
*
|
|
1438
1383
|
* @param {string} s
|
|
1439
1384
|
* @returns {string}
|
|
@@ -1447,101 +1392,6 @@ export class ConversationStore {
|
|
|
1447
1392
|
return safe || '_';
|
|
1448
1393
|
}
|
|
1449
1394
|
|
|
1450
|
-
/**
|
|
1451
|
-
* Sanitize a (sessionId, vpId) pair into a safe filename. We accept
|
|
1452
|
-
* arbitrary user strings here (sessionIds and vpIds are user-set), so
|
|
1453
|
-
* the result is purely a basename — never parsed back.
|
|
1454
|
-
*
|
|
1455
|
-
* @param {string} sessionId
|
|
1456
|
-
* @param {string} vpId
|
|
1457
|
-
* @returns {string|null} — full path, or null if either id missing
|
|
1458
|
-
*/
|
|
1459
|
-
#scopedCompactPath(sessionId, vpId) {
|
|
1460
|
-
if (!sessionId || !vpId) return null;
|
|
1461
|
-
const compactDir = join(this.#sessionConversationDir(sessionId, { create: true }), 'compact');
|
|
1462
|
-
return join(compactDir, `${this.#safeIdComponent(vpId)}.md`);
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
#legacyScopedCompactPath(sessionId, vpId) {
|
|
1466
|
-
if (!sessionId || !vpId) return null;
|
|
1467
|
-
return join(this.#legacyCompactScopedDir, `${this.#safeIdComponent(sessionId)}__${this.#safeIdComponent(vpId)}.md`);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
/**
|
|
1471
|
-
* Read a per-(sessionId, vpId) compact summary. Returns '' if no summary
|
|
1472
|
-
* has been written yet. Falls back to nothing — callers that need the
|
|
1473
|
-
* legacy global file should call `readCompactSummary()` explicitly.
|
|
1474
|
-
*
|
|
1475
|
-
* The (group, vp) scoping was introduced after we noticed the legacy
|
|
1476
|
-
* single-file `compact.md` was shared across every group AND every VP
|
|
1477
|
-
* in a session — so each new compact would clobber/append on top of
|
|
1478
|
-
* unrelated content and every VP read the same merged blob. See
|
|
1479
|
-
* `engine.#runOrchestratorCompact`.
|
|
1480
|
-
*
|
|
1481
|
-
* @param {string} sessionId
|
|
1482
|
-
* @param {string} vpId
|
|
1483
|
-
* @returns {string}
|
|
1484
|
-
*/
|
|
1485
|
-
readCompactSummaryFor(sessionId, vpId) {
|
|
1486
|
-
const path = this.#scopedCompactPath(sessionId, vpId);
|
|
1487
|
-
const legacyPath = this.#legacyScopedCompactPath(sessionId, vpId);
|
|
1488
|
-
for (const candidate of [path, legacyPath]) {
|
|
1489
|
-
if (!candidate || !existsSync(candidate)) continue;
|
|
1490
|
-
try { return readFileSync(candidate, 'utf8'); }
|
|
1491
|
-
catch { return ''; }
|
|
1492
|
-
}
|
|
1493
|
-
return '';
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
/**
|
|
1497
|
-
* Rewrite a per-(sessionId, vpId) compact summary in place. See
|
|
1498
|
-
* `replaceCompactSummary` for the rationale — same reason, scoped file.
|
|
1499
|
-
*
|
|
1500
|
-
* @param {string} sessionId
|
|
1501
|
-
* @param {string} vpId
|
|
1502
|
-
* @param {string} summary
|
|
1503
|
-
*/
|
|
1504
|
-
replaceCompactSummaryFor(sessionId, vpId, summary) {
|
|
1505
|
-
if (typeof summary !== 'string' || !summary) return;
|
|
1506
|
-
const path = this.#scopedCompactPath(sessionId, vpId);
|
|
1507
|
-
if (!path) return;
|
|
1508
|
-
try {
|
|
1509
|
-
writeFileSync(path, summary, { encoding: 'utf8', mode: 0o644 });
|
|
1510
|
-
} catch (err) {
|
|
1511
|
-
if (isPermissionError(err)) {
|
|
1512
|
-
if (!_permissionWarned) {
|
|
1513
|
-
console.warn(`[Yeaft] Cannot write scoped compact summary: ${err.code}`);
|
|
1514
|
-
_permissionWarned = true;
|
|
1515
|
-
}
|
|
1516
|
-
} else {
|
|
1517
|
-
throw err;
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
/**
|
|
1523
|
-
* Check whether ANY per-(session, vp) compact summary exists for `sessionId`.
|
|
1524
|
-
* Used by the history-replay path to decide whether to flag
|
|
1525
|
-
* `hasCompactSummary` for the UI without committing to one VP's view.
|
|
1526
|
-
*
|
|
1527
|
-
* @param {string} sessionId
|
|
1528
|
-
* @returns {boolean}
|
|
1529
|
-
*/
|
|
1530
|
-
hasAnyCompactSummaryForSession(sessionId) {
|
|
1531
|
-
if (!sessionId) return false;
|
|
1532
|
-
const compactDir = join(this.#sessionConversationDir(sessionId), 'compact');
|
|
1533
|
-
for (const dir of [compactDir, this.#legacyCompactScopedDir]) {
|
|
1534
|
-
if (!existsSync(dir)) continue;
|
|
1535
|
-
try {
|
|
1536
|
-
for (const f of readdirSync(dir)) {
|
|
1537
|
-
if (dir === compactDir && f.endsWith('.md')) return true;
|
|
1538
|
-
if (dir === this.#legacyCompactScopedDir && f.startsWith(`${this.#safeIdComponent(sessionId)}__`) && f.endsWith('.md')) return true;
|
|
1539
|
-
}
|
|
1540
|
-
} catch { /* best-effort */ }
|
|
1541
|
-
}
|
|
1542
|
-
return false;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
1395
|
/**
|
|
1546
1396
|
* Update the conversation index.md with current state.
|
|
1547
1397
|
*
|
|
@@ -1581,7 +1431,7 @@ export class ConversationStore {
|
|
|
1581
1431
|
}
|
|
1582
1432
|
|
|
1583
1433
|
/**
|
|
1584
|
-
* Clear all messages (hot + cold
|
|
1434
|
+
* Clear all persisted transcript messages (hot + cold).
|
|
1585
1435
|
*/
|
|
1586
1436
|
clear() {
|
|
1587
1437
|
for (const dir of [this.#chatMsgDir, this.#chatColdDir, ...this.#sessionMessageDirs('messages'), ...this.#sessionMessageDirs('cold')]) {
|
|
@@ -1600,16 +1450,6 @@ export class ConversationStore {
|
|
|
1600
1450
|
for (const dir of [this.#chatDir, ...this.#sessionConversationDirs({ primaryOnly: true })]) {
|
|
1601
1451
|
this.#segmentStoreForConversationDir(dir).clear();
|
|
1602
1452
|
}
|
|
1603
|
-
// Reset compact files in the new chat/session stores. Legacy
|
|
1604
|
-
// ~/.yeaft/conversation is intentionally left untouched.
|
|
1605
|
-
for (const path of [this.#compactPath]) {
|
|
1606
|
-
if (!existsSync(path)) continue;
|
|
1607
|
-
try {
|
|
1608
|
-
writeFileSync(path, '', { encoding: 'utf8', mode: 0o644 });
|
|
1609
|
-
} catch (err) {
|
|
1610
|
-
if (!isPermissionError(err)) throw err;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
1453
|
this.#nextSeq = 1;
|
|
1614
1454
|
this.updateIndex({ totalMessages: 0, lastMessageId: null });
|
|
1615
1455
|
this.#markAllDirty('clear');
|
|
@@ -1709,8 +1549,7 @@ export class ConversationStore {
|
|
|
1709
1549
|
includeReflections,
|
|
1710
1550
|
});
|
|
1711
1551
|
if (truncated) {
|
|
1712
|
-
|
|
1713
|
-
maybeWarnHistoryTruncated(sessionId, this.#dir, turnsLimit, hasCompact);
|
|
1552
|
+
maybeWarnHistoryTruncated(sessionId, this.#dir, turnsLimit);
|
|
1714
1553
|
}
|
|
1715
1554
|
return pairSanitize(messages);
|
|
1716
1555
|
}
|
|
@@ -1726,12 +1565,11 @@ export class ConversationStore {
|
|
|
1726
1565
|
}
|
|
1727
1566
|
|
|
1728
1567
|
/**
|
|
1729
|
-
* VP-scoped view of
|
|
1568
|
+
* VP-scoped view of Session history, used to build a pair-safe provider
|
|
1569
|
+
* snapshot for one VP. It operates on what the VP can actually see, not
|
|
1570
|
+
* the union of every VP's tool calls/results.
|
|
1730
1571
|
*
|
|
1731
|
-
*
|
|
1732
|
-
* not the union of every VP's tool calls/results — otherwise compact
|
|
1733
|
-
* tries to summarize tool transcripts that were never in this VP's
|
|
1734
|
-
* prompt window. The rule we settled on (with the user, 2026-06-01):
|
|
1572
|
+
* The rule we settled on (with the user, 2026-06-01):
|
|
1735
1573
|
*
|
|
1736
1574
|
* - User rows (no speakerVpId): KEEP — every VP sees the prompt.
|
|
1737
1575
|
* - This VP's own assistant rows + their paired tool rows: KEEP.
|
|
@@ -1741,8 +1579,8 @@ export class ConversationStore {
|
|
|
1741
1579
|
* - OTHER VPs' tool result rows (role:'tool'): DROP — they pair with
|
|
1742
1580
|
* stripped tool_use ids and would orphan on replay.
|
|
1743
1581
|
* - Hidden conversation rows (`_reflection`, `internal`, `systemOnly`,
|
|
1744
|
-
* `systemOnlyMessage`,
|
|
1745
|
-
*
|
|
1582
|
+
* `systemOnlyMessage`, and legacy internal-control content signatures):
|
|
1583
|
+
* DROP — they are engine-private and never enter
|
|
1746
1584
|
* visible history or another VP's context.
|
|
1747
1585
|
*
|
|
1748
1586
|
* The output is pair-safe by construction for THIS VP's tool arcs and
|
|
@@ -1774,7 +1612,7 @@ export class ConversationStore {
|
|
|
1774
1612
|
} else {
|
|
1775
1613
|
// Other VP's assistant text only — drop their toolCalls so the
|
|
1776
1614
|
// following role:'tool' rows (which we also drop) don't leave
|
|
1777
|
-
// orphan tool_use ids in the
|
|
1615
|
+
// orphan tool_use ids in the provider input.
|
|
1778
1616
|
const copy = { ...m };
|
|
1779
1617
|
delete copy.toolCalls;
|
|
1780
1618
|
delete copy.thinkingBlocks;
|
|
@@ -1790,10 +1628,8 @@ export class ConversationStore {
|
|
|
1790
1628
|
continue;
|
|
1791
1629
|
}
|
|
1792
1630
|
}
|
|
1793
|
-
//
|
|
1794
|
-
//
|
|
1795
|
-
// `partitionMessages`, and we don't want to pre-truncate before that
|
|
1796
|
-
// budget calc sees the full picture.
|
|
1631
|
+
// Do not truncate here. The provider-request history window owns
|
|
1632
|
+
// deterministic budget trimming after this VP-scoped view is built.
|
|
1797
1633
|
return pairSanitize(out);
|
|
1798
1634
|
}
|
|
1799
1635
|
|
|
@@ -2696,36 +2532,6 @@ export class ConversationStore {
|
|
|
2696
2532
|
.filter(dir => existsSync(dir));
|
|
2697
2533
|
}
|
|
2698
2534
|
|
|
2699
|
-
/** Per-chat scoped compact summary path. */
|
|
2700
|
-
#scopedChatCompactPath(chatId, vpId) {
|
|
2701
|
-
if (!chatId || !vpId) return null;
|
|
2702
|
-
const dir = join(this.#chatConversationDir(chatId, { create: true }), 'compact');
|
|
2703
|
-
return join(dir, `${this.#safeIdComponent(vpId)}.md`);
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
/** Read per-(chatId, vpId) compact summary. */
|
|
2707
|
-
readCompactSummaryForChat(chatId, vpId) {
|
|
2708
|
-
const p = this.#scopedChatCompactPath(chatId, vpId);
|
|
2709
|
-
if (!p || !existsSync(p)) return '';
|
|
2710
|
-
try { return readFileSync(p, 'utf8'); } catch { return ''; }
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
/** Rewrite per-(chatId, vpId) compact summary. */
|
|
2714
|
-
replaceCompactSummaryForChat(chatId, vpId, summary) {
|
|
2715
|
-
if (typeof summary !== 'string' || !summary) return;
|
|
2716
|
-
const p = this.#scopedChatCompactPath(chatId, vpId);
|
|
2717
|
-
if (!p) return;
|
|
2718
|
-
try { writeFileSync(p, summary, { encoding: 'utf8', mode: 0o644 }); }
|
|
2719
|
-
catch (err) {
|
|
2720
|
-
if (isPermissionError(err)) {
|
|
2721
|
-
if (!_permissionWarned) {
|
|
2722
|
-
console.warn(`[Yeaft] Cannot write scoped chat compact summary: ${err.code}`);
|
|
2723
|
-
_permissionWarned = true;
|
|
2724
|
-
}
|
|
2725
|
-
} else throw err;
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
2535
|
/** Recent messages for a chat — chat mode mirror of loadRecentBySession. */
|
|
2730
2536
|
loadRecentByChat(chatId, turnsLimit = DEFAULT_RECENT_TURNS) {
|
|
2731
2537
|
if (!chatId) return [];
|
|
@@ -2773,7 +2579,7 @@ export class ConversationStore {
|
|
|
2773
2579
|
}
|
|
2774
2580
|
|
|
2775
2581
|
#ensureConversationDirs(dir) {
|
|
2776
|
-
for (const d of [dir, join(dir, 'blobs'), join(dir, SEGMENT_DIR), join(dir, 'messages'), join(dir, 'cold')
|
|
2582
|
+
for (const d of [dir, join(dir, 'blobs'), join(dir, SEGMENT_DIR), join(dir, 'messages'), join(dir, 'cold')]) {
|
|
2777
2583
|
if (!existsSync(d)) mkdirSync(d, { recursive: true, mode: 0o755 });
|
|
2778
2584
|
}
|
|
2779
2585
|
}
|
|
@@ -101,7 +101,7 @@ function flushAssistantEntries(sessionId, entries, boundaryMessageId = null) {
|
|
|
101
101
|
* bucket above it. That boundary lets interleaved VP rows (A-B-A) coalesce by
|
|
102
102
|
* explicit turnId + speaker without scanning the whole transcript. Legacy
|
|
103
103
|
* assistant rows without a turnId remain separate because guessing from
|
|
104
|
-
* adjacency would make their identity change after prepend or
|
|
104
|
+
* adjacency would make their identity change after prepend or transcript edits.
|
|
105
105
|
*/
|
|
106
106
|
export function* iterateCanonicalVisibleEntriesNewestFirst(messages, sessionId) {
|
|
107
107
|
const seenMessageIds = new Set();
|
package/yeaft/effort.js
CHANGED
|
@@ -38,7 +38,6 @@ export const LONG_LOOP_TURN_THRESHOLD = 8;
|
|
|
38
38
|
* Tiers (6 scenarios per architect spec):
|
|
39
39
|
* chat → max (default interactive pair-programming turn —
|
|
40
40
|
* quality over latency; per user 2026-05-22)
|
|
41
|
-
* consolidate → max (memory compaction — quality matters, runs once)
|
|
42
41
|
* dream → max (memory maintenance — same rationale)
|
|
43
42
|
* sub_agent → max (coordinator spawns + merges)
|
|
44
43
|
* long_loop → max (auto-bumped when toolLoopTurns >= threshold)
|
|
@@ -49,7 +48,6 @@ export const LONG_LOOP_TURN_THRESHOLD = 8;
|
|
|
49
48
|
*/
|
|
50
49
|
export const SCENARIO_EFFORT = Object.freeze({
|
|
51
50
|
chat: 'max',
|
|
52
|
-
consolidate: 'max',
|
|
53
51
|
dream: 'max',
|
|
54
52
|
sub_agent: 'max',
|
|
55
53
|
long_loop: 'max',
|