@yeaft/webchat-agent 0.1.854 → 0.1.856

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.
Files changed (174) hide show
  1. package/bin/yeaft-stats.js +3 -3
  2. package/connection/message-router.js +73 -44
  3. package/context.js +4 -4
  4. package/expert-roles.js +1 -1
  5. package/index.js +1 -1
  6. package/package.json +4 -4
  7. package/service/index.js +1 -1
  8. package/{unify → yeaft}/attachments.js +7 -7
  9. package/{unify → yeaft}/cli.js +4 -4
  10. package/{unify → yeaft}/compact/compactor.js +10 -10
  11. package/{unify → yeaft}/config-api.js +12 -12
  12. package/{unify → yeaft}/config.js +19 -19
  13. package/{unify → yeaft}/conversation/migrate-messages-threadid.js +1 -1
  14. package/{unify → yeaft}/conversation/persist.js +194 -65
  15. package/{unify → yeaft}/conversation/search.js +28 -23
  16. package/{unify → yeaft}/debug-trace.js +1 -1
  17. package/{unify → yeaft}/dream-v2/limits.js +3 -3
  18. package/{unify → yeaft}/dream-v2/runner.js +1 -1
  19. package/{unify → yeaft}/dream-v2/session-wiring.js +2 -2
  20. package/{unify → yeaft}/effort.js +2 -2
  21. package/{unify → yeaft}/engine.js +8 -8
  22. package/{unify → yeaft}/eval/run-eval.js +11 -11
  23. package/{unify → yeaft}/eval/runner.js +1 -1
  24. package/{unify → yeaft}/groups/feature-flag.js +2 -2
  25. package/{unify → yeaft}/groups/index.js +2 -2
  26. package/{unify → yeaft}/groups/pre-flow.js +1 -1
  27. package/{unify → yeaft}/groups/project-doc.js +1 -1
  28. package/{unify → yeaft}/history-compact.js +3 -3
  29. package/{unify → yeaft}/index.js +1 -1
  30. package/{unify → yeaft}/init.js +15 -9
  31. package/{unify → yeaft}/llm/adapter.js +1 -1
  32. package/{unify → yeaft}/llm/anthropic.js +2 -2
  33. package/{unify → yeaft}/llm/models-dev.js +10 -3
  34. package/{unify → yeaft}/llm/openai-responses.js +2 -2
  35. package/{unify → yeaft}/llm/router.js +1 -1
  36. package/{unify → yeaft}/mcp.js +1 -1
  37. package/{unify → yeaft}/memory/ams-registry.js +1 -1
  38. package/{unify → yeaft}/memory/seed-backfill.js +1 -1
  39. package/{unify → yeaft}/memory/store-v2.js +1 -1
  40. package/{unify → yeaft}/models.js +3 -3
  41. package/{unify → yeaft}/pair-sanitize.js +2 -2
  42. package/{unify → yeaft}/personas.js +1 -1
  43. package/{unify → yeaft}/prompts.js +1 -1
  44. package/{unify → yeaft}/routing/index.js +1 -1
  45. package/{unify → yeaft}/routing/router.js +1 -1
  46. package/{unify → yeaft}/session.js +8 -8
  47. package/{unify → yeaft}/skills.js +2 -2
  48. package/{unify → yeaft}/stats/format.js +1 -1
  49. package/{unify → yeaft}/stop-hooks.js +1 -1
  50. package/{unify → yeaft}/sub-agent/runner.js +1 -1
  51. package/{unify → yeaft}/tools/ask-user.js +2 -2
  52. package/{unify → yeaft}/tools/enter-worktree.js +1 -1
  53. package/{unify → yeaft}/tools/exit-worktree.js +1 -1
  54. package/{unify → yeaft}/tools/mcp-tools.js +1 -1
  55. package/{unify → yeaft}/tools/registry.js +2 -2
  56. package/{unify → yeaft}/tools/skill.js +1 -1
  57. package/{unify → yeaft}/tools/start-plan.js +1 -1
  58. package/{unify → yeaft}/tools/types.js +3 -3
  59. package/{unify → yeaft}/tools/web-search.js +1 -1
  60. package/{unify → yeaft}/turn-utils.js +1 -1
  61. package/{unify → yeaft}/vp/index.js +1 -1
  62. package/{unify → yeaft}/vp/registry.js +1 -1
  63. package/{unify → yeaft}/vp/vp-bridge.js +8 -8
  64. package/{unify → yeaft}/web-bridge.js +230 -230
  65. /package/{unify → yeaft}/archive/tool-results.js +0 -0
  66. /package/{unify → yeaft}/archive/trace.js +0 -0
  67. /package/{unify → yeaft}/archive/turn-archive.js +0 -0
  68. /package/{unify → yeaft}/compact/decisions-log.js +0 -0
  69. /package/{unify → yeaft}/compact/orchestrator.js +0 -0
  70. /package/{unify → yeaft}/compact/triggers.js +0 -0
  71. /package/{unify → yeaft}/compact/turn-group.js +0 -0
  72. /package/{unify → yeaft}/dream-v2/apply.js +0 -0
  73. /package/{unify → yeaft}/dream-v2/merge.js +0 -0
  74. /package/{unify → yeaft}/dream-v2/prompts/create.md +0 -0
  75. /package/{unify → yeaft}/dream-v2/prompts/extract-group.md +0 -0
  76. /package/{unify → yeaft}/dream-v2/prompts/extract-topic.md +0 -0
  77. /package/{unify → yeaft}/dream-v2/prompts/extract-user.md +0 -0
  78. /package/{unify → yeaft}/dream-v2/prompts/extract-vp.md +0 -0
  79. /package/{unify → yeaft}/dream-v2/prompts/index.js +0 -0
  80. /package/{unify → yeaft}/dream-v2/prompts/summarize-scope.md +0 -0
  81. /package/{unify → yeaft}/dream-v2/prompts/triage-pass1.md +0 -0
  82. /package/{unify → yeaft}/dream-v2/prompts/triage-pass2.md +0 -0
  83. /package/{unify → yeaft}/dream-v2/prompts/update.md +0 -0
  84. /package/{unify → yeaft}/dream-v2/schedule.js +0 -0
  85. /package/{unify → yeaft}/dream-v2/segment.js +0 -0
  86. /package/{unify → yeaft}/dream-v2/snapshot.js +0 -0
  87. /package/{unify → yeaft}/dream-v2/state.js +0 -0
  88. /package/{unify → yeaft}/dream-v2/triage.js +0 -0
  89. /package/{unify → yeaft}/eval/cases/e2e.js +0 -0
  90. /package/{unify → yeaft}/eval/cases/memory.js +0 -0
  91. /package/{unify → yeaft}/eval/cases/skills.js +0 -0
  92. /package/{unify → yeaft}/eval/cases/tool-use.js +0 -0
  93. /package/{unify → yeaft}/groups/coordinator.js +0 -0
  94. /package/{unify → yeaft}/groups/group-config.js +0 -0
  95. /package/{unify → yeaft}/groups/group-crud.js +0 -0
  96. /package/{unify → yeaft}/groups/group-store.js +0 -0
  97. /package/{unify → yeaft}/groups/ids.js +0 -0
  98. /package/{unify → yeaft}/groups/roster.js +0 -0
  99. /package/{unify → yeaft}/groups/seed-default.js +0 -0
  100. /package/{unify → yeaft}/llm/credentials/github-copilot.js +0 -0
  101. /package/{unify → yeaft}/llm/credentials/index.js +0 -0
  102. /package/{unify → yeaft}/memory/adjust.js +0 -0
  103. /package/{unify → yeaft}/memory/ams.js +0 -0
  104. /package/{unify → yeaft}/memory/budget.js +0 -0
  105. /package/{unify → yeaft}/memory/consolidate.js +0 -0
  106. /package/{unify → yeaft}/memory/index-db.js +0 -0
  107. /package/{unify → yeaft}/memory/keywords.js +0 -0
  108. /package/{unify → yeaft}/memory/preflow.js +0 -0
  109. /package/{unify → yeaft}/memory/segment-store.js +0 -0
  110. /package/{unify → yeaft}/memory/segment-sync.js +0 -0
  111. /package/{unify → yeaft}/memory/segment.js +0 -0
  112. /package/{unify → yeaft}/memory/summary-store.js +0 -0
  113. /package/{unify → yeaft}/router/continuity.js +0 -0
  114. /package/{unify → yeaft}/router/thinking.js +0 -0
  115. /package/{unify → yeaft}/routing/loop-guard.js +0 -0
  116. /package/{unify → yeaft}/stats/tool-usage.js +0 -0
  117. /package/{unify → yeaft}/storage/atomic.js +0 -0
  118. /package/{unify → yeaft}/storage/compact.js +0 -0
  119. /package/{unify → yeaft}/storage/index.js +0 -0
  120. /package/{unify → yeaft}/storage/jsonl-index.js +0 -0
  121. /package/{unify → yeaft}/storage/jsonl-log.js +0 -0
  122. /package/{unify → yeaft}/storage/shard-index.js +0 -0
  123. /package/{unify → yeaft}/storage/shard-store.js +0 -0
  124. /package/{unify → yeaft}/sub-agent/spawned-prompt.js +0 -0
  125. /package/{unify → yeaft}/templates/base.md +0 -0
  126. /package/{unify → yeaft}/templates/common-rules.md +0 -0
  127. /package/{unify → yeaft}/templates/harness/router-handoff.md +0 -0
  128. /package/{unify → yeaft}/templates/harness/router-shape.md +0 -0
  129. /package/{unify → yeaft}/templates/harness/worker-shape.md +0 -0
  130. /package/{unify → yeaft}/templates/identity-yeaft.md +0 -0
  131. /package/{unify → yeaft}/templates/mode-dream.md +0 -0
  132. /package/{unify → yeaft}/templates/mode-unified.md +0 -0
  133. /package/{unify → yeaft}/templates/personas/explorer.md +0 -0
  134. /package/{unify → yeaft}/templates/personas/implementer.md +0 -0
  135. /package/{unify → yeaft}/templates/personas/researcher.md +0 -0
  136. /package/{unify → yeaft}/templates/personas/reviewer.md +0 -0
  137. /package/{unify → yeaft}/templates/plan-instruction.md +0 -0
  138. /package/{unify → yeaft}/templates/tool-guidance.md +0 -0
  139. /package/{unify → yeaft}/tool-folding/exec-log.js +0 -0
  140. /package/{unify → yeaft}/tool-folding/fallback-stub.js +0 -0
  141. /package/{unify → yeaft}/tool-folding/index.js +0 -0
  142. /package/{unify → yeaft}/tool-folding/reflection-prompt.js +0 -0
  143. /package/{unify → yeaft}/tool-folding/t1-reflector.js +0 -0
  144. /package/{unify → yeaft}/tool-folding/t2-reflector.js +0 -0
  145. /package/{unify → yeaft}/tools/agent.js +0 -0
  146. /package/{unify → yeaft}/tools/apply-patch.js +0 -0
  147. /package/{unify → yeaft}/tools/bash.js +0 -0
  148. /package/{unify → yeaft}/tools/close-agent.js +0 -0
  149. /package/{unify → yeaft}/tools/file-edit.js +0 -0
  150. /package/{unify → yeaft}/tools/file-read.js +0 -0
  151. /package/{unify → yeaft}/tools/file-write.js +0 -0
  152. /package/{unify → yeaft}/tools/glob.js +0 -0
  153. /package/{unify → yeaft}/tools/grep.js +0 -0
  154. /package/{unify → yeaft}/tools/history-search.js +0 -0
  155. /package/{unify → yeaft}/tools/image-generation.js +0 -0
  156. /package/{unify → yeaft}/tools/index.js +0 -0
  157. /package/{unify → yeaft}/tools/js-repl.js +0 -0
  158. /package/{unify → yeaft}/tools/list-agents.js +0 -0
  159. /package/{unify → yeaft}/tools/list-dir.js +0 -0
  160. /package/{unify → yeaft}/tools/notebook-edit.js +0 -0
  161. /package/{unify → yeaft}/tools/route-forward.js +0 -0
  162. /package/{unify → yeaft}/tools/send-message.js +0 -0
  163. /package/{unify → yeaft}/tools/todo-write.js +0 -0
  164. /package/{unify → yeaft}/tools/view-image.js +0 -0
  165. /package/{unify → yeaft}/tools/wait-agent.js +0 -0
  166. /package/{unify → yeaft}/tools/web-fetch.js +0 -0
  167. /package/{unify → yeaft}/vp/seed-defaults.js +0 -0
  168. /package/{unify → yeaft}/vp/seed-topup.js +0 -0
  169. /package/{unify → yeaft}/vp/stock-ids.js +0 -0
  170. /package/{unify → yeaft}/vp/thread-classifier.js +0 -0
  171. /package/{unify → yeaft}/vp/vp-crud.js +0 -0
  172. /package/{unify → yeaft}/vp/vp-loader.js +0 -0
  173. /package/{unify → yeaft}/vp/vp-store.js +0 -0
  174. /package/{unify → yeaft}/vp-status-broker.js +0 -0
@@ -33,15 +33,15 @@ const DEFAULTS = {
33
33
  maxOutputTokens: 16384,
34
34
  messageTokenBudget: 32768,
35
35
  maxContinueTurns: 3,
36
- // task-318: Unify-specific runtime caps. `maxConcurrentThreads` gates
36
+ // task-318: Yeaft-specific runtime caps. `maxConcurrentThreads` gates
37
37
  // how many ThreadEngineRegistry instances may be live at once; dispatch
38
38
  // refuses new threads beyond this. The count INCLUDES the always-on
39
39
  // `main` thread, so the default 6 gives users 5 user-initiated threads
40
40
  // on top of main. `autoArchiveIdleDays` is consumed by ThreadStore's
41
41
  // idle-archive pass (task-317 will wire the pass itself; here we just
42
42
  // plumb the knob through so the UI can set it).
43
- unifyMaxConcurrentThreads: 6,
44
- unifyAutoArchiveIdleDays: 30,
43
+ yeaftMaxConcurrentThreads: 6,
44
+ yeaftAutoArchiveIdleDays: 30,
45
45
  // CLAUDE.md / AGENTS.md project-doc cap, in bytes. Mirrors Codex's
46
46
  // `project_doc_max_bytes`. 0 disables the feature (no project-doc
47
47
  // block is injected). Hand-edited values are NOT clamped — we let
@@ -149,44 +149,44 @@ function isTruthy(val) {
149
149
  }
150
150
 
151
151
  /**
152
- * Clamp and normalise the `unify` config section. Missing / non-numeric
152
+ * Clamp and normalise the `yeaft` config section. Missing / non-numeric
153
153
  * inputs fall back to the DEFAULTS; numeric-but-out-of-range inputs are
154
154
  * *clamped* to the valid range rather than silently reverting to the
155
- * default. This keeps the read path aligned with `updateUnifySettings`,
155
+ * default. This keeps the read path aligned with `updateYeaftSettings`,
156
156
  * which rejects out-of-range writes up front — a hand-edited
157
157
  * `config.json` with `maxConcurrentThreads: 100` now loads as 50 (the
158
158
  * nearest valid value) instead of collapsing back to 5.
159
159
  *
160
160
  * Exported for tests and for `config-api.js` to share the same clamp
161
- * bounds via `clampUnifyField`.
161
+ * bounds via `clampYeaftField`.
162
162
  *
163
- * @param {any} raw — jsonConfig.unify (may be undefined / malformed)
163
+ * @param {any} raw — jsonConfig.yeaft (may be undefined / malformed)
164
164
  * @returns {{ maxConcurrentThreads: number, autoArchiveIdleDays: number }}
165
165
  */
166
- export function normaliseUnifySection(raw) {
166
+ export function normaliseYeaftSection(raw) {
167
167
  const out = {
168
- maxConcurrentThreads: DEFAULTS.unifyMaxConcurrentThreads,
169
- autoArchiveIdleDays: DEFAULTS.unifyAutoArchiveIdleDays,
168
+ maxConcurrentThreads: DEFAULTS.yeaftMaxConcurrentThreads,
169
+ autoArchiveIdleDays: DEFAULTS.yeaftAutoArchiveIdleDays,
170
170
  };
171
171
  if (!raw || typeof raw !== 'object') return out;
172
- const mc = clampUnifyField(raw.maxConcurrentThreads, 'maxConcurrentThreads');
172
+ const mc = clampYeaftField(raw.maxConcurrentThreads, 'maxConcurrentThreads');
173
173
  if (mc !== null) out.maxConcurrentThreads = mc;
174
- const ad = clampUnifyField(raw.autoArchiveIdleDays, 'autoArchiveIdleDays');
174
+ const ad = clampYeaftField(raw.autoArchiveIdleDays, 'autoArchiveIdleDays');
175
175
  if (ad !== null) out.autoArchiveIdleDays = ad;
176
176
  return out;
177
177
  }
178
178
 
179
179
  /**
180
- * Clamp a single unify field to its valid range. Returns `null` if the
180
+ * Clamp a single yeaft field to its valid range. Returns `null` if the
181
181
  * input is not a finite number (so callers can treat it as "not set" and
182
182
  * keep the previous value). Centralises the bounds used on both read
183
- * (`normaliseUnifySection`) and write (`updateUnifySettings` validation).
183
+ * (`normaliseYeaftSection`) and write (`updateYeaftSettings` validation).
184
184
  *
185
185
  * @param {unknown} v
186
186
  * @param {'maxConcurrentThreads'|'autoArchiveIdleDays'} field
187
187
  * @returns {number | null}
188
188
  */
189
- export function clampUnifyField(v, field) {
189
+ export function clampYeaftField(v, field) {
190
190
  if (v === null || v === undefined) return null;
191
191
  const n = Number(v);
192
192
  if (!Number.isFinite(n)) return null;
@@ -224,8 +224,8 @@ function loadLegacyConfig(dir, overrides) {
224
224
  // has no field for this, so it always lands on the default unless
225
225
  // explicitly overridden via CLI.
226
226
  projectDocMaxBytes: overrides.projectDocMaxBytes ?? fileConfig.projectDocMaxBytes ?? DEFAULTS.projectDocMaxBytes,
227
- // task-318: legacy path never had the `unify` section — defaults.
228
- unify: normaliseUnifySection(null),
227
+ // task-318: legacy path never had the `yeaft` section — defaults.
228
+ yeaft: normaliseYeaftSection(null),
229
229
  providers: null,
230
230
  primaryModel: null,
231
231
  fastModel: null,
@@ -323,9 +323,9 @@ export function loadConfig(overrides = {}) {
323
323
  // ~/.yeaft/config.json or threaded through CLI overrides.
324
324
  projectDocMaxBytes: overrides.projectDocMaxBytes ?? jsonConfig.projectDocMaxBytes ?? DEFAULTS.projectDocMaxBytes,
325
325
 
326
- // task-318: Unify runtime caps. `unify` is a nested section so we
326
+ // task-318: Yeaft runtime caps. `yeaft` is a nested section so we
327
327
  // don't pollute the flat config namespace used by chat/crew code.
328
- unify: normaliseUnifySection(jsonConfig.unify),
328
+ yeaft: normaliseYeaftSection(jsonConfig.yeaft),
329
329
 
330
330
  // Legacy fields (null when using config.json)
331
331
  apiKey: overrides.apiKey || null,
@@ -23,7 +23,7 @@
23
23
  * // { ran: true, migrated: 42, skipped: 3 }
24
24
  *
25
25
  * Usage (CLI):
26
- * node agent/unify/conversation/migrate-messages-threadid.js [yeaftDir]
26
+ * node agent/yeaft/conversation/migrate-messages-threadid.js [yeaftDir]
27
27
  */
28
28
 
29
29
  import {
@@ -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/conversation/messages/. Design: zero JSON, all Markdown.
5
+ * ~/.yeaft/chat/messages/ or ~/.yeaft/group/messages/. Design: zero JSON, all Markdown.
6
6
  *
7
7
  * Message format:
8
8
  * ---
@@ -15,7 +15,7 @@
15
15
  * ---
16
16
  * Message content here...
17
17
  *
18
- * Reference: yeaft-unify-core-systems.md §4.1, yeaft-unify-brainstorm-v5.1.md
18
+ * Reference: yeaft-yeaft-core-systems.md §4.1, yeaft-yeaft-brainstorm-v5.1.md
19
19
  */
20
20
 
21
21
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync, unlinkSync } from 'fs';
@@ -69,6 +69,13 @@ export function parseSeqFromId(id) {
69
69
  return m ? parseInt(m[1], 10) : NaN;
70
70
  }
71
71
 
72
+ function compareMessagesBySeq(a, b) {
73
+ const sa = parseSeqFromId(a?.id);
74
+ const sb = parseSeqFromId(b?.id);
75
+ if (Number.isFinite(sa) && Number.isFinite(sb) && sa !== sb) return sa - sb;
76
+ return String(a?.time || '').localeCompare(String(b?.time || ''));
77
+ }
78
+
72
79
  // ─── Frontmatter helpers ─────────────────────────────────────
73
80
 
74
81
  /**
@@ -315,22 +322,43 @@ export function parseMessage(raw) {
315
322
  * ConversationStore — persist and load messages to/from disk.
316
323
  *
317
324
  * Directory layout:
318
- * conversation/
319
- * index.md — message index with frontmatter
320
- * compact.md — cumulative compact summary
321
- * messages/ — hot messages (mNNNN.md)
322
- * cold/ — archived messages (moved from messages/)
323
- * blobs/ — attachments (never moved)
325
+ * chat/ — one-to-one chat mode history
326
+ * index.md
327
+ * compact.md
328
+ * messages/
329
+ * cold/
330
+ * blobs/
331
+ * group/ — group mode history
332
+ * index.md
333
+ * compact.md
334
+ * messages/
335
+ * cold/
336
+ * blobs/
337
+ *
338
+ * Legacy compatibility: ~/.yeaft/conversation is read as an old mixed store.
339
+ * New writes are split by mode: records with groupId go to group/, all others
340
+ * go to chat/.
324
341
  */
325
342
  export class ConversationStore {
326
343
  #dir; // root dir (e.g. ~/.yeaft)
327
- #convDir; // ~/.yeaft/conversation
328
- #msgDir; // ~/.yeaft/conversation/messages
329
- #coldDir; // ~/.yeaft/conversation/cold
330
- #indexPath; // ~/.yeaft/conversation/index.md
331
- #compactPath; // ~/.yeaft/conversation/compact.md
332
- #compactScopedDir; // ~/.yeaft/conversation/compact/ (per-(group,vp))
333
- #nextSeq; // next message sequence number (global, legacy)
344
+ #chatDir; // ~/.yeaft/chat
345
+ #groupDir; // ~/.yeaft/group
346
+ #legacyConvDir; // ~/.yeaft/conversation (read-only compatibility)
347
+ #convDir; // default thread dir root: ~/.yeaft/chat
348
+ #msgDir; // default hot messages dir: ~/.yeaft/chat/messages
349
+ #coldDir; // default cold messages dir: ~/.yeaft/chat/cold
350
+ #indexPath; // ~/.yeaft/chat/index.md
351
+ #compactPath; // ~/.yeaft/chat/compact.md
352
+ #compactScopedDir; // ~/.yeaft/group/compact/ (per-(group,vp))
353
+ #legacyCompactPath;
354
+ #legacyCompactScopedDir;
355
+ #chatMsgDir;
356
+ #chatColdDir;
357
+ #groupMsgDir;
358
+ #groupColdDir;
359
+ #legacyMsgDir;
360
+ #legacyColdDir;
361
+ #nextSeq; // next message sequence number across chat/group/legacy
334
362
  #nextSeqByThread; // Map<threadId, number> — per-thread counters (task-314)
335
363
 
336
364
  /**
@@ -338,20 +366,37 @@ export class ConversationStore {
338
366
  */
339
367
  constructor(dir) {
340
368
  this.#dir = dir;
341
- this.#convDir = join(dir, 'conversation');
342
- this.#msgDir = join(dir, 'conversation', 'messages');
343
- this.#coldDir = join(dir, 'conversation', 'cold');
344
- this.#indexPath = join(dir, 'conversation', 'index.md');
345
- this.#compactPath = join(dir, 'conversation', 'compact.md');
346
- // Per-(groupId, vpId) compact summary files live here. The legacy
347
- // single-file `compact.md` above is kept for backward compatibility
348
- // and the "no groupId/vpId" fallback (sub-agents, legacy callers).
349
- this.#compactScopedDir = join(dir, 'conversation', 'compact');
369
+ this.#chatDir = join(dir, 'chat');
370
+ this.#groupDir = join(dir, 'group');
371
+ this.#legacyConvDir = join(dir, 'conversation');
372
+
373
+ this.#convDir = this.#chatDir;
374
+ this.#msgDir = join(this.#chatDir, 'messages');
375
+ this.#coldDir = join(this.#chatDir, 'cold');
376
+ this.#indexPath = join(this.#chatDir, 'index.md');
377
+ this.#compactPath = join(this.#chatDir, 'compact.md');
378
+ this.#legacyCompactPath = join(this.#legacyConvDir, 'compact.md');
379
+
380
+ this.#chatMsgDir = this.#msgDir;
381
+ this.#chatColdDir = this.#coldDir;
382
+ this.#groupMsgDir = join(this.#groupDir, 'messages');
383
+ this.#groupColdDir = join(this.#groupDir, 'cold');
384
+ this.#legacyMsgDir = join(this.#legacyConvDir, 'messages');
385
+ this.#legacyColdDir = join(this.#legacyConvDir, 'cold');
386
+
387
+ // Per-(groupId, vpId) compact summary files live in group mode. The legacy
388
+ // ~/.yeaft/conversation/compact directory is read for compatibility.
389
+ this.#compactScopedDir = join(this.#groupDir, 'compact');
390
+ this.#legacyCompactScopedDir = join(this.#legacyConvDir, 'compact');
350
391
  this.#nextSeq = null;
351
392
  this.#nextSeqByThread = new Map();
352
393
 
353
- // Ensure directories exist (graceful on permission errors)
354
- for (const d of [this.#convDir, this.#msgDir, this.#coldDir, this.#compactScopedDir]) {
394
+ // Ensure new chat/group directories exist (graceful on permission errors).
395
+ // The legacy conversation directory is never created by new versions.
396
+ for (const d of [
397
+ this.#chatDir, join(this.#chatDir, 'blobs'), this.#chatMsgDir, this.#chatColdDir,
398
+ this.#groupDir, join(this.#groupDir, 'blobs'), this.#groupMsgDir, this.#groupColdDir, this.#compactScopedDir,
399
+ ]) {
355
400
  try {
356
401
  if (!existsSync(d)) mkdirSync(d, { recursive: true, mode: 0o755 });
357
402
  } catch (err) {
@@ -385,7 +430,7 @@ export class ConversationStore {
385
430
  tokens_est: msg.tokens_est || estimateTokens(msg.content || ''),
386
431
  };
387
432
 
388
- const filePath = join(this.#msgDir, `${id}.md`);
433
+ const filePath = join(this.#messageDirFor(fullMsg), `${id}.md`);
389
434
  try {
390
435
  writeFileSync(filePath, serializeMessage(fullMsg), { encoding: 'utf8', mode: 0o644 });
391
436
  } catch (err) {
@@ -420,9 +465,10 @@ export class ConversationStore {
420
465
  * @param {string} id — message id (e.g. "m0355")
421
466
  */
422
467
  moveToCold(id) {
423
- const src = join(this.#msgDir, `${id}.md`);
424
- const dst = join(this.#coldDir, `${id}.md`);
425
- if (existsSync(src)) {
468
+ for (const [hotDir, coldDir] of this.#hotColdDirPairs({ includeLegacy: false })) {
469
+ const src = join(hotDir, `${id}.md`);
470
+ const dst = join(coldDir, `${id}.md`);
471
+ if (!existsSync(src)) continue;
426
472
  try {
427
473
  renameSync(src, dst);
428
474
  } catch (err) {
@@ -435,6 +481,7 @@ export class ConversationStore {
435
481
  throw err;
436
482
  }
437
483
  }
484
+ return;
438
485
  }
439
486
  }
440
487
 
@@ -484,8 +531,9 @@ export class ConversationStore {
484
531
  * @returns {string}
485
532
  */
486
533
  readCompactSummary() {
487
- if (!existsSync(this.#compactPath)) return '';
488
- return readFileSync(this.#compactPath, 'utf8');
534
+ if (existsSync(this.#compactPath)) return readFileSync(this.#compactPath, 'utf8');
535
+ if (existsSync(this.#legacyCompactPath)) return readFileSync(this.#legacyCompactPath, 'utf8');
536
+ return '';
489
537
  }
490
538
 
491
539
  /**
@@ -517,6 +565,11 @@ export class ConversationStore {
517
565
  return join(this.#compactScopedDir, `${this.#safeIdComponent(groupId)}__${this.#safeIdComponent(vpId)}.md`);
518
566
  }
519
567
 
568
+ #legacyScopedCompactPath(groupId, vpId) {
569
+ if (!groupId || !vpId) return null;
570
+ return join(this.#legacyCompactScopedDir, `${this.#safeIdComponent(groupId)}__${this.#safeIdComponent(vpId)}.md`);
571
+ }
572
+
520
573
  /**
521
574
  * Read a per-(groupId, vpId) compact summary. Returns '' if no summary
522
575
  * has been written yet. Falls back to nothing — callers that need the
@@ -534,10 +587,13 @@ export class ConversationStore {
534
587
  */
535
588
  readCompactSummaryFor(groupId, vpId) {
536
589
  const path = this.#scopedCompactPath(groupId, vpId);
537
- if (!path) return '';
538
- if (!existsSync(path)) return '';
539
- try { return readFileSync(path, 'utf8'); }
540
- catch { return ''; }
590
+ const legacyPath = this.#legacyScopedCompactPath(groupId, vpId);
591
+ for (const candidate of [path, legacyPath]) {
592
+ if (!candidate || !existsSync(candidate)) continue;
593
+ try { return readFileSync(candidate, 'utf8'); }
594
+ catch { return ''; }
595
+ }
596
+ return '';
541
597
  }
542
598
 
543
599
  /**
@@ -576,13 +632,15 @@ export class ConversationStore {
576
632
  */
577
633
  hasAnyCompactSummaryForGroup(groupId) {
578
634
  if (!groupId) return false;
579
- if (!existsSync(this.#compactScopedDir)) return false;
580
635
  const prefix = `${this.#safeIdComponent(groupId)}__`;
581
- try {
582
- for (const f of readdirSync(this.#compactScopedDir)) {
583
- if (f.startsWith(prefix) && f.endsWith('.md')) return true;
584
- }
585
- } catch { /* best-effort */ }
636
+ for (const dir of [this.#compactScopedDir, this.#legacyCompactScopedDir]) {
637
+ if (!existsSync(dir)) continue;
638
+ try {
639
+ for (const f of readdirSync(dir)) {
640
+ if (f.startsWith(prefix) && f.endsWith('.md')) return true;
641
+ }
642
+ } catch { /* best-effort */ }
643
+ }
586
644
  return false;
587
645
  }
588
646
 
@@ -628,7 +686,7 @@ export class ConversationStore {
628
686
  * Clear all messages (hot + cold + compact).
629
687
  */
630
688
  clear() {
631
- for (const dir of [this.#msgDir, this.#coldDir]) {
689
+ for (const dir of [this.#chatMsgDir, this.#chatColdDir, this.#groupMsgDir, this.#groupColdDir]) {
632
690
  if (existsSync(dir)) {
633
691
  for (const file of readdirSync(dir)) {
634
692
  if (file.endsWith('.md')) {
@@ -641,10 +699,12 @@ export class ConversationStore {
641
699
  }
642
700
  }
643
701
  }
644
- // Reset compact
645
- if (existsSync(this.#compactPath)) {
702
+ // Reset compact files in the new chat/group stores. Legacy
703
+ // ~/.yeaft/conversation is intentionally left untouched.
704
+ for (const path of [this.#compactPath]) {
705
+ if (!existsSync(path)) continue;
646
706
  try {
647
- writeFileSync(this.#compactPath, '', { encoding: 'utf8', mode: 0o644 });
707
+ writeFileSync(path, '', { encoding: 'utf8', mode: 0o644 });
648
708
  } catch (err) {
649
709
  if (!isPermissionError(err)) throw err;
650
710
  }
@@ -653,6 +713,7 @@ export class ConversationStore {
653
713
  this.updateIndex({ totalMessages: 0, lastMessageId: null });
654
714
  }
655
715
 
716
+
656
717
  // ─── Read API ───────────────────────────────────────────
657
718
 
658
719
  /**
@@ -676,7 +737,7 @@ export class ConversationStore {
676
737
  * @returns {object[]} — parsed message objects
677
738
  */
678
739
  loadRecent(turnsLimit = DEFAULT_RECENT_TURNS) {
679
- const all = this.#loadFromDir(this.#msgDir, Infinity);
740
+ const all = this.#loadChatMessages();
680
741
  if (turnsLimit === Infinity || turnsLimit < 0) return pairSanitize(all);
681
742
  return pairSanitize(sliceLastNTurns(all, turnsLimit));
682
743
  }
@@ -687,7 +748,7 @@ export class ConversationStore {
687
748
  * @returns {object[]}
688
749
  */
689
750
  loadAll() {
690
- return this.#loadFromDir(this.#msgDir, Infinity);
751
+ return this.#loadAllMessages();
691
752
  }
692
753
 
693
754
  /**
@@ -724,7 +785,7 @@ export class ConversationStore {
724
785
  */
725
786
  loadRecentByGroup(groupId, turnsLimit = DEFAULT_RECENT_TURNS) {
726
787
  if (!groupId) return [];
727
- const all = this.#loadFromDir(this.#msgDir, Infinity);
788
+ const all = this.#loadGroupMessages();
728
789
  const filtered = all.filter(m => m && m.groupId === groupId);
729
790
  if (turnsLimit === Infinity || turnsLimit < 0) return pairSanitize(filtered);
730
791
  return pairSanitize(sliceLastNTurns(filtered, turnsLimit));
@@ -767,7 +828,7 @@ export class ConversationStore {
767
828
  */
768
829
  loadGroupHistoryForVp(groupId, vpId) {
769
830
  if (!groupId || !vpId) return [];
770
- const all = this.#loadFromDir(this.#msgDir, Infinity);
831
+ const all = this.#loadGroupMessages();
771
832
  const out = [];
772
833
  for (const m of all) {
773
834
  if (!m || m.groupId !== groupId) continue;
@@ -808,7 +869,7 @@ export class ConversationStore {
808
869
  /**
809
870
  * Pagination-cursor read: load the page of `turnsLimit` TURNS that ends
810
871
  * just before `beforeSeq` (exclusive) for the given `groupId`. Used by
811
- * the Unify "Load older messages" UI to walk backwards through history
872
+ * the Yeaft "Load older messages" UI to walk backwards through history
812
873
  * one click at a time.
813
874
  *
814
875
  * Crucially, this scans BOTH hot (`messages/`) and cold (`cold/`) dirs
@@ -839,8 +900,8 @@ export class ConversationStore {
839
900
  */
840
901
  loadOlderByGroup(groupId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
841
902
  if (!groupId) return { messages: [], oldestSeq: null, hasMore: false };
842
- const hot = this.#loadFromDir(this.#msgDir, Infinity);
843
- const cold = this.#loadFromDir(this.#coldDir, Infinity);
903
+ const hot = this.#loadGroupHotMessages();
904
+ const cold = this.#loadGroupColdMessages();
844
905
  // Cold ids strictly < hot ids by construction → chronological concat.
845
906
  const all = [...cold, ...hot];
846
907
  const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
@@ -880,8 +941,8 @@ export class ConversationStore {
880
941
  if (!groupId || !(turnsLimit > 0)) return { messages: [], oldestSeq: null, hasMore: false };
881
942
 
882
943
  const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
883
- const hot = this.#loadVisibleFromDirByGroup(this.#msgDir, groupId, cutoff);
884
- const cold = this.#loadVisibleFromDirByGroup(this.#coldDir, groupId, cutoff);
944
+ const hot = this.#loadVisibleFromDirsByGroup([this.#groupMsgDir, this.#legacyMsgDir], groupId, cutoff);
945
+ const cold = this.#loadVisibleFromDirsByGroup([this.#groupColdDir, this.#legacyColdDir], groupId, cutoff);
885
946
  const visible = [...cold, ...hot];
886
947
  if (visible.length === 0) return { messages: [], oldestSeq: null, hasMore: false };
887
948
 
@@ -908,8 +969,7 @@ export class ConversationStore {
908
969
  * @returns {number}
909
970
  */
910
971
  countHot() {
911
- if (!existsSync(this.#msgDir)) return 0;
912
- return readdirSync(this.#msgDir).filter(f => f.endsWith('.md')).length;
972
+ return this.#countFilesInDirs([this.#chatMsgDir, this.#groupMsgDir, this.#legacyMsgDir]);
913
973
  }
914
974
 
915
975
  /**
@@ -918,8 +978,7 @@ export class ConversationStore {
918
978
  * @returns {number}
919
979
  */
920
980
  countCold() {
921
- if (!existsSync(this.#coldDir)) return 0;
922
- return readdirSync(this.#coldDir).filter(f => f.endsWith('.md')).length;
981
+ return this.#countFilesInDirs([this.#chatColdDir, this.#groupColdDir, this.#legacyColdDir]);
923
982
  }
924
983
 
925
984
  /**
@@ -975,7 +1034,7 @@ export class ConversationStore {
975
1034
  deleteByGroup(groupId) {
976
1035
  if (!groupId) return 0;
977
1036
  let removed = 0;
978
- for (const dir of [this.#msgDir, this.#coldDir]) {
1037
+ for (const dir of [this.#chatMsgDir, this.#chatColdDir, this.#groupMsgDir, this.#groupColdDir, this.#legacyMsgDir, this.#legacyColdDir]) {
979
1038
  if (!existsSync(dir)) continue;
980
1039
  let files;
981
1040
  try {
@@ -1033,7 +1092,7 @@ export class ConversationStore {
1033
1092
  let scanned = 0;
1034
1093
  let removed = 0;
1035
1094
  const orphans = [];
1036
- for (const dir of [this.#msgDir, this.#coldDir]) {
1095
+ for (const dir of [this.#chatMsgDir, this.#chatColdDir, this.#groupMsgDir, this.#groupColdDir, this.#legacyMsgDir, this.#legacyColdDir]) {
1037
1096
  if (!existsSync(dir)) continue;
1038
1097
  let files;
1039
1098
  try {
@@ -1087,7 +1146,7 @@ export class ConversationStore {
1087
1146
  reassignThread(sourceId, targetId) {
1088
1147
  if (!sourceId || !targetId || sourceId === targetId) return 0;
1089
1148
  let rewritten = 0;
1090
- for (const dir of [this.#msgDir, this.#coldDir]) {
1149
+ for (const dir of [this.#chatMsgDir, this.#chatColdDir, this.#groupMsgDir, this.#groupColdDir, this.#legacyMsgDir, this.#legacyColdDir]) {
1091
1150
  if (!existsSync(dir)) continue;
1092
1151
  let files;
1093
1152
  try {
@@ -1166,7 +1225,7 @@ export class ConversationStore {
1166
1225
 
1167
1226
  // Collect source-thread candidate files from both hot + cold dirs.
1168
1227
  const candidates = [];
1169
- for (const dir of [this.#coldDir, this.#msgDir]) {
1228
+ for (const dir of [this.#chatColdDir, this.#chatMsgDir, this.#groupColdDir, this.#groupMsgDir, this.#legacyColdDir, this.#legacyMsgDir]) {
1170
1229
  if (!existsSync(dir)) continue;
1171
1230
  let files;
1172
1231
  try {
@@ -1273,7 +1332,7 @@ export class ConversationStore {
1273
1332
  }
1274
1333
  // Legacy: messages live in the flat dir stamped with threadId.
1275
1334
  const collected = [];
1276
- for (const dir of [this.#coldDir, this.#msgDir]) {
1335
+ for (const dir of [this.#chatColdDir, this.#chatMsgDir, this.#groupColdDir, this.#groupMsgDir, this.#legacyColdDir, this.#legacyMsgDir]) {
1277
1336
  if (!existsSync(dir)) continue;
1278
1337
  for (const f of readdirSync(dir).filter(x => x.endsWith('.md'))) {
1279
1338
  try {
@@ -1293,6 +1352,76 @@ export class ConversationStore {
1293
1352
  return collected.map(x => x.msg);
1294
1353
  }
1295
1354
 
1355
+ #messageDirFor(msg) {
1356
+ return msg?.groupId ? this.#groupMsgDir : this.#chatMsgDir;
1357
+ }
1358
+
1359
+ #hotColdDirPairs({ includeLegacy = true } = {}) {
1360
+ const pairs = [
1361
+ [this.#chatMsgDir, this.#chatColdDir],
1362
+ [this.#groupMsgDir, this.#groupColdDir],
1363
+ ];
1364
+ if (includeLegacy) pairs.push([this.#legacyMsgDir, this.#legacyColdDir]);
1365
+ return pairs;
1366
+ }
1367
+
1368
+ #loadChatMessages() {
1369
+ // Legacy ~/.yeaft/conversation held both chat and group records. For chat
1370
+ // mode compatibility, only import legacy records that are not stamped with
1371
+ // a groupId, so group mode cannot bleed into chat.
1372
+ return [
1373
+ ...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => !m?.groupId),
1374
+ ...this.#loadFromDir(this.#chatMsgDir, Infinity),
1375
+ ].sort(compareMessagesBySeq);
1376
+ }
1377
+
1378
+ #loadGroupHotMessages() {
1379
+ return [
1380
+ ...this.#loadFromDir(this.#legacyMsgDir, Infinity).filter(m => m?.groupId),
1381
+ ...this.#loadFromDir(this.#groupMsgDir, Infinity),
1382
+ ].sort(compareMessagesBySeq);
1383
+ }
1384
+
1385
+ #loadGroupColdMessages() {
1386
+ return [
1387
+ ...this.#loadFromDir(this.#legacyColdDir, Infinity).filter(m => m?.groupId),
1388
+ ...this.#loadFromDir(this.#groupColdDir, Infinity),
1389
+ ].sort(compareMessagesBySeq);
1390
+ }
1391
+
1392
+ #loadGroupMessages() {
1393
+ return [...this.#loadGroupColdMessages(), ...this.#loadGroupHotMessages()].sort(compareMessagesBySeq);
1394
+ }
1395
+
1396
+ #loadAllMessages() {
1397
+ return [
1398
+ ...this.#loadFromDir(this.#legacyColdDir, Infinity),
1399
+ ...this.#loadFromDir(this.#legacyMsgDir, Infinity),
1400
+ ...this.#loadFromDir(this.#chatColdDir, Infinity),
1401
+ ...this.#loadFromDir(this.#chatMsgDir, Infinity),
1402
+ ...this.#loadFromDir(this.#groupColdDir, Infinity),
1403
+ ...this.#loadFromDir(this.#groupMsgDir, Infinity),
1404
+ ].sort(compareMessagesBySeq);
1405
+ }
1406
+
1407
+ #countFilesInDirs(dirs) {
1408
+ let total = 0;
1409
+ for (const dir of dirs) {
1410
+ if (!existsSync(dir)) continue;
1411
+ try {
1412
+ total += readdirSync(dir).filter(f => f.endsWith('.md')).length;
1413
+ } catch (err) {
1414
+ if (!isPermissionError(err)) throw err;
1415
+ }
1416
+ }
1417
+ return total;
1418
+ }
1419
+
1420
+ #loadVisibleFromDirsByGroup(dirs, groupId, beforeSeq) {
1421
+ return dirs.flatMap(dir => this.#loadVisibleFromDirByGroup(dir, groupId, beforeSeq))
1422
+ .sort(compareMessagesBySeq);
1423
+ }
1424
+
1296
1425
  // task-314: per-thread sub-directory for forked threads.
1297
1426
  #threadMsgDir(threadId) {
1298
1427
  return join(this.#convDir, 'threads', threadId, 'messages');
@@ -1385,7 +1514,7 @@ export class ConversationStore {
1385
1514
  if (this.#nextSeq != null) return this.#nextSeq;
1386
1515
 
1387
1516
  let maxSeq = 0;
1388
- for (const dir of [this.#msgDir, this.#coldDir]) {
1517
+ for (const dir of [this.#chatMsgDir, this.#chatColdDir, this.#groupMsgDir, this.#groupColdDir, this.#legacyMsgDir, this.#legacyColdDir]) {
1389
1518
  if (!existsSync(dir)) continue;
1390
1519
  for (const file of readdirSync(dir)) {
1391
1520
  const match = file.match(/^m(\d+)\.md$/);