Skip to content

Remote memory system - #1

Open
kovrichard wants to merge 36 commits into
mainfrom
feat/portable-memory
Open

Remote memory system#1
kovrichard wants to merge 36 commits into
mainfrom
feat/portable-memory

Conversation

@kovrichard

Copy link
Copy Markdown
Owner

pal cli import called zip.extractAllTo(home, true), overwriting every colliding path. Importing machine A onto machine B discarded B's side of every append-only log — the receiving machine's ratings, reflections and project history were replaced wholesale, silently and with no undo. That is data loss inside the feature that advertises portability.

Import now merges by default:

*.jsonl union of both sides, deduplicated by exact line
new files written as-is
identical no-op
diverged local kept in place, incoming quarantined under backups/
denylisted never written

Deduplication is required rather than optional: a zip import has no merge base, so a plain concatenation would double every record on re-import. Exact-line identity is the key — PAL's jsonl files share no schema, and every writer serializes a record the same way, so byte equality is the only key that holds across all of them.

machine.json is denied at the import boundary. It carries an install's identity; importing it would give two machines one id and silently break every origin-scoped read built on top of it.

--overwrite preserves restore-a-backup semantics as an explicit choice, and dry-run names which of the two modes it would take.

Each run appends to memory/state/import-log.jsonl so a merged corpus stays attributable.

Verified by breaking it first: removing the dedupe check fails only the idempotency test; reverting to extractAllTo fails merge, quarantine and idempotency.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer

`pal cli import` called `zip.extractAllTo(home, true)`, overwriting every
colliding path. Importing machine A onto machine B discarded B's side of
every append-only log — the receiving machine's ratings, reflections and
project history were replaced wholesale, silently and with no undo. That
is data loss inside the feature that advertises portability.

Import now merges by default:

  *.jsonl      union of both sides, deduplicated by exact line
  new files    written as-is
  identical    no-op
  diverged     local kept in place, incoming quarantined under backups/
  denylisted   never written

Deduplication is required rather than optional: a zip import has no merge
base, so a plain concatenation would double every record on re-import.
Exact-line identity is the key — PAL's jsonl files share no schema, and
every writer serializes a record the same way, so byte equality is the
only key that holds across all of them.

`machine.json` is denied at the import boundary. It carries an install's
identity; importing it would give two machines one id and silently break
every origin-scoped read built on top of it.

`--overwrite` preserves restore-a-backup semantics as an explicit choice,
and dry-run names which of the two modes it would take.

Each run appends to memory/state/import-log.jsonl so a merged corpus stays
attributable.

Verified by breaking it first: removing the dedupe check fails only the
idempotency test; reverting to extractAllTo fails merge, quarantine and
idempotency.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- run the suite with --randomize
- scope the signals and token-usage homes to each test
- capture the cli init and subagent link results in beforeAll
- give the needsRebuild check its own agent directories
- build the export zip in each test that reads it
- prime the install idempotency check with its own install

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- add stryker.config.mjs mutating hook libraries, agent tools, and target helpers
- add .agents/scripts/mutate-diff.ts to mutate only the changed line ranges
- add test:mutate and test:mutate:diff scripts
- add a mutation workflow gating pull requests on ubuntu with bun 1.3.13
- register the config with biome and the script with knip
- ignore the mutation sandbox in biome, typescript, and git

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert the settings merge/unmerge round-trip returns user settings intact
- cover hook path canonicalization, permission dedup, and per-key user precedence
- cover the cursor hook merge/unmerge pair including exact-match uninstall
- cover vscodeSettingsFile across macos, linux, windows, and unsupported platforms

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- rewrite ISC-N's text while keeping its id and open or closed state
- resolve the ISC from Criteria first, then the Changelog
- return the previous text alongside the new one
- require replacement text

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- model an ISC as open, done, or retired instead of a checked flag
- store a retired ISC as [~] under a Retired changelog heading
- record the superseding ISC with --by
- count a retired ISC as neither open nor done, and list it with --retired
- reopen a retired ISC back into the open set
- keep a retired id reserved in both the project and migrate id scans
- report show-isc and list-isc status as open, done, or retired

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- skip .test-install-home, whose skill symlinks resolve only while the suite runs

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- report success with the skip reason when git status is empty
- treat an unreadable git status as changed so the gates still run
- count untracked files as changes

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- scan the tree with the recommended rule preset
- run it at session end in every agent, on pre-commit, and in CI
- skip lockfiles, archives, and generated test homes

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- detect circular imports across src
- run it at session end in every agent, on pre-commit, and in CI

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- fail when a tracked file carries CRLF in the index or the worktree
- convert offenders to LF with lf:fix, leaving binary content untouched
- run the check at session end in every agent, on pre-commit, and in CI

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
…hold

- scope the ring to the modules the in-process suite can reach
- record the ratchet rule for removing an exclusion and raising the threshold
- set break to 50, one rung below the measured 54.87 percent baseline

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert global fields survive for every platform
- assert the target platform block is un-indented into the frontmatter root
- assert other platform blocks are stripped
- assert an agent with no block for a platform keeps only its global fields
- assert body content and its horizontal rules survive extraction
- cover install idempotence and removal leaving unrelated files alone

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- turn the docs, tools, skills, claude-agents, and personal-agents paths into getters
- let a caller that sets PAL_HOME after import reach the intended directory

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert the index records name, description, and generation timestamp
- assert skills without frontmatter or without a name are skipped
- assert quoted descriptions are unwrapped
- cover trigger extraction: use-when keywords, stopword removal, domain terms, dedup
- assert linkPersonalSkill throws for an unknown skill and links only installed agents
- assert the link is a symlink and re-linking stays idempotent

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
…tallation

- assert scaffolding creates from templates and preserves user edits
- assert deprecated loadAtStartup entries are stripped and valid ones kept
- assert malformed settings are left as-is
- assert docs install with a tools symlink and both are removed together
- assert the statusline installs per agent target and removes only that target
- assert shipped skills link into the PAL store, the agent dir, and the agents skills link
- assert skill installation is idempotent and removal reports each name

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- record rung 2 of the ratchet at a measured 58.77 percent ring score

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert the dated header appears once and each append gets an HH:MM section
- assert opinion notes carry their confidence and omit it when absent
- assert the session id and cwd are recorded only when an id is given
- assert duplicate notes are skipped case-insensitively while fresh ones land
- assert recall joins days with a rule, honours the day window, and skips empties
- assert recall ignores non-month directories and non-markdown files

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert rating averages, low counts, and the ten-item recent window
- assert the trend classifies as improving, declining, or stable
- assert reflection criteria totals, pass rate, and the three-observation tail
- assert session titles parse from frontmatter, a bold marker, or the filename
- assert sessions group by date newest first and honour the day window
- assert writeSynthesis persists the state and derives the signal cache windows

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- record rung 3 of the ratchet at a measured 62.22 percent ring score

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert a transcript under two messages and an unparseable one are ignored
- assert the last assistant response caches per session, truncated to 2000 chars
- assert a supplied last message wins over the one in the transcript
- assert a corrupt cache is replaced and other sessions survive an upsert
- assert the cache caps at twenty sessions by evicting the oldest
- assert a pending failure is claimed out of the state directory
- ignore the per-suite sandboxes a detached child can recreate after cleanup

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- assert wisdom principles render with domain and confidence above the bar
- assert the learning digest lists cross-project titles and caps at five
- assert relationship notes keep world facts and scope sessions to the project
- assert opinion entries and html comments are stripped
- assert session intelligence renders trend, pass-rate, and project observations
- assert the handoff surfaces while in progress and lapses after a week
- assert semi-static content injects only when no agent loads it natively

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- raise the threshold from the modules a change touches, not a whole-ring run
- record why the whole-ring run costs 45 minutes for a three point move

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- replace the sh find pipeline with readdirSync so the check runs on Windows
- sort the hits so the asserted match is deterministic

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- give each agent directory its own klint-rules skill instead of a link into node_modules
- clear the klint/skill-legacy-link warnings the new version reports

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- rename within PAL_HOME so the claim survives a temp dir on another volume
- keep the atomic claim that stops two concurrent stop hooks racing

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- pin every action to the commit its version tag resolves to, annotated with that version
- install dependencies with --ignore-scripts so no package runs code at install time

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
- add .github/hooks/gates.json running the same eight checks on agentStop
- document the Codex and Copilot wiring alongside the other agents
- bring the gate table and chain listing in line with what the hooks now run

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
…eference

Records that cross machines need an origin, and a name for that origin has
to survive being renamed. This adds the identity without touching any
writer yet.

~/.pal/machine.json holds { id, label, os, createdAt }. The id is a uuid,
generated once and never regenerated: discarding one orphans every record
that referenced it. A stored file carrying a usable id is therefore
repaired rather than replaced when other fields are missing or corrupt.

The default label is derived from the id, deliberately not from the
hostname — a hostname routinely carries the owner's real name, and labels
travel inside every exported registry entry.

Records store the id and never the label. displayName() resolves id to
label at read time against memory/machines/<id>.md, so renaming a machine
is a one-file edit no stored record notices, and two machines sharing a
label is a display concern: the name is suffixed with the short id only
when the registry actually shows a collision. Uniqueness is never
enforced, because the registry is not always reachable. An unknown id
degrades to its short form rather than throwing.

machine.json sits at the PAL_HOME root, outside every exported directory,
and is denied at the import boundary. Registry entries live under memory/
and do travel, which is how a foreign id becomes a name. Export archives
also carry export-manifest.json naming the producing machine — after a
merge an archive can hold entries for several machines, so the manifest
says which one made this one. Import records it in the import log.

Importing now registers the receiving machine too: a box that only ever
imports previously had no identity at all.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
Stryker left 12 survivors in machine.ts. Each one named a real gap rather
than a cosmetic one:

- an empty, non-string, or array-shaped id was never exercised, so the
  usability check could have been reduced to a truthiness test
- the repair path never saw a whitespace-only label or an empty os, so the
  trim and the fallbacks were unasserted
- readRegistry was never given a non-markdown file, an entry with an id but
  no label, or an entry omitting os
- displayName was only ever asked for the first registry entry, so matching
  on id rather than position was unproven

machine.ts 87.25% -> 99.02%, export.ts 75% -> 100%.

The one remaining survivor is equivalent: dropping the optional chain in
`typeof v?.id` throws on a null parse, which the surrounding catch turns
into the same regeneration the original produces. There is no observable
difference to assert.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
kovrichard and others added 2 commits August 18, 2026 19:26
…ones

Two machines with the same project checked out at different absolute
paths could never benefit from retrieval.ts's cwd scope boost: a
reflection stamped one machine's absolute path can never string-equal
another machine's mount, so SCOPE_BOOST silently never fired across
machines. Relocating a project on one machine (project.ts set-path) also
orphaned every existing memory that had stamped the old absolute path,
since retrieval compared against that stamp directly rather than through
the registry.

An anchor replaces the absolute prefix with the project's registry slug —
`{proj:slug}/relative/path` instead of the machine-specific path — and
resolves back to a real path at read time against whatever is registered
locally. Same trick as machine.ts's id/label split, applied to paths:
store a stable reference, resolve it locally, so a relocation or a second
machine is a registry lookup rather than a rewrite of every record that
mentions the old path.

algorithm-reflect.ts and thread.ts anchor their cwd stamp at write time.
retrieval.ts resolves an anchored (or plain, pre-existing) cwd against the
local project registry before the scope-boost comparison, loading the
registry once per rank() call rather than once per doc.

No backfill: existing raw-path records pass through resolveAnchor
unchanged and are compared exactly as before.

Verified with break-tests (longest-match guard, ignoring the relative
segment, treating unresolvable as a match) and Stryker's diff-scoped
mutation gate: anchor.ts at 100% excluding two documented-equivalent
mutants on a guard that is unreachable under resolveProjectFromCwd's
current contract.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
@kovrichard
kovrichard force-pushed the feat/portable-memory branch from b0f6c9c to f270aaa Compare August 18, 2026 17:44
emitSignal is the single chokepoint for ratings.jsonl, token-usage.jsonl,
and every future signal type, so this covers all of them in one edit.
Without an origin, a synced signal from another machine is indistinguishable
from a local one — there is no way to scope stats, exclude a machine, or
attribute a rating to where it happened.

The id comes from machine.ts (already built): a stable uuid, never the
label, so renaming a machine never touches a stored signal.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
The <!-- session:… cwd:… --> comment in daily relationship notes was the
last cwd-stamping writer still storing a raw absolute path. Anchored it
the same way as the reflection and thread writers.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
…lection

Extracted the record-assembly logic out of the argv-parsing CLI entrypoint
into an exported buildReflection(), so the anchor and origin stamping are
directly unit-testable without spawning the CLI — Stryker's diff gate
excludes subprocess-driven suites from mutation scoring, so this is what
it takes for the wiring to actually be graded.

Same two stamps as the other writers: cwd anchored via encodeAnchor, and
m set to this machine's id via machine.ts. No behavior change to the CLI
itself.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
Exported addThread() so the anchor and origin stamping are directly
unit-testable without spawning the CLI. Same two stamps as the other
writers: cwd anchored via encodeAnchor, m set to this machine's id.

That closes out Phase 1's writer set: every cwd-stamping record
(reflections, threads, relationship notes) and the central signals
chokepoint now carry a portable anchor and a machine origin.

Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant