mirror-mind 0.36.3 → 0.38.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 +2 -0
- package/docs/releases/v0.37.0.md +221 -0
- package/docs/releases/v0.38.0.md +215 -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/closeTail.ts +7 -3
- package/ts/src/conversation/extraction.ts +1 -11
- package/ts/src/conversation/logger.ts +12 -6
- package/ts/src/conversation/userName.ts +44 -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/extraction/conversationMetadata.ts +53 -8
- package/ts/src/extraction/prompts.ts +8 -3
- 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/runtimeRoute.ts +4 -0
- 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/config.ts +105 -1
- package/ts/src/providers/cost.ts +5 -1
- package/ts/src/providers/llm.ts +40 -0
- package/ts/src/providers/transport.ts +14 -0
- package/ts/src/runtime/diagnose.ts +64 -7
- package/ts/src/runtime/pluginWiring.ts +6 -4
- package/ts/src/runtime/status.ts +24 -4
- 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
|
@@ -18,7 +18,14 @@ const ARG_SECRET_PATTERN =
|
|
|
18
18
|
// Model pins (AI-06, CV9.E2.S12): a deprecated pinned model would fail every
|
|
19
19
|
// extraction/embedding call, and those paths fail soft -- so the pins must be
|
|
20
20
|
// env-overridable to repoint a deployed instance without a release.
|
|
21
|
-
|
|
21
|
+
//
|
|
22
|
+
// The extraction pin moved from `google/gemini-2.5-flash-lite` to Claude Haiku 5.5
|
|
23
|
+
// on 2026-10-28 (CV9.DS9.US3), on DS9's measurement: the Navigator's own blind
|
|
24
|
+
// rankings on his conversations, the suite at reasoning off, and Flash Lite's
|
|
25
|
+
// upstream rate limits. A pin change is a migration: the pin, its entry in
|
|
26
|
+
// MODEL_DEFAULT_REASONING, and MODEL_PRICES (cost.ts) move in one commit, behind
|
|
27
|
+
// a green `eval --all` (the development guide's model upgrade playbook).
|
|
28
|
+
export const DEFAULT_EXTRACTION_MODEL = "anthropic/claude-haiku-5.5";
|
|
22
29
|
export const DEFAULT_EMBEDDING_MODEL = "openai/text-embedding-3-small";
|
|
23
30
|
|
|
24
31
|
export interface ModelPinOptions {
|
|
@@ -51,6 +58,103 @@ export function resolveEmbeddingModel(options: ModelPinOptions = {}): string {
|
|
|
51
58
|
return env.MEMORY_EMBEDDING_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The reasoning efforts OpenRouter's catalogue lists across its models (checked
|
|
63
|
+
* 2026-10-26, CV9.DS9.TS1). `none` turns reasoning off. Whether a given model
|
|
64
|
+
* honours a given effort is the operator's choice; this list is the vocabulary.
|
|
65
|
+
*/
|
|
66
|
+
export const REASONING_EFFORTS = [
|
|
67
|
+
"none",
|
|
68
|
+
"minimal",
|
|
69
|
+
"low",
|
|
70
|
+
"medium",
|
|
71
|
+
"high",
|
|
72
|
+
"xhigh",
|
|
73
|
+
"max",
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
77
|
+
|
|
78
|
+
/** What `MEMORY_EXTRACTION_REASONING` says: an effort to send, or a value that is not one. */
|
|
79
|
+
export interface ExtractionReasoning {
|
|
80
|
+
/** The effort to send as `reasoning.effort`, or null to send no reasoning field. */
|
|
81
|
+
effort: ReasoningEffort | null;
|
|
82
|
+
/** The value as written when it is not an effort; null otherwise. */
|
|
83
|
+
invalid: string | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read `MEMORY_EXTRACTION_REASONING` (CV9.DS9.TS1): the reasoning effort every
|
|
88
|
+
* extraction-family call sends. Absent or blank sends nothing, which is the request
|
|
89
|
+
* as it was before the setting existed.
|
|
90
|
+
*
|
|
91
|
+
* Unlike the numeric resolvers below, this never throws. They fail loudly, but they
|
|
92
|
+
* are read inside the session-end hooks, where a throw becomes an extraction attempt
|
|
93
|
+
* and three attempts quarantine a conversation for good (CR178). So a value that is
|
|
94
|
+
* not an effort sends nothing too, and comes back as `invalid` for `runtime status`
|
|
95
|
+
* and `runtime diagnose` to show.
|
|
96
|
+
*/
|
|
97
|
+
export function readExtractionReasoning(options: ModelPinOptions = {}): ExtractionReasoning {
|
|
98
|
+
const env = options.env ?? process.env;
|
|
99
|
+
const raw = env.MEMORY_EXTRACTION_REASONING ?? "";
|
|
100
|
+
const value = raw.trim().toLowerCase();
|
|
101
|
+
if (value === "") return { effort: null, invalid: null };
|
|
102
|
+
const effort = REASONING_EFFORTS.find((candidate) => candidate === value);
|
|
103
|
+
return effort ? { effort, invalid: null } : { effort: null, invalid: raw.trim() };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The reasoning effort a model sends when `MEMORY_EXTRACTION_REASONING` is unset
|
|
108
|
+
* (CV9.DS9.US3). A property of the model, because what DS9 measured is: Haiku
|
|
109
|
+
* reasons by default and spent the title call's 40-token budget, so it was
|
|
110
|
+
* measured and chosen at `none`; Flash Lite was measured with no reasoning field,
|
|
111
|
+
* and a model absent from this table gets none, which is the request as it was
|
|
112
|
+
* before the setting existed. So the playbook's rollback, `MEMORY_EXTRACTION_MODEL`
|
|
113
|
+
* set to the old pin, reproduces exactly the request the old pin was measured with,
|
|
114
|
+
* and naming the default pin explicitly sends the same request as leaving it unset.
|
|
115
|
+
*/
|
|
116
|
+
export const MODEL_DEFAULT_REASONING: Readonly<Record<string, ReasoningEffort>> = {
|
|
117
|
+
"anthropic/claude-haiku-5.5": "none",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Where the effective effort came from: the setting, the model's default, or nowhere. */
|
|
121
|
+
export type ReasoningSource = "setting" | "model_default";
|
|
122
|
+
|
|
123
|
+
/** The effective pair every extraction-family call sends: the model and its effort. */
|
|
124
|
+
export interface EffectiveExtractionReasoning {
|
|
125
|
+
/** The model the call names: `MEMORY_EXTRACTION_MODEL` or the default pin. */
|
|
126
|
+
model: string;
|
|
127
|
+
/** The effort to send as `reasoning.effort`, or null to send no reasoning field. */
|
|
128
|
+
effort: ReasoningEffort | null;
|
|
129
|
+
/** What decided `effort`; null when no field is sent. */
|
|
130
|
+
source: ReasoningSource | null;
|
|
131
|
+
/** `MEMORY_EXTRACTION_REASONING` as written when it is not an effort; null otherwise. */
|
|
132
|
+
invalid: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the model and the reasoning effort an extraction-family call sends
|
|
137
|
+
* (CV9.DS9.US3): the model first, then the effort: the setting when it names an
|
|
138
|
+
* effort, else the model's entry in `MODEL_DEFAULT_REASONING`, else no field. An
|
|
139
|
+
* invalid setting reads as unset, so the default pin still sends its model default;
|
|
140
|
+
* `invalid` carries the value for `runtime status` and `runtime diagnose`. The
|
|
141
|
+
* transport, the eval harness, and `runtime status` all read this one resolver.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveExtractionReasoning(
|
|
144
|
+
options: ModelPinOptions = {},
|
|
145
|
+
): EffectiveExtractionReasoning {
|
|
146
|
+
const model = resolveExtractionModel(options);
|
|
147
|
+
const setting = readExtractionReasoning(options);
|
|
148
|
+
if (setting.effort !== null) {
|
|
149
|
+
return { model, effort: setting.effort, source: "setting", invalid: null };
|
|
150
|
+
}
|
|
151
|
+
const modelDefault = MODEL_DEFAULT_REASONING[model];
|
|
152
|
+
if (modelDefault !== undefined) {
|
|
153
|
+
return { model, effort: modelDefault, source: "model_default", invalid: setting.invalid };
|
|
154
|
+
}
|
|
155
|
+
return { model, effort: null, source: null, invalid: setting.invalid };
|
|
156
|
+
}
|
|
157
|
+
|
|
54
158
|
// Observability (AI-09, CV9.E2.S13): mirrors Python's MEMORY_LOG_LLM_CALLS
|
|
55
159
|
// resolution exactly -- off | metadata | full, default metadata (never a
|
|
56
160
|
// silent full -- bodies are opt-in only). Legacy "1" maps to "full" for
|
package/ts/src/providers/cost.ts
CHANGED
|
@@ -29,7 +29,11 @@ export interface ModelPrice {
|
|
|
29
29
|
|
|
30
30
|
/** Estimated OpenRouter list prices (USD / 1K tokens). Verify on every pin change. */
|
|
31
31
|
export const MODEL_PRICES: Readonly<Record<string, ModelPrice>> = {
|
|
32
|
-
// Extraction pin --
|
|
32
|
+
// Extraction pin since CV9.DS9.US3 -- anthropic/claude-haiku-5.5: $0.10/M in,
|
|
33
|
+
// $0.50/M out, read from OpenRouter's catalogue on 2026-10-28.
|
|
34
|
+
"anthropic/claude-haiku-5.5": { promptPer1k: 0.0001, completionPer1k: 0.0005 },
|
|
35
|
+
// The pin before it, kept so a rolled-back install (`MEMORY_EXTRACTION_MODEL`)
|
|
36
|
+
// keeps pricing its calls -- google/gemini-2.5-flash-lite: ~$0.10/M in, ~$0.40/M out.
|
|
33
37
|
"google/gemini-2.5-flash-lite": { promptPer1k: 0.0001, completionPer1k: 0.0004 },
|
|
34
38
|
// Embedding pin -- openai/text-embedding-3-small: ~$0.02/M, no completion side.
|
|
35
39
|
"openai/text-embedding-3-small": { promptPer1k: 0.00002, completionPer1k: 0.0 },
|
package/ts/src/providers/llm.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type LlmTimeoutRole,
|
|
5
5
|
type ProviderConfig,
|
|
6
6
|
resolveExtractionModel,
|
|
7
|
+
resolveExtractionReasoning,
|
|
7
8
|
resolveLlmMaxRetries,
|
|
8
9
|
resolveLlmTimeoutMs,
|
|
9
10
|
resolveProviderConfig,
|
|
@@ -76,6 +77,33 @@ const LLM_TIMEOUT_TIER: Readonly<Record<LlmRole, LlmTimeoutRole>> = {
|
|
|
76
77
|
descriptor: "extraction",
|
|
77
78
|
};
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Which roles carry `MEMORY_EXTRACTION_REASONING` (CV9.DS9.TS1): every role that
|
|
82
|
+
* runs on the extraction pin. `consult` picks its own model on every call, so a
|
|
83
|
+
* setting meant for the extraction pin must not reach it.
|
|
84
|
+
*
|
|
85
|
+
* A full `Record<LlmRole, boolean>` for the reason `LLM_TIMEOUT_TIER` is one: a
|
|
86
|
+
* role added to `LLM_ROLES` cannot compile without declaring it. A check against
|
|
87
|
+
* `"consult"` would let the next role that names its own model inherit the
|
|
88
|
+
* setting silently.
|
|
89
|
+
*/
|
|
90
|
+
export const CARRIES_EXTRACTION_REASONING: Readonly<Record<LlmRole, boolean>> = {
|
|
91
|
+
extraction: true,
|
|
92
|
+
task_extraction: true,
|
|
93
|
+
summary: true,
|
|
94
|
+
curation: true,
|
|
95
|
+
consult: false,
|
|
96
|
+
reception: true,
|
|
97
|
+
consolidation: true,
|
|
98
|
+
shadow_scan: true,
|
|
99
|
+
conversation_title: true,
|
|
100
|
+
conversation_tags: true,
|
|
101
|
+
conversation_summary: true,
|
|
102
|
+
journal_classification: true,
|
|
103
|
+
week_plan: true,
|
|
104
|
+
descriptor: true,
|
|
105
|
+
};
|
|
106
|
+
|
|
79
107
|
/**
|
|
80
108
|
* The roles a message in Python's envelope can carry.
|
|
81
109
|
*
|
|
@@ -273,6 +301,16 @@ export class LiveLlmProvider implements LlmProvider {
|
|
|
273
301
|
async complete(request: LlmRequest): Promise<LlmResponse> {
|
|
274
302
|
const client = this.resolveClient();
|
|
275
303
|
const model = request.model ?? resolveExtractionModel({ env: this.env });
|
|
304
|
+
// CV9.DS9.TS1: the one added field; US3: the setting, else the model's own
|
|
305
|
+
// default (Haiku sends `none`; a model outside the table sends nothing). An
|
|
306
|
+
// invalid value reads as unset, so it fails nothing (a failed call would
|
|
307
|
+
// become an extraction attempt, CR178); `runtime status` and `diagnose`
|
|
308
|
+
// report it.
|
|
309
|
+
const { effort } = resolveExtractionReasoning({ env: this.env });
|
|
310
|
+
const reasoning =
|
|
311
|
+
effort !== null && CARRIES_EXTRACTION_REASONING[request.role]
|
|
312
|
+
? { reasoning: { effort } }
|
|
313
|
+
: {};
|
|
276
314
|
const startedAt = Date.now();
|
|
277
315
|
const body = await client.postJson(
|
|
278
316
|
"/chat/completions",
|
|
@@ -286,6 +324,8 @@ export class LiveLlmProvider implements LlmProvider {
|
|
|
286
324
|
: [{ role: "user", content: request.prompt }],
|
|
287
325
|
temperature: request.temperature ?? DEFAULT_TEMPERATURE,
|
|
288
326
|
max_tokens: request.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
327
|
+
// Unset, this adds nothing: the body is the one it was before the setting.
|
|
328
|
+
...reasoning,
|
|
289
329
|
},
|
|
290
330
|
{
|
|
291
331
|
timeoutMs: resolveLlmTimeoutMs(LLM_TIMEOUT_TIER[request.role], { env: this.env }),
|
|
@@ -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
|
|
@@ -34,11 +34,13 @@ import {
|
|
|
34
34
|
readExtensionPlugin,
|
|
35
35
|
} from "#extensions/extensionPlugin.ts";
|
|
36
36
|
import { identityKeyBreaksGrammar, SLUG_KEYED_LAYERS } from "#identity/identityKey.ts";
|
|
37
|
+
import { REASONING_EFFORTS, resolveExtractionReasoning } from "#providers/config.ts";
|
|
37
38
|
import { pyRepr, sortByCodePoint } from "#util/pythonText.ts";
|
|
38
39
|
import { configFilePath } from "./config.ts";
|
|
39
40
|
import { extensionPluginStates } from "./extensionPluginWiring.ts";
|
|
40
41
|
import type { GitWorktreeEntry } from "./git.ts";
|
|
41
42
|
import {
|
|
43
|
+
agyPluginCopyState,
|
|
42
44
|
codexPluginCacheState,
|
|
43
45
|
grokPluginCopyState,
|
|
44
46
|
installedPluginVersion,
|
|
@@ -46,7 +48,11 @@ import {
|
|
|
46
48
|
} from "./pluginWiring.ts";
|
|
47
49
|
|
|
48
50
|
import type { RuntimeStatusReport } from "./status.ts";
|
|
49
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
agyPluginInstallCommand,
|
|
53
|
+
CODEX_PLUGIN_ADD_COMMAND,
|
|
54
|
+
grokPluginReinstallCommand,
|
|
55
|
+
} from "./wiring.ts";
|
|
50
56
|
|
|
51
57
|
export interface DriftFinding {
|
|
52
58
|
code: string;
|
|
@@ -514,18 +520,19 @@ export function diagnoseRuntime(
|
|
|
514
520
|
|
|
515
521
|
/**
|
|
516
522
|
* 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
|
|
523
|
+
* CV21.DS10.US1; agy in CV21.DS11.TS4). Codex, agy, and Grok copy the plugin,
|
|
524
|
+
* so an npm update leaves the copy behind; Claude Code links it in place, so
|
|
525
|
+
* its failure mode is a dangling link, and Grok's rules link fails the same
|
|
526
|
+
* way. Grok adds one
|
|
520
527
|
* failure of its own: a respelt install path keys a SECOND copy, and Grok
|
|
521
528
|
* loads one of the two without saying which. Absence is not drift: no
|
|
522
529
|
* cache, no registry, and no link fire nothing, so an unwired machine stays
|
|
523
530
|
* green.
|
|
524
531
|
*
|
|
525
532
|
* 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).
|
|
533
|
+
* else ~/.codex, GROK_HOME else ~/.grok, ~/.claude, and ~/.gemini — never
|
|
534
|
+
* inside the readers, so every reader test is hermetic on a maintainer's
|
|
535
|
+
* machine with real wiring (CR123/128/137/142).
|
|
529
536
|
*/
|
|
530
537
|
export function pluginWiringFindings(
|
|
531
538
|
installRoot: string | null,
|
|
@@ -550,6 +557,25 @@ export function pluginWiringFindings(
|
|
|
550
557
|
}
|
|
551
558
|
}
|
|
552
559
|
|
|
560
|
+
// agy stages a copy under ~/.gemini/config/plugins/mirror-mind/ (agy 1.4.7,
|
|
561
|
+
// DS11.TS4's route), read the way the extensions plugin's copy is read: the
|
|
562
|
+
// `.gemini` home from HOME, with no Mirror variable to override it.
|
|
563
|
+
const geminiHome = home ? join(home, ".gemini") : null;
|
|
564
|
+
if (geminiHome && installedVersion && installRoot) {
|
|
565
|
+
const copy = agyPluginCopyState(geminiHome, installedVersion, "mirror-mind");
|
|
566
|
+
if (copy.state === "behind") {
|
|
567
|
+
const install = agyPluginInstallCommand(installRoot);
|
|
568
|
+
findings.push({
|
|
569
|
+
code: "agy_plugin_copy_behind",
|
|
570
|
+
severity: "warning",
|
|
571
|
+
subject: copy.copyPath,
|
|
572
|
+
detail: `copied ${copy.copiedVersion} < installed ${installedVersion}; agy runs the copy it staged at install`,
|
|
573
|
+
recommendation: `run \`${install}\` to bring the copy level (an update leaves agy's copy as it was, and installing again replaces it)`,
|
|
574
|
+
repair_route: install,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
553
579
|
if (home) {
|
|
554
580
|
const link = pluginLinkState(join(home, ".claude", "skills", "mirror-mind"));
|
|
555
581
|
if (link.state === "dangling") {
|
|
@@ -746,6 +772,37 @@ export function staleRevertGateFindings(env: NodeJS.ProcessEnv): DriftFinding[]
|
|
|
746
772
|
];
|
|
747
773
|
}
|
|
748
774
|
|
|
775
|
+
/**
|
|
776
|
+
* A `MEMORY_EXTRACTION_REASONING` that is not a reasoning effort (CV9.DS9.TS1).
|
|
777
|
+
*
|
|
778
|
+
* The transport reads it as unset rather than fail the call -- a failed call is
|
|
779
|
+
* an extraction attempt, and three quarantine a conversation for good (CR178) --
|
|
780
|
+
* so the setting is silently not in force: the calls send the model's own default
|
|
781
|
+
* effort (CV9.DS9.US3), or no reasoning field for a model without one. Diagnose
|
|
782
|
+
* says which, with the values that would be in force.
|
|
783
|
+
*/
|
|
784
|
+
export function extractionReasoningFindings(env: NodeJS.ProcessEnv): DriftFinding[] {
|
|
785
|
+
const { model, effort, invalid } = resolveExtractionReasoning({ env });
|
|
786
|
+
if (invalid === null) return [];
|
|
787
|
+
const sent = effort === null ? "no reasoning setting" : `${model}'s own default, ${effort}`;
|
|
788
|
+
return [
|
|
789
|
+
{
|
|
790
|
+
code: "extraction_reasoning_invalid",
|
|
791
|
+
severity: "attention",
|
|
792
|
+
subject: "MEMORY_EXTRACTION_REASONING",
|
|
793
|
+
detail: `"${invalid}" is not a reasoning effort, so extraction calls send ${sent}`,
|
|
794
|
+
recommendation:
|
|
795
|
+
`set it to ${formatEffortList()}, or unset it, where it is set: ` +
|
|
796
|
+
"the environment, the clone's .env, or ~/.config/mirror/env",
|
|
797
|
+
repair_route: "edit the setting",
|
|
798
|
+
},
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function formatEffortList(): string {
|
|
803
|
+
return `${REASONING_EFFORTS.slice(0, -1).join(", ")}, or ${REASONING_EFFORTS.at(-1)}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
749
806
|
/**
|
|
750
807
|
* Where a hook wrapper looks for Node after `MIRROR_NODE` and `PATH`, in order.
|
|
751
808
|
* The generated wrappers carry the same list (`for candidate in ...`), and
|
|
@@ -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
|
@@ -25,7 +25,7 @@ import { readExtensionPlugin } from "#extensions/extensionPlugin.ts";
|
|
|
25
25
|
import { loadExtensionManifest } from "#extensions/manifest.ts";
|
|
26
26
|
import { inspectMigrationFiles } from "#extensions/migrations.ts";
|
|
27
27
|
import { type DbPathEnv, dbNameForEnv, resolveMirrorHome } from "#frontDoor/dbPath.ts";
|
|
28
|
-
import { DEFAULT_EMBEDDING_MODEL,
|
|
28
|
+
import { DEFAULT_EMBEDDING_MODEL, resolveExtractionReasoning } from "#providers/config.ts";
|
|
29
29
|
import { sortByCodePoint } from "#util/pythonText.ts";
|
|
30
30
|
import { channelForReport } from "./channel.ts";
|
|
31
31
|
import type { MarkerValue } from "./git.ts";
|
|
@@ -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,
|
|
@@ -528,6 +529,24 @@ function intEnv(raw: string | undefined, fallback: number): number {
|
|
|
528
529
|
return Number.isNaN(parsed) ? fallback : parsed;
|
|
529
530
|
}
|
|
530
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The models line's reasoning clause (CV9.DS9.TS1, US3): the effort the calls
|
|
534
|
+
* send, marked `(model default)` when it is the model's rather than the setting's;
|
|
535
|
+
* empty when no field is sent, so a rolled-back pin's line stays byte for byte
|
|
536
|
+
* what the frozen status scenarios before US3 recorded; and an invalid setting
|
|
537
|
+
* shown, never thrown, beside the model default it falls back to.
|
|
538
|
+
*/
|
|
539
|
+
function reasoningClause(env: NodeJS.ProcessEnv): string {
|
|
540
|
+
const { effort, source, invalid } = resolveExtractionReasoning({ env });
|
|
541
|
+
if (effort !== null && source === "setting") return `reasoning=${effort}, `;
|
|
542
|
+
if (effort !== null) {
|
|
543
|
+
const note = invalid === null ? "model default" : `model default, invalid setting: ${invalid}`;
|
|
544
|
+
return `reasoning=${effort} (${note}), `;
|
|
545
|
+
}
|
|
546
|
+
if (invalid !== null) return `reasoning=invalid (${invalid}), `;
|
|
547
|
+
return "";
|
|
548
|
+
}
|
|
549
|
+
|
|
531
550
|
/** Port of `render_runtime_status`. */
|
|
532
551
|
export function renderRuntimeStatus(
|
|
533
552
|
report: RuntimeStatusReport,
|
|
@@ -569,7 +588,8 @@ export function renderRuntimeStatus(
|
|
|
569
588
|
`embedding=${formatG(embedding)}, extraction=${formatG(extraction)}; retries=${retries}`,
|
|
570
589
|
);
|
|
571
590
|
lines.push(
|
|
572
|
-
`Models: extraction=${env
|
|
591
|
+
`Models: extraction=${resolveExtractionReasoning({ env }).model}, ` +
|
|
592
|
+
reasoningClause(env) +
|
|
573
593
|
`embedding=${env.MEMORY_EMBEDDING_MODEL ?? DEFAULT_EMBEDDING_MODEL}`,
|
|
574
594
|
);
|
|
575
595
|
lines.push("");
|
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
|
}
|