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.
Files changed (52) hide show
  1. package/.pi/extensions/mirror-logger.ts +173 -46
  2. package/.pi/skills/mm-mirror/SKILL.md +25 -6
  3. package/README.md +2 -2
  4. package/docs/releases/index.md +2 -0
  5. package/docs/releases/v0.37.0.md +221 -0
  6. package/docs/releases/v0.38.0.md +215 -0
  7. package/package.json +1 -1
  8. package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
  9. package/plugins/mirror-mind/plugin.json +1 -1
  10. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +14 -6
  11. package/ts/src/builder/commands.ts +9 -6
  12. package/ts/src/builder/load.ts +14 -2
  13. package/ts/src/conversation/closeTail.ts +7 -3
  14. package/ts/src/conversation/extraction.ts +1 -11
  15. package/ts/src/conversation/logger.ts +12 -6
  16. package/ts/src/conversation/userName.ts +44 -0
  17. package/ts/src/db/migrateOnOpen.ts +1 -1
  18. package/ts/src/db/migrations.ts +16 -0
  19. package/ts/src/db/schema.ts +0 -1
  20. package/ts/src/db/schemaInventorySnapshot.ts +1 -8
  21. package/ts/src/db/schemaState.ts +2 -0
  22. package/ts/src/db/schemaTsDivergence.ts +25 -4
  23. package/ts/src/extraction/conversationMetadata.ts +53 -8
  24. package/ts/src/extraction/prompts.ts +8 -3
  25. package/ts/src/frontDoor/cli.ts +42 -2
  26. package/ts/src/frontDoor/exploreRoute.ts +2 -2
  27. package/ts/src/frontDoor/mirrorModeRoute.ts +129 -5
  28. package/ts/src/frontDoor/routing.ts +16 -15
  29. package/ts/src/frontDoor/runtimeRoute.ts +4 -0
  30. package/ts/src/frontDoor/soulRoute.ts +4 -4
  31. package/ts/src/guards/testTempHomes.ts +92 -0
  32. package/ts/src/hooks/agy.ts +136 -59
  33. package/ts/src/hooks/claude.ts +29 -90
  34. package/ts/src/hooks/mirrorState.ts +10 -22
  35. package/ts/src/mirror/context.ts +19 -4
  36. package/ts/src/mirror/defaultResolution.ts +2 -1
  37. package/ts/src/mirror/doorman.ts +420 -0
  38. package/ts/src/mirror/orchestration.ts +22 -3
  39. package/ts/src/mirror/render.ts +7 -1
  40. package/ts/src/mirror/runtimeSession.ts +66 -8
  41. package/ts/src/mode/operatingMode.ts +22 -4
  42. package/ts/src/providers/config.ts +105 -1
  43. package/ts/src/providers/cost.ts +5 -1
  44. package/ts/src/providers/llm.ts +40 -0
  45. package/ts/src/providers/transport.ts +14 -0
  46. package/ts/src/runtime/diagnose.ts +64 -7
  47. package/ts/src/runtime/pluginWiring.ts +6 -4
  48. package/ts/src/runtime/status.ts +24 -4
  49. package/ts/src/runtime/update.ts +17 -1
  50. package/ts/src/runtime/wiring.ts +14 -1
  51. package/ts/src/search/memorySearch.ts +38 -18
  52. 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 core's prompt rule (`user-prompt`), with a
11
- * turn id minted for it, NOT awaited: queued on a per-session
12
- * chain so the turn starts at once (CV9.E2.S32)
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
- * - session_shutdown → close conversation (after the chain settles) + backup
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 registration per Pi process. Pi can reach this file twice -- as the
207
- * project's `.pi/extensions/` inside the checkout and as a personal package
208
- * installed from `npm root -g`, which `npm link` makes the same file under
209
- * two paths -- and two registrations would log every turn twice. The guard is
210
- * process-scoped, not module-scoped, because two paths are two module
211
- * instances (CV22.DS10.US3 plateau 2, the quality-assurance finding).
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 LOADED_KEY = "__mirrorLoggerRegistered";
237
+ const HANDLED_KEY = "__mirrorLoggerHandledEvents";
214
238
 
215
239
  export default function (pi: ExtensionAPI) {
216
240
  const globals = globalThis as Record<string, unknown>;
217
- if (globals[LOADED_KEY]) {
218
- try {
219
- appendFileSync(LOG_FILE, `${new Date().toISOString()} [INFO] second registration skipped: ${import.meta.url}\n`);
220
- } catch {
221
- // never block Pi
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
- return;
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
- function enqueueUserLog(sessionId: string, args: string[]): void {
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 next = prior.then(() => runMirror(args)).then(
357
- () => undefined,
358
- () => undefined,
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
- pendingUserLog.set(sessionId, next);
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("resources_discover", async () => {
540
- const skillPaths = getInstalledPiSkillPaths();
541
- if (skillPaths.length > 0) {
542
- log("INFO", `resources_discover: loaded ${skillPaths.length} installed Pi external skill(s)`);
543
- }
544
- return { skillPaths };
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: (event: unknown, ctx: { sessionManager: { getSessionFile(): string | undefined } }) => Promise<void>,
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
- > No exceptions. Load first. Always.
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. For a
35
- pure mode-switch request such as `volte para o modo mirror`, use the full switch
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
- Activated personas use `✦ Persona:`. Do not reuse the Mirror identity symbol
80
- `◇` for personas.
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: Pi, Claude Code, Codex,
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.
@@ -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`.