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
|
@@ -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
|
|
@@ -36,18 +36,36 @@ export function modeLabel(state: OperatingModeState): string {
|
|
|
36
36
|
export interface OperatingModeState {
|
|
37
37
|
mode: string;
|
|
38
38
|
journey: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Present, and true, only on a session's stamp written under the session's
|
|
41
|
+
* NAME (`--session-id` or the harness's variable), never on a guessed one
|
|
42
|
+
* or the global row (CR008 option B, landed by CV9.DS8.US5). A lifecycle
|
|
43
|
+
* command binds a journey from a named session's Builder stamp only when
|
|
44
|
+
* this is set: `build load` with no name still stamps a guessed session,
|
|
45
|
+
* so another window's load can land on this one's row.
|
|
46
|
+
*/
|
|
47
|
+
namedSession?: true;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
export function activateOperatingMode(
|
|
42
51
|
db: WritableDatabase,
|
|
43
|
-
input: { mode: string; journey?: string | null; sessionId?: string | null },
|
|
52
|
+
input: { mode: string; journey?: string | null; sessionId?: string | null; named?: boolean },
|
|
44
53
|
nowIso: string,
|
|
45
54
|
): OperatingModeState {
|
|
46
55
|
const mode = input.mode.trim();
|
|
47
56
|
if (!mode) throw new Error("mode must not be empty");
|
|
48
57
|
const journey = input.journey?.trim() || null;
|
|
49
|
-
const
|
|
50
|
-
const
|
|
58
|
+
const named = Boolean(input.sessionId) && input.named === true;
|
|
59
|
+
const state: OperatingModeState = named
|
|
60
|
+
? { mode, journey, namedSession: true }
|
|
61
|
+
: { mode, journey };
|
|
62
|
+
// The key only when true: an unnamed stamp's bytes are what they were
|
|
63
|
+
// (`operating-mode-metadata.golden.json`).
|
|
64
|
+
const payload = {
|
|
65
|
+
active_mode: mode,
|
|
66
|
+
active_journey: journey,
|
|
67
|
+
...(named ? { named_session: true } : {}),
|
|
68
|
+
};
|
|
51
69
|
if (input.sessionId) {
|
|
52
70
|
const session = getRuntimeSession(db, input.sessionId);
|
|
53
71
|
const metadata = decodeMetadata(session?.metadata ?? null);
|
|
@@ -133,7 +151,7 @@ function stateFromPayload(payload: unknown): OperatingModeState | null {
|
|
|
133
151
|
typeof record.active_journey === "string" && record.active_journey.trim()
|
|
134
152
|
? record.active_journey.trim()
|
|
135
153
|
: null;
|
|
136
|
-
return { mode, journey };
|
|
154
|
+
return record.named_session === true ? { mode, journey, namedSession: true } : { mode, journey };
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
export function renderModeActivation(state: OperatingModeState): string {
|
|
@@ -321,6 +321,20 @@ export const MIRROR_QUERY_TRANSPORT: ProviderTransportSpec = {
|
|
|
321
321
|
liveReason: "DS8.US3 mirror load --query live",
|
|
322
322
|
};
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The mirror-query spec as the environment narrows it: with
|
|
326
|
+
* `MEMORY_RECEPTION=0` the classifier never runs, so the chat fixture stops
|
|
327
|
+
* being required and the embedding fixture alone is a complete replay. The
|
|
328
|
+
* router decided this way for `load --query` since CV22.DS8.US3; the doorman's
|
|
329
|
+
* route (CV9.DS8.TS2) resolves its providers from the same answer, so the
|
|
330
|
+
* route and the resolution cannot disagree about what "complete" means.
|
|
331
|
+
*/
|
|
332
|
+
export function mirrorQueryTransportFor(env: { MEMORY_RECEPTION?: string }): ProviderTransportSpec {
|
|
333
|
+
return env.MEMORY_RECEPTION === "0"
|
|
334
|
+
? { ...MIRROR_QUERY_TRANSPORT, replay: { embedding: MIRROR_QUERY_TRANSPORT.replay?.embedding } }
|
|
335
|
+
: MIRROR_QUERY_TRANSPORT;
|
|
336
|
+
}
|
|
337
|
+
|
|
324
338
|
/**
|
|
325
339
|
* `consolidate scan` and `shadow scan` -- the two cultivation leaves that send
|
|
326
340
|
* a PROMPT. Everything around the call landed in CV22.DS8.US3; the flip
|
|
@@ -39,6 +39,7 @@ import { configFilePath } from "./config.ts";
|
|
|
39
39
|
import { extensionPluginStates } from "./extensionPluginWiring.ts";
|
|
40
40
|
import type { GitWorktreeEntry } from "./git.ts";
|
|
41
41
|
import {
|
|
42
|
+
agyPluginCopyState,
|
|
42
43
|
codexPluginCacheState,
|
|
43
44
|
grokPluginCopyState,
|
|
44
45
|
installedPluginVersion,
|
|
@@ -46,7 +47,11 @@ import {
|
|
|
46
47
|
} from "./pluginWiring.ts";
|
|
47
48
|
|
|
48
49
|
import type { RuntimeStatusReport } from "./status.ts";
|
|
49
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
agyPluginInstallCommand,
|
|
52
|
+
CODEX_PLUGIN_ADD_COMMAND,
|
|
53
|
+
grokPluginReinstallCommand,
|
|
54
|
+
} from "./wiring.ts";
|
|
50
55
|
|
|
51
56
|
export interface DriftFinding {
|
|
52
57
|
code: string;
|
|
@@ -514,18 +519,19 @@ export function diagnoseRuntime(
|
|
|
514
519
|
|
|
515
520
|
/**
|
|
516
521
|
* The plugin wiring findings (CV21.DS4.TS6; the debt US1 carried; Grok in
|
|
517
|
-
* CV21.DS10.US1). Codex and Grok copy the plugin,
|
|
518
|
-
* the copy behind; Claude Code links it in place, so
|
|
519
|
-
* dangling link, and Grok's rules link fails the same
|
|
522
|
+
* CV21.DS10.US1; agy in CV21.DS11.TS4). Codex, agy, and Grok copy the plugin,
|
|
523
|
+
* so an npm update leaves the copy behind; Claude Code links it in place, so
|
|
524
|
+
* its failure mode is a dangling link, and Grok's rules link fails the same
|
|
525
|
+
* way. Grok adds one
|
|
520
526
|
* failure of its own: a respelt install path keys a SECOND copy, and Grok
|
|
521
527
|
* loads one of the two without saying which. Absence is not drift: no
|
|
522
528
|
* cache, no registry, and no link fire nothing, so an unwired machine stays
|
|
523
529
|
* green.
|
|
524
530
|
*
|
|
525
531
|
* The defaulting happens HERE, from the env the caller hands in — CODEX_HOME
|
|
526
|
-
* else ~/.codex, GROK_HOME else ~/.grok, and ~/.
|
|
527
|
-
* readers, so every reader test is hermetic on a maintainer's
|
|
528
|
-
* real wiring (CR123/128/137/142).
|
|
532
|
+
* else ~/.codex, GROK_HOME else ~/.grok, ~/.claude, and ~/.gemini — never
|
|
533
|
+
* inside the readers, so every reader test is hermetic on a maintainer's
|
|
534
|
+
* machine with real wiring (CR123/128/137/142).
|
|
529
535
|
*/
|
|
530
536
|
export function pluginWiringFindings(
|
|
531
537
|
installRoot: string | null,
|
|
@@ -550,6 +556,25 @@ export function pluginWiringFindings(
|
|
|
550
556
|
}
|
|
551
557
|
}
|
|
552
558
|
|
|
559
|
+
// agy stages a copy under ~/.gemini/config/plugins/mirror-mind/ (agy 1.4.7,
|
|
560
|
+
// DS11.TS4's route), read the way the extensions plugin's copy is read: the
|
|
561
|
+
// `.gemini` home from HOME, with no Mirror variable to override it.
|
|
562
|
+
const geminiHome = home ? join(home, ".gemini") : null;
|
|
563
|
+
if (geminiHome && installedVersion && installRoot) {
|
|
564
|
+
const copy = agyPluginCopyState(geminiHome, installedVersion, "mirror-mind");
|
|
565
|
+
if (copy.state === "behind") {
|
|
566
|
+
const install = agyPluginInstallCommand(installRoot);
|
|
567
|
+
findings.push({
|
|
568
|
+
code: "agy_plugin_copy_behind",
|
|
569
|
+
severity: "warning",
|
|
570
|
+
subject: copy.copyPath,
|
|
571
|
+
detail: `copied ${copy.copiedVersion} < installed ${installedVersion}; agy runs the copy it staged at install`,
|
|
572
|
+
recommendation: `run \`${install}\` to bring the copy level (an update leaves agy's copy as it was, and installing again replaces it)`,
|
|
573
|
+
repair_route: install,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
553
578
|
if (home) {
|
|
554
579
|
const link = pluginLinkState(join(home, ".claude", "skills", "mirror-mind"));
|
|
555
580
|
if (link.state === "dangling") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The plugin wiring readers (CV21.DS4.TS6; Grok in CV21.DS10.US1
|
|
1
|
+
// The plugin wiring readers (CV21.DS4.TS6; Grok in CV21.DS10.US1; agy's
|
|
2
|
+
// reader in CV21.DS5.US2, read for the canonical plugin since CV21.DS11.TS4).
|
|
2
3
|
//
|
|
3
4
|
// Three runtimes hold the plugin outside the install tree. Codex copies it
|
|
4
5
|
// into a versioned cache (`$CODEX_HOME/plugins/cache/mirror-mind/mirror-mind/
|
|
@@ -226,9 +227,10 @@ export interface AgyCopyState {
|
|
|
226
227
|
* Read agy's staged copy of a plugin below a `.gemini` home, read-only:
|
|
227
228
|
* `agy plugin install <dir>` stages the tree under `config/plugins/<name>/`
|
|
228
229
|
* (CV21.DS7.US1), and the copy's own `.claude-plugin/plugin.json` carries
|
|
229
|
-
* its version. Written for the extensions plugin (CV21.DS5.US2);
|
|
230
|
-
*
|
|
231
|
-
*
|
|
230
|
+
* its version. Written for the extensions plugin (CV21.DS5.US2); since
|
|
231
|
+
* CV21.DS11.TS4 it reads the canonical plugin's copy too, which agy 1.4.7
|
|
232
|
+
* stages the same way (`mirror-mind/.claude-plugin/plugin.json`, measured in
|
|
233
|
+
* a scratch HOME on 2026-10-24).
|
|
232
234
|
*/
|
|
233
235
|
export function agyPluginCopyState(
|
|
234
236
|
geminiHome: string,
|
package/ts/src/runtime/status.ts
CHANGED
|
@@ -119,7 +119,8 @@ function errorMessage(error: unknown): string {
|
|
|
119
119
|
*
|
|
120
120
|
* One rule, `assertSchemaState`'s, so the codebase keeps ONE definition of a
|
|
121
121
|
* healthy ledger: every id in `KNOWN_MIGRATION_IDS` is required, and an id
|
|
122
|
-
* outside it is unknown. A current database renders `current (
|
|
122
|
+
* outside it is unknown. A current database renders `current (18/18)` (CV9.DS8.US5's
|
|
123
|
+
* 018 is the eighteenth; the history below is TS5's).
|
|
123
124
|
*
|
|
124
125
|
* Until CV22.DS10.TS5 this grader kept a second rule for one migration.
|
|
125
126
|
* `017_journey_parent_column` is the first one TypeScript authored, and the
|
|
@@ -131,7 +132,7 @@ function errorMessage(error: unknown): string {
|
|
|
131
132
|
* the forgiveness hid a pending migration behind "ready" on exactly the
|
|
132
133
|
* databases `runtime migrate` exists to bring forward. TS5 finding F19 (a
|
|
133
134
|
* removal the comment here promised for plateau 3 and nobody performed) made
|
|
134
|
-
* the denominator honest:
|
|
135
|
+
* the denominator honest: every known migration, everywhere.
|
|
135
136
|
*/
|
|
136
137
|
export function inspectCoreMigrations(
|
|
137
138
|
dbPath: string | null,
|
package/ts/src/runtime/update.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { join } from "node:path";
|
|
|
30
30
|
import { verifyBackupArchive } from "#runtime/backup.ts";
|
|
31
31
|
import type { InstallKind } from "#runtime/installKind.ts";
|
|
32
32
|
import {
|
|
33
|
+
agyPluginCopyState,
|
|
33
34
|
codexPluginCacheState,
|
|
34
35
|
grokPluginCopyState,
|
|
35
36
|
installedPluginVersion,
|
|
@@ -43,7 +44,11 @@ import {
|
|
|
43
44
|
type UpdateResult,
|
|
44
45
|
type UpdateStage,
|
|
45
46
|
} from "#runtime/updatePipeline.ts";
|
|
46
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
agyPluginInstallCommand,
|
|
49
|
+
CODEX_PLUGIN_ADD_COMMAND,
|
|
50
|
+
grokPluginReinstallCommand,
|
|
51
|
+
} from "#runtime/wiring.ts";
|
|
47
52
|
import { commandOnPath } from "#util/paths.ts";
|
|
48
53
|
import { PROGRAM } from "#util/program.ts";
|
|
49
54
|
import { shellWord } from "#util/shellWord.ts";
|
|
@@ -456,6 +461,17 @@ export function postUpdateHints(
|
|
|
456
461
|
);
|
|
457
462
|
}
|
|
458
463
|
}
|
|
464
|
+
// CV21.DS11.TS4: agy stages a copy too, and an update leaves it as it was;
|
|
465
|
+
// installing again from the path init printed brings it level.
|
|
466
|
+
const geminiHome = home ? join(home, ".gemini") : null;
|
|
467
|
+
if (installRoot && geminiHome && landed) {
|
|
468
|
+
const copy = agyPluginCopyState(geminiHome, landed, "mirror-mind");
|
|
469
|
+
if (copy.state === "behind") {
|
|
470
|
+
hints.push(
|
|
471
|
+
`Antigravity's copy of the plugin is behind the update (${copy.copiedVersion} < ${landed}): run \`${agyPluginInstallCommand(installRoot)}\` to bring it level.`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
459
475
|
|
|
460
476
|
return hints;
|
|
461
477
|
}
|
package/ts/src/runtime/wiring.ts
CHANGED
|
@@ -83,6 +83,19 @@ function grokPluginInstallCommand(root: string): string {
|
|
|
83
83
|
return `grok plugin install "${pluginDirectory(root)}" --trust`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The one agy install command (CV21.DS7.US1), which is also its level-up
|
|
88
|
+
* (CV21.DS11.TS4). agy stages a copy of the plugin under
|
|
89
|
+
* `~/.gemini/config/plugins/mirror-mind/` (agy 1.4.7), an update leaves that
|
|
90
|
+
* copy as it was, and installing again replaces it (by name, from any path). A
|
|
91
|
+
* function of the root, so the line `init` prints, the recommendation
|
|
92
|
+
* `diagnose` prints, and the hint `update` prints cannot disagree (the CR071
|
|
93
|
+
* lesson at the string level).
|
|
94
|
+
*/
|
|
95
|
+
export function agyPluginInstallCommand(root: string): string {
|
|
96
|
+
return `agy plugin install "${pluginDirectory(root)}"`;
|
|
97
|
+
}
|
|
98
|
+
|
|
86
99
|
export interface RuntimeWiring {
|
|
87
100
|
/** The runtime, as a person names it. */
|
|
88
101
|
readonly title: string;
|
|
@@ -121,7 +134,7 @@ export const RUNTIME_WIRING: Readonly<Record<RuntimeName, RuntimeWiring>> = {
|
|
|
121
134
|
// the Claude surfaces and overwrites the layer's hooks file). Per-turn
|
|
122
135
|
// injection and logging land with DS7's TS2/TS3; this line promises
|
|
123
136
|
// the install, nothing more.
|
|
124
|
-
(root) => [
|
|
137
|
+
(root) => [agyPluginInstallCommand(root)],
|
|
125
138
|
],
|
|
126
139
|
},
|
|
127
140
|
grok: {
|
|
@@ -54,9 +54,22 @@ export interface FreshSearchOptions extends FreshSearchFilters {
|
|
|
54
54
|
* tool: search never learns it exists.
|
|
55
55
|
*/
|
|
56
56
|
recordEmbeddingLedger?: boolean | ((info: EmbeddingAttemptInfo) => void);
|
|
57
|
+
/**
|
|
58
|
+
* The query's embedding, already generated (CV9.DS8.TS2). The doorman embeds
|
|
59
|
+
* a turn's prompt once and hands the vector to this search and to the
|
|
60
|
+
* attachment search, so a Mirror Mode turn costs one embedding call, counted
|
|
61
|
+
* once (DS8 decision 1). When given, no provider is called and nothing is
|
|
62
|
+
* recorded here: the caller that embedded owns the ledger row.
|
|
63
|
+
*/
|
|
64
|
+
queryEmbedding?: readonly number[];
|
|
57
65
|
frozenNowMs?: number;
|
|
58
66
|
now?: string;
|
|
59
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Absent only with `queryEmbedding` given, or on a turn with no provider at
|
|
69
|
+
* all (the doorman without a key, CV9.DS8.TS2): the search then degrades to
|
|
70
|
+
* FTS as it does when the provider fails, with `no_provider` as the kind.
|
|
71
|
+
*/
|
|
72
|
+
provider?: EmbeddingProvider;
|
|
60
73
|
/** Test-injectable override for generateEmbeddingSafely's retry backoff
|
|
61
74
|
* (real timers by default) -- avoids paying real backoff wait time in tests
|
|
62
75
|
* that exercise the transient-retry path. */
|
|
@@ -111,26 +124,33 @@ export async function searchMemoriesWithStatus(
|
|
|
111
124
|
options: FreshSearchOptions,
|
|
112
125
|
): Promise<FreshSearchOutcome> {
|
|
113
126
|
const limit = options.limit ?? 5;
|
|
114
|
-
let queryEmbedding: readonly number[] = [];
|
|
127
|
+
let queryEmbedding: readonly number[] = options.queryEmbedding ?? [];
|
|
115
128
|
let degraded = false;
|
|
116
129
|
let degradedKind: string | undefined;
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
// calls the same generate_embedding used everywhere else, not a bespoke
|
|
121
|
-
// single-shot attempt. Any exhausted/permanent/provider-exception failure
|
|
122
|
-
// still maps to degraded=true here, preserving CR037's contract exactly.
|
|
123
|
-
queryEmbedding = await generateEmbeddingSafely(options.provider, options.query, {
|
|
124
|
-
onAttempt: resolveLedgerSink(db, options.recordEmbeddingLedger),
|
|
125
|
-
sleep: options.embeddingRetrySleep,
|
|
126
|
-
});
|
|
127
|
-
} catch (error) {
|
|
130
|
+
if (options.queryEmbedding !== undefined) {
|
|
131
|
+
// The caller embedded: nothing to generate, nothing to record.
|
|
132
|
+
} else if (!options.provider) {
|
|
128
133
|
degraded = true;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
degradedKind = "no_provider";
|
|
135
|
+
} else {
|
|
136
|
+
try {
|
|
137
|
+
// generateEmbeddingSafely (CR043) retries a transient empty response up to
|
|
138
|
+
// its default budget before giving up -- matching Python's search, which
|
|
139
|
+
// calls the same generate_embedding used everywhere else, not a bespoke
|
|
140
|
+
// single-shot attempt. Any exhausted/permanent/provider-exception failure
|
|
141
|
+
// still maps to degraded=true here, preserving CR037's contract exactly.
|
|
142
|
+
queryEmbedding = await generateEmbeddingSafely(options.provider, options.query, {
|
|
143
|
+
onAttempt: resolveLedgerSink(db, options.recordEmbeddingLedger),
|
|
144
|
+
sleep: options.embeddingRetrySleep,
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
degraded = true;
|
|
148
|
+
// Keep the CLASS of failure (AI-18 taxonomy) so an operator can tell an
|
|
149
|
+
// expired key from a timeout from a rate limit. Without this the front
|
|
150
|
+
// door only ever says "offline or no API key", which is a guess -- and
|
|
151
|
+
// the taxonomy the live transport builds would die here unseen.
|
|
152
|
+
degradedKind = classifyDegradedCause(error);
|
|
153
|
+
}
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
const memories = listSearchMemoryRows(db, options);
|
package/ts/src/search/ranker.ts
CHANGED
|
@@ -33,12 +33,22 @@ export interface RankableMemory {
|
|
|
33
33
|
|
|
34
34
|
export interface RankedMemory {
|
|
35
35
|
id: string;
|
|
36
|
+
/** The hybrid score the order is by. */
|
|
36
37
|
score: number;
|
|
38
|
+
/**
|
|
39
|
+
* The semantic component alone: the cosine of the query and the memory, 0 on
|
|
40
|
+
* a degraded search. Reported, not ranked by (CV9.DS8.US5): the doorman's
|
|
41
|
+
* memory threshold reads it, because half of `score` is recency,
|
|
42
|
+
* reinforcement, relevance, and lexical, and a fresh, reinforced memory
|
|
43
|
+
* floors near 0.35 with no semantic match at all.
|
|
44
|
+
*/
|
|
45
|
+
semantic: number;
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
interface Candidate {
|
|
40
49
|
id: string;
|
|
41
50
|
score: number;
|
|
51
|
+
semantic: number;
|
|
42
52
|
embedding: Float32Array;
|
|
43
53
|
}
|
|
44
54
|
|
|
@@ -131,7 +141,7 @@ export function rankMemories(
|
|
|
131
141
|
lexical: memory.lexical_score ?? 0,
|
|
132
142
|
weights: config.weights,
|
|
133
143
|
});
|
|
134
|
-
return { id: memory.id, score, embedding };
|
|
144
|
+
return { id: memory.id, score, semantic, embedding };
|
|
135
145
|
});
|
|
136
146
|
|
|
137
147
|
candidates.sort((a, b) => b.score - a.score);
|
|
@@ -150,5 +160,9 @@ export function rankMemories(
|
|
|
150
160
|
if (selected.length >= config.limit) break;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
return selected.map((candidate) => ({
|
|
163
|
+
return selected.map((candidate) => ({
|
|
164
|
+
id: candidate.id,
|
|
165
|
+
score: candidate.score,
|
|
166
|
+
semantic: candidate.semantic,
|
|
167
|
+
}));
|
|
154
168
|
}
|