@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.
- package/bin/yeaft-stats.js +3 -3
- package/connection/message-router.js +73 -44
- package/context.js +4 -4
- package/expert-roles.js +1 -1
- package/index.js +1 -1
- package/package.json +4 -4
- package/service/index.js +1 -1
- package/{unify → yeaft}/attachments.js +7 -7
- package/{unify → yeaft}/cli.js +4 -4
- package/{unify → yeaft}/compact/compactor.js +10 -10
- package/{unify → yeaft}/config-api.js +12 -12
- package/{unify → yeaft}/config.js +19 -19
- package/{unify → yeaft}/conversation/migrate-messages-threadid.js +1 -1
- package/{unify → yeaft}/conversation/persist.js +194 -65
- package/{unify → yeaft}/conversation/search.js +28 -23
- package/{unify → yeaft}/debug-trace.js +1 -1
- package/{unify → yeaft}/dream-v2/limits.js +3 -3
- package/{unify → yeaft}/dream-v2/runner.js +1 -1
- package/{unify → yeaft}/dream-v2/session-wiring.js +2 -2
- package/{unify → yeaft}/effort.js +2 -2
- package/{unify → yeaft}/engine.js +8 -8
- package/{unify → yeaft}/eval/run-eval.js +11 -11
- package/{unify → yeaft}/eval/runner.js +1 -1
- package/{unify → yeaft}/groups/feature-flag.js +2 -2
- package/{unify → yeaft}/groups/index.js +2 -2
- package/{unify → yeaft}/groups/pre-flow.js +1 -1
- package/{unify → yeaft}/groups/project-doc.js +1 -1
- package/{unify → yeaft}/history-compact.js +3 -3
- package/{unify → yeaft}/index.js +1 -1
- package/{unify → yeaft}/init.js +15 -9
- package/{unify → yeaft}/llm/adapter.js +1 -1
- package/{unify → yeaft}/llm/anthropic.js +2 -2
- package/{unify → yeaft}/llm/models-dev.js +10 -3
- package/{unify → yeaft}/llm/openai-responses.js +2 -2
- package/{unify → yeaft}/llm/router.js +1 -1
- package/{unify → yeaft}/mcp.js +1 -1
- package/{unify → yeaft}/memory/ams-registry.js +1 -1
- package/{unify → yeaft}/memory/seed-backfill.js +1 -1
- package/{unify → yeaft}/memory/store-v2.js +1 -1
- package/{unify → yeaft}/models.js +3 -3
- package/{unify → yeaft}/pair-sanitize.js +2 -2
- package/{unify → yeaft}/personas.js +1 -1
- package/{unify → yeaft}/prompts.js +1 -1
- package/{unify → yeaft}/routing/index.js +1 -1
- package/{unify → yeaft}/routing/router.js +1 -1
- package/{unify → yeaft}/session.js +8 -8
- package/{unify → yeaft}/skills.js +2 -2
- package/{unify → yeaft}/stats/format.js +1 -1
- package/{unify → yeaft}/stop-hooks.js +1 -1
- package/{unify → yeaft}/sub-agent/runner.js +1 -1
- package/{unify → yeaft}/tools/ask-user.js +2 -2
- package/{unify → yeaft}/tools/enter-worktree.js +1 -1
- package/{unify → yeaft}/tools/exit-worktree.js +1 -1
- package/{unify → yeaft}/tools/mcp-tools.js +1 -1
- package/{unify → yeaft}/tools/registry.js +2 -2
- package/{unify → yeaft}/tools/skill.js +1 -1
- package/{unify → yeaft}/tools/start-plan.js +1 -1
- package/{unify → yeaft}/tools/types.js +3 -3
- package/{unify → yeaft}/tools/web-search.js +1 -1
- package/{unify → yeaft}/turn-utils.js +1 -1
- package/{unify → yeaft}/vp/index.js +1 -1
- package/{unify → yeaft}/vp/registry.js +1 -1
- package/{unify → yeaft}/vp/vp-bridge.js +8 -8
- package/{unify → yeaft}/web-bridge.js +230 -230
- /package/{unify → yeaft}/archive/tool-results.js +0 -0
- /package/{unify → yeaft}/archive/trace.js +0 -0
- /package/{unify → yeaft}/archive/turn-archive.js +0 -0
- /package/{unify → yeaft}/compact/decisions-log.js +0 -0
- /package/{unify → yeaft}/compact/orchestrator.js +0 -0
- /package/{unify → yeaft}/compact/triggers.js +0 -0
- /package/{unify → yeaft}/compact/turn-group.js +0 -0
- /package/{unify → yeaft}/dream-v2/apply.js +0 -0
- /package/{unify → yeaft}/dream-v2/merge.js +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/create.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/extract-group.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/extract-topic.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/extract-user.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/extract-vp.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/index.js +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/summarize-scope.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/triage-pass1.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/triage-pass2.md +0 -0
- /package/{unify → yeaft}/dream-v2/prompts/update.md +0 -0
- /package/{unify → yeaft}/dream-v2/schedule.js +0 -0
- /package/{unify → yeaft}/dream-v2/segment.js +0 -0
- /package/{unify → yeaft}/dream-v2/snapshot.js +0 -0
- /package/{unify → yeaft}/dream-v2/state.js +0 -0
- /package/{unify → yeaft}/dream-v2/triage.js +0 -0
- /package/{unify → yeaft}/eval/cases/e2e.js +0 -0
- /package/{unify → yeaft}/eval/cases/memory.js +0 -0
- /package/{unify → yeaft}/eval/cases/skills.js +0 -0
- /package/{unify → yeaft}/eval/cases/tool-use.js +0 -0
- /package/{unify → yeaft}/groups/coordinator.js +0 -0
- /package/{unify → yeaft}/groups/group-config.js +0 -0
- /package/{unify → yeaft}/groups/group-crud.js +0 -0
- /package/{unify → yeaft}/groups/group-store.js +0 -0
- /package/{unify → yeaft}/groups/ids.js +0 -0
- /package/{unify → yeaft}/groups/roster.js +0 -0
- /package/{unify → yeaft}/groups/seed-default.js +0 -0
- /package/{unify → yeaft}/llm/credentials/github-copilot.js +0 -0
- /package/{unify → yeaft}/llm/credentials/index.js +0 -0
- /package/{unify → yeaft}/memory/adjust.js +0 -0
- /package/{unify → yeaft}/memory/ams.js +0 -0
- /package/{unify → yeaft}/memory/budget.js +0 -0
- /package/{unify → yeaft}/memory/consolidate.js +0 -0
- /package/{unify → yeaft}/memory/index-db.js +0 -0
- /package/{unify → yeaft}/memory/keywords.js +0 -0
- /package/{unify → yeaft}/memory/preflow.js +0 -0
- /package/{unify → yeaft}/memory/segment-store.js +0 -0
- /package/{unify → yeaft}/memory/segment-sync.js +0 -0
- /package/{unify → yeaft}/memory/segment.js +0 -0
- /package/{unify → yeaft}/memory/summary-store.js +0 -0
- /package/{unify → yeaft}/router/continuity.js +0 -0
- /package/{unify → yeaft}/router/thinking.js +0 -0
- /package/{unify → yeaft}/routing/loop-guard.js +0 -0
- /package/{unify → yeaft}/stats/tool-usage.js +0 -0
- /package/{unify → yeaft}/storage/atomic.js +0 -0
- /package/{unify → yeaft}/storage/compact.js +0 -0
- /package/{unify → yeaft}/storage/index.js +0 -0
- /package/{unify → yeaft}/storage/jsonl-index.js +0 -0
- /package/{unify → yeaft}/storage/jsonl-log.js +0 -0
- /package/{unify → yeaft}/storage/shard-index.js +0 -0
- /package/{unify → yeaft}/storage/shard-store.js +0 -0
- /package/{unify → yeaft}/sub-agent/spawned-prompt.js +0 -0
- /package/{unify → yeaft}/templates/base.md +0 -0
- /package/{unify → yeaft}/templates/common-rules.md +0 -0
- /package/{unify → yeaft}/templates/harness/router-handoff.md +0 -0
- /package/{unify → yeaft}/templates/harness/router-shape.md +0 -0
- /package/{unify → yeaft}/templates/harness/worker-shape.md +0 -0
- /package/{unify → yeaft}/templates/identity-yeaft.md +0 -0
- /package/{unify → yeaft}/templates/mode-dream.md +0 -0
- /package/{unify → yeaft}/templates/mode-unified.md +0 -0
- /package/{unify → yeaft}/templates/personas/explorer.md +0 -0
- /package/{unify → yeaft}/templates/personas/implementer.md +0 -0
- /package/{unify → yeaft}/templates/personas/researcher.md +0 -0
- /package/{unify → yeaft}/templates/personas/reviewer.md +0 -0
- /package/{unify → yeaft}/templates/plan-instruction.md +0 -0
- /package/{unify → yeaft}/templates/tool-guidance.md +0 -0
- /package/{unify → yeaft}/tool-folding/exec-log.js +0 -0
- /package/{unify → yeaft}/tool-folding/fallback-stub.js +0 -0
- /package/{unify → yeaft}/tool-folding/index.js +0 -0
- /package/{unify → yeaft}/tool-folding/reflection-prompt.js +0 -0
- /package/{unify → yeaft}/tool-folding/t1-reflector.js +0 -0
- /package/{unify → yeaft}/tool-folding/t2-reflector.js +0 -0
- /package/{unify → yeaft}/tools/agent.js +0 -0
- /package/{unify → yeaft}/tools/apply-patch.js +0 -0
- /package/{unify → yeaft}/tools/bash.js +0 -0
- /package/{unify → yeaft}/tools/close-agent.js +0 -0
- /package/{unify → yeaft}/tools/file-edit.js +0 -0
- /package/{unify → yeaft}/tools/file-read.js +0 -0
- /package/{unify → yeaft}/tools/file-write.js +0 -0
- /package/{unify → yeaft}/tools/glob.js +0 -0
- /package/{unify → yeaft}/tools/grep.js +0 -0
- /package/{unify → yeaft}/tools/history-search.js +0 -0
- /package/{unify → yeaft}/tools/image-generation.js +0 -0
- /package/{unify → yeaft}/tools/index.js +0 -0
- /package/{unify → yeaft}/tools/js-repl.js +0 -0
- /package/{unify → yeaft}/tools/list-agents.js +0 -0
- /package/{unify → yeaft}/tools/list-dir.js +0 -0
- /package/{unify → yeaft}/tools/notebook-edit.js +0 -0
- /package/{unify → yeaft}/tools/route-forward.js +0 -0
- /package/{unify → yeaft}/tools/send-message.js +0 -0
- /package/{unify → yeaft}/tools/todo-write.js +0 -0
- /package/{unify → yeaft}/tools/view-image.js +0 -0
- /package/{unify → yeaft}/tools/wait-agent.js +0 -0
- /package/{unify → yeaft}/tools/web-fetch.js +0 -0
- /package/{unify → yeaft}/vp/seed-defaults.js +0 -0
- /package/{unify → yeaft}/vp/seed-topup.js +0 -0
- /package/{unify → yeaft}/vp/stock-ids.js +0 -0
- /package/{unify → yeaft}/vp/thread-classifier.js +0 -0
- /package/{unify → yeaft}/vp/vp-crud.js +0 -0
- /package/{unify → yeaft}/vp/vp-loader.js +0 -0
- /package/{unify → yeaft}/vp/vp-store.js +0 -0
- /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:
|
|
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
|
-
|
|
44
|
-
|
|
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 `
|
|
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 `
|
|
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 `
|
|
161
|
+
* bounds via `clampYeaftField`.
|
|
162
162
|
*
|
|
163
|
-
* @param {any} raw — jsonConfig.
|
|
163
|
+
* @param {any} raw — jsonConfig.yeaft (may be undefined / malformed)
|
|
164
164
|
* @returns {{ maxConcurrentThreads: number, autoArchiveIdleDays: number }}
|
|
165
165
|
*/
|
|
166
|
-
export function
|
|
166
|
+
export function normaliseYeaftSection(raw) {
|
|
167
167
|
const out = {
|
|
168
|
-
maxConcurrentThreads: DEFAULTS.
|
|
169
|
-
autoArchiveIdleDays: DEFAULTS.
|
|
168
|
+
maxConcurrentThreads: DEFAULTS.yeaftMaxConcurrentThreads,
|
|
169
|
+
autoArchiveIdleDays: DEFAULTS.yeaftAutoArchiveIdleDays,
|
|
170
170
|
};
|
|
171
171
|
if (!raw || typeof raw !== 'object') return out;
|
|
172
|
-
const mc =
|
|
172
|
+
const mc = clampYeaftField(raw.maxConcurrentThreads, 'maxConcurrentThreads');
|
|
173
173
|
if (mc !== null) out.maxConcurrentThreads = mc;
|
|
174
|
-
const ad =
|
|
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
|
|
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
|
-
* (`
|
|
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
|
|
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 `
|
|
228
|
-
|
|
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:
|
|
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
|
-
|
|
328
|
+
yeaft: normaliseYeaftSection(jsonConfig.yeaft),
|
|
329
329
|
|
|
330
330
|
// Legacy fields (null when using config.json)
|
|
331
331
|
apiKey: overrides.apiKey || null,
|
|
@@ -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/
|
|
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-
|
|
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
|
-
*
|
|
319
|
-
* index.md
|
|
320
|
-
* compact.md
|
|
321
|
-
* messages/
|
|
322
|
-
* cold/
|
|
323
|
-
* blobs/
|
|
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
|
-
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
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.#
|
|
342
|
-
this.#
|
|
343
|
-
this.#
|
|
344
|
-
|
|
345
|
-
this.#
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this.#
|
|
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
|
-
|
|
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.#
|
|
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
|
|
424
|
-
|
|
425
|
-
|
|
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 (
|
|
488
|
-
return readFileSync(this.#
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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.#
|
|
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
|
-
|
|
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(
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
|
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.#
|
|
843
|
-
const cold = this.#
|
|
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.#
|
|
884
|
-
const cold = this.#
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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$/);
|