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
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// The one doorman (CV9.DS8.TS2).
|
|
2
|
+
//
|
|
3
|
+
// Mirror Mode is the product: the folder `mirror mirror load` builds from the
|
|
4
|
+
// database, open before the model answers. What is at stake is always whether
|
|
5
|
+
// the folder reaches the model, and when. Until DS8 there were three doormen
|
|
6
|
+
// with three policies (the Claude-family inject hook, Antigravity's
|
|
7
|
+
// PreInvocation, and none on Pi), and wherever one existed the skill's own
|
|
8
|
+
// hard constraint made an explicit turn load twice (CR165, measured on Pi
|
|
9
|
+
// 1.4.0, 2026-10-24).
|
|
10
|
+
//
|
|
11
|
+
// This module is the one decision. Given a turn's prompt and the session's
|
|
12
|
+
// state it answers NOTHING, THE FULL FOLDER, or A DELTA:
|
|
13
|
+
//
|
|
14
|
+
// 1. the explicit Mirror form in the prompt -> the full folder, routed from
|
|
15
|
+
// the query, before the first token; the session marked active by its id;
|
|
16
|
+
// 2. Mirror Mode active, an ordinary turn -> the prompt is routed with
|
|
17
|
+
// `detectPersona` (a keyword score, no model call); a persona that newly
|
|
18
|
+
// scores has its section delivered and becomes the session's; the
|
|
19
|
+
// memories and attachments relevant to the prompt follow; the identity
|
|
20
|
+
// layers are not repeated -- they already sit in the transcript;
|
|
21
|
+
// a skill invocation other than the Mirror form is not a question and
|
|
22
|
+
// gets nothing (CR160's command-turn rule);
|
|
23
|
+
// 3. Mirror Mode not active -> nothing. The model's own routing may invoke
|
|
24
|
+
// the skill; the session learns that load from the harness (TS1), and the
|
|
25
|
+
// doorman takes over from the next turn.
|
|
26
|
+
//
|
|
27
|
+
// `decideDoorman` is pure: no database, no clock, no provider. `runDoorman`
|
|
28
|
+
// renders and writes, at the edge. The hooks (US4, TS6) call both in-process;
|
|
29
|
+
// the Pi extension (US5) reaches them through `mirror doorman`. Neither is
|
|
30
|
+
// rewired here.
|
|
31
|
+
//
|
|
32
|
+
// Every block the doorman delivers opens with one line the model reads
|
|
33
|
+
// (`deliveredLine`), on every active turn, even with nothing under it: it is
|
|
34
|
+
// what carries the persona across the conversation. On an ordinary turn the
|
|
35
|
+
// skill is never invoked, so this line is the only Mirror text that reaches
|
|
36
|
+
// the model -- the signature rule (`AGENTS.md`: `◇ persona-name`) rides in it.
|
|
37
|
+
//
|
|
38
|
+
// What a turn costs: nothing when Mirror Mode is off; on an ordinary active
|
|
39
|
+
// turn, ONE embedding, shared by the memory and attachment searches and
|
|
40
|
+
// counted once in `llm_calls` (DS8 decision 1); on the explicit turn, what
|
|
41
|
+
// `mirror load --query` costs today (reception's LLM call when enabled, the
|
|
42
|
+
// attachment embedding), plus the skill's own load until TS3 teaches it to
|
|
43
|
+
// trust this line.
|
|
44
|
+
|
|
45
|
+
import { isCommandTurn } from "#conversation/commandTurn.ts";
|
|
46
|
+
import type { Database, WritableDatabase } from "#db/database.ts";
|
|
47
|
+
import { memoriesById } from "#frontDoor/searchRoute.ts";
|
|
48
|
+
import { getIdentityContent } from "#identity/identityRead.ts";
|
|
49
|
+
import { getActiveOperatingMode } from "#mode/operatingMode.ts";
|
|
50
|
+
import { embeddingLedgerHook } from "#observability/ledgerHooks.ts";
|
|
51
|
+
import { detectPersona, type PersonaRoutingRow } from "#persona/detectPersona.ts";
|
|
52
|
+
import { type EmbeddingProvider, generateEmbeddingSafely } from "#providers/embedding.ts";
|
|
53
|
+
import { searchMemoriesWithStatus } from "#search/memorySearch.ts";
|
|
54
|
+
import { pythonJsonDumps } from "#util/pyGenerators.ts";
|
|
55
|
+
import { relevantAttachments } from "./context.ts";
|
|
56
|
+
import { personaRoutingRows } from "./defaultResolution.ts";
|
|
57
|
+
import { mirrorForm } from "./mirrorForm.ts";
|
|
58
|
+
import { type MirrorLoadInput, runMirrorLoad } from "./orchestration.ts";
|
|
59
|
+
import {
|
|
60
|
+
decodeMetadata,
|
|
61
|
+
getRuntimeSession,
|
|
62
|
+
updateRuntimeSessionColumns,
|
|
63
|
+
} from "./runtimeSession.ts";
|
|
64
|
+
|
|
65
|
+
/** The session's Mirror fields as the decision reads them; null when unnamed or unknown. */
|
|
66
|
+
export interface DoormanSessionState {
|
|
67
|
+
readonly active: boolean;
|
|
68
|
+
readonly persona: string | null;
|
|
69
|
+
readonly journey: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* The folder left the transcript (the runtime compacted it, and said so
|
|
72
|
+
* through `forgetDoormanDelivery`): the next ordinary turn is full again.
|
|
73
|
+
*/
|
|
74
|
+
readonly refresh?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type DoormanDecision =
|
|
78
|
+
| { readonly kind: "nothing" }
|
|
79
|
+
| { readonly kind: "full"; readonly query: string }
|
|
80
|
+
| {
|
|
81
|
+
readonly kind: "delta";
|
|
82
|
+
readonly query: string;
|
|
83
|
+
/** The persona that newly scored, or null when the session's stands. */
|
|
84
|
+
readonly persona: string | null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** The stable prefix TS3's skill reads; everything after the colon varies. */
|
|
88
|
+
export const DELIVERED_LINE_PREFIX = "=== mirror context delivered by the runtime for this turn";
|
|
89
|
+
|
|
90
|
+
/** How many memories a delta carries at most. */
|
|
91
|
+
export const DOORMAN_MEMORY_LIMIT = 5;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The threshold DS8's policy names ("injected when any score above the
|
|
95
|
+
* threshold"), on the SEMANTIC score the ranker reports -- the cosine of the
|
|
96
|
+
* prompt and the memory -- not the hybrid one (CV9.DS8.US5). The hybrid is
|
|
97
|
+
* half recency, reinforcement, relevance, and lexical: a fresh, reinforced,
|
|
98
|
+
* relevance-1 memory floors at 0.35 with no semantic match and would pass a
|
|
99
|
+
* composite 0.40 at cosine 0.10, which unrelated texts reach routinely; the 27
|
|
100
|
+
* composite rows TS2 and US4 logged (off-topic deltas topping at 0.331-0.377,
|
|
101
|
+
* 3 of 5 memories repeating; on-topic at 0.444-0.448 and 0.587-0.591) show the
|
|
102
|
+
* gap in one home, not a property. 0.4 is the rule `relevantAttachments`
|
|
103
|
+
* already applies to attachments: one rule for both. Inclusive. A degraded
|
|
104
|
+
* search (no embedding) reports no semantic score and keeps TS2's FTS
|
|
105
|
+
* behavior: a lexical hit is relevance by another signal, not a miss.
|
|
106
|
+
*/
|
|
107
|
+
export const DOORMAN_MEMORY_SEMANTIC_THRESHOLD = 0.4;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where the session remembers the memories the doorman has delivered
|
|
111
|
+
* (`runtime_sessions.metadata`, beside `operating_mode`). Every delta is a
|
|
112
|
+
* message that stays in the transcript, and US4 measured 3 of 5 memories
|
|
113
|
+
* repeating on a follow-up: a delta skips what the conversation already holds
|
|
114
|
+
* and fills from the next ones above the threshold. A full load starts the
|
|
115
|
+
* list over -- the folder is fresh, and so is what the model has read.
|
|
116
|
+
*/
|
|
117
|
+
export const DOORMAN_DELIVERED_METADATA_KEY = "doorman_delivered";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set on the session when the runtime compacted the transcript (the Pi
|
|
121
|
+
* extension's `session_compact`, CV9.DS8.US5): a delta's premise -- the
|
|
122
|
+
* identity layers already sit in the transcript -- no longer holds, so the
|
|
123
|
+
* next ordinary turn in Mirror Mode delivers the full folder, routed from that
|
|
124
|
+
* prompt, and the full load clears the flag.
|
|
125
|
+
*/
|
|
126
|
+
export const DOORMAN_REFRESH_METADATA_KEY = "doorman_refresh";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The runtime says the folder left the transcript: the next ordinary turn is
|
|
130
|
+
* full, and the delivered list is forgotten with it. Unknown session: no-op.
|
|
131
|
+
*/
|
|
132
|
+
export function forgetDoormanDelivery(
|
|
133
|
+
db: WritableDatabase,
|
|
134
|
+
sessionId: string,
|
|
135
|
+
nowIso: string,
|
|
136
|
+
): void {
|
|
137
|
+
const session = getRuntimeSession(db, sessionId);
|
|
138
|
+
if (!session) return;
|
|
139
|
+
const metadata = decodeMetadata(session.metadata);
|
|
140
|
+
metadata[DOORMAN_REFRESH_METADATA_KEY] = true;
|
|
141
|
+
metadata[DOORMAN_DELIVERED_METADATA_KEY] = [];
|
|
142
|
+
updateRuntimeSessionColumns(db, sessionId, { metadata: pythonJsonDumps(metadata) }, nowIso);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** How many ranked memories a delta may read through to fill its five. */
|
|
146
|
+
const DOORMAN_SEARCH_CEILING = 25;
|
|
147
|
+
|
|
148
|
+
function deliveredMemoryIds(db: Database, sessionId: string | null): Set<string> {
|
|
149
|
+
if (!sessionId) return new Set();
|
|
150
|
+
const raw = decodeMetadata(getRuntimeSession(db, sessionId)?.metadata ?? null)[
|
|
151
|
+
DOORMAN_DELIVERED_METADATA_KEY
|
|
152
|
+
];
|
|
153
|
+
return new Set(
|
|
154
|
+
Array.isArray(raw) ? raw.filter((id): id is string => typeof id === "string") : [],
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rememberDelivered(
|
|
159
|
+
db: WritableDatabase,
|
|
160
|
+
sessionId: string | null,
|
|
161
|
+
ids: readonly string[],
|
|
162
|
+
nowIso: string,
|
|
163
|
+
options: { clearRefresh?: boolean } = {},
|
|
164
|
+
): void {
|
|
165
|
+
if (!sessionId) return;
|
|
166
|
+
const session = getRuntimeSession(db, sessionId);
|
|
167
|
+
if (!session) return;
|
|
168
|
+
const metadata = decodeMetadata(session.metadata);
|
|
169
|
+
metadata[DOORMAN_DELIVERED_METADATA_KEY] = [...ids];
|
|
170
|
+
if (options.clearRefresh) delete metadata[DOORMAN_REFRESH_METADATA_KEY];
|
|
171
|
+
updateRuntimeSessionColumns(db, sessionId, { metadata: pythonJsonDumps(metadata) }, nowIso);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One ranked memory's scores, content-free, for the front-door log. */
|
|
175
|
+
export interface MemoryScore {
|
|
176
|
+
readonly score: number;
|
|
177
|
+
readonly semantic: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The session's state as the decision reads it. "Mirror Mode on" is
|
|
182
|
+
* `mirror_active` -- which only `mirror deactivate` clears -- AND the active
|
|
183
|
+
* operating mode (the session's own row, else the global one) still being
|
|
184
|
+
* Mirror Mode (CV9.DS8.US4). `build load`, `explore`, and `soul load` switch the
|
|
185
|
+
* mode and leave the column set; read alone, it would put "answer as the
|
|
186
|
+
* mirror" on every Builder turn of a session that was once in Mirror Mode. A
|
|
187
|
+
* home with no mode record at all leaves the column to decide.
|
|
188
|
+
*/
|
|
189
|
+
export function readDoormanSessionState(
|
|
190
|
+
db: Database,
|
|
191
|
+
sessionId: string | null,
|
|
192
|
+
): DoormanSessionState | null {
|
|
193
|
+
if (!sessionId) return null;
|
|
194
|
+
const session = getRuntimeSession(db, sessionId);
|
|
195
|
+
if (!session) return null;
|
|
196
|
+
const mode = getActiveOperatingMode(db, sessionId);
|
|
197
|
+
const inMirrorMode = mode === null || mode.mode === "Mirror Mode";
|
|
198
|
+
return {
|
|
199
|
+
active: session.mirrorActive && inMirrorMode,
|
|
200
|
+
persona: session.persona,
|
|
201
|
+
journey: session.journey,
|
|
202
|
+
refresh: decodeMetadata(session.metadata)[DOORMAN_REFRESH_METADATA_KEY] === true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function decideDoorman(input: {
|
|
207
|
+
prompt: string;
|
|
208
|
+
state: DoormanSessionState | null;
|
|
209
|
+
personas: readonly PersonaRoutingRow[];
|
|
210
|
+
}): DoormanDecision {
|
|
211
|
+
const explicit = mirrorForm(input.prompt);
|
|
212
|
+
if (explicit) return { kind: "full", query: explicit.query };
|
|
213
|
+
if (!input.state?.active) return { kind: "nothing" };
|
|
214
|
+
// A skill invocation is not a question to the mirror (CR160's rule for the
|
|
215
|
+
// logger, held here since CV9.DS8.US5's walk): `/mm-build`, `/mm-journeys`,
|
|
216
|
+
// `/mm-explore` paid an embedding and got a delta, and one Builder card was
|
|
217
|
+
// signed. The Mirror form, the one skill whose argument IS the question,
|
|
218
|
+
// was answered above.
|
|
219
|
+
if (isCommandTurn(input.prompt)) return { kind: "nothing" };
|
|
220
|
+
// The runtime compacted the transcript: the folder is a summary now, so this
|
|
221
|
+
// turn delivers it whole, routed from the prompt, and the deltas resume after.
|
|
222
|
+
if (input.state.refresh) return { kind: "full", query: input.prompt };
|
|
223
|
+
const match = detectPersona(input.prompt, input.personas)[0]?.key ?? null;
|
|
224
|
+
const persona = match !== null && match !== input.state.persona ? match : null;
|
|
225
|
+
return { kind: "delta", query: input.prompt, persona };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function deliveredLine(input: {
|
|
229
|
+
kind: "full" | "delta";
|
|
230
|
+
persona: string | null;
|
|
231
|
+
journey: string | null;
|
|
232
|
+
}): string {
|
|
233
|
+
const clause = input.persona
|
|
234
|
+
? `answer as the mirror; sign ◇ ${input.persona}`
|
|
235
|
+
: "answer as the mirror";
|
|
236
|
+
const what =
|
|
237
|
+
input.kind === "full"
|
|
238
|
+
? "full"
|
|
239
|
+
: `delta (persona: ${input.persona ?? "none"} · journey: ${input.journey ?? "none"})`;
|
|
240
|
+
return `${DELIVERED_LINE_PREFIX}: ${what} — ${clause} ===`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface RunDoormanInput
|
|
244
|
+
extends Omit<MirrorLoadInput, "query" | "contextOnly" | "sessionId" | "environmentSessionId"> {
|
|
245
|
+
/** The named session, or null: an unnamed explicit turn loads without marking one. */
|
|
246
|
+
sessionId: string | null;
|
|
247
|
+
embeddingProvider?: EmbeddingProvider;
|
|
248
|
+
/**
|
|
249
|
+
* Where a delta turn's memory scores go -- every ranked memory's, the ones
|
|
250
|
+
* under the threshold included -- so the threshold is read against
|
|
251
|
+
* measurement (US5's live walk), not invented. Content-free: numbers only.
|
|
252
|
+
*/
|
|
253
|
+
onMemoryScores?: (scores: readonly MemoryScore[]) => void;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface RenderedDoorman {
|
|
257
|
+
stdout: string;
|
|
258
|
+
stderr: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Render and write for a decision already taken. The caller opened `db` for
|
|
263
|
+
* writing only because the decision is `full` or `delta`: a `nothing` turn must
|
|
264
|
+
* never reach a writable handle (the live-write path snapshots the database
|
|
265
|
+
* first, 95 ms measured), which is why decide and run are two steps.
|
|
266
|
+
*/
|
|
267
|
+
export async function runDoorman(
|
|
268
|
+
db: WritableDatabase,
|
|
269
|
+
decision: DoormanDecision,
|
|
270
|
+
input: RunDoormanInput,
|
|
271
|
+
): Promise<RenderedDoorman> {
|
|
272
|
+
if (decision.kind === "nothing") return { stdout: "", stderr: "" };
|
|
273
|
+
if (decision.kind === "full") return runFull(db, decision.query, input);
|
|
274
|
+
return runDelta(db, decision, input);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function runFull(
|
|
278
|
+
db: WritableDatabase,
|
|
279
|
+
query: string,
|
|
280
|
+
input: RunDoormanInput,
|
|
281
|
+
): Promise<RenderedDoorman> {
|
|
282
|
+
const loaded = await runMirrorLoad(db, {
|
|
283
|
+
...input,
|
|
284
|
+
query: query || null,
|
|
285
|
+
contextOnly: true,
|
|
286
|
+
sessionId: input.sessionId,
|
|
287
|
+
});
|
|
288
|
+
if (input.sessionId) {
|
|
289
|
+
// The conversation's context starts over with the folder: so does the
|
|
290
|
+
// list of memories the deltas have put in it, and a compaction's refresh
|
|
291
|
+
// is answered.
|
|
292
|
+
rememberDelivered(db, input.sessionId, [], input.nowIso(), { clearRefresh: true });
|
|
293
|
+
}
|
|
294
|
+
const line = deliveredLine({
|
|
295
|
+
kind: "full",
|
|
296
|
+
persona: loaded.resolved.persona,
|
|
297
|
+
journey: loaded.resolved.journey,
|
|
298
|
+
});
|
|
299
|
+
return { stdout: `${line}\n${loaded.stdout}`, stderr: loaded.stderr };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function runDelta(
|
|
303
|
+
db: WritableDatabase,
|
|
304
|
+
decision: Extract<DoormanDecision, { kind: "delta" }>,
|
|
305
|
+
input: RunDoormanInput,
|
|
306
|
+
): Promise<RenderedDoorman> {
|
|
307
|
+
const state = readDoormanSessionState(db, input.sessionId);
|
|
308
|
+
const persona = decision.persona ?? state?.persona ?? null;
|
|
309
|
+
const journey = state?.journey ?? null;
|
|
310
|
+
const parts: string[] = [deliveredLine({ kind: "delta", persona, journey })];
|
|
311
|
+
|
|
312
|
+
if (decision.persona) {
|
|
313
|
+
const content = getIdentityContent(db, "persona", decision.persona);
|
|
314
|
+
if (content) parts.push(`=== persona/${decision.persona} ===\n${content}`);
|
|
315
|
+
if (input.sessionId) {
|
|
316
|
+
updateRuntimeSessionColumns(
|
|
317
|
+
db,
|
|
318
|
+
input.sessionId,
|
|
319
|
+
{ persona: decision.persona },
|
|
320
|
+
input.nowIso(),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// One embedding for the turn, in the ledger once; both searches share it.
|
|
326
|
+
// Without a provider, or when it fails, the memory search degrades to FTS
|
|
327
|
+
// and the attachments are skipped: the delta still carries the line and the
|
|
328
|
+
// persona.
|
|
329
|
+
const queryEmbedding = await embedOnce(db, decision.query, input);
|
|
330
|
+
|
|
331
|
+
const delivered = deliveredMemoryIds(db, input.sessionId);
|
|
332
|
+
const memories = await searchMemoriesWithStatus(db, {
|
|
333
|
+
query: decision.query,
|
|
334
|
+
// Read past what the transcript already holds, so a delta still fills.
|
|
335
|
+
limit: Math.min(DOORMAN_MEMORY_LIMIT + delivered.size, DOORMAN_SEARCH_CEILING),
|
|
336
|
+
provider: input.embeddingProvider,
|
|
337
|
+
...(queryEmbedding ? { queryEmbedding } : {}),
|
|
338
|
+
// The doorman searches on the system's behalf N times a session: a turn's
|
|
339
|
+
// injection must not raise a memory's reinforcement for the next turn
|
|
340
|
+
// (AI-12). Reinforcement stays with the explicit load.
|
|
341
|
+
logAccess: false,
|
|
342
|
+
recordEmbeddingLedger: false,
|
|
343
|
+
now: input.nowIso(),
|
|
344
|
+
});
|
|
345
|
+
input.onMemoryScores?.(
|
|
346
|
+
memories.results.map((result) => ({ score: result.score, semantic: result.semantic })),
|
|
347
|
+
);
|
|
348
|
+
const relevant = (
|
|
349
|
+
memories.degraded
|
|
350
|
+
? memories.results
|
|
351
|
+
: memories.results.filter((result) => result.semantic >= DOORMAN_MEMORY_SEMANTIC_THRESHOLD)
|
|
352
|
+
)
|
|
353
|
+
.filter((result) => !delivered.has(result.id))
|
|
354
|
+
.slice(0, DOORMAN_MEMORY_LIMIT);
|
|
355
|
+
if (relevant.length > 0) {
|
|
356
|
+
rememberDelivered(
|
|
357
|
+
db,
|
|
358
|
+
input.sessionId,
|
|
359
|
+
[...delivered, ...relevant.map((result) => result.id)],
|
|
360
|
+
input.nowIso(),
|
|
361
|
+
);
|
|
362
|
+
const rows = memoriesById(
|
|
363
|
+
db,
|
|
364
|
+
relevant.map((result) => result.id),
|
|
365
|
+
);
|
|
366
|
+
const block = ["=== relevant memories ==="];
|
|
367
|
+
for (const result of relevant) {
|
|
368
|
+
const memory = rows.get(result.id);
|
|
369
|
+
if (!memory) continue;
|
|
370
|
+
block.push(`\n[${memory.layer}] ${memory.title}\n${memory.content}`);
|
|
371
|
+
}
|
|
372
|
+
parts.push(block.join("\n"));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (queryEmbedding) {
|
|
376
|
+
const attachments = await relevantAttachments(
|
|
377
|
+
db,
|
|
378
|
+
decision.query,
|
|
379
|
+
journey,
|
|
380
|
+
undefined,
|
|
381
|
+
undefined,
|
|
382
|
+
queryEmbedding,
|
|
383
|
+
);
|
|
384
|
+
if (attachments.length > 0) {
|
|
385
|
+
const block = ["=== relevant attachments ==="];
|
|
386
|
+
for (const { row, score } of attachments) {
|
|
387
|
+
const source = journey ? "" : ` [${row.journeyId}]`;
|
|
388
|
+
block.push(`--- ${row.name}${source} (score: ${score.toFixed(3)}) ---`);
|
|
389
|
+
if (row.description) block.push(`Description: ${row.description}`);
|
|
390
|
+
block.push(row.content);
|
|
391
|
+
}
|
|
392
|
+
parts.push(block.join("\n"));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { stdout: `${parts.join("\n\n")}\n`, stderr: "" };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function embedOnce(
|
|
400
|
+
db: WritableDatabase,
|
|
401
|
+
query: string,
|
|
402
|
+
input: RunDoormanInput,
|
|
403
|
+
): Promise<readonly number[] | null> {
|
|
404
|
+
if (!input.embeddingProvider) return null;
|
|
405
|
+
try {
|
|
406
|
+
return await generateEmbeddingSafely(input.embeddingProvider, query, {
|
|
407
|
+
onAttempt: embeddingLedgerHook(db, {
|
|
408
|
+
sessionId: input.sessionId,
|
|
409
|
+
now: input.nowIso,
|
|
410
|
+
}),
|
|
411
|
+
});
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The persona routing table the decision scores against; read once per turn. */
|
|
418
|
+
export function doormanPersonas(db: Database): PersonaRoutingRow[] {
|
|
419
|
+
return personaRoutingRows(db);
|
|
420
|
+
}
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
// $mirror-mind:mm:mirror … the Codex plugin (TS2 L10)
|
|
14
14
|
// /mm-mirror … Grok (TS4 L5)
|
|
15
15
|
// $mm-mirror … Codex's own skill form
|
|
16
|
+
// /skill:mm-mirror … Pi's command, which its menu sends (CR160)
|
|
16
17
|
//
|
|
17
18
|
// Three choices, each made by the runtime, not the person: the sigil (`/` or
|
|
18
19
|
// `$`), whether the plugin's name prefixes the skill, and whether the skill's
|
|
19
20
|
// `name: "mm:mirror"` or its directory `mm-mirror` is used. The grammar is
|
|
20
21
|
// their product, so a runtime that combines them differently is covered too.
|
|
22
|
+
// Pi is the exception: it registers every skill as the command `skill:<name>`
|
|
23
|
+
// (CR159 measured it on Pi 1.4.0), so its one form is spelled out whole.
|
|
21
24
|
//
|
|
22
25
|
// The form must open the prompt and end at whitespace or the end of it: a
|
|
23
26
|
// longer skill name (`/mm-mirrors`) is another skill, and a form quoted in the
|
|
@@ -32,7 +35,7 @@ export interface MirrorForm {
|
|
|
32
35
|
readonly query: string;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
const FORM = /^([/$]
|
|
38
|
+
const FORM = /^(?:\/skill:mm-mirror|[/$](?:mirror-mind:)?mm[:-]mirror)(?=\s|$)/;
|
|
36
39
|
|
|
37
40
|
/** The explicit Mirror Mode form at the start of a prompt, or null. */
|
|
38
41
|
export function mirrorForm(prompt: string): MirrorForm | null {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
decodeMetadata,
|
|
24
24
|
GLOBAL_STICKY_DEFAULTS_SESSION_ID,
|
|
25
25
|
getRuntimeSession,
|
|
26
|
+
resolveNamedRuntimeSessionId,
|
|
26
27
|
resolveRuntimeSessionId,
|
|
27
28
|
upsertRuntimeSession,
|
|
28
29
|
} from "./runtimeSession.ts";
|
|
@@ -120,13 +121,33 @@ export async function runMirrorLoad(
|
|
|
120
121
|
});
|
|
121
122
|
persistStickyDefaults(db, resolved.persona, resolved.journey, input.nowIso());
|
|
122
123
|
activateOperatingMode(db, { mode: "Mirror Mode", journey: resolved.journey }, input.nowIso());
|
|
124
|
+
// The NAMED session -- explicit, else the one the harness put in the
|
|
125
|
+
// environment -- never a guess (CV9.DS8.TS2, closing TS1's measured gap:
|
|
126
|
+
// the skill's own load passed the environment id into the route and this
|
|
127
|
+
// write ignored it, so a self-started Mirror Mode left `mirror_active`
|
|
128
|
+
// unwritten and the doorman had nothing to learn from).
|
|
129
|
+
const namedSessionId = resolveNamedRuntimeSessionId(
|
|
130
|
+
input.sessionId ?? null,
|
|
131
|
+
input.environmentSessionId ?? null,
|
|
132
|
+
);
|
|
133
|
+
if (namedSessionId) {
|
|
134
|
+
// The session's own mode row as well as the global one (CV9.DS8.US4): the
|
|
135
|
+
// doorman reads the session's row first, and `build load` stamps that same
|
|
136
|
+
// row Builder -- without this write a Mirror re-entry in the same session
|
|
137
|
+
// would read as Builder and the deltas would never resume.
|
|
138
|
+
activateOperatingMode(
|
|
139
|
+
db,
|
|
140
|
+
{ mode: "Mirror Mode", journey: resolved.journey, sessionId: namedSessionId },
|
|
141
|
+
input.nowIso(),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
123
144
|
writeMirrorState(
|
|
124
145
|
db,
|
|
125
146
|
{
|
|
126
147
|
active: true,
|
|
127
148
|
persona: resolved.persona,
|
|
128
149
|
journey: resolved.journey,
|
|
129
|
-
sessionId:
|
|
150
|
+
sessionId: namedSessionId,
|
|
130
151
|
},
|
|
131
152
|
input.nowIso(),
|
|
132
153
|
);
|
|
@@ -246,7 +267,6 @@ export function persistStickyDefaults(
|
|
|
246
267
|
mirrorActive: false,
|
|
247
268
|
...(persona !== null ? { persona } : {}),
|
|
248
269
|
...(journey !== null ? { journey } : {}),
|
|
249
|
-
hookInjected: true,
|
|
250
270
|
active: false,
|
|
251
271
|
},
|
|
252
272
|
nowIso,
|
|
@@ -271,7 +291,6 @@ function writeMirrorState(
|
|
|
271
291
|
mirrorActive: input.active,
|
|
272
292
|
...(input.persona !== undefined && input.persona !== null ? { persona: input.persona } : {}),
|
|
273
293
|
...(input.journey !== undefined && input.journey !== null ? { journey: input.journey } : {}),
|
|
274
|
-
hookInjected: !input.active,
|
|
275
294
|
},
|
|
276
295
|
nowIso,
|
|
277
296
|
);
|
package/ts/src/mirror/render.ts
CHANGED
|
@@ -16,10 +16,16 @@ export function renderMirrorModeTransition(input: {
|
|
|
16
16
|
return box("◌ MIRROR MODE ACTIVE", rows);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The stderr banner `mirror mirror load` prints. An active persona is signed
|
|
21
|
+
* `◇ <persona>`, the one rule `AGENTS.md` states (DS8 decision 2) -- on Pi the
|
|
22
|
+
* model reads stderr inside the tool result, so the banner is also an example
|
|
23
|
+
* of the signature it is asked to write.
|
|
24
|
+
*/
|
|
19
25
|
export function renderMirrorBanner(persona: string | null): string {
|
|
20
26
|
const first = "\u001b[38;5;183m⏺ Mirror Mode active\u001b[0m\n";
|
|
21
27
|
return persona
|
|
22
|
-
? `${first}\u001b[38;5;183m
|
|
28
|
+
? `${first}\u001b[38;5;183m ◇ ${persona}\u001b[0m\n`
|
|
23
29
|
: `${first}\u001b[38;5;183m Ego responding without persona\u001b[0m\n`;
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -10,7 +10,6 @@ export interface RuntimeSessionRow {
|
|
|
10
10
|
mirrorActive: boolean;
|
|
11
11
|
persona: string | null;
|
|
12
12
|
journey: string | null;
|
|
13
|
-
hookInjected: boolean;
|
|
14
13
|
active: boolean;
|
|
15
14
|
startedAt: string;
|
|
16
15
|
updatedAt: string;
|
|
@@ -24,7 +23,6 @@ export interface RuntimeSessionPatch {
|
|
|
24
23
|
mirrorActive?: boolean;
|
|
25
24
|
persona?: string | null;
|
|
26
25
|
journey?: string | null;
|
|
27
|
-
hookInjected?: boolean;
|
|
28
26
|
active?: boolean;
|
|
29
27
|
closedAt?: string | null;
|
|
30
28
|
metadata?: string | null;
|
|
@@ -40,7 +38,6 @@ export function getRuntimeSession(db: Database, sessionId: string): RuntimeSessi
|
|
|
40
38
|
mirrorActive: Boolean(row.mirror_active),
|
|
41
39
|
persona: nullableString(row.persona),
|
|
42
40
|
journey: nullableString(row.journey),
|
|
43
|
-
hookInjected: Boolean(row.hook_injected),
|
|
44
41
|
active: Boolean(row.active),
|
|
45
42
|
startedAt: String(row.started_at),
|
|
46
43
|
updatedAt: String(row.updated_at),
|
|
@@ -67,7 +64,6 @@ export function upsertRuntimeSession(
|
|
|
67
64
|
mirrorActive: value("mirrorActive", existing?.mirrorActive ?? false) as boolean,
|
|
68
65
|
persona: value("persona", existing?.persona ?? null) as string | null,
|
|
69
66
|
journey: value("journey", existing?.journey ?? null) as string | null,
|
|
70
|
-
hookInjected: value("hookInjected", existing?.hookInjected ?? false) as boolean,
|
|
71
67
|
active: value("active", existing?.active ?? true) as boolean,
|
|
72
68
|
startedAt: existing?.startedAt ?? nowIso,
|
|
73
69
|
updatedAt: nowIso,
|
|
@@ -77,15 +73,14 @@ export function upsertRuntimeSession(
|
|
|
77
73
|
db.prepare(
|
|
78
74
|
`INSERT INTO runtime_sessions
|
|
79
75
|
(session_id, conversation_id, interface, mirror_active, persona, journey,
|
|
80
|
-
|
|
81
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
76
|
+
active, started_at, updated_at, closed_at, metadata)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
78
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
83
79
|
conversation_id = excluded.conversation_id,
|
|
84
80
|
interface = excluded.interface,
|
|
85
81
|
mirror_active = excluded.mirror_active,
|
|
86
82
|
persona = excluded.persona,
|
|
87
83
|
journey = excluded.journey,
|
|
88
|
-
hook_injected = excluded.hook_injected,
|
|
89
84
|
active = excluded.active,
|
|
90
85
|
updated_at = excluded.updated_at,
|
|
91
86
|
closed_at = excluded.closed_at,
|
|
@@ -97,7 +92,6 @@ export function upsertRuntimeSession(
|
|
|
97
92
|
Number(row.mirrorActive),
|
|
98
93
|
row.persona,
|
|
99
94
|
row.journey,
|
|
100
|
-
Number(row.hookInjected),
|
|
101
95
|
Number(row.active),
|
|
102
96
|
row.startedAt,
|
|
103
97
|
row.updatedAt,
|
|
@@ -136,6 +130,70 @@ export function compareAndSwapRuntimeSessionMetadata(
|
|
|
136
130
|
return Number(result.changes) === 1;
|
|
137
131
|
}
|
|
138
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Columns the doorman moves one at a time (CV9.DS8.TS2).
|
|
135
|
+
*
|
|
136
|
+
* `upsertRuntimeSession` reads the row, rebuilds it, and writes every column
|
|
137
|
+
* back, with no transaction around the read and the write. Two writers on the
|
|
138
|
+
* same turn -- the prompt logger setting `conversation_id`/`active`, the doorman
|
|
139
|
+
* setting `persona` -- would each overwrite the other's column with the stale
|
|
140
|
+
* copy it read. A single-column UPDATE touches nothing it was not asked to.
|
|
141
|
+
*/
|
|
142
|
+
export interface RuntimeSessionColumnPatch {
|
|
143
|
+
persona?: string | null;
|
|
144
|
+
/** The whole metadata JSON, as the caller re-dumped it after its own read. */
|
|
145
|
+
metadata?: string | null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Set only the named columns on an existing row; false when there is no row. */
|
|
149
|
+
export function updateRuntimeSessionColumns(
|
|
150
|
+
db: WritableDatabase,
|
|
151
|
+
sessionId: string,
|
|
152
|
+
patch: RuntimeSessionColumnPatch,
|
|
153
|
+
nowIso: string,
|
|
154
|
+
): boolean {
|
|
155
|
+
const assignments: string[] = [];
|
|
156
|
+
const params: (string | number | null)[] = [];
|
|
157
|
+
if (patch.persona !== undefined) {
|
|
158
|
+
assignments.push("persona = ?");
|
|
159
|
+
params.push(patch.persona);
|
|
160
|
+
}
|
|
161
|
+
if (patch.metadata !== undefined) {
|
|
162
|
+
assignments.push("metadata = ?");
|
|
163
|
+
params.push(patch.metadata);
|
|
164
|
+
}
|
|
165
|
+
if (assignments.length === 0) return false;
|
|
166
|
+
assignments.push("updated_at = ?");
|
|
167
|
+
params.push(nowIso, sessionId);
|
|
168
|
+
const result = db
|
|
169
|
+
.prepare(`UPDATE runtime_sessions SET ${assignments.join(", ")} WHERE session_id = ?`)
|
|
170
|
+
.run(...params);
|
|
171
|
+
return Number(result.changes) === 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The session the harness named in the environment (CV9.DS8.TS1, measured
|
|
176
|
+
* 2026-10-24): `MIRROR_SESSION_ID`, which the Pi extension sets at
|
|
177
|
+
* `session_start`; else `CLAUDE_CODE_SESSION_ID` (Claude Code 2.1.310) or
|
|
178
|
+
* `CODEX_SESSION_ID` (Codex 0.164.0), both undocumented, both equal to the
|
|
179
|
+
* session row the runtime's hooks write, both present in the model's own shell
|
|
180
|
+
* -- so a `mirror mirror load` the model runs by itself names the same session
|
|
181
|
+
* the hooks do. One reader, as `runtimeDetection` reads Grok's variables.
|
|
182
|
+
*
|
|
183
|
+
* Read by the mirror load route and the doorman only (TS2): widening it to the
|
|
184
|
+
* Builder, Soul, and Explorer routes would bind those sessions on Claude Code
|
|
185
|
+
* and Codex too, a CR008 question a later story may open.
|
|
186
|
+
*/
|
|
187
|
+
export function environmentSessionId(
|
|
188
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
189
|
+
): string | null {
|
|
190
|
+
for (const name of ["MIRROR_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_SESSION_ID"]) {
|
|
191
|
+
const value = env[name]?.trim();
|
|
192
|
+
if (value) return value;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
139
197
|
/**
|
|
140
198
|
* The session a caller was TOLD about: `--session-id`, else `MIRROR_SESSION_ID`.
|
|
141
199
|
* `null` means nobody named one. It never guesses, so it is the only resolver a
|