@pi-unipi/unipi 2.20.4 → 2.20.5
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/CHANGELOG.md +11 -0
- package/package.json +24 -24
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/background-tasks/package.json +2 -2
- package/packages/btw/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +3 -3
- package/packages/fusion/package.json +3 -3
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/README.md +33 -23
- package/packages/memory/bridge/mempalace_bridge.py +56 -9
- package/packages/memory/mempalace.ts +541 -37
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +110 -63
- package/packages/milestone/package.json +2 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +2 -2
- package/packages/trajectory/package.json +1 -1
- package/packages/unipi/bundled.js +330 -120
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.20.5] — 2026-09-19
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `memory`: **MemPalace init no longer blocks startup by ~8–9s, and its migration finally converges.** `MemoryStorage.init()` ran the palace `ping` and the full `migrate` synchronously inside the memory `session_start` handler; whenever the source fingerprint had changed (i.e. after almost any memory write) the migrate re-embedded every changed record and, when a MemPalace daemon held the per-palace mine lock, every contended upsert raised `MineAlreadyRunning`, was counted as a hard `failed`, and the completion marker (`failed===0`) never advanced — so the whole ~8s sweep re-ran on every boot. Time-to-first-input dropped from ~9.5s to ~1s. The fix spans several layers:
|
|
14
|
+
- **Non-blocking init.** `init()` now does only the cheap install check and marks the backend active optimistically; `ping` + migration/catch-up run in a single-flight background task via `runBridgeAsync` (the spawned child is `unref()`d so a fire-and-forget migrate never keeps the process alive). `session_start` no longer awaits the bridge.
|
|
15
|
+
- **Contention-aware convergence.** The Python bridge now classifies `MineAlreadyRunning` as `deferred` (with `deferred_keys`), distinct from a genuine `failed`, and accepts an `only:[keys]` targeted pass. `markMigrated` advances when `failed===0 && verified+deferred===discovered`, persisting the deferred keys with an exponential backoff (15m → 1h → 6h → 24h) so later sessions retry just those keys instead of re-sweeping. `deferred` (lock) is kept strictly separate from `failed` (malformed) so the marker never advances over lost data.
|
|
16
|
+
- **Record-level ledger.** A new `~/.unipi/memory/.mempalace-ledger.json` maps `project/id` → sha256 of the exact markdown bytes last confirmed in the palace, replacing the size+mtime fingerprint that a normal `store()` invalidated on every write. Catch-up now migrates only records whose bytes differ from the ledger (plus due-for-retry deferred keys), `store()` records the ledger only on a confirmed upsert, and an existing `.mempalace-migrated` marker is used once to seed the ledger. The bridge stops discovering legacy `memory.db` (the SQLite tier is gone), so the corpus matches the ledger scanner exactly.
|
|
17
|
+
- **Daemon-aware catch-up.** `probeDaemon()` reads the palace daemon's `endpoint.json` + token and checks `/health`; when a daemon is reachable and actively mining, the background catch-up stands down for the session and lets the backoff ride out the lock instead of contending. When no daemon is running, behaviour is unchanged. (Writes are deliberately not routed through the daemon `/jobs` queue: it exposes no idempotent record-upsert job, and its generic write path uses a content-addressed drawer id incompatible with the bridge's source-URI id.)
|
|
18
|
+
- **Ping-cache hygiene.** Bridge calls now distinguish a successful `null` result and transient lock contention from real backend errors (`runBridgeOutcome`/`isTransientBridgeError`), so `.mempalace-ping-verified` is only invalidated on a genuine failure instead of on nearly every session.
|
|
19
|
+
|
|
9
20
|
## [2.20.4] — 2026-09-19
|
|
10
21
|
|
|
11
22
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,29 +84,29 @@
|
|
|
84
84
|
"typebox": "^1.1.38"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@pi-unipi/ask-user": "2.20.
|
|
88
|
-
"@pi-unipi/background-tasks": "2.20.
|
|
89
|
-
"@pi-unipi/btw": "2.20.
|
|
90
|
-
"@pi-unipi/command-enchantment": "2.20.
|
|
91
|
-
"@pi-unipi/compactor": "2.20.
|
|
92
|
-
"@pi-unipi/core": "2.20.
|
|
93
|
-
"@pi-unipi/footer": "2.20.
|
|
94
|
-
"@pi-unipi/image": "2.20.
|
|
95
|
-
"@pi-unipi/fusion": "2.20.
|
|
96
|
-
"@pi-unipi/info-screen": "2.20.
|
|
97
|
-
"@pi-unipi/input-shortcuts": "2.20.
|
|
98
|
-
"@pi-unipi/kanboard": "2.20.
|
|
99
|
-
"@pi-unipi/mcp": "2.20.
|
|
100
|
-
"@pi-unipi/memory": "2.20.
|
|
101
|
-
"@pi-unipi/milestone": "2.20.
|
|
102
|
-
"@pi-unipi/notify": "2.20.
|
|
103
|
-
"@pi-unipi/ralph": "2.20.
|
|
104
|
-
"@pi-unipi/subagents": "2.20.
|
|
105
|
-
"@pi-unipi/trajectory": "2.20.
|
|
106
|
-
"@pi-unipi/updater": "2.20.
|
|
107
|
-
"@pi-unipi/utility": "2.20.
|
|
108
|
-
"@pi-unipi/web-api": "2.20.
|
|
109
|
-
"@pi-unipi/workflow": "2.20.
|
|
87
|
+
"@pi-unipi/ask-user": "2.20.5",
|
|
88
|
+
"@pi-unipi/background-tasks": "2.20.5",
|
|
89
|
+
"@pi-unipi/btw": "2.20.5",
|
|
90
|
+
"@pi-unipi/command-enchantment": "2.20.5",
|
|
91
|
+
"@pi-unipi/compactor": "2.20.5",
|
|
92
|
+
"@pi-unipi/core": "2.20.5",
|
|
93
|
+
"@pi-unipi/footer": "2.20.5",
|
|
94
|
+
"@pi-unipi/image": "2.20.5",
|
|
95
|
+
"@pi-unipi/fusion": "2.20.5",
|
|
96
|
+
"@pi-unipi/info-screen": "2.20.5",
|
|
97
|
+
"@pi-unipi/input-shortcuts": "2.20.5",
|
|
98
|
+
"@pi-unipi/kanboard": "2.20.5",
|
|
99
|
+
"@pi-unipi/mcp": "2.20.5",
|
|
100
|
+
"@pi-unipi/memory": "2.20.5",
|
|
101
|
+
"@pi-unipi/milestone": "2.20.5",
|
|
102
|
+
"@pi-unipi/notify": "2.20.5",
|
|
103
|
+
"@pi-unipi/ralph": "2.20.5",
|
|
104
|
+
"@pi-unipi/subagents": "2.20.5",
|
|
105
|
+
"@pi-unipi/trajectory": "2.20.5",
|
|
106
|
+
"@pi-unipi/updater": "2.20.5",
|
|
107
|
+
"@pi-unipi/utility": "2.20.5",
|
|
108
|
+
"@pi-unipi/web-api": "2.20.5",
|
|
109
|
+
"@pi-unipi/workflow": "2.20.5"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@earendil-works/pi-agent-core": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@pi-unipi/core": "2.20.
|
|
43
|
+
"@pi-unipi/core": "2.20.5"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/background-tasks",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
13
|
-
"@pi-unipi/core": "2.20.
|
|
13
|
+
"@pi-unipi/core": "2.20.5",
|
|
14
14
|
"turndown": "^7.2.4"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/btw",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "extensions/btw.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@earendil-works/pi-tui": "^0.84.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "2.20.
|
|
40
|
+
"@pi-unipi/core": "2.20.5"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.20.
|
|
38
|
-
"@pi-unipi/info-screen": "2.20.
|
|
37
|
+
"@pi-unipi/core": "2.20.5",
|
|
38
|
+
"@pi-unipi/info-screen": "2.20.5",
|
|
39
39
|
"@earendil-works/pi-agent-core": "^0.84.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.20.
|
|
36
|
-
"@pi-unipi/background-tasks": "2.20.
|
|
35
|
+
"@pi-unipi/core": "2.20.5",
|
|
36
|
+
"@pi-unipi/background-tasks": "2.20.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/fusion",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.20.
|
|
36
|
-
"@pi-unipi/subagents": "2.20.
|
|
35
|
+
"@pi-unipi/core": "2.20.5",
|
|
36
|
+
"@pi-unipi/subagents": "2.20.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Image generation and image recognition tools for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.20.
|
|
37
|
+
"@pi-unipi/core": "2.20.5"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.20.
|
|
36
|
+
"@pi-unipi/core": "2.20.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/input-shortcuts",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.20.
|
|
36
|
+
"@pi-unipi/core": "2.20.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pi-unipi/core": "2.20.
|
|
42
|
+
"@pi-unipi/core": "2.20.5"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/mcp",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.5",
|
|
4
4
|
"description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "npx tsx --test tests/**/*.test.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@pi-unipi/core": "2.20.
|
|
33
|
+
"@pi-unipi/core": "2.20.5"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Persistent memory that survives across sessions. Stores facts, preferences, and decisions with semantic vector search, so the agent remembers what you told it last week.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Backend: [MemPalace](https://github.com/mempalace/mempalace)** — auto-installed via `uv` on first load, with verified, resumable, incremental migration of existing markdown memories. Detection, ping, and migration run off the startup path, so loading the package never blocks the first prompt.
|
|
6
6
|
|
|
7
7
|
Two storage tiers: MemPalace (or SQLite) for vector similarity search, markdown files for a durable human-readable copy you can edit by hand. Project-scoped memories stay separate per codebase, global memories are accessible everywhere.
|
|
8
8
|
|
|
@@ -74,46 +74,56 @@ Examples:
|
|
|
74
74
|
Memory has no configuration file. Storage paths are fixed:
|
|
75
75
|
|
|
76
76
|
```
|
|
77
|
-
~/.unipi/memory/ # UniPi memory root (
|
|
77
|
+
~/.unipi/memory/ # UniPi memory root (markdown tier)
|
|
78
78
|
├── .mempalace-install # Cached MemPalace venv detection
|
|
79
|
-
├── .mempalace-migrated #
|
|
79
|
+
├── .mempalace-migrated # Legacy migration marker (seeds the ledger once)
|
|
80
|
+
├── .mempalace-ledger.json # Record-level sync ledger (project/id -> content hash)
|
|
81
|
+
├── .mempalace-ping-verified # Recent-ping cache (skips the cold-start ping)
|
|
80
82
|
├── global/
|
|
81
|
-
│
|
|
82
|
-
│ └── *.md # Global memory files
|
|
83
|
+
│ └── *.md # Global memory files (durable, human-readable)
|
|
83
84
|
└── <project_name>/
|
|
84
|
-
|
|
85
|
-
└── *.md # Project memory files
|
|
85
|
+
└── *.md # Project memory files (durable, human-readable)
|
|
86
86
|
|
|
87
|
-
~/.mempalace/palace/ # MemPalace palace (
|
|
87
|
+
~/.mempalace/palace/ # MemPalace palace (vector backend)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
## MemPalace backend
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
MemPalace is the sole vector backend; the markdown files are the durable,
|
|
93
|
+
human-readable tier and the migration source. On load, the memory package:
|
|
93
94
|
1. Detects MemPalace; if missing and `uv` is available, runs
|
|
94
95
|
`uv tool install mempalace` once (caches the venv python path in
|
|
95
96
|
`~/.unipi/memory/.mempalace-install`).
|
|
96
|
-
2.
|
|
97
|
-
|
|
98
|
-
the
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
2. Marks the backend active immediately, then does everything else
|
|
98
|
+
**off the startup path** so time-to-first-input is never blocked: a
|
|
99
|
+
background task pings the bridge (skipped when recently ping-verified) and
|
|
100
|
+
runs an incremental catch-up.
|
|
101
|
+
3. Catch-up is driven by a record-level ledger
|
|
102
|
+
(`~/.unipi/memory/.mempalace-ledger.json`) mapping `project/id` to the
|
|
103
|
+
sha256 of the markdown bytes last confirmed in the palace. Only records whose
|
|
104
|
+
bytes differ from the ledger are upserted, so an ordinary write never
|
|
105
|
+
triggers a full re-scan. `store()` updates the ledger only after a confirmed
|
|
106
|
+
upsert; a pre-existing `.mempalace-migrated` marker seeds the ledger once.
|
|
103
107
|
Legacy files are never deleted or mutated.
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
Records contended by a running MemPalace daemon's mine lock are recorded as
|
|
110
|
+
**deferred** (never as failures) and retried with exponential backoff, so the
|
|
111
|
+
catch-up always converges instead of re-running every boot. When a daemon is
|
|
112
|
+
reachable and actively mining, the background catch-up stands down for the
|
|
113
|
+
session rather than fighting the lock.
|
|
114
|
+
|
|
115
|
+
Memory operations invoke the packaged Python bridge
|
|
116
|
+
(`bridge/mempalace_bridge.py`); startup-path work uses the async, non-blocking
|
|
117
|
+
variant. Both the standalone memory package and the all-in-one umbrella tarball
|
|
118
|
+
ship and resolve this bridge. The first MemPalace use on a machine also
|
|
119
|
+
downloads the default ONNX embedding model (~80MB, cached at
|
|
120
|
+
`~/.cache/chroma/onnx_models/`).
|
|
111
121
|
|
|
112
122
|
### Forcing re-detection / re-migration
|
|
113
123
|
|
|
114
124
|
```bash
|
|
115
125
|
rm ~/.unipi/memory/.mempalace-install # re-detect MemPalace next session
|
|
116
|
-
rm ~/.unipi/memory/.mempalace-
|
|
126
|
+
rm ~/.unipi/memory/.mempalace-ledger.json # force a full verified catch-up pass next session
|
|
117
127
|
```
|
|
118
128
|
|
|
119
129
|
### Backend override
|
|
@@ -46,6 +46,15 @@ except Exception: # pragma: no cover
|
|
|
46
46
|
MEMORY_TYPES = {"preference", "decision", "pattern", "summary"}
|
|
47
47
|
MIGRATION_AGENT = "unipi-memory-bridge"
|
|
48
48
|
|
|
49
|
+
# MemPalace's per-palace mine lock is non-blocking: when a daemon or another
|
|
50
|
+
# writer holds it, upserts raise MineAlreadyRunning (message "... is held by
|
|
51
|
+
# PID ..."). That is transient contention, not a data error.
|
|
52
|
+
_TRANSIENT_LOCK_RE = re.compile(r"MineAlreadyRunning|is held by", re.IGNORECASE)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_transient_lock_error(exc: Exception) -> bool:
|
|
56
|
+
return bool(_TRANSIENT_LOCK_RE.search(f"{type(exc).__name__}: {exc}"))
|
|
57
|
+
|
|
49
58
|
|
|
50
59
|
# ---------------------------------------------------------------------------
|
|
51
60
|
# ID + URI helpers (mirror mempalace.ids when available, else deterministic fallback)
|
|
@@ -311,8 +320,12 @@ def discover_legacy_memories(source_dir: Path, project_filter: list[str] | None
|
|
|
311
320
|
rec = parse_markdown_memory(project, md_path)
|
|
312
321
|
if rec:
|
|
313
322
|
by_key[(rec["project"], rec["id"])] = rec
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
# Markdown is the sole durable source now (the SQLite fallback was
|
|
324
|
+
# removed). Discovering legacy memory.db here would surface records the
|
|
325
|
+
# TS ledger scanner cannot see, so `--only` targeted retries could not
|
|
326
|
+
# name them and the delta would never converge. `load_sqlite_memories`
|
|
327
|
+
# is retained for any explicit one-off recovery use, but is no longer
|
|
328
|
+
# part of the automatic catch-up corpus.
|
|
316
329
|
return sorted(by_key.values(), key=lambda r: (r["project"], r["type"], r["title"], r["id"]))
|
|
317
330
|
|
|
318
331
|
|
|
@@ -492,18 +505,39 @@ class Bridge:
|
|
|
492
505
|
synced += 1
|
|
493
506
|
return synced
|
|
494
507
|
|
|
495
|
-
def migrate(
|
|
508
|
+
def migrate(
|
|
509
|
+
self,
|
|
510
|
+
source_dir: str,
|
|
511
|
+
project_filter: list[str] | None = None,
|
|
512
|
+
only: list[str] | None = None,
|
|
513
|
+
) -> dict[str, Any]:
|
|
496
514
|
"""Idempotently import and then verify every discovered UniPi record.
|
|
497
515
|
|
|
498
516
|
Migration callers must not infer success merely from a bridge process
|
|
499
517
|
exiting cleanly. Return explicit discovery/failure/verification counts
|
|
500
518
|
so UniPi only writes its completion marker after full verification.
|
|
519
|
+
|
|
520
|
+
Failures are split into two classes:
|
|
521
|
+
- ``deferred``: transient palace-lock contention (``MineAlreadyRunning``)
|
|
522
|
+
while a daemon/other writer holds the mine lock. The record is
|
|
523
|
+
untouched and simply needs retrying later; it is NOT a data error.
|
|
524
|
+
- ``failed``: a genuine per-record error (malformed record, backend
|
|
525
|
+
fault). The completion marker must never advance over these.
|
|
526
|
+
|
|
527
|
+
``only`` optionally restricts the pass to a set of ``"project/id"`` keys
|
|
528
|
+
(targeted retry of previously deferred records) so a catch-up does not
|
|
529
|
+
re-sweep the entire corpus.
|
|
501
530
|
"""
|
|
502
531
|
records = discover_legacy_memories(Path(source_dir), project_filter)
|
|
532
|
+
if only:
|
|
533
|
+
wanted = set(only)
|
|
534
|
+
records = [r for r in records if f"{r['project']}/{r['id']}" in wanted]
|
|
503
535
|
imported = 0
|
|
504
536
|
skipped = 0
|
|
505
537
|
failed = 0
|
|
538
|
+
deferred = 0
|
|
506
539
|
errors: list[str] = []
|
|
540
|
+
deferred_keys: list[str] = []
|
|
507
541
|
by_project: dict[str, int] = {}
|
|
508
542
|
expected = {(rec["project"], rec["id"]) for rec in records}
|
|
509
543
|
|
|
@@ -534,11 +568,19 @@ class Bridge:
|
|
|
534
568
|
existing_docs[key] = expected_doc
|
|
535
569
|
by_project[rec["project"]] = by_project.get(rec["project"], 0) + 1
|
|
536
570
|
except Exception as exc:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
)
|
|
571
|
+
if _is_transient_lock_error(exc):
|
|
572
|
+
# Palace held by a mine/daemon: the record is untouched and
|
|
573
|
+
# just needs a later retry. Record the key, do not count it
|
|
574
|
+
# as a data failure (which would block the completion marker
|
|
575
|
+
# forever whenever a daemon is running).
|
|
576
|
+
deferred += 1
|
|
577
|
+
deferred_keys.append(f"{rec['project']}/{rec['id']}")
|
|
578
|
+
else:
|
|
579
|
+
failed += 1
|
|
580
|
+
if len(errors) < 20:
|
|
581
|
+
errors.append(
|
|
582
|
+
f"{rec['project']}/{rec['id']}: {type(exc).__name__}: {exc}"
|
|
583
|
+
)
|
|
542
584
|
|
|
543
585
|
# Re-read after writes and verify exact durable documents, not just
|
|
544
586
|
# optimistic in-memory bookkeeping or collection counts. A palace may
|
|
@@ -560,6 +602,7 @@ class Bridge:
|
|
|
560
602
|
if persisted_rec:
|
|
561
603
|
persisted[(persisted_rec["project"], persisted_rec["id"])] = doc
|
|
562
604
|
verified = 0
|
|
605
|
+
verified_keys: list[str] = []
|
|
563
606
|
for rec in records:
|
|
564
607
|
expected_doc = build_document(
|
|
565
608
|
rec["title"], rec["content"], rec["tags"], rec["project"],
|
|
@@ -567,6 +610,7 @@ class Bridge:
|
|
|
567
610
|
)
|
|
568
611
|
if persisted.get((rec["project"], rec["id"])) == expected_doc:
|
|
569
612
|
verified += 1
|
|
613
|
+
verified_keys.append(f"{rec['project']}/{rec['id']}")
|
|
570
614
|
|
|
571
615
|
return {
|
|
572
616
|
"discovered": len(records),
|
|
@@ -574,9 +618,12 @@ class Bridge:
|
|
|
574
618
|
"updated": imported,
|
|
575
619
|
"skipped": skipped,
|
|
576
620
|
"failed": failed,
|
|
621
|
+
"deferred": deferred,
|
|
577
622
|
"verified": verified,
|
|
578
623
|
"projects": by_project,
|
|
579
624
|
"errors": errors,
|
|
625
|
+
"deferred_keys": deferred_keys[:200],
|
|
626
|
+
"verified_keys": verified_keys,
|
|
580
627
|
}
|
|
581
628
|
|
|
582
629
|
|
|
@@ -617,7 +664,7 @@ def main(argv: list[str]) -> int:
|
|
|
617
664
|
"has_title": lambda: bridge.has_title(args["wing"], args["title"]),
|
|
618
665
|
"find_similar": lambda: bridge.find_similar(args["wing"], args["title"], float(args.get("threshold", 0.6))),
|
|
619
666
|
"sync_orphaned": lambda: bridge.sync_orphaned(args["project_dir"], args["wing"]),
|
|
620
|
-
"migrate": lambda: bridge.migrate(args["source_dir"], args.get("projects")),
|
|
667
|
+
"migrate": lambda: bridge.migrate(args["source_dir"], args.get("projects"), args.get("only")),
|
|
621
668
|
}
|
|
622
669
|
handler = handlers.get(cmd)
|
|
623
670
|
if handler is None:
|