volute 0.51.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/{auth-X6EMNEGB.js → auth-KRDSM55Q.js} +3 -1
- package/dist/{chat-VAZ25KVT.js → chat-WK2BLM7B.js} +9 -9
- package/dist/{chunk-VAEZCIJC.js → chunk-23UJGVYP.js} +9 -9
- package/dist/{chunk-VSBMLB2W.js → chunk-5DBGQBQR.js} +138 -13
- package/dist/chunk-5RBEEAKW.js +58 -0
- package/dist/{chunk-BSHZ23MS.js → chunk-7ARONCZW.js} +25 -3
- package/dist/{chunk-HKP6OAJP.js → chunk-7LT3YZUR.js} +12 -0
- package/dist/{chunk-FXWBFDQR.js → chunk-AP2KQMMW.js} +48 -9
- package/dist/{chunk-DMK5ASKV.js → chunk-F6D766VQ.js} +6 -2
- package/dist/{chunk-VP5PFNZO.js → chunk-HASADR3E.js} +73 -20
- package/dist/{chunk-ICL6T4SN.js → chunk-IQ7VNWH6.js} +26 -2
- package/dist/{chunk-G7A5LEVI.js → chunk-LN7XJ56R.js} +6 -6
- package/dist/{chunk-DN3LKLYR.js → chunk-P2TBUKLW.js} +3 -3
- package/dist/{chunk-Q7XQLKZB.js → chunk-PIZDPY4G.js} +20 -3
- package/dist/{chunk-JHKBALS2.js → chunk-RMADJFA7.js} +1 -1
- package/dist/{chunk-7HN63Q7J.js → chunk-RQRO6PC7.js} +40 -6
- package/dist/{chunk-QT4D5XCI.js → chunk-TAWRJ24T.js} +1 -1
- package/dist/{chunk-AI47FDGL.js → chunk-UO4MZQNO.js} +1 -1
- package/dist/chunk-UTKUO2KY.js +102 -0
- package/dist/{chunk-CDKQJWGK.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-GPXYPTIC.js → cloud-sync-MJVJ33LD.js} +11 -10
- package/dist/{daemon-restart-2EWBRTN5.js → daemon-restart-BIGU4OKS.js} +1 -1
- package/dist/daemon.js +543 -236
- package/dist/{delete-O7PEFQDF.js → delete-CYVZUAV2.js} +4 -0
- package/dist/{delivery-manager-HGVZSEES.js → delivery-manager-FDHCZ7PU.js} +7 -5
- package/dist/delivery-notices-65TB3CBX.js +18 -0
- package/dist/{echo-text-6ANI5HHN.js → echo-text-KU6G4Z7V.js} +12 -11
- package/dist/{extensions-NY6WST33.js → extensions-VACUKFA5.js} +11 -10
- package/dist/{files-BRY5RRIM.js → files-CHPNEQXN.js} +5 -4
- package/dist/{join-PYA36HEC.js → join-KLRU72JJ.js} +10 -2
- package/dist/{message-delivery-JHMAJ2GU.js → message-delivery-TUXM3AAE.js} +15 -12
- package/dist/{mind-WF6QIXWV.js → mind-IX5AWUST.js} +13 -13
- 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-DZCUUC7J.js → mind-service-WHFBUKZE.js} +11 -10
- 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-B6O5APCZ.js → package-BN5KMU54.js} +2 -2
- package/dist/{prompts-NBO3HRK3.js → prompts-HNKGCAEY.js} +1 -1
- package/dist/{reject-TWB4QJHG.js → reject-K2A4UK2Y.js} +9 -5
- package/dist/{scheduler-TNBN54ZK.js → scheduler-5X5UIQOQ.js} +7 -5
- package/dist/{seed-K2LYODIH.js → seed-LHZ33UB4.js} +1 -1
- package/dist/{seed-cmd-2EWBTR6P.js → seed-cmd-U4RVJ3UA.js} +2 -2
- package/dist/{seed-create-VGTLU5FD.js → seed-create-OQU4KNCB.js} +1 -1
- package/dist/{seed-readiness-DYOVCYHN.js → seed-readiness-WJYYRRCK.js} +2 -2
- package/dist/{seed-sprout-PILHJ3YS.js → seed-sprout-SFUYH2RG.js} +7 -7
- package/dist/{send-2YKOZJJ5.js → send-FWZAMDBN.js} +35 -20
- package/dist/{setup-MDPSK2XG.js → setup-RVSGCIF3.js} +1 -1
- package/dist/skills/memory/SKILL.md +47 -43
- package/dist/skills/volute-mind/references/variants.md +1 -1
- package/dist/{skills-NH2GQKAF.js → skills-QRPU6BWY.js} +1 -1
- package/dist/{sleep-manager-7JXEQQ2P.js → sleep-manager-NWUCRBZB.js} +11 -10
- package/dist/{spirit-25JVAL3M.js → spirit-I5ECLGRI.js} +16 -2
- package/dist/{spirit-availability-PJDWZJ3J.js → spirit-availability-NIUUYKU2.js} +12 -11
- package/dist/{sprout-EXUZMIOR.js → sprout-A2PMEF7Z.js} +1 -1
- package/dist/{system-events-ZPSH63QX.js → system-events-YTOU66QI.js} +7 -1
- package/dist/{systems-VDRUZXNF.js → systems-2DIJY4RF.js} +6 -6
- package/dist/{turn-tracker-TAT2HGK6.js → turn-tracker-NNHS6MAR.js} +4 -2
- package/dist/{up-J7XCAH7P.js → up-UGBHH743.js} +1 -1
- package/dist/{variant-cleanup-YXN25ZVW.js → variant-cleanup-CZDLIY3F.js} +7 -5
- package/dist/{version-notify-EN3KJ7HH.js → version-notify-SJVAQLZI.js} +4 -4
- package/dist/web-assets/assets/index-BHSU9r-4.css +1 -0
- package/dist/web-assets/assets/index-b7G7ZsVk.js +97 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +2 -2
- package/templates/_base/.init/.config/prompts.json +1 -1
- package/templates/_base/src/lib/auto-commit.ts +49 -11
- 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-BOyJ4Gqs.css +0 -1
- package/dist/web-assets/assets/index-CrxBwnzp.js +0 -95
- 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-IWAVIJO2.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
|
@@ -24,11 +24,25 @@ import {
|
|
|
24
24
|
import { daemonEmit } from "./lib/daemon-client.js";
|
|
25
25
|
import { dispatchPrompt } from "./lib/dispatch.js";
|
|
26
26
|
import { createEventHandler, emit } from "./lib/event-handler.js";
|
|
27
|
+
import { compactTimestamp } from "./lib/format-prefix.js";
|
|
27
28
|
import { runHooks } from "./lib/hook-loader.js";
|
|
28
29
|
import { log } from "./lib/logger.js";
|
|
30
|
+
import {
|
|
31
|
+
computePiSeedCut,
|
|
32
|
+
DEFAULT_SEED_TOKENS,
|
|
33
|
+
type PiSeedCut,
|
|
34
|
+
rotatePiSession,
|
|
35
|
+
seedPiSession,
|
|
36
|
+
} from "./lib/pi-session-seed.js";
|
|
29
37
|
import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
|
|
30
38
|
import { resolveModel } from "./lib/resolve-model.js";
|
|
31
|
-
import {
|
|
39
|
+
import { buildSeededNote, type SeedCause } from "./lib/seed-note.js";
|
|
40
|
+
import {
|
|
41
|
+
getStartupContext,
|
|
42
|
+
loadPrompts,
|
|
43
|
+
renderCompactionWarning,
|
|
44
|
+
type SubagentConfig,
|
|
45
|
+
} from "./lib/startup.js";
|
|
32
46
|
import { createSubagentExtension, type SubagentDefinition } from "./lib/subagents.js";
|
|
33
47
|
import type {
|
|
34
48
|
HandlerMeta,
|
|
@@ -52,16 +66,51 @@ type PiSession = {
|
|
|
52
66
|
currentMessageId?: string;
|
|
53
67
|
messageChannels: Map<string, { channel: string; sender?: string }>;
|
|
54
68
|
contextTokens: number;
|
|
69
|
+
/**
|
|
70
|
+
* True when this session was seeded from the previous session's transcript.
|
|
71
|
+
* Consumed once, on the first turn, to inject the honest-boundary note.
|
|
72
|
+
*/
|
|
73
|
+
seeded?: boolean;
|
|
74
|
+
/** When the seeded-from session was archived (epoch ms), for the gap note; null if unknown. */
|
|
75
|
+
seededArchivedAt?: number | null;
|
|
76
|
+
/** Why the tail is seeded — picks the boundary note's wording. Last cause wins. */
|
|
77
|
+
seededCause?: SeedCause;
|
|
78
|
+
/**
|
|
79
|
+
* Set after an in-place rotation so the mind is told, on its next turn, that the
|
|
80
|
+
* session rotated at the context limit. Delivered separately from the restored-seed
|
|
81
|
+
* note (which fires only on the first turn of a freshly created agent session).
|
|
82
|
+
*/
|
|
83
|
+
rotationNotePending?: boolean;
|
|
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 transcript grows after the warning. Null until a warning fires.
|
|
88
|
+
*/
|
|
89
|
+
rotationCut?: PiSeedCut | null;
|
|
90
|
+
/**
|
|
91
|
+
* Back-to-back rotations that did NOT bring context under the threshold (reset by
|
|
92
|
+
* any healthy turn). Guards against a runaway loop when the tail alone can't fit —
|
|
93
|
+
* e.g. a system prompt (large MEMORY.md) that already fills most of the window,
|
|
94
|
+
* which rotation can't trim. Past the cap we defer to the SDK's native compaction.
|
|
95
|
+
*/
|
|
96
|
+
consecutiveRotations?: number;
|
|
55
97
|
};
|
|
56
98
|
|
|
99
|
+
/** Stop self-rotating after this many back-to-back rotations that didn't reduce context. */
|
|
100
|
+
const MAX_CONSECUTIVE_ROTATIONS = 3;
|
|
101
|
+
|
|
57
102
|
export function createMind(options: {
|
|
58
103
|
systemPrompt: string;
|
|
59
104
|
cwd: string;
|
|
60
105
|
mindDir: string;
|
|
106
|
+
/** Directory holding pi session subdirs (`<sessionsDir>/<name>/*.jsonl`). */
|
|
107
|
+
sessionsDir: string;
|
|
61
108
|
model?: string;
|
|
62
109
|
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
63
110
|
compactionMessage?: string;
|
|
64
111
|
maxContextTokens?: number;
|
|
112
|
+
/** Estimated-token budget for seeding a fresh persistent session. 0 disables. Default 30000. */
|
|
113
|
+
seedTokens?: number;
|
|
65
114
|
subagents?: Record<string, SubagentConfig>;
|
|
66
115
|
}): {
|
|
67
116
|
resolve: HandlerResolver;
|
|
@@ -70,12 +119,11 @@ export function createMind(options: {
|
|
|
70
119
|
} {
|
|
71
120
|
const sessions = new Map<string, PiSession>();
|
|
72
121
|
const prompts = loadPrompts();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} in prompt template
|
|
76
|
-
options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
|
|
122
|
+
const compactionMessage = () =>
|
|
123
|
+
options.compactionMessage ?? renderCompactionWarning(prompts.compaction_warning);
|
|
77
124
|
const compactionInstructions = prompts.compaction_instructions;
|
|
78
125
|
const maxContextTokens = options.maxContextTokens;
|
|
126
|
+
const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
|
|
79
127
|
|
|
80
128
|
if (maxContextTokens) {
|
|
81
129
|
log("mind", `compaction threshold: ${maxContextTokens} tokens`);
|
|
@@ -183,6 +231,22 @@ export function createMind(options: {
|
|
|
183
231
|
// Inject startup context on the first turn of each session
|
|
184
232
|
if (!startupContextInjected) {
|
|
185
233
|
startupContextInjected = true;
|
|
234
|
+
// On the first turn of a seeded session, lead with the honest-boundary
|
|
235
|
+
// note (consumed once) so the restored transcript above isn't mistaken
|
|
236
|
+
// for a continuously-lived conversation.
|
|
237
|
+
if (session.seeded) {
|
|
238
|
+
session.seeded = false;
|
|
239
|
+
const note = buildSeededNote({
|
|
240
|
+
cause: "restored",
|
|
241
|
+
archivedAtMs: session.seededArchivedAt ?? null,
|
|
242
|
+
});
|
|
243
|
+
emit(session, {
|
|
244
|
+
type: "context",
|
|
245
|
+
content: note,
|
|
246
|
+
metadata: { source: "seeded-session" },
|
|
247
|
+
});
|
|
248
|
+
parts.push(note);
|
|
249
|
+
}
|
|
186
250
|
const startupContext = await startupContextPromise;
|
|
187
251
|
if (startupContext) {
|
|
188
252
|
emit(session, {
|
|
@@ -194,6 +258,20 @@ export function createMind(options: {
|
|
|
194
258
|
}
|
|
195
259
|
}
|
|
196
260
|
|
|
261
|
+
// On the first turn after an in-place rotation, tell the mind the earlier
|
|
262
|
+
// turns collapsed at the context limit (distinct from the restored-seed note,
|
|
263
|
+
// which only fires on a freshly created agent session). Consumed once.
|
|
264
|
+
if (session.rotationNotePending) {
|
|
265
|
+
session.rotationNotePending = false;
|
|
266
|
+
const note = buildSeededNote({ cause: "rotation" });
|
|
267
|
+
emit(session, {
|
|
268
|
+
type: "context",
|
|
269
|
+
content: note,
|
|
270
|
+
metadata: { source: "seeded-session" },
|
|
271
|
+
});
|
|
272
|
+
parts.push(note);
|
|
273
|
+
}
|
|
274
|
+
|
|
197
275
|
try {
|
|
198
276
|
const result = await runHooks(hooksDir, "pre-prompt", {
|
|
199
277
|
event: "pre-prompt",
|
|
@@ -273,16 +351,49 @@ export function createMind(options: {
|
|
|
273
351
|
async function initSession(session: PiSession) {
|
|
274
352
|
const isEphemeral = session.name.startsWith("new-");
|
|
275
353
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
354
|
+
// Fresh persistent session — seed it from the previous session's archived
|
|
355
|
+
// transcript so the mind experiences the conversation continuing rather than
|
|
356
|
+
// waking into an empty context. seedPiSession no-ops when a live session
|
|
357
|
+
// already exists (continueRecent will resume that instead). Ephemeral
|
|
358
|
+
// `new-*` sessions are one-offs — they never seed at start.
|
|
359
|
+
const seeded = isEphemeral
|
|
360
|
+
? null
|
|
361
|
+
: seedPiSession({
|
|
362
|
+
cwd: options.cwd,
|
|
363
|
+
piSessionsDir: options.sessionsDir,
|
|
364
|
+
name: session.name,
|
|
365
|
+
seedTokens,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Every session (ephemeral too) is file-backed under its own name-scoped dir,
|
|
369
|
+
// so rotation applies uniformly at the context limit. Ephemerality is only that
|
|
370
|
+
// `new-*` sessions never seed at start and never archive on rotation — not a
|
|
371
|
+
// different backing store. Their unique `new-<ts>-<rand>` names mean the dir is
|
|
372
|
+
// always fresh, so continueRecent starts them empty (no seed to adopt).
|
|
373
|
+
const sessionManager = SessionManager.continueRecent(
|
|
374
|
+
options.cwd,
|
|
375
|
+
resolvePath(options.sessionsDir, session.name),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// If continueRecent adopted our seed file, its header id is the seeded id;
|
|
379
|
+
// if it fell back to a truly fresh session it minted a different id, so the
|
|
380
|
+
// honest-boundary note never lies about a conversation that didn't continue.
|
|
381
|
+
if (seeded && sessionManager.getSessionId() === seeded.sessionId) {
|
|
382
|
+
session.seeded = true;
|
|
383
|
+
session.seededArchivedAt = seeded.archivedAt;
|
|
384
|
+
session.seededCause = "restored";
|
|
385
|
+
log("mind", `session "${session.name}": seeded from previous transcript`);
|
|
386
|
+
}
|
|
279
387
|
|
|
280
388
|
log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
|
|
281
389
|
|
|
282
|
-
// Compaction state machine:
|
|
283
|
-
// 1. onContextTokens
|
|
390
|
+
// Compaction state machine (rotation, not SDK /compact):
|
|
391
|
+
// 1. threshold (onContextTokens) or native PreCompact -> warnAndPinRotation:
|
|
392
|
+
// pin the cut from the live transcript, interpolate it into the ${cutoff}
|
|
393
|
+
// warning, send it as the wrap-up prompt, set compactionTriggered
|
|
284
394
|
// 2. onTurnEnd (after warning turn): compactionTriggered -> compactOnNextTurnEnd
|
|
285
|
-
// 3. onTurnEnd (after mind's
|
|
395
|
+
// 3. onTurnEnd (after mind's wrap-up turn): compactOnNextTurnEnd -> rotateInPlace
|
|
396
|
+
// (or, past the runaway cap, the native compact() backstop)
|
|
286
397
|
let compactBlocked = false;
|
|
287
398
|
let manualCompactPending = false;
|
|
288
399
|
let compactionTriggered = false;
|
|
@@ -295,31 +406,144 @@ export function createMind(options: {
|
|
|
295
406
|
compactionInProgress = false;
|
|
296
407
|
}
|
|
297
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Warn the mind that the session will rotate at the context limit and pin the cut
|
|
411
|
+
* point it's told about. Computes which whole turns fit in `seedTokens` from the
|
|
412
|
+
* live transcript, pins the first-kept turn (so rotation honors the same boundary
|
|
413
|
+
* even as the transcript grows), interpolates its local time into the `${cutoff}`
|
|
414
|
+
* placeholder, and sends the warning as the wrap-up prompt. Shared by the
|
|
415
|
+
* threshold and native-PreCompact paths.
|
|
416
|
+
*/
|
|
417
|
+
function warnAndPinRotation() {
|
|
418
|
+
let cutoffLabel = "the cutoff";
|
|
419
|
+
session.rotationCut = null;
|
|
420
|
+
const sourcePath = session.agentSession?.sessionManager.getSessionFile();
|
|
421
|
+
if (sourcePath) {
|
|
422
|
+
try {
|
|
423
|
+
const cut = computePiSeedCut(readFileSync(sourcePath, "utf-8"), seedTokens);
|
|
424
|
+
if (cut) {
|
|
425
|
+
session.rotationCut = cut;
|
|
426
|
+
if (cut.boundaryTimestamp) {
|
|
427
|
+
const d = new Date(cut.boundaryTimestamp);
|
|
428
|
+
if (!Number.isNaN(d.getTime())) cutoffLabel = compactTimestamp(d);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
log("mind", `session "${session.name}": failed to pin rotation cut:`, err);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff} prompt placeholder
|
|
436
|
+
const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
|
|
437
|
+
compactionTriggered = true;
|
|
438
|
+
session.messageIds.push(undefined);
|
|
439
|
+
session.agentSession?.prompt(warning, { streamingBehavior: "followUp" });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Rotate the session in place onto a synthetic session holding the verbatim recent
|
|
444
|
+
* tail (from the pinned cut), then switch the running SessionManager to it. Returns
|
|
445
|
+
* false if rotation can't proceed (session not ready, or the file build failed) so
|
|
446
|
+
* the caller can fall back to a fresh session. The setSessionFile + state.messages
|
|
447
|
+
* re-sync is the load-bearing adoption step: the agent caches context in
|
|
448
|
+
* state.messages, so swapping the file alone wouldn't change what the mind sees
|
|
449
|
+
* (this mirrors the SDK's own compact() re-sync).
|
|
450
|
+
*/
|
|
451
|
+
function rotateInPlace(): boolean {
|
|
452
|
+
const as = session.agentSession;
|
|
453
|
+
const sourcePath = as?.sessionManager.getSessionFile();
|
|
454
|
+
if (!as || !sourcePath) return false; // not ready — fall back to fresh
|
|
455
|
+
const newPath = rotatePiSession({
|
|
456
|
+
cwd: options.cwd,
|
|
457
|
+
sessionsDir: options.sessionsDir,
|
|
458
|
+
name: session.name,
|
|
459
|
+
sourcePath,
|
|
460
|
+
cut: session.rotationCut ?? null,
|
|
461
|
+
seedTokens,
|
|
462
|
+
});
|
|
463
|
+
session.rotationCut = null;
|
|
464
|
+
if (!newPath) return false;
|
|
465
|
+
as.sessionManager.setSessionFile(newPath);
|
|
466
|
+
as.state.messages = as.sessionManager.buildSessionContext().messages;
|
|
467
|
+
session.rotationNotePending = true;
|
|
468
|
+
session.seededCause = "rotation";
|
|
469
|
+
session.consecutiveRotations = (session.consecutiveRotations ?? 0) + 1;
|
|
470
|
+
compactBlocked = false; // re-arm the native-compaction backstop
|
|
471
|
+
log(
|
|
472
|
+
"mind",
|
|
473
|
+
`session "${session.name}": rotated in place → ${as.sessionManager.getSessionId()} (rotation ${session.consecutiveRotations})`,
|
|
474
|
+
);
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Rotation couldn't proceed — start a fresh session in place (new empty transcript,
|
|
480
|
+
* cleared context), matching the claude template's rotation-failure fallback. Fresh
|
|
481
|
+
* is a far smaller cliff now that seeding exists; crucially it is NOT a silent native
|
|
482
|
+
* compaction.
|
|
483
|
+
*/
|
|
484
|
+
function freshFallback() {
|
|
485
|
+
const as = session.agentSession;
|
|
486
|
+
if (as) {
|
|
487
|
+
as.sessionManager.newSession();
|
|
488
|
+
as.state.messages = [];
|
|
489
|
+
}
|
|
490
|
+
session.rotationCut = null;
|
|
491
|
+
session.consecutiveRotations = 0;
|
|
492
|
+
compactBlocked = false;
|
|
493
|
+
log("mind", `session "${session.name}": rotation failed, starting fresh`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The SDK's native compaction, used as the emergency backstop past the runaway cap —
|
|
498
|
+
* when rotation can't relieve context (e.g. the system prompt alone fills most of the
|
|
499
|
+
* window, so no tail fits under the threshold). The sole remaining use of the custom
|
|
500
|
+
* compaction instructions.
|
|
501
|
+
*/
|
|
502
|
+
function runBackstopCompact() {
|
|
503
|
+
manualCompactPending = true;
|
|
504
|
+
compactionInProgress = true;
|
|
505
|
+
log("mind", `session "${session.name}": rotation cap reached — native compaction backstop`);
|
|
506
|
+
Promise.resolve(session.agentSession?.compact(compactionInstructions))
|
|
507
|
+
.catch((err) => log("mind", `session "${session.name}": backstop compact() failed:`, err))
|
|
508
|
+
.finally(() => {
|
|
509
|
+
compactionInProgress = false;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
298
513
|
const preCompactExtension: ExtensionFactory = (pi) => {
|
|
299
514
|
pi.on("session_before_compact", () => {
|
|
300
|
-
// Our
|
|
515
|
+
// Our own backstop compact() call (past the runaway cap) — allow through.
|
|
301
516
|
if (manualCompactPending) {
|
|
302
517
|
manualCompactPending = false;
|
|
303
|
-
log(
|
|
304
|
-
"mind",
|
|
305
|
-
`session "${session.name}": allowing manual compaction with custom instructions`,
|
|
306
|
-
);
|
|
518
|
+
log("mind", `session "${session.name}": allowing native compaction backstop`);
|
|
307
519
|
return;
|
|
308
520
|
}
|
|
309
521
|
|
|
310
|
-
//
|
|
522
|
+
// The SDK's native auto-compaction wants to fire. Converge it onto rotation:
|
|
523
|
+
// the first pass warns + pins + marks for rotation (unless the threshold path
|
|
524
|
+
// already did, or the runaway cap is hit) and blocks the native compaction;
|
|
525
|
+
// the second pass allows native compaction as the emergency backstop (only
|
|
526
|
+
// reachable if rotation never brought context under control).
|
|
311
527
|
if (!compactBlocked) {
|
|
312
528
|
compactBlocked = true;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
529
|
+
if (
|
|
530
|
+
!compactionTriggered &&
|
|
531
|
+
!compactOnNextTurnEnd &&
|
|
532
|
+
!compactionInProgress &&
|
|
533
|
+
(session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
|
|
534
|
+
) {
|
|
535
|
+
log(
|
|
536
|
+
"mind",
|
|
537
|
+
`session "${session.name}": native compaction — warning + pinning rotation`,
|
|
538
|
+
);
|
|
539
|
+
warnAndPinRotation();
|
|
540
|
+
} else {
|
|
541
|
+
log("mind", `session "${session.name}": blocking native compaction (rotation pending)`);
|
|
542
|
+
}
|
|
319
543
|
return { cancel: true };
|
|
320
544
|
}
|
|
321
545
|
compactBlocked = false;
|
|
322
|
-
log("mind", `session "${session.name}": allowing compaction`);
|
|
546
|
+
log("mind", `session "${session.name}": allowing native compaction backstop`);
|
|
323
547
|
});
|
|
324
548
|
};
|
|
325
549
|
|
|
@@ -372,7 +596,9 @@ export function createMind(options: {
|
|
|
372
596
|
maxContextTokens &&
|
|
373
597
|
tokens >= maxContextTokens &&
|
|
374
598
|
!compactionTriggered &&
|
|
375
|
-
!
|
|
599
|
+
!compactOnNextTurnEnd &&
|
|
600
|
+
!compactionInProgress &&
|
|
601
|
+
(session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
|
|
376
602
|
) {
|
|
377
603
|
if (!session.agentSession) {
|
|
378
604
|
log(
|
|
@@ -381,39 +607,39 @@ export function createMind(options: {
|
|
|
381
607
|
);
|
|
382
608
|
return;
|
|
383
609
|
}
|
|
384
|
-
compactionTriggered = true;
|
|
385
610
|
log(
|
|
386
611
|
"mind",
|
|
387
|
-
`session "${session.name}": ${tokens} tokens >= ${maxContextTokens} —
|
|
612
|
+
`session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — warning + pinning rotation`,
|
|
388
613
|
);
|
|
389
|
-
|
|
390
|
-
session.messageIds.push(undefined);
|
|
391
|
-
session.agentSession.prompt(compactionMessage, {
|
|
392
|
-
streamingBehavior: "followUp",
|
|
393
|
-
});
|
|
614
|
+
warnAndPinRotation();
|
|
394
615
|
}
|
|
395
616
|
},
|
|
396
617
|
onTurnEnd: maxContextTokens
|
|
397
618
|
? () => {
|
|
398
619
|
try {
|
|
399
|
-
//
|
|
620
|
+
// The wrap-up turn (after the warning) is done — rotate the session in
|
|
621
|
+
// place onto the verbatim tail, honoring the pinned cut.
|
|
400
622
|
if (compactOnNextTurnEnd) {
|
|
401
623
|
compactOnNextTurnEnd = false;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
624
|
+
if ((session.consecutiveRotations ?? 0) >= MAX_CONSECUTIVE_ROTATIONS) {
|
|
625
|
+
// Runaway guard: rotation isn't relieving context (system prompt too
|
|
626
|
+
// large to fit the tail under the threshold) — defer to the SDK.
|
|
627
|
+
runBackstopCompact();
|
|
628
|
+
} else if (!rotateInPlace()) {
|
|
629
|
+
// Rotation couldn't proceed — fresh session, not a silent native
|
|
630
|
+
// compaction.
|
|
631
|
+
freshFallback();
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
412
634
|
}
|
|
413
635
|
if (compactionTriggered) {
|
|
414
636
|
compactionTriggered = false;
|
|
415
637
|
compactOnNextTurnEnd = true;
|
|
638
|
+
return;
|
|
416
639
|
}
|
|
640
|
+
// A healthy turn (context under the threshold) — the rotation streak,
|
|
641
|
+
// if any, is over, so re-arm the self-rotation cap.
|
|
642
|
+
session.consecutiveRotations = 0;
|
|
417
643
|
} catch (err) {
|
|
418
644
|
log(
|
|
419
645
|
"mind",
|
|
@@ -144,6 +144,17 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
144
144
|
if (messageId) {
|
|
145
145
|
session.messageChannels.delete(messageId);
|
|
146
146
|
}
|
|
147
|
+
// pi's agent loop drains every queued prompt (follow-ups, steers) before
|
|
148
|
+
// a final agent_end, so anything still pending here was consumed by this
|
|
149
|
+
// turn. Prune it all — a stranded id would be shifted in as the NEXT
|
|
150
|
+
// turn's id, tagging that turn's rows with the wrong channel (#700).
|
|
151
|
+
// Exception: on a retryable error the loop emits agent_end WITHOUT
|
|
152
|
+
// draining and the session retries — those queued prompts haven't run
|
|
153
|
+
// yet, so they keep their entries for the continuation.
|
|
154
|
+
if (!event.willRetry) {
|
|
155
|
+
session.messageIds.length = 0;
|
|
156
|
+
session.messageChannels.clear();
|
|
157
|
+
}
|
|
147
158
|
const channel = messageId ? session.messageChannels.get(messageId)?.channel : undefined;
|
|
148
159
|
log("mind", `session "${session.name}": turn done`);
|
|
149
160
|
// Collect any agent-level errors (e.g. a provider 401 reported inside agent_end).
|
|
@@ -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.thinkingLevel) log("server", `thinking level: ${config.thinkingLevel}`);
|
|
20
20
|
|
|
21
|
-
const systemPrompt = loadSystemPrompt();
|
|
21
|
+
const systemPrompt = loadSystemPrompt(config);
|
|
22
22
|
const pkg = loadPackageInfo();
|
|
23
23
|
|
|
24
24
|
const mindDir = resolve(".");
|
|
@@ -26,10 +26,12 @@ const mind = createMind({
|
|
|
26
26
|
systemPrompt,
|
|
27
27
|
cwd: resolve("home"),
|
|
28
28
|
mindDir,
|
|
29
|
+
sessionsDir: resolve(".mind/pi-sessions"),
|
|
29
30
|
model: config.model,
|
|
30
31
|
thinkingLevel: config.thinkingLevel,
|
|
31
32
|
compactionMessage: config.compactionMessage,
|
|
32
33
|
maxContextTokens: config.compaction?.maxContextTokens,
|
|
34
|
+
seedTokens: config.continuity?.seedTokens,
|
|
33
35
|
subagents: config.subagents,
|
|
34
36
|
});
|
|
35
37
|
|