mirror-mind 0.33.3 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +16 -0
- package/AGENTS.md +4 -4
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.33.4.md +104 -0
- package/docs/releases/v0.34.0.md +142 -0
- package/package.json +2 -3
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/ts/src/conversation/backfill.ts +13 -129
- package/ts/src/conversation/commandTurn.ts +42 -0
- package/ts/src/conversation/deferredCloseTail.ts +154 -0
- package/ts/src/conversation/logger.ts +104 -20
- package/ts/src/conversation/loggerCli.ts +6 -18
- package/ts/src/conversation/loggerRuntime.ts +6 -1
- package/ts/src/conversation/runtimePolicy.ts +50 -0
- package/ts/src/conversation/sessionComposites.ts +54 -8
- package/ts/src/frontDoor/cli.ts +12 -2
- package/ts/src/frontDoor/routing.ts +3 -2
- package/ts/src/frontDoor/runtimeRoute.ts +13 -1
- package/ts/src/guards/claudePlugin.ts +39 -2
- package/ts/src/guards/packContents.ts +4 -4
- package/ts/src/guards/retiredSurfaces.ts +36 -0
- package/ts/src/hooks/claude.ts +16 -4
- package/ts/src/hooks/gemini.ts +3 -1
- package/ts/src/hooks/main.ts +6 -16
- package/ts/src/identity/identityEdit.ts +11 -2
- package/ts/src/identity/identityKey.ts +18 -2
- package/ts/src/providers/config.ts +11 -0
- package/ts/src/runtime/diagnose.ts +115 -1
- package/ts/src/runtime/pluginWiring.ts +135 -0
- package/ts/src/runtime/update.ts +39 -6
- package/ts/src/runtime/wiring.ts +24 -10
- package/scripts/codex-hooks/session-end.sh +0 -85
- package/scripts/codex-hooks/session-start.sh +0 -85
- package/scripts/codex-mirror.sh +0 -48
- package/ts/src/hooks/codex.ts +0 -45
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mirror-mind",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Mirror Mind"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "mirror-mind",
|
|
9
|
+
"source": {
|
|
10
|
+
"source": "local",
|
|
11
|
+
"path": "./plugins/mirror-mind"
|
|
12
|
+
},
|
|
13
|
+
"description": "Mirror Mind — local-first memory and identity for agentic runtimes."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
package/AGENTS.md
CHANGED
|
@@ -18,10 +18,10 @@ The mirror operates in four modes, chosen automatically based on context:
|
|
|
18
18
|
Mirror, Builder, Explorer, and Soul.
|
|
19
19
|
|
|
20
20
|
Each skill below is named three ways: `/mm-<skill>` in Pi and Gemini CLI,
|
|
21
|
-
`$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A
|
|
22
|
-
|
|
23
|
-
`mirror-mind:mm-<skill>` as the skill's own
|
|
24
|
-
the same skill.
|
|
21
|
+
`$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install
|
|
22
|
+
prefixes the plugin's name — `/mirror-mind:mm:<skill>` in Claude Code's menu,
|
|
23
|
+
`$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
|
|
24
|
+
name — and the forms below refer to the same skill.
|
|
25
25
|
|
|
26
26
|
**Mirror Mode** — activate for: life decisions, feelings, business strategy,
|
|
27
27
|
writing, mentoring, health, existential questions, sensemaking, psychological
|
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.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.
|
|
16
|
+
- [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.
|
|
15
17
|
- [v0.33.3 — Mirror Finds Itself Under Any Path](v0.33.3.md) — patch release of one Change Request, CR140, carried by a contributor's PR #42: Mirror read its own install path from `URL.pathname`, so a checkout or package under a path with a space or a non-ASCII character read as `Install: unknown`, the welcome card showed no update, and `runtime update` refused. The path is now a filesystem path, a guard keeps the idiom out, and an `unknown` install names the path it looked up from. On Windows, npm's global layout is recognized and npm runs with no shell, verified on Windows in CI for the two files they touch; there is still no Windows product. Affected installs update once by hand. No migration.
|
|
16
18
|
- [v0.33.2 — Git Runs Where Mirror Lives](v0.33.2.md) — patch release of one Change Request, CR138: on a clone install, `runtime status`, `version`, and `diagnose` describe the install from any directory, and `diagnose` grades Mirror's own tree. Git now runs only in the install's own checkout, so a checkout someone else made can no longer run a `core.fsmonitor` command through these commands; `version --start PATH` is the one explicit exception. Package installs were never exposed. No migration.
|
|
17
19
|
- [v0.33.1 — The Install Answers For Itself](v0.33.1.md) — patch release of six RS010 Change Requests: the welcome card, the status line, and both `release-notes` readers answer for the install that is running, never for the cwd or the repository around the install. A package asks its own tree and the npm registry, a clone its own checkout and remote; `pending` says `unknown` when it cannot tell, and a `--ref` value can no longer run a command through git. No migration.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.33.4 is a maintenance release of three first-run corrections, two from a
|
|
4
|
+
contributor's PR #43 and one Change Request, CR141, that its review opened.
|
|
5
|
+
`mirror init --help` created a home named `--help`; `init ../escaped` created
|
|
6
|
+
one outside `.mirror-minds`; `init "My User"` put a space into `MIRROR_USER`
|
|
7
|
+
for every wiring command. A user name is now a kebab-case key, like a journey
|
|
8
|
+
slug or a persona id, refused otherwise with a name that would work and
|
|
9
|
+
nothing created. And `identity edit` on Windows opens Notepad when no editor
|
|
10
|
+
is named, instead of failing to start nano. Existing homes are untouched; no
|
|
11
|
+
migration.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v0.33.4 — A Name Is a Key
|
|
15
|
+
|
|
16
|
+
**Date:** 2026-10-17
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- **`init` takes a kebab-case key for a user name, and nothing else** (CR141, from the
|
|
21
|
+
review of PR #43). `init ../escaped` created `~/escaped`, outside the directory the
|
|
22
|
+
product owns; `init "My User"` put a space into `MIRROR_USER`, which every wiring
|
|
23
|
+
command `init` prints then carried; `init a/b` nested. Each is refused before anything
|
|
24
|
+
is created, with the sentence `identity set` already uses: the consequence, the rule,
|
|
25
|
+
and a key that would work.
|
|
26
|
+
- **`init --help` no longer creates a home named `--help`** (PR #43 by
|
|
27
|
+
@rodrigoimmaginario). The command has no flags; a word that starts with `-` gets the
|
|
28
|
+
usage line and exit 2.
|
|
29
|
+
- **`identity edit` opens Notepad on Windows when no editor is named** (PR #43). The
|
|
30
|
+
order is unchanged, `EDITOR`, then `VISUAL`, then the platform's default: `notepad` on
|
|
31
|
+
Windows, where nano exists only inside Git Bash, `nano` elsewhere.
|
|
32
|
+
- **Existing homes are untouched.** `init` only creates; a home with another kind of name
|
|
33
|
+
keeps it.
|
|
34
|
+
|
|
35
|
+
## Where We Started
|
|
36
|
+
|
|
37
|
+
A contributor's first run on Windows met two small things and sent a PR for each:
|
|
38
|
+
`init --help` created a home, and `identity edit` with no `EDITOR` set failed to start
|
|
39
|
+
nano. The first was not a Windows bug at all; it reproduced on macOS in a second.
|
|
40
|
+
|
|
41
|
+
Its review asked what else `init` accepted. The answer was anything. The name becomes
|
|
42
|
+
`~/.mirror-minds/<user>` and the value of `MIRROR_USER`, and the repository had already
|
|
43
|
+
decided, in CR104, that a key which goes into paths and shell commands must be one plain
|
|
44
|
+
word a shell cannot split, substitute, or read as an option. Journey slugs and persona
|
|
45
|
+
ids had met that rule since `v0.32.0`. The user name was the last key of its kind that
|
|
46
|
+
did not.
|
|
47
|
+
|
|
48
|
+
## What Changed
|
|
49
|
+
|
|
50
|
+
**One sentence for every key.** `newUserNameProblem` joins `newIdentityKeyProblem` in
|
|
51
|
+
`ts/src/identity/identityKey.ts`, both over one builder, so `init`, `identity set`,
|
|
52
|
+
`identity edit`, and `seed` refuse a non-key the same way. The key is printed escaped: a
|
|
53
|
+
name with a newline or a `;` in it cannot forge output or a command. `runInit` asks before
|
|
54
|
+
`initUserHome`, so a refusal leaves `$HOME` as it was: no home, no identity directory, no
|
|
55
|
+
config file.
|
|
56
|
+
|
|
57
|
+
**The test is the PR's, widened.** `../escaped`, `My User`, `a/b`, and an accented name go
|
|
58
|
+
through the real front door; each asserts the exit, the sentence with its suggestion, and
|
|
59
|
+
an empty home afterwards. PR #43's own `init --help` test stands beside it.
|
|
60
|
+
|
|
61
|
+
**Notepad, with its evidence still owed.** The platform is a dependency of
|
|
62
|
+
`resolveEditor`, so both defaults are tested on every OS. The PR verified the Windows
|
|
63
|
+
round trip on an unchanged buffer, which prints `No changes detected.` whether Notepad
|
|
64
|
+
blocked or returned at once; early Windows 11 Notepad did return at once. A round trip
|
|
65
|
+
that changes the buffer is asked for on the PR, and the default ships as the better of
|
|
66
|
+
the two available answers: nano could not start at all.
|
|
67
|
+
|
|
68
|
+
### What you will notice
|
|
69
|
+
|
|
70
|
+
- A first `mirror init` with a name that is not a plain lowercase word is refused with
|
|
71
|
+
a name that would work. `mirror init your-name`, as Getting Started has always said,
|
|
72
|
+
is unchanged.
|
|
73
|
+
- On Windows, `mirror identity edit …` opens Notepad instead of failing.
|
|
74
|
+
- Nothing else.
|
|
75
|
+
|
|
76
|
+
## Upgrading
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mirror runtime update # or: npm install -g mirror-mind
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
There is no database migration and no runtime wiring change. If `v0.33.3`'s note applied
|
|
83
|
+
to you, a Mirror under a path with a space or an accent, and you have not yet taken its
|
|
84
|
+
one manual update, take it first; from `v0.33.3` on, `runtime update` works.
|
|
85
|
+
|
|
86
|
+
## Conscious Exclusions
|
|
87
|
+
|
|
88
|
+
- **Selecting an existing home by name.** `MIRROR_USER` pointing at a home created
|
|
89
|
+
before this rule, under whatever name, still selects it. Only creation is constrained.
|
|
90
|
+
- **`init`'s wiring lines on Windows.** They print `ln -s`, which Windows has no answer
|
|
91
|
+
to; that waits for the runtimes to be verified there, as the contributor's PR says.
|
|
92
|
+
- **Counting verdicts as front-door errors** (CR139), unchanged since `v0.33.2`.
|
|
93
|
+
|
|
94
|
+
## What We Learned
|
|
95
|
+
|
|
96
|
+
A fix for an instance invites the question "what else?", and the cheapest moment to ask
|
|
97
|
+
it is the review. The `-` check was right; the class was one line away, and the rule
|
|
98
|
+
already existed with its sentence, its escaping, and its tests. When a repository has
|
|
99
|
+
decided something once, the second instance should cost a reference, not a decision.
|
|
100
|
+
|
|
101
|
+
## Next Horizon
|
|
102
|
+
|
|
103
|
+
CR139, the verdict loop, is the visible next step in RS010. The contributor's Windows
|
|
104
|
+
series, if it comes, begins with the suite on `windows-latest`.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.34.0 is a minor release closing CV21.DS4: Codex runs Mirror through the
|
|
4
|
+
canonical plugin, the same one Claude Code loads. The package ships a Codex
|
|
5
|
+
marketplace; two commands install the plugin, Codex asks once to trust its
|
|
6
|
+
hooks, and from then on every session carries the skills, the Operating
|
|
7
|
+
Instructions, per-turn Mirror Mode injection, the MCP server, and logging as
|
|
8
|
+
`codex` with every reply beside its prompt — measured live at L4 on Codex
|
|
9
|
+
0.163.0. The v0.32.1 wrapper wiring is removed: `scripts/codex-mirror.sh`,
|
|
10
|
+
its hooks, and the JSONL backfill no longer ship. Because Codex copies the
|
|
11
|
+
plugin into a versioned cache, `runtime diagnose` and `runtime update` now
|
|
12
|
+
say when that copy is behind and name the one command that brings it level.
|
|
13
|
+
No migration.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# v0.34.0 — Codex Through the Canonical Plugin
|
|
17
|
+
|
|
18
|
+
**Date:** 2026-10-17
|
|
19
|
+
|
|
20
|
+
## Highlights
|
|
21
|
+
|
|
22
|
+
- **Codex installs Mirror as a plugin, from a marketplace the package ships**
|
|
23
|
+
(CV21.DS4.US1). Two commands — `codex plugin marketplace add "<pkg>"`, then
|
|
24
|
+
`codex plugin add mirror-mind@mirror-mind` — and Codex asks once, at the
|
|
25
|
+
next session, to trust the plugin's hooks. `mirror init` prints exactly
|
|
26
|
+
these steps when it finds `codex` on the PATH.
|
|
27
|
+
- **Codex is at L4, measured live** (CV21.DS4.US4, on Codex 0.163.0): all 25
|
|
28
|
+
skills, the Operating Instructions as a developer message, Mirror Mode on
|
|
29
|
+
an ordinary turn and on `$mirror-mind:mm:mirror`, the 7 MCP tools behind
|
|
30
|
+
Codex's `tool_search`, and every prompt and reply logged as `codex`. Codex
|
|
31
|
+
detaches the TUI on `/quit` and fires SessionEnd only when its daemon reaps
|
|
32
|
+
the idle thread, so Mirror gained a safety net for sessions that outlive
|
|
33
|
+
their TUI: session maintenance closes them as orphans
|
|
34
|
+
(`MIRROR_STALE_ORPHAN_MINUTES`, default 30).
|
|
35
|
+
- **A bounded SessionEnd no longer cuts extraction** (CV21.DS4.TS2). Codex
|
|
36
|
+
bounds the hook at 1 s; the close now runs no LLM work, and the next
|
|
37
|
+
session start finishes the deferred tail. Grok's 1.5 s bound rides the same
|
|
38
|
+
change.
|
|
39
|
+
- **`$`-skill turns are command turns** (CV21.DS4.TS3), as `/`-commands are
|
|
40
|
+
on every other runtime, with Mirror's explicit form the one exception.
|
|
41
|
+
- **The v0.32.1 Codex wiring is removed** (CV21.DS4.TS5). What no longer
|
|
42
|
+
exists: `scripts/codex-mirror.sh`, `scripts/codex-hooks/`, the
|
|
43
|
+
`codex:session-*` hook entries, and
|
|
44
|
+
`conversation-logger backfill-codex-session` — the plugin logs Codex turns
|
|
45
|
+
as they happen, so nothing re-imports the transcript at exit. A
|
|
46
|
+
retired-surfaces guard keeps the wiring from drifting back.
|
|
47
|
+
- **An updated Mirror now reaches Codex's plugin cache — or says so**
|
|
48
|
+
(CV21.DS4.TS6). Codex copies the plugin into its cache at a version, so an
|
|
49
|
+
npm update changes the install but not the copy Codex runs. `runtime
|
|
50
|
+
diagnose` reports `codex_plugin_cache_behind` naming both versions and the
|
|
51
|
+
measured command — `codex plugin add mirror-mind@mirror-mind`; re-adding
|
|
52
|
+
refreshes the cache in place, no remove needed — and a successful `runtime
|
|
53
|
+
update` prints the same line. A dangling Claude Code skills link is
|
|
54
|
+
reported too (`claude_plugin_link_dangling`). Absence is not drift: an
|
|
55
|
+
unwired machine stays green.
|
|
56
|
+
|
|
57
|
+
## Where We Started
|
|
58
|
+
|
|
59
|
+
v0.33.0 proved one plugin works as it is on Claude Code, Codex, and Grok.
|
|
60
|
+
But a Codex user still wired Mirror by hand: a skills link, an `AGENTS.md`
|
|
61
|
+
link, and a wrapper script around every session that imported the transcript
|
|
62
|
+
from JSONL at exit — v0.32.1's wiring, built when Codex had no hook system.
|
|
63
|
+
Codex 0.157 added plugins; DS4 asked whether the canonical plugin could be
|
|
64
|
+
the whole answer, and set a gate: the old wiring retires only once parity is
|
|
65
|
+
shown live.
|
|
66
|
+
|
|
67
|
+
## What Changed
|
|
68
|
+
|
|
69
|
+
**The install is two commands.** The npm package root carries
|
|
70
|
+
`.agents/plugins/marketplace.json`, one local plugin — the same
|
|
71
|
+
`plugins/mirror-mind` Claude Code loads. Codex copies it into its cache at
|
|
72
|
+
the plugin's version and runs its Claude-family hooks as they are; runtime
|
|
73
|
+
detection reads Codex's `turn_id` and labels the session `codex`.
|
|
74
|
+
|
|
75
|
+
**The gate was met, so the old path is gone.** US4 measured L4 twice, live,
|
|
76
|
+
through the shipped plugin alone. TS5 then removed the wrapper, its generated
|
|
77
|
+
hooks, the `codex:*` hook family, the JSONL backfill (its only caller was the
|
|
78
|
+
wrapper's exit hook; the plugin's SessionEnd closes without reading it),
|
|
79
|
+
their pack-contents guards, and their CI step. The docs name the marketplace
|
|
80
|
+
install and nothing else.
|
|
81
|
+
|
|
82
|
+
**The one stale copy is watched.** Claude Code reads the plugin through a
|
|
83
|
+
link, in place; Codex runs a versioned copy. TS6 taught `runtime diagnose`
|
|
84
|
+
and `runtime update`'s post-update hints to compare that copy against the
|
|
85
|
+
installed version — the landed version, read from the just-updated tree —
|
|
86
|
+
and to print the one command that works on Codex 0.163.0, which has no
|
|
87
|
+
plugin-update verb: the re-add.
|
|
88
|
+
|
|
89
|
+
## Upgrading
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install -g mirror-mind@latest # or: mirror runtime update
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**If Codex runs the plugin already:** after this update (and any future one),
|
|
96
|
+
bring the cache level once:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
codex plugin add mirror-mind@mirror-mind
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`mirror runtime diagnose` will tell you whenever this is needed.
|
|
103
|
+
|
|
104
|
+
**If you wired Codex before the plugin** (a skills link, an `AGENTS.md` link,
|
|
105
|
+
the wrapper script): install the plugin with the two commands above, then
|
|
106
|
+
undo the old wiring — the exact steps are in Getting Started's Codex section.
|
|
107
|
+
If you do nothing, everything still works, at the cost of doubled skills and
|
|
108
|
+
double logging where the old wrapper still runs; an older installed package
|
|
109
|
+
keeps its own wrapper copy, but this version ships none.
|
|
110
|
+
|
|
111
|
+
No migration. Existing homes, sessions, and memories are untouched.
|
|
112
|
+
|
|
113
|
+
## Conscious Exclusions
|
|
114
|
+
|
|
115
|
+
- **Codex logging is no longer end-to-end tested**, by decision: the retired
|
|
116
|
+
wrapper smoke was the only artifact-level Codex check. Confidence now rests
|
|
117
|
+
on the shared Claude-family hook path (smoked), runtime-detection units,
|
|
118
|
+
and US4's live measurement.
|
|
119
|
+
- **Mirror never runs `codex` itself.** The cache readers are read-only
|
|
120
|
+
filesystem checks; the person runs the printed command.
|
|
121
|
+
- **Two named silences in the wiring checks:** a cache *ahead* of the install
|
|
122
|
+
(a deliberate downgrade gets no advice from the older tree), and a Claude
|
|
123
|
+
link that resolves to another install tree (healthy means the target
|
|
124
|
+
reads). Both are recorded as decisions with tests.
|
|
125
|
+
- **CR142 is captured, not fixed:** the Claude plugin smoke's negative probe
|
|
126
|
+
fails on a machine with a globally installed `mirror-hook` — a smoke
|
|
127
|
+
isolation bug found during TS5's validation, pre-existing, filed under
|
|
128
|
+
RS010.
|
|
129
|
+
|
|
130
|
+
## What We Learned
|
|
131
|
+
|
|
132
|
+
The gate ordering did the work: retiring the old wiring only after L4 was
|
|
133
|
+
measured live meant the removal was a deletion, not a bet. And the one real
|
|
134
|
+
surprise of the arc — Codex sessions outlive their TUI — was caught by the
|
|
135
|
+
live measurement, not by any test, which is why the measurement is part of
|
|
136
|
+
the method.
|
|
137
|
+
|
|
138
|
+
## Next Horizon
|
|
139
|
+
|
|
140
|
+
DS4 closes with this release. CV21's remaining runtime stories — Antigravity
|
|
141
|
+
through a native layer (DS7), Grok (DS10) — are pulled deliberately from the
|
|
142
|
+
CV21 index.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirror-mind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Mirror Mind: a local-first memory and identity framework for agentic AI runtimes (Pi, Claude Code, Gemini CLI, Codex)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
".claude/hooks",
|
|
39
39
|
".gemini/hooks",
|
|
40
40
|
"plugins/mirror-mind",
|
|
41
|
-
"
|
|
42
|
-
"scripts/codex-mirror.sh",
|
|
41
|
+
".agents/plugins",
|
|
43
42
|
"AGENTS.md",
|
|
44
43
|
"README.md",
|
|
45
44
|
"LICENSE"
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
// Session backfills from runtime-written JSONL (CV22.DS7.US10 slice E).
|
|
2
2
|
//
|
|
3
|
-
// Ports `backfill_pi_sessions`
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Ports `backfill_pi_sessions` from `src/memory/cli/conversation_logger.py`:
|
|
4
|
+
// it imports closed sessions the live hooks never saw, through the atomic
|
|
5
|
+
// seam in `sessionImport.ts`, and is graded against
|
|
6
|
+
// `ts/test/goldens/backfill.golden.json`. (The Codex importer,
|
|
7
|
+
// `backfill_codex_session`, was ported beside it and retired with the
|
|
8
|
+
// v0.32.1 Codex wrapper by CV21.DS4.TS5: its only caller was the wrapper's
|
|
9
|
+
// session-end hook, and the plugin logs Codex turns live.)
|
|
7
10
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// through `_generate_title` and a bare column write with no provenance.
|
|
13
|
-
// * Pi tolerates a corrupt or structurally invalid file by skipping THAT
|
|
14
|
-
// file and continuing the walk; Codex gives up on its single file.
|
|
15
|
-
// * Pi parses every timestamp form (millisecond epoch, string, absent);
|
|
16
|
-
// Codex stores the raw `timestamp` value and lets the model reject a
|
|
17
|
-
// missing one.
|
|
11
|
+
// Pi titles through `set_provisional_title` (whitespace collapse plus
|
|
12
|
+
// `title_source`/`title_status` provenance in metadata), tolerates a corrupt
|
|
13
|
+
// or structurally invalid file by skipping THAT file and continuing the
|
|
14
|
+
// walk, and parses every timestamp form (millisecond epoch, string, absent).
|
|
18
15
|
//
|
|
19
16
|
// Nothing here touches a provider or the clock directly: `now` is injected
|
|
20
17
|
// through `LoggerDeps`, which is what makes "no timestamp -> now" pinnable.
|
|
@@ -24,12 +21,11 @@
|
|
|
24
21
|
// home meant the person's whole Pi history, copied and spent on before the
|
|
25
22
|
// first prompt. The floor is explicit (`notBefore`), never defaulted, and
|
|
26
23
|
// the rule fails closed: there is no value that means "everything" except
|
|
27
|
-
// the epoch, which says so.
|
|
28
|
-
// one session the wrapper names, not a history.
|
|
24
|
+
// the epoch, which says so.
|
|
29
25
|
|
|
30
26
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
31
27
|
import { join, relative, sep } from "node:path";
|
|
32
|
-
import {
|
|
28
|
+
import { type LoggerDeps, setProvisionalTitle } from "#conversation/logger.ts";
|
|
33
29
|
import { type ImportedMessage, importClosedConversation } from "#conversation/sessionImport.ts";
|
|
34
30
|
import type { WritableDatabase } from "#db/database.ts";
|
|
35
31
|
import { expandHome } from "#util/paths.ts";
|
|
@@ -76,13 +72,6 @@ function getFrom(container: unknown, key: string): unknown {
|
|
|
76
72
|
return getOr(container, key, undefined);
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
/** Python truthiness for JSON values: empty containers are falsy too. */
|
|
80
|
-
function pythonTruthy(value: unknown): boolean {
|
|
81
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
82
|
-
if (isRecord(value)) return Object.keys(value).length > 0;
|
|
83
|
-
return Boolean(value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
75
|
/** Python `_parse_pi_content`: a string as-is, text blocks joined by `\n`, else `""`. */
|
|
87
76
|
export function parsePiContent(content: unknown): string {
|
|
88
77
|
if (typeof content === "string") return content;
|
|
@@ -280,108 +269,3 @@ export function backfillPiSessions(
|
|
|
280
269
|
}
|
|
281
270
|
return count;
|
|
282
271
|
}
|
|
283
|
-
|
|
284
|
-
interface CodexParse {
|
|
285
|
-
sessionId: string | null;
|
|
286
|
-
messages: { role: string; content: unknown; createdAt: unknown }[];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Parse a Codex session file. Throws on the first line that is not valid
|
|
291
|
-
* JSON or not the expected shape; Python's single `try` turns that into
|
|
292
|
-
* "return 0". Values are collected raw: Python validates them only when the
|
|
293
|
-
* import constructs its models.
|
|
294
|
-
*/
|
|
295
|
-
function parseCodexSessionFile(path: string): CodexParse {
|
|
296
|
-
const parsed: CodexParse = { sessionId: null, messages: [] };
|
|
297
|
-
for (const line of readFileSync(path, "utf8").split(/\r?\n|\r/)) {
|
|
298
|
-
if (!line.trim()) continue;
|
|
299
|
-
const entry: unknown = JSON.parse(line);
|
|
300
|
-
const entryType = getFrom(entry, "type");
|
|
301
|
-
const payload = getOr(entry, "payload", {});
|
|
302
|
-
if (entryType === "session_meta") {
|
|
303
|
-
// Codex writes a UUID string; a non-string id is treated as absent
|
|
304
|
-
// (the same simplification the hook payloads make).
|
|
305
|
-
const id = getFrom(payload, "id");
|
|
306
|
-
parsed.sessionId = typeof id === "string" ? id : null;
|
|
307
|
-
} else if (entryType === "event_msg") {
|
|
308
|
-
const payloadType = getFrom(payload, "type");
|
|
309
|
-
if (payloadType === "user_message" || payloadType === "agent_message") {
|
|
310
|
-
const content = getOr(payload, "message", "");
|
|
311
|
-
// Python: `if content:` -- any truthy value is kept and validated later.
|
|
312
|
-
if (pythonTruthy(content)) {
|
|
313
|
-
parsed.messages.push({
|
|
314
|
-
role: payloadType === "user_message" ? "user" : "assistant",
|
|
315
|
-
content,
|
|
316
|
-
createdAt: getFrom(entry, "timestamp"),
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return parsed;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export interface CodexBackfillOptions {
|
|
326
|
-
jsonlPath: string;
|
|
327
|
-
interface?: string;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Python `backfill_codex_session`: import one Codex session file as a closed
|
|
332
|
-
* conversation titled through `_generate_title`. Returns 1 on import, else 0.
|
|
333
|
-
*
|
|
334
|
-
* Throws when a kept event has a non-string `message` or `timestamp`: Python's
|
|
335
|
-
* `Message(...)` model rejects those inside the import transaction, so the
|
|
336
|
-
* database is left untouched and the caller sees the failure.
|
|
337
|
-
*/
|
|
338
|
-
export function backfillCodexSession(
|
|
339
|
-
db: WritableDatabase,
|
|
340
|
-
options: CodexBackfillOptions,
|
|
341
|
-
deps: LoggerDeps,
|
|
342
|
-
): number {
|
|
343
|
-
const jsonlPath = expandHome(options.jsonlPath);
|
|
344
|
-
if (!existsSync(jsonlPath) || !statSync(jsonlPath).isFile()) return 0;
|
|
345
|
-
|
|
346
|
-
let parsed: CodexParse;
|
|
347
|
-
try {
|
|
348
|
-
parsed = parseCodexSessionFile(jsonlPath);
|
|
349
|
-
} catch {
|
|
350
|
-
return 0;
|
|
351
|
-
}
|
|
352
|
-
if (!parsed.sessionId || parsed.messages.length === 0) return 0;
|
|
353
|
-
if (db.prepare("SELECT 1 FROM runtime_sessions WHERE session_id = ?").get(parsed.sessionId)) {
|
|
354
|
-
return 0;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const messages: ImportedMessage[] = parsed.messages.map((message) => {
|
|
358
|
-
if (typeof message.content !== "string") {
|
|
359
|
-
throw new TypeError("Codex event message must be a string");
|
|
360
|
-
}
|
|
361
|
-
if (typeof message.createdAt !== "string") {
|
|
362
|
-
throw new TypeError("Codex event timestamp must be a string");
|
|
363
|
-
}
|
|
364
|
-
return { role: message.role, content: message.content, createdAt: message.createdAt };
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const conversationId = importClosedConversation(
|
|
368
|
-
db,
|
|
369
|
-
parsed.sessionId,
|
|
370
|
-
{
|
|
371
|
-
interface: options.interface ?? "codex",
|
|
372
|
-
messages,
|
|
373
|
-
endedAt: (messages.at(-1) as ImportedMessage).createdAt,
|
|
374
|
-
},
|
|
375
|
-
deps,
|
|
376
|
-
);
|
|
377
|
-
if (conversationId === null) return 0;
|
|
378
|
-
|
|
379
|
-
const firstUser = messages.find((message) => message.role === "user");
|
|
380
|
-
if (firstUser) {
|
|
381
|
-
db.prepare("UPDATE conversations SET title = ? WHERE id = ?").run(
|
|
382
|
-
generateTitle(firstUser.content),
|
|
383
|
-
conversationId,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
return 1;
|
|
387
|
-
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A command turn: a prompt that opens with a skill invocation, in any runtime's
|
|
2
|
+
// sigil (CV21.DS4.TS3).
|
|
3
|
+
//
|
|
4
|
+
// The prompt hook skips a command turn and records it, so the Stop hook skips
|
|
5
|
+
// its reply too (`command_turn`, CV21.DS2.US8): a skill's rendering -- a
|
|
6
|
+
// journey table, help text -- is not conversation, and extraction reading it
|
|
7
|
+
// as if the person said it is noise. What you say to the mirror is remembered;
|
|
8
|
+
// a skill you invoke is not. The one exception is the Mirror skill's explicit
|
|
9
|
+
// form, whose question IS the conversation turn (`mirrorForm.ts`); the prompt
|
|
10
|
+
// hook checks that form first, and a test holds every Mirror form to be a
|
|
11
|
+
// command turn, so the exception always sits inside this rule.
|
|
12
|
+
//
|
|
13
|
+
// The two sides are deliberately asymmetric:
|
|
14
|
+
//
|
|
15
|
+
// `/` skips ANYTHING. Every runtime uses `/` for its own built-ins as well as
|
|
16
|
+
// for skills (`/hooks`, `/mcp`, `/compact`), so a prompt opening with it
|
|
17
|
+
// is a command whatever follows. This is the rule US8 inherited.
|
|
18
|
+
// `$` skips only a SKILL NAME. `$` is Codex's skill sigil and nothing else
|
|
19
|
+
// (`$mirror-mind:mm:journeys` through the plugin, DS12.TS2 L10;
|
|
20
|
+
// `$mm-journeys` through a skills link), and `$5 a month` is English.
|
|
21
|
+
// The name is lowercase segments -- letters, digits, hyphens, and
|
|
22
|
+
// underscores after the first letter, since Codex discovers
|
|
23
|
+
// `~/.codex/skills/<dir>` by directory name -- with an optional
|
|
24
|
+
// plugin-name prefix joined by `:`, ending at whitespace or the end of
|
|
25
|
+
// the prompt, the boundary `mirrorForm` draws. A comma is not a boundary
|
|
26
|
+
// and a leading space is not a prompt start, as ` /help` is not a
|
|
27
|
+
// command today.
|
|
28
|
+
//
|
|
29
|
+
// One rule for every runtime: a Claude Code user who types `$mm-journeys`,
|
|
30
|
+
// which Claude Code does not understand, is still invoking a skill, and that
|
|
31
|
+
// turn is not conversation either. The rule has one home for every hook that
|
|
32
|
+
// runs in our process (the Claude-family logger, the Gemini hook); the Pi
|
|
33
|
+
// extension, a separate process in Pi's file, keeps its own `/` check.
|
|
34
|
+
|
|
35
|
+
const SKILL_SEGMENT = "[a-z][a-z0-9_-]*";
|
|
36
|
+
const DOLLAR_SKILL_FORM = new RegExp(`^\\$${SKILL_SEGMENT}(?::${SKILL_SEGMENT})*(?=\\s|$)`);
|
|
37
|
+
|
|
38
|
+
/** Whether the prompt opens with a skill invocation in any runtime's sigil. */
|
|
39
|
+
export function isCommandTurn(prompt: string): boolean {
|
|
40
|
+
if (prompt.startsWith("/")) return true;
|
|
41
|
+
return DOLLAR_SKILL_FORM.test(prompt);
|
|
42
|
+
}
|