mirror-mind 0.37.0 → 0.38.1
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/docs/releases/index.md +2 -0
- package/docs/releases/v0.33.3.md +9 -1
- package/docs/releases/v0.38.0.md +215 -0
- package/docs/releases/v0.38.1.md +152 -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/ts/src/conversation/closeTail.ts +7 -3
- package/ts/src/conversation/extraction.ts +1 -11
- package/ts/src/conversation/userName.ts +44 -0
- package/ts/src/extraction/conversationMetadata.ts +53 -8
- package/ts/src/extraction/prompts.ts +8 -3
- package/ts/src/frontDoor/cli.ts +32 -103
- package/ts/src/frontDoor/consultRoute.ts +4 -2
- package/ts/src/frontDoor/dbPath.ts +80 -7
- package/ts/src/frontDoor/initRoute.ts +190 -0
- package/ts/src/frontDoor/mirrorHomes.ts +95 -0
- package/ts/src/frontDoor/namedRefusal.ts +29 -1
- package/ts/src/frontDoor/runtimeRoute.ts +13 -5
- package/ts/src/frontDoor/searchRoute.ts +31 -3
- package/ts/src/frontDoor/usage.ts +17 -3
- package/ts/src/init/init.ts +2 -1
- package/ts/src/providers/config.ts +193 -2
- package/ts/src/providers/cost.ts +5 -1
- package/ts/src/providers/embedding.ts +26 -3
- package/ts/src/providers/familyProviders.ts +43 -0
- package/ts/src/providers/llm.ts +40 -0
- package/ts/src/runtime/diagnose.ts +59 -0
- package/ts/src/runtime/git.ts +153 -42
- package/ts/src/runtime/status.ts +47 -18
- package/ts/src/runtime/strategies/clone.ts +76 -55
- package/ts/src/runtime/update.ts +2 -1
- package/ts/src/search/memorySearch.ts +4 -2
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,8 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.38.1 — The Refusal Names the Way Out](v0.38.1.md) — patch release of three Change Requests, CR185, CR186, and CR187: a package with no home configured names the homes it finds and the `mirror init <name>` that selects one, and `init` adopts a home that already exists; a missing or refused OpenRouter key is one line that names the file, before anything is touched; a `stable` clone ahead only by `main`'s commits is told that `stable` is behind it, not to push or reset. No migration.
|
|
16
|
+
- [v0.38.0 — The Model Behind the Memories, Chosen by Measurement](v0.38.0.md) — minor release closing CV9.DS9: the model behind memories, tasks, titles, summaries, tags, reception, journal, shadow, and consolidation is Claude Haiku 5.5 at reasoning off, chosen on the eval suite with reasoning on and off and on 24 of the owner's own conversations ranked blind, where it came first on 16 of 26 surfaces. Every extraction-pin call can carry a reasoning effort, and a pin carries its own default. With CR180, summaries open with their subject, every lifecycle prompt sees the owner's name, and a framed opening is written once more. Release gate 70 of 70 probes. Rollback is one env line. No migration.
|
|
15
17
|
- [v0.37.0 — The Mirror Stays in the Conversation](v0.37.0.md) — minor release closing CV9.DS8 and, with CV21.DS11, CV21: on Pi, Claude Code, Codex, and Antigravity one doorman in the core puts Mirror's context before the model on every turn of Mirror Mode, all of it on the turn Mirror Mode starts and then the persona and memories each question calls for; an explicit turn loads once; one signature, `◇ persona-name`; Pi reaches L4, measured live. One parity matrix in the runtime interface contract, sourced and held by a test. Codex, Antigravity, and Grok Build need their plugin copy brought level. Migration 018, applied on its own.
|
|
16
18
|
- [v0.36.3 — A Skill You Invoke Is Not Something You Said](v0.36.3.md) — patch release of one Change Request, CR160, with CR159's docs: on Pi, a skill picked from the menu or typed is no longer stored as the person's words, and neither is its reply; a question asked with `/mm-mirror` is stored as the question. Pi's turns go through the rule the other runtimes have. `mirror conversations skill-turns` repairs what earlier versions stored, only on `--apply` and after a verified backup. No migration.
|
|
17
19
|
- [v0.36.2 — Clearing Out What Another Home Copied](v0.36.2.md) — patch release of one Change Request, CR154, finishing what v0.36.1 began: `mirror conversations imported` lists the Pi conversations an earlier version imported, by the folder each session came from and beside how many sessions the home logged live there, and `remove` deletes the ones you name, only on `--apply` and after a verified backup. `discard-current` no longer fails on a conversation a Soul Mode integration names. No migration.
|
package/docs/releases/v0.33.3.md
CHANGED
|
@@ -111,7 +111,8 @@ hand:
|
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
# a clone: in the checkout
|
|
114
|
-
git
|
|
114
|
+
git fetch origin
|
|
115
|
+
git merge --ff-only origin/stable # origin/main if the clone follows main
|
|
115
116
|
# a package
|
|
116
117
|
npm install -g mirror-mind@latest
|
|
117
118
|
```
|
|
@@ -124,6 +125,13 @@ mirror runtime update # or: npm install -g mirror-mind
|
|
|
124
125
|
|
|
125
126
|
There is no database migration and no runtime wiring change.
|
|
126
127
|
|
|
128
|
+
*Corrected on 2026-10-29:* the clone line first read `git pull --ff-only`. A clone follows
|
|
129
|
+
`stable` unless you chose `main`. On `main`, that command pulls past the release, so the
|
|
130
|
+
next `runtime update` refused with `plan: local commits present` (CR186). If that happened
|
|
131
|
+
to you, your checkout is fine. Every later release contains the commits it pulled, so
|
|
132
|
+
`runtime update` works again from the next release on. `mirror runtime channel main`
|
|
133
|
+
follows `main` instead.
|
|
134
|
+
|
|
127
135
|
## Conscious Exclusions
|
|
128
136
|
|
|
129
137
|
- **A Windows product.** REFERENCE.md's platform envelope stands: POSIX. The Windows
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.38.0 is a minor release closing CV9.DS9. The model that writes your memories,
|
|
4
|
+
tasks, titles, summaries, and tags is now Claude Haiku 5.5 at reasoning off, chosen by
|
|
5
|
+
measurement: on the eval suite with reasoning on and off, and on 24 of the owner's own
|
|
6
|
+
conversations ranked blind, where it came first on 16 of 26 surfaces. Every call on
|
|
7
|
+
the extraction pin can carry a reasoning effort, and a pin carries its own default. The
|
|
8
|
+
summary prompt stops naming the conversation, every lifecycle prompt finally sees the
|
|
9
|
+
owner's name, and a summary that still opens by describing the conversation is
|
|
10
|
+
written once more. The release gate ran 70 of 70 probes clean. Rollback is one line.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# v0.38.0 — The Model Behind the Memories, Chosen by Measurement
|
|
14
|
+
|
|
15
|
+
**Date:** 2026-10-29
|
|
16
|
+
|
|
17
|
+
## Highlights
|
|
18
|
+
|
|
19
|
+
- The model behind memory and task extraction, titles, summaries, tags, reception,
|
|
20
|
+
journal classification, week plans, descriptors, shadow, and consolidation is now
|
|
21
|
+
`anthropic/claude-haiku-5.5`, at reasoning off. It was chosen on three measurements:
|
|
22
|
+
the eval suite with reasoning on, the suite with reasoning off, and 24 of the owner's
|
|
23
|
+
own conversations replayed through the product and ranked blind, where Haiku came
|
|
24
|
+
first on 16 of 26 surfaces, ahead of Gemini 2.5 Flash Lite 17–9 and of GPT-6 Luna
|
|
25
|
+
20–6.
|
|
26
|
+
- Every call on the extraction pin can carry a reasoning effort
|
|
27
|
+
(`MEMORY_EXTRACTION_REASONING`: `none` to `max`), and a pin carries its own default:
|
|
28
|
+
Haiku sends `none`, the setting it was measured with. A model named by
|
|
29
|
+
`MEMORY_EXTRACTION_MODEL` sends what it was measured with too, so the rollback is
|
|
30
|
+
exactly the request Flash Lite always got.
|
|
31
|
+
- Summaries open with their subject, carrying its status, instead of "The conversation
|
|
32
|
+
centered on…"; and for the first time the title, summary, and tags prompts see the
|
|
33
|
+
owner's name rather than `User`. A summary that still opens by describing the
|
|
34
|
+
conversation is generated once more, with feedback.
|
|
35
|
+
- The release gate: `eval --all` 9 of 9 modules and 70 of 70 probes, the first fully
|
|
36
|
+
clean run in the development home's history. Flash Lite had failed four probes in
|
|
37
|
+
every recorded run.
|
|
38
|
+
- What you pay: about 40% more on these calls at typical volume, about a dollar a month
|
|
39
|
+
where Flash Lite cost seventy-five cents. What you wait: about half a second more on
|
|
40
|
+
reception, before a turn that then waits seconds for the answering model.
|
|
41
|
+
- No migration. Rollback is one line in `~/.config/mirror/env`.
|
|
42
|
+
|
|
43
|
+
## Where We Started
|
|
44
|
+
|
|
45
|
+
Since mid-2025 one model, Gemini 2.5 Flash Lite, wrote everything Mirror writes about a
|
|
46
|
+
conversation after it ends. It dates from mid-2025, failed the same four eval probes in
|
|
47
|
+
every recorded run (duplicate memories across the two passes, a summary that reads as a
|
|
48
|
+
description of a conversation, a persona routed on an open existential question, a
|
|
49
|
+
trivial conversation given a title), and drew HTTP 429 from Google AI Studio on a third
|
|
50
|
+
of the eval suite's attempts. Two newer models cost about the same on OpenRouter. The
|
|
51
|
+
question was whether either wrote better memories for the person who reads them, and at
|
|
52
|
+
what cost in latency, reliability, and money.
|
|
53
|
+
|
|
54
|
+
The development guide treats a model pin as a versioned dependency and a swap as a
|
|
55
|
+
migration, measured before it is decided. CV9.DS9 was that migration, in three stories.
|
|
56
|
+
|
|
57
|
+
## What Changed
|
|
58
|
+
|
|
59
|
+
### Reasoning off, so the candidates could be measured at all (CV9.DS9.TS1)
|
|
60
|
+
|
|
61
|
+
Both candidates reason by default, and the transport sent nothing about it. Reasoning
|
|
62
|
+
spent the title call's 40-token budget, so 13 of 56 candidate title calls came back cut
|
|
63
|
+
off, mostly empty, and an empty title reads as a trivial conversation: silently
|
|
64
|
+
untitled. The blocking title-injection probe "passed" without an answer.
|
|
65
|
+
|
|
66
|
+
`MEMORY_EXTRACTION_REASONING` names an OpenRouter reasoning effort, sent as
|
|
67
|
+
`reasoning.effort` on every call that runs on the extraction pin, never on `consult`,
|
|
68
|
+
which picks its own model. A value that is not an effort is ignored rather than fatal,
|
|
69
|
+
because the setting is read inside the session-end hooks, where a failed call is an
|
|
70
|
+
extraction attempt and three quarantine a conversation; `runtime status` and
|
|
71
|
+
`runtime diagnose` show it. Eval records carry the effort, and `--history` labels a
|
|
72
|
+
flip across a model or reasoning change.
|
|
73
|
+
|
|
74
|
+
Re-measured at `none`, seven runs each: neither candidate had a call cut off in 1,428,
|
|
75
|
+
both passed every blocking probe 14 of 14, Luna kept every fix and improved two, and
|
|
76
|
+
Haiku lost one, the summary's opening, accepted as measured.
|
|
77
|
+
|
|
78
|
+
### The owner's own conversations, ranked blind (CV9.DS9.TS2)
|
|
79
|
+
|
|
80
|
+
24 ended conversations from the owner's production home, drawn by a recorded seed, were
|
|
81
|
+
replayed through the product's own functions: Flash Lite as it ships, Luna and Haiku at
|
|
82
|
+
`none`, one process per model. Kimi K3.1 Pro, a model from a fourth family, ranked every
|
|
83
|
+
surface blind against an anchored rubric, with the transcript fenced as data; the owner
|
|
84
|
+
ranked six conversations blind to calibrate it. The judge agreed with itself (0.83) and
|
|
85
|
+
not with the owner (0.55, under the 0.60 the plan required), so it did not stand, and the
|
|
86
|
+
decision rested on the owner's own rankings: Haiku first on 16 of 26 surfaces. The
|
|
87
|
+
judge's arithmetic, by a rule fixed before any output was seen, pointed the same way.
|
|
88
|
+
Nothing was written to either home, no transcript left the owner's machine, and the
|
|
89
|
+
evidence committed holds numbers and fixed labels only.
|
|
90
|
+
|
|
91
|
+
### The pin moves (CV9.DS9.US3)
|
|
92
|
+
|
|
93
|
+
The owner decided at the plan's approval: `anthropic/claude-haiku-5.5` at `none`.
|
|
94
|
+
Against it, weighed and accepted: about 40% more cost, because Haiku's tokenizer counts
|
|
95
|
+
1.5× the input tokens; reception about half a second slower at p95; and summaries
|
|
96
|
+
weaker as standalone notes than Luna's, though better than Flash Lite's, a defect that
|
|
97
|
+
turned out to be the prompt's.
|
|
98
|
+
|
|
99
|
+
One commit changed the pin, its reasoning default, and the price table together.
|
|
100
|
+
A pin's reasoning default is now a property of the model, looked up by the model the
|
|
101
|
+
call names: Haiku sends `none` whether defaulted or named explicitly; Flash Lite, and any
|
|
102
|
+
model not measured, sends no reasoning field. So the rollback reproduces exactly the
|
|
103
|
+
request Flash Lite was measured with. `runtime status` reads
|
|
104
|
+
`extraction=anthropic/claude-haiku-5.5, reasoning=none (model default)`.
|
|
105
|
+
|
|
106
|
+
The gate ran as the playbook orders: a fresh `eval --all` on Flash Lite before the
|
|
107
|
+
change (9 of 9 modules), then on Haiku after it (9 of 9, one expected probe failure),
|
|
108
|
+
every module's prompt hash equal, `--history` labelling the flip as a model and
|
|
109
|
+
reasoning change. The owner's first real conversation on the new pin came back titled,
|
|
110
|
+
tagged, summarised, and priced.
|
|
111
|
+
|
|
112
|
+
### The summary reads as a note (CR180)
|
|
113
|
+
|
|
114
|
+
The summary prompt told the model to write "a summary of the conversation" and to
|
|
115
|
+
"open with the topic the conversation addressed", then forbade writing "the
|
|
116
|
+
conversation" four rules later. The positive pull won: Haiku at `none` opened "The
|
|
117
|
+
conversation centered on…" in 10 of 14 eval runs with the prohibition in view, and every
|
|
118
|
+
Flash Lite summary in TS2 was flagged not standalone.
|
|
119
|
+
|
|
120
|
+
Three edits, each measured seven times: the first line now says who will read the
|
|
121
|
+
summary and why ("on its own, months from now, by someone deciding whether the full
|
|
122
|
+
transcript is worth opening"); the opening rule shows one example with its status in
|
|
123
|
+
it instead of describing what to open with; the standalone rule asks for what is now
|
|
124
|
+
known or decided, with the person called by name. Across 21 runs no summary named the
|
|
125
|
+
conversation.
|
|
126
|
+
|
|
127
|
+
The owner's validation conversation then failed anyway, and taught two more things.
|
|
128
|
+
First, the title, summary, and tags prompts had never been given the owner's name: the
|
|
129
|
+
close tail did not pass one, and the extraction path's resolver matched "You are
|
|
130
|
+
talking to" while the shipped identity template writes "You are speaking with", so
|
|
131
|
+
every lifecycle prompt since the port had labelled the speaker `User`. One fail-soft
|
|
132
|
+
resolver now serves both paths, and "the user" vanished from summaries with it. Second,
|
|
133
|
+
on a conversation that is itself about sessions and roadmaps, the model opens with the
|
|
134
|
+
frame about half the time whatever the wording says, and a rule that names the frame
|
|
135
|
+
gets echoed. So the guard is in the system: a summary whose first clause is the
|
|
136
|
+
conversation, discussion, session, exchange, chat, dialogue, transcript, or user is
|
|
137
|
+
generated once more with a short positive note, and the second is kept only if it
|
|
138
|
+
opens clean. A blind retry recovered 2 of 5; the note recovered 5 of 5. Both calls reach
|
|
139
|
+
the ledger. The owner's next conversation summarised as "<the owner's name> asked the
|
|
140
|
+
mirror to explain its operating modes… Settled: … Open: …", and the ledger showed the
|
|
141
|
+
guard had fired on it.
|
|
142
|
+
|
|
143
|
+
## Upgrading
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
mirror runtime update # or: npm install -g mirror-mind@latest
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
No migration. The pin is configuration: the first conversation that ends after the
|
|
150
|
+
update is written by Claude Haiku 5.5. Your OpenRouter key needs no change; Anthropic
|
|
151
|
+
models are routed by OpenRouter like the rest.
|
|
152
|
+
|
|
153
|
+
Then restart your Pi sessions, or start a new session on Claude Code, Codex,
|
|
154
|
+
Antigravity, or Grok Build. The plugin copies Codex, Antigravity, and Grok Build keep
|
|
155
|
+
did not change in this release.
|
|
156
|
+
|
|
157
|
+
**To roll back**, put this line in `${XDG_CONFIG_HOME:-~/.config}/mirror/env` (in a
|
|
158
|
+
clone, its `.env`):
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
MEMORY_EXTRACTION_MODEL=google/gemini-2.5-flash-lite
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Then run `mirror runtime status`: the models line names Flash Lite with no reasoning
|
|
165
|
+
clause, which is the request it was always sent, and its calls stay priced. Rolling back
|
|
166
|
+
also brings back Flash Lite's upstream rate limits. Remove the line to return to the pin.
|
|
167
|
+
Nothing in the database changes either way.
|
|
168
|
+
|
|
169
|
+
## Conscious Exclusions
|
|
170
|
+
|
|
171
|
+
- One pin still serves every surface. The measurement favoured Luna for summaries and
|
|
172
|
+
Haiku for everything else; a model per surface is a Change Request, not this release.
|
|
173
|
+
- The ledger does not record which reasoning effort a call carried
|
|
174
|
+
([CR181](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr181-the-ledger-cannot-say-which-reasoning-effort-a-call-carried.md));
|
|
175
|
+
after the swap, the model column tells the two regimes apart in practice.
|
|
176
|
+
- No provider routing or fallback: the move retired Flash Lite's rate-limit exposure
|
|
177
|
+
for the pin, and a rolled-back install takes it back.
|
|
178
|
+
- The extraction eval's prompt hash still excludes the summary prompt its probe sends
|
|
179
|
+
([CR184](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr184-the-extraction-eval-s-prompt-hash-excludes-the-summary-prompt-its-probe-sends.md));
|
|
180
|
+
the `conversation_summary` module's hash covers it.
|
|
181
|
+
- A summary may still mention "the conversation" mid-text in about one of five
|
|
182
|
+
meta-heavy conversations; the guard watches the opening, where the framing lived.
|
|
183
|
+
- Summaries already stored are not rewritten.
|
|
184
|
+
- Found on the way and captured, not fixed: a test that writes a synthetic consult row
|
|
185
|
+
into the developer's real home on every `npm test` (CR182), and a Builder checkpoint
|
|
186
|
+
card that once listed a record that did not exist (CR183).
|
|
187
|
+
|
|
188
|
+
## What We Learned
|
|
189
|
+
|
|
190
|
+
An LLM judge can agree with itself and not with the person. TS2's judge was
|
|
191
|
+
self-consistent and agreed with the owner's blind rankings on 55% of pairwise orderings,
|
|
192
|
+
under the bar the plan had set, so the decision was made on the owner's rankings
|
|
193
|
+
instead. Calibrate a judge with the person on four to six conversations before the full
|
|
194
|
+
run; learning it afterwards cost six times as much. The playbook says so now.
|
|
195
|
+
|
|
196
|
+
A prompt rule that names the thing it forbids gets echoed. "Do not describe the
|
|
197
|
+
conversation; record what it established" came back as "What the conversation
|
|
198
|
+
established is…". The fixes that held said what to write, not what not to, and showed
|
|
199
|
+
one example instead of describing. And when the content itself is about conversations,
|
|
200
|
+
no wording removes the attractor; a guard in the system does, in one retry, if the retry
|
|
201
|
+
says what the first sentence must do.
|
|
202
|
+
|
|
203
|
+
The biggest lever on real summaries was not the prompt. It was a name the prompts had
|
|
204
|
+
never been given, found because a validation on a real conversation failed after 21
|
|
205
|
+
clean runs on a fixture that had no name and no meta content. A fixture measures what
|
|
206
|
+
it contains.
|
|
207
|
+
|
|
208
|
+
## Next Horizon
|
|
209
|
+
|
|
210
|
+
CV9, Mirror Mind 1.0, continues: Boundary Hardening and the rest of Stabilization, with
|
|
211
|
+
CV9.E2.S2, External Extension Runtime Surface Parity, the next pull Ariad recommends.
|
|
212
|
+
On 2026-11-04 the development home's ledger is read for the pin's reception latency,
|
|
213
|
+
30-day cost, and error count against the numbers the plan accepted. The Change Requests
|
|
214
|
+
this release left, CR181 to CR184, wait for their triggers. When CV9's done condition
|
|
215
|
+
is met, the release is `v1.0.0`.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.38.1 is a patch release of three Change Requests, CR185, CR186, and CR187, two of
|
|
4
|
+
them from Leandro Garcia's report on PR #42. Where Mirror refused to go on, it now
|
|
5
|
+
says what to do next. A package with no home configured names the homes it finds and
|
|
6
|
+
the `mirror init <name>` that selects one, and `init` adopts a home that already
|
|
7
|
+
exists. A missing or refused OpenRouter key is one line that names the file the key
|
|
8
|
+
goes in, said before anything is touched. A `stable` clone that took `main`'s commits
|
|
9
|
+
early is told that `stable` is behind it, not to push or reset. No migration.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# v0.38.1 — The Refusal Names the Way Out
|
|
13
|
+
|
|
14
|
+
**Date:** 2026-10-30
|
|
15
|
+
|
|
16
|
+
## Highlights
|
|
17
|
+
|
|
18
|
+
- **A package with no home configured says where it looks and what to run**
|
|
19
|
+
([CR185](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr185-a-package-with-no-home-configured-does-not-say-where-it-looks.md)).
|
|
20
|
+
The error names the homes it finds under `~/.mirror-minds`, the `mirror init <name>`
|
|
21
|
+
that selects one, and the file that command writes. `mirror init <name>` now adopts a
|
|
22
|
+
home that already exists, changing nothing in it, so moving from a clone to the
|
|
23
|
+
package is one command. The bare `mirror` names the home too.
|
|
24
|
+
- **A missing OpenRouter key stops the command before it touches anything**
|
|
25
|
+
([CR187](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr187-a-missing-openrouter-key-crashes-three-commands-with-a-stack-trace-that-names-no-file.md)).
|
|
26
|
+
The six commands that cannot work without it (`consult`, `journal`, `week plan`,
|
|
27
|
+
`descriptor generate`, `consolidate scan`, and `shadow scan`) refuse in one line that
|
|
28
|
+
says what was not done and where the key goes, and exit 2. `journal` says the entry
|
|
29
|
+
was not saved, instead of crashing after it had copied the database into a snapshot.
|
|
30
|
+
A key that OpenRouter refuses is one line too. `runtime status`, `runtime diagnose`,
|
|
31
|
+
and a degraded search say when the key is missing.
|
|
32
|
+
- **A `stable` clone ahead only by `main`'s commits is told that `stable` is behind it**
|
|
33
|
+
([CR186](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr186-runtime-update-reads-main-s-commits-as-local-work.md)).
|
|
34
|
+
The ways out are `runtime channel main` or the next release, in the words a package
|
|
35
|
+
install already used. It is no longer told to push commits the remote already has, or
|
|
36
|
+
to reset its code below a database that may be newer than `stable`'s. Real local work
|
|
37
|
+
keeps the push-or-reset refusal.
|
|
38
|
+
- No migration, and no model or prompt change.
|
|
39
|
+
|
|
40
|
+
## Where We Started
|
|
41
|
+
|
|
42
|
+
v0.33.3 fixed installs under a non-ASCII path, which Leandro Garcia reproduced on PR #42.
|
|
43
|
+
On 2026-10-17 he came back with two bumps he met while following the update
|
|
44
|
+
instructions:
|
|
45
|
+
|
|
46
|
+
- Moving to the package, `runtime update` stopped with "Mirror home is not configured"
|
|
47
|
+
and did not say where the package looks.
|
|
48
|
+
- On a clone, the release notes' manual step, `git pull --ff-only`, pulled `main` past
|
|
49
|
+
the release. The next `runtime update` refused with `local commits present` and
|
|
50
|
+
"push or reset", for commits that were not his.
|
|
51
|
+
|
|
52
|
+
The answer came on 2026-10-29, twelve days late. The v0.33.3 notes were corrected to
|
|
53
|
+
`git fetch origin` and `git merge --ff-only origin/stable`. CR185's review turned up a
|
|
54
|
+
third refusal of the same kind: with no `OPENROUTER_API_KEY`, `consult`, `journal`, and
|
|
55
|
+
`week plan` crashed with a stack trace that named no file, and `journal` lost the entry.
|
|
56
|
+
|
|
57
|
+
## What Changed
|
|
58
|
+
|
|
59
|
+
**A home the error can name (CR185).** One module now owns the homes root,
|
|
60
|
+
`~/.mirror-minds`, and decides what counts as a home there: a directory with a non-empty
|
|
61
|
+
`identity/` or a `memory*.db` file. The not-configured error is one line everywhere it
|
|
62
|
+
appears. It ends with the command that finishes the job:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
Mirror home is not configured. To use the home at <root>/<name>, run: mirror init <name> (it writes MIRROR_USER=<name> to <config>).
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
There are forms for several homes, for a home whose name `init` would refuse, and for
|
|
69
|
+
none. `init <name>` on an existing home copies nothing and changes nothing in it. It
|
|
70
|
+
writes `MIRROR_USER` to an owner-only config file when no home is selected, and prints
|
|
71
|
+
only what is left to do: the key, `mirror seed` when there is no database, and the
|
|
72
|
+
runtime wiring. Selecting the only home automatically was considered and refused. A
|
|
73
|
+
silent fallback once caused the wrong-session export incident, so one explicit command,
|
|
74
|
+
whose effect is kept in a file, is the honest minimum.
|
|
75
|
+
|
|
76
|
+
**A key checked before anything is touched (CR187).** The key used to be read at the
|
|
77
|
+
first model call, so a failing command could already have done work. A keyless
|
|
78
|
+
`journal` copied the whole database into its pre-write snapshot, then crashed. Now the
|
|
79
|
+
six commands that cannot work without the key check it before the database opens. Any
|
|
80
|
+
other command that meets a missing key is answered by the front door's handler: one
|
|
81
|
+
line, no stack trace. All the sentences come from one place and name the file the
|
|
82
|
+
install reads its configuration from. When that file does not exist yet, the line says
|
|
83
|
+
to create it readable only by its owner. A key that OpenRouter refuses (HTTP 401 or 403)
|
|
84
|
+
gets its own line, whether a chat call or an embedding call met it. `consolidate apply`
|
|
85
|
+
is not checked up front, because only a merge proposal embeds.
|
|
86
|
+
|
|
87
|
+
**The channel behind the clone (CR186).** The plan stage of `runtime update` counted
|
|
88
|
+
every commit in `origin/stable..HEAD` as the person's own. Releases only fast-forward
|
|
89
|
+
`stable` to `main`, so one more local count tells the two cases apart. When every commit
|
|
90
|
+
past `stable` is already on `origin/main`, the clone's channel is behind it. The update
|
|
91
|
+
then refuses the way the package lane refuses a channel behind the installed version,
|
|
92
|
+
and in its words: "A downgrade is refused: migrations do not run backwards.", then
|
|
93
|
+
`runtime channel main`, or staying until the next release. The captured Change Request
|
|
94
|
+
also offered a reset to `origin/stable`; it was dropped. `main`'s code can migrate the
|
|
95
|
+
database when it opens it, and `stable`'s core refuses a database newer than itself. One
|
|
96
|
+
function now reads where a clone stands, for both the update plan and the welcome card,
|
|
97
|
+
which had counted two different ways. A count that cannot run reads as local work, so
|
|
98
|
+
real work is never mistaken for published work.
|
|
99
|
+
|
|
100
|
+
**Reviewed before code, validated by hand.** Each plan was reviewed through persona
|
|
101
|
+
lenses before any code was written: five for CR185, four each for CR186 and CR187. The
|
|
102
|
+
Navigator validated each change on a natural route: CR185 and CR187 as a package user,
|
|
103
|
+
the lane Leandro was on, and CR186 in a scratch clone ahead of `stable`. CR186's reviews
|
|
104
|
+
turned its plan around. It had proposed reporting the clone as up to date, until the
|
|
105
|
+
engineer found the package lane already refusing the same state.
|
|
106
|
+
|
|
107
|
+
## Upgrading
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
mirror runtime update # or: npm install -g mirror-mind@latest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No migration, and nothing to configure. Moving from a clone to the package with a home
|
|
114
|
+
that already exists: `mirror init <your-name>` now selects it without changing anything
|
|
115
|
+
in it, and says what is left, such as the key. No skill, plugin, or extension changed in
|
|
116
|
+
this release, so a reinstall line the updater prints for Codex, Antigravity, or Grok
|
|
117
|
+
Build only moves the version number.
|
|
118
|
+
|
|
119
|
+
## Conscious Exclusions
|
|
120
|
+
|
|
121
|
+
- Transport failures other than a refused key (a timeout, an outage, a rate limit, a
|
|
122
|
+
server error) still crash the commands that call a model with a stack trace, and
|
|
123
|
+
`journal` still loses the entry
|
|
124
|
+
([CR188](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr188-a-provider-outage-crashes-the-commands-that-call-a-model-with-a-stack-trace.md)).
|
|
125
|
+
- Extraction at session end with no key is unchanged, and a configuration error still
|
|
126
|
+
counts as an attempt, which can quarantine a conversation
|
|
127
|
+
([CR178](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr178-a-configuration-error-counts-as-an-extraction-attempt-and-nothing-releases-a-quarantine.md)).
|
|
128
|
+
- With a wrong key, `descriptor generate` still prints "skipped (empty response)" for each
|
|
129
|
+
persona. That line is golden-graded.
|
|
130
|
+
- `runtime update --check` still reports `local_ahead` for a clone ahead of `stable` by
|
|
131
|
+
`main`'s commits, where a package reports `channel_behind`.
|
|
132
|
+
- A `main`-channel clone ahead by commits on another pushed branch keeps the
|
|
133
|
+
push-or-reset refusal.
|
|
134
|
+
|
|
135
|
+
## What We Learned
|
|
136
|
+
|
|
137
|
+
A validation route is code too, and it has to be run before it is handed over. CR186's
|
|
138
|
+
route, run first by the agent, never reached the plan: a `node_modules` symlink made the
|
|
139
|
+
scratch tree dirty, because `.gitignore`'s `node_modules/` matches only a directory.
|
|
140
|
+
CR187's first route could not prove that no snapshot was taken, because `ls -l` shows
|
|
141
|
+
minutes. Both were caught before the Navigator ran them.
|
|
142
|
+
|
|
143
|
+
CI runs the suite under `MEMORY_ENV=test`. CR185's first push failed a test that had
|
|
144
|
+
assumed `memory.db`. The suite is now run that way before every push.
|
|
145
|
+
|
|
146
|
+
The same state deserves the same words. CR186's answer was already written, in the
|
|
147
|
+
package lane's refusal of a channel behind the installed version.
|
|
148
|
+
|
|
149
|
+
## Next Horizon
|
|
150
|
+
|
|
151
|
+
CR188, the transport failures, is the next refusal of this kind. CV9, Mirror Mind 1.0,
|
|
152
|
+
continues, and when its done condition is met the release is `v1.0.0`.
|
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
metadataProfileAction,
|
|
28
28
|
titleNeedsImprovement,
|
|
29
29
|
} from "#conversation/metadataLifecycle.ts";
|
|
30
|
+
import { resolveUserName } from "#conversation/userName.ts";
|
|
30
31
|
import type { WritableDatabase } from "#db/database.ts";
|
|
31
32
|
import type { ExtractionMessage } from "#extraction/conversation.ts";
|
|
32
33
|
import {
|
|
@@ -163,7 +164,7 @@ export async function applyGeneratedMetadataLifecycle(
|
|
|
163
164
|
]),
|
|
164
165
|
);
|
|
165
166
|
|
|
166
|
-
const suggest = buildSuggesters(conversation, messages, options);
|
|
167
|
+
const suggest = buildSuggesters(db, conversation, messages, options);
|
|
167
168
|
|
|
168
169
|
let generatedTitle: string | null = null;
|
|
169
170
|
let generatedSummary: string | null = null;
|
|
@@ -315,7 +316,7 @@ export async function maybeGenerateTitle(
|
|
|
315
316
|
|
|
316
317
|
try {
|
|
317
318
|
const suggestion = await generateConversationTitle(deps.llm, messages, {
|
|
318
|
-
userName: deps.userName ??
|
|
319
|
+
userName: deps.userName ?? resolveUserName(db),
|
|
319
320
|
onLlmCall: (response, prompt) =>
|
|
320
321
|
deps.onLlmCall?.("conversation_title", response, conversation.id, prompt),
|
|
321
322
|
});
|
|
@@ -375,11 +376,14 @@ export function createCloseHooks(
|
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
function buildSuggesters(
|
|
379
|
+
db: WritableDatabase,
|
|
378
380
|
conversation: ConversationRow,
|
|
379
381
|
messages: ExtractionMessage[],
|
|
380
382
|
deps: CloseTailDeps,
|
|
381
383
|
) {
|
|
382
|
-
|
|
384
|
+
// CR180: the owner's name from the database unless the caller passed one, so the
|
|
385
|
+
// transcript these prompts see is labelled as the extraction path labels it.
|
|
386
|
+
const userName = deps.userName ?? resolveUserName(db);
|
|
383
387
|
const record = (role: string) => (response: LlmResponse, prompt: string) =>
|
|
384
388
|
deps.onLlmCall?.(role, response, conversation.id, prompt);
|
|
385
389
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { metadataDict } from "#conversation/closeTail.ts";
|
|
2
|
+
import { resolveUserName } from "#conversation/userName.ts";
|
|
2
3
|
import type { WritableDatabase } from "#db/database.ts";
|
|
3
4
|
import { embeddingToBytes } from "#db/decode.ts";
|
|
4
5
|
import { optionalString, requireString } from "#db/rowDecode.ts";
|
|
@@ -273,17 +274,6 @@ function getMessages(db: WritableDatabase, conversationId: string): ExtractionMe
|
|
|
273
274
|
.map((row) => ({ role: requireString(row, "role"), content: requireString(row, "content") }));
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
function resolveUserName(db: WritableDatabase): string {
|
|
277
|
-
const row = db
|
|
278
|
-
.prepare("SELECT content FROM identity WHERE layer = ? AND key = ? LIMIT 1")
|
|
279
|
-
.get("user", "identity");
|
|
280
|
-
const content = row ? optionalString(row, "content") : null;
|
|
281
|
-
const match = content?.match(
|
|
282
|
-
/(?:You are talking to|Você está falando com) ([A-Z][a-zA-Záéíóúãõ]+)/,
|
|
283
|
-
);
|
|
284
|
-
return match?.[1] ?? "User";
|
|
285
|
-
}
|
|
286
|
-
|
|
287
277
|
/**
|
|
288
278
|
* The close tail's two ledger hooks. Roles match Python's `build_llm_logger`
|
|
289
279
|
* roles so the `llm_calls` ledger agrees across engines; the orchestration's
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The owner's first name for the prompts the conversation lifecycle sends: memory
|
|
2
|
+
// and task extraction, and the close tail's title, summary, and tags (CR180).
|
|
3
|
+
//
|
|
4
|
+
// Ported from Python's `conversation.py` resolver, which matched only "You are
|
|
5
|
+
// talking to" and "Você está falando com". The shipped identity template writes
|
|
6
|
+
// "You are speaking with {{user_name}}", so against a seeded identity the port
|
|
7
|
+
// matched nothing and every prompt labelled the speaker `**User:**`, silently
|
|
8
|
+
// (CR014's finding, confirmed on the development home 2026-10-28). The template's
|
|
9
|
+
// phrasing is accepted here. The cultivation path keeps its own resolver
|
|
10
|
+
// (`cultivation/promptContext.ts`, fallback "the user"); unifying them is CR014's.
|
|
11
|
+
|
|
12
|
+
import type { Database } from "#db/database.ts";
|
|
13
|
+
import { optionalString } from "#db/rowDecode.ts";
|
|
14
|
+
|
|
15
|
+
/** Python's fallback when the row or the marker phrase is absent. */
|
|
16
|
+
export const FALLBACK_USER_NAME = "User";
|
|
17
|
+
|
|
18
|
+
/** One name token: Unicode letters, so an accented first name is whole (a plain `\w` stops at the accent). */
|
|
19
|
+
const MARKER =
|
|
20
|
+
/(?:You are (?:talking to|speaking with)|Você está (?:falando com|conversando com)) (\p{Lu}[\p{L}]*)/u;
|
|
21
|
+
|
|
22
|
+
/** The first name after the identity's marker phrase, or the fallback. */
|
|
23
|
+
export function userNameFromIdentity(content: string | null | undefined): string {
|
|
24
|
+
const match = content?.match(MARKER);
|
|
25
|
+
return match?.[1] ?? FALLBACK_USER_NAME;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read `user/identity` and resolve the name; the fallback when the row is absent.
|
|
30
|
+
* Fail-soft: a database without the table, or a read that throws, is the fallback
|
|
31
|
+
* too. The close tail runs inside the session-end hooks, where a throw becomes an
|
|
32
|
+
* extraction attempt and three attempts quarantine a conversation (CR178); a name
|
|
33
|
+
* is never worth that.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveUserName(db: Database): string {
|
|
36
|
+
try {
|
|
37
|
+
const row = db
|
|
38
|
+
.prepare("SELECT content FROM identity WHERE layer = ? AND key = ? LIMIT 1")
|
|
39
|
+
.get("user", "identity");
|
|
40
|
+
return userNameFromIdentity(row ? optionalString(row, "content") : null);
|
|
41
|
+
} catch {
|
|
42
|
+
return FALLBACK_USER_NAME;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -188,6 +188,40 @@ function pythonStr(value: unknown): string {
|
|
|
188
188
|
* Generate a 3-4 sentence summary. Returns "" on empty messages, provider
|
|
189
189
|
* failure, or trivial conversation.
|
|
190
190
|
*/
|
|
191
|
+
/**
|
|
192
|
+
* The opening the summary prompt forbids, as a model writes it (CR180): a first
|
|
193
|
+
* clause whose subject is the conversation itself rather than its content. One
|
|
194
|
+
* adjective is allowed between the article and the noun ("The Mirror conversation").
|
|
195
|
+
*/
|
|
196
|
+
const CONVERSATION_FRAME =
|
|
197
|
+
/^\s*(?:the|this)\s+(?:[\p{L}\d.]+\s+)?(?:conversation|discussion|session|exchange|chat|dialogue|transcript|user)\b/iu;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Appended to the prompt for the one regeneration (CR180): what the first sentence
|
|
201
|
+
* must do, said positively. It never quotes the frame it is replacing, because a
|
|
202
|
+
* quoted phrase comes back as the phrase.
|
|
203
|
+
*/
|
|
204
|
+
export const SUMMARY_RETRY_NOTE = `
|
|
205
|
+
|
|
206
|
+
Rewrite the summary. Its first sentence states the matter itself with its status, or begins with the person's name and what they decided or asked. Nothing in it describes what kind of text this is.`;
|
|
207
|
+
|
|
208
|
+
/** Whether a summary opens by describing the conversation instead of stating its content. */
|
|
209
|
+
export function opensWithConversationFrame(summary: string): boolean {
|
|
210
|
+
return CONVERSATION_FRAME.test(summary);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The summary, with one regeneration when it opens with the frame (CR180).
|
|
215
|
+
*
|
|
216
|
+
* After the prompt stopped naming the conversation, ordinary conversations opened
|
|
217
|
+
* clean 21 of 21 on the pin, but a conversation that is itself about sessions and
|
|
218
|
+
* roadmaps still opened "The conversation centers on…" about half the time, and a
|
|
219
|
+
* rule that names the frame gets echoed. So the guard is here, not in the words:
|
|
220
|
+
* a framed summary is generated once more, the prompt plus a positive retry note,
|
|
221
|
+
* and the second is kept only if it opens clean. Every call that happened reaches `onLlmCall`, so
|
|
222
|
+
* the ledger prices both. An empty summary (the trivial-conversation answer) and
|
|
223
|
+
* a failed second call keep the first result; failure never costs the summary.
|
|
224
|
+
*/
|
|
191
225
|
export async function generateConversationSummary(
|
|
192
226
|
llm: LlmProvider,
|
|
193
227
|
messages: readonly ExtractionMessage[],
|
|
@@ -195,12 +229,23 @@ export async function generateConversationSummary(
|
|
|
195
229
|
): Promise<string> {
|
|
196
230
|
if (messages.length === 0) return "";
|
|
197
231
|
const prompt = buildConversationSummaryPrompt(messages, options.userName ?? "User");
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
232
|
+
const once = async (sent: string): Promise<string | null> => {
|
|
233
|
+
let response: LlmResponse;
|
|
234
|
+
try {
|
|
235
|
+
response = await llm.complete({
|
|
236
|
+
role: "conversation_summary",
|
|
237
|
+
prompt: sent,
|
|
238
|
+
temperature: 0.3,
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
options.onLlmCall?.(response, sent);
|
|
244
|
+
return response.content.trim();
|
|
245
|
+
};
|
|
246
|
+
const first = await once(prompt);
|
|
247
|
+
if (first === null) return "";
|
|
248
|
+
if (!opensWithConversationFrame(first)) return first;
|
|
249
|
+
const second = await once(`${prompt}${SUMMARY_RETRY_NOTE}`);
|
|
250
|
+
return second !== null && second !== "" && !opensWithConversationFrame(second) ? second : first;
|
|
206
251
|
}
|
|
@@ -238,15 +238,20 @@ Example of the exact attack you must resist:
|
|
|
238
238
|
|
|
239
239
|
export const CONVERSATION_SUMMARY_PROMPT = `You are the memory system for Mirror Mind, a Jungian mirror AI.
|
|
240
240
|
|
|
241
|
-
Write
|
|
241
|
+
Write 3-4 sentences of flowing prose, not a list. It will be read on its own, months
|
|
242
|
+
from now, by someone deciding whether the full transcript is worth opening.
|
|
242
243
|
|
|
243
244
|
## Rules
|
|
244
245
|
|
|
245
|
-
- Open with the
|
|
246
|
+
- Open with the subject itself, carrying its real status: a decision as decided, a
|
|
247
|
+
question as open. "The move to Porto is set for March; what stays open is whether
|
|
248
|
+
to sell the car."
|
|
246
249
|
- Include the key decision, insight, or commitment reached, if any.
|
|
247
250
|
- Note emotional tone or psychological layer only when clearly present and significant.
|
|
248
251
|
- Standalone: a reader six months from now must understand what happened from the
|
|
249
|
-
summary alone.
|
|
252
|
+
summary alone. Write what is now known or decided, not that it was talked about.
|
|
253
|
+
Call the person by the name the transcript gives them. Do not write "we discussed",
|
|
254
|
+
"the user said", "the conversation", or "the discussion".
|
|
250
255
|
- If the conversation is trivial (greetings, scheduling, one-line exchange), return
|
|
251
256
|
an empty string and nothing else.
|
|
252
257
|
|