mirror-mind 0.36.2 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/mirror-logger.ts +293 -64
- package/.pi/skills/mm-help/SKILL.md +4 -1
- package/.pi/skills/mm-mirror/SKILL.md +25 -6
- package/README.md +3 -3
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.36.3.md +142 -0
- package/docs/releases/v0.37.0.md +221 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/plugin.json +1 -1
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +14 -6
- package/ts/src/builder/commands.ts +9 -6
- package/ts/src/builder/load.ts +14 -2
- package/ts/src/conversation/commandTurn.ts +3 -2
- package/ts/src/conversation/logger.ts +12 -6
- package/ts/src/conversation/piSkillTurns.ts +267 -0
- package/ts/src/db/migrateOnOpen.ts +1 -1
- package/ts/src/db/migrations.ts +16 -0
- package/ts/src/db/schema.ts +0 -1
- package/ts/src/db/schemaInventorySnapshot.ts +1 -8
- package/ts/src/db/schemaState.ts +2 -0
- package/ts/src/db/schemaTsDivergence.ts +25 -4
- package/ts/src/frontDoor/cli.ts +58 -2
- package/ts/src/frontDoor/conversationLoggerRoute.ts +20 -2
- package/ts/src/frontDoor/exploreRoute.ts +2 -2
- package/ts/src/frontDoor/mirrorModeRoute.ts +129 -5
- package/ts/src/frontDoor/routing.ts +26 -15
- package/ts/src/frontDoor/skillTurnsRoute.ts +213 -0
- package/ts/src/frontDoor/soulRoute.ts +4 -4
- package/ts/src/frontDoor/stdinSource.ts +5 -0
- package/ts/src/guards/testTempHomes.ts +92 -0
- package/ts/src/hooks/agy.ts +136 -59
- package/ts/src/hooks/claude.ts +29 -90
- package/ts/src/hooks/mirrorState.ts +10 -22
- package/ts/src/mirror/context.ts +19 -4
- package/ts/src/mirror/defaultResolution.ts +2 -1
- package/ts/src/mirror/doorman.ts +420 -0
- package/ts/src/mirror/mirrorForm.ts +4 -1
- package/ts/src/mirror/orchestration.ts +22 -3
- package/ts/src/mirror/render.ts +7 -1
- package/ts/src/mirror/runtimeSession.ts +66 -8
- package/ts/src/mode/operatingMode.ts +22 -4
- package/ts/src/providers/transport.ts +14 -0
- package/ts/src/runtime/diagnose.ts +32 -7
- package/ts/src/runtime/pluginWiring.ts +6 -4
- package/ts/src/runtime/status.ts +3 -2
- package/ts/src/runtime/update.ts +17 -1
- package/ts/src/runtime/wiring.ts +14 -1
- package/ts/src/search/memorySearch.ts +38 -18
- package/ts/src/search/ranker.ts +16 -2
|
@@ -5,11 +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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
8
|
+
* process (CV9.E2.S32); maintenance detached; the session
|
|
9
|
+
* named in the environment (`MIRROR_SESSION_ID`, CV9.DS8.US5)
|
|
10
|
+
* - input → what the person typed, kept for the run it starts (CR160)
|
|
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)
|
|
18
|
+
* - agent_end → the run's assistant text, kept for the prompt's reply
|
|
19
|
+
* - agent_settled → the prompt's whole reply to `stop`, after the chain settles
|
|
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).
|
|
13
33
|
*
|
|
14
34
|
* All heavy logic lives in the TypeScript core, reached through the `mirror`
|
|
15
35
|
* bin that ships beside this file (`bin/mirror.js`). This extension is a thin
|
|
@@ -35,6 +55,7 @@ import { homedir } from "node:os";
|
|
|
35
55
|
import { delimiter, dirname, join } from "node:path";
|
|
36
56
|
import { fileURLToPath } from "node:url";
|
|
37
57
|
import { spawn } from "node:child_process";
|
|
58
|
+
import { randomUUID } from "node:crypto";
|
|
38
59
|
|
|
39
60
|
/** The tree this extension ships in: `.pi/extensions/` sits two below the root. */
|
|
40
61
|
const TREE_ROOT = fileURLToPath(new URL("../../", import.meta.url)).replace(/\/$/, "");
|
|
@@ -199,26 +220,48 @@ type MirrorStatusContext = {
|
|
|
199
220
|
};
|
|
200
221
|
|
|
201
222
|
/**
|
|
202
|
-
* One
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* two
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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.
|
|
208
236
|
*/
|
|
209
|
-
const
|
|
237
|
+
const HANDLED_KEY = "__mirrorLoggerHandledEvents";
|
|
210
238
|
|
|
211
239
|
export default function (pi: ExtensionAPI) {
|
|
212
240
|
const globals = globalThis as Record<string, unknown>;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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);
|
|
218
255
|
}
|
|
219
|
-
|
|
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
|
+
};
|
|
220
264
|
}
|
|
221
|
-
globals[LOADED_KEY] = import.meta.url;
|
|
222
265
|
|
|
223
266
|
// --- Helpers ---
|
|
224
267
|
|
|
@@ -347,19 +390,118 @@ export default function (pi: ExtensionAPI) {
|
|
|
347
390
|
*/
|
|
348
391
|
const pendingUserLog = new Map<string, Promise<void>>();
|
|
349
392
|
|
|
350
|
-
|
|
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> {
|
|
351
399
|
const prior = pendingUserLog.get(sessionId) ?? Promise.resolve();
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
400
|
+
const stdout = prior.then(() => runMirror(args));
|
|
401
|
+
pendingUserLog.set(
|
|
402
|
+
sessionId,
|
|
403
|
+
stdout.then(
|
|
404
|
+
() => undefined,
|
|
405
|
+
() => undefined,
|
|
406
|
+
),
|
|
355
407
|
);
|
|
356
|
-
|
|
408
|
+
return stdout.then(
|
|
409
|
+
(out) => out,
|
|
410
|
+
() => "",
|
|
411
|
+
);
|
|
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
|
+
]);
|
|
357
445
|
}
|
|
358
446
|
|
|
359
447
|
async function settleUserLog(sessionId: string): Promise<void> {
|
|
360
448
|
await (pendingUserLog.get(sessionId) ?? Promise.resolve());
|
|
361
449
|
}
|
|
362
450
|
|
|
451
|
+
/**
|
|
452
|
+
* CR160: Pi's turns reach the core's prompt rule, the one the hooks use
|
|
453
|
+
* (`user-prompt`, `stop`): a skill invocation and its reply are not
|
|
454
|
+
* conversation, a Mirror question is (`commandTurn.ts`, `mirrorForm.ts`).
|
|
455
|
+
*
|
|
456
|
+
* The rule needs what the person typed, and Pi hands `before_agent_start` the
|
|
457
|
+
* prompt after it expanded a skill into its body. Its documented `input` event
|
|
458
|
+
* carries the text as sent, so that text is kept here for the run it starts --
|
|
459
|
+
* only when it starts one: a message queued while Pi streams joins a run
|
|
460
|
+
* already going. The payload travels as `--payload`: `pi.exec` cannot write
|
|
461
|
+
* stdin (`conversationLoggerRoute.ts`).
|
|
462
|
+
*/
|
|
463
|
+
const typedInput = new Map<string, string>();
|
|
464
|
+
|
|
465
|
+
/** The prompt in flight and the assistant text its runs have produced so far. */
|
|
466
|
+
interface PendingReply {
|
|
467
|
+
turnId: string;
|
|
468
|
+
texts: string[];
|
|
469
|
+
}
|
|
470
|
+
const pendingReply = new Map<string, PendingReply>();
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Pi's expansion of `/skill:<name> args`: `<skill name="<name>" location="…">`,
|
|
474
|
+
* `References are relative to ….`, the body, `</skill>`, then the arguments.
|
|
475
|
+
* With no typed text kept for a run, the wrapper is read back to the command
|
|
476
|
+
* it came from, so the rule still sees a command (CR160, A3).
|
|
477
|
+
*/
|
|
478
|
+
const PI_SKILL_WRAPPER =
|
|
479
|
+
/^<skill name="([^"]+)" location="[^"]*">\nReferences are relative to [^\n]*\n\n[\s\S]*?\n<\/skill>(?:\n\n([\s\S]*))?$/;
|
|
480
|
+
|
|
481
|
+
function typedFormOf(prompt: string): string {
|
|
482
|
+
const match = PI_SKILL_WRAPPER.exec(prompt);
|
|
483
|
+
if (!match) return prompt;
|
|
484
|
+
const args = (match[2] ?? "").trim();
|
|
485
|
+
return args ? `/skill:${match[1]} ${args}` : `/skill:${match[1]}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The `stop` argv for a prompt's reply, or null when it produced no text. */
|
|
489
|
+
function takeStopArgs(sessionId: string): string[] | null {
|
|
490
|
+
const reply = pendingReply.get(sessionId);
|
|
491
|
+
pendingReply.delete(sessionId);
|
|
492
|
+
if (!reply || reply.texts.length === 0) return null;
|
|
493
|
+
const content = truncate(reply.texts.join("\n\n---\n\n"));
|
|
494
|
+
log("INFO", `stop (turn ${reply.turnId}): ${reply.texts.length} block(s), ${content.length} chars`);
|
|
495
|
+
return [
|
|
496
|
+
"conversation-logger",
|
|
497
|
+
"stop",
|
|
498
|
+
"--interface",
|
|
499
|
+
"pi",
|
|
500
|
+
"--payload",
|
|
501
|
+
JSON.stringify({ session_id: sessionId, turn_id: reply.turnId, last_assistant_message: content }),
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
|
|
363
505
|
/** Extract readable text from a content blocks array or plain string. */
|
|
364
506
|
function extractText(content: unknown): string {
|
|
365
507
|
if (typeof content === "string") return content;
|
|
@@ -478,13 +620,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
478
620
|
|
|
479
621
|
// --- dynamic resources → installed external Pi skills ---
|
|
480
622
|
|
|
481
|
-
pi.on(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
);
|
|
488
633
|
|
|
489
634
|
// --- 0. the Operating Instructions, for a session Pi did not open in the tree ---
|
|
490
635
|
//
|
|
@@ -495,7 +640,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
495
640
|
// a Mirror session (CV22.DS10.US3 D13). So the extension appends the file
|
|
496
641
|
// it ships with, unless Pi already loaded that very file.
|
|
497
642
|
|
|
498
|
-
pi.on("before_agent_start", async (event) => {
|
|
643
|
+
pi.on("before_agent_start", once("operating_instructions", async (event) => {
|
|
499
644
|
const files = event.systemPromptOptions.contextFiles;
|
|
500
645
|
if (files.some((file) => _isOurAgentsFile(file.path))) return;
|
|
501
646
|
let content: string;
|
|
@@ -508,7 +653,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
508
653
|
if (files.some((file) => file.content === content)) return;
|
|
509
654
|
files.push({ path: AGENTS_FILE, content });
|
|
510
655
|
log("INFO", `operating instructions appended from ${AGENTS_FILE}`);
|
|
511
|
-
});
|
|
656
|
+
}));
|
|
512
657
|
|
|
513
658
|
// --- 1. session_start → unmute + welcome + status line in one process ---
|
|
514
659
|
//
|
|
@@ -517,8 +662,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
517
662
|
// the composite runs them in one process, write first. Maintenance stays
|
|
518
663
|
// detached. Fires on startup, reload, new, resume, and fork alike.
|
|
519
664
|
|
|
520
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
665
|
+
pi.on("session_start", once("session_start", async (_event, ctx) => {
|
|
521
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;
|
|
522
673
|
if (ctx.hasUI) {
|
|
523
674
|
ctx.ui.setStatus("mirror", "◇ Mirror · starting… maintenance will continue in background");
|
|
524
675
|
}
|
|
@@ -556,37 +707,76 @@ export default function (pi: ExtensionAPI) {
|
|
|
556
707
|
setMirrorStatus(ctx, (envelope?.statusLine ?? "").trim());
|
|
557
708
|
}
|
|
558
709
|
runMirrorBackground(["conversation-logger", "session-maintenance"], "session-maintenance");
|
|
559
|
-
});
|
|
710
|
+
}));
|
|
560
711
|
|
|
561
|
-
// --- 2.
|
|
712
|
+
// --- 2. input → what the person typed, for the run it starts (CR160) ---
|
|
562
713
|
|
|
563
|
-
pi.on("
|
|
714
|
+
pi.on("input", once("input", async (event, ctx) => {
|
|
564
715
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
565
716
|
if (!sessionId) return;
|
|
717
|
+
// The fields are Pi's (`InputEvent`); the types this directory pins predate one.
|
|
718
|
+
const { text, streamingBehavior } = event as { text: string; streamingBehavior?: string };
|
|
719
|
+
// Queued while Pi streams: it joins a run already going and starts none.
|
|
720
|
+
if (streamingBehavior) return;
|
|
721
|
+
typedInput.set(sessionId, text);
|
|
722
|
+
}));
|
|
566
723
|
|
|
567
|
-
|
|
568
|
-
if (!prompt || prompt.startsWith("/")) return;
|
|
724
|
+
// --- 3. before_agent_start → the turn to the core's prompt rule ---
|
|
569
725
|
|
|
570
|
-
|
|
571
|
-
|
|
726
|
+
pi.on("before_agent_start", once("prompt", async (event, ctx) => {
|
|
727
|
+
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
728
|
+
if (!sessionId) return;
|
|
729
|
+
// A run a custom message started answers no prompt of the person's. Pi's
|
|
730
|
+
// field (`messageType`); the types this directory pins predate it.
|
|
731
|
+
const { messageType } = event as { messageType?: string };
|
|
732
|
+
if (messageType !== undefined && messageType !== "user") return;
|
|
733
|
+
|
|
734
|
+
const typed = typedInput.get(sessionId);
|
|
735
|
+
typedInput.delete(sessionId);
|
|
736
|
+
const prompt = typed ?? typedFormOf(event.prompt ?? "");
|
|
737
|
+
|
|
738
|
+
// A reply still unsent belongs to the prompt before this one: a Pi without
|
|
739
|
+
// `agent_settled` (before 0.80.4) never sent it. It goes first, on the chain.
|
|
740
|
+
const earlier = takeStopArgs(sessionId);
|
|
741
|
+
if (earlier) enqueueUserLog(sessionId, earlier);
|
|
742
|
+
|
|
743
|
+
const turnId = randomUUID();
|
|
744
|
+
pendingReply.set(sessionId, { turnId, texts: [] });
|
|
745
|
+
if (!prompt) return;
|
|
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
|
+
|
|
751
|
+
log("INFO", `user-prompt (turn ${turnId}): ${prompt.slice(0, 80)}...`);
|
|
752
|
+
// Not awaited (CV9.E2.S32): the turn starts now; `agent_settled` and
|
|
572
753
|
// `session_shutdown` settle the chain before anything that must follow.
|
|
573
754
|
enqueueUserLog(sessionId, [
|
|
574
755
|
"conversation-logger",
|
|
575
|
-
"
|
|
576
|
-
sessionId,
|
|
577
|
-
truncate(prompt),
|
|
756
|
+
"user-prompt",
|
|
578
757
|
"--interface",
|
|
579
758
|
"pi",
|
|
759
|
+
"--payload",
|
|
760
|
+
JSON.stringify({ session_id: sessionId, prompt: truncate(prompt), turn_id: turnId }),
|
|
580
761
|
]);
|
|
581
|
-
});
|
|
582
762
|
|
|
583
|
-
|
|
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
|
+
}));
|
|
771
|
+
|
|
772
|
+
// --- 4. agent_end → the run's assistant text, kept for the prompt's reply ---
|
|
584
773
|
//
|
|
585
|
-
// agent_end fires once per
|
|
586
|
-
//
|
|
587
|
-
//
|
|
774
|
+
// agent_end fires once per run, with ALL messages in it (assistant + tool
|
|
775
|
+
// calls + tool results), and a prompt can take more than one run. The
|
|
776
|
+
// assistant text is kept until the prompt settles: the hooks' `stop` takes
|
|
777
|
+
// one answer per turn (CR160, A2).
|
|
588
778
|
|
|
589
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
779
|
+
pi.on("agent_end", once("agent_end", async (event, ctx) => {
|
|
590
780
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
591
781
|
if (!sessionId) return;
|
|
592
782
|
|
|
@@ -610,30 +800,69 @@ export default function (pi: ExtensionAPI) {
|
|
|
610
800
|
|
|
611
801
|
if (assistantTexts.length === 0) return;
|
|
612
802
|
|
|
613
|
-
|
|
803
|
+
// No prompt in flight: a run a custom message started, after the last
|
|
804
|
+
// prompt settled. It answers nothing the person asked, and `stop` would
|
|
805
|
+
// find nothing waiting, as on the other runtimes.
|
|
806
|
+
pendingReply.get(sessionId)?.texts.push(...assistantTexts);
|
|
807
|
+
await refreshMirrorStatus(ctx);
|
|
808
|
+
}));
|
|
614
809
|
|
|
615
|
-
|
|
616
|
-
const content = truncate(combined);
|
|
810
|
+
// --- 5. agent_settled → the prompt's whole reply to `stop` ---
|
|
617
811
|
|
|
618
|
-
|
|
812
|
+
// `agent_settled` arrived in Pi 0.80.4, after the Pi types this directory pins
|
|
813
|
+
// (`.pi/package.json`), so it is registered by its name alone.
|
|
814
|
+
const onEvent = pi.on.bind(pi) as unknown as (
|
|
815
|
+
event: string,
|
|
816
|
+
handler: (
|
|
817
|
+
event: unknown,
|
|
818
|
+
ctx: { sessionManager: { getSessionFile(): string | undefined } },
|
|
819
|
+
) => Promise<void> | undefined,
|
|
820
|
+
) => void;
|
|
821
|
+
|
|
822
|
+
onEvent("agent_settled", once("agent_settled", async (_event, ctx) => {
|
|
823
|
+
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
824
|
+
if (!sessionId) return;
|
|
825
|
+
const args = takeStopArgs(sessionId);
|
|
826
|
+
if (!args) return;
|
|
827
|
+
// The prompt's process has exited before this one is spawned; detached, so
|
|
828
|
+
// the write survives Pi exiting right after the answer.
|
|
619
829
|
await settleUserLog(sessionId);
|
|
620
|
-
runMirrorBackground(
|
|
621
|
-
|
|
622
|
-
"log-assistant",
|
|
623
|
-
);
|
|
624
|
-
await refreshMirrorStatus(ctx);
|
|
625
|
-
});
|
|
830
|
+
runMirrorBackground(args, "stop");
|
|
831
|
+
}));
|
|
626
832
|
|
|
627
|
-
// ---
|
|
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
|
+
);
|
|
849
|
+
|
|
850
|
+
// --- 6. session_shutdown → close conversation + backup ---
|
|
628
851
|
//
|
|
629
852
|
// Uses extract=False because extraction calls the LLM and can take 30s+.
|
|
630
853
|
// Extraction happens at the next session_start via extract_pending.
|
|
631
854
|
|
|
632
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
855
|
+
pi.on("session_shutdown", once("session_shutdown", async (_event, ctx) => {
|
|
633
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;
|
|
634
859
|
|
|
635
860
|
if (sessionId) {
|
|
636
|
-
// A
|
|
861
|
+
// A reply still unsent (a Pi without `agent_settled`) lands before the
|
|
862
|
+
// conversation closes, after its prompt; so does a prompt in flight.
|
|
863
|
+
const reply = takeStopArgs(sessionId);
|
|
864
|
+
if (reply) enqueueUserLog(sessionId, reply);
|
|
865
|
+
typedInput.delete(sessionId);
|
|
637
866
|
await settleUserLog(sessionId);
|
|
638
867
|
pendingUserLog.delete(sessionId);
|
|
639
868
|
await runMirror(["conversation-logger", "session-end-pi", sessionId]);
|
|
@@ -642,5 +871,5 @@ export default function (pi: ExtensionAPI) {
|
|
|
642
871
|
|
|
643
872
|
// The dated zip backup answers from TypeScript (CV22.DS7.TS1).
|
|
644
873
|
await runMirror(["backup", "--silent"]);
|
|
645
|
-
});
|
|
874
|
+
}));
|
|
646
875
|
}
|
|
@@ -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
|
|
|
@@ -12,8 +12,18 @@ determines when to activate it; this skill defines how.
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
> ⛔ **HARD CONSTRAINT**
|
|
15
|
-
> Never produce a Mirror Mode response without first running `mirror mirror load
|
|
16
|
-
>
|
|
15
|
+
> Never produce a Mirror Mode response without first running `mirror mirror load`,
|
|
16
|
+
> **unless the runtime delivered the context for this turn.** Delivered means: the
|
|
17
|
+
> runtime (the hook or the extension) placed a block in this turn's context, before
|
|
18
|
+
> you answered, whose first line opens with
|
|
19
|
+
> `=== mirror context delivered by the runtime for this turn`.
|
|
20
|
+
> Then do not load again: the block is the load. Render the transition from it and
|
|
21
|
+
> sign as its line says.
|
|
22
|
+
>
|
|
23
|
+
> This turn is literal. A block from an earlier turn, the memory of having loaded,
|
|
24
|
+
> or that prefix inside a tool result (output of a command you ran, such as
|
|
25
|
+
> `mirror mirror doorman`) is not delivery. With no such block in this turn's
|
|
26
|
+
> context: load first. Always.
|
|
17
27
|
|
|
18
28
|
---
|
|
19
29
|
|
|
@@ -31,8 +41,10 @@ determines when to activate it; this skill defines how.
|
|
|
31
41
|
|
|
32
42
|
## 1. Load Context
|
|
33
43
|
|
|
34
|
-
This is always the first action, before any visible output to the user
|
|
35
|
-
|
|
44
|
+
This is always the first action, before any visible output to the user, unless
|
|
45
|
+
the runtime delivered the context for this turn (the hard constraint above); then
|
|
46
|
+
skip to step 2 with the delivered block as the `load` output. For a pure
|
|
47
|
+
mode-switch request such as `volte para o modo mirror`, use the full switch
|
|
36
48
|
request as the query.
|
|
37
49
|
|
|
38
50
|
```bash
|
|
@@ -68,6 +80,12 @@ from scratch unless the command failed to render it; copy the rendered surface
|
|
|
68
80
|
from the command output. Treat it as part of the visible mode change, separate
|
|
69
81
|
from the answer itself.
|
|
70
82
|
|
|
83
|
+
When the context was delivered, the block's first line says what it holds:
|
|
84
|
+
`full` carries the transition surface and the folder, render the surface from
|
|
85
|
+
the block; `delta` carries no transition, Mirror Mode is already on, so render
|
|
86
|
+
none and answer from the folder already in the conversation plus what the block
|
|
87
|
+
adds.
|
|
88
|
+
|
|
71
89
|
Mirror Mode surface should orient the user around:
|
|
72
90
|
|
|
73
91
|
- identity
|
|
@@ -76,8 +94,9 @@ Mirror Mode surface should orient the user around:
|
|
|
76
94
|
- persona routing examples (`when the topic asks: persona_1, persona_2, persona_3 and N more available`)
|
|
77
95
|
- available lenses: `◌ Mirror Mode`, `■ Builder Mode`, `△ Explorer Mode`, `☾ Soul Mode`
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
An active persona signs `◇ persona-name` on its own line, the rule `AGENTS.md`
|
|
98
|
+
states; the delivered line names the persona to sign. No signature when the ego
|
|
99
|
+
answers alone.
|
|
81
100
|
|
|
82
101
|
## 3. Answer
|
|
83
102
|
|
package/README.md
CHANGED
|
@@ -60,8 +60,8 @@ This is not a chatbot with memory. This is a mirror — and yours.
|
|
|
60
60
|
|
|
61
61
|
## How It Works
|
|
62
62
|
|
|
63
|
-
Local-first. SQLite. Five supported runtimes:
|
|
64
|
-
Antigravity, Grok Build. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
|
|
63
|
+
Local-first. SQLite. [Five supported runtimes](docs/product/specs/runtime-interface/index.md#parity-matrix):
|
|
64
|
+
Pi, Claude Code, Codex, Antigravity, Grok Build. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
|
|
65
65
|
Personas, Shadow.
|
|
66
66
|
Context-activated modes — Mirror, Builder, Explorer, and Soul. The identity lives
|
|
67
67
|
in your machine, not a server.
|
|
@@ -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.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.
|
|
16
|
+
- [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.
|
|
15
17
|
- [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.
|
|
16
18
|
- [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.
|
|
17
19
|
- [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.
|