Commit Graph

9 Commits

Author SHA1 Message Date
Joakim Persson b609cf5a69 docs(synlig): the transcript inbox is the primary's third moving part; and survive an unset HOME
Runbook gaps found while fixing the 2026-08-15 feed failure:

- §2.5 still titled "written but not installed" and still asserting "Not
  installed, not enabled" — false since 2026-08-12. A reader landing there got
  a flat contradiction of §4 item 3. Retitled, with the verified-2026-08-16
  process line, and it now states the fact §2.6 depends on: the server is a
  NATIVE process (no mempalace container on synlig), so it sees host paths.
- New §2.6 documents ~/mempalace-feed/<device>/: why transcripts cannot travel
  over the HTTPS leg at all, the three client variables, and why
  MEMPALACE_PI_REMOTE_PATH is the trap (its /data/feed default assumes a
  containerized server; here it must equal the ssh-target path, and a mismatch
  fails with rsync succeeding and only the mine failing). Plus the operational
  notes that cost time: dedup keys on the absolute path so the inbox path is
  load-bearing, grown sessions are purged+refiled by mtime, and a client-side
  MCP timeout is NOT a failed mine.
- Header status: counts refreshed with an explicit "treat counts as timestamps".

Also, bin/mempalace-pi-session: default HOME from the passwd database when it is
unset. `docker run --entrypoint="" <image>` inherits no HOME when the image
config declares none, and every default is HOME-anchored under `set -u`, so the
script — including the palace-free --self-test — died with "HOME: unbound
variable" in exactly the environment pi-devbox's smoke suite uses. pi-devbox
v1.8.0 lost a release to the same assumption from the other side.
2026-08-16 00:52:27 +02:00
Joakim Persson 6e1f4f30fc fix(pi-session): a failed remote mine reported success — MCP escapes the payload
The remote-mine leg decided success with `'"error"' in body`. MCP answers a
hard tool failure with HTTP 200 and a JSON-RPC *result* whose content[].text
carries the tool's own JSON as an ESCAPED string, so those bytes are
\"error\" and the substring never matches. On 2026-08-15 (EMB-7KJ4VR4G, first
boot of the fresh pi-devbox image) a mine that failed with

  {"success": false, "error": "source directory not found: '/data/feed/emb-7kj4vr4g'"}

printed "Done. Wing 'wing_conversations' updated." directly under that error
and exited 0. Transcripts had been rsynced for the whole session and filed
nowhere; the only artifact anyone would check said it worked.

- classify(): parse the envelope instead of grepping it. Catches JSON-RPC
  errors, MCP isError, and inner success=false/error, and distinguishes
  "verified ok" from "unverified: no JSON tool payload" rather than assuming.
- --self-test: six recorded MCP responses (fixture 1 is the real 2026-08-15
  body) plus a regression guard asserting the old substring check is blind to
  it. Needs no palace, no network, no sessions dir.
- Preflight warning when the rsync destination path and
  MEMPALACE_PI_REMOTE_PATH disagree. The /data/feed default assumes a
  CONTAINERIZED palace server; a native one (systemd unit / uv tool) sees host
  paths, and then the two must match. Warned in preflight so --dry-run and
  --prepare surface it too.
- Remote mode no longer previews NEW/SKIP from the LOCAL palace: dedup happens
  on the palace host keyed on the remote inbox path, so this machine cannot
  answer it. Tags become [?] and the summary says who decides. It had been
  reporting "6 already filed" about a palace it was not feeding.
2026-08-16 00:28:50 +02:00
Joakim Persson f60cf9c732 feat(census): RFC 002 Phase A — read-only join census, and three RFC corrections it found
bin/mempalace-census classifies a palace on disk into the RFC 002 §2 classes
(mined / diary / agent-authored) and emits a human report or a --json manifest
that feeds Phases B/C. Read-only: every sqlite handle is opened mode=ro, no
-wal/-shm is created, safe against a live mempalace-serve. Reads LOCAL DISK only
and warns if MEMPALACE_REMOTE_URL is set, so a local census can't be mistaken
for a remote one.

The design point is that it SELF-VERIFIES instead of trusting ids.py's
docstrings: for every replayable drawer it reassembles content from chunks,
recomputes the upstream id and compares to the stored id. That single check
covers the id recipe, the chunk reassembly order and the classifier at once --
176/176 accounted for on the reference palace -- and it falsified three things
the RFC previously asserted from a docs-only reading (now RFC 002 §2.1):

  (a) The hash input is LENGTH-PREFIXED, not "|"-joined. ids.py:31 defines
      _DELIM = "|" and the make_* docstrings describe f"{wing}|{room}|{content}",
      but _DELIM is dead code and _delimited_sha256 builds
      "".join(f"{len(part)}:{part}"). Measured: length-prefixed reproduces real
      ids 5/5, pipe-joined 0/5. Diary ids differ again -- a PLAIN sha256.

  (b) id_recipe is NOT a mined-only marker. It looked like a clean
      discriminator (same 14,586 count as source_file) but the server stamps
      'v3' on content ids too, so classifying on `source_file OR id_recipe`
      swallowed all 60 agent-authored drawers into MINED -- the dangerous
      direction, since Phase C would try to re-mine drawers that have no source
      file and silently drop them. Discriminator is a TRUTHY source_file (the
      writer stores "" rather than omitting the key), cross-checked against the
      miner-only keys source_mtime / normalize_version; disagreement is now a
      first-class warning.

  (c) Content ids DRIFT: 9 of 60 agent-authored drawers no longer reproduce
      their own id, because update_drawer preserves the id while rewriting and
      re-chunking. So "recompute the content id and skip if present" -- the
      strategy this RFC specified for regime A -- misses every drifted drawer
      and duplicates it. Phase C must key on the STORED id. Flagged as
      edited_since_filing in the manifest so Phase C can be tested on them.

Also corrects §4.1's headline number: 15,949 mined / 98.9% was reconstructible
exactly as 16,338 (all embeddings rows) - 192 (diary rows) - 197 (agent rows),
i.e. it counted rows rather than parent drawers AND spanned both collections,
absorbing all 1,560 non-joinable mempalace_closets rows into the mined total.
Correct figures: 14,389 mined / 116 diary / 60 agent-authored = 14,565 parents,
replay surface 176 (1.2%). Two rules now enforced in the tool: always filter by
collection (one sqlite file holds both), and always say whether a count is rows
or parent drawers -- a chunked drawer contributes N rows and no parent row.

One implementation trap worth recording: Chroma splits metadata across
string_value and int_value, so reading only string_value nulls every numeric key
(source_mtime, chunk_index, line_start) -- which made the miner-marker
cross-check report 100% conflict until the loader coalesced the two columns.
2026-08-15 09:06:40 +02:00
Joakim Persson 29e660e18f feeders: stage beside the palace, not in ~/.cache; document Phase 1 exposure
Staging default moves out of ~/.cache to <palace-root>/pi-stage (pi) and
<palace-root>/opencode-stage (opencode), resolved with mempalace's own
palace-path precedence ($MEMPALACE_PALACE_PATH -> $MEMPAL_PALACE_PATH ->
~/.mempalace/config.json -> ~/.mempalace/palace), then dirname.

Why: the convos miner keys dedup on the *staged* path, so a wiped stage plus a
sync scoped to include it prunes the drawers mined from those sources --
deleting memories, not a cache. Under ~/.cache that state was reachable by
anything treating a cache as disposable. Staging inside the palace makes the
coupling structural: the stage cannot be wiped without touching the palace
itself. Overrides ($MEMPALACE_PI_STAGE / $MEMPALACE_SESSION_STAGE, --stage) are
unchanged. Note the old default had never been created on any host, so this
closed a latent hazard, not a live one.

Measured, and the docs now claim only this much: sync prunes only within the
scope it is given -- wing-only, 1299 scanned / 1299 out of scope / 0 removed;
scoped at the palace root, 651 kept / 648 out of scope. The previous blanket
"sync prunes every drawer" wording overstated it, which is a liability: the next
reader disproves the overstatement and discards the real constraint with it.

Also in this change:
- cron log dir ~/.cache/mempalace-session -> ~/.cache/mempalace-logs. The stage
  left that namespace, so the old name now read as "the stage".
- AGENTS.md: the convos miner *does* check mtime (verified against upstream
  convo_miner.py); the previous "no mtime check" claim was wrong.
- smoke-test assertions use `mktemp -d` for --sessions-dir. One pointed at /tmp,
  which still held earlier synthetic transcripts, so a --dry-run exported a fake
  session into the real stage: --dry-run skips the mine, not the export.

docs/phase-1-exposure-runbook.md -- the newt/DNS/auth step that RFC 001 and the
synlig runbook leave open (runbook section 4, items 2 and 5). Port 8765 at /mcp,
newt targets 172.17.0.1, and the authentication is the single shared bearer
token (RFC 6.2, decided 2026-08-09) rather than per-device proxy users. The
latter cannot work today: mempalace validates exactly one token, and Pangolin's
SSO/PIN/password are browser-shaped while every client here is a headless
JSON-RPC POST -- enabling that protection breaks the clients it protects. The
per-device axis that *does* exist is the feeder's SSH key + per-device inbox.

New finding recorded there: a loopback bind does not merely 403 behind a tunnel
(already known, runbook 2.4) -- it also silently starts the server with no token
at all, because auto-minting is gated on the bind being non-loopback.

extensions/pi/README.md: the HTTP transport IS authenticated as of mempalace
3.6.0; the "sessionless and unauthenticated" note dated from the v1.3.0 era.
Closes the RFC section 8 Phase-0 hygiene item.
2026-08-12 17:04:01 +02:00
joakimp 6352373a1f fix(feeders): make post-mine repair opt-in, not default
The three feeder wrappers (mempalace-docs, mempalace-pi-session,
mempalace-session) unconditionally ran 'mempalace repair --yes' after
mining, controllable only via --no-repair opt-out. The contrib launchd
and systemd templates did not pass --no-repair, so every scheduled tick
invoked the destructive in-place HNSW rebuild.

This has bitten us twice:
  - 2026-05-04 09:08: a kickstart triggered repair while an MCP
    subprocess held the DB open; the live collection was wiped (0
    drawers) and had to be restored from the palace.backup snapshot.
  - 2026-05-05 10:00: post-mine repair crashed mid-rebuild with
    'NotFoundError: Collection [<uuid>] does not exist' - chromadb's
    rebuild recreated the collection under a new UUID while the code
    still held the old handle. Live DB survived only by luck (crash
    hit before the swap).

Fix: flip the default.
  - New flag: --repair (opt-in). Prints a warning and sleeps 3s before
    invoking 'mempalace repair --yes'.
  - --no-repair is retained as a deprecated no-op alias for backward
    compatibility with any scripts/units still passing it.
  - Default behavior: no repair. Routine ChromaDB add() keeps HNSW
    consistent; repair is a recovery op, not a maintenance tick.

Docs updated to match: README, SKILL, ARCHITECTURE, AGENTS,
contrib/README. Scheduling guidance now explicitly warns against
enabling --repair on cron/launchd/systemd-timer runs.
2026-05-05 12:35:04 +02:00
joakimp 14d253f929 feat(session): tag opencode staging headers with '| source: opencode'
Complement to the mempalace-pi-session feeder: now that a second source
mines into wing_conversations, every session's synthetic header carries
an explicit source tag so the LLM can discriminate at read time when
searches return first-chunk content:

  [session: <title> | <directory> | <YYYY-MM-DD> | source: opencode]

The primary disambiguator in search results remains source_file basename
(opencode: '<slug>_<id>.jsonl', pi: 'pi_<uuid>.jsonl'), which is present
in every chunk's metadata regardless of where the search hit landed in
the session. This header tag is a cosmetic second signal on first-chunk
hits.

Caveat: existing drawers keep their old header — mempalace mine dedups
by source_file path, which didn't change, so old opencode sessions are
not re-mined. They are implicitly opencode (the only pre-pi source).
2026-05-05 08:48:27 +02:00
joakimp 9450a45194 feat(pi-session): add mempalace-pi-session feeder for pi coding-agent sessions
Parallel to mempalace-session, this wrapper walks ~/.pi/agent/sessions/
JSONL files and mines qualifying sessions into wing_conversations via
'mempalace mine --mode convos'.

Design choices mirror mempalace-session:
- Export-stage-mine idiom with deterministic per-session staging paths
  under ~/.cache/mempalace-pi-session/<wing>/, so 'mempalace mine' dedup
  on source_file makes re-runs idempotent.
- --dry-run classifies each export as [NEW] or [SKIP] by matching staging
  path against the palace's already-filed source_files.
- --min-messages filter skips throwaway single-prompt sessions.

Pi-specific parsing:
- Pi JSONL is a typed tree (id/parentId) per docs/session-format.md;
  this walks in file order, which is correct for the overwhelmingly
  linear case and harmlessly duplicative on branched sessions (palace
  semantic dedup handles it).
- Roles mapped to Claude Code JSONL shape:
    user      -> {type:user, content:text}
    assistant -> {type:assistant, content:[text, tool_use]}
    toolResult-> {type:human, content:[tool_result]} (folded back by normalizer)
    bashExecution/custom(display)/branchSummary/compactionSummary
              -> rendered as text annotations
- thinking blocks and image blocks dropped (noise / palace is text-only).

Source labelling:
- Staging filenames prefixed 'pi_<uuid>.jsonl' so every drawer's
  source_file metadata (visible in search results) unambiguously
  identifies the harness. Opencode's convention ('<slug>_<id>.jsonl')
  is preserved to keep the existing 19k+ drawers deduped.
- Inline synthetic header on first chunk:
    [session: <title> | <cwd> | <date> | source: pi]
  as a secondary signal.
2026-05-05 08:48:20 +02:00
Joakim Persson 349a3a3d3d mempalace-session: make --dry-run dedup-aware
A --dry-run report showed all qualifying sessions without indicating
which would actually hit the palace on a real run. On a second run
against an already-mined corpus this was misleading — output said
'Exported 62 session(s)' but the real mine step would skip all 62.

The wrapper now queries the palace's chroma.sqlite3 (read-only, via
file:...?mode=ro URI) for source_file values under the staging dir,
then tags each exported session as [NEW] or [SKIP] during listing and
reports the split in the summary:

  Exported 62 session(s) to ~/.cache/mempalace-session/wing_conversations
    0 new   → will be filed on mine
    62 already filed → will be skipped (dedup by source_file)

  --dry-run: no new sessions to mine. A real run would skip all 62.

Implementation notes:
- Classification is best-effort. If the palace is unreachable (fresh
  install, moved, permission-denied, file missing) the wrapper falls
  back to treating all exports as NEW — the real mine step still
  delegates dedup to 'mempalace mine --mode convos' which is the
  authoritative source of truth. Getting the classification wrong
  in --dry-run is cosmetic; behaviour of a real run is unchanged.
- Palace path respects $MEMPALACE_PATH env var for non-default setups.
- Same classification also shown on a real (non-dry-run) mine so users
  see upfront how much of the export set is actually new before the
  miner runs.

Verified both directions:
- All-already-filed case (current box, 62 sessions in palace): reports
  0 new, 62 skipped. --dry-run message correctly says 'would skip all'.
- Partial case (simulated by deleting one session's metadata from
  palace): reports 1 new, 61 skipped. --dry-run message correctly
  says 'would file 1 new'. Palace was restored from backup
  immediately after the test.

README and SKILL.md both updated with the new dedup-aware output and
a direct answer to the FAQ 'will it mine the same sessions again?'
2026-04-30 08:33:36 +00:00
Joakim Persson 954c3f2ebb Initial commit — split out from cli_utils
Producer-side MemPalace tooling: two bash wrappers that bridge opencode
session history and project documentation into the palace. Originally
developed in cli_utils (2026-04-28); split into its own repo on
2026-04-30 because the conceptual fit was weak — cli_utils is
interactive shell tooling, while this is agent memory infrastructure
with its own architecture, dependency surface, and growth trajectory.

Contents:
- bin/mempalace-docs — docs-only mining wrapper (originally a2ddcc9 in
  cli_utils), bridges the gap until MemPalace PR #1213 (exclude_patterns)
  merges upstream.
- bin/mempalace-session — opencode → palace session bridge (originally
  dacca0e in cli_utils). Reads ~/.local/share/opencode/opencode.db,
  exports each session to Claude Code JSONL, mines via
  'mempalace mine --mode convos'. Bridges the gap until opencode
  session-stopping hooks + an opencode harness in hooks_cli.py land
  upstream.
- ARCHITECTURE.md — canonical spec: architecture diagram, component
  details, setup recipe, operational notes, upstream-retirement
  roadmap. Originally a4cf314 in cli_utils.
- SKILL.md — companion agent skill (producer side). Pairs with the
  consumer-side mempalace skill. Symlinked into
  ~/.agents/skills/opencode-mempalace-bridge/ by install.sh.
- install.sh — idempotent installer, also handles --uninstall.
- AGENTS.md — repo conventions.

History of the individual files is not preserved in this split; see
cli_utils (gitea.jordbo.se/joakimp/cli_utils) commits a2ddcc9, dacca0e,
and a4cf314 for the original authorship context.
2026-04-30 05:30:04 +00:00