mirror-mind 0.39.5 → 0.40.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.
@@ -74,8 +74,9 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
74
74
  # runtime spawned this hook from. Anything else is noted with its reason, in
75
75
  # the Pi extension's words, once a day, and the search goes on without it.
76
76
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
- # C:\Program Files\nodejs\node.exe (PR #47).
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
78
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
79
+ # (PR #47).
79
80
  NODE=""
80
81
  if [ -n "${MIRROR_NODE:-}" ]; then
81
82
  refused=""
@@ -74,8 +74,9 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
74
74
  # runtime spawned this hook from. Anything else is noted with its reason, in
75
75
  # the Pi extension's words, once a day, and the search goes on without it.
76
76
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
- # C:\Program Files\nodejs\node.exe (PR #47).
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
78
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
79
+ # (PR #47).
79
80
  NODE=""
80
81
  if [ -n "${MIRROR_NODE:-}" ]; then
81
82
  refused=""
@@ -74,8 +74,9 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
74
74
  # runtime spawned this hook from. Anything else is noted with its reason, in
75
75
  # the Pi extension's words, once a day, and the search goes on without it.
76
76
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
- # C:\Program Files\nodejs\node.exe (PR #47).
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
78
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
79
+ # (PR #47).
79
80
  NODE=""
80
81
  if [ -n "${MIRROR_NODE:-}" ]; then
81
82
  refused=""
@@ -74,8 +74,9 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
74
74
  # runtime spawned this hook from. Anything else is noted with its reason, in
75
75
  # the Pi extension's words, once a day, and the search goes on without it.
76
76
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
- # C:\Program Files\nodejs\node.exe (PR #47).
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
78
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
79
+ # (PR #47).
79
80
  NODE=""
80
81
  if [ -n "${MIRROR_NODE:-}" ]; then
81
82
  refused=""
@@ -74,8 +74,9 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
74
74
  # runtime spawned this hook from. Anything else is noted with its reason, in
75
75
  # the Pi extension's words, once a day, and the search goes on without it.
76
76
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
- # C:\Program Files\nodejs\node.exe (PR #47).
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
78
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
79
+ # (PR #47).
79
80
  NODE=""
80
81
  if [ -n "${MIRROR_NODE:-}" ]; then
81
82
  refused=""
@@ -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.40.0 — A Reply Stays in Its Own Conversation, and a Journey Can Go](v0.40.0.md) — minor release closing the first release train: CR151, a session's transcript fills only the conversation that session is bound to, each reply stamped with its transcript time, where it used to fill every open reply-less conversation in the transcript's window, another session's included; a transcript no session owns backfills nothing; and CR200, previewed as v0.40.0-next.1, `mirror-mind/desktop` deletes a Journey with every record that belongs to it, by digest, behind a verified backup. Update every install that has a home. No migration.
16
+ - [v0.40.0-next.1 — A Journey Can Go, Showing What Goes With It](v0.40.0-next.1.md) — preview under npm's `next`, read by no updater, for Nautilus for Mirror (the release train): CR200, `mirror-mind/desktop` gains `previewJourneyDeletion` and `deleteJourneyWithRecords`, deleting a Journey with every record that belongs to it after a preview that counts it, by digest, behind a verified backup; child Journeys refuse. Terminal users receive it with v0.40.0. No migration.
15
17
  - [v0.39.5 — The Node You Named, or Why Not](v0.39.5.md) — patch release of CR192, from PR #47's thread (@rodrigoimmaginario): the hook wrappers, the plugin's MCP entry, and `runtime diagnose` use `MIRROR_NODE` only as an absolute path to an executable file, as the Pi extension does; a relative value is never run, and a refused one is named with its reason, once a day in `hooks.log` and on the MCP entry's stderr; the Windows installer's drive-letter path counts as absolute, in CI under Git Bash and on the installer. No migration.
16
18
  - [v0.39.4 — The Reply Finds Its Prompt](v0.39.4.md) — patch release of PR #51 (@alannascimento1), CR166 and CR196: on Claude Code, a prompt typed while a turn runs and a journey entered in plain words no longer lose the reply; `nothing_waiting` in `hooks.log` means a lost reply again. No schema change, no migration.
17
19
  - [v0.39.3 — Windows Gets Its Hooks](v0.39.3.md) — patch release of PR #53, PR #50, and PR #54 (@rodrigoimmaginario): the plugin's hook wrappers and MCP locator run the package's own bin when npm's global entry is a shell shim, so Claude Code hooks and the MCP server work on a Windows package install; `runtime diagnose` no longer counts its own verdict as a front-door error (CR139); the docs point a Windows reader to the installer in its own repository. No migration.
@@ -0,0 +1,103 @@
1
+ ---
2
+ digest: >
3
+ v0.40.0-next.1 is the first preview of the release train: published under npm's `next`
4
+ dist-tag, read by no updater, for Nautilus for Mirror, which embeds this Core. It carries
5
+ CR200: a host can delete a Journey together with every record that belongs to it, after
6
+ a preview that counts what will go, what other Journeys keep with a link cleared, and
7
+ which extension tables lose rows. The deletion acts only on the preview the person saw,
8
+ in one transaction, behind a named backup verified before any write. Terminal users
9
+ receive it with v0.40.0, when the train leaves. No schema change, no migration, and no
10
+ model or prompt change.
11
+ ---
12
+
13
+ # v0.40.0-next.1 — A Journey Can Go, Showing What Goes With It
14
+
15
+ **Date:** 2026-11-06
16
+
17
+ **A preview, not a release users receive.** It is staged under `next`, its GitHub Release is
18
+ a prerelease, and `stable`, `latest`, `mirror update` and the release-notes reader all stay
19
+ on v0.39.5. It exists so Nautilus for Mirror can ship the change below before the train
20
+ leaves ([the release train](../process/versioning.md#the-release-train)).
21
+
22
+ ## Highlights
23
+
24
+ - **A Journey and its records, deleted in one act**
25
+ ([CR200](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr200-delete-a-journey-with-its-records-after-showing-what-will-go.md)).
26
+ `mirror-mind/desktop` gains `previewJourneyDeletion` and `deleteJourneyWithRecords`.
27
+ `deleteJourney` still deletes only an empty leaf.
28
+ - **The preview is the contract.** It counts the Journey's own records, its Conversations'
29
+ contents, other Journeys' records that only lose a link, and the `ext_*` rows that name
30
+ the Journey, by owning extension, and hashes all of it. The deletion takes that digest
31
+ and refuses any other.
32
+ - **A backup first, verified.** `backups/pre-journey-deletion-<id>-<stamp>-<random>.db`,
33
+ owner-only, taken under the write lock and read back as the same digest before anything
34
+ is deleted. A refused deletion removes it; a committed one keeps it.
35
+ - **One friction kept:** a Journey with child Journeys is refused, naming them.
36
+ - No schema change, no migration, and no model or prompt change.
37
+
38
+ ## Where We Started
39
+
40
+ CV24.DS8 gave a host five Journey calls, and its `deleteJourney` refuses a Journey that
41
+ holds anything: conversations, memories, tasks, attachments, stories, change requests,
42
+ cursors. Nautilus for Mirror's Journey management (its RS028) asked for the act a person
43
+ means by "delete this Journey". Its Navigator decided on 2026-11-05 and 2026-11-06 that
44
+ everything that belongs to the Journey goes, that the confirmation shows it first, and,
45
+ where the Core cannot judge data it does not own, to trade refusal for transparency.
46
+
47
+ A long-lived home showed why "belongs" needed rules: 956 memories sit under a Journey other
48
+ than their conversation's, and 101 runtime sessions likewise. Deleting by reach would have
49
+ erased other Journeys' history.
50
+
51
+ ## What Changed
52
+
53
+ **Four rules.** A record that belongs to the Journey goes. What belongs to a deleted
54
+ conversation or memory goes with it: messages, embeddings, provider call records, memory
55
+ access records. A record of another Journey that only points at something deleted stays,
56
+ with that link cleared. And an `ext_*` row goes when the table's `journey_id` or `journey`
57
+ column names the Journey exactly; the preview names the extension from `_ext_migrations`,
58
+ and a row an extension's own foreign key holds refuses the deletion as
59
+ `extension_rows_held`, naming the table.
60
+
61
+ **A closed inventory.** `JOURNEY_DELETION_INVENTORY` names every core table the deletion
62
+ touches, keeps or detaches, and a test reads the schema against it: a table that names a
63
+ Journey, or references a row the deletion removes, without a decision fails. Its first
64
+ run found `journey_mutation_receipts`, kept as the audit trail.
65
+
66
+ **Refusals.** `journey_changed` for a digest that no longer matches, `journey_has_children`,
67
+ `extension_rows_held`, `backup_failed`, and as before an unknown Journey, a malformed
68
+ request and a database that is not current. Every refusal writes nothing.
69
+
70
+ **Validated** in `journeyDeletion.test.ts` (11 tests, ten mutations each caught), in CI on
71
+ Ubuntu and macOS (PR #55), and end to end by Nautilus: its host drives both calls
72
+ against a scratch home, and its Navigator deleted Journeys in the app on 2026-11-06.
73
+
74
+ **Also in this preview.** [CR199](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr199-deliver-mirror-context-takes-three-to-seven-seconds-before-every-desktop-turn.md)
75
+ is captured: `deliverMirrorContext` takes three to seven seconds before every Desktop
76
+ turn, measured. And the release train itself (PR #56) is in
77
+ [versioning](../process/versioning.md#the-release-train).
78
+
79
+ ## Conscious Exclusions
80
+
81
+ - No CLI command deletes a Journey with its records; a host is the only caller.
82
+ - No cascade through child Journeys: they are refused, by the Navigator's decision.
83
+ - Files on disk are the host's to decide; the Core touches only the database.
84
+ - An extension cannot yet take over its own cleanup; recorded for when the extension
85
+ mechanism exists (CV24.DS8).
86
+ - The test suite, run with `MIRROR_HOME` or `DB_PATH` inherited from a real home, writes
87
+ into that home. Found while validating this change; worth its own Change Request.
88
+
89
+ ## What We Learned
90
+
91
+ A preview that is also the contract keeps both honest: the person confirms a list, the
92
+ digest makes the deletion that list and nothing more, and a home that changed in between
93
+ is told so instead of deleted differently.
94
+
95
+ ## Upgrading
96
+
97
+ Nothing to do in the terminal: this preview is not offered by `mirror update`. A host that
98
+ embeds the Core pins `mirror-mind@0.40.0-next.1` from the registry.
99
+
100
+ ## Next Horizon
101
+
102
+ v0.40.0 leaves with the next train, carrying this change to every install, with whatever
103
+ else merges before it.
@@ -0,0 +1,158 @@
1
+ ---
2
+ digest: >
3
+ v0.40.0 closes the first release train. It carries CR151: a session's transcript is
4
+ written only into the conversation that session is bound to, each reply stamped with its
5
+ own transcript time, where before it was copied into every open, reply-less conversation
6
+ in the transcript's window, another session's included. It also carries CR200, previewed
7
+ as v0.40.0-next.1 for Nautilus for Mirror: a host can delete a Journey with every record
8
+ that belongs to it, after a preview that counts what will go, by digest, behind a
9
+ verified backup. A transcript no session owns, or a session bound to no conversation,
10
+ now backfills nothing. No schema change, no migration, and no model or prompt change.
11
+ ---
12
+
13
+ # v0.40.0 — A Reply Stays in Its Own Conversation, and a Journey Can Go
14
+
15
+ **Date:** 2026-11-06
16
+
17
+ The first stable release under [the release train](../process/versioning.md#the-release-train):
18
+ everything merged into `main` since v0.39.5, in one batch. One of its two changes users
19
+ have never had; the other reached embedding hosts as a preview on the same day.
20
+
21
+ ## Highlights
22
+
23
+ - **A session's replies go into its own conversation, and only there**
24
+ ([CR151](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr151-the-transcript-backfill-writes-a-session-s-replies-into-other-sessions-conversations.md)).
25
+ At `SessionEnd`, on Claude Code and the runtimes that share its hook, the transcript
26
+ filled every conversation in its time window that had no assistant message yet. An open
27
+ conversation that started earlier was always in the window: a session killed in its first
28
+ turn, or a concurrent session still in its own. Now the transcript fills the conversation
29
+ `runtime_sessions` binds to the ending session, inside that conversation's own window.
30
+ - **A reply carries its own time.** The copy used to be stamped with the hook's clock, so
31
+ every reply sorted after every prompt of its conversation, and an extraction read all the
32
+ questions and then all the answers. A reply now carries its transcript timestamp,
33
+ normalized to the logger's microsecond form, so recall, extraction, and the MCP read
34
+ model read the turns in order.
35
+ - **A transcript nobody owns writes nothing.** A payload with no `session_id`, and a session
36
+ bound to no conversation, used to backfill by window alone. Both are retired: with no
37
+ key there is no attribution. A "same runtime only" fallback was rejected because it would
38
+ not have fixed the recorded case, in which the orphan and the session that wrote into it
39
+ were both `claude_code`.
40
+ - **A Journey and its records, deleted in one act**
41
+ ([CR200](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr200-delete-a-journey-with-its-records-after-showing-what-will-go.md),
42
+ previewed as [v0.40.0-next.1](v0.40.0-next.1.md)). `mirror-mind/desktop` gains
43
+ `previewJourneyDeletion` and `deleteJourneyWithRecords`: the preview counts the Journey's
44
+ records, and the deletion acts only on the preview the person saw, in one transaction,
45
+ behind a named backup verified before any write. Child Journeys refuse.
46
+ - No schema change, no migration, and no model or prompt change.
47
+
48
+ ## Where We Started
49
+
50
+ CV21.DS5.US3's live route on 2026-10-20 found three product defects and stopped at its own
51
+ rule: capture, don't fix inside the story. One of them was CR151. A hand-run diagnostic had
52
+ left an open `claude_code` conversation with one prompt and no Stop; the route's Claude
53
+ session ended at 20:41, and its reply `ready` landed in its own conversation and in the
54
+ orphan's, at the same instant. The port had inherited the behavior from the Python fallback
55
+ it replaced, written when one session at a time was the only case, and no test pinned the
56
+ scoping.
57
+
58
+ The Navigator accepted the plan after a review through three lenses. The database
59
+ architect's finding set the shape: the key already existed (`runtime_sessions.conversation_id`)
60
+ and the backfill ignored it, so the fix is to use it, and no fallback survives that the
61
+ evidence case would pass through.
62
+
63
+ ## What Changed
64
+
65
+ **CR151.** `backfillAssistantMessages(db, transcriptPath, conversationId, deps)` takes the
66
+ conversation as a required argument: one row, its own `[started_at, ended_at]` window, and
67
+ skipped when it already logged a reply, so a Stop-logged one is not recorded twice.
68
+ `handleSessionEndHook` reads the bound conversation before `endSession` closes the
69
+ session, rather than relying on the close to preserve it. `normalizeTimestamp` pads
70
+ Claude's millisecond `.123Z` to the logger's `.123000Z`, because as strings `.123Z` sorts
71
+ after `.123456Z`. Both headers say the code departs from the Python port on purpose, so
72
+ nobody fixes it back.
73
+
74
+ The seven-conversation fixture that sat on every window boundary is kept, graded now by
75
+ what it was always meant to show: naming one conversation changes it and leaves the other
76
+ six exactly as they were. The golden that recorded Python filling all seven at once stays as
77
+ a frozen record of the defect, since its generator and its Python source are gone.
78
+
79
+ **Validated on a real session.** The Navigator ran the route against Claude Code 2.1.318:
80
+ the orphan kept its prompt alone, and the session's reply carried the transcript's own
81
+ timestamp. Two earlier runs failed, and told us something worth keeping: the Navigator's
82
+ machine has Claude settings that pin `MIRROR_BIN` to the installed package over whatever the shell exports,
83
+ and a second copy of the plugin loads from `~/.claude/skills`, so both runs executed the
84
+ published 0.39.5. They reproduced the defect on a real session (`ready` in the orphan, after
85
+ the conversation's close, with no `turn_id`), which is the "before" the CR now records.
86
+ The route pins the checkout with `--settings`.
87
+
88
+ **CR200** is as [v0.40.0-next.1](v0.40.0-next.1.md) describes it: four rules for what
89
+ belongs to a Journey, a closed inventory a test holds against the schema, and refusals that
90
+ write nothing.
91
+
92
+ **Also in this release.** The Windows installer's real `MIRROR_NODE` path is named where
93
+ docs and comments had named another. The tests and smokes read their paths through
94
+ `fileURLToPath`, so a checkout under `Código` or `My Projects` no longer fails 113 tests
95
+ (issue #44), and the guard that keeps the product from reading a URL's pathname as a path
96
+ now covers them. The lifecycle smoke asserts the new session-less contract. Captured for
97
+ later: CR198 (`runtime diagnose` does not name a refused `MIRROR_NODE`), CR199
98
+ (`deliverMirrorContext` takes three to seven seconds before a Desktop turn), and CR201.
99
+
100
+ ## Conscious Exclusions
101
+
102
+ - **Replies already copied stay where they are.** A home that ran an earlier version may
103
+ hold a reply under the wrong conversation. Nothing in the data tells it from a real one,
104
+ and removing one is a manual edit; the selector under Upgrading finds the candidates.
105
+ - **A late `SessionEnd` still adds replies after the extraction.** When maintenance closed
106
+ and extracted a conversation first, the replies the backfill then adds are read by no
107
+ extraction. CR151 pins that they land in the right conversation, not what then reads them.
108
+ [CR201](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr201-a-reply-backfilled-after-the-close-misses-the-extraction-that-closed-the-conversation.md)
109
+ holds the decision.
110
+ - **The session-less route is gone, not narrowed.** A caller that fed a transcript alone to
111
+ `conversation-logger session-end` now gets a no-op.
112
+ - No CLI command deletes a Journey with its records; a host is the only caller. No cascade
113
+ through child Journeys; files on disk are the host's.
114
+
115
+ ## What We Learned
116
+
117
+ A fallback written for one case does not know about the second. The Python backfill was
118
+ right for a machine with one session at a time, and ordinary use of Mirror, a Ctrl-C in a
119
+ first turn, was enough to make it wrong.
120
+
121
+ The test that proves a fix is the one that runs the code you changed. Two of the Navigator's three live
122
+ runs exercised the previous release, because a setting the validator did not know about decided
123
+ which Mirror the hooks ran. The checkout's own `mirror-hook` was never in the path. Pinning
124
+ the code under test is part of the route now, and the failed runs stay in the record as
125
+ the defect, measured on a real session, in the published release.
126
+
127
+ ## Upgrading
128
+
129
+ ```bash
130
+ mirror runtime update
131
+ ```
132
+
133
+ Update every install that has a home: the fix lives in the core that the hooks run, and a
134
+ machine whose Claude Code runs a global install keeps the old backfill until that install
135
+ is updated. Plugin copies need no change.
136
+
137
+ To find replies an earlier version copied into the wrong conversation, read-only:
138
+
139
+ ```bash
140
+ sqlite3 ~/.mirror-minds/<user>/memory.db \
141
+ "SELECT substr(a.conversation_id,1,8) AS conv, a.created_at, substr(a.content,1,40)
142
+ FROM messages a JOIN messages b
143
+ ON a.content = b.content
144
+ AND substr(a.created_at,1,19) = substr(b.created_at,1,19)
145
+ AND a.conversation_id < b.conversation_id
146
+ WHERE a.role = 'assistant' AND b.role = 'assistant'
147
+ ORDER BY a.created_at"
148
+ ```
149
+
150
+ A match is one reply written into two conversations within the same second, which is the
151
+ trace of one backfill; read each row, because two identical short replies a second apart in
152
+ different conversations are a different thing. The home's dated backups hold the rows as they
153
+ were.
154
+
155
+ ## Next Horizon
156
+
157
+ CR201's decision, how often the late path is taken on a real home; CR198 and CR199; CR195
158
+ in RS010's backlog. The next train leaves when the Navigator calls it.
package/lib/desktop.js CHANGED
@@ -22,6 +22,7 @@ const migration = await import("../ts/src/db/migrateHome.ts");
22
22
  const context = await import("../ts/src/mirror/deliverContext.ts");
23
23
  const learning = await import("../ts/src/conversation/learnFromConversations.ts");
24
24
  const journeys = await import("../ts/src/journey/journeyCalls.ts");
25
+ const journeyDeletion = await import("../ts/src/journey/journeyDeletion.ts");
25
26
 
26
27
  export const DESKTOP_API_VERSION = 1;
27
28
  export const inspectMigrationState = migrationReport.inspectMigrationState;
@@ -70,3 +71,10 @@ export const setJourneyProjectPath = journeys.setJourneyProjectPath;
70
71
  export const moveJourney = journeys.moveJourney;
71
72
  export const deleteJourney = journeys.deleteJourney;
72
73
  export const JourneyCallRejectedError = journeys.JourneyCallRejectedError;
74
+
75
+ // Delete a Journey with its records, after showing what will go: the preview
76
+ // counts every record by rule and answers a digest; the deletion takes that
77
+ // digest back, refuses a home that no longer matches it, and runs behind a
78
+ // retained, verified backup it names. Rejections are `JourneyCallRejectedError`s.
79
+ export const previewJourneyDeletion = journeyDeletion.previewJourneyDeletion;
80
+ export const deleteJourneyWithRecords = journeyDeletion.deleteJourneyWithRecords;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.39.5",
3
+ "version": "0.40.0",
4
4
  "description": "Mirror Mind: a local-first memory and identity framework for agentic AI runtimes (Pi, Claude Code, Codex, Antigravity)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.39.5",
3
+ "version": "0.40.0",
4
4
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
5
5
  "author": {
6
6
  "name": "Mirror Mind"
@@ -100,8 +100,9 @@ fi
100
100
  # runtime spawned this hook from. Anything else is noted with its reason, in
101
101
  # the Pi extension's words, once a day, and the search goes on without it.
102
102
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
103
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
104
- # C:\Program Files\nodejs\node.exe (PR #47).
103
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
104
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
105
+ # (PR #47).
105
106
  NODE=""
106
107
  if [ -n "${MIRROR_NODE:-}" ]; then
107
108
  refused=""
@@ -100,8 +100,9 @@ fi
100
100
  # runtime spawned this hook from. Anything else is noted with its reason, in
101
101
  # the Pi extension's words, once a day, and the search goes on without it.
102
102
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
103
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
104
- # C:\Program Files\nodejs\node.exe (PR #47).
103
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
104
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
105
+ # (PR #47).
105
106
  NODE=""
106
107
  if [ -n "${MIRROR_NODE:-}" ]; then
107
108
  refused=""
@@ -100,8 +100,9 @@ fi
100
100
  # runtime spawned this hook from. Anything else is noted with its reason, in
101
101
  # the Pi extension's words, once a day, and the search goes on without it.
102
102
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
103
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
104
- # C:\Program Files\nodejs\node.exe (PR #47).
103
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
104
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
105
+ # (PR #47).
105
106
  NODE=""
106
107
  if [ -n "${MIRROR_NODE:-}" ]; then
107
108
  refused=""
@@ -95,8 +95,9 @@ fi
95
95
  # runtime spawned this hook from. Anything else is noted with its reason, in
96
96
  # the Pi extension's words, once a day, and the search goes on without it.
97
97
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
98
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
99
- # C:\Program Files\nodejs\node.exe (PR #47).
98
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
99
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
100
+ # (PR #47).
100
101
  NODE=""
101
102
  if [ -n "${MIRROR_NODE:-}" ]; then
102
103
  refused=""
@@ -95,8 +95,9 @@ fi
95
95
  # runtime spawned this hook from. Anything else is noted with its reason, in
96
96
  # the Pi extension's words, once a day, and the search goes on without it.
97
97
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
98
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
99
- # C:\Program Files\nodejs\node.exe (PR #47).
98
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
99
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
100
+ # (PR #47).
100
101
  NODE=""
101
102
  if [ -n "${MIRROR_NODE:-}" ]; then
102
103
  refused=""
@@ -95,8 +95,9 @@ fi
95
95
  # runtime spawned this hook from. Anything else is noted with its reason, in
96
96
  # the Pi extension's words, once a day, and the search goes on without it.
97
97
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
98
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
99
- # C:\Program Files\nodejs\node.exe (PR #47).
98
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
99
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
100
+ # (PR #47).
100
101
  NODE=""
101
102
  if [ -n "${MIRROR_NODE:-}" ]; then
102
103
  refused=""
@@ -95,8 +95,9 @@ fi
95
95
  # runtime spawned this hook from. Anything else is noted with its reason, in
96
96
  # the Pi extension's words, once a day, and the search goes on without it.
97
97
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
98
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
99
- # C:\Program Files\nodejs\node.exe (PR #47).
98
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
99
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
100
+ # (PR #47).
100
101
  NODE=""
101
102
  if [ -n "${MIRROR_NODE:-}" ]; then
102
103
  refused=""
@@ -95,8 +95,9 @@ fi
95
95
  # runtime spawned this hook from. Anything else is noted with its reason, in
96
96
  # the Pi extension's words, once a day, and the search goes on without it.
97
97
  # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
98
- # path.win32.isAbsolute: under Git Bash the Windows installer writes
99
- # C:\Program Files\nodejs\node.exe (PR #47).
98
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes a
99
+ # drive-letter path, C:\Users\...\AppData\Local\Programs\MirrorMind\node\node.exe
100
+ # (PR #47).
100
101
  NODE=""
101
102
  if [ -n "${MIRROR_NODE:-}" ]; then
102
103
  refused=""
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
3
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
4
- "version": "0.39.5",
4
+ "version": "0.40.0",
5
5
  "mcpServers": {
6
6
  "mirror-mind": {
7
7
  "command": "/bin/sh",
@@ -584,7 +584,6 @@ export interface HookLoggingOptions {
584
584
  export type SessionEndHookOutcome =
585
585
  | { action: "skipped"; reason: "malformed_payload" | "nothing_to_do" }
586
586
  | { action: "ended"; sessionId: string; transcriptPath: string | null; backfilled: boolean }
587
- | { action: "backfilled"; transcriptPath: string }
588
587
  | { action: "failed"; error: string };
589
588
 
590
589
  const parsePayload = parseHookPayloadObject;
@@ -917,12 +916,15 @@ export function handleStopHook(
917
916
  }
918
917
 
919
918
  /**
920
- * Python `hook_session_end`: end the session when the payload names one, then
921
- * backfill assistant turns from the transcript when it exists -- in that
922
- * order, so the just-ended conversation's `ended_at` bounds the backfill.
919
+ * Python `hook_session_end`: end the session the payload names, then backfill
920
+ * assistant turns from its transcript when it exists -- in that order, so the
921
+ * just-ended conversation's `ended_at` bounds the backfill.
923
922
  *
924
- * The two halves are independent: an empty `session_id` with a transcript
925
- * still backfills (the session-less route, slice E), and a session without a
923
+ * The backfill fills only the conversation the session is bound to (CR151).
924
+ * Python's session-less route -- no `session_id`, a transcript alone, filling
925
+ * every conversation in the transcript's time window -- is gone: a transcript
926
+ * no session owns cannot be attributed, so it writes nothing. A session with no
927
+ * bound conversation likewise backfills nothing; a session without a
926
928
  * transcript still ends. Everything is swallowed; the hook always exits 0.
927
929
  */
928
930
  export async function handleSessionEndHook(
@@ -943,42 +945,40 @@ export async function handleSessionEndHook(
943
945
  if (!payload) return { action: "skipped", reason: "malformed_payload" };
944
946
 
945
947
  const sessionId = payloadString(payload, "session_id");
948
+ if (!sessionId) return { action: "skipped", reason: "nothing_to_do" };
946
949
  const transcriptPath = resolveTranscriptPath(
947
950
  payloadString(payload, "transcript_path"),
948
951
  sessionId,
949
952
  options.claudeProjectDir,
950
953
  options.homeDir,
951
954
  );
955
+ // Read before the close, so the attribution does not lean on the close
956
+ // preserving `conversation_id` when it deactivates the session.
957
+ const runtimeSession = getRuntimeSession(db, sessionId);
958
+ const conversationId = runtimeSession?.conversationId ?? null;
952
959
  let backfilled = false;
953
960
  // After the close, which bounds the backfill window, and before the tail,
954
- // which should see what the backfill adds (CV21.DS4.TS2).
961
+ // which should see what the backfill adds (CV21.DS4.TS2). A session with no
962
+ // bound conversation closes nothing and backfills nothing (CR151).
955
963
  const backfill = () => {
956
- backfilled = transcriptPath !== null && existsSync(transcriptPath);
957
- if (backfilled && transcriptPath !== null)
958
- backfillAssistantMessages(db, transcriptPath, deps);
964
+ if (conversationId === null || transcriptPath === null || !existsSync(transcriptPath)) return;
965
+ backfilled = true;
966
+ backfillAssistantMessages(db, transcriptPath, conversationId, deps);
959
967
  };
960
968
 
961
- if (sessionId) {
962
- // The label US8 stamps on every prompt; the hook's own detection only when
963
- // the row carries none; today's behavior when neither names a runtime.
964
- const label = getRuntimeSession(db, sessionId)?.interface || options.interface || null;
965
- const finalize = sessionEndRunsLlmTail(label);
966
- const closed = await endSession(
967
- db,
968
- sessionId,
969
- { extract: finalize, finalize, beforeTail: backfill },
970
- deps,
971
- hooks,
972
- );
973
- // A session with no bound conversation closes nothing; the backfill still runs.
974
- if (!closed) backfill();
975
- } else {
976
- backfill();
977
- }
969
+ // The label US8 stamps on every prompt; the hook's own detection only when
970
+ // the row carries none; today's behavior when neither names a runtime.
971
+ const label = runtimeSession?.interface || options.interface || null;
972
+ const finalize = sessionEndRunsLlmTail(label);
973
+ await endSession(
974
+ db,
975
+ sessionId,
976
+ { extract: finalize, finalize, beforeTail: backfill },
977
+ deps,
978
+ hooks,
979
+ );
978
980
 
979
- if (sessionId) return { action: "ended", sessionId, transcriptPath, backfilled };
980
- if (backfilled && transcriptPath !== null) return { action: "backfilled", transcriptPath };
981
- return { action: "skipped", reason: "nothing_to_do" };
981
+ return { action: "ended", sessionId, transcriptPath, backfilled };
982
982
  } catch (error) {
983
983
  return { action: "failed", error: error instanceof Error ? error.message : String(error) };
984
984
  }