mirror-mind 0.39.2 → 0.39.4
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/README.md +3 -0
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.39.3.md +94 -0
- package/docs/releases/v0.39.4.md +81 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +2 -2
- package/plugins/mirror-mind/agy/pre-invocation.sh +8 -0
- package/plugins/mirror-mind/agy/session-start.sh +8 -0
- package/plugins/mirror-mind/agy/stop.sh +8 -0
- package/plugins/mirror-mind/hooks/log-assistant-reply.sh +8 -0
- package/plugins/mirror-mind/hooks/log-session-end.sh +8 -0
- package/plugins/mirror-mind/hooks/log-user-prompt.sh +8 -0
- package/plugins/mirror-mind/hooks/mirror-inject.sh +8 -0
- package/plugins/mirror-mind/hooks/session-start.sh +8 -0
- package/plugins/mirror-mind/mcp_config.json +1 -1
- package/plugins/mirror-mind/plugin.json +2 -2
- package/ts/src/conversation/logger.ts +104 -9
- package/ts/src/frontDoor/cli.ts +11 -2
- package/ts/src/frontDoor/frontDoorLog.ts +8 -1
- package/ts/src/frontDoor/runtimeRoute.ts +11 -0
- package/ts/src/guards/claudePlugin.ts +5 -0
package/README.md
CHANGED
|
@@ -76,6 +76,9 @@ in your machine, not a server.
|
|
|
76
76
|
- [Node.js 24+](https://nodejs.org/) — runs Mirror Mind's core
|
|
77
77
|
(`node --version` must be ≥ v24; POSIX/macOS/Linux supported). Nothing
|
|
78
78
|
else to install: there is no Python and no build step
|
|
79
|
+
- On Windows: [Mirror Mind for Windows](https://github.com/mirror-mind-ai/mirror-installer),
|
|
80
|
+
a separate per-user installer that brings Node, Git Bash, Pi and Mirror in
|
|
81
|
+
one pass. It lives in its own repository, outside this package and its CI
|
|
79
82
|
- [OpenRouter](https://openrouter.ai) account with at least $5 in credits —
|
|
80
83
|
embeddings, memory extraction, and multi-LLM
|
|
81
84
|
- An AI runtime subscription:
|
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.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.
|
|
16
|
+
- [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.
|
|
15
17
|
- [v0.39.2 — Input, Read the Platform's Way](v0.39.2.md) — patch release of PR #49 and PR #48 (@rodrigoimmaginario), CR194, and CR193: `runtime diagnose` stops warning every Windows install about Node; a PowerShell byte-order mark is not stored; stdin is decoded by its mark, UTF-8 or UTF-16, and `identity set` and `journey update` gain `--file`, the route around PowerShell 5.1's ASCII pipe; `package-lock.json` names the package at its version, and the release doctor checks it. No migration.
|
|
16
18
|
- [v0.39.1 — Pi Finds Its Node, or Says So](v0.39.1.md) — patch release of PR #47 (@rodrigoimmaginario) and CR191: a Pi opened where `node` is not on the PATH no longer exits; the Pi extension resolves Node where the hook wrappers look, so a global install needs nothing configured; when no Node resolves, one warning names the fix and `runtime diagnose` reports the `hooks.log` line. No migration.
|
|
17
19
|
- [v0.39.0 — The Core as a Library](v0.39.0.md) — minor release of CV24's first eight Delivery Stories: `mirror-mind/desktop`, a library entry through which a host such as Mirror Desktop 0.3 embeds this core; calls for a home's migration state, Conversations, sidebar reads, home creation, migration on request, Mirror's context, learning from open Conversations, and Journey administration, each with an explicit path and no environment. The new `exports` field closes every other deep import. No migration.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.39.3 is a patch release of three pull requests by Rodrigo Immaginario. On a Windows
|
|
4
|
+
package install, npm's global `mirror` and `mirror-hook` are shell shims, and the plugin's
|
|
5
|
+
hook wrappers and MCP locator handed them to Node: no Claude Code conversation was ever
|
|
6
|
+
logged there, and the MCP server died on arrival. They now run the package's own bin
|
|
7
|
+
(PR #53). `runtime diagnose` no longer counts its own "attention needed" exit as a
|
|
8
|
+
front-door error, so the finding stops feeding itself (PR #50, CR139). The docs point a
|
|
9
|
+
Windows reader to the installer that now lives in its own repository (PR #54). No
|
|
10
|
+
migration, and no model or prompt change.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# v0.39.3 — Windows Gets Its Hooks
|
|
14
|
+
|
|
15
|
+
**Date:** 2026-11-03
|
|
16
|
+
|
|
17
|
+
## Highlights
|
|
18
|
+
|
|
19
|
+
- **Claude Code hooks and the MCP server work on a Windows package install**
|
|
20
|
+
([PR #53](https://github.com/mirror-mind-ai/mirror/pull/53), by @rodrigoimmaginario). On
|
|
21
|
+
Windows npm writes no symlink: its global `mirror-hook` and `mirror` are POSIX shell
|
|
22
|
+
shims with `node_modules/` beside them. Handed to Node, the shim ran as JavaScript:
|
|
23
|
+
every hook ended `node exited 1` in `hooks.log`, and the MCP server died with a
|
|
24
|
+
`SyntaxError` from the shim's second line. The wrappers and the locator now run the bin
|
|
25
|
+
the shim wraps, one fixed path away, when it is there. A POSIX prefix is unchanged.
|
|
26
|
+
- **`runtime diagnose` no longer counts its own verdict as a front-door error**
|
|
27
|
+
([PR #50](https://github.com/mirror-mind-ai/mirror/pull/50), by @rodrigoimmaginario;
|
|
28
|
+
[CR139](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr139-a-verdict-that-exits-1-is-logged-as-a-front-door-error.md)).
|
|
29
|
+
Its exit 1 means "attention needed", a contract with scripts, and the front door logged
|
|
30
|
+
it as `ERROR`, which `front_door_errors` then counted: one more error per run, a finding
|
|
31
|
+
that could never clear. The exit is now logged at `INFO` as a verdict; a real failure of
|
|
32
|
+
the same command stays `ERROR`.
|
|
33
|
+
- **A Windows reader is pointed to the installer**
|
|
34
|
+
([PR #54](https://github.com/mirror-mind-ai/mirror/pull/54), by @rodrigoimmaginario):
|
|
35
|
+
[mirror-mind-ai/mirror-installer](https://github.com/mirror-mind-ai/mirror-installer), a
|
|
36
|
+
per-user installer in its own repository, from README, Getting Started, and REFERENCE's
|
|
37
|
+
platform envelope. The envelope itself is unchanged: this repository ships no Windows
|
|
38
|
+
product, and POSIX is what is supported.
|
|
39
|
+
- No migration, and no model or prompt change.
|
|
40
|
+
|
|
41
|
+
## Where We Started
|
|
42
|
+
|
|
43
|
+
v0.39.2 shipped the morning's Windows fixes: `runtime diagnose` reading the PATH the
|
|
44
|
+
platform's way, and PowerShell's marks and encodings on stdin. Three of Rodrigo's PRs
|
|
45
|
+
remained, and the largest of them was the one that mattered most: on a Windows package
|
|
46
|
+
install, Mirror's Claude Code integration had never worked, and nothing said so but
|
|
47
|
+
`hooks.log`.
|
|
48
|
+
|
|
49
|
+
The diagnose loop in PR #50 had been in the backlog three times over, CR127, CR139, and
|
|
50
|
+
CR176, the last two captured without finding the first. The PR implements CR139's first
|
|
51
|
+
option as written, arrived at independently. CR139 is done with Rodrigo as its Driver;
|
|
52
|
+
CR127 and CR176 are closed as its duplicates.
|
|
53
|
+
|
|
54
|
+
## What Changed
|
|
55
|
+
|
|
56
|
+
**The shim rule, in the two generators.** `scripts/generate_hook_wrappers.sh` and
|
|
57
|
+
`MCP_LOCATOR` in `ts/src/guards/claudePlugin.ts` each gain one rule: when
|
|
58
|
+
`<dir of the entry>/node_modules/mirror-mind/bin/<entry>.js` exists, that is the entry.
|
|
59
|
+
The twelve wrappers and manifests are regenerated from it, and regenerating on the merge
|
|
60
|
+
leaves the tree unchanged. A hook test models the Windows prefix on POSIX with a fake
|
|
61
|
+
Node that records the entry it was handed; a maintainer commit added the locator's
|
|
62
|
+
run-as-written case, which fails on the previous locator.
|
|
63
|
+
|
|
64
|
+
**The verdict, beside the command.** `isRuntimeVerdict` in `runtimeRoute.ts` says which
|
|
65
|
+
exit is an answer; the dispatcher asks it. A maintainer commit moved the rule there from
|
|
66
|
+
the dispatcher, and made the end-to-end test's finding sure on purpose (`loose_permissions`
|
|
67
|
+
by `chmod`), since v0.39.1's Node search no longer guarantees the one the test had relied
|
|
68
|
+
on.
|
|
69
|
+
|
|
70
|
+
## Conscious Exclusions
|
|
71
|
+
|
|
72
|
+
- Windows is not a supported platform of this repository: CI runs the suite on macOS and
|
|
73
|
+
Ubuntu, and the Windows job runs two files. The installer repository pins the core it
|
|
74
|
+
tests against. Whether that changes is a later decision.
|
|
75
|
+
- The front-door log's `route=usage` lines still count as errors (CR139's second option,
|
|
76
|
+
not taken). On the maintainer's home every such line comes from the test suite:
|
|
77
|
+
[CR195](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr195-the-test-suite-writes-four-lines-into-the-real-home-s-front-door-log.md),
|
|
78
|
+
captured.
|
|
79
|
+
- One macOS CI run on PR #54 failed in the updater smoke's package lane with a bare exit
|
|
80
|
+
1 and passed on re-run. Noted; not yet understood.
|
|
81
|
+
|
|
82
|
+
## Upgrading
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
mirror runtime update
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
On Windows, the installer at mirror-mind-ai/mirror-installer pins the core version it
|
|
89
|
+
ships; this release is the one its Claude Code hooks need.
|
|
90
|
+
|
|
91
|
+
## Next Horizon
|
|
92
|
+
|
|
93
|
+
Alan Nascimento's PR #51 (the stop hook losing a reply) and Moacir Moda's PR #27; CR190,
|
|
94
|
+
CR192, and CR195 in RS010's backlog. CV24 continues on its branch.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.39.4 is a patch release of one pull request by Alan Nascimento, his first here. On
|
|
4
|
+
Claude Code, two ordinary situations made the Stop hook drop a real reply: a prompt typed
|
|
5
|
+
while a turn was still running, whose prompt hook fires at queue time under the running
|
|
6
|
+
turn's id, and a journey entered in plain words, whose Builder load switches the
|
|
7
|
+
conversation mid-turn. Both replies are now logged beside their prompts, and
|
|
8
|
+
`nothing_waiting` in `hooks.log` means what it says again. No schema change, no
|
|
9
|
+
migration, and no model or prompt change.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# v0.39.4 — The Reply Finds Its Prompt
|
|
13
|
+
|
|
14
|
+
**Date:** 2026-11-03
|
|
15
|
+
|
|
16
|
+
## Highlights
|
|
17
|
+
|
|
18
|
+
- **A prompt typed while a turn runs keeps its reply**
|
|
19
|
+
([PR #51](https://github.com/mirror-mind-ai/mirror/pull/51), by @alannascimento1;
|
|
20
|
+
[CR196](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr196-a-prompt-typed-while-a-turn-runs-loses-its-reply-on-claude-code.md)).
|
|
21
|
+
Claude Code fires the prompt hook when such a prompt enters its queue, with the running
|
|
22
|
+
turn's id; the prompt later runs under an id no hook sent, and its Stop found nothing
|
|
23
|
+
waiting. The prompt is now recorded as queued during the running turn, the running turn's
|
|
24
|
+
reply stays with its own question, and the Stop of the turn the queued prompt ran as
|
|
25
|
+
claims it. Never for a command turn, and never a prompt an ordinary one has since
|
|
26
|
+
followed.
|
|
27
|
+
- **Entering a journey in plain words keeps the turn together**
|
|
28
|
+
([CR166](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr166-a-plain-language-journey-switch-strands-its-turn.md)).
|
|
29
|
+
"I want to enter journey X" logs the prompt in one conversation; the model's `build load`
|
|
30
|
+
closes it and binds the journey's; the Stop paired in the new, empty one. A hook prompt
|
|
31
|
+
still owed its reply now moves with the switch. A prompt logged by hand stays where it
|
|
32
|
+
was, so `mm-new` and the logger CLI behave as before. Pi gets this half too: its prompts
|
|
33
|
+
carry turn ids.
|
|
34
|
+
- **`nothing_waiting` means a lost reply again.** On one host, 20 of 22 such lines in a
|
|
35
|
+
week were these two cases; `runtime diagnose`'s `hook_failures_recorded` was mostly
|
|
36
|
+
counting them.
|
|
37
|
+
- No schema change (`queued_in_turn` is one more optional key in the `metadata` JSON
|
|
38
|
+
column `turn_id` already lives in), no migration, and no model or prompt change.
|
|
39
|
+
|
|
40
|
+
## Where We Started
|
|
41
|
+
|
|
42
|
+
Alan Nascimento opened PR #51 on 2026-10-30 with the session transcript that shows the
|
|
43
|
+
mechanism, Claude Code's own `queue-operation` record between the prompt hook and the
|
|
44
|
+
Stop, and a week of `hooks.log` counted by cause. The journey-switch half had been in the
|
|
45
|
+
backlog as CR166 since 2026-10-24, unassigned; the queued-prompt half no record described.
|
|
46
|
+
The review, through four lenses, had nothing to amend: the reply rule gains one branch
|
|
47
|
+
between "by turn id" and "by position", each new path bounded by an explicit guard, and
|
|
48
|
+
ten tests of which five fail without the change.
|
|
49
|
+
|
|
50
|
+
## What Changed
|
|
51
|
+
|
|
52
|
+
All in `ts/src/conversation/logger.ts`. `promptMetadata` records a second prompt under a
|
|
53
|
+
turn still owed its reply as `queued_in_turn`. `handleStopHook`, for a turn id with no
|
|
54
|
+
prompt of its own, claims the oldest unclaimed queued prompt that no ordinary prompt has
|
|
55
|
+
followed, and logs the reply as paired by `queued_prompt`. `switchConversation` moves the
|
|
56
|
+
old conversation's last row to the new one when it is a hook prompt with no reply after
|
|
57
|
+
it. The reply rule's docblock names the new branch.
|
|
58
|
+
|
|
59
|
+
## Conscious Exclusions
|
|
60
|
+
|
|
61
|
+
Both named by the author and recorded in CR166 and CR196:
|
|
62
|
+
|
|
63
|
+
- In the queued case the running turn's reply is still written after the queued prompt,
|
|
64
|
+
because the prompt hook fires at queue time: the rows pair by `turn_id`, but a reader
|
|
65
|
+
ordering by `created_at` sees `Q1, Q2, A1, A2`. Rewriting `created_at` would make it lie
|
|
66
|
+
about when the prompt was typed.
|
|
67
|
+
- The waiting prompt moves after the old conversation's close-time extraction, which
|
|
68
|
+
therefore still saw it. Moving it earlier would mean starting the new conversation
|
|
69
|
+
before closing the old.
|
|
70
|
+
- The two `nothing_waiting` lines in the week that match neither pattern.
|
|
71
|
+
|
|
72
|
+
## Upgrading
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
mirror runtime update
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Next Horizon
|
|
79
|
+
|
|
80
|
+
Moacir Moda's PR #27; CR190, CR192, and CR195 in RS010's backlog. CV24 continues on its
|
|
81
|
+
branch.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirror-mind",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.4",
|
|
4
4
|
"description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mirror Mind"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"command": "/bin/sh",
|
|
11
11
|
"args": [
|
|
12
12
|
"-c",
|
|
13
|
-
"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"
|
|
13
|
+
"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; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; 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"
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -61,6 +61,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
61
61
|
[ -n "$ENTRY" ] && break
|
|
62
62
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
63
63
|
done
|
|
64
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
65
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
66
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
67
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
68
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
69
|
+
shim_entry=""
|
|
70
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
71
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
64
72
|
|
|
65
73
|
if [ -z "$ENTRY" ]; then
|
|
66
74
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -61,6 +61,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
61
61
|
[ -n "$ENTRY" ] && break
|
|
62
62
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
63
63
|
done
|
|
64
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
65
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
66
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
67
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
68
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
69
|
+
shim_entry=""
|
|
70
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
71
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
64
72
|
|
|
65
73
|
if [ -z "$ENTRY" ]; then
|
|
66
74
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -61,6 +61,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
61
61
|
[ -n "$ENTRY" ] && break
|
|
62
62
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
63
63
|
done
|
|
64
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
65
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
66
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
67
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
68
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
69
|
+
shim_entry=""
|
|
70
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
71
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
64
72
|
|
|
65
73
|
if [ -z "$ENTRY" ]; then
|
|
66
74
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
56
56
|
[ -n "$ENTRY" ] && break
|
|
57
57
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
58
58
|
done
|
|
59
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
60
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
61
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
62
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
63
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
64
|
+
shim_entry=""
|
|
65
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
66
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
59
67
|
|
|
60
68
|
if [ -z "$ENTRY" ]; then
|
|
61
69
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
56
56
|
[ -n "$ENTRY" ] && break
|
|
57
57
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
58
58
|
done
|
|
59
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
60
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
61
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
62
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
63
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
64
|
+
shim_entry=""
|
|
65
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
66
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
59
67
|
|
|
60
68
|
if [ -z "$ENTRY" ]; then
|
|
61
69
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
56
56
|
[ -n "$ENTRY" ] && break
|
|
57
57
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
58
58
|
done
|
|
59
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
60
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
61
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
62
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
63
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
64
|
+
shim_entry=""
|
|
65
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
66
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
59
67
|
|
|
60
68
|
if [ -z "$ENTRY" ]; then
|
|
61
69
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
56
56
|
[ -n "$ENTRY" ] && break
|
|
57
57
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
58
58
|
done
|
|
59
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
60
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
61
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
62
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
63
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
64
|
+
shim_entry=""
|
|
65
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
66
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
59
67
|
|
|
60
68
|
if [ -z "$ENTRY" ]; then
|
|
61
69
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
|
56
56
|
[ -n "$ENTRY" ] && break
|
|
57
57
|
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
58
58
|
done
|
|
59
|
+
# On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
|
|
60
|
+
# a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
|
|
61
|
+
# shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
|
|
62
|
+
# package's own bin, so that is the entry when it is there. A POSIX prefix
|
|
63
|
+
# keeps node_modules under lib/ and its bin is already the .js: no change.
|
|
64
|
+
shim_entry=""
|
|
65
|
+
[ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
|
|
66
|
+
[ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
|
|
59
67
|
|
|
60
68
|
if [ -z "$ENTRY" ]; then
|
|
61
69
|
note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "/bin/sh",
|
|
5
5
|
"args": [
|
|
6
6
|
"-c",
|
|
7
|
-
"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"
|
|
7
|
+
"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; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; 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"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirror-mind",
|
|
3
3
|
"description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
|
|
4
|
-
"version": "0.39.
|
|
4
|
+
"version": "0.39.4",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"mirror-mind": {
|
|
7
7
|
"command": "/bin/sh",
|
|
8
8
|
"args": [
|
|
9
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"
|
|
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; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; 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
11
|
]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -212,11 +212,13 @@ function discardMarkerActive(session: RuntimeSessionRow | null): boolean {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
* What a message row may carry beside its text
|
|
216
|
-
*
|
|
217
|
-
*
|
|
215
|
+
* What a message row may carry beside its text: the runtime's turn id (US8),
|
|
216
|
+
* written by the prompt and Stop hooks and read by the reply rule, and the
|
|
217
|
+
* turn a prompt was queued during, when it arrived while that turn still owed
|
|
218
|
+
* its reply. NULL for every message logged another way, as before.
|
|
218
219
|
*/
|
|
219
220
|
export interface MessageMetadata {
|
|
221
|
+
readonly queued_in_turn?: string;
|
|
220
222
|
readonly turn_id?: string;
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -375,7 +377,15 @@ export async function endConversation(
|
|
|
375
377
|
}
|
|
376
378
|
}
|
|
377
379
|
|
|
378
|
-
/**
|
|
380
|
+
/**
|
|
381
|
+
* Python `switch_conversation`: close the old conversation, bind a new one.
|
|
382
|
+
*
|
|
383
|
+
* A prompt still owed its reply moves to the new conversation: when the
|
|
384
|
+
* model switches during a turn (a Builder load asked for in plain words), the
|
|
385
|
+
* turn's Stop pairs its reply in the conversation the session is bound to by
|
|
386
|
+
* then, and the prompt it answers has to be there. Only a hook prompt counts,
|
|
387
|
+
* one that carries a turn id; a prompt logged by hand stays where it was.
|
|
388
|
+
*/
|
|
379
389
|
export async function switchConversation(
|
|
380
390
|
db: WritableDatabase,
|
|
381
391
|
sessionId: string | null,
|
|
@@ -388,6 +398,8 @@ export async function switchConversation(
|
|
|
388
398
|
|
|
389
399
|
const runtimeSession = getRuntimeSession(db, resolved);
|
|
390
400
|
const oldConversationId = runtimeSession?.conversationId ?? null;
|
|
401
|
+
const last = oldConversationId ? lastMessage(db, oldConversationId) : null;
|
|
402
|
+
const waitingPrompt = last && last.role === "user" && turnIdOfRow(last) ? last : null;
|
|
391
403
|
if (oldConversationId) {
|
|
392
404
|
await endConversation(db, oldConversationId, { extract: true }, deps, hooks);
|
|
393
405
|
}
|
|
@@ -404,6 +416,12 @@ export async function switchConversation(
|
|
|
404
416
|
},
|
|
405
417
|
deps,
|
|
406
418
|
);
|
|
419
|
+
if (waitingPrompt) {
|
|
420
|
+
db.prepare("UPDATE messages SET conversation_id = ? WHERE rowid = ?").run(
|
|
421
|
+
newConversationId,
|
|
422
|
+
waitingPrompt.rowid,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
407
425
|
upsertRuntimeSession(
|
|
408
426
|
db,
|
|
409
427
|
resolved,
|
|
@@ -550,7 +568,11 @@ export type StopHookOutcome =
|
|
|
550
568
|
// command -- the reply is lost. The only skip the caller reports.
|
|
551
569
|
| "nothing_waiting";
|
|
552
570
|
}
|
|
553
|
-
| {
|
|
571
|
+
| {
|
|
572
|
+
action: "logged";
|
|
573
|
+
sessionId: string;
|
|
574
|
+
pairedBy: "turn_id" | "queued_prompt" | "last_message";
|
|
575
|
+
}
|
|
554
576
|
| { action: "failed"; error: string };
|
|
555
577
|
|
|
556
578
|
export interface HookLoggingOptions {
|
|
@@ -653,7 +675,7 @@ export function handleUserPromptHook(
|
|
|
653
675
|
db,
|
|
654
676
|
sessionId,
|
|
655
677
|
content,
|
|
656
|
-
{ interface: interfaceName, metadata:
|
|
678
|
+
{ interface: interfaceName, metadata: promptMetadata(db, sessionId, turnIdOf(payload)) },
|
|
657
679
|
deps,
|
|
658
680
|
);
|
|
659
681
|
return { action: "logged", sessionId };
|
|
@@ -725,6 +747,64 @@ function turnIdOfRow(row: MessageRow): string {
|
|
|
725
747
|
return typeof value === "string" ? value : "";
|
|
726
748
|
}
|
|
727
749
|
|
|
750
|
+
/**
|
|
751
|
+
* A prompt's metadata. Claude Code fires the prompt hook when a prompt typed
|
|
752
|
+
* during a running turn enters its queue, with the running turn's id; the
|
|
753
|
+
* prompt later runs as a turn of its own, under an id no prompt hook sends. A
|
|
754
|
+
* second prompt under a turn whose reply is still owed is therefore recorded
|
|
755
|
+
* as queued during it, not as its prompt, so the running turn's reply stays
|
|
756
|
+
* with its own question and the queued turn's Stop can claim this one.
|
|
757
|
+
*/
|
|
758
|
+
function promptMetadata(
|
|
759
|
+
db: WritableDatabase,
|
|
760
|
+
sessionId: string,
|
|
761
|
+
turnId: string,
|
|
762
|
+
): MessageMetadata | null {
|
|
763
|
+
const conversationId = getRuntimeSession(db, sessionId)?.conversationId ?? null;
|
|
764
|
+
if (turnId && conversationId) {
|
|
765
|
+
const running = promptOfTurn(db, conversationId, turnId);
|
|
766
|
+
if (running && !answeredAfter(db, conversationId, running, turnId)) {
|
|
767
|
+
return { queued_in_turn: turnId };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return turnMetadata(turnId);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* The oldest queued prompt no turn has claimed, among those typed after the
|
|
775
|
+
* conversation's last ordinary prompt. A queued prompt Claude Code folded into
|
|
776
|
+
* the running turn instead of running it as its own is never claimed, and a
|
|
777
|
+
* later ordinary prompt retires it, so a Stop long after cannot take it.
|
|
778
|
+
*/
|
|
779
|
+
function unclaimedQueuedPrompt(db: WritableDatabase, conversationId: string): MessageRow | null {
|
|
780
|
+
const row = db
|
|
781
|
+
.prepare(
|
|
782
|
+
`SELECT rowid, role, created_at, metadata FROM messages AS queued
|
|
783
|
+
WHERE conversation_id = ? AND role = 'user'
|
|
784
|
+
AND json_extract(metadata, '$.queued_in_turn') IS NOT NULL
|
|
785
|
+
AND json_extract(metadata, '$.turn_id') IS NULL
|
|
786
|
+
AND NOT EXISTS (
|
|
787
|
+
SELECT 1 FROM messages AS later
|
|
788
|
+
WHERE later.conversation_id = queued.conversation_id AND later.role = 'user'
|
|
789
|
+
AND json_extract(later.metadata, '$.queued_in_turn') IS NULL
|
|
790
|
+
AND (later.created_at > queued.created_at
|
|
791
|
+
OR (later.created_at = queued.created_at AND later.rowid > queued.rowid)))
|
|
792
|
+
ORDER BY created_at, rowid LIMIT 1`,
|
|
793
|
+
)
|
|
794
|
+
.get(conversationId);
|
|
795
|
+
return (row as MessageRow | undefined) ?? null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Give a queued prompt the id of the turn it ran as. */
|
|
799
|
+
function claimQueuedPrompt(db: WritableDatabase, row: MessageRow, turnId: string): void {
|
|
800
|
+
const metadata = decodeMetadata(row.metadata);
|
|
801
|
+
metadata.turn_id = turnId;
|
|
802
|
+
db.prepare("UPDATE messages SET metadata = ? WHERE rowid = ?").run(
|
|
803
|
+
pythonJsonDumps(metadata),
|
|
804
|
+
row.rowid,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
728
808
|
/**
|
|
729
809
|
* The Stop hook (CV21.DS2.US8): log the turn's final answer beside its prompt.
|
|
730
810
|
*
|
|
@@ -734,6 +814,9 @@ function turnIdOfRow(row: MessageRow): string {
|
|
|
734
814
|
* same -- unless a reply with that turn id already follows it, which is a
|
|
735
815
|
* repeat Stop (a Stop re-fired after another hook blocked). An assistant
|
|
736
816
|
* row without the turn id (a hand-logged summary) does not count;
|
|
817
|
+
* * else, when the turn id is new, by the oldest queued prompt no turn has
|
|
818
|
+
* claimed (`promptMetadata`): the turn a prompt typed mid-turn ran as.
|
|
819
|
+
* Never for a turn the prompt hook skipped as a command;
|
|
737
820
|
* * else by position, when the conversation's last message is a logged
|
|
738
821
|
* prompt -- unless both sides carry a turn id and they differ, which means
|
|
739
822
|
* this turn's prompt was a command and an older prompt is waiting because
|
|
@@ -770,11 +853,11 @@ export function handleStopHook(
|
|
|
770
853
|
const session = getRuntimeSession(db, sessionId);
|
|
771
854
|
if (discardMarkerActive(session)) return { action: "skipped", reason: "discarded" };
|
|
772
855
|
const turnId = turnIdOf(payload);
|
|
856
|
+
const commandTurn = (): boolean =>
|
|
857
|
+
Boolean(turnId) && decodeMetadata(session?.metadata ?? null)[SKIPPED_TURN_KEY] === turnId;
|
|
773
858
|
const nothingWaiting = (): StopHookOutcome => {
|
|
774
859
|
if (!turnId) return { action: "skipped", reason: "session_event" };
|
|
775
|
-
if (
|
|
776
|
-
return { action: "skipped", reason: "command_turn" };
|
|
777
|
-
}
|
|
860
|
+
if (commandTurn()) return { action: "skipped", reason: "command_turn" };
|
|
778
861
|
return { action: "skipped", reason: "nothing_waiting" };
|
|
779
862
|
};
|
|
780
863
|
const conversationId = session?.conversationId ?? null;
|
|
@@ -798,6 +881,18 @@ export function handleStopHook(
|
|
|
798
881
|
);
|
|
799
882
|
return { action: "logged", sessionId, pairedBy: "turn_id" };
|
|
800
883
|
}
|
|
884
|
+
const queued = commandTurn() ? null : unclaimedQueuedPrompt(db, conversationId);
|
|
885
|
+
if (queued) {
|
|
886
|
+
claimQueuedPrompt(db, queued, turnId);
|
|
887
|
+
logAssistantMessage(
|
|
888
|
+
db,
|
|
889
|
+
sessionId,
|
|
890
|
+
message,
|
|
891
|
+
{ interface: interfaceName, metadata: turnMetadata(turnId) },
|
|
892
|
+
deps,
|
|
893
|
+
);
|
|
894
|
+
return { action: "logged", sessionId, pairedBy: "queued_prompt" };
|
|
895
|
+
}
|
|
801
896
|
if (last && last.role === "user" && !turnIdOfRow(last)) {
|
|
802
897
|
logAssistantMessage(
|
|
803
898
|
db,
|
package/ts/src/frontDoor/cli.ts
CHANGED
|
@@ -170,7 +170,7 @@ import {
|
|
|
170
170
|
} from "./render/tasksImportSync.ts";
|
|
171
171
|
import { renderWeekView } from "./render/week.ts";
|
|
172
172
|
import { retiredRefusal, routeMemoryCommand } from "./routing.ts";
|
|
173
|
-
import { runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
|
|
173
|
+
import { isRuntimeVerdict, runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
|
|
174
174
|
import { runMemorySearchRoute } from "./searchRoute.ts";
|
|
175
175
|
import { resolveSeedPaths } from "./seedPaths.ts";
|
|
176
176
|
import { isSessionStartWelcome, runSessionStartWelcomeRoute } from "./sessionStartWelcomeRoute.ts";
|
|
@@ -1711,6 +1711,8 @@ interface DispatchOutcome {
|
|
|
1711
1711
|
exitCode: number;
|
|
1712
1712
|
/** Content-free route metadata for the durable front-door log. */
|
|
1713
1713
|
detail?: string;
|
|
1714
|
+
/** The non-zero exit is the command's answer, not a failure (`frontDoorLog.ts`). */
|
|
1715
|
+
verdict?: boolean;
|
|
1714
1716
|
}
|
|
1715
1717
|
|
|
1716
1718
|
async function dispatch(argv: readonly string[]): Promise<DispatchOutcome> {
|
|
@@ -1819,7 +1821,13 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
|
|
|
1819
1821
|
// database, when at all, through a read-only handle -- no bootstrap, no
|
|
1820
1822
|
// migrate-on-open, and for `welcome --status-line` no subprocess whatsoever.
|
|
1821
1823
|
if (argv[0] === "welcome") return runWelcomeRoute(argv);
|
|
1822
|
-
if (argv[0] === "runtime")
|
|
1824
|
+
if (argv[0] === "runtime") {
|
|
1825
|
+
const exitCode = await runRuntimeReadRoute(argv);
|
|
1826
|
+
// A verdict, not a failure: `isRuntimeVerdict` says which (CR139).
|
|
1827
|
+
return isRuntimeVerdict(argv, exitCode)
|
|
1828
|
+
? { exitCode, detail: "verdict=attention", verdict: true }
|
|
1829
|
+
: exitCode;
|
|
1830
|
+
}
|
|
1823
1831
|
if (argv[0] === "mcp") return runMcpServer();
|
|
1824
1832
|
if (isMemorySearch(argv)) return runMemorySearch(argv);
|
|
1825
1833
|
if (isConsult(argv)) {
|
|
@@ -1895,6 +1903,7 @@ export async function main(rawArgv = process.argv.slice(2)): Promise<number> {
|
|
|
1895
1903
|
route: decision.engine,
|
|
1896
1904
|
exitCode: outcome.exitCode,
|
|
1897
1905
|
...(outcome.detail ? { detail: outcome.detail } : {}),
|
|
1906
|
+
...(outcome.verdict ? { verdict: true } : {}),
|
|
1898
1907
|
});
|
|
1899
1908
|
return outcome.exitCode;
|
|
1900
1909
|
} catch (error) {
|
|
@@ -29,6 +29,13 @@ export interface FrontDoorLogEntry {
|
|
|
29
29
|
exitCode: number;
|
|
30
30
|
/** A short, content-free note or error category. Never argument values. */
|
|
31
31
|
detail?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The non-zero exit is the command's answer, not a failure: `runtime
|
|
34
|
+
* diagnose` exits 1 to say "attention needed". Logged at INFO, so the line
|
|
35
|
+
* does not count as a front-door error -- which `front_door_errors` reads,
|
|
36
|
+
* and which each diagnose with findings then fed, one line per run.
|
|
37
|
+
*/
|
|
38
|
+
verdict?: boolean;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
/** The log path for a resolved database path (sibling of the DB). */
|
|
@@ -40,7 +47,7 @@ export function frontDoorLogPath(dbPath: string): string {
|
|
|
40
47
|
export function logFrontDoor(logPath: string | null, entry: FrontDoorLogEntry): void {
|
|
41
48
|
if (!logPath) return;
|
|
42
49
|
try {
|
|
43
|
-
const level = entry.exitCode === 0 ? "INFO" : "ERROR";
|
|
50
|
+
const level = entry.exitCode === 0 || entry.verdict ? "INFO" : "ERROR";
|
|
44
51
|
const detail = entry.detail ? entry.detail.replace(/[\r\n\t]+/g, " ") : "";
|
|
45
52
|
const line = [
|
|
46
53
|
new Date().toISOString(),
|
|
@@ -254,6 +254,17 @@ function logWelcomeCheck(home: string, env: NodeJS.ProcessEnv, check: RemoteChec
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/** Port of the read half of `cmd_runtime`. */
|
|
257
|
+
/**
|
|
258
|
+
* `runtime diagnose` exits 1 to say "attention needed": its contract with
|
|
259
|
+
* scripts, not a failure. The front door logs that exit as a verdict (PR #50,
|
|
260
|
+
* CR139), at INFO, so `front_door_errors` does not count the diagnose that
|
|
261
|
+
* reported it -- every diagnose with findings used to feed the finding, one
|
|
262
|
+
* line per run. The rule lives here, beside the command whose contract it is.
|
|
263
|
+
*/
|
|
264
|
+
export function isRuntimeVerdict(argv: readonly string[], exitCode: number): boolean {
|
|
265
|
+
return argv[0] === "runtime" && argv[1] === "diagnose" && exitCode === 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
257
268
|
export async function runRuntimeReadRoute(
|
|
258
269
|
argv: readonly string[],
|
|
259
270
|
io: InstallRouteIo = {},
|
|
@@ -111,6 +111,11 @@ export const MCP_LOCATOR = [
|
|
|
111
111
|
'[ -n "$mmbin" ] || mmbin="$(command -v mirror 2>/dev/null)"',
|
|
112
112
|
`for mmdir in ${GLOBAL_BIN_DIRS.join(" ")}; do [ -n "$mmbin" ] && break; [ -x "$mmdir/mirror" ] && mmbin="$mmdir/mirror"; done`,
|
|
113
113
|
'[ -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; }',
|
|
114
|
+
// On Windows npm's global `mirror` is a POSIX shell shim with node_modules/
|
|
115
|
+
// beside it, and Node cannot run the shim; the package's own bin is the
|
|
116
|
+
// entry when it is there (a POSIX prefix keeps node_modules under lib/).
|
|
117
|
+
'mmjs="$(dirname "$mmbin")/node_modules/mirror-mind/bin/mirror.js"',
|
|
118
|
+
'[ -f "$mmjs" ] && mmbin="$mmjs"',
|
|
114
119
|
'mmnode="$MIRROR_NODE"',
|
|
115
120
|
'[ -n "$mmnode" ] && [ -x "$mmnode" ] || mmnode="$(command -v node 2>/dev/null)"',
|
|
116
121
|
`for mmdir in "$(dirname "$mmbin")" ${GLOBAL_BIN_DIRS.join(" ")}; do [ -n "$mmnode" ] && break; [ -x "$mmdir/node" ] && mmnode="$mmdir/node"; done`,
|