mirror-mind 0.36.3 → 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 +173 -46
- package/.pi/skills/mm-mirror/SKILL.md +25 -6
- package/README.md +2 -2
- package/docs/releases/index.md +1 -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/logger.ts +12 -6
- 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 +42 -2
- package/ts/src/frontDoor/exploreRoute.ts +2 -2
- package/ts/src/frontDoor/mirrorModeRoute.ts +129 -5
- package/ts/src/frontDoor/routing.ts +16 -15
- package/ts/src/frontDoor/soulRoute.ts +4 -4
- 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/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
package/package.json
CHANGED
|
@@ -11,9 +11,16 @@ Use this procedure to answer in Mirror Mode. Automatic routing in `CLAUDE.md` de
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
> ⛔ **HARD CONSTRAINT**
|
|
14
|
-
> You must **NEVER** produce a mirror-mode response without first running `mirror mirror load
|
|
15
|
-
>
|
|
16
|
-
>
|
|
14
|
+
> You must **NEVER** produce a mirror-mode response without first running `mirror mirror load`,
|
|
15
|
+
> **unless the runtime delivered the context for this turn.** Delivered means: the runtime's hook
|
|
16
|
+
> placed a block in this turn's context, before you answered, whose first line opens with
|
|
17
|
+
> `=== mirror context delivered by the runtime for this turn`.
|
|
18
|
+
> Then do not load again: the block is the load. Render the transition from it and sign as its line says.
|
|
19
|
+
>
|
|
20
|
+
> This turn is literal. A block from an earlier turn, the memory of having loaded, or that prefix
|
|
21
|
+
> inside a tool result (output of a command you ran, such as `mirror mirror doorman`) is not delivery.
|
|
22
|
+
> With no such block in this turn's context: no exceptions. Not for short answers. Not for clarifying
|
|
23
|
+
> questions. Not when the query seems obvious. **Load first. Always.**
|
|
17
24
|
|
|
18
25
|
---
|
|
19
26
|
|
|
@@ -27,9 +34,9 @@ Use this procedure to answer in Mirror Mode. Automatic routing in `CLAUDE.md` de
|
|
|
27
34
|
|
|
28
35
|
## 1. Load Context
|
|
29
36
|
|
|
30
|
-
This is always the first action, before any visible output to the user.
|
|
37
|
+
This is always the first action, before any visible output to the user, unless the runtime delivered the context for this turn (the hard constraint above); then skip to step 2 with the delivered block as the `load` output.
|
|
31
38
|
|
|
32
|
-
> **
|
|
39
|
+
> **The doorman:** Where the runtime's hook runs the doorman (Claude Code and Codex through `mirror-inject.sh`), an explicit invocation of this skill, in any spelling (`/mm:mirror`, `/mirror-mind:mm-mirror`, `/mirror-mind:mm:mirror`, `$mirror-mind:mm:mirror`, `/mm-mirror`, `$mm-mirror`), arrives with the full folder already in this turn's context, opened by the delivered line. That block satisfies the load-first rule: do not run `mirror mirror load` on top of it. Where no block arrived, load. Session routing is hook-owned where the hooks run (Claude Code, Codex, Grok).
|
|
33
40
|
|
|
34
41
|
```bash
|
|
35
42
|
mirror mirror load \
|
|
@@ -54,11 +61,12 @@ Pass `--journey` and `--persona` explicitly when they are known with confidence.
|
|
|
54
61
|
|
|
55
62
|
## 2. Answer
|
|
56
63
|
|
|
57
|
-
Use the `load` output as the complete response context.
|
|
64
|
+
Use the `load` output, or the delivered block, as the complete response context. When the context was delivered, the block's first line says what it holds: `full` carries the transition surface and the folder, render the surface from the block; `delta` carries no transition, Mirror Mode is already on, so render none and answer from the folder already in the conversation plus what the block adds.
|
|
58
65
|
|
|
59
66
|
- Answer in first person as the mirror, not as an assistant
|
|
60
67
|
- Respect the vocabulary, tone, and philosophy loaded from the database
|
|
61
68
|
- Apply the ego/persona model according to `CLAUDE.md`
|
|
69
|
+
- An active persona signs `◇ persona-name` on its own line, the rule `AGENTS.md` states; the delivered line names the persona to sign. No signature when the ego answers alone.
|
|
62
70
|
|
|
63
71
|
## 3. Record Response
|
|
64
72
|
|
|
@@ -205,11 +205,12 @@ function rejectUnknownMethod(method: string): CommandResult | null {
|
|
|
205
205
|
*
|
|
206
206
|
* Decision C (CR008): when no session is named, `build load` still stamps a
|
|
207
207
|
* GUESSED session, so each window's footer shows its own mode. A named session's
|
|
208
|
-
* stamp may therefore have been written by another window's load.
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* only
|
|
212
|
-
*
|
|
208
|
+
* stamp may therefore have been written by another window's load. Since
|
|
209
|
+
* CV9.DS8.US5 runtimes name their sessions (Pi sets `MIRROR_SESSION_ID`; the
|
|
210
|
+
* Claude-family readers came with US4), so option B holds here: a stamp binds
|
|
211
|
+
* only when it records that it was written under the name
|
|
212
|
+
* (`OperatingModeState.namedSession`). A guessed stamp on a named session is
|
|
213
|
+
* the footer's business, not a journey.
|
|
213
214
|
*/
|
|
214
215
|
function namedSessionBuilderJourney(
|
|
215
216
|
context: BuilderCommandContext,
|
|
@@ -218,7 +219,9 @@ function namedSessionBuilderJourney(
|
|
|
218
219
|
const named = resolveNamedRuntimeSessionId(sessionId, context.environmentSessionId ?? null);
|
|
219
220
|
if (!named || !getRuntimeSession(context.db, named)?.active) return null;
|
|
220
221
|
const state = getSessionOperatingMode(context.db, named);
|
|
221
|
-
return state?.mode === "Builder Mode" && state.journey
|
|
222
|
+
return state?.mode === "Builder Mode" && state.journey && state.namedSession === true
|
|
223
|
+
? state.journey
|
|
224
|
+
: null;
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
/**
|
package/ts/src/builder/load.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { getIdentityContent } from "#identity/identityRead.ts";
|
|
|
33
33
|
import { getProjectPath } from "#journey/journeyStatus.ts";
|
|
34
34
|
import { loadMirrorContext } from "#mirror/context.ts";
|
|
35
35
|
import { persistStickyDefaults } from "#mirror/orchestration.ts";
|
|
36
|
-
import { resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
36
|
+
import { resolveNamedRuntimeSessionId, resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
37
37
|
import { activateOperatingMode } from "#mode/operatingMode.ts";
|
|
38
38
|
import { ProviderConfigError } from "#providers/config.ts";
|
|
39
39
|
import type { EmbeddingProvider } from "#providers/embedding.ts";
|
|
@@ -286,6 +286,13 @@ export async function runBuildLoad(
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
persistStickyDefaults(db, "engineer", slug, deps.nowIso());
|
|
289
|
+
// A named session's stamp records that it was (CR008 option B, CV9.DS8.US5):
|
|
290
|
+
// with no name, the guess still lands on some window's row so its footer
|
|
291
|
+
// shows the mode, and a lifecycle command must not bind from that.
|
|
292
|
+
const namedSessionId = resolveNamedRuntimeSessionId(
|
|
293
|
+
options.sessionId ?? null,
|
|
294
|
+
options.environmentSessionId ?? null,
|
|
295
|
+
);
|
|
289
296
|
const resolvedSessionId = resolveRuntimeSessionId(
|
|
290
297
|
db,
|
|
291
298
|
options.sessionId ?? null,
|
|
@@ -293,7 +300,12 @@ export async function runBuildLoad(
|
|
|
293
300
|
);
|
|
294
301
|
activateOperatingMode(
|
|
295
302
|
db,
|
|
296
|
-
{
|
|
303
|
+
{
|
|
304
|
+
mode: "Builder Mode",
|
|
305
|
+
journey: slug,
|
|
306
|
+
sessionId: resolvedSessionId,
|
|
307
|
+
named: namedSessionId !== null && namedSessionId === resolvedSessionId,
|
|
308
|
+
},
|
|
297
309
|
deps.nowIso(),
|
|
298
310
|
);
|
|
299
311
|
await deps.switchConversation?.(slug, resolvedSessionId);
|
|
@@ -173,18 +173,24 @@ export function getOrCreateSessionConversation(
|
|
|
173
173
|
options.persona ?? null,
|
|
174
174
|
options.journey ?? null,
|
|
175
175
|
);
|
|
176
|
-
// Mirrors the service's conflict clause
|
|
177
|
-
//
|
|
176
|
+
// Mirrors the service's conflict clause: it does NOT touch mirror_active,
|
|
177
|
+
// started_at, or metadata on update. Since CV9.DS8.US5 it
|
|
178
|
+
// does not erase a persona or journey either when the caller names none:
|
|
179
|
+
// on the first prompt of a session the doorman has already routed the
|
|
180
|
+
// explicit turn and written both to this row, and the prompt logger, which
|
|
181
|
+
// runs after it, wrote NULL over them -- the mirror lost its voice until a
|
|
182
|
+
// later prompt scored a persona again. A caller that names a persona or
|
|
183
|
+
// journey still sets it.
|
|
178
184
|
db.prepare(
|
|
179
185
|
`INSERT INTO runtime_sessions
|
|
180
186
|
(session_id, conversation_id, interface, mirror_active, persona, journey,
|
|
181
|
-
|
|
182
|
-
VALUES (?, ?, ?, 0, ?, ?,
|
|
187
|
+
active, started_at, updated_at, closed_at, metadata)
|
|
188
|
+
VALUES (?, ?, ?, 0, ?, ?, 1, ?, ?, NULL, NULL)
|
|
183
189
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
184
190
|
conversation_id = excluded.conversation_id,
|
|
185
191
|
interface = excluded.interface,
|
|
186
|
-
persona = excluded.persona,
|
|
187
|
-
journey = excluded.journey,
|
|
192
|
+
persona = COALESCE(excluded.persona, runtime_sessions.persona),
|
|
193
|
+
journey = COALESCE(excluded.journey, runtime_sessions.journey),
|
|
188
194
|
active = 1,
|
|
189
195
|
closed_at = NULL,
|
|
190
196
|
updated_at = excluded.updated_at`,
|
|
@@ -132,7 +132,7 @@ function appliedMigrationIds(dbPath: string, options: OpenOptions): Set<string>
|
|
|
132
132
|
* No split any more. Until CV22.DS10.TS5 this separated the TS-authored tail
|
|
133
133
|
* from Python-authored ids and declined the whole thing whenever a Python one
|
|
134
134
|
* was pending -- correct while Python could still apply them. TypeScript
|
|
135
|
-
* carries all
|
|
135
|
+
* carries all of them and is the only engine left, so every pending known
|
|
136
136
|
* migration is simply pending.
|
|
137
137
|
*/
|
|
138
138
|
function pendingMigrations(applied: Set<string>): string[] {
|
package/ts/src/db/migrations.ts
CHANGED
|
@@ -514,6 +514,21 @@ function migrateJourneyParentColumn(db: WritableDatabase): void {
|
|
|
514
514
|
);
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
/**
|
|
518
|
+
* 018 (CV9.DS8.US5) -- `runtime_sessions.hook_injected` goes. It was the
|
|
519
|
+
* Claude-family inject hook's once-per-session flag (CV21.DS2); US4 handed the
|
|
520
|
+
* hook to the one doorman and retired its last reader, and the four writers
|
|
521
|
+
* that still filled it were carried as one debt item, paid here with the
|
|
522
|
+
* column. Guarded on the column's presence: a fresh database's `createSchema`
|
|
523
|
+
* no longer has it, and a re-run finds nothing to drop. `DROP COLUMN` is
|
|
524
|
+
* SQLite 3.35; the engine ships 3.54.
|
|
525
|
+
*/
|
|
526
|
+
function migrateDropHookInjected(db: WritableDatabase): void {
|
|
527
|
+
if (!tableExists(db, "runtime_sessions")) return;
|
|
528
|
+
if (!tableColumns(db, "runtime_sessions").has("hook_injected")) return;
|
|
529
|
+
db.exec("ALTER TABLE runtime_sessions DROP COLUMN hook_injected");
|
|
530
|
+
}
|
|
531
|
+
|
|
517
532
|
export type MigrationApply = (db: WritableDatabase) => void;
|
|
518
533
|
|
|
519
534
|
export const MIGRATIONS: readonly (readonly [string, MigrationApply])[] = [
|
|
@@ -534,6 +549,7 @@ export const MIGRATIONS: readonly (readonly [string, MigrationApply])[] = [
|
|
|
534
549
|
["015_create_builder_workbench", migrateCreateBuilderWorkbench],
|
|
535
550
|
["016_builder_workbench_display_codes", migrateBuilderWorkbenchDisplayCodes],
|
|
536
551
|
["017_journey_parent_column", migrateJourneyParentColumn],
|
|
552
|
+
["018_drop_hook_injected", migrateDropHookInjected],
|
|
537
553
|
];
|
|
538
554
|
|
|
539
555
|
/**
|
package/ts/src/db/schema.ts
CHANGED
|
@@ -84,7 +84,6 @@ CREATE TABLE IF NOT EXISTS runtime_sessions (
|
|
|
84
84
|
mirror_active INTEGER NOT NULL DEFAULT 0,
|
|
85
85
|
persona TEXT,
|
|
86
86
|
journey TEXT,
|
|
87
|
-
hook_injected INTEGER NOT NULL DEFAULT 0,
|
|
88
87
|
active INTEGER NOT NULL DEFAULT 1,
|
|
89
88
|
started_at TEXT NOT NULL,
|
|
90
89
|
updated_at TEXT NOT NULL,
|
|
@@ -1925,13 +1925,6 @@ export const SCHEMA_INVENTORY_SNAPSHOT: SchemaInventory = {
|
|
|
1925
1925
|
"pk": 0,
|
|
1926
1926
|
"type": "TEXT"
|
|
1927
1927
|
},
|
|
1928
|
-
{
|
|
1929
|
-
"dflt_value": "0",
|
|
1930
|
-
"name": "hook_injected",
|
|
1931
|
-
"notnull": 1,
|
|
1932
|
-
"pk": 0,
|
|
1933
|
-
"type": "INTEGER"
|
|
1934
|
-
},
|
|
1935
1928
|
{
|
|
1936
1929
|
"dflt_value": "1",
|
|
1937
1930
|
"name": "active",
|
|
@@ -1979,7 +1972,7 @@ export const SCHEMA_INVENTORY_SNAPSHOT: SchemaInventory = {
|
|
|
1979
1972
|
}
|
|
1980
1973
|
],
|
|
1981
1974
|
"sql":
|
|
1982
|
-
"CREATE TABLE runtime_sessions ( session_id TEXT PRIMARY KEY, conversation_id TEXT REFERENCES conversations(id), interface TEXT, mirror_active INTEGER NOT NULL DEFAULT 0, persona TEXT, journey TEXT,
|
|
1975
|
+
"CREATE TABLE runtime_sessions ( session_id TEXT PRIMARY KEY, conversation_id TEXT REFERENCES conversations(id), interface TEXT, mirror_active INTEGER NOT NULL DEFAULT 0, persona TEXT, journey TEXT, active INTEGER NOT NULL DEFAULT 1, started_at TEXT NOT NULL, updated_at TEXT NOT NULL, closed_at TEXT, metadata TEXT)"
|
|
1983
1976
|
},
|
|
1984
1977
|
"tasks": {
|
|
1985
1978
|
"columns": [
|
package/ts/src/db/schemaState.ts
CHANGED
|
@@ -47,6 +47,8 @@ export const KNOWN_MIGRATION_IDS: readonly string[] = [
|
|
|
47
47
|
"016_builder_workbench_display_codes",
|
|
48
48
|
// TS-authored, no Python counterpart (CV22.DS6.US2 — first TS ⊋ Python migration).
|
|
49
49
|
"017_journey_parent_column",
|
|
50
|
+
// CV9.DS8.US5: the inject hook's once-per-session flag, without a reader since US4.
|
|
51
|
+
"018_drop_hook_injected",
|
|
50
52
|
];
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -17,6 +17,15 @@ const TS_ONLY_COLUMNS: Record<string, ColumnInventory[]> = {
|
|
|
17
17
|
identity: [{ name: "parent_journey", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 }],
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Columns the Python snapshot has that TS has dropped (CV9.DS8.US5, migration
|
|
22
|
+
* 018): `runtime_sessions.hook_injected`, the inject hook's once-per-session
|
|
23
|
+
* flag, without a reader since CV9.DS8.US4.
|
|
24
|
+
*/
|
|
25
|
+
const TS_REMOVED_COLUMNS: Record<string, string[]> = {
|
|
26
|
+
runtime_sessions: ["hook_injected"],
|
|
27
|
+
};
|
|
28
|
+
|
|
20
29
|
/** Indexes TS creates that the Python snapshot does not have. */
|
|
21
30
|
const TS_ONLY_INDEXES: Record<string, { table: string; unique: number; columns: string[] }> = {
|
|
22
31
|
idx_identity_parent_journey: { table: "identity", unique: 0, columns: ["parent_journey"] },
|
|
@@ -46,21 +55,33 @@ export function diffTsInventoryAgainstSnapshot(
|
|
|
46
55
|
const actualTable = actual.tables[name];
|
|
47
56
|
if (!actualTable) continue;
|
|
48
57
|
const additions = TS_ONLY_COLUMNS[name] ?? [];
|
|
49
|
-
const
|
|
58
|
+
const removals = TS_REMOVED_COLUMNS[name] ?? [];
|
|
59
|
+
const expectedColumns = [
|
|
60
|
+
...snapshot.tables[name].columns.filter((column) => !removals.includes(column.name)),
|
|
61
|
+
...additions,
|
|
62
|
+
];
|
|
50
63
|
if (!isDeepStrictEqual(actualTable.columns, expectedColumns)) {
|
|
51
|
-
problems.push(
|
|
64
|
+
problems.push(
|
|
65
|
+
`tables[${name}].columns differ from snapshot + enumerated TS-only additions and removals`,
|
|
66
|
+
);
|
|
52
67
|
}
|
|
53
68
|
if (!isDeepStrictEqual(actualTable.foreign_keys, snapshot.tables[name].foreign_keys)) {
|
|
54
69
|
problems.push(`tables[${name}].foreign_keys differ from snapshot`);
|
|
55
70
|
}
|
|
56
|
-
if (additions.length > 0) {
|
|
71
|
+
if (additions.length > 0 || removals.length > 0) {
|
|
57
72
|
// The raw CREATE TABLE text diverges by exactly these columns; assert each
|
|
58
|
-
// added column name is present
|
|
73
|
+
// added column name is present and each removed one absent, rather than
|
|
74
|
+
// reproduce SQLite's stored text.
|
|
59
75
|
for (const column of additions) {
|
|
60
76
|
if (!(actualTable.sql ?? "").includes(column.name)) {
|
|
61
77
|
problems.push(`tables[${name}].sql is missing TS-only column ${column.name}`);
|
|
62
78
|
}
|
|
63
79
|
}
|
|
80
|
+
for (const column of removals) {
|
|
81
|
+
if ((actualTable.sql ?? "").includes(column)) {
|
|
82
|
+
problems.push(`tables[${name}].sql still carries the dropped column ${column}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
64
85
|
} else if (!isDeepStrictEqual(actualTable.sql, snapshot.tables[name].sql)) {
|
|
65
86
|
problems.push(`tables[${name}].sql differs from snapshot`);
|
|
66
87
|
}
|
package/ts/src/frontDoor/cli.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { setIdentity } from "#identity/setIdentity.ts";
|
|
|
52
52
|
import { IdentityRootExistsError, initUserHome, TemplatesNotFoundError } from "#init/init.ts";
|
|
53
53
|
import { JOURNEY_PATH_LAYER } from "#journey/journeyStatus.ts";
|
|
54
54
|
import { JourneyNotFoundError } from "#journey/journeyWrite.ts";
|
|
55
|
+
import { environmentSessionId } from "#mirror/runtimeSession.ts";
|
|
55
56
|
import {
|
|
56
57
|
CallOutcomeTally,
|
|
57
58
|
formatCallOutcome,
|
|
@@ -116,9 +117,11 @@ import {
|
|
|
116
117
|
import { applyJourneySetPath } from "./journeyWriteRoute.ts";
|
|
117
118
|
import { openLiveWriteDatabase } from "./liveBackup.ts";
|
|
118
119
|
import {
|
|
120
|
+
isMirrorDoorman,
|
|
119
121
|
isMirrorWrite,
|
|
120
122
|
isModeWrite,
|
|
121
123
|
mirrorDeactivateMissingSession,
|
|
124
|
+
runMirrorDoormanRoute,
|
|
122
125
|
runMirrorRead,
|
|
123
126
|
runMirrorWriteRoute,
|
|
124
127
|
runModeRead,
|
|
@@ -1477,7 +1480,10 @@ function runShadowWrite(argv: readonly string[]): number | Promise<number> {
|
|
|
1477
1480
|
return runShadowScanWrite(argv); // "scan"
|
|
1478
1481
|
}
|
|
1479
1482
|
|
|
1480
|
-
function withMirrorReadDb
|
|
1483
|
+
function withMirrorReadDb<T = number>(
|
|
1484
|
+
argv: readonly string[],
|
|
1485
|
+
read: (db: Database) => T,
|
|
1486
|
+
): T | number {
|
|
1481
1487
|
const dbPath = resolveDbPathForCli(argv.slice(1));
|
|
1482
1488
|
if (dbPath === null) return 2;
|
|
1483
1489
|
ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
|
|
@@ -1539,6 +1545,35 @@ function runModeWrite(argv: readonly string[]): Promise<number> {
|
|
|
1539
1545
|
return withMirrorWriteDb(argv, (db) => runModeWriteRoute(db, argv));
|
|
1540
1546
|
}
|
|
1541
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* `mirror doorman` (CV9.DS8.TS2): decide on a read-only handle, write behind
|
|
1550
|
+
* the pre-write snapshot only for a turn that delivers. The memory scores a
|
|
1551
|
+
* delta turn ranks go to the front-door log as numbers -- content-free -- the
|
|
1552
|
+
* hybrid and the semantic the threshold reads (US5), every ranked memory's,
|
|
1553
|
+
* so the threshold is read against measurement.
|
|
1554
|
+
*/
|
|
1555
|
+
function runMirrorDoorman(argv: readonly string[]): Promise<number> {
|
|
1556
|
+
const reporter = outcomeReporter(argv, "reception");
|
|
1557
|
+
const logPath = resolveLogPath(argv);
|
|
1558
|
+
return runMirrorDoormanRoute(argv, {
|
|
1559
|
+
withReadOnlyDatabase: (read) => withMirrorReadDb(argv, read),
|
|
1560
|
+
withLiveWriteDatabase: (write) => withMirrorWriteDb(argv, write),
|
|
1561
|
+
onReceptionOutcome: reporter.onOutcome,
|
|
1562
|
+
onMemoryScores: (scores) =>
|
|
1563
|
+
logFrontDoor(logPath, {
|
|
1564
|
+
command: "mirror",
|
|
1565
|
+
route: "ts",
|
|
1566
|
+
exitCode: 0,
|
|
1567
|
+
detail:
|
|
1568
|
+
`doorman memory_scores=${scores.map((s) => s.score.toFixed(3)).join(",")}` +
|
|
1569
|
+
` memory_semantic=${scores.map((s) => s.semantic.toFixed(3)).join(",")}`,
|
|
1570
|
+
}),
|
|
1571
|
+
}).then((exitCode) => {
|
|
1572
|
+
if (reporter.calls > 0) reporter.logSummary();
|
|
1573
|
+
return exitCode;
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1542
1577
|
function runSoulWrite(argv: readonly string[]): Promise<number> {
|
|
1543
1578
|
return withMirrorWriteDb(argv, (db) => runSoulRoute(db, argv));
|
|
1544
1579
|
}
|
|
@@ -1739,7 +1774,11 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1739
1774
|
createLoadRuntime: (db, dbPath, ignoreProductionRole) =>
|
|
1740
1775
|
createBuildLoadRuntime({ db, dbPath, ignoreProductionRole }),
|
|
1741
1776
|
nowIso,
|
|
1742
|
-
|
|
1777
|
+
// The session the harness named -- MIRROR_SESSION_ID, else Claude Code's or
|
|
1778
|
+
// Codex's own -- the same reader the mirror route uses (CV9.DS8.US4: read
|
|
1779
|
+
// MIRROR_SESSION_ID alone, a Builder switch on Claude Code stamped a guessed
|
|
1780
|
+
// session and the doorman never saw it).
|
|
1781
|
+
environmentSessionId: environmentSessionId(process.env),
|
|
1743
1782
|
});
|
|
1744
1783
|
}
|
|
1745
1784
|
if (isInit(argv)) return runInit(argv);
|
|
@@ -1759,6 +1798,7 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1759
1798
|
if (argv[0] === "descriptor" && argv[1] === "generate") return runDescriptorContentRoute(argv);
|
|
1760
1799
|
if (isConsolidateSubcommandWrite(argv)) return runConsolidateWrite(argv);
|
|
1761
1800
|
if (isShadowSubcommandWrite(argv)) return runShadowWrite(argv);
|
|
1801
|
+
if (isMirrorDoorman(argv)) return runMirrorDoorman(argv);
|
|
1762
1802
|
if (isMirrorWrite(argv)) return runMirrorWrite(argv);
|
|
1763
1803
|
if (isModeWrite(argv)) return runModeWrite(argv);
|
|
1764
1804
|
// CV22.DS7.US6: the Soul ritual; `routing.ts` allowlists its subcommands by
|
|
@@ -61,7 +61,7 @@ import { getIdentityContent } from "#identity/identityRead.ts";
|
|
|
61
61
|
import { getProjectPath } from "#journey/journeyStatus.ts";
|
|
62
62
|
import { loadMirrorContext } from "#mirror/context.ts";
|
|
63
63
|
import { persistStickyDefaults } from "#mirror/orchestration.ts";
|
|
64
|
-
import { resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
64
|
+
import { environmentSessionId, resolveRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
65
65
|
import { activateOperatingMode, deactivateOperatingMode } from "#mode/operatingMode.ts";
|
|
66
66
|
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
67
67
|
import { optionValue, stripOptionWithValue } from "./args.ts";
|
|
@@ -430,7 +430,7 @@ async function runPromote(
|
|
|
430
430
|
// `--session-id`, which its promote parser does not even accept.
|
|
431
431
|
const result = await runBuildLoad(
|
|
432
432
|
db,
|
|
433
|
-
{ slug, environmentSessionId: process.env
|
|
433
|
+
{ slug, environmentSessionId: environmentSessionId(process.env) },
|
|
434
434
|
runtime.deps,
|
|
435
435
|
);
|
|
436
436
|
process.stderr.write(result.stderr);
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import type { Database, WritableDatabase } from "#db/database.ts";
|
|
4
|
+
import {
|
|
5
|
+
type DoormanDecision,
|
|
6
|
+
decideDoorman,
|
|
7
|
+
doormanPersonas,
|
|
8
|
+
forgetDoormanDelivery,
|
|
9
|
+
type MemoryScore,
|
|
10
|
+
readDoormanSessionState,
|
|
11
|
+
runDoorman,
|
|
12
|
+
} from "#mirror/doorman.ts";
|
|
4
13
|
import {
|
|
5
14
|
deactivateMirrorState,
|
|
6
15
|
listActiveMirrorJourneys,
|
|
7
16
|
logMirrorResponse,
|
|
8
17
|
runMirrorLoad,
|
|
9
18
|
} from "#mirror/orchestration.ts";
|
|
19
|
+
import { environmentSessionId, resolveNamedRuntimeSessionId } from "#mirror/runtimeSession.ts";
|
|
10
20
|
import {
|
|
11
21
|
activateOperatingMode,
|
|
12
22
|
deactivateOperatingMode,
|
|
@@ -16,7 +26,7 @@ import {
|
|
|
16
26
|
} from "#mode/operatingMode.ts";
|
|
17
27
|
import type { OnProviderCallOutcome } from "#observability/callOutcome.ts";
|
|
18
28
|
import { resolveFamilyProviders } from "#providers/familyProviders.ts";
|
|
19
|
-
import { MIRROR_QUERY_TRANSPORT } from "#providers/transport.ts";
|
|
29
|
+
import { MIRROR_QUERY_TRANSPORT, mirrorQueryTransportFor } from "#providers/transport.ts";
|
|
20
30
|
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
21
31
|
import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
22
32
|
|
|
@@ -28,6 +38,8 @@ import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
|
28
38
|
*/
|
|
29
39
|
export type MirrorRouteEnvironment = {
|
|
30
40
|
MIRROR_SESSION_ID?: string;
|
|
41
|
+
CLAUDE_CODE_SESSION_ID?: string;
|
|
42
|
+
CODEX_SESSION_ID?: string;
|
|
31
43
|
MEMORY_RECEPTION?: string;
|
|
32
44
|
MIRROR_TS_MIRROR_LLM_REPLAY?: string;
|
|
33
45
|
MIRROR_TS_MIRROR_EMBEDDING_REPLAY?: string;
|
|
@@ -37,14 +49,119 @@ export function isMirrorWrite(argv: readonly string[]): boolean {
|
|
|
37
49
|
return argv[0] === "mirror" && ["load", "deactivate", "log"].includes(argv[1] ?? "");
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
/**
|
|
53
|
+
* `mirror doorman` is a write surface with its own handle discipline
|
|
54
|
+
* (CV9.DS8.TS2): it is not `isMirrorWrite`, because the front door's shared
|
|
55
|
+
* write seam opens the live-write path -- a database snapshot -- before any
|
|
56
|
+
* route runs, and a doorman turn that owes nothing must never pay that.
|
|
57
|
+
*/
|
|
58
|
+
export function isMirrorDoorman(argv: readonly string[]): boolean {
|
|
59
|
+
return argv[0] === "mirror" && argv[1] === "doorman";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DoormanRouteDeps {
|
|
63
|
+
/** The database, read-only: enough to decide. */
|
|
64
|
+
withReadOnlyDatabase: (read: (db: Database) => DoormanDecision) => DoormanDecision | number;
|
|
65
|
+
/** The database behind the pre-write snapshot: only for a turn that delivers. */
|
|
66
|
+
withLiveWriteDatabase: (
|
|
67
|
+
write: (db: WritableDatabase, dbPath: string) => Promise<number>,
|
|
68
|
+
) => Promise<number>;
|
|
69
|
+
onReceptionOutcome?: OnProviderCallOutcome;
|
|
70
|
+
/** Content-free: the turn's memory scores, hybrid and semantic, for the front-door log. */
|
|
71
|
+
onMemoryScores?: (scores: readonly MemoryScore[]) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `mirror doorman --prompt "<text>" [--session-id <id>]`: the one doorman as a
|
|
76
|
+
* command, for the Pi extension (US5). Prints the block or nothing; exit 0.
|
|
77
|
+
* `mirror doorman --forget [--session-id <id>]`: the runtime compacted the
|
|
78
|
+
* transcript; the named session's next ordinary turn is full again (US5's
|
|
79
|
+
* debt). Nothing printed; an unnamed session is nothing to mark.
|
|
80
|
+
*
|
|
81
|
+
* Two steps on purpose. The decision is read from a read-only handle; the
|
|
82
|
+
* live-write handle -- `VACUUM INTO` first, 95 ms measured on a 53 MB database
|
|
83
|
+
* -- opens only when the decision is `full` or `delta`. The ordinary turn
|
|
84
|
+
* outside Mirror Mode costs a read and nothing else, as the inject hook's does.
|
|
85
|
+
*/
|
|
86
|
+
export async function runMirrorDoormanRoute(
|
|
87
|
+
argv: readonly string[],
|
|
88
|
+
deps: DoormanRouteDeps,
|
|
89
|
+
env: MirrorRouteEnvironment = process.env,
|
|
90
|
+
): Promise<number> {
|
|
91
|
+
const args = argv.slice(2);
|
|
92
|
+
const sessionId = resolveNamedRuntimeSessionId(
|
|
93
|
+
optionValue(args, "--session-id") ?? null,
|
|
94
|
+
environmentSessionId(env),
|
|
95
|
+
);
|
|
96
|
+
if (hasOption(args, "--forget")) {
|
|
97
|
+
if (!sessionId) return 0;
|
|
98
|
+
return deps.withLiveWriteDatabase(async (db) => {
|
|
99
|
+
forgetDoormanDelivery(db, sessionId, nowIso());
|
|
100
|
+
return 0;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const prompt = optionValue(args, "--prompt");
|
|
104
|
+
if (prompt === null || prompt === undefined) {
|
|
105
|
+
console.error("mirror doorman requires --prompt");
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
const decided = deps.withReadOnlyDatabase((db) =>
|
|
109
|
+
decideDoorman({
|
|
110
|
+
prompt,
|
|
111
|
+
state: readDoormanSessionState(db, sessionId),
|
|
112
|
+
personas: doormanPersonas(db),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
if (typeof decided === "number") return decided;
|
|
116
|
+
if (decided.kind === "nothing") return 0;
|
|
117
|
+
|
|
118
|
+
// Providers only now: a turn that delivers may embed (delta) or route
|
|
119
|
+
// (full); the turn above resolved none, let alone a key.
|
|
120
|
+
const receptionEnabled = env.MEMORY_RECEPTION !== "0";
|
|
121
|
+
const family = await resolveFamilyProviders(env, mirrorQueryTransportFor(env));
|
|
122
|
+
return deps.withLiveWriteDatabase(async (db, dbPath) => {
|
|
123
|
+
const mirrorHome = dirname(dbPath);
|
|
124
|
+
const rendered = await runDoorman(db, decided, {
|
|
125
|
+
identity: basename(mirrorHome),
|
|
126
|
+
databasePath: dbPath,
|
|
127
|
+
mirrorHome,
|
|
128
|
+
user: basename(mirrorHome),
|
|
129
|
+
sessionId,
|
|
130
|
+
receptionEnabled,
|
|
131
|
+
onReceptionOutcome: deps.onReceptionOutcome,
|
|
132
|
+
llmProvider: receptionEnabled ? family.llm : undefined,
|
|
133
|
+
embeddingProvider: family.embedding,
|
|
134
|
+
onMemoryScores: deps.onMemoryScores,
|
|
135
|
+
newId,
|
|
136
|
+
nowIso,
|
|
137
|
+
});
|
|
138
|
+
process.stdout.write(rendered.stdout);
|
|
139
|
+
process.stderr.write(rendered.stderr);
|
|
140
|
+
return 0;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
40
144
|
export function isModeWrite(argv: readonly string[]): boolean {
|
|
41
145
|
if (argv[0] !== "mode") return false;
|
|
42
146
|
const args = modePositionals(argv.slice(1));
|
|
43
147
|
return args[0] === "activate" || args[0] === "deactivate";
|
|
44
148
|
}
|
|
45
149
|
|
|
46
|
-
|
|
47
|
-
|
|
150
|
+
/**
|
|
151
|
+
* `mirror deactivate` with nobody naming a session: `--session-id` absent AND
|
|
152
|
+
* no harness variable (CV9.DS8.US5 -- until then `--session-id` alone, so
|
|
153
|
+
* `/mm-new` on a Pi whose extension had named the session still refused).
|
|
154
|
+
*/
|
|
155
|
+
export function mirrorDeactivateMissingSession(
|
|
156
|
+
argv: readonly string[],
|
|
157
|
+
env: MirrorRouteEnvironment = process.env,
|
|
158
|
+
): boolean {
|
|
159
|
+
return (
|
|
160
|
+
argv[0] === "mirror" &&
|
|
161
|
+
argv[1] === "deactivate" &&
|
|
162
|
+
!optionValue(argv, "--session-id") &&
|
|
163
|
+
environmentSessionId(env) === null
|
|
164
|
+
);
|
|
48
165
|
}
|
|
49
166
|
|
|
50
167
|
export function runMirrorRead(db: Database, args: readonly string[]): number {
|
|
@@ -70,7 +187,10 @@ export async function runMirrorWriteRoute(
|
|
|
70
187
|
const args = argv.slice(2);
|
|
71
188
|
const sessionId = optionValue(args, "--session-id");
|
|
72
189
|
if (sub === "deactivate") {
|
|
73
|
-
|
|
190
|
+
// The session the caller named, else the harness's (TS2's reader): the
|
|
191
|
+
// unnamed case was refused before this route ran.
|
|
192
|
+
const named = resolveNamedRuntimeSessionId(sessionId ?? null, environmentSessionId(env));
|
|
193
|
+
deactivateMirrorState(db, named as string, nowIso());
|
|
74
194
|
console.error("Mirror Mode deactivated.");
|
|
75
195
|
return 0;
|
|
76
196
|
}
|
|
@@ -112,7 +232,11 @@ export async function runMirrorWriteRoute(
|
|
|
112
232
|
org: hasOption(args, "--org"),
|
|
113
233
|
contextOnly: hasOption(args, "--context-only"),
|
|
114
234
|
sessionId,
|
|
115
|
-
|
|
235
|
+
// The harness's variable, whichever runtime set it (CV9.DS8.TS2): the
|
|
236
|
+
// skill's own load on Claude Code or Codex names the session the hooks
|
|
237
|
+
// write. `mirror log` keeps reading `MIRROR_SESSION_ID` alone: its
|
|
238
|
+
// fallback is a guess, and widening that is a CR008 question.
|
|
239
|
+
environmentSessionId: environmentSessionId(env),
|
|
116
240
|
receptionEnabled,
|
|
117
241
|
onReceptionOutcome: options.onReceptionOutcome,
|
|
118
242
|
llmProvider: receptionEnabled ? family?.llm : undefined,
|