mirror-mind 0.36.3 → 0.38.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/.pi/extensions/mirror-logger.ts +173 -46
- package/.pi/skills/mm-mirror/SKILL.md +25 -6
- package/README.md +2 -2
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.37.0.md +221 -0
- package/docs/releases/v0.38.0.md +215 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/plugin.json +1 -1
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +14 -6
- package/ts/src/builder/commands.ts +9 -6
- package/ts/src/builder/load.ts +14 -2
- package/ts/src/conversation/closeTail.ts +7 -3
- package/ts/src/conversation/extraction.ts +1 -11
- package/ts/src/conversation/logger.ts +12 -6
- package/ts/src/conversation/userName.ts +44 -0
- package/ts/src/db/migrateOnOpen.ts +1 -1
- package/ts/src/db/migrations.ts +16 -0
- package/ts/src/db/schema.ts +0 -1
- package/ts/src/db/schemaInventorySnapshot.ts +1 -8
- package/ts/src/db/schemaState.ts +2 -0
- package/ts/src/db/schemaTsDivergence.ts +25 -4
- package/ts/src/extraction/conversationMetadata.ts +53 -8
- package/ts/src/extraction/prompts.ts +8 -3
- package/ts/src/frontDoor/cli.ts +42 -2
- package/ts/src/frontDoor/exploreRoute.ts +2 -2
- package/ts/src/frontDoor/mirrorModeRoute.ts +129 -5
- package/ts/src/frontDoor/routing.ts +16 -15
- package/ts/src/frontDoor/runtimeRoute.ts +4 -0
- package/ts/src/frontDoor/soulRoute.ts +4 -4
- package/ts/src/guards/testTempHomes.ts +92 -0
- package/ts/src/hooks/agy.ts +136 -59
- package/ts/src/hooks/claude.ts +29 -90
- package/ts/src/hooks/mirrorState.ts +10 -22
- package/ts/src/mirror/context.ts +19 -4
- package/ts/src/mirror/defaultResolution.ts +2 -1
- package/ts/src/mirror/doorman.ts +420 -0
- package/ts/src/mirror/orchestration.ts +22 -3
- package/ts/src/mirror/render.ts +7 -1
- package/ts/src/mirror/runtimeSession.ts +66 -8
- package/ts/src/mode/operatingMode.ts +22 -4
- package/ts/src/providers/config.ts +105 -1
- package/ts/src/providers/cost.ts +5 -1
- package/ts/src/providers/llm.ts +40 -0
- package/ts/src/providers/transport.ts +14 -0
- package/ts/src/runtime/diagnose.ts +64 -7
- package/ts/src/runtime/pluginWiring.ts +6 -4
- package/ts/src/runtime/status.ts +24 -4
- package/ts/src/runtime/update.ts +17 -1
- package/ts/src/runtime/wiring.ts +14 -1
- package/ts/src/search/memorySearch.ts +38 -18
- package/ts/src/search/ranker.ts +16 -2
|
@@ -5,14 +5,31 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Events handled:
|
|
7
7
|
* - session_start → unmute + welcome card + status line, in ONE front-door
|
|
8
|
-
* process (CV9.E2.S32); maintenance detached
|
|
8
|
+
* process (CV9.E2.S32); maintenance detached; the session
|
|
9
|
+
* named in the environment (`MIRROR_SESSION_ID`, CV9.DS8.US5)
|
|
9
10
|
* - input → what the person typed, kept for the run it starts (CR160)
|
|
10
|
-
* - before_agent_start → the turn to the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* - before_agent_start → the turn to the one doorman (`mirror doorman`), AWAITED:
|
|
12
|
+
* its block, when there is one, is the run's message, so
|
|
13
|
+
* Mirror's context is before the model on this turn
|
|
14
|
+
* (CV9.DS8.US5); then the turn to the core's prompt rule
|
|
15
|
+
* (`user-prompt`), with a turn id minted for it, NOT
|
|
16
|
+
* awaited: queued on the per-session chain behind the
|
|
17
|
+
* doorman, so the turn starts at once (CV9.E2.S32)
|
|
13
18
|
* - agent_end → the run's assistant text, kept for the prompt's reply
|
|
14
19
|
* - agent_settled → the prompt's whole reply to `stop`, after the chain settles
|
|
15
|
-
* -
|
|
20
|
+
* - session_compact → the folder left the transcript: `mirror doorman --forget`,
|
|
21
|
+
* so the next ordinary turn is delivered whole (CV9.DS8.US5)
|
|
22
|
+
* - session_shutdown → close conversation (after the chain settles) + backup;
|
|
23
|
+
* the session's name cleared from the environment
|
|
24
|
+
*
|
|
25
|
+
* The session's name (CV9.DS8.US5, from TS1's P2 measurement on Pi 1.4.0): at
|
|
26
|
+
* `session_start` this extension sets `MIRROR_SESSION_ID` in Pi's OWN process
|
|
27
|
+
* environment, which every later `pi.exec` and every bash command the model runs
|
|
28
|
+
* inherits. So a `mirror mirror load` the model runs by itself on an unprompted
|
|
29
|
+
* personal question, and a `mirror build load` on a mode switch, name THIS
|
|
30
|
+
* session's row instead of guessing one -- and the doorman, which reads that row,
|
|
31
|
+
* takes the next turn. Named-session behaviors that were dormant on Pi while no
|
|
32
|
+
* session was named wake with it (`mirror log`, journey binding, the mode stamps).
|
|
16
33
|
*
|
|
17
34
|
* All heavy logic lives in the TypeScript core, reached through the `mirror`
|
|
18
35
|
* bin that ships beside this file (`bin/mirror.js`). This extension is a thin
|
|
@@ -203,26 +220,48 @@ type MirrorStatusContext = {
|
|
|
203
220
|
};
|
|
204
221
|
|
|
205
222
|
/**
|
|
206
|
-
* One
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* two
|
|
210
|
-
*
|
|
211
|
-
*
|
|
223
|
+
* One extension, reached twice. Pi can reach this file as the project's
|
|
224
|
+
* `.pi/extensions/` inside the checkout AND as a personal package installed
|
|
225
|
+
* from `npm root -g`, which `npm link` makes the same file under two paths:
|
|
226
|
+
* two module instances in one process (CV22.DS10.US3 plateau 2). Which of
|
|
227
|
+
* them Pi keeps is Pi's: 1.4.0 dedupes the two by their real path and keeps
|
|
228
|
+
* the one it loaded second; an older Pi kept both. A "register once per
|
|
229
|
+
* process" guard chose the first -- and on 1.4.0 that was the instance Pi
|
|
230
|
+
* dropped, so the checkout's Pi had no handlers at all (found by CV9.DS8.US5's
|
|
231
|
+
* live walk). So every instance registers, and the guard is on the EVENT: the
|
|
232
|
+
* handled set is process-scoped, and the first instance to see an event
|
|
233
|
+
* object handles it, the other returns. Pi hands every registration the same
|
|
234
|
+
* event object (its documented in-place mutation of `systemPromptOptions`
|
|
235
|
+
* depends on it), which is what makes the set a guard.
|
|
212
236
|
*/
|
|
213
|
-
const
|
|
237
|
+
const HANDLED_KEY = "__mirrorLoggerHandledEvents";
|
|
214
238
|
|
|
215
239
|
export default function (pi: ExtensionAPI) {
|
|
216
240
|
const globals = globalThis as Record<string, unknown>;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
const handled = (globals[HANDLED_KEY] ??= new Map<string, WeakSet<object>>()) as Map<
|
|
242
|
+
string,
|
|
243
|
+
WeakSet<object>
|
|
244
|
+
>;
|
|
245
|
+
/**
|
|
246
|
+
* The handler, run by the first registration an event reaches. `slot` names
|
|
247
|
+
* the handler, not the event: this file registers two handlers on
|
|
248
|
+
* `before_agent_start`, and each must see the event once.
|
|
249
|
+
*/
|
|
250
|
+
function once<E, C, R>(slot: string, handler: (event: E, ctx: C) => R): (event: E, ctx: C) => R | undefined {
|
|
251
|
+
let seen = handled.get(slot);
|
|
252
|
+
if (!seen) {
|
|
253
|
+
seen = new WeakSet<object>();
|
|
254
|
+
handled.set(slot, seen);
|
|
222
255
|
}
|
|
223
|
-
|
|
256
|
+
const set = seen;
|
|
257
|
+
return (event, ctx) => {
|
|
258
|
+
if (event !== null && typeof event === "object") {
|
|
259
|
+
if (set.has(event)) return undefined;
|
|
260
|
+
set.add(event);
|
|
261
|
+
}
|
|
262
|
+
return handler(event, ctx);
|
|
263
|
+
};
|
|
224
264
|
}
|
|
225
|
-
globals[LOADED_KEY] = import.meta.url;
|
|
226
265
|
|
|
227
266
|
// --- Helpers ---
|
|
228
267
|
|
|
@@ -351,13 +390,58 @@ export default function (pi: ExtensionAPI) {
|
|
|
351
390
|
*/
|
|
352
391
|
const pendingUserLog = new Map<string, Promise<void>>();
|
|
353
392
|
|
|
354
|
-
|
|
393
|
+
/**
|
|
394
|
+
* One front-door process on the session's chain: spawned when the link
|
|
395
|
+
* before it has exited, its stdout returned to the caller. A failure settles
|
|
396
|
+
* the chain all the same and reads as empty stdout (`runMirror` swallows).
|
|
397
|
+
*/
|
|
398
|
+
function runOnChain(sessionId: string, args: string[]): Promise<string> {
|
|
355
399
|
const prior = pendingUserLog.get(sessionId) ?? Promise.resolve();
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
400
|
+
const stdout = prior.then(() => runMirror(args));
|
|
401
|
+
pendingUserLog.set(
|
|
402
|
+
sessionId,
|
|
403
|
+
stdout.then(
|
|
404
|
+
() => undefined,
|
|
405
|
+
() => undefined,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
return stdout.then(
|
|
409
|
+
(out) => out,
|
|
410
|
+
() => "",
|
|
359
411
|
);
|
|
360
|
-
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function enqueueUserLog(sessionId: string, args: string[]): void {
|
|
415
|
+
void runOnChain(sessionId, args);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* CV9.DS8.US5: the turn through the one doorman. `mirror doorman` decides
|
|
420
|
+
* nothing, the full folder, or a delta from the prompt and the session's
|
|
421
|
+
* state (`ts/src/mirror/doorman.ts`, DS8's policy); this extension decides
|
|
422
|
+
* nothing itself and carries the answer. Awaited: the turn cannot start
|
|
423
|
+
* before the block exists. Outside Mirror Mode the doorman reads and exits
|
|
424
|
+
* with nothing -- one process, no snapshot, no embedding, no context (0.21 s
|
|
425
|
+
* on a 61 MB home, measured 2026-10-25).
|
|
426
|
+
*
|
|
427
|
+
* On the chain AHEAD of the `user-prompt` logger: the logger's first-turn
|
|
428
|
+
* upsert rebuilds the whole `runtime_sessions` row from what it read, and
|
|
429
|
+
* overlapping the doorman's full load it would write back `mirror_active = 0`
|
|
430
|
+
* and a null persona. The doorman's process has exited before the logger's is
|
|
431
|
+
* spawned, so it does not race. The prompt is truncated as the payload is:
|
|
432
|
+
* the explicit form is at its start, routing and the embedding do not need
|
|
433
|
+
* the tail, and an untruncated paste would fail the exec on ARG_MAX --
|
|
434
|
+
* silently, and the skill would load twice.
|
|
435
|
+
*/
|
|
436
|
+
function deliverMirrorContext(sessionId: string, prompt: string): Promise<string> {
|
|
437
|
+
return runOnChain(sessionId, [
|
|
438
|
+
"mirror",
|
|
439
|
+
"doorman",
|
|
440
|
+
"--prompt",
|
|
441
|
+
truncate(prompt),
|
|
442
|
+
"--session-id",
|
|
443
|
+
sessionId,
|
|
444
|
+
]);
|
|
361
445
|
}
|
|
362
446
|
|
|
363
447
|
async function settleUserLog(sessionId: string): Promise<void> {
|
|
@@ -536,13 +620,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
536
620
|
|
|
537
621
|
// --- dynamic resources → installed external Pi skills ---
|
|
538
622
|
|
|
539
|
-
pi.on(
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
623
|
+
pi.on(
|
|
624
|
+
"resources_discover",
|
|
625
|
+
once("resources_discover", async () => {
|
|
626
|
+
const skillPaths = getInstalledPiSkillPaths();
|
|
627
|
+
if (skillPaths.length > 0) {
|
|
628
|
+
log("INFO", `resources_discover: loaded ${skillPaths.length} installed Pi external skill(s)`);
|
|
629
|
+
}
|
|
630
|
+
return { skillPaths };
|
|
631
|
+
}),
|
|
632
|
+
);
|
|
546
633
|
|
|
547
634
|
// --- 0. the Operating Instructions, for a session Pi did not open in the tree ---
|
|
548
635
|
//
|
|
@@ -553,7 +640,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
553
640
|
// a Mirror session (CV22.DS10.US3 D13). So the extension appends the file
|
|
554
641
|
// it ships with, unless Pi already loaded that very file.
|
|
555
642
|
|
|
556
|
-
pi.on("before_agent_start", async (event) => {
|
|
643
|
+
pi.on("before_agent_start", once("operating_instructions", async (event) => {
|
|
557
644
|
const files = event.systemPromptOptions.contextFiles;
|
|
558
645
|
if (files.some((file) => _isOurAgentsFile(file.path))) return;
|
|
559
646
|
let content: string;
|
|
@@ -566,7 +653,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
566
653
|
if (files.some((file) => file.content === content)) return;
|
|
567
654
|
files.push({ path: AGENTS_FILE, content });
|
|
568
655
|
log("INFO", `operating instructions appended from ${AGENTS_FILE}`);
|
|
569
|
-
});
|
|
656
|
+
}));
|
|
570
657
|
|
|
571
658
|
// --- 1. session_start → unmute + welcome + status line in one process ---
|
|
572
659
|
//
|
|
@@ -575,8 +662,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
575
662
|
// the composite runs them in one process, write first. Maintenance stays
|
|
576
663
|
// detached. Fires on startup, reload, new, resume, and fork alike.
|
|
577
664
|
|
|
578
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
665
|
+
pi.on("session_start", once("session_start", async (_event, ctx) => {
|
|
579
666
|
log("INFO", "session_start fired");
|
|
667
|
+
// The session's name, for the model's own `mirror` commands (CV9.DS8.US5;
|
|
668
|
+
// see the header). Fires again on `/new`, `/resume`, and `/fork`, with the
|
|
669
|
+
// new file; the shutdown before it cleared the old one.
|
|
670
|
+
const namedSession = ctx.sessionManager.getSessionFile();
|
|
671
|
+
if (namedSession) process.env.MIRROR_SESSION_ID = namedSession;
|
|
672
|
+
else delete process.env.MIRROR_SESSION_ID;
|
|
580
673
|
if (ctx.hasUI) {
|
|
581
674
|
ctx.ui.setStatus("mirror", "◇ Mirror · starting… maintenance will continue in background");
|
|
582
675
|
}
|
|
@@ -614,11 +707,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
707
|
setMirrorStatus(ctx, (envelope?.statusLine ?? "").trim());
|
|
615
708
|
}
|
|
616
709
|
runMirrorBackground(["conversation-logger", "session-maintenance"], "session-maintenance");
|
|
617
|
-
});
|
|
710
|
+
}));
|
|
618
711
|
|
|
619
712
|
// --- 2. input → what the person typed, for the run it starts (CR160) ---
|
|
620
713
|
|
|
621
|
-
pi.on("input", async (event, ctx) => {
|
|
714
|
+
pi.on("input", once("input", async (event, ctx) => {
|
|
622
715
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
623
716
|
if (!sessionId) return;
|
|
624
717
|
// The fields are Pi's (`InputEvent`); the types this directory pins predate one.
|
|
@@ -626,11 +719,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
626
719
|
// Queued while Pi streams: it joins a run already going and starts none.
|
|
627
720
|
if (streamingBehavior) return;
|
|
628
721
|
typedInput.set(sessionId, text);
|
|
629
|
-
});
|
|
722
|
+
}));
|
|
630
723
|
|
|
631
724
|
// --- 3. before_agent_start → the turn to the core's prompt rule ---
|
|
632
725
|
|
|
633
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
726
|
+
pi.on("before_agent_start", once("prompt", async (event, ctx) => {
|
|
634
727
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
635
728
|
if (!sessionId) return;
|
|
636
729
|
// A run a custom message started answers no prompt of the person's. Pi's
|
|
@@ -651,6 +744,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
651
744
|
pendingReply.set(sessionId, { turnId, texts: [] });
|
|
652
745
|
if (!prompt) return;
|
|
653
746
|
|
|
747
|
+
// The doorman first, awaited (CV9.DS8.US5): Mirror's context before the
|
|
748
|
+
// model, on the turn that asks for it.
|
|
749
|
+
const block = await deliverMirrorContext(sessionId, prompt);
|
|
750
|
+
|
|
654
751
|
log("INFO", `user-prompt (turn ${turnId}): ${prompt.slice(0, 80)}...`);
|
|
655
752
|
// Not awaited (CV9.E2.S32): the turn starts now; `agent_settled` and
|
|
656
753
|
// `session_shutdown` settle the chain before anything that must follow.
|
|
@@ -662,7 +759,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
662
759
|
"--payload",
|
|
663
760
|
JSON.stringify({ session_id: sessionId, prompt: truncate(prompt), turn_id: turnId }),
|
|
664
761
|
]);
|
|
665
|
-
|
|
762
|
+
|
|
763
|
+
if (!block) return;
|
|
764
|
+
log("INFO", `mirror-context (turn ${turnId}): ${block.length} chars delivered`);
|
|
765
|
+
// Pi adds it to this run after the user message and hands it to the model
|
|
766
|
+
// as a user-role message; its first line says the runtime, not the person,
|
|
767
|
+
// is speaking, and the `mm-mirror` skill reads that line as delivery (TS3).
|
|
768
|
+
// Not displayed: the person sees the mirror's reply, not the plumbing.
|
|
769
|
+
return { message: { customType: "mirror-context", content: block, display: false } };
|
|
770
|
+
}));
|
|
666
771
|
|
|
667
772
|
// --- 4. agent_end → the run's assistant text, kept for the prompt's reply ---
|
|
668
773
|
//
|
|
@@ -671,7 +776,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
671
776
|
// assistant text is kept until the prompt settles: the hooks' `stop` takes
|
|
672
777
|
// one answer per turn (CR160, A2).
|
|
673
778
|
|
|
674
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
779
|
+
pi.on("agent_end", once("agent_end", async (event, ctx) => {
|
|
675
780
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
676
781
|
if (!sessionId) return;
|
|
677
782
|
|
|
@@ -700,7 +805,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
700
805
|
// find nothing waiting, as on the other runtimes.
|
|
701
806
|
pendingReply.get(sessionId)?.texts.push(...assistantTexts);
|
|
702
807
|
await refreshMirrorStatus(ctx);
|
|
703
|
-
});
|
|
808
|
+
}));
|
|
704
809
|
|
|
705
810
|
// --- 5. agent_settled → the prompt's whole reply to `stop` ---
|
|
706
811
|
|
|
@@ -708,10 +813,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
708
813
|
// (`.pi/package.json`), so it is registered by its name alone.
|
|
709
814
|
const onEvent = pi.on.bind(pi) as unknown as (
|
|
710
815
|
event: string,
|
|
711
|
-
handler: (
|
|
816
|
+
handler: (
|
|
817
|
+
event: unknown,
|
|
818
|
+
ctx: { sessionManager: { getSessionFile(): string | undefined } },
|
|
819
|
+
) => Promise<void> | undefined,
|
|
712
820
|
) => void;
|
|
713
821
|
|
|
714
|
-
onEvent("agent_settled", async (_event, ctx) => {
|
|
822
|
+
onEvent("agent_settled", once("agent_settled", async (_event, ctx) => {
|
|
715
823
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
716
824
|
if (!sessionId) return;
|
|
717
825
|
const args = takeStopArgs(sessionId);
|
|
@@ -720,15 +828,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
720
828
|
// the write survives Pi exiting right after the answer.
|
|
721
829
|
await settleUserLog(sessionId);
|
|
722
830
|
runMirrorBackground(args, "stop");
|
|
723
|
-
});
|
|
831
|
+
}));
|
|
832
|
+
|
|
833
|
+
// --- 5b. session_compact → the folder left the transcript (CV9.DS8.US5) ---
|
|
834
|
+
//
|
|
835
|
+
// A delta's premise is that the identity layers sit in the transcript. Pi
|
|
836
|
+
// compacts on its own (threshold, overflow) and on `/compact`; after that
|
|
837
|
+
// the folder is a summary. The core is told, on the chain and not awaited;
|
|
838
|
+
// the next ordinary turn in Mirror Mode is full again, routed from its prompt.
|
|
839
|
+
|
|
840
|
+
pi.on(
|
|
841
|
+
"session_compact",
|
|
842
|
+
once("session_compact", async (_event, ctx) => {
|
|
843
|
+
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
844
|
+
if (!sessionId) return;
|
|
845
|
+
log("INFO", "session_compact: the doorman forgets what the transcript held");
|
|
846
|
+
enqueueUserLog(sessionId, ["mirror", "doorman", "--forget", "--session-id", sessionId]);
|
|
847
|
+
}),
|
|
848
|
+
);
|
|
724
849
|
|
|
725
850
|
// --- 6. session_shutdown → close conversation + backup ---
|
|
726
851
|
//
|
|
727
852
|
// Uses extract=False because extraction calls the LLM and can take 30s+.
|
|
728
853
|
// Extraction happens at the next session_start via extract_pending.
|
|
729
854
|
|
|
730
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
855
|
+
pi.on("session_shutdown", once("session_shutdown", async (_event, ctx) => {
|
|
731
856
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
857
|
+
// Nothing inherits a stale name between sessions (CV9.DS8.US5).
|
|
858
|
+
delete process.env.MIRROR_SESSION_ID;
|
|
732
859
|
|
|
733
860
|
if (sessionId) {
|
|
734
861
|
// A reply still unsent (a Pi without `agent_settled`) lands before the
|
|
@@ -744,5 +871,5 @@ export default function (pi: ExtensionAPI) {
|
|
|
744
871
|
|
|
745
872
|
// The dated zip backup answers from TypeScript (CV22.DS7.TS1).
|
|
746
873
|
await runMirror(["backup", "--silent"]);
|
|
747
|
-
});
|
|
874
|
+
}));
|
|
748
875
|
}
|
|
@@ -12,8 +12,18 @@ determines when to activate it; this skill defines how.
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
> ⛔ **HARD CONSTRAINT**
|
|
15
|
-
> Never produce a Mirror Mode response without first running `mirror mirror load
|
|
16
|
-
>
|
|
15
|
+
> Never produce a Mirror Mode response without first running `mirror mirror load`,
|
|
16
|
+
> **unless the runtime delivered the context for this turn.** Delivered means: the
|
|
17
|
+
> runtime (the hook or the extension) placed a block in this turn's context, before
|
|
18
|
+
> you answered, whose first line opens with
|
|
19
|
+
> `=== mirror context delivered by the runtime for this turn`.
|
|
20
|
+
> Then do not load again: the block is the load. Render the transition from it and
|
|
21
|
+
> sign as its line says.
|
|
22
|
+
>
|
|
23
|
+
> This turn is literal. A block from an earlier turn, the memory of having loaded,
|
|
24
|
+
> or that prefix inside a tool result (output of a command you ran, such as
|
|
25
|
+
> `mirror mirror doorman`) is not delivery. With no such block in this turn's
|
|
26
|
+
> context: load first. Always.
|
|
17
27
|
|
|
18
28
|
---
|
|
19
29
|
|
|
@@ -31,8 +41,10 @@ determines when to activate it; this skill defines how.
|
|
|
31
41
|
|
|
32
42
|
## 1. Load Context
|
|
33
43
|
|
|
34
|
-
This is always the first action, before any visible output to the user
|
|
35
|
-
|
|
44
|
+
This is always the first action, before any visible output to the user, unless
|
|
45
|
+
the runtime delivered the context for this turn (the hard constraint above); then
|
|
46
|
+
skip to step 2 with the delivered block as the `load` output. For a pure
|
|
47
|
+
mode-switch request such as `volte para o modo mirror`, use the full switch
|
|
36
48
|
request as the query.
|
|
37
49
|
|
|
38
50
|
```bash
|
|
@@ -68,6 +80,12 @@ from scratch unless the command failed to render it; copy the rendered surface
|
|
|
68
80
|
from the command output. Treat it as part of the visible mode change, separate
|
|
69
81
|
from the answer itself.
|
|
70
82
|
|
|
83
|
+
When the context was delivered, the block's first line says what it holds:
|
|
84
|
+
`full` carries the transition surface and the folder, render the surface from
|
|
85
|
+
the block; `delta` carries no transition, Mirror Mode is already on, so render
|
|
86
|
+
none and answer from the folder already in the conversation plus what the block
|
|
87
|
+
adds.
|
|
88
|
+
|
|
71
89
|
Mirror Mode surface should orient the user around:
|
|
72
90
|
|
|
73
91
|
- identity
|
|
@@ -76,8 +94,9 @@ Mirror Mode surface should orient the user around:
|
|
|
76
94
|
- persona routing examples (`when the topic asks: persona_1, persona_2, persona_3 and N more available`)
|
|
77
95
|
- available lenses: `◌ Mirror Mode`, `■ Builder Mode`, `△ Explorer Mode`, `☾ Soul Mode`
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
An active persona signs `◇ persona-name` on its own line, the rule `AGENTS.md`
|
|
98
|
+
states; the delivered line names the persona to sign. No signature when the ego
|
|
99
|
+
answers alone.
|
|
81
100
|
|
|
82
101
|
## 3. Answer
|
|
83
102
|
|
package/README.md
CHANGED
|
@@ -60,8 +60,8 @@ This is not a chatbot with memory. This is a mirror — and yours.
|
|
|
60
60
|
|
|
61
61
|
## How It Works
|
|
62
62
|
|
|
63
|
-
Local-first. SQLite. Five supported runtimes:
|
|
64
|
-
Antigravity, Grok Build. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
|
|
63
|
+
Local-first. SQLite. [Five supported runtimes](docs/product/specs/runtime-interface/index.md#parity-matrix):
|
|
64
|
+
Pi, Claude Code, Codex, Antigravity, Grok Build. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
|
|
65
65
|
Personas, Shadow.
|
|
66
66
|
Context-activated modes — Mirror, Builder, Explorer, and Soul. The identity lives
|
|
67
67
|
in your machine, not a server.
|
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,8 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.38.0 — The Model Behind the Memories, Chosen by Measurement](v0.38.0.md) — minor release closing CV9.DS9: the model behind memories, tasks, titles, summaries, tags, reception, journal, shadow, and consolidation is Claude Haiku 5.5 at reasoning off, chosen on the eval suite with reasoning on and off and on 24 of the owner's own conversations ranked blind, where it came first on 16 of 26 surfaces. Every extraction-pin call can carry a reasoning effort, and a pin carries its own default. With CR180, summaries open with their subject, every lifecycle prompt sees the owner's name, and a framed opening is written once more. Release gate 70 of 70 probes. Rollback is one env line. No migration.
|
|
16
|
+
- [v0.37.0 — The Mirror Stays in the Conversation](v0.37.0.md) — minor release closing CV9.DS8 and, with CV21.DS11, CV21: on Pi, Claude Code, Codex, and Antigravity one doorman in the core puts Mirror's context before the model on every turn of Mirror Mode, all of it on the turn Mirror Mode starts and then the persona and memories each question calls for; an explicit turn loads once; one signature, `◇ persona-name`; Pi reaches L4, measured live. One parity matrix in the runtime interface contract, sourced and held by a test. Codex, Antigravity, and Grok Build need their plugin copy brought level. Migration 018, applied on its own.
|
|
15
17
|
- [v0.36.3 — A Skill You Invoke Is Not Something You Said](v0.36.3.md) — patch release of one Change Request, CR160, with CR159's docs: on Pi, a skill picked from the menu or typed is no longer stored as the person's words, and neither is its reply; a question asked with `/mm-mirror` is stored as the question. Pi's turns go through the rule the other runtimes have. `mirror conversations skill-turns` repairs what earlier versions stored, only on `--apply` and after a verified backup. No migration.
|
|
16
18
|
- [v0.36.2 — Clearing Out What Another Home Copied](v0.36.2.md) — patch release of one Change Request, CR154, finishing what v0.36.1 began: `mirror conversations imported` lists the Pi conversations an earlier version imported, by the folder each session came from and beside how many sessions the home logged live there, and `remove` deletes the ones you name, only on `--apply` and after a verified backup. `discard-current` no longer fails on a conversation a Soul Mode integration names. No migration.
|
|
17
19
|
- [v0.36.1 — A Home Keeps Only Its Own Pi Sessions](v0.36.1.md) — patch release of one Change Request, CR153: session maintenance imports no Pi session, so the Mirror homes on one machine no longer copy each other's Pi conversations; a home's Pi conversations are the ones the Pi extension logged into it. `PI_SESSIONS_DIR` is inert. Conversations an earlier version imported stay, and the note shows how to list them. Update every install that has a home. No migration.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.37.0 is a minor release closing CV9.DS8 and, with CV21.DS11, CV21. On Pi,
|
|
4
|
+
Claude Code, Codex, and Antigravity, Mirror's context now reaches the model before
|
|
5
|
+
it answers through one decision in the core, the doorman: all of it on the turn
|
|
6
|
+
Mirror Mode starts, then on every turn the persona the question calls for and the
|
|
7
|
+
memories relevant to it, so the mirror keeps its voice and its `◇ persona-name`
|
|
8
|
+
signature across a conversation. Pi, the recommended harness, moves from L3 to L4,
|
|
9
|
+
measured live. CV21 closes with one parity matrix in the runtime interface
|
|
10
|
+
contract, each row sourced to the measurement behind it. Migration 018 drops a
|
|
11
|
+
column nothing reads any more, and Mirror applies it on its own.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v0.37.0 — The Mirror Stays in the Conversation
|
|
15
|
+
|
|
16
|
+
**Date:** 2026-10-25
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- One doorman decides what Mirror puts before the model on each turn, on Pi, Claude
|
|
21
|
+
Code, Codex, and Antigravity: the full context on the turn Mirror Mode starts, then
|
|
22
|
+
the persona each question calls for and the memories relevant to it, and nothing
|
|
23
|
+
when Mirror Mode is off.
|
|
24
|
+
- Pi reaches L4. A Pi conversation keeps the mirror's voice after the first answer,
|
|
25
|
+
moves to another persona when the topic moves, and signs every answer. Measured live
|
|
26
|
+
on Pi 1.4.0.
|
|
27
|
+
- An explicit Mirror turn loads once. The `mm-mirror` skill no longer loads a second
|
|
28
|
+
time when the runtime already delivered the context.
|
|
29
|
+
- One signature rule, `◇ persona-name`, in `AGENTS.md`, the skill, and the core's
|
|
30
|
+
banner; `✦ Persona:` is gone.
|
|
31
|
+
- Measured live before it was written down: Claude Code 2.1.310 and Codex 0.164.0
|
|
32
|
+
(16 of 16 checks), Pi 1.4.0 (three walks), agy 1.4.7.
|
|
33
|
+
- CV21 closes: the runtime interface contract holds one parity matrix, each row
|
|
34
|
+
sourced to a measurement, and a test holds README, Getting Started, REFERENCE, and
|
|
35
|
+
the architecture to it.
|
|
36
|
+
- Migration 018 drops `runtime_sessions.hook_injected`; Mirror backs up and migrates
|
|
37
|
+
each home on its own.
|
|
38
|
+
- Codex, Antigravity, and Grok Build keep their own copy of the plugin: bring it level
|
|
39
|
+
after updating; `mirror runtime diagnose` names the command.
|
|
40
|
+
|
|
41
|
+
## Where We Started
|
|
42
|
+
|
|
43
|
+
Mirror Mode is the product. Mirror builds a folder from the database (the identity
|
|
44
|
+
layers, a persona routed from the question, the journey, the shadow patterns, and the
|
|
45
|
+
memories and attachments relevant to it) and the model answers as the mirror with that
|
|
46
|
+
folder open. What decides the experience is whether the folder reaches the model, and
|
|
47
|
+
when.
|
|
48
|
+
|
|
49
|
+
Until this release, three runtimes gave three answers. The Claude-family inject hook
|
|
50
|
+
delivered the folder on an explicit Mirror turn and once on the next ordinary turn,
|
|
51
|
+
then never again. Antigravity's hook delivered the whole folder on every turn, but agy
|
|
52
|
+
1.4.2's hooks carried no prompt, so nothing could be routed from the question. Pi had
|
|
53
|
+
no doorman at all. Measured on Pi 1.4.0 on 2026-10-24
|
|
54
|
+
([CR165](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr165-pi-puts-no-mirror-context-before-the-model.md)),
|
|
55
|
+
the model loaded the folder once, through the skill, and answered every follow-up from
|
|
56
|
+
that one load: no reload, no new persona when the topic changed, no signature after the
|
|
57
|
+
first reply. And wherever a doorman did exist, the skill's own rule, "never answer
|
|
58
|
+
without loading first", made the explicit turn load twice.
|
|
59
|
+
|
|
60
|
+
CV21.DS11, writing the parity matrix, made the line explicit: at L4 the runtime puts
|
|
61
|
+
Mirror's context before the model; at L3 the model loads it itself. By that line Pi, the
|
|
62
|
+
harness Getting Started recommends, was L3.
|
|
63
|
+
|
|
64
|
+
## What Changed
|
|
65
|
+
|
|
66
|
+
### One doorman
|
|
67
|
+
|
|
68
|
+
[CV9.DS8](../project/roadmap/cv9-mirror-1-0/cv9-ds8-one-doorman-mirror-s-context-before-the-model-on-every-runtime/index.md)
|
|
69
|
+
built one decision in the core, `ts/src/mirror/doorman.ts`. Given a turn's prompt and
|
|
70
|
+
the session's state it answers one of three things:
|
|
71
|
+
|
|
72
|
+
- **the full folder**, when the prompt opens with the explicit Mirror form in any
|
|
73
|
+
runtime's spelling (`/mm-mirror …`, `/mm:mirror …`, `$mm-mirror …`, the plugin
|
|
74
|
+
spellings); the session is marked in Mirror Mode;
|
|
75
|
+
- **a delta**, on an ordinary turn while Mirror Mode is on: the section of a persona the
|
|
76
|
+
question newly calls for, which becomes the session's; up to five memories scoring at
|
|
77
|
+
least 0.4 on the semantic score that the conversation has not been given yet; and the
|
|
78
|
+
attachments scoring above 0.4. The identity layers are already in the transcript and
|
|
79
|
+
are not repeated;
|
|
80
|
+
- **nothing**, when Mirror Mode is off, or on a skill command other than the Mirror
|
|
81
|
+
form, with no model call.
|
|
82
|
+
|
|
83
|
+
Every block opens with one line the model reads, on every turn Mirror Mode is on: it
|
|
84
|
+
says what was delivered and carries the persona and the signature rule across the
|
|
85
|
+
conversation. The `mm-mirror` skill trusts that line and loads only when the runtime
|
|
86
|
+
did not deliver the context for this turn, so an explicit turn loads once.
|
|
87
|
+
|
|
88
|
+
"Mirror Mode on" takes two facts: the session's Mirror flag, and its operating mode
|
|
89
|
+
still being Mirror Mode. Switching to Builder, Explorer, or Soul Mode ends the deltas,
|
|
90
|
+
and `/mm-new` ends Mirror Mode; on Pi it now does, because the session is named. When
|
|
91
|
+
the model answers a personal question as the mirror on its own, its load names the
|
|
92
|
+
session from the runtime's environment (`MIRROR_SESSION_ID`, `CLAUDE_CODE_SESSION_ID`,
|
|
93
|
+
`CODEX_SESSION_ID`), so the doorman takes over from the next turn. The doorman never
|
|
94
|
+
guesses a session.
|
|
95
|
+
|
|
96
|
+
Three choices shaped the policy, recorded in the
|
|
97
|
+
[decisions log](../project/decisions.md#one-doorman-puts-mirrors-context-before-the-model-on-every-runtime):
|
|
98
|
+
memories are refreshed on every turn in Mirror Mode, at one embedding call each, counted
|
|
99
|
+
in the ledger; there is one signature, `◇ persona-name`; and the work was a Delivery
|
|
100
|
+
Story under CV9, measured live on every runtime it touched.
|
|
101
|
+
|
|
102
|
+
### Each runtime through it
|
|
103
|
+
|
|
104
|
+
- **Claude Code and Codex.** The inject hook hands each prompt to the doorman and
|
|
105
|
+
returns its block as the hook's context. Measured live on Claude Code 2.1.310 and
|
|
106
|
+
Codex 0.164.0: the explicit turn loads once, a follow-up in another domain moves to
|
|
107
|
+
that persona and signs it, a Builder switch ends the deltas, and a personal question
|
|
108
|
+
the model answers on its own is followed by doorman turns. 16 of 16.
|
|
109
|
+
- **Pi.** The extension's `before_agent_start` hands each turn to the doorman and
|
|
110
|
+
carries its block into the run; after Pi compacts a session, the next turn gets the
|
|
111
|
+
full folder again. Measured live on Pi 1.4.0 in three walks, and Pi's row in the
|
|
112
|
+
parity matrix moves to L4.
|
|
113
|
+
- **Antigravity.** agy 1.4.7 carries the prompt in every hook payload
|
|
114
|
+
(`lastUserInput`), so `PreInvocation` hands each turn to the doorman and answers its
|
|
115
|
+
block as a persistent step; a conversation's first turn gets the full folder.
|
|
116
|
+
Measured live on agy 1.4.7, at 0.23–1.02 s a turn against agy's 15 s budget. Before
|
|
117
|
+
agy 1.4.7, the earlier injection stays as it was.
|
|
118
|
+
- **Grok Build** stays at L3. Grok discards the context a hook offers, so Mirror Mode
|
|
119
|
+
there still comes from the skill.
|
|
120
|
+
|
|
121
|
+
### What a turn costs
|
|
122
|
+
|
|
123
|
+
A turn in Mirror Mode writes one `embedding` row in `llm_calls`; the explicit turn
|
|
124
|
+
costs what `mirror load --query` costs; a turn outside Mirror Mode makes no model call.
|
|
125
|
+
Measured with the `mirror mirror doorman` command on a copy of a real home: a delta in
|
|
126
|
+
about 1 s, 142–154 bytes when no memory passes the threshold and up to 8.5 KB when some
|
|
127
|
+
do; the full folder, 33 KB, in 1.33 s; a turn outside Mirror Mode in 0.21 s. No price in
|
|
128
|
+
dollars was measured; the ledger counts the calls.
|
|
129
|
+
|
|
130
|
+
### CV21 closes
|
|
131
|
+
|
|
132
|
+
[CV21.DS11](../project/roadmap/cv21-runtime-expansion-ii/cv21-ds11-contract-docs-refresh/index.md)
|
|
133
|
+
put one parity matrix in the
|
|
134
|
+
[runtime interface contract](../product/specs/runtime-interface/index.md#parity-matrix):
|
|
135
|
+
five runtimes, each row sourced to the record that measured it, with version, binary,
|
|
136
|
+
and date. A decision makes it the single authority, and `runtimeParityDocs.test.ts`
|
|
137
|
+
holds README, Getting Started, REFERENCE, and the architecture to it. Measured on Claude
|
|
138
|
+
Code 2.1.310: Mirror's status line is a step a user adds
|
|
139
|
+
(`mirror welcome --status-line --session-id $CLAUDE_CODE_SESSION_ID`), and Claude Code
|
|
140
|
+
reads a project's `AGENTS.md` only when the project has no `CLAUDE.md`. `runtime
|
|
141
|
+
diagnose` and `runtime update` now name an Antigravity copy of the plugin that an
|
|
142
|
+
update left behind. With DS11,
|
|
143
|
+
[CV21, Runtime Expansion II](../project/roadmap/cv21-runtime-expansion-ii/index.md), is
|
|
144
|
+
done: one canonical plugin, five runtimes, each at the level it was measured at.
|
|
145
|
+
|
|
146
|
+
### Also
|
|
147
|
+
|
|
148
|
+
- The pages a user reads (the runtime interface contract, Getting Started, REFERENCE,
|
|
149
|
+
and the architecture) describe the doorman as it was measured (CV9.DS8.TS7).
|
|
150
|
+
- Pi run inside a checkout of this repository, where Pi reaches Mirror's extension
|
|
151
|
+
twice, ran none of its handlers: no welcome, no logging. It now runs each once.
|
|
152
|
+
- With Pi now naming its session, a Builder journey binds only from a mode stamp written
|
|
153
|
+
under that session's name, never from a guessed session's (CR008).
|
|
154
|
+
- For contributors: the test suite leaves no temp directories behind, and CI fails if it
|
|
155
|
+
does (CR163).
|
|
156
|
+
|
|
157
|
+
## Upgrading
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
mirror runtime update # or: npm install -g mirror-mind@latest
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Migration 018 drops one column nothing reads any more, `runtime_sessions.hook_injected`.
|
|
164
|
+
Mirror applies it on its own: the first time it opens a home after the update, it backs
|
|
165
|
+
the database up and migrates it.
|
|
166
|
+
|
|
167
|
+
Then, for each runtime you use:
|
|
168
|
+
|
|
169
|
+
- **Pi:** restart every Pi session. A session keeps the extension it loaded.
|
|
170
|
+
- **Claude Code:** start a new session. The plugin is a link into the package, so there
|
|
171
|
+
is nothing to re-wire.
|
|
172
|
+
- **Codex, Antigravity, Grok Build:** each keeps its own copy of the plugin, which an
|
|
173
|
+
update leaves behind, and the `mm-mirror` skill in it changed. Run
|
|
174
|
+
`mirror runtime diagnose`: it names the one command that brings each copy level
|
|
175
|
+
(`codex plugin add mirror-mind@mirror-mind` for Codex, `agy plugin install
|
|
176
|
+
"<pkg>/plugins/mirror-mind"` for Antigravity, an uninstall and an install for Grok
|
|
177
|
+
Build). Then start a new session.
|
|
178
|
+
- **Antigravity before 1.4.7** keeps the earlier injection; update agy to get the
|
|
179
|
+
doorman.
|
|
180
|
+
|
|
181
|
+
## Conscious Exclusions
|
|
182
|
+
|
|
183
|
+
- **Grok Build stays at L3** (CR143): it discards hook context, so there is nothing for
|
|
184
|
+
a doorman to deliver through.
|
|
185
|
+
- **The model's own commands on Antigravity cannot name the session yet**
|
|
186
|
+
([CR169](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr169-on-agy-the-model-s-own-mirror-commands-cannot-name-the-session.md)):
|
|
187
|
+
a Mirror load the model starts by itself there is not followed by doorman turns, and a
|
|
188
|
+
Builder switch the model runs guesses the session. Antigravity's compaction has no
|
|
189
|
+
hook to bring the folder back (CR173), and an execution agy starts with no new user
|
|
190
|
+
input would hand the doorman the previous prompt (CR170, unmeasured).
|
|
191
|
+
- **A prompt that is exactly a doorman flag**, such as `--forget`, is read as the flag
|
|
192
|
+
(CR171).
|
|
193
|
+
- **What the folder contains is unchanged.** The doorman decides when Mirror's context
|
|
194
|
+
reaches the model and which part of it; how personas are scored and how memories are
|
|
195
|
+
ranked stay as they were.
|
|
196
|
+
- **No price was measured.** The ledger counts every call; a turn's cost in dollars is
|
|
197
|
+
still decision 1's estimate.
|
|
198
|
+
- **A checkout plus the installed plugin runs Claude Code's inject hook twice**, paying
|
|
199
|
+
two embeddings a turn: a contributor-only condition, named in CV9.DS8.US4 and not yet
|
|
200
|
+
a Change Request.
|
|
201
|
+
|
|
202
|
+
## What We Learned
|
|
203
|
+
|
|
204
|
+
- **A level is a measurement.** Pi was claimed at L4 until the parity matrix asked for
|
|
205
|
+
the record behind every level; asking is what showed the gap this release closes.
|
|
206
|
+
- **Probe before building.** The plan for Antigravity was to read the prompt from the
|
|
207
|
+
transcript. The probe meant to time that read found agy 1.4.7 carrying the prompt in
|
|
208
|
+
every payload, and the transcript reader was never written.
|
|
209
|
+
- **Say what "on" means.** "Mirror Mode on" needed two facts, the session's flag and its
|
|
210
|
+
operating mode. A review found it before a live walk did, which would have been a
|
|
211
|
+
Builder answer in the mirror's voice.
|
|
212
|
+
- **Threshold the score that measures relevance.** A threshold on the hybrid ranking
|
|
213
|
+
score would have been a recency gate; the memory threshold reads the semantic score,
|
|
214
|
+
the one attachments already used.
|
|
215
|
+
|
|
216
|
+
## Next Horizon
|
|
217
|
+
|
|
218
|
+
CV9, Mirror Mind 1.0, continues: Boundary Hardening and the rest of Stabilization, with
|
|
219
|
+
CV9.E2.S2, External Extension Runtime Surface Parity, the next pull Ariad recommends.
|
|
220
|
+
The Change Requests DS8 left, CR169 to CR174, wait for their triggers. When CV9's done
|
|
221
|
+
condition is met, the release is `v1.0.0`.
|