mirror-mind 0.35.0 → 0.36.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/AGENTS.md +2 -2
- package/README.md +3 -3
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.36.0.md +222 -0
- package/docs/releases/v0.36.1.md +137 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/plugin.json +11 -1
- package/plugins/mirror-mind/rules/AGENTS.md +2 -2
- package/ts/src/conversation/loggerRuntime.ts +5 -18
- package/ts/src/conversation/runtimePolicy.ts +26 -3
- package/ts/src/conversation/sessionComposites.ts +5 -7
- package/ts/src/conversation/transcriptBackfill.ts +5 -1
- package/ts/src/db/migrations.ts +4 -7
- package/ts/src/extensions/bindings.ts +53 -2
- package/ts/src/extensions/catalogCommands.ts +82 -23
- package/ts/src/extensions/catalogWrites.ts +12 -0
- package/ts/src/extensions/contextRuntime.ts +36 -1
- package/ts/src/extensions/dispatch.ts +43 -10
- package/ts/src/extensions/extensionPlugin.ts +325 -0
- package/ts/src/extensions/manifest.ts +7 -1
- package/ts/src/extensions/settings.ts +147 -0
- package/ts/src/frontDoor/cli.ts +16 -1
- package/ts/src/frontDoor/extensionCatalogRoute.ts +8 -2
- package/ts/src/frontDoor/runtimeRoute.ts +11 -0
- package/ts/src/guards/claudePlugin.ts +24 -7
- package/ts/src/guards/retiredSurfaces.ts +42 -5
- package/ts/src/hooks/claude.ts +6 -0
- package/ts/src/hooks/main.ts +12 -1
- package/ts/src/runtime/brokenPipe.ts +52 -0
- package/ts/src/runtime/diagnose.ts +122 -13
- package/ts/src/runtime/extensionPluginWiring.ts +222 -0
- package/ts/src/runtime/pluginWiring.ts +168 -29
- package/ts/src/runtime/status.ts +28 -1
- package/ts/src/runtime/update.ts +25 -11
- package/ts/src/runtime/wiring.ts +135 -3
- package/ts/src/conversation/backfill.ts +0 -271
- package/ts/src/conversation/sessionImport.ts +0 -90
package/AGENTS.md
CHANGED
|
@@ -18,8 +18,8 @@ Applies to all sessions, regardless of project.
|
|
|
18
18
|
The mirror operates in four modes, chosen automatically based on context:
|
|
19
19
|
Mirror, Builder, Explorer, and Soul.
|
|
20
20
|
|
|
21
|
-
Each skill below is named three ways: `/mm-<skill>` in Pi
|
|
22
|
-
Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
|
|
21
|
+
Each skill below is named three ways: `/mm-<skill>` in Pi and Grok Build,
|
|
22
|
+
`$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
|
|
23
23
|
name — `/mirror-mind:mm:<skill>` in Claude Code's menu and in Antigravity,
|
|
24
24
|
`$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
|
|
25
25
|
name — and the forms below refer to the same skill.
|
package/README.md
CHANGED
|
@@ -60,8 +60,8 @@ This is not a chatbot with memory. This is a mirror — and yours.
|
|
|
60
60
|
|
|
61
61
|
## How It Works
|
|
62
62
|
|
|
63
|
-
Local-first. SQLite.
|
|
64
|
-
Antigravity. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
|
|
63
|
+
Local-first. SQLite. Five supported runtimes: Pi, Claude Code, Codex,
|
|
64
|
+
Antigravity, Grok Build. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
|
|
65
65
|
Personas, Shadow.
|
|
66
66
|
Context-activated modes — Mirror, Builder, Explorer, and Soul. The identity lives
|
|
67
67
|
in your machine, not a server.
|
|
@@ -110,7 +110,7 @@ this repository, turning a personal experiment into a reusable framework:
|
|
|
110
110
|
|
|
111
111
|
- **English** — full migration from Portuguese across code, CLI, schema, identity, and docs
|
|
112
112
|
- **Framework/user separation** — user identity and memory live privately under `~/.mirror/<user>/`, outside the repository
|
|
113
|
-
- **Multi-runtime** — from Claude Code only to Pi, Claude Code, Codex, and
|
|
113
|
+
- **Multi-runtime** — from Claude Code only to Pi, Claude Code, Codex, Antigravity, and Grok Build
|
|
114
114
|
- **Multi-user** — independent mirrors for multiple users on the same machine
|
|
115
115
|
- **Multi-session** — concurrent sessions tracked safely in a database-backed session registry
|
|
116
116
|
- **Intelligence depth** — hybrid semantic and lexical search, honest reinforcement, two-pass extraction, memory consolidation, and shadow cultivation
|
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.36.1 — A Home Keeps Only Its Own Pi Sessions](v0.36.1.md) — patch release of one Change Request, CR153: session maintenance imports no Pi session, so the Mirror homes on one machine no longer copy each other's Pi conversations; a home's Pi conversations are the ones the Pi extension logged into it. `PI_SESSIONS_DIR` is inert. Conversations an earlier version imported stay, and the note shows how to list them. Update every install that has a home. No migration.
|
|
16
|
+
- [v0.36.0 — Five Runtimes, and a Home's Extensions on Every One](v0.36.0.md) — minor release closing CV21.DS10: Grok Build is the fifth runtime, at its honest L3 through the canonical plugin in two printed steps, measured live on grok 1.0.62. With it, CV21.DS5.US2: a home's installed extensions reach Claude Code, Codex, Antigravity, and Grok Build as one generated plugin per home, chosen by measurement on all four. Three Change Requests found by use: an extension's settings file (`extensions/<id>.env`, merged under the environment for the one spawned process), a quiet exit on a closed pipe, and a `bind` that refuses what it cannot bind. No migration.
|
|
15
17
|
- [v0.35.0 — Four Runtimes, One Successor](v0.35.0.md) — minor release closing CV21.DS8: the Gemini CLI integration no longer ships and Antigravity stands in its place, the gate met by DS7's live L4 on agy 1.4.2. Four runtimes — Pi, Claude Code, Codex, Antigravity — named everywhere; the cutoff recorded in full, with the four hook entries a Gemini CLI user deletes and v0.34.0 as the last release that carries the integration; a retired-surface guard keeps it removed. No migration.
|
|
16
18
|
- [v0.34.0 — Codex Through the Canonical Plugin](v0.34.0.md) — minor release closing CV21.DS4: Codex runs Mirror through the canonical plugin, installed in two commands from a marketplace the package ships, measured live at L4 on Codex 0.163.0. The v0.32.1 wrapper wiring no longer ships; `runtime diagnose` and `runtime update` say when Codex's cached plugin copy is behind and name the re-add that brings it level. No migration.
|
|
17
19
|
- [v0.33.4 — A Name Is a Key](v0.33.4.md) — patch release of three first-run corrections: two from a contributor's PR #43 and CR141, which its review opened. `init --help` created a home named `--help`, `init ../escaped` created one outside `.mirror-minds`, and `init "My User"` put a space into `MIRROR_USER`; a user name is now a kebab-case key like a journey slug or a persona id, refused otherwise with a name that would work and nothing created. `identity edit` on Windows opens Notepad when no editor is named. Existing homes untouched. No migration.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.36.0 is a minor release closing CV21.DS10: Grok Build is Mirror Mind's
|
|
4
|
+
fifth runtime, at its honest L3 through the canonical plugin, in two steps
|
|
5
|
+
`mirror init` prints and `runtime diagnose` understands. With it ships
|
|
6
|
+
CV21.DS5.US2: a home's installed extensions reach Claude Code, Codex,
|
|
7
|
+
Antigravity, and Grok Build as one generated plugin per home, the shape
|
|
8
|
+
DS5.TS1 chose by measurement on all four. Three Change Requests ride along,
|
|
9
|
+
each found by using the product: an extension's installation settings get a
|
|
10
|
+
file Mirror reads for them (`extensions/<id>.env`), a closed pipe ends a
|
|
11
|
+
command quietly instead of with a stack trace, and `bind` refuses what it
|
|
12
|
+
cannot bind instead of writing a row that does nothing. No migration.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# v0.36.0 — Five Runtimes, and a Home's Extensions on Every One
|
|
16
|
+
|
|
17
|
+
**Date:** 2026-10-20
|
|
18
|
+
|
|
19
|
+
## Highlights
|
|
20
|
+
|
|
21
|
+
- **Grok Build is the fifth runtime** (CV21.DS10). Two printed steps — a
|
|
22
|
+
trusted plugin install and a home rule for the Operating Instructions — and
|
|
23
|
+
Grok has Mirror's skills, its hooks, and its MCP server. Measured live on
|
|
24
|
+
grok 1.0.62: the rule reaches the model, `/mm-mirror` answers signed by a
|
|
25
|
+
persona, every turn is logged as `grok_build`, the close fits Grok's 1.5 s
|
|
26
|
+
bound. Honest ceiling, written where a user reads it: Grok adds nothing to
|
|
27
|
+
the model's context per turn, so Mirror Mode lasts as long as the skill's
|
|
28
|
+
own instructions do.
|
|
29
|
+
- **A home's extensions reach every plugin runtime** (CV21.DS5.US2).
|
|
30
|
+
`mirror extensions install` and `uninstall` regenerate one small plugin per
|
|
31
|
+
Mirror home, `runtime/plugin/mirror-ext-<user>/`, and print the one wiring
|
|
32
|
+
step a runtime on the `PATH` still needs. Chosen over two other shapes by a
|
|
33
|
+
measured table on claude 2.1.296, codex 0.163.0, agy 1.4.2, and grok 1.0.62
|
|
34
|
+
([decision](../project/decisions.md#user-extensions-reach-every-runtime-as-a-per-home-plugin)).
|
|
35
|
+
`runtime diagnose` names a stale copy and the command that brings it level.
|
|
36
|
+
- **An extension's installation settings have a home**
|
|
37
|
+
([CR145](../project/refinement/rs011-extension-contract-completeness/cr145-an-extension-s-installation-settings-have-nowhere-to-live-but-the-login-shell.md)).
|
|
38
|
+
`~/.mirror-minds/<user>/extensions/<id>.env`, merged *under* the environment
|
|
39
|
+
for the one process Mirror spawns — subcommand or context provider — and
|
|
40
|
+
nothing else. The reason it had to be Mirror: Node reads
|
|
41
|
+
`NODE_EXTRA_CA_CERTS` before any extension code runs, so only the spawner can
|
|
42
|
+
set it, and the alternative was the login shell, where it broke every other
|
|
43
|
+
Postgres client on the machine.
|
|
44
|
+
- **Two front-door manners** ([CR148](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr148-the-front-door-dies-with-a-stack-trace-when-its-stdout-closes-early.md),
|
|
45
|
+
[CR149](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr149-bind-accepts-a-persona-or-journey-that-does-not-exist.md)).
|
|
46
|
+
`mirror … | head -1` exits 0 in silence instead of dying of `EPIPE` with a
|
|
47
|
+
stack trace — on the hook entry too, where exit 1 was the one code it
|
|
48
|
+
promised never to return. And `mirror ext <id> bind` checks the extension,
|
|
49
|
+
the capability, and the persona or journey before writing a row, refusing
|
|
50
|
+
with the fix in the line: `no persona 'engine' (mirror list personas shows
|
|
51
|
+
this home's)`.
|
|
52
|
+
|
|
53
|
+
## Where We Started
|
|
54
|
+
|
|
55
|
+
v0.35.0 closed CV21's runtime set at four — Pi, Claude Code, Codex,
|
|
56
|
+
Antigravity — with Grok Build and user extensions named as what remained.
|
|
57
|
+
Grok had been reading Mirror's skills by accident since DS12, through its
|
|
58
|
+
Claude compatibility, with no hooks and no logging, and DS12.TS4 had left the
|
|
59
|
+
MCP attachment as a contradiction: the server ready with its seven tools in
|
|
60
|
+
Grok's own view, every session created with `mcp_server_count: 0`. User
|
|
61
|
+
extensions reached Claude Code through `expose-claude` and Pi through its
|
|
62
|
+
catalog, and nothing else; DS12.TS5 had measured that and left the exposure
|
|
63
|
+
as a choice among three shapes, deciding none.
|
|
64
|
+
|
|
65
|
+
Then the product was used. An extension built for a private journey needed a
|
|
66
|
+
TLS mode and a certificate that belonged to the machine, and the only place
|
|
67
|
+
the contract left for them was `~/.zshrc`. A mistyped `tail -` produced a
|
|
68
|
+
Node stack trace from the front door. A mistyped persona was bound without a
|
|
69
|
+
word. Three Change Requests, two days.
|
|
70
|
+
|
|
71
|
+
## What Changed
|
|
72
|
+
|
|
73
|
+
**Grok Build, in four stories on one day** (2026-10-18, grok 1.0.62).
|
|
74
|
+
CV21.DS10.US1 measured five install shapes and chose two printed steps:
|
|
75
|
+
`grok plugin install "<pkg>/plugins/mirror-mind" --trust`, and the Operating
|
|
76
|
+
Instructions linked as a rule at `~/.grok/rules/mirror-mind.md` — Grok shows a
|
|
77
|
+
session-start hook's context on screen only, so the rule is how the
|
|
78
|
+
instructions reach the model. US1 also found that the root `plugin.json`
|
|
79
|
+
marker DS7 had added was hiding the plugin's version and MCP server from
|
|
80
|
+
Grok; it now carries both, one server on all four plugin runtimes. TS2
|
|
81
|
+
closed DS12's contradiction: the server attaches, its tools sit behind Grok's
|
|
82
|
+
`search_tool` and `use_tool` meta-tools (as `mirror-mind__list_journeys`), and
|
|
83
|
+
the zero count was a snapshot taken while the server was `pending`. US3
|
|
84
|
+
measured L3 with an ablation — remove the rule and the model loses the
|
|
85
|
+
instructions; the hook canaries never reached it — and the deferred close
|
|
86
|
+
tail finished at the next start, DS4.TS2's design on Grok's bound. TS4 wrote
|
|
87
|
+
it: the contract's Grok reference implementation, README, Getting Started,
|
|
88
|
+
REFERENCE, and the Operating Instructions name Grok Build as the fifth
|
|
89
|
+
runtime with its two steps and its ceiling. Paid on the way, CR143: the
|
|
90
|
+
inject hook returns at once on Grok instead of spending 2–3 s per load on a
|
|
91
|
+
context Grok discards.
|
|
92
|
+
|
|
93
|
+
**User extensions, chosen then built** (2026-10-19). CV21.DS5.TS1 measured
|
|
94
|
+
the three shapes DS12.TS5 had left — user-level skill roots, a per-home
|
|
95
|
+
generated plugin, MCP prompts — in scratch homes on all four runtimes, each
|
|
96
|
+
cell read by the runtime's own listing and then by one headless turn asking
|
|
97
|
+
for a canary, with every tool call classified so a find reached by searching
|
|
98
|
+
the disk did not count. Only the per-home plugin qualified on all four: the
|
|
99
|
+
skill roots are three roots that no runtime set reads in common, and MCP
|
|
100
|
+
prompts attached everywhere and reached no model. US2 built it.
|
|
101
|
+
`mirror extensions install`, `uninstall`, and `sync` regenerate
|
|
102
|
+
`runtime/plugin/mirror-ext-<user>/` in the canonical plugin's format,
|
|
103
|
+
version `0.1.<generation>`, carrying each extension's `claude` skill as its
|
|
104
|
+
manifest already declares it — no manifest rewritten. The install prints what
|
|
105
|
+
a runtime on the `PATH` still needs: a link for Claude Code, two commands for
|
|
106
|
+
Codex, one for Antigravity, one with `--trust` for Grok (and the line that
|
|
107
|
+
there is nothing for `--trust` to activate). After a change, the three
|
|
108
|
+
runtimes that copy a plugin hold a stale copy until one command each, which
|
|
109
|
+
the verbs print and `runtime diagnose` raises as five `ext_plugin_*` findings,
|
|
110
|
+
each with its repair. Two homes wire beside each other. `runtime status`
|
|
111
|
+
gained `Extensions plugin: mirror-ext-<user> <version> (<n> skills)`.
|
|
112
|
+
|
|
113
|
+
**The settings file** (CR145). `mirror-cli-v1` handed a subcommand the five
|
|
114
|
+
`MIRROR_*` variables and whatever environment `mirror` had; `mirror-context-v1`
|
|
115
|
+
handed a provider the environment untouched. Now both read
|
|
116
|
+
`extensions/<id>.env` — dotenv, the parser behind Mirror's own `.env` — and
|
|
117
|
+
merge it *under* the process environment: an explicit export still wins, the
|
|
118
|
+
five `MIRROR_*` names can never come from the file, and the result reaches
|
|
119
|
+
that one process. The file sits beside the installed directory, not inside
|
|
120
|
+
it: `install` rewrites the directory, and in a development home the directory
|
|
121
|
+
is a symlink into the extension's own repository. `mirror ext <id>` ends its
|
|
122
|
+
listing with the file's absolute path and one of four states — the variable
|
|
123
|
+
*names* it carries, `no variables`, `not present` with what to do, or
|
|
124
|
+
`unreadable` with the reason — and says when other users can read it. A file
|
|
125
|
+
that exists and cannot be read refuses the subcommand (one line, exit 1,
|
|
126
|
+
nothing spawned) and skips the provider with a `settings_unreadable`
|
|
127
|
+
diagnostic. A full `uninstall` keeps the file and says so. The contract
|
|
128
|
+
reference has the rules in full.
|
|
129
|
+
|
|
130
|
+
**A closed pipe** (CR148). One listener at each entry — `mirror` and
|
|
131
|
+
`mirror-hook` — on `EPIPE`: exit 0, write nothing. `npm`, `cargo`, and
|
|
132
|
+
`kubectl`'s choice over git's 141, because under `set -o pipefail` 141 fails a
|
|
133
|
+
pipeline a person wrote on purpose. Anything that is not `EPIPE` is rethrown.
|
|
134
|
+
The hook leaves one line in `hooks.log` first, `stdout closed early`, because
|
|
135
|
+
its try/catch could never see a stream's `error` event, and a runtime that
|
|
136
|
+
gives up on a hook closes its pipe. Stop means stop: work after the reader
|
|
137
|
+
closes is abandoned, as `SIGPIPE` abandons git's.
|
|
138
|
+
|
|
139
|
+
**`bind` checks what it binds** (CR149). Four checks before the one insert,
|
|
140
|
+
outside in: the id against the manifest's grammar, the extension installed,
|
|
141
|
+
the capability declared (the validator first, then the context reader for the
|
|
142
|
+
list), the persona or journey present in the identity rows a load is later
|
|
143
|
+
matched against. Each refusal is one line on stdout at exit 1 and writes no
|
|
144
|
+
row. `unbind` and `bindings` are untouched: removing a row that points at
|
|
145
|
+
nothing is the repair. A deliberate divergence from the Python oracle; the
|
|
146
|
+
bindings golden kept every byte, with its world given what the checks need.
|
|
147
|
+
|
|
148
|
+
## Upgrading
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install -g mirror-mind@latest # or: mirror runtime update
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
No migration. Existing homes, sessions, memories, and bindings are untouched.
|
|
155
|
+
|
|
156
|
+
**Grok Build:** `mirror init` prints the two steps when it finds `grok` on
|
|
157
|
+
your `PATH`; Getting Started's Grok Build section has them with what they
|
|
158
|
+
cost. If you had wired Claude Code, Grok already read the skills from that
|
|
159
|
+
link; after the plugin install each skill appears twice in Grok's menu, and
|
|
160
|
+
the hooks run once.
|
|
161
|
+
|
|
162
|
+
**If you have extensions installed:** the per-home plugin is generated by the
|
|
163
|
+
next `extensions` verb, not by the update. Re-install any one extension —
|
|
164
|
+
`mirror extensions install <id> --extensions-root <root>` from its source, or
|
|
165
|
+
from the home's own `~/.mirror-minds/<user>/extensions` — and the report
|
|
166
|
+
prints the one wiring step each runtime on your `PATH` still needs. Pi needs
|
|
167
|
+
nothing; its catalog path is unchanged.
|
|
168
|
+
|
|
169
|
+
**If an extension of yours reads machine settings from your shell profile:**
|
|
170
|
+
move them to `~/.mirror-minds/<user>/extensions/<id>.env`, `chmod 600` it,
|
|
171
|
+
and `mirror ext <id>` will show the keys it carries. Then delete the exports
|
|
172
|
+
— they reached every other program you run.
|
|
173
|
+
|
|
174
|
+
## Conscious Exclusions
|
|
175
|
+
|
|
176
|
+
- **DS5 is not closed.** US3 — `review-copy` invoked live on each runtime
|
|
177
|
+
from the shipped package, with version, binary, and date — and TS4 — the
|
|
178
|
+
contract, extension docs, Getting Started, and REFERENCE saying how an
|
|
179
|
+
extension reaches each runtime — remain. Until TS4, the install's printed
|
|
180
|
+
steps and the contract's per-home plugin section are the documentation.
|
|
181
|
+
- **DS11**, the contract and docs refresh that folds all five runtimes into
|
|
182
|
+
one parity matrix, is not this release's.
|
|
183
|
+
- **An extension cannot yet declare the settings it expects** (CR146), so the
|
|
184
|
+
settings file is written from the extension's README, not from a
|
|
185
|
+
checklist Mirror prints. **`install` shows `suggested_personas` the home does
|
|
186
|
+
not have as if it did** (CR150). **Three copies of the extension-id grammar
|
|
187
|
+
remain, two of them disagreeing** (CR147); CR145 and CR149 used the
|
|
188
|
+
manifest's. All three captured, none done.
|
|
189
|
+
- **Still open from earlier:** the plugin smoke's negative probe on a machine
|
|
190
|
+
with a global `mirror-hook` (CR142); the DS7 routes removing agy's credential
|
|
191
|
+
where 1.3.x kept it (CR144).
|
|
192
|
+
- **141** as the exit on a closed pipe, and `EPIPE` handling inside extension
|
|
193
|
+
subcommands: not chosen, recorded in CR148.
|
|
194
|
+
|
|
195
|
+
## What We Learned
|
|
196
|
+
|
|
197
|
+
Twice in one day a validation route had to be shown failing before it could
|
|
198
|
+
be trusted passing. CR145's first run validated nothing: the `mirror` on the
|
|
199
|
+
Navigator's `PATH` was the published 0.35.0, and the shell wrapped it in the
|
|
200
|
+
very function the CR retires — the workaround, measured where it lived. CR148's
|
|
201
|
+
planned control, `| head -1`, could not fail: a 2 KB listing lands whole in
|
|
202
|
+
the pipe buffer before `head` quits, so there was never an `EPIPE` to handle;
|
|
203
|
+
the reproduction is a reader that closes without reading. A route that names
|
|
204
|
+
the binary by path and a control that has been seen red are now part of what
|
|
205
|
+
a Change Request's validation means.
|
|
206
|
+
|
|
207
|
+
The three CRs came from using the product, not from reading it: a private
|
|
208
|
+
journey's extension, a typo, another typo. The review that preceded each —
|
|
209
|
+
five lenses on CR145, three on CR148, four on CR149 — changed each plan
|
|
210
|
+
before a line was written, and the things it added (a result type instead of
|
|
211
|
+
a throw, a diagnostic kind instead of a generic failure, a check zero on the
|
|
212
|
+
id grammar) are the parts a future reader will be glad of.
|
|
213
|
+
|
|
214
|
+
## Next Horizon
|
|
215
|
+
|
|
216
|
+
DS5.US3 is the recommended pull: the live proof of `review-copy` on all four
|
|
217
|
+
plugin runtimes from the shipped package, through the steps this release
|
|
218
|
+
prints. TS4 writes what US3 measures. DS11 then folds five runtimes into one
|
|
219
|
+
contract and one matrix, and CV21 closes. RS011 — extension contract
|
|
220
|
+
completeness — holds the gaps the contract has, found one extension at a time;
|
|
221
|
+
CR146, a manifest that declares the settings it expects, is its next natural
|
|
222
|
+
candidate.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.36.1 is a patch release of one Change Request, CR153. Session
|
|
4
|
+
maintenance used to read Pi's session directory and import every session a
|
|
5
|
+
home did not already track, so on a machine with more than one Mirror home
|
|
6
|
+
each home copied the other's Pi conversations and titled them again through
|
|
7
|
+
the provider. Maintenance now imports no Pi session: a home's Pi
|
|
8
|
+
conversations are the ones the Pi extension logged into it, as they
|
|
9
|
+
happened, and `PI_SESSIONS_DIR` is no longer read. Conversations an earlier
|
|
10
|
+
version imported stay where they are, and this note shows how to list them.
|
|
11
|
+
Update every install that has a home. No migration.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v0.36.1 — A Home Keeps Only Its Own Pi Sessions
|
|
15
|
+
|
|
16
|
+
**Date:** 2026-10-21
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- **Session maintenance imports no Pi session**
|
|
21
|
+
([CR153](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr153-a-home-s-pi-backfill-imports-another-home-s-sessions.md)).
|
|
22
|
+
It used to read every file in Pi's session directory and import each one the home
|
|
23
|
+
did not track, every session another Mirror home on the same machine had logged
|
|
24
|
+
among them. A home's Pi conversations are now exactly the ones the Pi extension
|
|
25
|
+
logged into it, as they happened.
|
|
26
|
+
- **Nothing in Mirror reads Pi's session files.** Not your history from before Mirror,
|
|
27
|
+
not sessions you run without it, and not another home's. `PI_SESSIONS_DIR` is inert:
|
|
28
|
+
a `.env` that sets it keeps working, and it changes nothing.
|
|
29
|
+
- **What was already imported stays.** This release removes the cause, not the copies;
|
|
30
|
+
[Upgrading](#upgrading) shows how to list them.
|
|
31
|
+
- **Update every install that has a home.** A home is safe from a neighbour's
|
|
32
|
+
maintenance only once the install that neighbour runs carries this release.
|
|
33
|
+
|
|
34
|
+
## Where We Started
|
|
35
|
+
|
|
36
|
+
The import is older than the idea of two homes on one machine. It existed to fill a
|
|
37
|
+
gap: a Pi session the extension never logged reached the home anyway, at the next
|
|
38
|
+
maintenance. v0.32.0 narrowed it to sessions newer than the home, so a new home would
|
|
39
|
+
not swallow a lifetime of Pi history. Nothing scoped it to the home.
|
|
40
|
+
|
|
41
|
+
CV21.DS5.US3's live route exposed it. A Claude Code cell ran on the real `HOME` against
|
|
42
|
+
a scratch home born that evening, and two seconds into its first session, maintenance
|
|
43
|
+
imported four of the person's Pi sessions from that day, from both of their real homes,
|
|
44
|
+
and titled them through the provider. The route closed its own hole the same evening;
|
|
45
|
+
the product's was CR153.
|
|
46
|
+
|
|
47
|
+
Measured before planning, read-only, on the maintainer's two homes, one a copy of the
|
|
48
|
+
other since July: each had taken the other's Pi conversations all along, 543 between
|
|
49
|
+
them, and both were still doing it. Titling them took 411 provider calls. No memory
|
|
50
|
+
came from any of them, because extraction needs a journey and an import carries none,
|
|
51
|
+
but every one listed as the newest conversation, since an import's start is the moment
|
|
52
|
+
it was imported. The gap the import existed to fill came up once in three months.
|
|
53
|
+
|
|
54
|
+
## What Changed
|
|
55
|
+
|
|
56
|
+
**The decision.** Three ways out were weighed: a claim recorded at session start, so a
|
|
57
|
+
home imports only sessions it saw begin; a mark written into each Pi session file, so a
|
|
58
|
+
home imports only files carrying its name; or no import at all. The claim would have
|
|
59
|
+
kept the one recovery in three months for a table, a migration, and a claim lifecycle.
|
|
60
|
+
The mark would have left every home reading every other home's files at every start.
|
|
61
|
+
Retiring the import is the only option in which no home reads Pi's files at all, and
|
|
62
|
+
it gives up almost nothing
|
|
63
|
+
([decision](../project/decisions.md#session-maintenance-imports-no-pi-session-a-home-holds-the-pi-conversations-it-logged-live)).
|
|
64
|
+
|
|
65
|
+
**The change.** The Pi backfill, the import seam only it still used, and v0.32.0's
|
|
66
|
+
birth floor are deleted, and a retired-surface guard keeps them deleted. The tests came
|
|
67
|
+
first: three cases — a session in Pi's default directory, one under `PI_SESSIONS_DIR`,
|
|
68
|
+
and the full `session-start` the SessionStart hooks run — failed on the old code, each
|
|
69
|
+
importing a session another home had logged, and pass now. The Navigator then ran the
|
|
70
|
+
published 0.36.0 beside the change on the same session: the old version took it and
|
|
71
|
+
the new one did not. On the real Pi history the new one took nothing, and a live Pi
|
|
72
|
+
session against a scratch home was logged as before.
|
|
73
|
+
|
|
74
|
+
### What you will notice
|
|
75
|
+
|
|
76
|
+
- The maintenance report has four lines: `Closed stale conversations`, `Retitled pending
|
|
77
|
+
conversations`, `Extracted pending conversations`, and `Finished deferred close tails`.
|
|
78
|
+
- If you have run more than one home on this machine, each may hold Pi conversations
|
|
79
|
+
the other logged. They are not removed.
|
|
80
|
+
- A Pi session you run without Mirror's extension never appears in Mirror. Run Pi with
|
|
81
|
+
the Mirror package, and its sessions are logged as they happen.
|
|
82
|
+
|
|
83
|
+
## Upgrading
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mirror runtime update # or: npm install -g mirror-mind
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Update every install that has a home: the package, and each clone. There is no
|
|
90
|
+
database migration, and no runtime wiring changes.
|
|
91
|
+
|
|
92
|
+
To list the Pi conversations an earlier version imported into a home, read-only:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
sqlite3 ~/.mirror-minds/<user>/memory.db \
|
|
96
|
+
"SELECT id, title, started_at FROM conversations
|
|
97
|
+
WHERE interface = 'pi' AND julianday(started_at) >= julianday(ended_at)
|
|
98
|
+
ORDER BY started_at"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
An import gives itself away: it started when it was imported, after its last message.
|
|
102
|
+
The home's dated backups and its pre-write backup hold the same rows.
|
|
103
|
+
|
|
104
|
+
## Conscious Exclusions
|
|
105
|
+
|
|
106
|
+
- **Removing what was imported.** A command that lists and removes those conversations
|
|
107
|
+
is [CR154](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr154-a-home-cannot-list-or-remove-the-pi-conversations-it-imported.md).
|
|
108
|
+
- **A session whose logging never reached the core** is now simply missing from the
|
|
109
|
+
home, with an error line in `mirror-logger.log` and nowhere else. `runtime diagnose`
|
|
110
|
+
reading those lines is
|
|
111
|
+
[CR155](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr155-runtime-diagnose-does-not-read-the-pi-extension-s-error-lines.md).
|
|
112
|
+
- **A home made by copying another's database** is the same home twice — the same ids,
|
|
113
|
+
the same history. The docs saying so is
|
|
114
|
+
[CR156](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr156-a-home-made-by-copying-another-s-database-is-the-same-home-twice.md).
|
|
115
|
+
- **Importing a Pi session on request.** If it is wanted, it is a feature with its own
|
|
116
|
+
story, not something maintenance does in the background.
|
|
117
|
+
- **The transcript backfill** matches a session's replies by time window — the same
|
|
118
|
+
shape, inside one home. That is
|
|
119
|
+
[CR151](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr151-the-transcript-backfill-writes-a-session-s-replies-into-other-sessions-conversations.md),
|
|
120
|
+
its own change.
|
|
121
|
+
|
|
122
|
+
## What We Learned
|
|
123
|
+
|
|
124
|
+
The import decided ownership from an absence: a session the home did not track was the
|
|
125
|
+
home's to take. That was true while a machine had one home, and it stopped being true
|
|
126
|
+
on the day a second home appeared, with nothing to mark the day.
|
|
127
|
+
|
|
128
|
+
The leak was also invisible to the check that should have caught it. The first look at
|
|
129
|
+
the real homes asked by journey, and an import has no journey. Measured by construction
|
|
130
|
+
instead — an import starts after its own last message — the same homes held 543. A
|
|
131
|
+
measurement taken before planning turned "consider pulling it first" into a fact about
|
|
132
|
+
the maintainer's own data.
|
|
133
|
+
|
|
134
|
+
## Next Horizon
|
|
135
|
+
|
|
136
|
+
CR154: a home lists, and on request removes, the Pi conversations an earlier version
|
|
137
|
+
imported. Then CV21.DS5.TS4 and DS11, and CV21's own release.
|
package/package.json
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirror-mind",
|
|
3
|
-
"description": "Mirror Mind — local-first memory and identity for agentic runtimes."
|
|
3
|
+
"description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
|
|
4
|
+
"version": "0.36.1",
|
|
5
|
+
"mcpServers": {
|
|
6
|
+
"mirror-mind": {
|
|
7
|
+
"command": "/bin/sh",
|
|
8
|
+
"args": [
|
|
9
|
+
"-c",
|
|
10
|
+
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
}
|
|
@@ -18,8 +18,8 @@ Applies to all sessions, regardless of project.
|
|
|
18
18
|
The mirror operates in four modes, chosen automatically based on context:
|
|
19
19
|
Mirror, Builder, Explorer, and Soul.
|
|
20
20
|
|
|
21
|
-
Each skill below is named three ways: `/mm-<skill>` in Pi
|
|
22
|
-
Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
|
|
21
|
+
Each skill below is named three ways: `/mm-<skill>` in Pi and Grok Build,
|
|
22
|
+
`$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
|
|
23
23
|
name — `/mirror-mind:mm:<skill>` in Claude Code's menu and in Antigravity,
|
|
24
24
|
`$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
|
|
25
25
|
name — and the forms below refer to the same skill.
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Everything the `conversation-logger` subcommands need beyond the database
|
|
4
4
|
// is assembled here, once per invocation, from an EXPLICIT environment: the
|
|
5
|
-
// clock and id generator, the transcript/session resolution inputs, the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// clock and id generator, the transcript/session resolution inputs, the
|
|
6
|
+
// pre-write backup, and -- only for the subcommands that cross the LLM close
|
|
7
|
+
// tail -- the replay providers and the maintenance dependencies built on them.
|
|
8
|
+
// (No Pi sessions directory: maintenance reads no Pi session file, CR153.)
|
|
9
9
|
//
|
|
10
10
|
// The providers are loaded lazily. `diagnose-journeys`
|
|
11
11
|
// or `session-start --fast` must never fail because a replay fixture is not
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
// Nothing here reads `process.env` or `os.homedir()`; the front door passes
|
|
18
18
|
// them in, which is what keeps every test of this module hermetic.
|
|
19
19
|
|
|
20
|
-
import { backfillPiSessions, homeBornAt, resolvePiSessionsDir } from "#conversation/backfill.ts";
|
|
21
20
|
import { createCloseHooks, maybeGenerateTitle } from "#conversation/closeTail.ts";
|
|
22
21
|
import { runConversationExtraction } from "#conversation/extraction.ts";
|
|
23
22
|
import { runExtractionWithAccounting } from "#conversation/extractionRun.ts";
|
|
@@ -47,7 +46,6 @@ import {
|
|
|
47
46
|
export type LoggerRuntimeEnv = {
|
|
48
47
|
MIRROR_SESSION_ID?: string;
|
|
49
48
|
CLAUDE_PROJECT_DIR?: string;
|
|
50
|
-
PI_SESSIONS_DIR?: string;
|
|
51
49
|
MIRROR_TS_CONVERSATION_LLM_REPLAY?: string;
|
|
52
50
|
MIRROR_TS_CONVERSATION_EMBEDDING_REPLAY?: string;
|
|
53
51
|
/** Read by the live providers when the close tail runs against a real model. */
|
|
@@ -100,12 +98,11 @@ export interface LoggerRuntime {
|
|
|
100
98
|
readonly homeDir: string;
|
|
101
99
|
readonly claudeProjectDir: string | null;
|
|
102
100
|
readonly environmentSessionId: string | null;
|
|
103
|
-
readonly piSessionsDir: string;
|
|
104
101
|
/** Which transport answers the close tail here: replay, incomplete replay, or live. */
|
|
105
102
|
readonly transportMode: ProviderTransportMode;
|
|
106
103
|
/** The close hooks (extraction + finalization) behind the replay transport. */
|
|
107
104
|
closeHooks(): Promise<CloseHooks>;
|
|
108
|
-
/** The full maintenance wiring
|
|
105
|
+
/** The full maintenance wiring. */
|
|
109
106
|
maintenanceDeps(): Promise<MaintenanceDeps>;
|
|
110
107
|
/** The dated zip backup for the mutating journey repair; see `LoggerRuntimeOptions.backup`. */
|
|
111
108
|
readonly backup: ((stdout: (line: string) => void) => string | null) | null;
|
|
@@ -135,7 +132,6 @@ function liveTailOverrides(
|
|
|
135
132
|
|
|
136
133
|
export function createLoggerRuntime(options: LoggerRuntimeOptions): LoggerRuntime {
|
|
137
134
|
const { db, env, deps } = options;
|
|
138
|
-
const piSessionsDir = resolvePiSessionsDir(null, env, options.homeDir);
|
|
139
135
|
|
|
140
136
|
// One precedence, shared with the router so the two cannot disagree about
|
|
141
137
|
// which transport is live: replay -> incomplete -> live. The pair rule that
|
|
@@ -225,7 +221,6 @@ export function createLoggerRuntime(options: LoggerRuntimeOptions): LoggerRuntim
|
|
|
225
221
|
homeDir: options.homeDir,
|
|
226
222
|
claudeProjectDir: env.CLAUDE_PROJECT_DIR || null,
|
|
227
223
|
environmentSessionId: env.MIRROR_SESSION_ID?.trim() || null,
|
|
228
|
-
piSessionsDir,
|
|
229
224
|
transportMode: transport.mode,
|
|
230
225
|
backup: options.backup ?? null,
|
|
231
226
|
closeHooks,
|
|
@@ -246,14 +241,6 @@ export function createLoggerRuntime(options: LoggerRuntimeOptions): LoggerRuntim
|
|
|
246
241
|
// CV21.DS4.TS2: the finalization a bounded runtime's SessionEnd deferred.
|
|
247
242
|
finalizeCloseTail: (database, conversationId) =>
|
|
248
243
|
hooks.finalizeMetadata(database, conversationId),
|
|
249
|
-
// CR121: only the sessions newer than this home; its birth is read from
|
|
250
|
-
// the ledger once per maintenance run, and an unknown birth is an error.
|
|
251
|
-
backfillPiSessions: (database) =>
|
|
252
|
-
backfillPiSessions(
|
|
253
|
-
database,
|
|
254
|
-
{ sessionsDir: piSessionsDir, notBefore: homeBornAt(database) },
|
|
255
|
-
deps,
|
|
256
|
-
),
|
|
257
244
|
extractionLimit: resolveMaintenanceMaxExtractions({ env }),
|
|
258
245
|
staleOrphanMinutes: resolveStaleOrphanMinutes({ env }),
|
|
259
246
|
monotonic: options.monotonic,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What a runtime's session lifecycle lets Mirror do at its edges, by the label
|
|
2
2
|
// US8 stamps on the session row (`claude_code`, `codex`, `grok_build`). One home
|
|
3
|
-
// for the
|
|
3
|
+
// for the policies the hooks read: the two the close path reads (CV21.DS4.TS2,
|
|
4
|
+
// US4) and the one the inject hook reads (CV21.DS10.TS4, CR143).
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Whether a runtime's SessionEnd hook has the time to run the LLM tail at exit
|
|
@@ -11,8 +12,10 @@
|
|
|
11
12
|
* codex synchronous ("running async SessionEnd hook synchronously",
|
|
12
13
|
* DS12.TS2 L2), 1 s for a handler with no `timeout` (DS2's live
|
|
13
14
|
* `/hooks` table, 2026-10-13, Codex 0.162.1).
|
|
14
|
-
* grok_build 1.5 s for a handler with no `timeout` (DS12.TS4, Grok 1.0.52)
|
|
15
|
-
*
|
|
15
|
+
* grok_build 1.5 s for a handler with no `timeout` (DS12.TS4, Grok 1.0.52);
|
|
16
|
+
* re-measured on 1.0.62 (CV21.DS10.US3): the guide says a
|
|
17
|
+
* plugin's SessionEnd hook gets at most that default whatever
|
|
18
|
+
* its own `timeout`, and the close landed 213–259 ms after it.
|
|
16
19
|
*
|
|
17
20
|
* Any other label, and no label, keeps today's behavior: the tail runs.
|
|
18
21
|
*/
|
|
@@ -57,3 +60,23 @@ export function sessionEndRunsLlmTail(interfaceName: string | null | undefined):
|
|
|
57
60
|
export function sessionOutlivesTui(interfaceName: string | null | undefined): boolean {
|
|
58
61
|
return interfaceName === "codex" || interfaceName === "antigravity";
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether a runtime gives the model what a `UserPromptSubmit` hook returns as
|
|
66
|
+
* `additionalContext` — the channel the inject hook uses (CV21.DS10.TS4, CR143).
|
|
67
|
+
*
|
|
68
|
+
* grok_build no. Grok shows a SessionStart hook's `systemMessage` on
|
|
69
|
+
* screen and discards both events' `additionalContext`
|
|
70
|
+
* (DS12.TS4 L2/L3 on 1.0.52; CV21.DS10.US3 on 1.0.62, with a
|
|
71
|
+
* random canary the probe hook emitted on every turn and the
|
|
72
|
+
* model never quoted). The loads the hook made there held a
|
|
73
|
+
* turn 2.3–2.6 s for a context nobody read; the hook returns
|
|
74
|
+
* before any of them when this is false.
|
|
75
|
+
*
|
|
76
|
+
* Any other label, and no label, reads it: an unlabelled payload is a Claude
|
|
77
|
+
* Code payload by detection's own rule (`detectHookRuntime`), and a predicate
|
|
78
|
+
* that failed closed would silence injection on the runtime that needs it.
|
|
79
|
+
*/
|
|
80
|
+
export function runtimeReadsPromptContext(interfaceName: string | null | undefined): boolean {
|
|
81
|
+
return interfaceName !== "grok_build";
|
|
82
|
+
}
|
|
@@ -48,12 +48,6 @@ export interface MaintenanceDeps {
|
|
|
48
48
|
* only for callers written before the step existed; the front door wires it.
|
|
49
49
|
*/
|
|
50
50
|
finalizeCloseTail?: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
|
|
51
|
-
/**
|
|
52
|
-
* `backfillPiSessions` (slice E) bound to its resolved sessions directory.
|
|
53
|
-
* Injected rather than resolved here so the composite never reads the
|
|
54
|
-
* environment itself; the front door owns that resolution.
|
|
55
|
-
*/
|
|
56
|
-
backfillPiSessions: (db: WritableDatabase) => Promise<number> | number;
|
|
57
51
|
/** Python's `MEMORY_MAINTENANCE_MAX_EXTRACTIONS` (AI-05); defaults to the released budget. */
|
|
58
52
|
extractionLimit?: number;
|
|
59
53
|
/** `MIRROR_STALE_ORPHAN_MINUTES` (CV21.DS4.US4); defaults to the 30-minute threshold. */
|
|
@@ -231,6 +225,11 @@ function formatElapsed(elapsed: number): string {
|
|
|
231
225
|
* warning tails. The step order is behavior, not presentation — closing stale
|
|
232
226
|
* orphans first is what makes their conversations eligible for the extraction
|
|
233
227
|
* step in the same run.
|
|
228
|
+
*
|
|
229
|
+
* CR153: there is no Pi backfill step. Python's walked Pi's session directory
|
|
230
|
+
* and imported every file this home did not track, which on a machine with two
|
|
231
|
+
* homes meant each other's sessions; a home's Pi conversations are the ones the
|
|
232
|
+
* Pi extension logged into it, and maintenance reads no Pi session file.
|
|
234
233
|
*/
|
|
235
234
|
export async function sessionMaintenance(
|
|
236
235
|
db: WritableDatabase,
|
|
@@ -246,7 +245,6 @@ export async function sessionMaintenance(
|
|
|
246
245
|
thresholdMinutes: deps.staleOrphanMinutes ?? STALE_ORPHAN_THRESHOLD_MINUTES,
|
|
247
246
|
}),
|
|
248
247
|
),
|
|
249
|
-
await timedStep("Backfilled Pi sessions", monotonic, () => deps.backfillPiSessions(db)),
|
|
250
248
|
await timedStep("Retitled pending conversations", monotonic, () =>
|
|
251
249
|
retitlePendingConversations(db, { retitleConversation: deps.retitleConversation }),
|
|
252
250
|
),
|
|
@@ -23,7 +23,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
23
23
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Python `dict.get(key, default)` on a value that MUST be a dict: anything else
|
|
28
|
+
* raises, as Python's AttributeError does. The default applies only when the
|
|
29
|
+
* key is ABSENT -- a present `null` comes back as null, as Python returns None.
|
|
30
|
+
*/
|
|
27
31
|
function getOr(container: unknown, key: string, fallback: unknown): unknown {
|
|
28
32
|
if (!isRecord(container)) throw new TypeError(`expected an object with '${key}'`);
|
|
29
33
|
return key in container ? container[key] : fallback;
|
package/ts/src/db/migrations.ts
CHANGED
|
@@ -548,13 +548,10 @@ export const MIGRATIONS: readonly (readonly [string, MigrationApply])[] = [
|
|
|
548
548
|
* contract.
|
|
549
549
|
*/
|
|
550
550
|
export function runMigrations(db: WritableDatabase): void {
|
|
551
|
-
// The ledger
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
// re-inserts them with a later date. Retiring a migration id moves the
|
|
556
|
-
// minimum forward at worst, which imports less; re-stamping could move it
|
|
557
|
-
// and would be a lie about the home's age.
|
|
551
|
+
// The ledger records which migrations ran, and when -- nothing more. Do not
|
|
552
|
+
// infer a home's birth from its first `applied_at`: a database copied to
|
|
553
|
+
// make a second home carries its source's ledger, so two homes would share
|
|
554
|
+
// one birth (CR153 retired the reader that did, CR156 records the copy).
|
|
558
555
|
db.exec(`
|
|
559
556
|
CREATE TABLE IF NOT EXISTS _migrations (
|
|
560
557
|
id TEXT PRIMARY KEY,
|