# Changelog

Notable changes to parallelclaw (formerly memex-mvp). Older history lives in the git log.

## 1.12.0 — hygiene: executor sessions excluded from the shared corpus

Cross-node memory search was picking up the executor's own automated runs (its
claim/run reasoning), polluting results. Now:
- The OpenClaw executor turn carries a sentinel (`parallelclaw-executor:internal-turn`),
  and the capture layer **skips any session containing it** — these automated
  coordination runs never enter the shared corpus. (Self-hosted layout keeps the
  dedicated `--session-key parallelclaw-exec` session in its own file, dropped wholesale.)
- (Reminder) our executor already avoids the per-minute "NO_TASKS" LLM flood via
  the cheap `--quiet` shell gate; this removes the remaining real work-turn noise.
- +2 `ingest-file` tests (executor session skipped; normal session still imports).

**Operator note:** a node still running an OLD hand-rolled executor that spins an
LLM every tick (e.g. Kimi's `memex-task-executor-v3`, which floods "NO_TASKS")
should switch to the standard one: remove that cron and run
`memex-sync openclaw-executor install` (cheap gate → no flood; its turns are
sentinel-excluded).

## 1.11.0 — coordination contract: agents now understand the mesh

Live tests (Mac↔VPS1↔Kimi) showed the mechanism works flawlessly but agents
didn't know HOW to use it: OpenClaw delegated to a non-existent node
`claude-code` (it's `mac`); Kimi reached for ACP (local-only) instead of
`task-delegate`; OpenClaw searched its local session list instead of the shared
corpus. The plumbing wasn't the problem — discoverability was. This release makes
the shared layer self-explanatory.

- **`memex-sync nodes`** — the mesh roster (origin, source, last-seen, ★this-node),
  so agents/users know valid delegation targets.
- **Delegation alias resolution** (`lib/nodes.js`): `--to claude-code` / `claude`
  / `openclaw` resolve to the matching node (claude-code → the claude-code node);
  an unknown target now WARNS with the valid list + a suggestion instead of
  silently creating a phantom task. Wired into BOTH the CLI (`task-delegate`) and
  the MCP tool (`parallelclaw_delegate`).
- **Coordination Card** (`memex-sync coordination-card`) — a compact, authoritative
  how-to generated from the live roster: delegate cross-machine via `task-delegate`
  (NOT ACP, which is local-only), recall cross-node via `memex_search origin=…`.
- **Auto-injected into agent contexts** (the lazy-user part): the Claude Code
  SessionStart hook now shows a compact mesh block; `memex-sync inject-card`
  writes a managed block into OpenClaw's `AGENTS.md`; `openclaw-setup` runs it
  automatically (new `card` step).
- New `lib/nodes.js` + `test/nodes.test.js` (8 checks). Full suite green.

## 1.10.0 — one-command OpenClaw setup + role-aware status (lazy-user)

Finishing the "fresh lazy user installs ParallelClaw on their OpenClaw VPS" flow,
all proven live on a real node:
- **`memex-sync openclaw-setup [--no-restart]`** — the whole install in ONE shell
  command (no ClawHub skill / permission model): capture daemon → back-fill →
  wire MCP (native `openclaw mcp add`) → coordination executor → restart gateway.
  Idempotent; MCP-add + restart are soft (a re-run won't abort). New
  `lib/openclaw-setup.js` + 7 tests (pure plan + injected-io orchestration).
- **`parallelclaw status` is now node-role-aware:** shows `hub — serving on
  <bind>:<port>` when THIS node actually runs the sync-server (detected via the
  real service status — not the stale `config.server.enabled`, and not a port
  probe a spoke's forward-tunnel would falsely answer); shows the **OpenClaw cron
  executor** on non-Mac nodes (was "macOS only").
- **openclaw-executor idempotency fix:** install/uninstall now remove cron jobs
  **by id** (`openclaw cron rm <name>` is a no-op → repeated installs were
  stacking DUPLICATE every-minute jobs); status detects via `cron list`.
- **install-memex-claw Step 5** prefers native `openclaw mcp add` (probes before
  saving) over the hand-rolled python JSON merge.

## 1.9.1 — openclaw-executor fixes (from the live VPS experiment)

Live install on a real OpenClaw VPS surfaced three bugs in the 1.9.0 executor —
all fixed and proven end-to-end (Mac delegates `--to vps1` → OpenClaw runs it
autonomously → result round-trips back):
- **`--expect-final` is a `cron add` flag, not an `openclaw agent` flag** — the
  wrapper choked on it. Removed.
- **`openclaw agent` needs a target session** — added `--agent <id>`
  (auto-detected, default `main`) + a dedicated `--session-key parallelclaw-exec`
  so executor turns don't pollute the user's main thread.
- **cron fallback delivery fail-closed** ("no route") — added `--no-deliver`
  (the executor writes results to the ledger via `task-update`, not to a chat).

## 1.9.0 — one-command coordination for OpenClaw nodes (lazy-user)

An always-on OpenClaw node (a VPS) had no turnkey way to join the coordination
loop: the Mac executor needs `claude -p` (absent on a server), and the
install-memex-claw skill's Step 8 was a hand-wavy "register a recurring job on
your scheduler." Now it's one command:

```sh
memex-sync openclaw-executor install     # + status / uninstall
```

- Uses OpenClaw's own primitives (2026.5+): registers an `openclaw cron` job
  (every ~60s) that runs a CHEAP shell gate (`task-list --inbox --quiet`, zero
  LLM tokens) and only when work waits hands the executor prompt to
  `openclaw agent` — OpenClaw's LLM does the work. **No `claude` needed.**
- Writes the prompt + a gated wrapper script under `~/.memex/`; idempotent
  (drops any prior `parallelclaw-executor` job before re-adding).
- install-memex-claw Step 8 now calls this command (manual procedure kept as a
  fallback); Step 9 points to `parallelclaw status` + the uninstall command.
- New `lib/openclaw-executor.js` + 6 tests (pure builders + injected-io install).

## 1.8.0 — multi-sync polish: loud failures · local time · ledger GC

Three small lazy-user wins from the prod-readiness backlog:
- **Never fail silently.** A `failed` delegation is now loud: the SessionStart 📥
  block shows a `⚠️ N FAILED` callout, lists failures FIRST (so a long list never
  truncates them away), and tags each `**FAILED**`; the delivery push banner reads
  `⚠️ … FAILED a task` (single) or `(N FAILED)` (summary).
- **Local time.** `task-list` timestamps render in the machine's local timezone
  (`YYYY-MM-DD HH:MM`) instead of UTC — no more UTC-vs-local confusion.
- **Ledger GC.** New `task-gc [--days N] [--dry-run] [--json]` prunes the event
  rows of tasks whose CURRENT state is terminal AND older than N days (default
  30). Non-terminal tasks are never touched; the FTS delete trigger cleans the
  index; safe vs sync (peers don't re-push below the pull cursor).

## 1.7.0 — per-task dollar cap on the executor (no bill surprise)

Closes prod-readiness gap #5 ("cost is opaque"). The Mac headless executor now
caps every `claude -p` run with the CLI's `--max-budget-usd`:
- Default **$0.50 per task**; override with `task-executor install --budget <usd>`
  or `PARALLELCLAW_EXEC_BUDGET_USD`; set `0` to disable the cap.
- The cap is baked into the launchd plist env, so the timer-launched executor
  inherits it. A run that hits the cap stops and its partial output is captured
  like any other end-of-run (lease/requeue still recovers a killed run).
- Surfaced in `task-executor status` and `parallelclaw status` ("cap $0.50").
- Threaded `runInboxOnce → claudeRunner`; +2 tests (cap reaches the runner; the
  plist bakes the env). Existing injected-runner tests unaffected.

## 1.6.0 — `parallelclaw status`: one-glance health for a lazy user

You couldn't tell at a glance whether the mesh was alive, the executor was
running, or anything was stuck. New **`parallelclaw status`** (also `pclaw` /
`memex status`) prints one screen:
- **Mesh (sync)** — per remote: a freshness dot (green < 15m · yellow stale ·
  ✗ last_error), "synced Nm ago", and the pull/push cursors. Says "standalone"
  when no hub is configured.
- **Executor** — installed / running (launchd), or "macOS only" elsewhere.
- **Tasks** — inbox (waiting to run), in-flight, failures (highlighted),
  answers waiting for you, incoming notes — straight from the ledger.
- `--json` for scripts. Read-only; dynamically imports sync/task/executor
  modules so other commands don't pay for them.

## 1.5.0 — push notification on delivery (results arrive on their own)

The #1 lazy-user pain: a delegated answer only surfaced in the SessionStart 📥
block — i.e. when you next OPENED Claude Code. Until then you had to come back and
check. Now a **desktop banner fires the moment a result/note lands**:
- New `notifyDeliveredResults()` runs at the END of every sync round
  (`sync-run` / the scheduled timer): for tasks THIS node delegated that are now
  terminal (and incoming `task-note`s), it fires one clickable macOS banner —
  clicking opens Claude Code, where the 📥 block shows the full answer.
- Dedup uses a SEPARATE notified-set (`~/.memex/task-results-notified.json`), so
  the push is independent of the SessionStart delivered-set: you get the banner
  **now** AND the 📥 block next session — neither suppresses the other.
- Reuses the existing `fireClickableNotification` path (terminal-notifier →
  osascript fallback; no-ops off macOS but still records ids so a later session
  doesn't replay a backlog). Many-at-once collapse to a single summary banner.
- Opt-out: `PARALLELCLAW_NO_NOTIFY=1` or `notifications.task_results=false`.
- New `task-notify [--force] [--json]` CLI to fire/inspect on demand.

## 1.4.0 — note channel: deliver a result without the recipient re-executing it

Found live: a scheduled job on one node reported back via `task-delegate --to mac`,
so the Mac executor tried to "execute" the delivered report as if it were a task
(wasted `claude -p` run + a permission error). Root cause: no distinction between
"a task to DO" and "a result to SEE". New delivery channel:
- **`task-note "<message>" --to <node>`** — a one-shot terminal note (`kind=note`)
  that NO executor ever claims or runs. The recipient surfaces it in its
  SessionStart 📥 ("📨 notes delivered to you") and via `task-notes`.
- `NOTE_KINDS` (note / result / delivery / pulse) are excluded from the executor
  inbox AND from `task-results` (which is for delegated-task answers).
  Added `createNote` / `listIncomingNotes` / `cmdTaskNote` / `cmdTaskNotes`.
- Docs + install skill: report back to another node with `task-note`, not
  `task-delegate`; use `task-update done --result` only to complete a task that
  node delegated TO you.

Tests +2 (note is terminal / not-in-inbox / surfaced-to-recipient / not-a-result;
the executor never runs a delivered note).

## 1.3.4 — `task-get`: retrieve a delegated task's FULL result

A delegating agent had no obvious command for the complete result: `task-list`
and `task-results` truncate fields for display (80 / 200 chars), so an agent that
tried them concluded the answer was "truncated / on the other machine's disk" —
when the full result was sitting in its own synced DB all along. Added:
- **`task-get <id> [--json]`** — the full task: complete prompt AND complete
  result, untruncated. (`task-results --json` `.result` is also the full text.)
- `memex-sync` usage + the delivery docs/skill now point agents to `task-get` /
  `--json` for the full result, never the truncated display.

Surfaced live: Kimi retrieved the result fine via its web UI but, over Telegram,
fumbled the CLI and reported the answer as unretrievable.

## 1.3.3 — install wires coordination automatically (lazy-user)

The OpenClaw install skill (`install-memex-claw`) now sets up COORDINATION, not
just memory capture. A new step registers one recurring "agent loop" job
(**default on**) that (a) executes tasks delegated to the node and (b) DELIVERS
results of its own delegations back to the user (e.g. DMs the Telegram chat that
asked) — the piece that was missing: an always-on node captured memory but never
reported delegated results, so a requester saw silence. Safe defaults: the
`--quiet` cost gate (no LLM tokens on empty ticks), SAFE_KINDS only, destructive
cross-machine actions not auto-run; a single plain "coordination is on — turn off
by removing the job" notice (no blocking question, per the opted-in default-on).

## 1.3.2 — raise per-task time budget to 10 minutes

Research/build tasks routinely outran the 4-minute executor budget. Raised:
- **Mac executor `claude -p` budget: 4 min → 10 min** (`DEFAULT_TIMEOUT_MS`),
  overridable per-deploy via `PARALLELCLAW_EXEC_TIMEOUT_MS`.
- **Claim lease: 15 min → 20 min** — it must stay above the task budget so a
  genuinely-running 10-min task is never mistaken for a dead claimer and requeued;
  the extra margin also absorbs moderate cross-node clock skew.
- The always-on executor (Kimi/OpenClaw) budget is set in ITS scheduler — raise
  that agentTurn timeout to ~10 min (600s) to match (relayed separately).

A task that still exceeds 10 min should return partial progress as `failed`
(it won't be lost — the lease requeues a truly-dead run).

## 1.3.1 — discoverability + result-delivery fixes (from a live Kimi run)

Two issues surfaced when Kimi ran a long research delegation:
- **`memex-sync` usage now lists all command groups** (tasks: `task-delegate` /
  `task-list` / `task-claim` / `task-update` / `task-results` / `task-run-once` /
  `task-executor`; plus the `sync-*` family), not just `install|...|serve`. An
  agent inspecting its own capabilities was misled into thinking it couldn't
  create tasks — `task-delegate` was there all along, just absent from the usage
  line.
- **Executor prompt: return content INLINE in `--result`, never a local file
  path.** Kimi saved a 178-line report to its own `/tmp` and put only the path in
  `--result`; the requester is on another machine and can't read it. Verbatim
  text in `--result` syncs; a path delivers nothing.

## 1.3.0 — instant delegation: push-on-write (no waiting for the sync schedule)

Delegations, claims, and results now fly to the mesh the moment they're written,
instead of waiting up to a full sync interval. Every task write on an initiating
node fires a detached `sync-run --all` immediately — fire-and-forget,
best-effort, a no-op when sync isn't configured (or `PARALLELCLAW_NO_PUSH=1`).
Wired into `task-delegate` / `task-claim` / `task-update`, the
`parallelclaw_delegate` MCP tool, and the Mac headless executor's results.
Live-verified: a delegation pushed to the hub in ~1s (last-sync jumped from 82m
ago to 0m ago) with no manual sync. +1 guard test.

This removes the **requester→hub** leg of the latency. Two legs remain
schedule-bound (tuned by interval, not code): the **hub→executor** forward (the
hub syncs the executor on its own timer) and the **executor's inbox poll**. Drop
those to ~30–60s for near-instant end-to-end; true sub-second needs hub
push-routing + an event-driven executor (next).

Also: **`task-list --inbox --quiet`** — a no-output exit-code gate (0 = has work,
1 = empty) so a 1-min executor cron can pre-check with a free shell call and only
spin up the (token-costly) LLM agentTurn when a task is actually waiting.

## 1.2.2 — durable execution: claim lease + auto-requeue for dead executors

Found live: a research task outran Kimi's 240-second cron tick. The executor had
already written `working` (the claim), but its agentTurn was killed before it
could write a terminal event → the task was stranded in `working` forever. Fixes:
- **Claim lease + lazy auto-requeue (#3b).** A `working` claim is leased (default
  15 min). A stale `working` (claimer died — cron timeout, crash, laptop sleep)
  becomes claimable again and reappears in `task-list --inbox`, so the next
  poller retries it. No task is lost to a dead executor, and no sweeper is needed.
  `claimTask`/`listTasks` gain `now`/`leaseMs` knobs; the Mac executor passes them.
- **Executor prompt + docs:** read the FULL prompt from the `task-claim` output
  (or `task-claim <id> --json`), never the 80-char `task-list` display line; on a
  too-big task write `failed` with partial progress instead of spinning until the
  run is killed or closing `done` with a placeholder (which would block requeue —
  the exact thing that lost the first research delegation).

Tests: +2 (stale working is re-claimable and back in inbox; terminal never
requeues). Full npm test green.

## 1.2.1 — patch: make the Mac executor's `claude -p` actually complete

The Phase 2b headless executor invoked `claude -p` but every run hung until the
4-minute timeout. Two fixes, live-verified end-to-end (delegate → Mac executor →
real `claude -p` result returned in ~7s):
- **Close the child's stdin.** `claude -p` reads stdin to EOF even with the prompt
  passed as an arg; `execFile` left it open, so the process waited forever. Ending
  stdin lets the run finish and exit.
- **Strip Claude-session env markers** (`CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT`,
  `CLAUDE_CODE_SSE_PORT`) before spawning, so the headless child is a clean,
  separate session instead of being rejected as "nested inside another Claude
  Code session" (matters when `task-run-once` is invoked from a Claude shell;
  launchd already runs clean).

Upgrade required for the installed Mac executor: `npm i -g parallelclaw@1.2.1`
(the launchd job picks up the fixed code on its next tick — no reinstall needed).

## 1.2.0 — delegate by talking + the laptop as executor

Coordination, now from natural language. Two MCP tools so any MCP client (Claude
Code, Cursor, OpenClaw) can delegate and check on work without touching the CLI:
- **`parallelclaw_delegate`** `{prompt, to, kind, content}` — hand a task to
  another of your agents. Fired when you say "tell Kimi to …", "delegate this to
  the VPS agent", "have X do this while I'm away". `to:"any"` broadcasts.
- **`parallelclaw_tasks`** `{filter: mine|inbox|results|all, status, limit}` —
  status of what you delegated, your inbox, or completed answers.

SERVER_INSTRUCTIONS gained an "AGENT COORDINATION" recipe so the agent recognizes
the delegation intent and surfaces results from the SessionStart 📥 block.

### Mac headless executor (Phase 2b) — the laptop runs delegated tasks too
`lib/executor.js` + `task-executor install|uninstall|status` / `task-run-once`:
a launchd timer drains the inbox while the Mac is awake, running each claimed
safe-kind task through `claude -p` and writing the result back — closing any↔any
auto-coordination both directions. Selective-auto: only SAFE_KINDS (research /
content / test / general / review / summarize / analysis) auto-run, addressed-to-me
only unless `--any`; the exec prompt forbids destructive actions and `claude -p`
runs without `--dangerously-skip-permissions`. Honest limit: only while awake.
The always-on cron-prompt flow stays the path for Linux/VPS nodes
(`docs/design/executor-prompt.md`). Hands-off loop live-proven on the Mac↔Kimi mesh.

## 1.1.0 — agent coordination: hardened task ledger + executor + result-routing

The first release where the **coordination layer works end-to-end**: one of your
agents delegates a task, an always-on agent runs it, and the answer comes back to
where you asked — riding the existing sync mesh, no new transport.

### Added
- **`task-claim <id>`** — atomically claim a submitted task for execution (exit 0
  claimed / 3 not-claimable / 1 error; `--json`). The always-on executor uses it
  so two pollers never double-run one task. See `docs/design/executor-prompt.md`
  for the ready-to-paste cron-prompt.
- **`task-results [--ack] [--json] [--all]`** — results of tasks you delegated,
  to deliver to your surface. The SessionStart hook now injects a "📥 N results
  from tasks you delegated" block automatically (Claude Code requester); a
  Telegram requester's cron DMs them and acks after sending.

### Changed / fixed (ledger correctness — required before unattended execution)
- **Skew-proof status (#1):** current task state is decided by a monotonic
  per-task `seq` (then lifecycle-rank → ts → rowid), not wall-clock ts. Clock
  skew across nodes can no longer regress a `done` back to `working`.
- **Claim/lock (#3):** optimistic claim resolves concurrent take attempts to one
  deterministic winner once synced.
- **#2** a `to:'any'` task lands in every node's inbox (first to claim wins).
  **#4** unique per-event `msg_id` (no same-ms silent drop). **#5** lifecycle
  guard (no un-completing a terminal, no regress to submitted). **#6** `done`/
  `failed` must carry a result.

Deferred: #7 `listTasks` scan-bound + index (perf), #8 excluding `agent-task`
rows from default search/overview. NL delegate tool (2c) and the Mac headless
`claude -p` runner (2b) are the next slices.

## 1.0.0 — rebrand: memex → ParallelClaw

The product is renamed **ParallelClaw** and repositioned from "AI memory" to a
**personal AI ops layer**: shared verbatim memory is the foundation, and a
coordination layer — any of your agents delegates a task to any other, across
tools and machines — is the direction. No users yet = the cheapest moment to fix
the name before building coordination under it.

This release renames the **surface** only; the **plumbing stays** as back-compat
aliases, so the live sync mesh and every existing install keep working untouched.

### Changed
- **npm package `memex-mvp` → `parallelclaw`.** New bins `parallelclaw` + `pclaw`
  (aliases of `server.js` — the query CLI / MCP server). `--version` now prints
  the package name. Install is now `npm install -g parallelclaw`.
- **READMEs / landing / HELP / SYNC / skills / installers** rebranded to
  ParallelClaw, plus a coordination **vision section** on the landing and READMEs
  (no vaporware: memory ships today; the turnkey delegation loop is rolling out).

### Kept as-is (back-compat — must NOT break the live mesh)
- Legacy bins `memex` / `memex-sync` / `memex-ingest` / `memex-bot` (docs still
  use `memex …` / `memex-sync …` verbatim).
- Data dir `~/.memex`, DB `memex.db`, env vars `MEMEX_*`.
- launchd/systemd labels `com.parallelclaw.memex.*`.
- MCP tool names `memex_*`.
- GitHub repo path `parallelclaw/memex-mvp` and `git clone` dirs (repo rename
  pending — a separate, out-of-band step; URLs unchanged for now).

## 0.14.1 — patch: schema migration at sync-server boot

Upgrading a hub by `npm install -g` alone (no daemon/MCP boot in between) and
restarting the sync-server crash-looped it: the pull handler prepares a SELECT
over the new `origin` column at startup, against a not-yet-migrated DB.
`startSyncServer` now runs `initializeDb` before opening its handle. Found
live during the 0.14.0 rollout.

## 0.14.0 — provenance: know which machine captured every row

In a synced mesh, all nodes' captures share the same `source` labels — two
OpenClaw instances both write `source='openclaw'`, and when they bridge the
same Telegram account they even interleave into ONE conversation. Two agents
in a row misread their own synced DB because of this (one invented a
nonexistent `source='vps1'` to look for — telling). v0.14 stamps every row
with the node that captured it.

### Added
- **`origin` column on messages** — stamped at capture time on every local
  write path (capture daemon, MCP imports, Telegram import, store_document,
  sync self-test, OpenClaw plugin). Value resolution: `MEMEX_ORIGIN` env →
  `origin` in `~/.memex/config.json` → sanitised short hostname, which is
  then **persisted** so a later hostname change doesn't fork the node's
  identity. Rename your node by editing `origin` in config.json.
- **Wire carries provenance** — sync pull/push move `origin` verbatim in both
  directions; a synced row keeps the origin of the node that captured it,
  never the receiver's. Old peers interoperate (unknown field ignored;
  their rows arrive as NULL = "pre-provenance era").
- **`memex_search(origin: "vps1")`** — filter recall by capture node.
- **`memex_get_conversation`** tags each line `[@origin]` when a conversation
  interleaves rows from more than one node (the merged-Telegram-chat case);
  single-origin chats stay untagged. JSON format always includes `origin`.
- **`memex_overview`** shows a per-origin breakdown when the corpus is
  multi-node.

### Notes
- No blind backfill: a node cannot tell its own pre-v0.14 rows from peer rows
  that synced in before provenance existed, and fabricating provenance is
  worse than NULL. Re-imports of local source files DO backfill origin via
  the conflict branch (`COALESCE(existing, incoming)` — never overwrites).
- The OpenClaw plugin (`plugins/memex-openclaw`) ships separately from the
  npm package — update it on agent nodes to start stamping there.

## 0.13.0 — `sync-join`: cross-device memory in two copy-paste steps

Multi-device sync goes from "for operators" to "for lazy users". Laptop with
Claude/Cursor + a server with an agent: the agent runs three commands and
hands you a `memex-join:` token; you paste **one command** on the laptop.
Dogfooded end-to-end by migrating the maintainer's own live mesh — every step
below shipped only after surviving that.

### Added
- **`memex-sync sync-join <memex-join:...>`** — one-command spoke setup:
  token validation → SSH probe (prints your pubkey + exact instructions if
  access is missing) → durable forward tunnel (launchd KeepAlive / systemd
  Restart=always; `ExitOnForwardFailure`, `ServerAlive 30×3`, explicit IPv4
  loopback) → pinned-cert health check → remote registration → first sync →
  15-min auto-sync schedule → hourly watchdog → **marker self-test** that
  proves a note round-trips before declaring success (`✓ end-to-end verified:
  … 3.4s` on the live pair). Flags: `--alias`, `--local-port`, `--every`,
  `--no-watchdog`, `--no-selftest`.
- **`memex-sync sync-server invite --join [--ssh-target u@h]`** — hub-side
  join-token emission (`memex-join:` = pair blob + `ssh_target`, host pinned
  to 127.0.0.1, TTL 30m). The server stays loopback-only; nothing is ever
  exposed publicly — all traffic rides inside SSH on port 22.
- **`memex-sync sync-watchdog`** — read-only hourly health pass (installed by
  join): remotes' `last_sync_at` freshness + tunnel unit state; on silence
  writes `~/.memex/sync-alert.txt` + desktop notification. The "tunnel died
  silently for 6 days" failure mode, productized away.
- **`sync-status`** now reports the tunnel keeper (route, self-healing state)
  and watchdog alongside server/schedule/remotes.

### Changed
- **No env var after joining** — a successful `sync-join` persists
  `sync.enabled: true`; every sync command then works in any shell without
  `MEMEX_SYNC_EXPERIMENTAL=1`.
- **Replication is resumable** — cursors persist after every clean pull page
  and push batch, so an interrupted first sync (network reset, sleep, Ctrl-C)
  resumes instead of restarting from zero; transient network errors
  (ECONNRESET/EPIPE/ETIMEDOUT/ECONNREFUSED) retry in place with backoff tuned
  to the tunnel's ~15s self-heal.
- **Re-joining a known hub keeps cursors** — if the token's cert fingerprint
  matches the existing remote's, sync-join preserves cursor state instead of
  forcing a full re-replication of an already-converged pair.
- **Docs lead with the lazy flow** — landing page, README (EN/RU), SYNC.md
  quickstart, HELP.md section, MULTI_MACHINE.md legacy patterns marked
  deprecated.

### Fixed
- Tunnel-keeper script generation emitted a broken line continuation (`\ \`)
  when no SSH identity file was configured — ssh received a stray empty
  argument and the tunnel flapped forever. Regression-tested, including
  backslash hygiene of every continuation line.

## 0.12.0 — agent retrieval: reach any part of memory

Sharper recall for agents querying memex — especially across long sessions and
narrow time windows. Backward-compatible: every new parameter is optional and
defaults to the previous behaviour.

### Added
- **`memex_get_conversation` paging** — new `offset` and `order` (`asc`|`desc`)
  parameters, plus a `total` count in the output. A long session (e.g. 3000
  messages) can now return its **freshest** tail (`order:"desc"`) or be paged
  end-to-end — previously only the first ~N (oldest) were reachable. Fixes a real
  gap found live: an agent could not show the June messages of a 2900-message
  Claude Code session.
- **`memex_search` date-range filter** — `since_ts` / `until_ts` (Unix seconds,
  inclusive) restrict results to a window ("what did we discuss about X in June").
  A true filter, distinct from `sort` (orders only) and `half_life_days` (boosts
  only). Numeric bounds naturally exclude undated rows.
- **`memex_search` within-conversation scope** — `conversation_id` confines a
  keyword search to ONE session (exact id, unlike the fuzzy `chat` title match).
  Pair it with `memex_get_conversation` paging to locate, then read around a hit
  in a huge session.
- **Retrieval recipes** baked into the tool descriptions and `HELP.md` (new
  "Рецепты поиска для агентов" table) so agents discover these paths without
  guessing — date window, within-session search, freshest-first, page-a-giant.

### Fixed
- **`memex_get_conversation` no longer hides the tail of long conversations** —
  the handler always sorted `ts ASC LIMIT N` with no offset, so the newest
  messages of any session past the limit were unreachable.
- **`test/sync/mcp-invite.test.js` is now hermetic** — the liveness-warning
  assertion used a hard-coded port and failed on hosts that actually run a
  sync-server on 8766; it now probes a guaranteed-closed port.

## 0.11.11 — experimental multi-device sync

First cut of **local-first, multi-device sync** — converge two machines'
`memex.db` over the network with no cloud relay. Gated behind
`MEMEX_SYNC_EXPERIMENTAL=1`; the wire protocol may change before it graduates
to stable. Full guide + spec in [SYNC.md](SYNC.md).

### Added
- **Sync engine** — HTTP push/pull + per-peer cursors. Conflict-free via the
  existing `UNIQUE(source, conversation_id, msg_id)` constraint (verbatim memory
  is append-only — nothing to merge). TLS with self-signed cert + **fingerprint
  pinning**, 256-bit bearer auth.
- **CLI** (`memex-sync sync-*`):
  - `sync-server start | install | uninstall | status` — run the hub, optionally
    as a durable systemd-user / LaunchAgent service that survives reboot.
  - `sync-server invite [--host H] [--port N] [--ttl 30]` — print a one-paste
    `memex-pair:` token (bundles host + port + cert fingerprint + bearer, with TTL).
  - `sync-pair <blob> [--alias vps]` — register a remote from a pair token.
  - `sync-add <alias> <url> <bearer> (--cert-fp F | --insecure)` — explicit form.
  - `sync-run <alias> | --all` — one bidirectional sync.
  - `sync-schedule install [--every 15m] | uninstall | status` — hands-off
    auto-sync on a timer.
  - `sync-list / sync-remove / sync-status`.
- **`memex_sync_invite` MCP tool** — lets an agent emit a pairing token from a
  plain-language request ("set up sync with my Mac"). Surfaced only when
  `MEMEX_SYNC_EXPERIMENTAL=1` is set in the MCP server's environment.

### Reliability
- **Adaptive push batching** — pre-flights payload size and shrinks before the
  2 MB body cap; backstops 413 / EPIPE by halving and retrying.
- **No silent row loss** — pulled rows are applied with retry; on the FTS5
  "database disk image is malformed" error the index is rebuilt once and the
  batch retried; if rows still won't apply, sync aborts **without advancing the
  cursor** (loud failure over silent loss). Skips are counted and surfaced.
- **busy_timeout** on the sync connection to ride out the capture daemon's
  concurrent writes.

### Verified live
- Bidirectional Mac ↔ VPS sync over the public internet (32k+ rows each side,
  cert-pinned), durable-server restart with credentials preserved, hands-off
  auto-sync of a fresh conversation within one interval, and the full
  agent-chat-phrase → pair-token → sync wow-flow.

### Dependencies
- Added `selfsigned` (self-signed TLS cert generation for the sync server).
