@yeaft/webchat-agent 0.1.873 → 0.1.875
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/connection/message-router.js +20 -13
- package/package.json +1 -1
- package/providers/copilot-models.js +105 -89
- package/providers/copilot.js +3 -1
- package/yeaft/attachments.js +2 -2
- package/yeaft/cli.js +13 -13
- package/yeaft/compact/compactor.js +20 -20
- package/yeaft/conversation/persist.js +95 -95
- package/yeaft/debug-trace.js +12 -12
- package/yeaft/dream-v2/apply.js +15 -15
- package/yeaft/dream-v2/merge.js +12 -12
- package/yeaft/dream-v2/prompts/{extract-group.md → extract-session.md} +12 -12
- package/yeaft/dream-v2/prompts/index.js +3 -3
- package/yeaft/dream-v2/prompts/triage-pass1.md +1 -1
- package/yeaft/dream-v2/runner.js +37 -37
- package/yeaft/dream-v2/segment.js +3 -3
- package/yeaft/dream-v2/session-wiring.js +22 -22
- package/yeaft/dream-v2/state.js +7 -7
- package/yeaft/dream-v2/triage.js +22 -22
- package/yeaft/engine.js +65 -65
- package/yeaft/memory/ams-registry.js +22 -22
- package/yeaft/memory/seed-backfill.js +9 -9
- package/yeaft/memory/store-v2.js +27 -27
- package/yeaft/prompts.js +9 -9
- package/yeaft/routing/loop-guard.js +14 -14
- package/yeaft/routing/router.js +5 -5
- package/yeaft/session.js +5 -5
- package/yeaft/sessions/coordinator.js +96 -20
- package/yeaft/{groups → sessions}/ids.js +3 -3
- package/yeaft/{groups → sessions}/index.js +28 -28
- package/yeaft/sessions/pre-flow.js +178 -42
- package/yeaft/{groups → sessions}/seed-default.js +19 -19
- package/yeaft/{groups/group-config.js → sessions/session-config.js} +29 -29
- package/yeaft/{groups/group-crud.js → sessions/session-crud.js} +113 -113
- package/yeaft/sessions/session-store.js +85 -154
- package/yeaft/stop-hooks.js +4 -4
- package/yeaft/tools/todo-write.js +1 -1
- package/yeaft/tools/types.js +2 -2
- package/yeaft/vp/registry.js +1 -1
- package/yeaft/vp/vp-crud.js +1 -1
- package/yeaft/vp-status-broker.js +28 -28
- package/yeaft/web-bridge.js +411 -398
- package/yeaft/groups/coordinator.js +0 -221
- package/yeaft/groups/group-store.js +0 -212
- package/yeaft/groups/pre-flow.js +0 -329
- /package/yeaft/{groups → sessions}/feature-flag.js +0 -0
- /package/yeaft/{groups → sessions}/project-doc.js +0 -0
- /package/yeaft/{groups → sessions}/roster.js +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* persist.js — Conversation message persistence
|
|
3
3
|
*
|
|
4
4
|
* Each message is stored as a .md file with YAML frontmatter in
|
|
5
|
-
* ~/.yeaft/chat/messages/ or ~/.yeaft/groups/<
|
|
5
|
+
* ~/.yeaft/chat/messages/ or ~/.yeaft/groups/<sessionId>/conversation/messages/. Design: zero JSON, all Markdown.
|
|
6
6
|
*
|
|
7
7
|
* Message format:
|
|
8
8
|
* ---
|
|
@@ -104,11 +104,11 @@ function serializeMessage(msg) {
|
|
|
104
104
|
// their original thread id in `sourceThreadId` so the UI can still
|
|
105
105
|
// render a small "#source" pill next to each bubble.
|
|
106
106
|
if (msg.sourceThreadId) fm.push(`sourceThreadId: ${msg.sourceThreadId}`);
|
|
107
|
-
// Bug 6: persist
|
|
107
|
+
// Bug 6: persist sessionId so history replay can stamp messages with the
|
|
108
108
|
// group they originated in. Without this, every replayed message lands
|
|
109
109
|
// in the default group and switching back to the originating group
|
|
110
110
|
// shows an empty pane.
|
|
111
|
-
if (msg.
|
|
111
|
+
if (msg.sessionId) fm.push(`sessionId: ${msg.sessionId}`);
|
|
112
112
|
if (msg.chatId) fm.push(`chatId: ${msg.chatId}`);
|
|
113
113
|
// Group-chat attribution: when a VP authors an assistant turn (either
|
|
114
114
|
// its own reply or a route_forward injection from another VP), stamp
|
|
@@ -225,7 +225,7 @@ export function parseMessage(raw) {
|
|
|
225
225
|
case 'tokens_est': msg.tokens_est = parseInt(value, 10); break;
|
|
226
226
|
case 'threadId': msg.threadId = value; break;
|
|
227
227
|
case 'sourceThreadId': msg.sourceThreadId = value; break;
|
|
228
|
-
case '
|
|
228
|
+
case 'sessionId': msg.sessionId = value; break;
|
|
229
229
|
case 'chatId': msg.chatId = value; break;
|
|
230
230
|
case 'speakerVpId': msg.speakerVpId = value; break;
|
|
231
231
|
case 'attachmentsB64':
|
|
@@ -330,15 +330,15 @@ export function parseMessage(raw) {
|
|
|
330
330
|
* messages/
|
|
331
331
|
* cold/
|
|
332
332
|
* blobs/
|
|
333
|
-
* groups/<
|
|
333
|
+
* groups/<sessionId>/conversation/
|
|
334
334
|
* compact/
|
|
335
335
|
* messages/
|
|
336
336
|
* cold/
|
|
337
337
|
* blobs/
|
|
338
338
|
*
|
|
339
339
|
* Legacy compatibility: ~/.yeaft/conversation is read as an old mixed store.
|
|
340
|
-
* New writes are split by mode: records with
|
|
341
|
-
* groups/<
|
|
340
|
+
* New writes are split by mode: records with sessionId go to
|
|
341
|
+
* groups/<sessionId>/conversation/, all others go to chat/.
|
|
342
342
|
*/
|
|
343
343
|
export class ConversationStore {
|
|
344
344
|
#dir; // root dir (e.g. ~/.yeaft)
|
|
@@ -380,7 +380,7 @@ export class ConversationStore {
|
|
|
380
380
|
this.#legacyMsgDir = join(this.#legacyConvDir, 'messages');
|
|
381
381
|
this.#legacyColdDir = join(this.#legacyConvDir, 'cold');
|
|
382
382
|
|
|
383
|
-
// Per-(
|
|
383
|
+
// Per-(sessionId, vpId) compact summary files live under that group's
|
|
384
384
|
// conversation directory. The legacy ~/.yeaft/conversation/compact directory
|
|
385
385
|
// is read for compatibility.
|
|
386
386
|
this.#legacyCompactScopedDir = join(this.#legacyConvDir, 'compact');
|
|
@@ -389,7 +389,7 @@ export class ConversationStore {
|
|
|
389
389
|
|
|
390
390
|
// Ensure new chat and group-root directories exist (graceful on permission
|
|
391
391
|
// errors). Per-group conversation directories are created lazily once a
|
|
392
|
-
//
|
|
392
|
+
// sessionId is known. The legacy conversation directory is never created by
|
|
393
393
|
// new versions.
|
|
394
394
|
for (const d of [
|
|
395
395
|
this.#chatDir, join(this.#chatDir, 'blobs'), this.#chatMsgDir, this.#chatColdDir,
|
|
@@ -535,7 +535,7 @@ export class ConversationStore {
|
|
|
535
535
|
}
|
|
536
536
|
|
|
537
537
|
/**
|
|
538
|
-
* Sanitize one id (
|
|
538
|
+
* Sanitize one id (sessionId or vpId) into a safe filename component.
|
|
539
539
|
* Anything outside `[A-Za-z0-9._-]` collapses to `_`; max 120 chars.
|
|
540
540
|
* For directory path components, use `#safeDirComponent` instead; this
|
|
541
541
|
* helper intentionally preserves historical compact-summary filenames.
|
|
@@ -553,27 +553,27 @@ export class ConversationStore {
|
|
|
553
553
|
}
|
|
554
554
|
|
|
555
555
|
/**
|
|
556
|
-
* Sanitize a (
|
|
557
|
-
* arbitrary user strings here (
|
|
556
|
+
* Sanitize a (sessionId, vpId) pair into a safe filename. We accept
|
|
557
|
+
* arbitrary user strings here (sessionIds and vpIds are user-set), so
|
|
558
558
|
* the result is purely a basename — never parsed back.
|
|
559
559
|
*
|
|
560
|
-
* @param {string}
|
|
560
|
+
* @param {string} sessionId
|
|
561
561
|
* @param {string} vpId
|
|
562
562
|
* @returns {string|null} — full path, or null if either id missing
|
|
563
563
|
*/
|
|
564
|
-
#scopedCompactPath(
|
|
565
|
-
if (!
|
|
566
|
-
const compactDir = join(this.#groupConversationDir(
|
|
564
|
+
#scopedCompactPath(sessionId, vpId) {
|
|
565
|
+
if (!sessionId || !vpId) return null;
|
|
566
|
+
const compactDir = join(this.#groupConversationDir(sessionId, { create: true }), 'compact');
|
|
567
567
|
return join(compactDir, `${this.#safeIdComponent(vpId)}.md`);
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
-
#legacyScopedCompactPath(
|
|
571
|
-
if (!
|
|
572
|
-
return join(this.#legacyCompactScopedDir, `${this.#safeIdComponent(
|
|
570
|
+
#legacyScopedCompactPath(sessionId, vpId) {
|
|
571
|
+
if (!sessionId || !vpId) return null;
|
|
572
|
+
return join(this.#legacyCompactScopedDir, `${this.#safeIdComponent(sessionId)}__${this.#safeIdComponent(vpId)}.md`);
|
|
573
573
|
}
|
|
574
574
|
|
|
575
575
|
/**
|
|
576
|
-
* Read a per-(
|
|
576
|
+
* Read a per-(sessionId, vpId) compact summary. Returns '' if no summary
|
|
577
577
|
* has been written yet. Falls back to nothing — callers that need the
|
|
578
578
|
* legacy global file should call `readCompactSummary()` explicitly.
|
|
579
579
|
*
|
|
@@ -583,13 +583,13 @@ export class ConversationStore {
|
|
|
583
583
|
* unrelated content and every VP read the same merged blob. See
|
|
584
584
|
* `engine.#runOrchestratorCompact`.
|
|
585
585
|
*
|
|
586
|
-
* @param {string}
|
|
586
|
+
* @param {string} sessionId
|
|
587
587
|
* @param {string} vpId
|
|
588
588
|
* @returns {string}
|
|
589
589
|
*/
|
|
590
|
-
readCompactSummaryFor(
|
|
591
|
-
const path = this.#scopedCompactPath(
|
|
592
|
-
const legacyPath = this.#legacyScopedCompactPath(
|
|
590
|
+
readCompactSummaryFor(sessionId, vpId) {
|
|
591
|
+
const path = this.#scopedCompactPath(sessionId, vpId);
|
|
592
|
+
const legacyPath = this.#legacyScopedCompactPath(sessionId, vpId);
|
|
593
593
|
for (const candidate of [path, legacyPath]) {
|
|
594
594
|
if (!candidate || !existsSync(candidate)) continue;
|
|
595
595
|
try { return readFileSync(candidate, 'utf8'); }
|
|
@@ -599,16 +599,16 @@ export class ConversationStore {
|
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
/**
|
|
602
|
-
* Rewrite a per-(
|
|
602
|
+
* Rewrite a per-(sessionId, vpId) compact summary in place. See
|
|
603
603
|
* `replaceCompactSummary` for the rationale — same reason, scoped file.
|
|
604
604
|
*
|
|
605
|
-
* @param {string}
|
|
605
|
+
* @param {string} sessionId
|
|
606
606
|
* @param {string} vpId
|
|
607
607
|
* @param {string} summary
|
|
608
608
|
*/
|
|
609
|
-
replaceCompactSummaryFor(
|
|
609
|
+
replaceCompactSummaryFor(sessionId, vpId, summary) {
|
|
610
610
|
if (typeof summary !== 'string' || !summary) return;
|
|
611
|
-
const path = this.#scopedCompactPath(
|
|
611
|
+
const path = this.#scopedCompactPath(sessionId, vpId);
|
|
612
612
|
if (!path) return;
|
|
613
613
|
try {
|
|
614
614
|
writeFileSync(path, summary, { encoding: 'utf8', mode: 0o644 });
|
|
@@ -625,22 +625,22 @@ export class ConversationStore {
|
|
|
625
625
|
}
|
|
626
626
|
|
|
627
627
|
/**
|
|
628
|
-
* Check whether ANY per-(group, vp) compact summary exists for `
|
|
628
|
+
* Check whether ANY per-(group, vp) compact summary exists for `sessionId`.
|
|
629
629
|
* Used by the history-replay path to decide whether to flag
|
|
630
630
|
* `hasCompactSummary` for the UI without committing to one VP's view.
|
|
631
631
|
*
|
|
632
|
-
* @param {string}
|
|
632
|
+
* @param {string} sessionId
|
|
633
633
|
* @returns {boolean}
|
|
634
634
|
*/
|
|
635
|
-
hasAnyCompactSummaryForGroup(
|
|
636
|
-
if (!
|
|
637
|
-
const compactDir = join(this.#groupConversationDir(
|
|
635
|
+
hasAnyCompactSummaryForGroup(sessionId) {
|
|
636
|
+
if (!sessionId) return false;
|
|
637
|
+
const compactDir = join(this.#groupConversationDir(sessionId), 'compact');
|
|
638
638
|
for (const dir of [compactDir, this.#legacyCompactScopedDir]) {
|
|
639
639
|
if (!existsSync(dir)) continue;
|
|
640
640
|
try {
|
|
641
641
|
for (const f of readdirSync(dir)) {
|
|
642
642
|
if (dir === compactDir && f.endsWith('.md')) return true;
|
|
643
|
-
if (dir === this.#legacyCompactScopedDir && f.startsWith(`${this.#safeIdComponent(
|
|
643
|
+
if (dir === this.#legacyCompactScopedDir && f.startsWith(`${this.#safeIdComponent(sessionId)}__`) && f.endsWith('.md')) return true;
|
|
644
644
|
}
|
|
645
645
|
} catch { /* best-effort */ }
|
|
646
646
|
}
|
|
@@ -755,11 +755,11 @@ export class ConversationStore {
|
|
|
755
755
|
}
|
|
756
756
|
|
|
757
757
|
/**
|
|
758
|
-
* Load recent hot messages stamped with `
|
|
758
|
+
* Load recent hot messages stamped with `sessionId`, sliced to the last
|
|
759
759
|
* `turnsLimit` TURNS and sorted chronologically.
|
|
760
760
|
*
|
|
761
761
|
* Group-history-isolation (Bug 7): a message lives in exactly one
|
|
762
|
-
* group. Messages without a `
|
|
762
|
+
* group. Messages without a `sessionId` frontmatter (legacy / pre-
|
|
763
763
|
* grouping) are NOT returned — they would otherwise leak into every
|
|
764
764
|
* group's stream.
|
|
765
765
|
*
|
|
@@ -782,26 +782,26 @@ export class ConversationStore {
|
|
|
782
782
|
* typical inboxes (≤ a few thousand hot messages) this is cheap; if
|
|
783
783
|
* it ever becomes a hot path we add a per-group on-disk index.
|
|
784
784
|
*
|
|
785
|
-
* @param {string}
|
|
785
|
+
* @param {string} sessionId — required; null/empty returns []
|
|
786
786
|
* @param {number} [turnsLimit=DEFAULT_RECENT_TURNS]
|
|
787
787
|
* @returns {object[]}
|
|
788
788
|
*/
|
|
789
|
-
loadRecentByGroup(
|
|
790
|
-
if (!
|
|
791
|
-
const all = this.#loadGroupMessages(
|
|
792
|
-
const filtered = all.filter(m => m && m.
|
|
789
|
+
loadRecentByGroup(sessionId, turnsLimit = DEFAULT_RECENT_TURNS) {
|
|
790
|
+
if (!sessionId) return [];
|
|
791
|
+
const all = this.#loadGroupMessages(sessionId)
|
|
792
|
+
const filtered = all.filter(m => m && m.sessionId === sessionId);
|
|
793
793
|
if (turnsLimit === Infinity || turnsLimit < 0) return pairSanitize(filtered);
|
|
794
794
|
return pairSanitize(sliceLastNTurns(filtered, turnsLimit));
|
|
795
795
|
}
|
|
796
796
|
|
|
797
797
|
/**
|
|
798
|
-
* Load every hot message stamped with `
|
|
798
|
+
* Load every hot message stamped with `sessionId`.
|
|
799
799
|
*
|
|
800
|
-
* @param {string}
|
|
800
|
+
* @param {string} sessionId
|
|
801
801
|
* @returns {object[]}
|
|
802
802
|
*/
|
|
803
|
-
loadAllByGroup(
|
|
804
|
-
return this.loadRecentByGroup(
|
|
803
|
+
loadAllByGroup(sessionId) {
|
|
804
|
+
return this.loadRecentByGroup(sessionId, Infinity);
|
|
805
805
|
}
|
|
806
806
|
|
|
807
807
|
/**
|
|
@@ -825,16 +825,16 @@ export class ConversationStore {
|
|
|
825
825
|
* The output is pair-safe by construction for THIS VP's tool arcs and
|
|
826
826
|
* carries only summary-relevant text for the other VPs.
|
|
827
827
|
*
|
|
828
|
-
* @param {string}
|
|
828
|
+
* @param {string} sessionId
|
|
829
829
|
* @param {string} vpId
|
|
830
830
|
* @returns {object[]}
|
|
831
831
|
*/
|
|
832
|
-
loadGroupHistoryForVp(
|
|
833
|
-
if (!
|
|
834
|
-
const all = this.#loadGroupMessages(
|
|
832
|
+
loadGroupHistoryForVp(sessionId, vpId) {
|
|
833
|
+
if (!sessionId || !vpId) return [];
|
|
834
|
+
const all = this.#loadGroupMessages(sessionId)
|
|
835
835
|
const out = [];
|
|
836
836
|
for (const m of all) {
|
|
837
|
-
if (!m || m.
|
|
837
|
+
if (!m || m.sessionId !== sessionId) continue;
|
|
838
838
|
if (m._reflection || m.internal || m.systemOnly || m.systemOnlyMessage) continue;
|
|
839
839
|
if (m.role === 'user') {
|
|
840
840
|
out.push(m);
|
|
@@ -871,7 +871,7 @@ export class ConversationStore {
|
|
|
871
871
|
|
|
872
872
|
/**
|
|
873
873
|
* Pagination-cursor read: load the page of `turnsLimit` TURNS that ends
|
|
874
|
-
* just before `beforeSeq` (exclusive) for the given `
|
|
874
|
+
* just before `beforeSeq` (exclusive) for the given `sessionId`. Used by
|
|
875
875
|
* the Yeaft "Load older messages" UI to walk backwards through history
|
|
876
876
|
* one click at a time.
|
|
877
877
|
*
|
|
@@ -889,7 +889,7 @@ export class ConversationStore {
|
|
|
889
889
|
* are already pair-safe, but historical / hand-edited stores may
|
|
890
890
|
* contain orphan tool_use/tool_result pairs.
|
|
891
891
|
*
|
|
892
|
-
* @param {string}
|
|
892
|
+
* @param {string} sessionId — required; null/empty returns empty result
|
|
893
893
|
* @param {number|null} beforeSeq — exclusive upper bound on message
|
|
894
894
|
* sequence id. Special cases:
|
|
895
895
|
* - `null` / `undefined` / non-finite (e.g. `Infinity`, `NaN`) → start
|
|
@@ -901,14 +901,14 @@ export class ConversationStore {
|
|
|
901
901
|
* @param {number} [turnsLimit=DEFAULT_RECENT_TURNS] — max turns per page
|
|
902
902
|
* @returns {{ messages: object[], oldestSeq: number|null, hasMore: boolean }}
|
|
903
903
|
*/
|
|
904
|
-
loadOlderByGroup(
|
|
905
|
-
if (!
|
|
906
|
-
const hot = this.#loadGroupHotMessages(
|
|
907
|
-
const cold = this.#loadGroupColdMessages(
|
|
904
|
+
loadOlderByGroup(sessionId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
|
|
905
|
+
if (!sessionId) return { messages: [], oldestSeq: null, hasMore: false };
|
|
906
|
+
const hot = this.#loadGroupHotMessages(sessionId);
|
|
907
|
+
const cold = this.#loadGroupColdMessages(sessionId);
|
|
908
908
|
// Cold ids strictly < hot ids by construction → chronological concat.
|
|
909
909
|
const all = [...cold, ...hot];
|
|
910
910
|
const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
|
|
911
|
-
const prefix = all.filter(m => m && m.
|
|
911
|
+
const prefix = all.filter(m => m && m.sessionId === sessionId
|
|
912
912
|
&& parseSeqFromId(m.id) < cutoff);
|
|
913
913
|
if (prefix.length === 0) return { messages: [], oldestSeq: null, hasMore: false };
|
|
914
914
|
const sliced = pairSanitize(sliceLastNTurns(prefix, turnsLimit));
|
|
@@ -935,17 +935,17 @@ export class ConversationStore {
|
|
|
935
935
|
* window, so a dense run of hidden metadata cannot force the first screen to
|
|
936
936
|
* scan and materialize the group's entire history in the web bridge.
|
|
937
937
|
*
|
|
938
|
-
* @param {string}
|
|
938
|
+
* @param {string} sessionId
|
|
939
939
|
* @param {number|null} beforeSeq — exclusive upper bound, or null for newest
|
|
940
940
|
* @param {number} [turnsLimit=DEFAULT_RECENT_TURNS]
|
|
941
941
|
* @returns {{ messages: object[], oldestSeq: number|null, hasMore: boolean }}
|
|
942
942
|
*/
|
|
943
|
-
loadVisibleByGroup(
|
|
944
|
-
if (!
|
|
943
|
+
loadVisibleByGroup(sessionId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
|
|
944
|
+
if (!sessionId || !(turnsLimit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
|
|
945
945
|
|
|
946
946
|
const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
|
|
947
|
-
const hot = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('messages',
|
|
948
|
-
const cold = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('cold',
|
|
947
|
+
const hot = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('messages', sessionId), this.#legacyMsgDir], sessionId, cutoff);
|
|
948
|
+
const cold = this.#loadVisibleFromDirsByGroup([...this.#groupMessageDirs('cold', sessionId), this.#legacyColdDir], sessionId, cutoff);
|
|
949
949
|
const visible = [...cold, ...hot];
|
|
950
950
|
if (visible.length === 0) return { messages: [], oldestSeq: null, hasMore: false };
|
|
951
951
|
|
|
@@ -1018,9 +1018,9 @@ export class ConversationStore {
|
|
|
1018
1018
|
// ─── Internal ───────────────────────────────────────────
|
|
1019
1019
|
|
|
1020
1020
|
/**
|
|
1021
|
-
* Delete every persisted message stamped with `
|
|
1021
|
+
* Delete every persisted message stamped with `sessionId`. Scans both hot
|
|
1022
1022
|
* (`messages/`) and cold (`cold/`) directories and `unlink`s matching
|
|
1023
|
-
* files. Messages without a `
|
|
1023
|
+
* files. Messages without a `sessionId` frontmatter are NOT touched —
|
|
1024
1024
|
* they may be legitimate pre-grouping legacy messages and are handled
|
|
1025
1025
|
* by `compactOrphans` instead.
|
|
1026
1026
|
*
|
|
@@ -1031,11 +1031,11 @@ export class ConversationStore {
|
|
|
1031
1031
|
* Idempotent and safe: missing dirs / unparseable files are skipped.
|
|
1032
1032
|
* Returns the number of message files removed.
|
|
1033
1033
|
*
|
|
1034
|
-
* @param {string}
|
|
1034
|
+
* @param {string} sessionId
|
|
1035
1035
|
* @returns {number}
|
|
1036
1036
|
*/
|
|
1037
|
-
deleteByGroup(
|
|
1038
|
-
if (!
|
|
1037
|
+
deleteByGroup(sessionId) {
|
|
1038
|
+
if (!sessionId) return 0;
|
|
1039
1039
|
let removed = 0;
|
|
1040
1040
|
for (const dir of [this.#chatMsgDir, this.#chatColdDir, ...this.#groupMessageDirs('messages'), ...this.#groupMessageDirs('cold'), this.#legacyMsgDir, this.#legacyColdDir]) {
|
|
1041
1041
|
if (!existsSync(dir)) continue;
|
|
@@ -1056,7 +1056,7 @@ export class ConversationStore {
|
|
|
1056
1056
|
throw err;
|
|
1057
1057
|
}
|
|
1058
1058
|
const msg = parseMessage(raw);
|
|
1059
|
-
if (!msg || msg.
|
|
1059
|
+
if (!msg || msg.sessionId !== sessionId) continue;
|
|
1060
1060
|
try {
|
|
1061
1061
|
unlinkSync(path);
|
|
1062
1062
|
removed += 1;
|
|
@@ -1073,7 +1073,7 @@ export class ConversationStore {
|
|
|
1073
1073
|
|
|
1074
1074
|
/**
|
|
1075
1075
|
* Sweep messages that don't belong to any live group. A message is
|
|
1076
|
-
* considered an orphan when its frontmatter `
|
|
1076
|
+
* considered an orphan when its frontmatter `sessionId`:
|
|
1077
1077
|
* - is missing entirely (legacy / pre-grouping); OR
|
|
1078
1078
|
* - is set to a value not in `keepGroupIds`.
|
|
1079
1079
|
*
|
|
@@ -1116,7 +1116,7 @@ export class ConversationStore {
|
|
|
1116
1116
|
const msg = parseMessage(raw);
|
|
1117
1117
|
if (!msg) continue;
|
|
1118
1118
|
scanned += 1;
|
|
1119
|
-
const isOrphan = !msg.
|
|
1119
|
+
const isOrphan = !msg.sessionId || !keep.has(msg.sessionId);
|
|
1120
1120
|
if (!isOrphan) continue;
|
|
1121
1121
|
orphans.push(path);
|
|
1122
1122
|
if (dryRun) continue;
|
|
@@ -1357,8 +1357,8 @@ export class ConversationStore {
|
|
|
1357
1357
|
|
|
1358
1358
|
#messageDirFor(msg) {
|
|
1359
1359
|
if (msg?.chatId) return join(this.#chatConversationDir(msg.chatId, { create: true }), 'messages');
|
|
1360
|
-
if (!msg?.
|
|
1361
|
-
return join(this.#groupConversationDir(msg.
|
|
1360
|
+
if (!msg?.sessionId) return this.#chatMsgDir;
|
|
1361
|
+
return join(this.#groupConversationDir(msg.sessionId, { create: true }), 'messages');
|
|
1362
1362
|
}
|
|
1363
1363
|
|
|
1364
1364
|
#chatConversationDir(chatId, { create = false } = {}) {
|
|
@@ -1456,8 +1456,8 @@ export class ConversationStore {
|
|
|
1456
1456
|
return pairSanitize(out);
|
|
1457
1457
|
}
|
|
1458
1458
|
|
|
1459
|
-
#groupConversationDir(
|
|
1460
|
-
const dir = join(this.#groupsDir, this.#safeDirComponent(
|
|
1459
|
+
#groupConversationDir(sessionId, { create = false } = {}) {
|
|
1460
|
+
const dir = join(this.#groupsDir, this.#safeDirComponent(sessionId), 'conversation');
|
|
1461
1461
|
if (create) this.#ensureConversationDirs(dir);
|
|
1462
1462
|
return dir;
|
|
1463
1463
|
}
|
|
@@ -1472,22 +1472,22 @@ export class ConversationStore {
|
|
|
1472
1472
|
if (!existsSync(this.#groupsDir)) return [];
|
|
1473
1473
|
const dirs = [];
|
|
1474
1474
|
for (const name of readdirSync(this.#groupsDir)) {
|
|
1475
|
-
const
|
|
1475
|
+
const sessionDir = join(this.#groupsDir, name);
|
|
1476
1476
|
try {
|
|
1477
|
-
if (!statSync(
|
|
1477
|
+
if (!statSync(sessionDir).isDirectory()) continue;
|
|
1478
1478
|
} catch (err) {
|
|
1479
1479
|
if (isPermissionError(err)) continue;
|
|
1480
1480
|
throw err;
|
|
1481
1481
|
}
|
|
1482
|
-
const conversationDir = join(
|
|
1482
|
+
const conversationDir = join(sessionDir, 'conversation');
|
|
1483
1483
|
if (existsSync(conversationDir)) dirs.push(conversationDir);
|
|
1484
1484
|
}
|
|
1485
1485
|
return dirs;
|
|
1486
1486
|
}
|
|
1487
1487
|
|
|
1488
|
-
#groupMessageDirs(kind,
|
|
1489
|
-
if (
|
|
1490
|
-
const dir = join(this.#groupConversationDir(
|
|
1488
|
+
#groupMessageDirs(kind, sessionId = null) {
|
|
1489
|
+
if (sessionId) {
|
|
1490
|
+
const dir = join(this.#groupConversationDir(sessionId), kind);
|
|
1491
1491
|
return existsSync(dir) ? [dir] : [];
|
|
1492
1492
|
}
|
|
1493
1493
|
return this.#groupConversationDirs()
|
|
@@ -1507,29 +1507,29 @@ export class ConversationStore {
|
|
|
1507
1507
|
#loadChatMessages() {
|
|
1508
1508
|
// Legacy ~/.yeaft/conversation held both chat and group records. For chat
|
|
1509
1509
|
// mode compatibility, only import legacy records that are not stamped with
|
|
1510
|
-
// a
|
|
1510
|
+
// a sessionId, so group mode cannot bleed into chat.
|
|
1511
1511
|
return [
|
|
1512
|
-
...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => !m?.
|
|
1512
|
+
...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => !m?.sessionId),
|
|
1513
1513
|
...this.#loadFromDir(this.#chatMsgDir, Infinity),
|
|
1514
1514
|
].sort(compareMessagesBySeq);
|
|
1515
1515
|
}
|
|
1516
1516
|
|
|
1517
|
-
#loadGroupHotMessages(
|
|
1517
|
+
#loadGroupHotMessages(sessionId = null) {
|
|
1518
1518
|
return [
|
|
1519
|
-
...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => m?.
|
|
1520
|
-
...this.#groupMessageDirs('messages',
|
|
1519
|
+
...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => m?.sessionId),
|
|
1520
|
+
...this.#groupMessageDirs('messages', sessionId).flatMap(dir => this.#loadFromDir(dir, Infinity)),
|
|
1521
1521
|
].sort(compareMessagesBySeq);
|
|
1522
1522
|
}
|
|
1523
1523
|
|
|
1524
|
-
#loadGroupColdMessages(
|
|
1524
|
+
#loadGroupColdMessages(sessionId = null) {
|
|
1525
1525
|
return [
|
|
1526
|
-
...this.#loadFromDir(this.#legacyColdDir, Infinity).filter(m => m?.
|
|
1527
|
-
...this.#groupMessageDirs('cold',
|
|
1526
|
+
...this.#loadFromDir(this.#legacyColdDir, Infinity).filter(m => m?.sessionId),
|
|
1527
|
+
...this.#groupMessageDirs('cold', sessionId).flatMap(dir => this.#loadFromDir(dir, Infinity)),
|
|
1528
1528
|
].sort(compareMessagesBySeq);
|
|
1529
1529
|
}
|
|
1530
1530
|
|
|
1531
|
-
#loadGroupMessages(
|
|
1532
|
-
return [...this.#loadGroupColdMessages(
|
|
1531
|
+
#loadGroupMessages(sessionId = null) {
|
|
1532
|
+
return [...this.#loadGroupColdMessages(sessionId), ...this.#loadGroupHotMessages(sessionId)].sort(compareMessagesBySeq);
|
|
1533
1533
|
}
|
|
1534
1534
|
|
|
1535
1535
|
#loadAllMessages() {
|
|
@@ -1556,8 +1556,8 @@ export class ConversationStore {
|
|
|
1556
1556
|
return total;
|
|
1557
1557
|
}
|
|
1558
1558
|
|
|
1559
|
-
#loadVisibleFromDirsByGroup(dirs,
|
|
1560
|
-
return dirs.flatMap(dir => this.#loadVisibleFromDirByGroup(dir,
|
|
1559
|
+
#loadVisibleFromDirsByGroup(dirs, sessionId, beforeSeq) {
|
|
1560
|
+
return dirs.flatMap(dir => this.#loadVisibleFromDirByGroup(dir, sessionId, beforeSeq))
|
|
1561
1561
|
.sort(compareMessagesBySeq);
|
|
1562
1562
|
}
|
|
1563
1563
|
|
|
@@ -1619,7 +1619,7 @@ export class ConversationStore {
|
|
|
1619
1619
|
return messages;
|
|
1620
1620
|
}
|
|
1621
1621
|
|
|
1622
|
-
#loadVisibleFromDirByGroup(dir,
|
|
1622
|
+
#loadVisibleFromDirByGroup(dir, sessionId, beforeSeq) {
|
|
1623
1623
|
if (!existsSync(dir)) return [];
|
|
1624
1624
|
|
|
1625
1625
|
const files = readdirSync(dir)
|
|
@@ -1632,11 +1632,11 @@ export class ConversationStore {
|
|
|
1632
1632
|
if (!Number.isFinite(seq) || seq >= beforeSeq) continue;
|
|
1633
1633
|
|
|
1634
1634
|
const raw = readFileSync(join(dir, file), 'utf8');
|
|
1635
|
-
if (!raw.includes(`
|
|
1635
|
+
if (!raw.includes(`sessionId: ${sessionId}`)) continue;
|
|
1636
1636
|
if (!raw.includes('role: user') && !raw.includes('role: assistant')) continue;
|
|
1637
1637
|
|
|
1638
1638
|
const parsed = parseMessage(raw);
|
|
1639
|
-
if (!parsed || parsed.
|
|
1639
|
+
if (!parsed || parsed.sessionId !== sessionId) continue;
|
|
1640
1640
|
if (parsed._reflection || parsed.internal || parsed.systemOnly || parsed.systemOnlyMessage) continue;
|
|
1641
1641
|
if (parsed.role !== 'user' && parsed.role !== 'assistant') continue;
|
|
1642
1642
|
out.push(parsed);
|
package/yeaft/debug-trace.js
CHANGED
|
@@ -209,16 +209,16 @@ export class DebugTrace {
|
|
|
209
209
|
|
|
210
210
|
/**
|
|
211
211
|
* Start a new turn.
|
|
212
|
-
* @param {{ traceId: string, messageId?: string, mode?: string, turnNumber?: number,
|
|
212
|
+
* @param {{ traceId: string, messageId?: string, mode?: string, turnNumber?: number, sessionId?: string, vpId?: string, threadId?: string, userPrompt?: string }} opts
|
|
213
213
|
* @returns {string} — turnId
|
|
214
214
|
*/
|
|
215
|
-
startTurn({ traceId, messageId = null, mode = null, turnNumber = null,
|
|
215
|
+
startTurn({ traceId, messageId = null, mode = null, turnNumber = null, sessionId = null, vpId = null, threadId = null, userPrompt = null }) {
|
|
216
216
|
const id = randomUUID();
|
|
217
217
|
const now = Date.now();
|
|
218
218
|
this.#prepare('insertTurn', `
|
|
219
219
|
INSERT INTO trace_turns (id, trace_id, message_id, mode, turn_number, started_at, group_id, vp_id, thread_id, user_prompt)
|
|
220
220
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
221
|
-
`).run(id, traceId, messageId, mode, turnNumber, now,
|
|
221
|
+
`).run(id, traceId, messageId, mode, turnNumber, now, sessionId, vpId, threadId, truncate(userPrompt, MAX_LOOP_PAYLOAD));
|
|
222
222
|
return id;
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -406,17 +406,17 @@ export class DebugTrace {
|
|
|
406
406
|
* fields the panel expects. JSON columns are parsed; truncated /
|
|
407
407
|
* malformed payloads degrade to null instead of failing the call.
|
|
408
408
|
*
|
|
409
|
-
* @param {{ limit?: number, dreamLimit?: number,
|
|
409
|
+
* @param {{ limit?: number, dreamLimit?: number, sessionId?: string|null, threadId?: string|null }} [opts]
|
|
410
410
|
* @returns {{ loops: object[], turns: object[], dreamEvents: object[] }}
|
|
411
411
|
*/
|
|
412
|
-
fetchRecentDebugHistory({ limit = 100, dreamLimit = 5,
|
|
412
|
+
fetchRecentDebugHistory({ limit = 100, dreamLimit = 5, sessionId = null, threadId = null } = {}) {
|
|
413
413
|
const lim = Math.max(1, Math.min(500, Number(limit) || 100));
|
|
414
414
|
const dreamLim = Number.isFinite(Number(dreamLimit))
|
|
415
415
|
? Math.max(0, Math.min(50, Number(dreamLimit)))
|
|
416
416
|
: 5;
|
|
417
417
|
const where = [];
|
|
418
418
|
const args = [];
|
|
419
|
-
if (
|
|
419
|
+
if (sessionId) { where.push('group_id = ?'); args.push(sessionId); }
|
|
420
420
|
if (threadId) { where.push('thread_id = ?'); args.push(threadId); }
|
|
421
421
|
const sql = `
|
|
422
422
|
SELECT * FROM trace_turns
|
|
@@ -437,7 +437,7 @@ export class DebugTrace {
|
|
|
437
437
|
try { return JSON.parse(s); }
|
|
438
438
|
catch { return null; }
|
|
439
439
|
};
|
|
440
|
-
// Group rows by (turnId, threadId,
|
|
440
|
+
// Group rows by (turnId, threadId, sessionId, vpId) → frontend Turn
|
|
441
441
|
// record. Each row is also surfaced as a Loop.
|
|
442
442
|
const turnsById = new Map();
|
|
443
443
|
const loops = rows.map((r) => {
|
|
@@ -461,7 +461,7 @@ export class DebugTrace {
|
|
|
461
461
|
stopReason: r.stop_reason || null,
|
|
462
462
|
rawRequest: r.raw_request || null,
|
|
463
463
|
rawResponse: r.raw_response || null,
|
|
464
|
-
|
|
464
|
+
sessionId: r.group_id || null,
|
|
465
465
|
vpId: r.vp_id || null,
|
|
466
466
|
threadId: r.thread_id || null,
|
|
467
467
|
};
|
|
@@ -474,7 +474,7 @@ export class DebugTrace {
|
|
|
474
474
|
// cumulative conversation snapshot, so `messages[0].content`
|
|
475
475
|
// would be turn-1's prompt for every subsequent turn header.
|
|
476
476
|
userPrompt: r.user_prompt || '',
|
|
477
|
-
|
|
477
|
+
sessionId: r.group_id || null,
|
|
478
478
|
vpId: r.vp_id || null,
|
|
479
479
|
threadId: r.thread_id || null,
|
|
480
480
|
openedAt: r.started_at || 0,
|
|
@@ -525,11 +525,11 @@ export class DebugTrace {
|
|
|
525
525
|
`).all(Math.max(dreamLim * 5, dreamLim));
|
|
526
526
|
for (const er of eventRows) {
|
|
527
527
|
const data = parseJsonSafe(er.event_data) || {};
|
|
528
|
-
const evtGroupId = typeof data.
|
|
528
|
+
const evtGroupId = typeof data.sessionId === 'string' && data.sessionId ? data.sessionId : null;
|
|
529
529
|
const target = typeof data.target === 'string' ? data.target : '';
|
|
530
|
-
if (
|
|
530
|
+
if (sessionId) {
|
|
531
531
|
const isBroadcast = !evtGroupId && !target;
|
|
532
|
-
const isThisGroup = evtGroupId ===
|
|
532
|
+
const isThisGroup = evtGroupId === sessionId || target === `group/${sessionId}`;
|
|
533
533
|
if (!isBroadcast && !isThisGroup) continue;
|
|
534
534
|
}
|
|
535
535
|
dreamEvents.push({
|
package/yeaft/dream-v2/apply.js
CHANGED
|
@@ -36,13 +36,13 @@ function applySystem(language) {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Build the UPDATE prompt body. Accepts the current scope state +
|
|
39
|
-
* one or more `(
|
|
39
|
+
* one or more `(sessionId, diff)` source blocks.
|
|
40
40
|
*
|
|
41
41
|
* @param {{
|
|
42
42
|
* target: string,
|
|
43
43
|
* memoryMd: string,
|
|
44
44
|
* summaryMd: string,
|
|
45
|
-
* sources: Array<{
|
|
45
|
+
* sources: Array<{ sessionId: string, diff: Array<object> }>,
|
|
46
46
|
* batchInfo?: { index: number, total: number },
|
|
47
47
|
* }} ctx
|
|
48
48
|
*/
|
|
@@ -68,7 +68,7 @@ export function buildUpdatePrompt(ctx) {
|
|
|
68
68
|
*
|
|
69
69
|
* @param {{
|
|
70
70
|
* target: string,
|
|
71
|
-
* sources: Array<{
|
|
71
|
+
* sources: Array<{ sessionId: string, diff: Array<object> }>,
|
|
72
72
|
* siblingTopics?: Array<{ path: string, summary: string }>,
|
|
73
73
|
* }} ctx
|
|
74
74
|
*/
|
|
@@ -89,17 +89,17 @@ export function buildCreatePrompt(ctx) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Render a list of `(
|
|
92
|
+
* Render a list of `(sessionId, diff)` source blocks for inclusion in the
|
|
93
93
|
* update / create prompts. Single-source aware: omits leading blank line
|
|
94
94
|
* if there's only one source, to keep small prompts compact.
|
|
95
95
|
*
|
|
96
|
-
* @param {Array<{
|
|
96
|
+
* @param {Array<{ sessionId: string, diff: Array<object> }>} sources
|
|
97
97
|
*/
|
|
98
98
|
function renderSourceBlocks(sources, language) {
|
|
99
99
|
const out = [];
|
|
100
100
|
for (const src of (sources || [])) {
|
|
101
101
|
out.push('');
|
|
102
|
-
out.push(`[group/${src.
|
|
102
|
+
out.push(`[group/${src.sessionId}]`);
|
|
103
103
|
for (const m of (src.diff || [])) {
|
|
104
104
|
const head = `[${m.role || 'message'}${m.kind === 'overlap' ? (String(language || '').toLowerCase().startsWith('zh') ? '(已处理)' : ' (already processed)') : ''}]`;
|
|
105
105
|
out.push(head);
|
|
@@ -128,19 +128,19 @@ export function targetToScope(target) {
|
|
|
128
128
|
if (segs.length === 2) return { kind: 'group', id: segs[1] };
|
|
129
129
|
// group/<g>/user
|
|
130
130
|
if (segs.length === 3 && segs[2] === 'user') {
|
|
131
|
-
return { kind: 'group-user',
|
|
131
|
+
return { kind: 'group-user', sessionId: segs[1] };
|
|
132
132
|
}
|
|
133
133
|
// group/<g>/vp/<v>
|
|
134
134
|
if (segs.length === 4 && segs[2] === 'vp') {
|
|
135
|
-
return { kind: 'group-vp',
|
|
135
|
+
return { kind: 'group-vp', sessionId: segs[1], id: segs[3] };
|
|
136
136
|
}
|
|
137
137
|
// group/<g>/feature/<f>
|
|
138
138
|
if (segs.length === 4 && segs[2] === 'feature') {
|
|
139
|
-
return { kind: 'group-feature',
|
|
139
|
+
return { kind: 'group-feature', sessionId: segs[1], id: segs[3] };
|
|
140
140
|
}
|
|
141
141
|
// group/<g>/topic/<l1>[/<l2>]
|
|
142
142
|
if (segs[2] === 'topic' && (segs.length === 4 || segs.length === 5)) {
|
|
143
|
-
return { kind: 'group-topic',
|
|
143
|
+
return { kind: 'group-topic', sessionId: segs[1], path: segs.slice(3) };
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
if (segs[0] === 'chat') {
|
|
@@ -159,7 +159,7 @@ export function targetToScope(target) {
|
|
|
159
159
|
* @param {{
|
|
160
160
|
* target: string,
|
|
161
161
|
* kind: 'update'|'create',
|
|
162
|
-
* sources: Array<{
|
|
162
|
+
* sources: Array<{ sessionId: string, diff: any }>,
|
|
163
163
|
* }} merged
|
|
164
164
|
* @param {{
|
|
165
165
|
* root: string,
|
|
@@ -276,10 +276,10 @@ function scopeRelDir(scope) {
|
|
|
276
276
|
switch (scope.kind) {
|
|
277
277
|
case 'user': return 'user';
|
|
278
278
|
case 'group': return `group/${scope.id}`;
|
|
279
|
-
case 'group-user': return `group/${scope.
|
|
280
|
-
case 'group-vp': return `group/${scope.
|
|
281
|
-
case 'group-feature': return `group/${scope.
|
|
282
|
-
case 'group-topic': return `group/${scope.
|
|
279
|
+
case 'group-user': return `group/${scope.sessionId}/user`;
|
|
280
|
+
case 'group-vp': return `group/${scope.sessionId}/vp/${scope.id}`;
|
|
281
|
+
case 'group-feature': return `group/${scope.sessionId}/feature/${scope.id}`;
|
|
282
|
+
case 'group-topic': return `group/${scope.sessionId}/topic/${scope.path.join('/')}`;
|
|
283
283
|
case 'chat': return `chat/${scope.id}`;
|
|
284
284
|
case 'chat-vp': return `chat/${scope.chatId}/vp/${scope.id}`;
|
|
285
285
|
default: throw new Error(`apply.scopeRelDir: unknown kind ${scope.kind}`);
|