volute 0.52.0 → 0.53.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-I4GVGWA3.js} +6 -3
- package/dist/{chat-VAZ25KVT.js → chat-WK2BLM7B.js} +9 -9
- package/dist/{chunk-724DVYHU.js → chunk-23UJGVYP.js} +9 -9
- package/dist/{chunk-K2F237KS.js → chunk-5DBGQBQR.js} +138 -13
- package/dist/chunk-5RBEEAKW.js +58 -0
- package/dist/{chunk-XRWUQPCA.js → chunk-7ARONCZW.js} +24 -2
- package/dist/{chunk-FXWBFDQR.js → chunk-AP2KQMMW.js} +48 -9
- package/dist/{chunk-DMK5ASKV.js → chunk-F6D766VQ.js} +6 -2
- package/dist/{chunk-ZEZQNP2G.js → chunk-HASADR3E.js} +46 -9
- package/dist/{chunk-AXFLY5GU.js → chunk-IQ7VNWH6.js} +16 -1
- package/dist/{chunk-G7A5LEVI.js → chunk-LN7XJ56R.js} +6 -6
- package/dist/{chunk-CW7BQKF6.js → chunk-P2TBUKLW.js} +3 -3
- package/dist/{chunk-SW2U6HRB.js → chunk-PIZDPY4G.js} +20 -3
- package/dist/{chunk-JHKBALS2.js → chunk-RMADJFA7.js} +1 -1
- package/dist/{chunk-RU74YG46.js → chunk-RQRO6PC7.js} +40 -6
- package/dist/{chunk-RPPTGPYF.js → chunk-TAWRJ24T.js} +1 -1
- package/dist/{chunk-ITK6HV47.js → chunk-UO4MZQNO.js} +1 -1
- package/dist/chunk-UTKUO2KY.js +102 -0
- package/dist/{chunk-YVR6EHWL.js → chunk-VDZ4I4ZP.js} +160 -18
- package/dist/cli.js +16 -16
- package/dist/{clock-ZT5YT6HA.js → clock-SH4NQC2F.js} +3 -3
- package/dist/{cloud-sync-H7EWZASD.js → cloud-sync-MJVJ33LD.js} +10 -9
- package/dist/{daemon-restart-4FWUA5JF.js → daemon-restart-BIGU4OKS.js} +1 -1
- package/dist/daemon.js +261 -89
- package/dist/{delivery-manager-VYXVKUMP.js → delivery-manager-FDHCZ7PU.js} +7 -5
- package/dist/delivery-notices-65TB3CBX.js +18 -0
- package/dist/{echo-text-PEEV7FQU.js → echo-text-KU6G4Z7V.js} +11 -10
- package/dist/{extensions-N6L5NYTK.js → extensions-VACUKFA5.js} +10 -9
- package/dist/{files-BRY5RRIM.js → files-CHPNEQXN.js} +5 -4
- package/dist/{message-delivery-7URC52ME.js → message-delivery-TUXM3AAE.js} +14 -11
- package/dist/{mind-NEXGT3KE.js → mind-IX5AWUST.js} +11 -11
- package/dist/{mind-contacts-QYU4TPEA.js → mind-contacts-BKUOUJPV.js} +5 -5
- package/dist/{mind-history-OGNR4L7W.js → mind-history-RVF6KJ7K.js} +83 -11
- package/dist/{mind-list-SYRA7CH5.js → mind-list-KNU34SUD.js} +2 -2
- package/dist/{mind-service-E26ZQXDS.js → mind-service-WHFBUKZE.js} +10 -9
- package/dist/{mind-sleep-LZAFBDZD.js → mind-sleep-D3DEYAWG.js} +5 -5
- package/dist/{mind-status-Z2LQFA6F.js → mind-status-BPTLQ5Z6.js} +15 -3
- package/dist/{package-2W2XKSIP.js → package-BN5KMU54.js} +1 -1
- package/dist/{prompts-NBO3HRK3.js → prompts-HNKGCAEY.js} +1 -1
- package/dist/{reject-TWB4QJHG.js → reject-K2A4UK2Y.js} +9 -5
- package/dist/{scheduler-JTTWZDYU.js → scheduler-5X5UIQOQ.js} +7 -5
- package/dist/{seed-64XEHRUP.js → seed-LHZ33UB4.js} +1 -1
- package/dist/{seed-cmd-UEL2QB3Q.js → seed-cmd-U4RVJ3UA.js} +2 -2
- package/dist/{seed-create-4JRQDPGA.js → seed-create-OQU4KNCB.js} +1 -1
- package/dist/{seed-readiness-DYOVCYHN.js → seed-readiness-WJYYRRCK.js} +2 -2
- package/dist/{seed-sprout-NIDIOJXT.js → seed-sprout-SFUYH2RG.js} +7 -7
- package/dist/{send-2YKOZJJ5.js → send-FWZAMDBN.js} +35 -20
- package/dist/{setup-4IX6PCD3.js → setup-RVSGCIF3.js} +1 -1
- package/dist/skills/memory/SKILL.md +47 -43
- package/dist/{skills-MPUALV3S.js → skills-QRPU6BWY.js} +1 -1
- package/dist/{sleep-manager-OFP2GFXF.js → sleep-manager-NWUCRBZB.js} +10 -9
- package/dist/{spirit-4ECYIVGE.js → spirit-I5ECLGRI.js} +16 -2
- package/dist/{spirit-availability-Y54Y3675.js → spirit-availability-NIUUYKU2.js} +11 -10
- package/dist/{sprout-GJMZFRGZ.js → sprout-A2PMEF7Z.js} +1 -1
- package/dist/{system-events-V43HDA3T.js → system-events-YTOU66QI.js} +5 -1
- package/dist/{systems-VDRUZXNF.js → systems-2DIJY4RF.js} +6 -6
- package/dist/{turn-tracker-C5PVIC3B.js → turn-tracker-NNHS6MAR.js} +4 -2
- package/dist/{up-H6LJZDRK.js → up-UGBHH743.js} +1 -1
- package/dist/{variant-cleanup-STC6B5HR.js → variant-cleanup-CZDLIY3F.js} +7 -5
- package/dist/{version-notify-LBVHH5YV.js → version-notify-SJVAQLZI.js} +4 -4
- package/dist/web-assets/assets/index-BHSU9r-4.css +1 -0
- package/dist/web-assets/assets/{index-B_eYTa3R.js → index-b7G7ZsVk.js} +13 -13
- package/dist/web-assets/index.html +2 -2
- package/package.json +1 -1
- package/templates/_base/.init/.config/prompts.json +1 -1
- package/templates/_base/src/lib/codex-session-seed.ts +463 -0
- 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/seed-note.ts +78 -0
- package/templates/_base/src/lib/session-seed.ts +375 -0
- package/templates/_base/src/lib/startup.ts +108 -4
- package/templates/claude/.init/CLAUDE.md +3 -3
- package/templates/claude/src/agent.ts +267 -89
- package/templates/claude/src/lib/hooks/pre-compact.ts +30 -5
- package/templates/claude/src/lib/stream-consumer.ts +13 -0
- package/templates/claude/src/server.ts +2 -1
- package/templates/codex/.init/AGENTS.md +3 -3
- package/templates/codex/src/agent.ts +256 -39
- package/templates/codex/src/server.ts +2 -1
- package/templates/pi/.init/MINDS.md +3 -3
- package/templates/pi/src/agent.ts +269 -43
- package/templates/pi/src/lib/event-handler.ts +11 -0
- package/templates/pi/src/server.ts +3 -1
- package/dist/web-assets/assets/index-DhZrzddW.css +0 -1
- package/dist/{backup-YVGV2ZNM.js → backup-WWXVIMYQ.js} +5 -5
- package/dist/{bridge-YK7NTRU5.js → bridge-KU2IKSH3.js} +3 -3
- package/dist/{channels-FSAHOQWI.js → channels-LW5ZLMSQ.js} +3 -3
- package/dist/{chunk-VPOMJMV6.js → chunk-BDHKCLWK.js} +0 -0
- package/dist/{chunk-EEOMA4DX.js → chunk-HTB7KDRU.js} +3 -3
- package/dist/{create-M727VUDF.js → create-C3XZACTV.js} +3 -3
- package/dist/{env-IBW2QI6K.js → env-BJGM6FBA.js} +5 -5
- package/dist/{extension-MRYJWSMV.js → extension-B2W52WZY.js} +3 -3
- package/dist/{list-UQGIW7OF.js → list-R2GDWOEE.js} +3 -3
- package/dist/{login-RJJTCVR3.js → login-EZ4RRDQX.js} +3 -3
- package/dist/{login-SB5OENMV.js → login-JDOAK465.js} +4 -4
- package/dist/{logout-YFZAJRPY.js → logout-K6ZGAJY5.js} +3 -3
- package/dist/{logout-BYV433X6.js → logout-MWSIUOQU.js} +3 -3
- package/dist/{mind-wake-LDGPRA42.js → mind-wake-DCDFFQKX.js} +3 -3
- package/dist/{read-RJ5NAW5J.js → read-J5RM674N.js} +3 -3
- package/dist/{register-OX4HRSGC.js → register-53LBGSDC.js} +3 -3
- package/dist/{restart-AGEGNSYQ.js → restart-R2WFNF5K.js} +4 -4
- package/dist/{skill-B5OJEQUA.js → skill-DXLLSUHB.js} +5 -5
- package/dist/{start-MUMNFR52.js → start-XDOUAWN3.js} +4 -4
- package/dist/{status-MWLE3RVC.js → status-EYG4A3OK.js} +3 -3
- package/dist/{stop-EKERNYTX.js → stop-O62RTEMD.js} +4 -4
|
@@ -2,6 +2,13 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve as resolvePath } from "node:path";
|
|
3
3
|
import { Codex } from "@openai/codex-sdk";
|
|
4
4
|
import { flushFileChanges, trackFileChange } from "./lib/auto-commit.js";
|
|
5
|
+
import {
|
|
6
|
+
type CodexSeedCut,
|
|
7
|
+
computeCodexSeedCut,
|
|
8
|
+
DEFAULT_SEED_TOKENS,
|
|
9
|
+
rotateCodexSession,
|
|
10
|
+
seedCodexSession,
|
|
11
|
+
} from "./lib/codex-session-seed.js";
|
|
5
12
|
import { extractText } from "./lib/content.js";
|
|
6
13
|
import {
|
|
7
14
|
countSdkInstructionTokens,
|
|
@@ -14,10 +21,17 @@ import {
|
|
|
14
21
|
readSkillDescriptions,
|
|
15
22
|
} from "./lib/context-breakdown.js";
|
|
16
23
|
import { daemonEmit, daemonRestart, type EventType } from "./lib/daemon-client.js";
|
|
24
|
+
import { compactTimestamp } from "./lib/format-prefix.js";
|
|
17
25
|
import { runHooks } from "./lib/hook-loader.js";
|
|
18
26
|
import { log, warn } from "./lib/logger.js";
|
|
27
|
+
import { buildSeededNote, type SeedCause } from "./lib/seed-note.js";
|
|
19
28
|
import { createSessionStore } from "./lib/session-store.js";
|
|
20
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
getStartupContext,
|
|
31
|
+
loadPrompts,
|
|
32
|
+
loadSystemPrompt,
|
|
33
|
+
renderCompactionWarning,
|
|
34
|
+
} from "./lib/startup.js";
|
|
21
35
|
import { filterEvent, loadTransparencyPreset } from "./lib/transparency.js";
|
|
22
36
|
import { turnContextFor } from "./lib/turn-context.js";
|
|
23
37
|
import type {
|
|
@@ -38,12 +52,14 @@ type CodexThread = {
|
|
|
38
52
|
): Promise<{ events: AsyncIterable<Record<string, any>> }>;
|
|
39
53
|
};
|
|
40
54
|
|
|
55
|
+
type QueuedMessage = { text: string; meta: HandlerMeta; isRotationWarning?: boolean };
|
|
56
|
+
|
|
41
57
|
type CodexSession = {
|
|
42
58
|
name: string;
|
|
43
59
|
thread: CodexThread | null;
|
|
44
60
|
listeners: Set<Listener>;
|
|
45
61
|
currentMessageId?: string;
|
|
46
|
-
messageQueue:
|
|
62
|
+
messageQueue: QueuedMessage[];
|
|
47
63
|
processing: boolean;
|
|
48
64
|
abortController?: AbortController;
|
|
49
65
|
messageChannels: Map<string, string>;
|
|
@@ -51,8 +67,40 @@ type CodexSession = {
|
|
|
51
67
|
/** The event note is a standing fact about events, so it fires once per session. */
|
|
52
68
|
eventNoteFired: boolean;
|
|
53
69
|
cumulativeInputTokens: number;
|
|
70
|
+
/**
|
|
71
|
+
* True when this session was seeded from a previous session's rollout (a restart
|
|
72
|
+
* restore) or a rotation. Consumed once, on the first turn, to inject the note.
|
|
73
|
+
*/
|
|
74
|
+
seeded: boolean;
|
|
75
|
+
/** When the seeded-from session was archived (epoch ms), for the gap note; null if unknown. */
|
|
76
|
+
seededArchivedAt: number | null;
|
|
77
|
+
/**
|
|
78
|
+
* The live Codex thread id, tracked in-memory (from thread.started) so rotation can
|
|
79
|
+
* locate the rollout even for ephemeral `new-*` sessions, which persist no pointer.
|
|
80
|
+
*/
|
|
81
|
+
currentThreadId: string | null;
|
|
82
|
+
/** Why the tail is seeded — picks the boundary note's wording. Last cause wins. */
|
|
83
|
+
seededCause: SeedCause;
|
|
84
|
+
/**
|
|
85
|
+
* The cut point pinned at warn time (the first-kept turn the mind was told would
|
|
86
|
+
* survive verbatim). Honored at rotation so the promise in the warning holds even
|
|
87
|
+
* though the rollout grows after the warning. Null until a warning fires.
|
|
88
|
+
*/
|
|
89
|
+
rotationCut: CodexSeedCut | null;
|
|
90
|
+
/** A rotation warning turn is queued/in-flight — suppress re-triggering until it rotates. */
|
|
91
|
+
rotatePending: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Back-to-back rotations that did NOT bring context under the threshold (reset by any
|
|
94
|
+
* healthy turn). Guards against a runaway loop when the tail alone can't fit — e.g. a
|
|
95
|
+
* system prompt (large MEMORY.md) that already fills most of the window, which rotation
|
|
96
|
+
* can't trim. Past the cap we stop rotating and defer to the SDK's native backstop.
|
|
97
|
+
*/
|
|
98
|
+
consecutiveRotations: number;
|
|
54
99
|
};
|
|
55
100
|
|
|
101
|
+
/** Stop self-rotating after this many back-to-back rotations that didn't reduce context. */
|
|
102
|
+
const MAX_CONSECUTIVE_ROTATIONS = 3;
|
|
103
|
+
|
|
56
104
|
// Loaded once at startup
|
|
57
105
|
const preset = loadTransparencyPreset();
|
|
58
106
|
|
|
@@ -79,6 +127,8 @@ export function createMind(options: {
|
|
|
79
127
|
model?: string;
|
|
80
128
|
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
81
129
|
maxContextTokens?: number;
|
|
130
|
+
/** Estimated-token budget for seeding a fresh persistent session. 0 disables. Default 30000. */
|
|
131
|
+
seedTokens?: number;
|
|
82
132
|
}): {
|
|
83
133
|
resolve: HandlerResolver;
|
|
84
134
|
getContextInfo: () => Promise<ContextInfo>;
|
|
@@ -87,6 +137,13 @@ export function createMind(options: {
|
|
|
87
137
|
const sessions = new Map<string, CodexSession>();
|
|
88
138
|
const prompts = loadPrompts();
|
|
89
139
|
const maxContextTokens = options.maxContextTokens;
|
|
140
|
+
const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
|
|
141
|
+
|
|
142
|
+
// The warning the mind gets when the context limit approaches: it authors summaries
|
|
143
|
+
// of the turns that will collapse, then the session rotates onto the verbatim tail.
|
|
144
|
+
// ${date}/${memory_size} are filled per-warning (the date rolls over and MEMORY.md
|
|
145
|
+
// changes as the mind edits it); ${cutoff} is filled with the pinned boundary.
|
|
146
|
+
const compactionMessage = () => renderCompactionWarning(prompts.compaction_warning);
|
|
90
147
|
|
|
91
148
|
if (maxContextTokens) {
|
|
92
149
|
log("mind", `compaction threshold: ${maxContextTokens} tokens`);
|
|
@@ -116,8 +173,14 @@ export function createMind(options: {
|
|
|
116
173
|
...(apiKey ? { apiKey } : {}),
|
|
117
174
|
config: {
|
|
118
175
|
model_instructions_file: promptPath,
|
|
119
|
-
//
|
|
120
|
-
|
|
176
|
+
// Rotation (mind-authored compaction) is the primary path: at maxContextTokens we
|
|
177
|
+
// warn the mind and rotate onto the verbatim tail. The SDK's native auto-compaction
|
|
178
|
+
// is only an emergency backstop — set above the volute threshold (1.5×) so it fires
|
|
179
|
+
// solely when rotation can't relieve context (e.g. a system prompt too large to fit
|
|
180
|
+
// the tail under the threshold), after the runaway guard stops self-rotating.
|
|
181
|
+
model_auto_compact_token_limit: maxContextTokens
|
|
182
|
+
? Math.floor(maxContextTokens * 1.5)
|
|
183
|
+
: 999999999,
|
|
121
184
|
// Enable reasoning summaries so they appear as events
|
|
122
185
|
model_reasoning_summary: "auto",
|
|
123
186
|
model_supports_reasoning_summaries: true,
|
|
@@ -150,6 +213,13 @@ export function createMind(options: {
|
|
|
150
213
|
firstMessagePerChannel: new Set(),
|
|
151
214
|
eventNoteFired: false,
|
|
152
215
|
cumulativeInputTokens: 0,
|
|
216
|
+
seeded: false,
|
|
217
|
+
seededArchivedAt: null,
|
|
218
|
+
currentThreadId: null,
|
|
219
|
+
seededCause: "restored",
|
|
220
|
+
rotationCut: null,
|
|
221
|
+
rotatePending: false,
|
|
222
|
+
consecutiveRotations: 0,
|
|
153
223
|
};
|
|
154
224
|
sessions.set(name, session);
|
|
155
225
|
|
|
@@ -157,44 +227,62 @@ export function createMind(options: {
|
|
|
157
227
|
return session;
|
|
158
228
|
}
|
|
159
229
|
|
|
230
|
+
/** Thread options shared by startThread, resumeThread, and rotation resume. */
|
|
231
|
+
function threadOptions() {
|
|
232
|
+
return {
|
|
233
|
+
workingDirectory: options.cwd,
|
|
234
|
+
model: options.model,
|
|
235
|
+
modelReasoningEffort: options.reasoningEffort,
|
|
236
|
+
skipGitRepoCheck: true,
|
|
237
|
+
// Codex's native seatbelt sandbox panics on macOS due to a bug in the
|
|
238
|
+
// system-configuration Rust crate (SCDynamicStore NULL object). Use full
|
|
239
|
+
// access until upstream ships a fix (mullvad/system-configuration-rs#59).
|
|
240
|
+
sandboxMode: "danger-full-access" as const,
|
|
241
|
+
networkAccessEnabled: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
160
245
|
function initSession(session: CodexSession) {
|
|
161
246
|
const isEphemeral = session.name.startsWith("new-");
|
|
162
247
|
log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
|
|
163
248
|
emit(session, { type: "session_start" });
|
|
164
249
|
|
|
165
250
|
if (!isEphemeral) {
|
|
166
|
-
|
|
167
|
-
if (
|
|
251
|
+
let resumeThreadId = sessionStore.load(session.name);
|
|
252
|
+
if (!resumeThreadId) {
|
|
253
|
+
// Fresh persistent session — seed it from the previous session's archived
|
|
254
|
+
// rollout so the mind experiences the conversation continuing rather than
|
|
255
|
+
// waking into an empty context.
|
|
256
|
+
const seeded = seedCodexSession({
|
|
257
|
+
mindDir: options.mindDir,
|
|
258
|
+
name: session.name,
|
|
259
|
+
seedTokens,
|
|
260
|
+
});
|
|
261
|
+
if (seeded) {
|
|
262
|
+
resumeThreadId = seeded.threadId;
|
|
263
|
+
session.seeded = true;
|
|
264
|
+
session.seededArchivedAt = seeded.archivedAt;
|
|
265
|
+
session.seededCause = "restored";
|
|
266
|
+
log("mind", `session "${session.name}": seeded from previous transcript`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (resumeThreadId) {
|
|
168
270
|
try {
|
|
169
|
-
log("mind", `session "${session.name}": resuming thread ${
|
|
170
|
-
session.thread = codex.resumeThread(
|
|
171
|
-
|
|
172
|
-
model: options.model,
|
|
173
|
-
modelReasoningEffort: options.reasoningEffort,
|
|
174
|
-
skipGitRepoCheck: true,
|
|
175
|
-
// Codex's native seatbelt sandbox panics on macOS due to a bug in the
|
|
176
|
-
// system-configuration Rust crate (SCDynamicStore NULL object). Use full
|
|
177
|
-
// access until upstream ships a fix (mullvad/system-configuration-rs#59).
|
|
178
|
-
sandboxMode: "danger-full-access",
|
|
179
|
-
networkAccessEnabled: true,
|
|
180
|
-
});
|
|
271
|
+
log("mind", `session "${session.name}": resuming thread ${resumeThreadId}`);
|
|
272
|
+
session.thread = codex.resumeThread(resumeThreadId, threadOptions());
|
|
273
|
+
session.currentThreadId = resumeThreadId;
|
|
181
274
|
return;
|
|
182
275
|
} catch (err) {
|
|
183
276
|
warn("mind", `session "${session.name}": failed to resume thread, starting new:`, err);
|
|
277
|
+
// We fell back to a truly fresh thread — don't tell the mind its
|
|
278
|
+
// conversation continued when the seeded rollout failed to resume.
|
|
279
|
+
session.seeded = false;
|
|
184
280
|
}
|
|
185
281
|
}
|
|
186
282
|
}
|
|
187
283
|
|
|
188
284
|
try {
|
|
189
|
-
session.thread = codex.startThread(
|
|
190
|
-
workingDirectory: options.cwd,
|
|
191
|
-
model: options.model,
|
|
192
|
-
modelReasoningEffort: options.reasoningEffort,
|
|
193
|
-
skipGitRepoCheck: true,
|
|
194
|
-
// See comment in resumeThread above
|
|
195
|
-
sandboxMode: "danger-full-access",
|
|
196
|
-
networkAccessEnabled: true,
|
|
197
|
-
});
|
|
285
|
+
session.thread = codex.startThread(threadOptions());
|
|
198
286
|
log("mind", `session "${session.name}": new thread started`);
|
|
199
287
|
} catch (err) {
|
|
200
288
|
warn("mind", `session "${session.name}": failed to start thread:`, err);
|
|
@@ -241,6 +329,24 @@ export function createMind(options: {
|
|
|
241
329
|
}
|
|
242
330
|
}
|
|
243
331
|
|
|
332
|
+
// On the first turn of a seeded session, prepend the honest-boundary note
|
|
333
|
+
// (consumed once) so the mind knows the tail above was restored (restart) or
|
|
334
|
+
// rotated (context limit). The restored note carries a coarse gap-duration
|
|
335
|
+
// clause when the archive time is known; the rotation note has no gap clause.
|
|
336
|
+
if (session.seeded) {
|
|
337
|
+
session.seeded = false;
|
|
338
|
+
const note = buildSeededNote({
|
|
339
|
+
cause: session.seededCause,
|
|
340
|
+
archivedAtMs: session.seededArchivedAt,
|
|
341
|
+
});
|
|
342
|
+
emit(session, {
|
|
343
|
+
type: "context",
|
|
344
|
+
content: note,
|
|
345
|
+
metadata: { source: "seeded-session" },
|
|
346
|
+
});
|
|
347
|
+
text = `${note}\n\n${text}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
244
350
|
// Run pre-prompt hooks
|
|
245
351
|
try {
|
|
246
352
|
const hookResult = await runHooks(hooksDir, "pre-prompt", {
|
|
@@ -312,11 +418,15 @@ export function createMind(options: {
|
|
|
312
418
|
try {
|
|
313
419
|
switch (event.type) {
|
|
314
420
|
case "thread.started": {
|
|
315
|
-
//
|
|
421
|
+
// Track the live thread id in-memory (used by rotation) and persist it for
|
|
422
|
+
// resume — persistent sessions only; ephemeral `new-*` keep no pointer.
|
|
316
423
|
const threadId = event.thread_id ?? event.threadId ?? event.thread?.id;
|
|
317
|
-
if (threadId
|
|
318
|
-
|
|
319
|
-
|
|
424
|
+
if (threadId) {
|
|
425
|
+
session.currentThreadId = threadId;
|
|
426
|
+
if (!session.name.startsWith("new-")) {
|
|
427
|
+
sessionStore.save(session.name, threadId);
|
|
428
|
+
log("mind", `session "${session.name}": saved thread ${threadId}`);
|
|
429
|
+
}
|
|
320
430
|
}
|
|
321
431
|
break;
|
|
322
432
|
}
|
|
@@ -514,14 +624,6 @@ export function createMind(options: {
|
|
|
514
624
|
daemonRestart({ type: "reload" }).catch((err) => log("mind", "daemonRestart failed:", err));
|
|
515
625
|
}
|
|
516
626
|
|
|
517
|
-
// Compaction warning — actual compaction is handled by the Codex SDK via model_auto_compact_token_limit
|
|
518
|
-
if (maxContextTokens && session.cumulativeInputTokens >= maxContextTokens) {
|
|
519
|
-
log(
|
|
520
|
-
"mind",
|
|
521
|
-
`session "${session.name}": ${session.cumulativeInputTokens} tokens >= ${maxContextTokens} — SDK auto-compaction will handle`,
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
627
|
log("mind", `session "${session.name}": turn done`);
|
|
526
628
|
} catch (err: any) {
|
|
527
629
|
if (err?.name === "AbortError") {
|
|
@@ -540,6 +642,115 @@ export function createMind(options: {
|
|
|
540
642
|
session.currentMessageId = undefined;
|
|
541
643
|
}
|
|
542
644
|
|
|
645
|
+
// --- Rotation (mind-authored compaction) ---
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Warn the mind that the session will rotate at the context limit and pin the cut
|
|
649
|
+
* point it's told about. Computes which whole turns fit in `seedTokens` from the live
|
|
650
|
+
* rollout, pins the first-kept turn's timestamp (so rotation honors the same boundary
|
|
651
|
+
* even as the rollout grows), interpolates its local time into the `${cutoff}`
|
|
652
|
+
* placeholder, and queues the warning as the mind's next (wrap-up) turn.
|
|
653
|
+
*/
|
|
654
|
+
function warnAndPinRotation(session: CodexSession) {
|
|
655
|
+
let cutoffLabel = "the cutoff";
|
|
656
|
+
session.rotationCut = null;
|
|
657
|
+
const threadId = session.currentThreadId;
|
|
658
|
+
if (threadId) {
|
|
659
|
+
try {
|
|
660
|
+
const path = findCodexSessionFile(threadId, options.mindDir);
|
|
661
|
+
const cut = path ? computeCodexSeedCut(readFileSync(path, "utf-8"), seedTokens) : null;
|
|
662
|
+
if (cut) {
|
|
663
|
+
session.rotationCut = cut;
|
|
664
|
+
const d = new Date(cut.boundaryTimestamp);
|
|
665
|
+
if (!Number.isNaN(d.getTime())) cutoffLabel = compactTimestamp(d);
|
|
666
|
+
}
|
|
667
|
+
} catch (err) {
|
|
668
|
+
log("mind", `session "${session.name}": failed to pin rotation cut:`, err);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff} prompt placeholder
|
|
672
|
+
const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
|
|
673
|
+
session.rotatePending = true;
|
|
674
|
+
// Run the warning next, ahead of any queued messages, so the mind wraps up before
|
|
675
|
+
// more context piles on. Remaining queued messages then run on the rotated thread.
|
|
676
|
+
session.messageQueue.unshift({
|
|
677
|
+
text: warning,
|
|
678
|
+
meta: { messageId: `rotation-warning-${Date.now()}` },
|
|
679
|
+
isRotationWarning: true,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* After the wrap-up warning turn completes, rotate the thread in place: build a seeded
|
|
685
|
+
* rollout from the pinned cut out of the live rollout, archive the rotated-out thread,
|
|
686
|
+
* and resume the new thread. On any failure we leave the old thread in place and let
|
|
687
|
+
* the SDK's native backstop handle it if context keeps climbing.
|
|
688
|
+
*/
|
|
689
|
+
function performRotation(session: CodexSession) {
|
|
690
|
+
session.rotatePending = false;
|
|
691
|
+
const oldThreadId = session.currentThreadId;
|
|
692
|
+
const cut = session.rotationCut;
|
|
693
|
+
session.rotationCut = null;
|
|
694
|
+
|
|
695
|
+
const newThreadId = oldThreadId
|
|
696
|
+
? rotateCodexSession({
|
|
697
|
+
mindDir: options.mindDir,
|
|
698
|
+
name: session.name,
|
|
699
|
+
oldThreadId,
|
|
700
|
+
cut,
|
|
701
|
+
seedTokens,
|
|
702
|
+
})
|
|
703
|
+
: null;
|
|
704
|
+
if (!newThreadId) {
|
|
705
|
+
log("mind", `session "${session.name}": rotation failed, deferring to SDK backstop`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
session.thread = codex.resumeThread(newThreadId, threadOptions());
|
|
711
|
+
} catch (err) {
|
|
712
|
+
warn("mind", `session "${session.name}": failed to resume rotated thread:`, err);
|
|
713
|
+
return; // keep the old thread; the SDK backstop covers a runaway
|
|
714
|
+
}
|
|
715
|
+
session.currentThreadId = newThreadId;
|
|
716
|
+
if (!session.name.startsWith("new-")) sessionStore.save(session.name, newThreadId);
|
|
717
|
+
// Fresh thread — reset token tracking (the next turn.completed sets the real value)
|
|
718
|
+
// and arm the rotation-cause boundary note for the mind's next turn.
|
|
719
|
+
session.cumulativeInputTokens = 0;
|
|
720
|
+
session.seeded = true;
|
|
721
|
+
session.seededCause = "rotation";
|
|
722
|
+
session.seededArchivedAt = null;
|
|
723
|
+
// Count this rotation; a healthy turn resets it. If back-to-back rotations don't
|
|
724
|
+
// reduce context (system prompt too large to fit the tail), the cap stops the loop.
|
|
725
|
+
session.consecutiveRotations++;
|
|
726
|
+
if (session.consecutiveRotations >= MAX_CONSECUTIVE_ROTATIONS) {
|
|
727
|
+
log(
|
|
728
|
+
"mind",
|
|
729
|
+
`session "${session.name}": ${session.consecutiveRotations} rotations without relief — deferring further compaction to the SDK (system prompt likely too large)`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
log("mind", `session "${session.name}": rotated to ${newThreadId}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Decide, after a normal (non-warning) turn, whether to warn + pin a rotation: only when
|
|
737
|
+
* a threshold is configured, context is at/over it, no rotation is already pending, and
|
|
738
|
+
* we're under the runaway cap. A turn that came back under the threshold ends any streak.
|
|
739
|
+
*/
|
|
740
|
+
function maybeWarnRotation(session: CodexSession) {
|
|
741
|
+
if (!maxContextTokens) return;
|
|
742
|
+
if (session.cumulativeInputTokens < maxContextTokens) {
|
|
743
|
+
session.consecutiveRotations = 0; // healthy turn — streak over
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (session.rotatePending || session.consecutiveRotations >= MAX_CONSECUTIVE_ROTATIONS) return;
|
|
747
|
+
log(
|
|
748
|
+
"mind",
|
|
749
|
+
`session "${session.name}": ${session.cumulativeInputTokens} tokens >= ${maxContextTokens} — warning + pinning rotation`,
|
|
750
|
+
);
|
|
751
|
+
warnAndPinRotation(session);
|
|
752
|
+
}
|
|
753
|
+
|
|
543
754
|
// --- Message queue processing ---
|
|
544
755
|
|
|
545
756
|
async function processQueue(session: CodexSession) {
|
|
@@ -550,6 +761,12 @@ export function createMind(options: {
|
|
|
550
761
|
const next = session.messageQueue.shift()!;
|
|
551
762
|
session.currentMessageId = next.meta.messageId;
|
|
552
763
|
await runTurn(session, next.text, next.meta);
|
|
764
|
+
if (next.isRotationWarning) {
|
|
765
|
+
// The wrap-up warning turn is done — rotate the thread in place.
|
|
766
|
+
performRotation(session);
|
|
767
|
+
} else {
|
|
768
|
+
maybeWarnRotation(session);
|
|
769
|
+
}
|
|
553
770
|
}
|
|
554
771
|
|
|
555
772
|
session.processing = false;
|
|
@@ -18,7 +18,7 @@ if (config.logLevel) setLevel(config.logLevel);
|
|
|
18
18
|
if (config.model) log("server", `using model: ${config.model}`);
|
|
19
19
|
if (config.reasoningEffort) log("server", `reasoning effort: ${config.reasoningEffort}`);
|
|
20
20
|
|
|
21
|
-
const systemPrompt = loadSystemPrompt();
|
|
21
|
+
const systemPrompt = loadSystemPrompt(config);
|
|
22
22
|
const pkg = loadPackageInfo();
|
|
23
23
|
|
|
24
24
|
const mindDir = resolve(".");
|
|
@@ -31,6 +31,7 @@ const mind = createMind({
|
|
|
31
31
|
model: config.model,
|
|
32
32
|
reasoningEffort: config.reasoningEffort,
|
|
33
33
|
maxContextTokens: config.compaction?.maxContextTokens,
|
|
34
|
+
seedTokens: config.continuity?.seedTokens,
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
const router = createRouter({
|
|
@@ -17,9 +17,9 @@ You can also reach out proactively — see the **volute-mind** skill.
|
|
|
17
17
|
|
|
18
18
|
Two-tier memory, both managed via file tools:
|
|
19
19
|
|
|
20
|
-
- **`MEMORY.md`** — Your
|
|
21
|
-
- **`memory/journal/YYYY-MM-DD.md
|
|
22
|
-
- Periodically consolidate
|
|
20
|
+
- **`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.
|
|
21
|
+
- **`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.
|
|
22
|
+
- Periodically consolidate: distill detail out of `MEMORY.md` into `memory/` files, leaving one-line index entries behind.
|
|
23
23
|
|
|
24
24
|
See the **memory** skill for detailed guidance.
|
|
25
25
|
|