mirror-mind 0.36.1 → 0.36.3

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.
@@ -6,9 +6,12 @@
6
6
  * Events handled:
7
7
  * - session_start → unmute + welcome card + status line, in ONE front-door
8
8
  * process (CV9.E2.S32); maintenance detached
9
- * - before_agent_start → log user prompt, NOT awaited: queued on a per-session
9
+ * - 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
10
12
  * chain so the turn starts at once (CV9.E2.S32)
11
- * - agent_end → log assistant response, after the chain settles
13
+ * - agent_end → the run's assistant text, kept for the prompt's reply
14
+ * - agent_settled → the prompt's whole reply to `stop`, after the chain settles
12
15
  * - session_shutdown → close conversation (after the chain settles) + backup
13
16
  *
14
17
  * All heavy logic lives in the TypeScript core, reached through the `mirror`
@@ -35,6 +38,7 @@ import { homedir } from "node:os";
35
38
  import { delimiter, dirname, join } from "node:path";
36
39
  import { fileURLToPath } from "node:url";
37
40
  import { spawn } from "node:child_process";
41
+ import { randomUUID } from "node:crypto";
38
42
 
39
43
  /** The tree this extension ships in: `.pi/extensions/` sits two below the root. */
40
44
  const TREE_ROOT = fileURLToPath(new URL("../../", import.meta.url)).replace(/\/$/, "");
@@ -360,6 +364,60 @@ export default function (pi: ExtensionAPI) {
360
364
  await (pendingUserLog.get(sessionId) ?? Promise.resolve());
361
365
  }
362
366
 
367
+ /**
368
+ * CR160: Pi's turns reach the core's prompt rule, the one the hooks use
369
+ * (`user-prompt`, `stop`): a skill invocation and its reply are not
370
+ * conversation, a Mirror question is (`commandTurn.ts`, `mirrorForm.ts`).
371
+ *
372
+ * The rule needs what the person typed, and Pi hands `before_agent_start` the
373
+ * prompt after it expanded a skill into its body. Its documented `input` event
374
+ * carries the text as sent, so that text is kept here for the run it starts --
375
+ * only when it starts one: a message queued while Pi streams joins a run
376
+ * already going. The payload travels as `--payload`: `pi.exec` cannot write
377
+ * stdin (`conversationLoggerRoute.ts`).
378
+ */
379
+ const typedInput = new Map<string, string>();
380
+
381
+ /** The prompt in flight and the assistant text its runs have produced so far. */
382
+ interface PendingReply {
383
+ turnId: string;
384
+ texts: string[];
385
+ }
386
+ const pendingReply = new Map<string, PendingReply>();
387
+
388
+ /**
389
+ * Pi's expansion of `/skill:<name> args`: `<skill name="<name>" location="…">`,
390
+ * `References are relative to ….`, the body, `</skill>`, then the arguments.
391
+ * With no typed text kept for a run, the wrapper is read back to the command
392
+ * it came from, so the rule still sees a command (CR160, A3).
393
+ */
394
+ const PI_SKILL_WRAPPER =
395
+ /^<skill name="([^"]+)" location="[^"]*">\nReferences are relative to [^\n]*\n\n[\s\S]*?\n<\/skill>(?:\n\n([\s\S]*))?$/;
396
+
397
+ function typedFormOf(prompt: string): string {
398
+ const match = PI_SKILL_WRAPPER.exec(prompt);
399
+ if (!match) return prompt;
400
+ const args = (match[2] ?? "").trim();
401
+ return args ? `/skill:${match[1]} ${args}` : `/skill:${match[1]}`;
402
+ }
403
+
404
+ /** The `stop` argv for a prompt's reply, or null when it produced no text. */
405
+ function takeStopArgs(sessionId: string): string[] | null {
406
+ const reply = pendingReply.get(sessionId);
407
+ pendingReply.delete(sessionId);
408
+ if (!reply || reply.texts.length === 0) return null;
409
+ const content = truncate(reply.texts.join("\n\n---\n\n"));
410
+ log("INFO", `stop (turn ${reply.turnId}): ${reply.texts.length} block(s), ${content.length} chars`);
411
+ return [
412
+ "conversation-logger",
413
+ "stop",
414
+ "--interface",
415
+ "pi",
416
+ "--payload",
417
+ JSON.stringify({ session_id: sessionId, turn_id: reply.turnId, last_assistant_message: content }),
418
+ ];
419
+ }
420
+
363
421
  /** Extract readable text from a content blocks array or plain string. */
364
422
  function extractText(content: unknown): string {
365
423
  if (typeof content === "string") return content;
@@ -558,33 +616,60 @@ export default function (pi: ExtensionAPI) {
558
616
  runMirrorBackground(["conversation-logger", "session-maintenance"], "session-maintenance");
559
617
  });
560
618
 
561
- // --- 2. before_agent_start → log user prompt with explicit session id ---
619
+ // --- 2. input → what the person typed, for the run it starts (CR160) ---
562
620
 
563
- pi.on("before_agent_start", async (event, ctx) => {
621
+ pi.on("input", async (event, ctx) => {
564
622
  const sessionId = ctx.sessionManager.getSessionFile() ?? null;
565
623
  if (!sessionId) return;
624
+ // The fields are Pi's (`InputEvent`); the types this directory pins predate one.
625
+ const { text, streamingBehavior } = event as { text: string; streamingBehavior?: string };
626
+ // Queued while Pi streams: it joins a run already going and starts none.
627
+ if (streamingBehavior) return;
628
+ typedInput.set(sessionId, text);
629
+ });
566
630
 
567
- const prompt = event.prompt ?? "";
568
- if (!prompt || prompt.startsWith("/")) return;
631
+ // --- 3. before_agent_start → the turn to the core's prompt rule ---
569
632
 
570
- log("INFO", `log-user: ${prompt.slice(0, 80)}...`);
571
- // Not awaited (CV9.E2.S32): the turn starts now; `agent_end` and
633
+ pi.on("before_agent_start", async (event, ctx) => {
634
+ const sessionId = ctx.sessionManager.getSessionFile() ?? null;
635
+ if (!sessionId) return;
636
+ // A run a custom message started answers no prompt of the person's. Pi's
637
+ // field (`messageType`); the types this directory pins predate it.
638
+ const { messageType } = event as { messageType?: string };
639
+ if (messageType !== undefined && messageType !== "user") return;
640
+
641
+ const typed = typedInput.get(sessionId);
642
+ typedInput.delete(sessionId);
643
+ const prompt = typed ?? typedFormOf(event.prompt ?? "");
644
+
645
+ // A reply still unsent belongs to the prompt before this one: a Pi without
646
+ // `agent_settled` (before 0.80.4) never sent it. It goes first, on the chain.
647
+ const earlier = takeStopArgs(sessionId);
648
+ if (earlier) enqueueUserLog(sessionId, earlier);
649
+
650
+ const turnId = randomUUID();
651
+ pendingReply.set(sessionId, { turnId, texts: [] });
652
+ if (!prompt) return;
653
+
654
+ log("INFO", `user-prompt (turn ${turnId}): ${prompt.slice(0, 80)}...`);
655
+ // Not awaited (CV9.E2.S32): the turn starts now; `agent_settled` and
572
656
  // `session_shutdown` settle the chain before anything that must follow.
573
657
  enqueueUserLog(sessionId, [
574
658
  "conversation-logger",
575
- "log-user",
576
- sessionId,
577
- truncate(prompt),
659
+ "user-prompt",
578
660
  "--interface",
579
661
  "pi",
662
+ "--payload",
663
+ JSON.stringify({ session_id: sessionId, prompt: truncate(prompt), turn_id: turnId }),
580
664
  ]);
581
665
  });
582
666
 
583
- // --- 3. agent_end → log assistant response ---
667
+ // --- 4. agent_end → the run's assistant text, kept for the prompt's reply ---
584
668
  //
585
- // agent_end fires once per user prompt, with ALL messages in the cycle
586
- // (assistant + tool calls + tool results). Extract only assistant text
587
- // and log as a single consolidated message.
669
+ // agent_end fires once per run, with ALL messages in it (assistant + tool
670
+ // calls + tool results), and a prompt can take more than one run. The
671
+ // assistant text is kept until the prompt settles: the hooks' `stop` takes
672
+ // one answer per turn (CR160, A2).
588
673
 
589
674
  pi.on("agent_end", async (event, ctx) => {
590
675
  const sessionId = ctx.sessionManager.getSessionFile() ?? null;
@@ -610,21 +695,34 @@ export default function (pi: ExtensionAPI) {
610
695
 
611
696
  if (assistantTexts.length === 0) return;
612
697
 
613
- log("INFO", `log-assistant: ${assistantTexts.length} block(s), ${assistantTexts.join("").length} chars`);
698
+ // No prompt in flight: a run a custom message started, after the last
699
+ // prompt settled. It answers nothing the person asked, and `stop` would
700
+ // find nothing waiting, as on the other runtimes.
701
+ pendingReply.get(sessionId)?.texts.push(...assistantTexts);
702
+ await refreshMirrorStatus(ctx);
703
+ });
704
+
705
+ // --- 5. agent_settled → the prompt's whole reply to `stop` ---
614
706
 
615
- const combined = assistantTexts.join("\n\n---\n\n");
616
- const content = truncate(combined);
707
+ // `agent_settled` arrived in Pi 0.80.4, after the Pi types this directory pins
708
+ // (`.pi/package.json`), so it is registered by its name alone.
709
+ const onEvent = pi.on.bind(pi) as unknown as (
710
+ event: string,
711
+ handler: (event: unknown, ctx: { sessionManager: { getSessionFile(): string | undefined } }) => Promise<void>,
712
+ ) => void;
617
713
 
618
- // The user message's process has exited before this one is spawned.
714
+ onEvent("agent_settled", async (_event, ctx) => {
715
+ const sessionId = ctx.sessionManager.getSessionFile() ?? null;
716
+ if (!sessionId) return;
717
+ const args = takeStopArgs(sessionId);
718
+ if (!args) return;
719
+ // The prompt's process has exited before this one is spawned; detached, so
720
+ // the write survives Pi exiting right after the answer.
619
721
  await settleUserLog(sessionId);
620
- runMirrorBackground(
621
- ["conversation-logger", "log-assistant", sessionId, content, "--interface", "pi"],
622
- "log-assistant",
623
- );
624
- await refreshMirrorStatus(ctx);
722
+ runMirrorBackground(args, "stop");
625
723
  });
626
724
 
627
- // --- 4. session_shutdown → close conversation + backup ---
725
+ // --- 6. session_shutdown → close conversation + backup ---
628
726
  //
629
727
  // Uses extract=False because extraction calls the LLM and can take 30s+.
630
728
  // Extraction happens at the next session_start via extract_pending.
@@ -633,7 +731,11 @@ export default function (pi: ExtensionAPI) {
633
731
  const sessionId = ctx.sessionManager.getSessionFile() ?? null;
634
732
 
635
733
  if (sessionId) {
636
- // A prompt still in flight must land before the conversation closes.
734
+ // A reply still unsent (a Pi without `agent_settled`) lands before the
735
+ // conversation closes, after its prompt; so does a prompt in flight.
736
+ const reply = takeStopArgs(sessionId);
737
+ if (reply) enqueueUserLog(sessionId, reply);
738
+ typedInput.delete(sessionId);
637
739
  await settleUserLog(sessionId);
638
740
  pendingUserLog.delete(sessionId);
639
741
  await runMirror(["conversation-logger", "session-end-pi", sessionId]);
@@ -6,7 +6,10 @@ user-invocable: true
6
6
 
7
7
  # Help
8
8
 
9
- Show the user the available commands:
9
+ Show the user the available commands, starting with this line:
10
+
11
+ > In Pi, type `/mm-` and pick a command from the menu, which lists each one as
12
+ > `skill:mm-<name>`.
10
13
 
11
14
  ## Operating Modes
12
15
 
package/README.md CHANGED
@@ -114,7 +114,7 @@ this repository, turning a personal experiment into a reusable framework:
114
114
  - **Multi-user** — independent mirrors for multiple users on the same machine
115
115
  - **Multi-session** — concurrent sessions tracked safely in a database-backed session registry
116
116
  - **Intelligence depth** — hybrid semantic and lexical search, honest reinforcement, two-pass extraction, memory consolidation, and shadow cultivation
117
- - **Extension system** — a stable contract for adding user-specific capabilities outside the core; extensions own their own SQLite tables, CLI subcommands, and Mirror Mode context providers without touching the framework
117
+ - **Extension system** — a stable contract for adding user-specific capabilities outside the core; extensions own their own SQLite tables, CLI subcommands, and Mirror Mode context providers without touching the framework, and reach every runtime: Pi through the home's catalog, Claude Code, Codex, Antigravity, and Grok Build through a plugin each home generates
118
118
 
119
119
  The move toward Pi as the preferred runtime was inspired by
120
120
  [Henrique Bastos](https://github.com/henriquebastos) and his early work in
@@ -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.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
+ - [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.
15
17
  - [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.
16
18
  - [v0.36.0 — Five Runtimes, and a Home's Extensions on Every One](v0.36.0.md) — minor release closing CV21.DS10: Grok Build is the fifth runtime, at its honest L3 through the canonical plugin in two printed steps, measured live on grok 1.0.62. With it, CV21.DS5.US2: a home's installed extensions reach Claude Code, Codex, Antigravity, and Grok Build as one generated plugin per home, chosen by measurement on all four. Three Change Requests found by use: an extension's settings file (`extensions/<id>.env`, merged under the environment for the one spawned process), a quiet exit on a closed pipe, and a `bind` that refuses what it cannot bind. No migration.
17
19
  - [v0.35.0 — Four Runtimes, One Successor](v0.35.0.md) — minor release closing CV21.DS8: the Gemini CLI integration no longer ships and Antigravity stands in its place, the gate met by DS7's live L4 on agy 1.4.2. Four runtimes — Pi, Claude Code, Codex, Antigravity — named everywhere; the cutoff recorded in full, with the four hook entries a Gemini CLI user deletes and v0.34.0 as the last release that carries the integration; a retired-surface guard keeps it removed. No migration.
@@ -0,0 +1,130 @@
1
+ ---
2
+ digest: >
3
+ v0.36.2 is a patch release of one Change Request, CR154, which finishes what
4
+ v0.36.1 began. v0.36.1 stopped Mirror importing Pi sessions, but the
5
+ conversations an earlier version had imported stayed, and the only way to
6
+ see them was a SQL query. `mirror conversations imported` now lists them by
7
+ the folder each session came from, beside how many Pi sessions the home
8
+ logged live there, and `remove` deletes the ones you name, only on
9
+ `--apply` and only after a verified backup. With it, `discard-current` no
10
+ longer fails on a conversation a Soul Mode integration names. No migration.
11
+ ---
12
+
13
+ # v0.36.2 — Clearing Out What Another Home Copied
14
+
15
+ **Date:** 2026-10-21
16
+
17
+ ## Highlights
18
+
19
+ - **`mirror conversations imported` shows what an earlier version imported**
20
+ ([CR154](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr154-a-home-cannot-list-or-remove-the-pi-conversations-it-imported.md)):
21
+ one line per folder the session came from, with how many conversations were imported
22
+ from it and how many Pi sessions this home logged live there. A folder where the home
23
+ logged nothing live is most likely another home's work. `--project <text>` lists a
24
+ folder's conversations.
25
+ - **`remove` deletes what you name, and nothing else.** Ids, or every import of one
26
+ folder, written exactly as the listing prints it. It is a dry run until `--apply`;
27
+ `--apply` takes a dated backup, verifies it, and removes in one transaction. Memories
28
+ and provider-call records that named a removed conversation are kept, unlinked, and
29
+ the report names the backups that still hold copies.
30
+ - **A typo cannot widen a removal.** Every option is declared, and one the command does
31
+ not know is refused before the database opens.
32
+ - **`discard-current` works on a Soul Mode conversation.** It failed with `FOREIGN KEY
33
+ constraint failed` on any conversation a Soul Mode integration named. Deleting a
34
+ conversation now decides every table that references one, and a test reads the
35
+ schema so the next such table cannot be missed.
36
+
37
+ ## Where We Started
38
+
39
+ v0.36.1 stopped session maintenance from importing Pi sessions, the defect that let the
40
+ Mirror homes on one machine copy each other's conversations. It removed the cause and
41
+ said so: the copies stayed, and the note gave a SQL query to list them. Deleting them
42
+ was the person's decision, and no command could carry it out — `discard-current`
43
+ removes only the current conversation.
44
+
45
+ ## What Changed
46
+
47
+ **One command, folders first.** A home can hold hundreds of imports, and the decision a
48
+ person makes is about where they came from, so the listing leads with folders. The
49
+ folder is the directory the Pi session file lived in, read from the path the home
50
+ stored; Mirror reads no Pi file to learn it. Beside each folder, the number of Pi
51
+ sessions the home logged live from it. On the maintainer's production home, 96 imports
52
+ came from one development worktree and 76 from the development checkout, where it had
53
+ logged none live, and 3 from its own workspace, where it had logged 714. In a home made
54
+ by copying another, the copied rows count as its own, which blurs the number.
55
+
56
+ **Removal is careful by construction.** A dry run first, with the exact command that
57
+ applies it to the same home. On `--apply`, the set is decided read-only; a refusal or
58
+ an empty set stops there, with no backup. Then the dated archive, verified; then one
59
+ transaction that decides the set again and refuses if it moved. An id must name exactly
60
+ one imported conversation, by its full id or by a prefix of at least eight characters:
61
+ a live conversation, an unknown id, or an ambiguous prefix refuses the whole command.
62
+
63
+ **A review before any code.** Five personas reviewed the plan. The database architect
64
+ found that the deletion `discard-current` used, the one this command meant to reuse,
65
+ missed one of the six tables that reference a conversation, so with foreign keys on, a
66
+ conversation a Soul Mode integration named could not be deleted at all — measured on a
67
+ scratch home, then fixed for both commands. The security engineer found that under the
68
+ listing's lenient parser, a typo such as `--projet` would drop the folder filter and
69
+ remove every import in the home; the command parses strictly, with the parser `backup`
70
+ and `repair-encoding` already used.
71
+
72
+ **Validated on a copy of the real thing.** On a scratch home made from a fresh backup of
73
+ the maintainer's development home, the command listed 515 imports from 22 folders
74
+ and 5 of unknown origin, refused the typo, and removed the 418 from the production workspace's folder with their
75
+ 21,901 messages. Live conversations, memories, and provider-call records kept their
76
+ counts.
77
+
78
+ ### What you will notice
79
+
80
+ - A new command, `mirror conversations imported`, and its `remove`.
81
+ - `discard-current` deletes a conversation that a Soul Mode integration names.
82
+
83
+ ## Upgrading
84
+
85
+ ```bash
86
+ mirror runtime update # or: npm install -g mirror-mind
87
+ ```
88
+
89
+ There is no database migration, and no runtime wiring changes. Then, if you ran more
90
+ than one home:
91
+
92
+ ```bash
93
+ mirror conversations imported
94
+ mirror conversations imported --project <text>
95
+ mirror conversations imported remove --all --project <folder>
96
+ mirror conversations imported remove --all --project <folder> --apply
97
+ ```
98
+
99
+ Start with the folders where "logged live here" is 0, and look at a folder's
100
+ conversations before removing them. The dated backups made since the import, and the
101
+ pre-write backup, keep copies until you delete them or they age out (30 days for an
102
+ archive).
103
+
104
+ ## Conscious Exclusions
105
+
106
+ - **Telling another home's copy from a gap this home filled for itself.** That takes
107
+ both homes' databases, and no home reads another's. The folders and the logged-live
108
+ counts are the evidence a person decides on.
109
+ - **Removing anything from backups.**
110
+ - **Options before the subcommand.** `mirror conversations --db-path X imported` shows
111
+ the plain listing and removes nothing; that is
112
+ [CR096](../project/refinement/rs009-cv22-front-door-routing-correctness/cr096-conversations-lists-instead-of-appending-when-options-come-first.md)'s
113
+ class.
114
+ - **A tool for models.** No MCP tool and no skill runs this command: a person does.
115
+
116
+ ## What We Learned
117
+
118
+ Reusing a routine is a claim about it. The deletion `discard-current` used was correct
119
+ for the tables it knew, and wrong the day another table began to reference a
120
+ conversation. The question that found it — which tables reference a conversation? — is
121
+ now a test that asks the schema, not a person's memory.
122
+
123
+ A lenient parser is harmless in a listing and dangerous in a deletion: the same ignored
124
+ token is a no-op in one and a wider removal in the other.
125
+
126
+ ## Next Horizon
127
+
128
+ CV21.DS5.TS4, then DS11 and CV21's own release. Captured and open: CR155, `runtime
129
+ diagnose` reading the Pi extension's error lines; CR156, what copying a home makes;
130
+ CR157, three settings `REFERENCE.md` lists that nothing reads.
@@ -0,0 +1,142 @@
1
+ ---
2
+ digest: >
3
+ v0.36.3 is a patch release of one Change Request, CR160, with CR159's docs.
4
+ On Pi, a skill picked from the command menu, or typed as `/skill:<name>`,
5
+ reached Mirror after Pi had expanded it into the skill's whole body, and
6
+ Mirror stored that body as something the person said. Pi's turns now go
7
+ through the rule every other runtime has: a skill invocation and its reply
8
+ are not recorded, and a question asked with `/mm-mirror` is recorded as the
9
+ question. `mirror conversations skill-turns` repairs what earlier versions
10
+ stored, only on `--apply` and only after a verified backup. No migration.
11
+ ---
12
+
13
+ # v0.36.3 — A Skill You Invoke Is Not Something You Said
14
+
15
+ **Date:** 2026-10-23
16
+
17
+ ## Highlights
18
+
19
+ - **On Pi, a skill you invoke is not recorded as your words**
20
+ ([CR160](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr160-a-pi-skill-invoked-as-skill-is-logged-as-its-body.md)),
21
+ whether you pick it from the menu or type it, and neither is its answer. A question
22
+ you ask with `/mm-mirror` is recorded as the question, with one reply. A prompt has
23
+ one reply row.
24
+ - **`mirror conversations skill-turns` repairs what an earlier version stored.** It
25
+ shows, by skill, what it would change, and each conversation it would delete because
26
+ no user turn would be left; on `--apply` it takes a dated backup, verifies it, and
27
+ repairs in one transaction. Mirror questions keep their question; other skill turns
28
+ go; a conversation left with no user turn goes, its memories kept, unlinked; a title
29
+ that came from a skill's body is rebuilt from the first remaining user turn.
30
+ - **Pi joins the rule the other runtimes already had.** Mirror's Pi extension sends
31
+ what you typed, read before Pi expands a skill, to the same prompt and reply handlers
32
+ Claude Code, Codex, and Grok Build use.
33
+ - **The docs say how Pi invokes a skill**
34
+ ([CR159](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr159-pi-skills-are-spelled-by-their-bare-name-and-step-7a-lost-two-dashes.md)):
35
+ typing `/mm-` opens Pi's command menu, which lists each skill as `skill:mm-<skill>`;
36
+ pick it there.
37
+
38
+ ## Where We Started
39
+
40
+ Writing down how extensions reach each runtime (CV21.DS5.TS4), the docs called Pi's
41
+ skill commands `/mm-<skill>`, as if Pi registered them. CR159 measured Pi 1.4.0: Pi
42
+ registers each skill as `/skill:<name>`, its menu offers it under that name, and it
43
+ expands the skill into its whole body before any extension sees the prompt. Mirror's
44
+ Pi extension skipped a prompt only when it opened with `/`. An expanded skill opens
45
+ with `<skill`, so it was stored as the person's turn. On the maintainer's two homes,
46
+ 225 and 185 such turns, from April to October: conversations titled
47
+ `<skill name="mm-build" location=…`, and extraction reading a skill's instructions as
48
+ something the person said.
49
+
50
+ ## What Changed
51
+
52
+ **One rule, in one place.** The core already held the rule: what you say to the mirror
53
+ is remembered, a skill you invoke is not. Claude Code, Codex, and Grok Build reach it
54
+ through the hooks' prompt and reply handlers; Pi's extension kept a check of its own,
55
+ and Pi's expansion slipped past it. Rather than teach the extension Pi's wrapper, Pi
56
+ now uses the same handlers: the extension reads what you typed from Pi's documented
57
+ `input` event, gives each run a turn id, and sends the prompt's whole reply once, when
58
+ Pi settles. The payload travels as an argument, because Pi's extension API cannot
59
+ write a process's input; Pi's prompts already travelled that way.
60
+
61
+ **Measured before and after.** A route committed with the Change Request runs the same
62
+ four prompts on Pi 1.4.0 against the tree before the fix and after it. Before, a skill
63
+ picked from the menu or typed as `/skill:` was stored as the person's turn, a question
64
+ typed with `/mm-mirror` was lost, and every skill's reply was kept. After, a skill
65
+ invocation leaves nothing, and a Mirror question leaves the question and one reply.
66
+
67
+ **A repair, careful by construction.** Shaped as v0.36.2's `remove`: a dry run first,
68
+ with the exact command that applies it; on `--apply`, the set decided read-only, then a
69
+ dated archive, verified, then one transaction that decides again and refuses if
70
+ anything moved. It matches only Pi's whole skill block, and the one the old logger cut
71
+ at its 50,000-character limit, and only in Pi conversations: a person may paste
72
+ `<skill name=` anywhere. On copies of the maintainer's two homes it kept 20 Mirror
73
+ questions in each, removed 205 and 165 skill turns, deleted 31 and 24 conversations
74
+ left with no user turn, and a second dry run found nothing.
75
+
76
+ **A review before any code.** Five personas reviewed the plan. The engineer found that
77
+ the hooks already had the rule and a tested handler for each side of a turn, so Pi
78
+ joined them rather than the core learning a second copy. The devops engineer found that
79
+ a running Pi session keeps the extension it loaded, so the old logging commands stay
80
+ for one more release, and the repair is run after every session has restarted.
81
+
82
+ ### What you will notice
83
+
84
+ - In Pi, a skill you pick or type no longer appears in your conversations as something
85
+ you said, and neither does its answer.
86
+ - A question you ask with `/mm-mirror` is recorded as the question; typed in full, it
87
+ used to be lost.
88
+ - A new command, `mirror conversations skill-turns`.
89
+
90
+ ## Upgrading
91
+
92
+ ```bash
93
+ mirror runtime update # or: npm install -g mirror-mind
94
+ ```
95
+
96
+ There is no database migration, and no runtime wiring changes. Then, for every home
97
+ you use with Pi:
98
+
99
+ 1. Restart every Pi session on that home. A session keeps the extension it loaded and,
100
+ until it restarts, stores a skill's body as before.
101
+ 2. See what the repair would change, then repair:
102
+
103
+ ```bash
104
+ mirror conversations skill-turns
105
+ mirror conversations skill-turns --apply
106
+ ```
107
+
108
+ The dry run lists each conversation it would delete, with its message count. Running it
109
+ again later repairs anything a session that had not restarted stored meanwhile. The
110
+ dated backup it takes, and the pre-write backup, keep copies until they age out (30
111
+ days for an archive).
112
+
113
+ ## Conscious Exclusions
114
+
115
+ - **Past replies to skipped turns.** A reply to a skill typed as `/mm-<skill>` was
116
+ recorded before this release, and cannot be told from a reply to an ordinary turn,
117
+ since one prompt can record more than one reply. Those replies stay.
118
+ - **Summaries, their embeddings, and memories** of affected conversations stay: they
119
+ describe a session in which a skill was invoked.
120
+ - **Pi's own session files** are not read or touched.
121
+ - **Older weak titles.** Startup maintenance improves only the titles of the most
122
+ recently ended conversations; the rest wait for
123
+ [CR162](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr162-maintenance-retitles-only-the-latest-conversations.md).
124
+ - **A tool for models.** No MCP tool and no skill runs the repair: a person does.
125
+
126
+ ## What We Learned
127
+
128
+ A rule kept in two places drifts at the edges. The hooks' rule and the Pi extension's
129
+ check agreed for months, until what Pi hands an extension stopped opening with `/`.
130
+ The second copy kept the letter of the rule after its input changed shape; one copy,
131
+ reached by every runtime, cannot.
132
+
133
+ The data that says a thing is wrong is often already there. Four hundred rows titled
134
+ `<skill name=…` sat in two homes for six months, read by every title, summary, and
135
+ extraction pass, and named by none of them.
136
+
137
+ ## Next Horizon
138
+
139
+ CV21.DS11, the contract and docs refresh with the parity matrix, then CV21's own
140
+ release. Captured and open: CR161, the Pi extension's types and the check it lacks;
141
+ CR162, the titles maintenance never reaches. `log-user` and `log-assistant`, kept here
142
+ for sessions that have not restarted, retire in the release after this one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.36.1",
3
+ "version": "0.36.3",
4
4
  "description": "Mirror Mind: a local-first memory and identity framework for agentic AI runtimes (Pi, Claude Code, Codex, Antigravity)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.36.1",
3
+ "version": "0.36.3",
4
4
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
5
5
  "author": {
6
6
  "name": "Mirror Mind"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
3
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
4
- "version": "0.36.1",
4
+ "version": "0.36.3",
5
5
  "mcpServers": {
6
6
  "mirror-mind": {
7
7
  "command": "/bin/sh",
@@ -29,8 +29,9 @@
29
29
  // One rule for every runtime: a Claude Code user who types `$mm-journeys`,
30
30
  // which Claude Code does not understand, is still invoking a skill, and that
31
31
  // turn is not conversation either. The rule has one home for every hook that
32
- // runs in our process (the Claude-family logger, agy's Stop); the Pi
33
- // extension, a separate process in Pi's file, keeps its own `/` check.
32
+ // runs in our process (the Claude-family logger, agy's Stop), and since CR160
33
+ // for Pi as well: its extension sends what the person typed, read before Pi
34
+ // expands a skill into its body, to the same `user-prompt` and `stop` handlers.
34
35
 
35
36
  const SKILL_SEGMENT = "[a-z][a-z0-9_-]*";
36
37
  const DOLLAR_SKILL_FORM = new RegExp(`^\\$${SKILL_SEGMENT}(?::${SKILL_SEGMENT})*(?=\\s|$)`);
@@ -0,0 +1,45 @@
1
+ // What deleting a conversation means, in one place (CR154, review findings D1
2
+ // and E2).
3
+ //
4
+ // Ported as Python's `ConversationService.delete_conversations` for one id, and
5
+ // private to `logger.ts` while `discard-current` was its only caller. CR154's
6
+ // removal of imported Pi conversations is the second, so it lives here now.
7
+ //
8
+ // Six tables reference a conversation, and each gets a decision: what is only
9
+ // the conversation's own is deleted with it, and what outlives it is kept with
10
+ // its link cleared -- a memory extracted from the conversation is still a
11
+ // memory, a provider call still happened, a session row still names its
12
+ // session, an identity integration is still part of the identity. Foreign keys
13
+ // are on (`database.ts`), so a table without a decision makes the conversation
14
+ // undeletable: before CR154 the routine missed `identity_integrations`, and
15
+ // `discard-current` failed with `FOREIGN KEY constraint failed` on any
16
+ // conversation a Soul Mode integration named. `conversationDeletion.test.ts`
17
+ // reads the schema's foreign keys and fails when a table appears that the
18
+ // lists below do not decide.
19
+
20
+ import type { WritableDatabase } from "#db/database.ts";
21
+
22
+ /** Every table that references `conversations(id)`, by what deletion does to its rows. */
23
+ export const CONVERSATION_REFERENCES = {
24
+ /** The conversation's own rows: deleted with it. */
25
+ deleted: ["conversation_embeddings", "messages"],
26
+ /** Rows that outlive it: kept, their `conversation_id` set to null. */
27
+ unlinked: ["identity_integrations", "llm_calls", "memories", "runtime_sessions"],
28
+ } as const;
29
+
30
+ /**
31
+ * Delete one conversation: its own rows first, then the links that outlive
32
+ * it, then the conversation. Runs no transaction of its own -- every caller
33
+ * wraps it in one with whatever else must move together.
34
+ */
35
+ export function deleteConversation(db: WritableDatabase, conversationId: string): void {
36
+ for (const table of CONVERSATION_REFERENCES.deleted) {
37
+ db.prepare(`DELETE FROM ${table} WHERE conversation_id = ?`).run(conversationId);
38
+ }
39
+ for (const table of CONVERSATION_REFERENCES.unlinked) {
40
+ db.prepare(`UPDATE ${table} SET conversation_id = NULL WHERE conversation_id = ?`).run(
41
+ conversationId,
42
+ );
43
+ }
44
+ db.prepare("DELETE FROM conversations WHERE id = ?").run(conversationId);
45
+ }