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.
- package/.pi/extensions/mirror-logger.ts +128 -26
- package/.pi/skills/mm-help/SKILL.md +4 -1
- package/README.md +1 -1
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.36.2.md +130 -0
- package/docs/releases/v0.36.3.md +142 -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/ts/src/conversation/commandTurn.ts +3 -2
- package/ts/src/conversation/conversationDeletion.ts +45 -0
- package/ts/src/conversation/importedConversations.ts +288 -0
- package/ts/src/conversation/logger.ts +4 -18
- package/ts/src/conversation/piSkillTurns.ts +267 -0
- package/ts/src/frontDoor/cli.ts +38 -0
- package/ts/src/frontDoor/conversationLoggerRoute.ts +20 -2
- package/ts/src/frontDoor/dbSafetyToolsRoute.ts +1 -35
- package/ts/src/frontDoor/importedConversationsRoute.ts +337 -0
- package/ts/src/frontDoor/routing.ts +22 -0
- package/ts/src/frontDoor/skillTurnsRoute.ts +213 -0
- package/ts/src/frontDoor/stdinSource.ts +5 -0
- package/ts/src/mirror/mirrorForm.ts +4 -1
- package/ts/src/util/strictArgs.ts +59 -0
|
@@ -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
|
-
* -
|
|
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 →
|
|
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.
|
|
619
|
+
// --- 2. input → what the person typed, for the run it starts (CR160) ---
|
|
562
620
|
|
|
563
|
-
pi.on("
|
|
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
|
-
|
|
568
|
-
if (!prompt || prompt.startsWith("/")) return;
|
|
631
|
+
// --- 3. before_agent_start → the turn to the core's prompt rule ---
|
|
569
632
|
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
"
|
|
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
|
-
// ---
|
|
667
|
+
// --- 4. agent_end → the run's assistant text, kept for the prompt's reply ---
|
|
584
668
|
//
|
|
585
|
-
// agent_end fires once per
|
|
586
|
-
//
|
|
587
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
|
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
|
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.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
|
@@ -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)
|
|
33
|
-
//
|
|
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
|
+
}
|