volute 0.52.0 → 0.54.0
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/dist/{accept-5NK5XJB5.js → accept-RSDIVJL6.js} +3 -0
- package/dist/auto-upgrade-HEVZKPTF.js +46 -0
- package/dist/{backup-YVGV2ZNM.js → backup-QRNX6RK7.js} +10 -10
- package/dist/channels-CKCY64TD.js +203 -0
- package/dist/{chat-VAZ25KVT.js → chat-ZUGNL5GS.js} +5 -5
- package/dist/chunk-3LHUXY3E.js +102 -0
- package/dist/{chunk-SW2U6HRB.js → chunk-444RR3YF.js} +21 -4
- package/dist/{chunk-YVR6EHWL.js → chunk-56IT2ZXF.js} +160 -18
- package/dist/chunk-5RBEEAKW.js +58 -0
- package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
- package/dist/{chunk-G7A5LEVI.js → chunk-7XKPG66D.js} +5 -5
- package/dist/{chunk-R772ET2M.js → chunk-AAPNU7GA.js} +4 -0
- package/dist/{chunk-XRWUQPCA.js → chunk-ATZF2RNJ.js} +25 -3
- package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
- package/dist/{chunk-JHKBALS2.js → chunk-CLCHOXLR.js} +1 -1
- package/dist/{chunk-DMK5ASKV.js → chunk-F6D766VQ.js} +6 -2
- package/dist/{chunk-AXFLY5GU.js → chunk-HKGYZVWN.js} +16 -1
- package/dist/{chunk-ZEZQNP2G.js → chunk-LGXVGIWN.js} +103 -25
- package/dist/{chunk-CW7BQKF6.js → chunk-LKUGUQUW.js} +3 -3
- package/dist/{chunk-FXWBFDQR.js → chunk-OAYY3WF3.js} +59 -12
- package/dist/{chunk-ITK6HV47.js → chunk-OGPVJ53N.js} +14 -6
- package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
- package/dist/chunk-OT5SXF7L.js +904 -0
- package/dist/{chunk-EDR47UE2.js → chunk-OYF25B62.js} +1 -1
- package/dist/{chunk-RPPTGPYF.js → chunk-PPSN2MLC.js} +2 -2
- package/dist/{chunk-K2F237KS.js → chunk-RGHF3OK5.js} +154 -18
- package/dist/{chunk-RU74YG46.js → chunk-VDUCLVZI.js} +338 -30
- package/dist/{chunk-724DVYHU.js → chunk-WHL6PKYP.js} +10 -10
- package/dist/{chunk-EEOMA4DX.js → chunk-WZZCA23D.js} +2 -1
- package/dist/{chunk-UDZLPR34.js → chunk-YSRYCS3N.js} +13 -8
- package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
- package/dist/{chunk-VPOMJMV6.js → chunk-ZNHL5KQJ.js} +1 -1
- package/dist/cli.js +17 -17
- package/dist/{clock-ZT5YT6HA.js → clock-X7V3C7NL.js} +2 -2
- package/dist/{cloud-sync-H7EWZASD.js → cloud-sync-52YNJRMR.js} +12 -11
- package/dist/{daemon-restart-4FWUA5JF.js → daemon-restart-QWHN5VMU.js} +4 -4
- package/dist/daemon.js +538 -782
- package/dist/{db-IPXRJUI7.js → db-I4EH5SLF.js} +3 -1
- package/dist/{delivery-manager-VYXVKUMP.js → delivery-manager-7R2YIQ2W.js} +13 -7
- package/dist/delivery-notices-RZQ6GONP.js +18 -0
- package/dist/{delivery-router-RHANNTAC.js → delivery-router-QS4NLJR6.js} +3 -1
- package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
- package/dist/{echo-text-PEEV7FQU.js → echo-text-73CYQRL7.js} +14 -13
- package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
- package/dist/{extensions-N6L5NYTK.js → extensions-QT65JGV6.js} +22 -11
- package/dist/{files-BRY5RRIM.js → files-PCKBQHAY.js} +2 -1
- package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
- package/dist/{message-delivery-7URC52ME.js → message-delivery-TRKT4T2P.js} +16 -13
- package/dist/migrate-name-placeholder-C7NCNLWF.js +37 -0
- package/dist/{mind-NEXGT3KE.js → mind-CT6ANXK4.js} +11 -11
- package/dist/{mind-contacts-QYU4TPEA.js → mind-contacts-PEXCDRXA.js} +6 -6
- package/dist/{mind-history-OGNR4L7W.js → mind-history-RZMMCHMX.js} +83 -11
- package/dist/{mind-list-SYRA7CH5.js → mind-list-7KBLPLES.js} +1 -1
- package/dist/{mind-service-E26ZQXDS.js → mind-service-7EFIJ3ZA.js} +12 -11
- package/dist/{mind-sleep-LZAFBDZD.js → mind-sleep-24HV4RCA.js} +2 -2
- package/dist/{mind-status-Z2LQFA6F.js → mind-status-4YW4PHMO.js} +15 -0
- package/dist/{package-2W2XKSIP.js → package-F6S5NHY2.js} +4 -4
- package/dist/{prompts-NBO3HRK3.js → prompts-JECJIFPQ.js} +1 -1
- package/dist/{reject-TWB4QJHG.js → reject-4NFLXKRN.js} +6 -2
- package/dist/{scheduler-JTTWZDYU.js → scheduler-73Q4MQ7Y.js} +8 -6
- package/dist/{seed-64XEHRUP.js → seed-DRKAP3XV.js} +1 -1
- package/dist/{seed-cmd-UEL2QB3Q.js → seed-cmd-6IMTO3LG.js} +2 -2
- package/dist/{seed-create-4JRQDPGA.js → seed-create-VNLDYRFK.js} +1 -1
- package/dist/{seed-readiness-DYOVCYHN.js → seed-readiness-H5TTPAQH.js} +2 -2
- package/dist/{seed-sprout-NIDIOJXT.js → seed-sprout-PPGYGK4G.js} +5 -5
- package/dist/{send-2YKOZJJ5.js → send-UXUQKO4N.js} +37 -22
- package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
- package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
- package/dist/{setup-4IX6PCD3.js → setup-YUAQMDVZ.js} +4 -4
- package/dist/skills/dreaming/SKILL.md +2 -2
- package/dist/skills/dreaming/references/INSTALL.md +1 -1
- package/dist/skills/memory/SKILL.md +47 -43
- package/dist/skills/tending/SKILL.md +2 -0
- package/dist/skills/volute-mind/references/integrations.md +3 -2
- package/dist/skills/volute-mind/references/routing.md +18 -5
- package/dist/{skills-MPUALV3S.js → skills-GMQ5UIIW.js} +2 -2
- package/dist/{sleep-manager-OFP2GFXF.js → sleep-manager-KHI65FEX.js} +12 -11
- package/dist/{spirit-4ECYIVGE.js → spirit-Q5VGFWUI.js} +19 -3
- package/dist/{spirit-availability-Y54Y3675.js → spirit-availability-G5FMWKRH.js} +13 -12
- package/dist/{sprout-GJMZFRGZ.js → sprout-PMTRL7QN.js} +1 -1
- package/dist/{src-LCSOQY6Z.js → src-3FIW7DLH.js} +241 -87
- package/dist/src-H3UCOP6B.js +516 -0
- package/dist/{src-HGNE2XG4.js → src-LTTQVN2Z.js} +1 -1
- package/dist/{status-MWLE3RVC.js → status-LCKTTIWS.js} +2 -2
- package/dist/{system-events-V43HDA3T.js → system-events-Q7IVZ5T4.js} +5 -1
- package/dist/{systems-VDRUZXNF.js → systems-HL7SGSW7.js} +2 -2
- package/dist/{turn-tracker-C5PVIC3B.js → turn-tracker-HZDPMSCC.js} +4 -2
- package/dist/{up-H6LJZDRK.js → up-XZDSJVMU.js} +3 -3
- package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
- package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
- package/dist/{variant-cleanup-STC6B5HR.js → variant-cleanup-QHGGY7ND.js} +8 -6
- package/dist/{version-notify-LBVHH5YV.js → version-notify-VNVFX6UP.js} +22 -7
- package/dist/web-assets/assets/{index-B_eYTa3R.js → index-CwtxGwe_.js} +16 -16
- package/dist/web-assets/assets/index-DhM1yYRN.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +4 -4
- package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
- package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +67 -0
- package/packages/extensions/{plan → intentions}/dist/ui/index.html +3 -3
- package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
- package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
- package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
- package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
- package/packages/extensions/pages/skills/pages/SKILL.md +18 -13
- package/templates/_base/.init/.config/prompts.json +3 -3
- package/templates/{pi → _base}/.init/.config/routes.json +1 -0
- package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/codex-session-seed.ts +463 -0
- package/templates/_base/src/lib/context-breakdown.ts +11 -3
- package/templates/_base/src/lib/daemon-client.ts +35 -0
- package/templates/_base/src/lib/pi-session-seed.ts +434 -0
- package/templates/_base/src/lib/routing.ts +2 -2
- package/templates/_base/src/lib/seed-note.ts +78 -0
- package/templates/_base/src/lib/session-seed.ts +375 -0
- package/templates/_base/src/lib/startup.ts +112 -6
- package/templates/claude/.init/CLAUDE.md +7 -3
- package/templates/claude/src/agent.ts +295 -101
- package/templates/claude/src/lib/hooks/pre-compact.ts +30 -5
- package/templates/claude/src/lib/message-channel.ts +43 -15
- package/templates/claude/src/lib/recover.ts +69 -0
- package/templates/claude/src/lib/stream-consumer.ts +33 -2
- package/templates/claude/src/server.ts +2 -1
- package/templates/claude/volute-template.json +1 -1
- package/templates/codex/.init/AGENTS.md +7 -3
- package/templates/codex/src/agent.ts +256 -39
- package/templates/codex/src/server.ts +2 -1
- package/templates/codex/volute-template.json +1 -1
- package/templates/pi/.init/MINDS.md +7 -3
- package/templates/pi/src/agent.ts +269 -43
- package/templates/pi/src/lib/event-handler.ts +11 -0
- package/templates/pi/src/lib/mechanics-doc.ts +30 -0
- package/templates/pi/src/server.ts +5 -1
- package/templates/pi/volute-template.json +1 -1
- package/dist/channels-FSAHOQWI.js +0 -90
- package/dist/skills/plan-coordinator/SKILL.md +0 -60
- package/dist/src-UDMJZ6MH.js +0 -426
- package/dist/web-assets/assets/index-DhZrzddW.css +0 -1
- package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
- package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
- package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
- package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
- package/templates/claude/.init/.config/routes.json +0 -11
- package/templates/codex/.init/.config/routes.json +0 -11
- package/dist/{env-IBW2QI6K.js → env-7AZFBPWX.js} +5 -5
- package/dist/{login-RJJTCVR3.js → login-4NUS72Z5.js} +3 -3
- package/dist/{login-SB5OENMV.js → login-57MMYILL.js} +4 -4
- package/dist/{register-OX4HRSGC.js → register-WNCSOTAU.js} +3 -3
- package/dist/{restart-AGEGNSYQ.js → restart-B4GNMVF2.js} +4 -4
- package/dist/{skill-B5OJEQUA.js → skill-QYTBUDNQ.js} +4 -4
- package/dist/{start-MUMNFR52.js → start-BDHJVOVX.js} +3 -3
- package/dist/{stop-EKERNYTX.js → stop-45QYSGYS.js} +4 -4
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session seeding.
|
|
3
|
+
*
|
|
4
|
+
* When a mind starts a fresh *persistent* session (no saved session id — e.g.
|
|
5
|
+
* after a sleep archived the live pointer, or an orphaned reference), we seed the
|
|
6
|
+
* new session by copying the tail of the previous session's raw SDK transcript
|
|
7
|
+
* into a new synthetic session file. The Claude Agent SDK then resumes it
|
|
8
|
+
* natively, so the mind experiences the same conversation continuing rather than
|
|
9
|
+
* waking into an empty context.
|
|
10
|
+
*
|
|
11
|
+
* The copy is verbatim: marker lines, thinking blocks, tool_use/tool_result all
|
|
12
|
+
* survive as-is. Only two things are rewritten — the `sessionId` on every line
|
|
13
|
+
* (to the freshly generated session id) and the first chain event's `parentUuid`
|
|
14
|
+
* (nulled, to detach the tail from the dropped history).
|
|
15
|
+
*
|
|
16
|
+
* Nothing here throws: any failure returns null so session start is never blocked.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, resolve } from "node:path";
|
|
22
|
+
import { findClaudeSessionFile } from "./context-breakdown.js";
|
|
23
|
+
import { log } from "./logger.js";
|
|
24
|
+
import { parseArchiveTimestamp } from "./seed-note.js";
|
|
25
|
+
|
|
26
|
+
/** Default seed budget (estimated tokens) when config omits continuity.seedTokens. */
|
|
27
|
+
export const DEFAULT_SEED_TOKENS = 30000;
|
|
28
|
+
|
|
29
|
+
// Archived pointers are named `<name>-<timestamp>.json`, where the timestamp is
|
|
30
|
+
// `new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16)` → `YYYY-MM-DDTHH-MM`
|
|
31
|
+
// (see archiveSessions in the daemon's sleep-manager). Matching the strict shape
|
|
32
|
+
// after the `<name>-` prefix disambiguates `main-...` from a `main-thread-...`.
|
|
33
|
+
const ARCHIVE_SUFFIX = /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})\.json$/;
|
|
34
|
+
|
|
35
|
+
type JsonlLine = Record<string, unknown> & {
|
|
36
|
+
type?: string;
|
|
37
|
+
uuid?: string;
|
|
38
|
+
parentUuid?: string | null;
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
timestamp?: string;
|
|
41
|
+
message?: { role?: string; content?: unknown };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A resolved archive pointer: the previous session id and when it was archived. */
|
|
45
|
+
export type ArchivedSession = { sessionId: string; archivedAt: number | null };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Newest archived session pointer for `<name>` under `<sessionsDir>/archive/`,
|
|
49
|
+
* or null if there's no matching pointer (or it can't be read). `archivedAt` is
|
|
50
|
+
* the archive timestamp in epoch millis (null if unparseable).
|
|
51
|
+
*/
|
|
52
|
+
export function findLatestArchivedSession(
|
|
53
|
+
sessionsDir: string,
|
|
54
|
+
name: string,
|
|
55
|
+
): ArchivedSession | null {
|
|
56
|
+
const archiveDir = resolve(sessionsDir, "archive");
|
|
57
|
+
let files: string[];
|
|
58
|
+
try {
|
|
59
|
+
files = readdirSync(archiveDir);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const prefix = `${name}-`;
|
|
65
|
+
let bestTs = "";
|
|
66
|
+
let bestFile: string | null = null;
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
if (!file.startsWith(prefix)) continue;
|
|
69
|
+
const match = file.slice(prefix.length).match(ARCHIVE_SUFFIX);
|
|
70
|
+
if (!match) continue;
|
|
71
|
+
// Timestamps are zero-padded ISO, so lexicographic comparison is chronological.
|
|
72
|
+
if (match[1] > bestTs) {
|
|
73
|
+
bestTs = match[1];
|
|
74
|
+
bestFile = file;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!bestFile) return null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const data = JSON.parse(readFileSync(resolve(archiveDir, bestFile), "utf-8"));
|
|
81
|
+
if (typeof data.sessionId !== "string") return null;
|
|
82
|
+
return { sessionId: data.sessionId, archivedAt: parseArchiveTimestamp(bestTs) };
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A chain event is a real conversation node (has a uuid), not a marker line. */
|
|
89
|
+
function isChainEvent(o: JsonlLine): boolean {
|
|
90
|
+
return typeof o.uuid === "string" && (o.type === "user" || o.type === "assistant");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A turn boundary is a genuine incoming prompt: a `user` chain event whose
|
|
95
|
+
* content is NOT tool_result blocks (those are tool-loop continuations, not the
|
|
96
|
+
* start of a new turn).
|
|
97
|
+
*/
|
|
98
|
+
function isTurnBoundary(o: JsonlLine): boolean {
|
|
99
|
+
if (o.type !== "user" || typeof o.uuid !== "string") return false;
|
|
100
|
+
const content = o.message?.content;
|
|
101
|
+
if (typeof content === "string") return true;
|
|
102
|
+
if (Array.isArray(content)) {
|
|
103
|
+
return !content.some(
|
|
104
|
+
(b) => b && typeof b === "object" && (b as { type?: string }).type === "tool_result",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
// Unusual shape — treat as a boundary rather than folding it into a prior turn.
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Estimate tokens for a raw JSONL line as its JSON text length / 4. */
|
|
112
|
+
function estimateTokens(raw: string): number {
|
|
113
|
+
return raw.length / 4;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse jsonl into aligned parsed/raw arrays. In strict mode a corrupt line aborts
|
|
118
|
+
* (returns null — used when copying verbatim, where a broken line means we can't
|
|
119
|
+
* faithfully reconstruct the chain). In lenient mode corrupt lines are skipped
|
|
120
|
+
* (used when reading a transcript that may still be mid-write, e.g. computing the
|
|
121
|
+
* cut point while the SDK is streaming).
|
|
122
|
+
*/
|
|
123
|
+
function parseJsonl(
|
|
124
|
+
jsonl: string,
|
|
125
|
+
lenient: boolean,
|
|
126
|
+
): { parsed: JsonlLine[]; raws: string[] } | null {
|
|
127
|
+
const rawLines = jsonl.split("\n").filter((l) => l.trim().length > 0);
|
|
128
|
+
const parsed: JsonlLine[] = [];
|
|
129
|
+
const raws: string[] = [];
|
|
130
|
+
for (const raw of rawLines) {
|
|
131
|
+
let obj: JsonlLine;
|
|
132
|
+
try {
|
|
133
|
+
obj = JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
if (lenient) continue;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
parsed.push(obj);
|
|
139
|
+
raws.push(raw);
|
|
140
|
+
}
|
|
141
|
+
return { parsed, raws };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Indices (into parsed) of the genuine turn boundaries. */
|
|
145
|
+
function turnBoundaries(parsed: JsonlLine[]): number[] {
|
|
146
|
+
const boundaries: number[] = [];
|
|
147
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
148
|
+
if (isTurnBoundary(parsed[i])) boundaries.push(i);
|
|
149
|
+
}
|
|
150
|
+
return boundaries;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Choose the first-kept line index: walk backward from the final turn, taking as
|
|
155
|
+
* many whole turns as fit in `seedTokens` (always at least the final turn even if
|
|
156
|
+
* it alone exceeds the budget). Returns the parsed index of the chosen boundary.
|
|
157
|
+
*/
|
|
158
|
+
function tailStartByBudget(parsed: JsonlLine[], raws: string[], seedTokens: number): number {
|
|
159
|
+
const boundaries = turnBoundaries(parsed);
|
|
160
|
+
// Turn t spans lines [boundaries[t], boundaries[t+1]); the last turn runs to EOF.
|
|
161
|
+
const turnTokens = (t: number): number => {
|
|
162
|
+
const start = boundaries[t];
|
|
163
|
+
const end = t + 1 < boundaries.length ? boundaries[t + 1] : parsed.length;
|
|
164
|
+
let sum = 0;
|
|
165
|
+
for (let i = start; i < end; i++) sum += estimateTokens(raws[i]);
|
|
166
|
+
return sum;
|
|
167
|
+
};
|
|
168
|
+
const last = boundaries.length - 1;
|
|
169
|
+
let startTurn = last;
|
|
170
|
+
let accum = turnTokens(last);
|
|
171
|
+
for (let t = last - 1; t >= 0; t--) {
|
|
172
|
+
const cost = turnTokens(t);
|
|
173
|
+
if (accum + cost > seedTokens) break;
|
|
174
|
+
accum += cost;
|
|
175
|
+
startTurn = t;
|
|
176
|
+
}
|
|
177
|
+
return boundaries[startTurn];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Copy the tail from `startLine` to EOF into a fresh synthetic transcript,
|
|
182
|
+
* rewriting the sessionId on every line and nulling the first chain event's
|
|
183
|
+
* parentUuid (to detach the tail from the dropped history).
|
|
184
|
+
*/
|
|
185
|
+
function emitTail(parsed: JsonlLine[], startLine: number): SeededTranscript {
|
|
186
|
+
const newId = randomUUID();
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
let firstChainSeen = false;
|
|
189
|
+
for (let i = startLine; i < parsed.length; i++) {
|
|
190
|
+
const obj = parsed[i];
|
|
191
|
+
if ("sessionId" in obj) obj.sessionId = newId;
|
|
192
|
+
if (!firstChainSeen && isChainEvent(obj)) {
|
|
193
|
+
obj.parentUuid = null;
|
|
194
|
+
firstChainSeen = true;
|
|
195
|
+
}
|
|
196
|
+
lines.push(JSON.stringify(obj));
|
|
197
|
+
}
|
|
198
|
+
return { sessionId: newId, lines };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type SeededTranscript = { sessionId: string; lines: string[] };
|
|
202
|
+
|
|
203
|
+
/** A pinned cut point for rotation: where the verbatim tail begins. */
|
|
204
|
+
export type SeedCut = { boundaryUuid: string; boundaryTimestamp: string | null };
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build the seeded transcript from an old transcript's raw jsonl text: take as
|
|
208
|
+
* many whole trailing turns as fit in `seedTokens` (always at least the final
|
|
209
|
+
* turn), rewrite the session id on every line, and null the first chain event's
|
|
210
|
+
* parentUuid. Returns null if there's nothing seedable (empty, no genuine turn,
|
|
211
|
+
* or a corrupt line — in which case the caller starts clean).
|
|
212
|
+
*/
|
|
213
|
+
export function buildSeededTranscript(jsonl: string, seedTokens: number): SeededTranscript | null {
|
|
214
|
+
const p = parseJsonl(jsonl, false);
|
|
215
|
+
if (!p || p.parsed.length === 0) return null;
|
|
216
|
+
if (turnBoundaries(p.parsed).length === 0) return null;
|
|
217
|
+
const startLine = tailStartByBudget(p.parsed, p.raws, seedTokens);
|
|
218
|
+
return emitTail(p.parsed, startLine);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Compute the cut point (the first-kept turn's identity) for a `seedTokens` budget,
|
|
223
|
+
* from raw jsonl. Parsed leniently because the live transcript may still be
|
|
224
|
+
* mid-write when this runs at warn time. Returns null if there's no genuine turn
|
|
225
|
+
* or the chosen boundary lacks a uuid.
|
|
226
|
+
*/
|
|
227
|
+
export function computeSeedCut(jsonl: string, seedTokens: number): SeedCut | null {
|
|
228
|
+
const p = parseJsonl(jsonl, true);
|
|
229
|
+
if (!p || p.parsed.length === 0) return null;
|
|
230
|
+
if (turnBoundaries(p.parsed).length === 0) return null;
|
|
231
|
+
const startLine = tailStartByBudget(p.parsed, p.raws, seedTokens);
|
|
232
|
+
const boundary = p.parsed[startLine];
|
|
233
|
+
if (typeof boundary.uuid !== "string") return null;
|
|
234
|
+
return {
|
|
235
|
+
boundaryUuid: boundary.uuid,
|
|
236
|
+
boundaryTimestamp: typeof boundary.timestamp === "string" ? boundary.timestamp : null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Build the seeded transcript starting at a pinned boundary uuid (the whole tail
|
|
242
|
+
* from that line through EOF). Used by rotation to honor the cut point the mind
|
|
243
|
+
* was warned about, regardless of how much the transcript grew after the warning.
|
|
244
|
+
* Returns null if the boundary uuid isn't found or a line is corrupt.
|
|
245
|
+
*/
|
|
246
|
+
export function buildSeededTranscriptFromCut(
|
|
247
|
+
jsonl: string,
|
|
248
|
+
boundaryUuid: string,
|
|
249
|
+
): SeededTranscript | null {
|
|
250
|
+
const p = parseJsonl(jsonl, false);
|
|
251
|
+
if (!p || p.parsed.length === 0) return null;
|
|
252
|
+
const startLine = p.parsed.findIndex((o) => o.uuid === boundaryUuid);
|
|
253
|
+
if (startLine < 0) return null;
|
|
254
|
+
return emitTail(p.parsed, startLine);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Result of a successful seed: the new session id and when the source was archived. */
|
|
258
|
+
export type SeedOutcome = { sessionId: string; archivedAt: number | null };
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Seed a fresh persistent session from the mind's previous archived transcript.
|
|
262
|
+
* Writes the synthetic transcript next to the source file (same project dir) and
|
|
263
|
+
* returns the new SDK session id plus the archived-at time (for the gap note), or
|
|
264
|
+
* null if there's nothing to seed. Never throws — any failure returns null so
|
|
265
|
+
* session start is never blocked.
|
|
266
|
+
*/
|
|
267
|
+
export function seedSession(opts: {
|
|
268
|
+
cwd: string;
|
|
269
|
+
sessionsDir: string;
|
|
270
|
+
name: string;
|
|
271
|
+
seedTokens: number;
|
|
272
|
+
}): SeedOutcome | null {
|
|
273
|
+
const { cwd, sessionsDir, name, seedTokens } = opts;
|
|
274
|
+
// Ephemeral `new-*` sessions are never persisted or archived, so they never
|
|
275
|
+
// seed. The agent caller already gates on this; guard here too so the invariant
|
|
276
|
+
// holds wherever seedSession is called.
|
|
277
|
+
if (name.startsWith("new-")) return null;
|
|
278
|
+
if (seedTokens <= 0) return null; // seeding disabled
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const archived = findLatestArchivedSession(sessionsDir, name);
|
|
282
|
+
if (!archived) return null;
|
|
283
|
+
|
|
284
|
+
const sourcePath = findClaudeSessionFile(cwd, archived.sessionId);
|
|
285
|
+
if (!sourcePath) return null; // transcript didn't survive archival — start clean
|
|
286
|
+
|
|
287
|
+
const seeded = buildSeededTranscript(readFileSync(sourcePath, "utf-8"), seedTokens);
|
|
288
|
+
if (!seeded) return null;
|
|
289
|
+
|
|
290
|
+
const destPath = resolve(dirname(sourcePath), `${seeded.sessionId}.jsonl`);
|
|
291
|
+
writeFileSync(destPath, `${seeded.lines.join("\n")}\n`);
|
|
292
|
+
log(
|
|
293
|
+
"mind",
|
|
294
|
+
`session "${name}": seeded ${seeded.lines.length} line(s) from ${archived.sessionId} → ${seeded.sessionId}`,
|
|
295
|
+
);
|
|
296
|
+
return { sessionId: seeded.sessionId, archivedAt: archived.archivedAt };
|
|
297
|
+
} catch (err) {
|
|
298
|
+
log("mind", `session "${name}": seeding failed, starting fresh:`, err);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Archive-pointer timestamp, matching the daemon sleep-manager's archiveSessions:
|
|
305
|
+
* `new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16)` → UTC `YYYY-MM-DDTHH-MM`.
|
|
306
|
+
*/
|
|
307
|
+
export function archivePointerTimestamp(now: Date = new Date()): string {
|
|
308
|
+
return now.toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Write an archive pointer for a rotated-out session, matching exactly how sleep
|
|
313
|
+
* archival preserves the live pointer: `<sessionsDir>/archive/<name>-<UTC-ts>.json`
|
|
314
|
+
* holding `{ sessionId }` (the session-store format). Keeps the name→session chain
|
|
315
|
+
* uniform so the full transcript stays findable after a rotation.
|
|
316
|
+
*/
|
|
317
|
+
export function writeRotationArchivePointer(
|
|
318
|
+
sessionsDir: string,
|
|
319
|
+
name: string,
|
|
320
|
+
sessionId: string,
|
|
321
|
+
now: Date = new Date(),
|
|
322
|
+
): void {
|
|
323
|
+
const archiveDir = resolve(sessionsDir, "archive");
|
|
324
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
325
|
+
const dest = resolve(archiveDir, `${name}-${archivePointerTimestamp(now)}.json`);
|
|
326
|
+
writeFileSync(dest, JSON.stringify({ sessionId }));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Rotate a session in place at the context limit. Reads the live transcript, builds
|
|
331
|
+
* a seeded tail from the pinned `cut` (falling back to a budget-based tail when the
|
|
332
|
+
* pin can't be honored), writes it as a new synthetic session file next to the
|
|
333
|
+
* source, and — for persistent sessions — archives the rotated-out pointer so the
|
|
334
|
+
* full transcript stays findable. Returns the new session id, or null if rotation
|
|
335
|
+
* can't proceed (the caller then falls back to a fresh session). Never throws.
|
|
336
|
+
*/
|
|
337
|
+
export function rotateSession(opts: {
|
|
338
|
+
cwd: string;
|
|
339
|
+
sessionsDir: string;
|
|
340
|
+
name: string;
|
|
341
|
+
oldSessionId: string;
|
|
342
|
+
cut: SeedCut | null;
|
|
343
|
+
seedTokens: number;
|
|
344
|
+
}): string | null {
|
|
345
|
+
const { cwd, sessionsDir, name, oldSessionId, cut, seedTokens } = opts;
|
|
346
|
+
try {
|
|
347
|
+
const sourcePath = findClaudeSessionFile(cwd, oldSessionId);
|
|
348
|
+
if (!sourcePath) return null; // live transcript not found — fall back to fresh
|
|
349
|
+
const jsonl = readFileSync(sourcePath, "utf-8");
|
|
350
|
+
// Honor the pinned boundary the mind was warned about; only fall back to a
|
|
351
|
+
// budget-based tail if the pin can't be resolved (missing/renamed uuid).
|
|
352
|
+
const seeded =
|
|
353
|
+
(cut ? buildSeededTranscriptFromCut(jsonl, cut.boundaryUuid) : null) ??
|
|
354
|
+
buildSeededTranscript(jsonl, seedTokens);
|
|
355
|
+
if (!seeded) return null;
|
|
356
|
+
|
|
357
|
+
writeFileSync(
|
|
358
|
+
resolve(dirname(sourcePath), `${seeded.sessionId}.jsonl`),
|
|
359
|
+
`${seeded.lines.join("\n")}\n`,
|
|
360
|
+
);
|
|
361
|
+
// Ephemeral `new-*` sessions rotate too, but leave no pointer/archive behind
|
|
362
|
+
// (they're one-offs and never seed at a true session start).
|
|
363
|
+
if (!name.startsWith("new-")) {
|
|
364
|
+
writeRotationArchivePointer(sessionsDir, name, oldSessionId);
|
|
365
|
+
}
|
|
366
|
+
log(
|
|
367
|
+
"mind",
|
|
368
|
+
`session "${name}": rotated ${oldSessionId} → ${seeded.sessionId} (${seeded.lines.length} lines)`,
|
|
369
|
+
);
|
|
370
|
+
return seeded.sessionId;
|
|
371
|
+
} catch (err) {
|
|
372
|
+
log("mind", `session "${name}": rotation failed:`, err);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -44,6 +44,18 @@ export type MindConfig = {
|
|
|
44
44
|
compaction?: { maxContextTokens?: number };
|
|
45
45
|
/** Idle minutes before a session's SDK subprocess is reaped. 0 disables. Default 30. */
|
|
46
46
|
sessionIdleMinutes?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Session continuity across restarts. When a fresh persistent session starts,
|
|
49
|
+
* the tail of the previous session's transcript (up to `seedTokens` estimated
|
|
50
|
+
* tokens) is copied into it so the conversation continues. Default 30000; 0 disables.
|
|
51
|
+
*/
|
|
52
|
+
continuity?: { seedTokens?: number };
|
|
53
|
+
/**
|
|
54
|
+
* MEMORY.md token budgets (estimated as chars/4). Over `softBudgetTokens` a
|
|
55
|
+
* consolidation nudge is added to the system prompt; over `hardCapTokens` only
|
|
56
|
+
* the head of the file is loaded (the file on disk is never modified).
|
|
57
|
+
*/
|
|
58
|
+
memory?: { softBudgetTokens?: number; hardCapTokens?: number };
|
|
47
59
|
subagents?: Record<string, SubagentConfig>;
|
|
48
60
|
// Template-specific config fields (claude, pi, codex)
|
|
49
61
|
thinking?: ThinkingConfig;
|
|
@@ -71,8 +83,96 @@ function loadFile(path: string): string {
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
/** Default MEMORY.md budgets, overridable via `memory` in home/.config/config.json. */
|
|
87
|
+
export const MEMORY_SOFT_BUDGET_TOKENS = 5000;
|
|
88
|
+
export const MEMORY_HARD_CAP_TOKENS = 25000;
|
|
89
|
+
|
|
90
|
+
export function estimateTokens(chars: number): number {
|
|
91
|
+
return Math.round(chars / 4);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatTokens(tokens: number): string {
|
|
95
|
+
if (tokens < 1000) return `~${tokens} tokens`;
|
|
96
|
+
// One decimal so a value just past a budget doesn't render as the budget
|
|
97
|
+
// itself ("~5.1k tokens ... budget of ~5k tokens", not "~5k ... ~5k").
|
|
98
|
+
return `~${(tokens / 1000).toFixed(1).replace(/\.0$/, "")}k tokens`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Current MEMORY.md size as a "~Nk tokens" label, for prompt substitution. */
|
|
102
|
+
export function memorySizeLabel(): string {
|
|
103
|
+
return formatTokens(estimateTokens(loadFile(resolve("home/MEMORY.md")).length));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render the compaction warning for sending: substitute ${date} and
|
|
108
|
+
* ${memory_size} with current values. Called per send — the date rolls over
|
|
109
|
+
* and MEMORY.md changes as the mind edits it.
|
|
110
|
+
*/
|
|
111
|
+
export function renderCompactionWarning(template: string): string {
|
|
112
|
+
return (
|
|
113
|
+
template
|
|
114
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} placeholder
|
|
115
|
+
.replaceAll("${date}", new Date().toLocaleDateString("en-CA"))
|
|
116
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${memory_size} placeholder
|
|
117
|
+
.replaceAll("${memory_size}", memorySizeLabel())
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function headAtLineBoundary(text: string, maxChars: number): string {
|
|
122
|
+
const cut = text.lastIndexOf("\n", maxChars);
|
|
123
|
+
// Prefer a whole-line cut, but not when the nearest newline is so far back
|
|
124
|
+
// that it would discard most of the allowed head (one giant line).
|
|
125
|
+
if (cut >= maxChars / 2) return text.slice(0, cut);
|
|
126
|
+
let head = text.slice(0, maxChars);
|
|
127
|
+
// Don't split a surrogate pair at a hard cut.
|
|
128
|
+
const last = head.charCodeAt(head.length - 1);
|
|
129
|
+
if (last >= 0xd800 && last <= 0xdbff) head = head.slice(0, -1);
|
|
130
|
+
return head;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The Memory section of the system prompt: header carries the token cost so the
|
|
135
|
+
* mind sees what its memory weighs on every request; over the soft budget a
|
|
136
|
+
* consolidation nudge is added; over the hard cap only the head is loaded (the
|
|
137
|
+
* file on disk is never touched) with a loud notice explaining how to recover.
|
|
138
|
+
*/
|
|
139
|
+
export function buildMemorySection(memory: string, config: MindConfig): string {
|
|
140
|
+
// typeof guards mirror the daemon's getMemoryStatus: a malformed override
|
|
141
|
+
// (null, string) falls back to the default instead of poisoning arithmetic.
|
|
142
|
+
const softBudget =
|
|
143
|
+
typeof config.memory?.softBudgetTokens === "number"
|
|
144
|
+
? config.memory.softBudgetTokens
|
|
145
|
+
: MEMORY_SOFT_BUDGET_TOKENS;
|
|
146
|
+
const hardCap =
|
|
147
|
+
typeof config.memory?.hardCapTokens === "number"
|
|
148
|
+
? config.memory.hardCapTokens
|
|
149
|
+
: MEMORY_HARD_CAP_TOKENS;
|
|
150
|
+
const totalTokens = estimateTokens(memory.length);
|
|
151
|
+
|
|
152
|
+
let body = memory;
|
|
153
|
+
let notice = "";
|
|
154
|
+
if (totalTokens > hardCap) {
|
|
155
|
+
body = headAtLineBoundary(memory, hardCap * 4);
|
|
156
|
+
notice =
|
|
157
|
+
`\n\n⚠ MEMORY.md is ${formatTokens(totalTokens)} — only the first ` +
|
|
158
|
+
`${formatTokens(hardCap)} are loaded. The full file is untouched on disk. ` +
|
|
159
|
+
`Consolidate it to restore your full memory.`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const loadedTokens = estimateTokens(body.length);
|
|
163
|
+
let header = `## Memory (${formatTokens(loadedTokens)}, always loaded)`;
|
|
164
|
+
if (!notice && loadedTokens > softBudget) {
|
|
165
|
+
header +=
|
|
166
|
+
`\n\nYour memory exceeds the recommended budget of ${formatTokens(softBudget)} — ` +
|
|
167
|
+
`consider consolidating; see the memory skill.`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return `${header}\n\n${body}${notice}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function loadSystemPrompt(config: MindConfig = loadConfig()): string {
|
|
75
174
|
const soulPath = resolve("home/SOUL.md");
|
|
175
|
+
const spiritPath = resolve("home/SPIRIT.md");
|
|
76
176
|
const memoryPath = resolve("home/MEMORY.md");
|
|
77
177
|
const volutePath = resolve("home/VOLUTE.md");
|
|
78
178
|
|
|
@@ -82,12 +182,16 @@ export function loadSystemPrompt(): string {
|
|
|
82
182
|
process.exit(1);
|
|
83
183
|
}
|
|
84
184
|
|
|
185
|
+
// SPIRIT.md exists only for the system spirit — daemon-owned doctrine (role,
|
|
186
|
+
// philosophy), kept separate so SOUL.md can be entirely the spirit's own.
|
|
187
|
+
const spirit = loadFile(spiritPath);
|
|
85
188
|
const memory = loadFile(memoryPath);
|
|
86
189
|
const volute = loadFile(volutePath);
|
|
87
190
|
|
|
88
191
|
const promptParts = [soul];
|
|
192
|
+
if (spirit) promptParts.push(spirit);
|
|
89
193
|
if (volute) promptParts.push(volute);
|
|
90
|
-
if (memory) promptParts.push(
|
|
194
|
+
if (memory) promptParts.push(buildMemorySection(memory, config));
|
|
91
195
|
return promptParts.join("\n\n---\n\n");
|
|
92
196
|
}
|
|
93
197
|
|
|
@@ -161,12 +265,14 @@ export type MindPrompts = {
|
|
|
161
265
|
*/
|
|
162
266
|
export const DEFAULT_PROMPTS: MindPrompts = {
|
|
163
267
|
compaction_warning:
|
|
164
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} prompt template
|
|
165
|
-
"
|
|
268
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff}/${date}/${memory_size} prompt template
|
|
269
|
+
"Context limit approaching — this session will rotate shortly. Turns before ${cutoff} will be collapsed to their summaries; turns from ${cutoff} on are kept verbatim in the continued session, so there's no need to re-describe them.\n\nFor the turns that will collapse, make sure they read the way you'd want: `volute mind history --provisional` shows the provisional summaries, and `volute mind history --write --turn <id> --text \"...\"` replaces any with your own account. Also save anything important to your files (memory/journal/${date}.md, or a memory/ file). Provisional summaries are kept if you write nothing — nothing blocks on this.\n\nYour MEMORY.md is currently ${memory_size}. It is loaded into every request, so consolidate rather than append — distill detail into memory/ files and keep MEMORY.md lean (see the memory skill).",
|
|
166
270
|
compaction_instructions:
|
|
167
271
|
"Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
|
|
272
|
+
// Quoted: `${channel}` is `#garden` for a Volute channel, and an unquoted `#` starts a
|
|
273
|
+
// shell comment, so the target would be stripped before the CLI saw it.
|
|
168
274
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${channel} prompt template
|
|
169
|
-
reply_instructions: 'To reply to this message, use: volute chat send ${channel} "your message"',
|
|
275
|
+
reply_instructions: 'To reply to this message, use: volute chat send "${channel}" "your message"',
|
|
170
276
|
event_instructions:
|
|
171
277
|
"This is a system event from your environment — not a message from anyone, and nothing awaits a reply. If it calls for action, use your normal channels. Your closing thoughts on an event turn are kept as a private reflection in your history.",
|
|
172
278
|
channel_invite: `[Channel Invite]
|
|
@@ -179,7 +285,7 @@ Further messages will be saved to \${filePath}
|
|
|
179
285
|
|
|
180
286
|
To accept, add to .config/routes.json:
|
|
181
287
|
Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
|
|
182
|
-
\${batchRecommendation}To respond, use: volute chat send \${channel} "your message"
|
|
288
|
+
\${batchRecommendation}To respond, use: volute chat send "\${channel}" "your message"
|
|
183
289
|
To reject, delete \${filePath}`,
|
|
184
290
|
};
|
|
185
291
|
|
|
@@ -11,6 +11,10 @@ Messages arrive with a context prefix:
|
|
|
11
11
|
|
|
12
12
|
You can also reach out proactively — see the **volute-mind** skill.
|
|
13
13
|
|
|
14
|
+
## Framework Upgrades
|
|
15
|
+
|
|
16
|
+
When the host updates Volute, your framework code (`src/`, plus `VOLUTE.md`) upgrades automatically the next time you're eligible — usually your next restart. Identity and memory files in `home/` — `SOUL.md`, `MEMORY.md`, everything you author — are never touched by an upgrade. If you'd rather manage your own framework code by hand, set `"upgrades": "manual"` in `.config/volute.json`.
|
|
17
|
+
|
|
14
18
|
## Identity & Sessions
|
|
15
19
|
|
|
16
20
|
These files shape your starting identity. They're loaded into your system prompt, but they belong to you — edit them as you evolve:
|
|
@@ -26,8 +30,8 @@ You may have **multiple named threads** — each maintains its own conversation
|
|
|
26
30
|
|
|
27
31
|
Two-tier memory, both managed via file tools:
|
|
28
32
|
|
|
29
|
-
- **`MEMORY.md`** — Your
|
|
30
|
-
- **`memory/journal/YYYY-MM-DD.md
|
|
31
|
-
- Periodically consolidate
|
|
33
|
+
- **`MEMORY.md`** — Your core memory, always in context. Every token it holds is paid on every request, so keep it lean: identity, active context, key relationships, and an index of your `memory/` files.
|
|
34
|
+
- **`memory/`** — Everything else, read on demand: your daily journal (`memory/journal/YYYY-MM-DD.md` — a permanent record of what you're doing, thinking, and learning) plus any topic, channel, or project files you create.
|
|
35
|
+
- Periodically consolidate: distill detail out of `MEMORY.md` into `memory/` files, leaving one-line index entries behind.
|
|
32
36
|
|
|
33
37
|
See the **memory** skill for detailed guidance.
|