Skip to content

Develop draft pr - #47

Closed
gowtham0992 wants to merge 62 commits into
mainfrom
develop
Closed

Develop draft pr#47
gowtham0992 wants to merge 62 commits into
mainfrom
develop

Conversation

@gowtham0992

Copy link
Copy Markdown
Owner

No description provided.

The field converged on procedural memory (skills-as-memory: how-to
knowledge for recurring tasks) as the next layer for coding agents.
Link's version keeps every architectural commitment: recipes are plain
Markdown memories, review-gated, deterministic, and shared across agents.

- New memory_type 'procedure' with an optional trigger phrase describing
  when it applies (frontmatter field, 200-char bound). The page template
  puts the trigger in 'Use This When'.
- Recall scores the trigger like intent-bearing head fields and includes
  it in semantic embeddings, so task-shaped queries ('how do I prepare a
  release') find recipes phrased differently. Recalled procedures carry
  a bounded steps excerpt in recall packets so agents can follow them
  without another file read. Procedures get the same temporal rank boost
  as preferences/decisions.
- --trigger on lnk remember; trigger on MCP remember and remember_memory.
- Session-end proposals detect runs of three or more numbered step lines
  and propose them as procedure memories with the preceding goal line as
  the trigger; capture acceptance carries the trigger through. Saving
  still requires explicit user approval.
- Agent instructions (installed variants + MCP resource), LINK.md,
  PyPI README, CLI docs, and CHANGELOG updated: after a notable
  multi-step task, agents offer to save a recipe.

Verified end-to-end: remember -> task-shaped recall with steps ->
auto-proposal from a session transcript. 819 tests + guards green.
The dominant junk source in LLM-extracted memory systems is the system
hearing itself: injected prompts stored as memories and recalled
memories re-extracted as new entries (a mem0 production audit measured
97.8% junk, 52.7% of it the system's own prompt text).

Link's automatic session capture now makes that loop impossible:

- Layer 1 (extraction): transcript messages carrying Link-injected
  output (session-start brief, consolidation plans, session-end text)
  are dropped before proposal extraction, via LINK_ECHO_MARKERS.
- Layer 2 (proposals): is_existing_memory_echo() drops proposals that
  restate an existing active memory. It checks asymmetric token
  containment against the memory's core claim (title+TLDR and the
  Memory section), which catches framing-diluted restatements
  ('Per your saved preference, we decided ...') that symmetric
  duplicate detection misses. Conflicting proposals are kept — a
  contradiction is signal, not echo.

Automatic capture stores only fresh or conflicting proposals;
deliberate refinements still flow through manual lnk session-end.
Tests prove both layers end-to-end: a session that only restates
stored memory leaves no capture; a session with the injected brief
plus a genuinely new decision captures only the new decision.
…y labels

Memory mis-scoping (one project's conventions leaking into another) is a
documented top failure mode of agent memory systems. Link's answer is
deterministic and inspectable, like everything else in the wiki:

- applies_when frontmatter with project:<slug>, path:<glob>, and
  task:<phrase> conditions (OR semantics), validated at write time,
  settable via lnk remember --applies-when and the MCP remember tools.
- Recall evaluates conditions against the query, active project, and an
  optional context path: matched conditions boost rank; out-of-context
  conditional memories are demoted, still findable, and labeled
  applicability: out_of_context so agents ask before applying them.
- Startup briefs (including hook-injected briefs, which evaluate path:
  conditions against the session's working directory) exclude
  out-of-context conditional memories entirely.
- CLI recall/query evaluate path: conditions against the caller's cwd.

No classifier, no model: conditions are plain frontmatter a user can read
and edit. Tests cover parsing validation, all three condition kinds,
recall demotion/boost ordering, and brief exclusion.
…n similarity checks

Temporal reasoning is the declared open frontier in agent memory research.
Link's answer works at personal scale with zero graph database:

- lnk remember --supersedes <name> (and MCP remember tools) replaces an
  outdated memory atomically inside one operation journal: the successor
  carries supersedes lineage, the predecessor is archived with
  superseded_by and reason, and default recall returns only the current
  truth. Conflict refusals now suggest supersession instead of only
  offering coexistence overrides. explain-memory walks the chain in both
  directions.
- lnk recall --as-of YYYY-MM-DD reconstructs what was active on a past
  date purely from lifecycle fields (capture, supersession/archive,
  expiry): memory_active_at() is deterministic and needs no new storage.

Also fixes a latent bug this work exposed: duplicate/conflict detection
compared full templated pages, whose boilerplate diluted token overlap —
contradictions between real memory pages could slip through undetected
while hand-built test records were caught. All similarity checks
(duplicates, conflicts, echoes) now compare memory core claims via
memory_claim_text(). Conflicts take priority over duplicates, and
conflict candidates are excluded from duplicate candidates, since a
claim cannot be both the same and opposing.

Verified end-to-end: conflicting write refused with supersede guidance;
supersession archives with two-way lineage; recall returns only the
successor; as-of returns the predecessor for past dates; lineage renders
from either end of the chain.
…d against it

Track 3 of benchmarks/RESULTS.md: a deterministic multi-month session
simulation (112 authored events: 42 facts, 12 mid-stream revisions, agent
echoes, Link's own injected briefs, noise sessions — every event
ground-truth labeled, no LLM) driving two pipelines over real Link core
functions:

- gated: Link's pipeline (extraction filter, echo containment, duplicate
  refusal, detected contradictions resolved by supersession with lineage)
- ungated: the same extractor and retrieval with governance off — what
  unsupervised LLM-extraction memory does architecturally

Measured: junk 0 (0.0%) vs 16 (23.9%); contradiction exposure@3 0.333 vs
0.833; active store 40 vs 67; as-of temporal accuracy 1.00. CI runs the
benchmark as a hard gate (tests/test_hygiene_benchmark.py).

Building the benchmark immediately earned its keep by catching two real
gaps, both fixed:

- Conflict detection missed revision-shaped contradictions ('we don't X
  anymore; now Y'): negation-XOR fails when the original claim also
  contains a negation, and symmetric overlap punishes revisions for
  adding replacement content. New rule: a revision cue plus subject-token
  containment of the record's head claim (boilerplate cue tokens excluded
  on both sides, which also fixed a false positive connecting unrelated
  memories). Detector now catches 8/12 authored revision shapes; exposure
  improved from 0.417 to 0.333 and the remaining gap is published.
- Echo guard missed partial restatements whose truncated text contained
  little of the full claim but whose own tokens lived almost entirely
  inside it; mirrored containment now drops them.
…olidation

- lnk recipes lists active procedure memories newest-first with their
  trigger phrases and first step; lnk recall --type <memory_type> filters
  recall (e.g. --type procedure for recipes only).
- Consolidation plans now surface recurring themes: captures that are
  related but not duplicates (snippet Jaccard between the theme floor and
  the duplicate threshold) are clustered deterministically and presented
  as candidates for one durable memory. This is the review-gated answer
  to 'memory systems should learn user patterns': detection is automatic,
  writing is always proposed to the user. Documented as runnable by a
  scheduled idle-time agent session (a dream pass).
- Renamed the MCP server's recall adapter to satisfy the runtime
  duplication guard after the CLI adapter grew context/temporal params.
…hmark framing

From a critical self-review of 1.6.0+1.7.0 for user friction and
misunderstanding risks:

Visible safety (silent protections were unexplainable):
- Conflict refusals now lead with the paste-ready resolution: rerun with
  --supersedes <name> (and --supersedes accepts the memory title, not
  just the slug). Success output shows what was superseded.
- Terminal recall shows applicability warnings ('out of context here —
  verify with the user'), recipe triggers, and step previews; these
  signals existed in JSON but were invisible to humans.
- lnk hook session-end --explain prints the decision trail: messages
  dropped as Link's own output, proposals dropped as echoes or
  duplicates, trivial-session and duplicate-event skips.

Concept clarity (ten memory knobs, three overlapping):
- One field rule everywhere users and agents look (remember --help
  epilog, both installed instruction variants, the MCP remember
  docstring, CLI docs): finding it -> trigger · fencing it ->
  applies_when · owning it -> scope/project/visibility · replacing it ->
  supersedes · aging it -> review_after/expires_at; when unsure, none.
- All Commands reference lines updated for recall/remember flags;
  lnk recipes panel added.

Defaults and reach:
- Semantic tier now splits by surface: short-lived CLI prefers the
  instant fast tier, the MCP server prefers quality; explicit
  LINK_SEMANTIC_PROVIDER always wins. Installing the quality extra no
  longer slows interactive commands. Verified with both providers
  installed.
- lnk connect --hooks-settings <path> enables project-scoped hooks
  (e.g. a repo's .claude/settings.json) for multi-workspace users.

Benchmark honesty:
- Hygiene baseline explicitly labeled a governance ablation of Link
  itself with an invitation to run the stream through other systems;
  conflict-detector 8/12 disclosed as a fit on its development set;
  'zero junk' defined precisely as zero self-inflicted junk.
lnk seed now also reads decision-record files (docs/adr/, docs/adrs/,
docs/decisions/, adr/) alongside the existing allowlist, and
deterministically extracts each ADR's Decision section into a
proposal-only decision memory candidate: title plus a bounded excerpt,
with a paste-ready save command in the seed output.

Nothing is written automatically — a repo's existing rationale becomes
governed, recallable, supersedable decision memory only after the user
approves each candidate. Extraction is deterministic (heading parse, no
LLM), respects the seed pipeline's existing secret scanning and size
limits, and files without a Decision section are ignored.

Verified live: an ADR's Decision section surfaces as a review-gated
candidate in seed output; dry runs write nothing.
Session-end hooks extracted the whole transcript (user + assistant) and
fed it to the memory-proposal extractor, which is speaker-blind. A cue
like the assistant writing "you prefer small commits" was mined and
proposed as the user's own preference — found while dogfooding 1.6.

Mine proposals from the user's turns only via a new roles= parameter on
extract_transcript_text; keep the full transcript in the raw capture for
review context via session_end(proposal_text=...). Precision over recall
for automatic writes: the user can always remember explicitly, and junk
proposals are worse than the occasional miss.

Regression tests cover both the extractor (assistant prose excluded,
user preference kept) and the hook (assistant-only session captures
nothing; a user-stated decision still captures).
The prior fix commit accidentally bundled .claude/launch.json (a local
Preview dev-server config, not part of the project). Untrack it and
gitignore .claude/ so local editor/preview tooling never lands in a
feature commit again. Matches develop, which never tracked it.
First Windows CI run of the hooks tests (CI is PR-triggered, so the 1.6
work had only seen POSIX until the release PR) failed on two assertions,
both test bugs — the product output is correct on both platforms:

- test_build_preview_includes_both_events_and_commands asserted POSIX
  shlex single-quoting; display_command intentionally uses
  list2cmdline double quotes on Windows. Assert the platform's quoting.
- test_connect_hooks_preview_includes_session_hooks_payload compared the
  absolute temp path, but Windows mkdtemp returns the 8.3 short form
  (RUNNER~1) while the built command holds the resolved long form
  (runneradmin). Assert the stable demo/link.py path tail instead.
# Conflicts:
#	.gitignore
#	CHANGELOG.md
#	link.py
#	mcp_package/link_core/agent_hooks.py
#	tests/test_agent_hooks_core.py
#	tests/test_link_cli.py
Adversarial review of the 1.7 train found memory_applicability swallowed
parse errors and returned "unconditional" — the least safe reading. The
wiki is hand-editable by design, so a one-character typo in a condition
("proj:" for "project:") silently removed the fence: the memory recalled
in every project with no out-of-context warning, which is the exact
mis-scoping failure applies_when exists to prevent. Verified end-to-end
before the fix.

Treat malformed conditions as out_of_context (the fence the user wrote
still holds, conservatively) and flag the syntax error as a high-severity
invalid_applies_when review issue with repair guidance, so it surfaces in
the memory inbox instead of failing silently either direction.
1. A procedure saved from numbered steps without --title was literally
   titled "1" (the sentence splitter took the list marker as the first
   sentence), landing at wiki/memories/1.md. Procedures now fall back to
   their trigger phrase for the title, and derived titles skip leading
   step numbering for every memory type.

2. Accepting a capture dropped the procedure proposal's trigger: capture
   accept args carried it, but both the CLI and MCP accept paths omitted
   the kwarg when writing the page. The changelog promise ("accepting a
   capture carries the trigger through") is now true end-to-end, with a
   CLI test proving transcript -> proposal -> accept -> frontmatter.

3. The ADR "Save if approved" command truncated the decision text at 80
   chars with an ellipsis inside the quoted command — pasting it would
   have saved a corrupted memory. The command now carries the full
   bounded text via shlex quoting, tested for parseability.

Also verified this pass: the entire 1.7 command surface runs with
Python sockets hard-blocked (local-first holds at runtime, not just in
CI lint), and supersedes/as-of/conflict-guidance/--explain all work
end-to-end as documented.
…lysis

Per-category LoCoMo analysis showed the dominant retrieval miss is
conversational deixis: gold turns like "the stories were so inspiring"
share no vocabulary with their question — the meaning lives in the turns
around them. Indexing each memory in isolation throws that context away.

New primitive: a record's optional `context` field carries text from
around the memory's origin. It counts toward lexical scoring and the
semantic embedding, so recall can FIND the memory by what it was about —
but it is never part of the claim: echo/duplicate/conflict checks compare
claims only (memory_claim_text), and slim recall output drops it, so
governance and display stay clean. Tests pin all three properties.

The LoCoMo adapter now populates context with each turn's ±1 dialogue
neighbors. Measured (10 conversations, 5,882 turn-memories, 1,536
third-party queries): hybrid any-evidence hit@10 0.685 -> 0.737, evidence
recall@10 0.608 -> 0.660, lexical hit@10 0.578 -> 0.628; single-hop
hit@10 0.713 -> 0.816 in the prototype breakdown. The tempting
alternative — splicing a hit's neighbors into the ranked list at recall
time — measured WORSE (0.685 -> 0.550) and is recorded in RESULTS.md as
a failed ablation: context must inform scoring, not bypass it.
applies_when path: conditions could only ever match through session hooks
and the CLI (both know the working directory); the MCP server had no way
to learn it, so path-fenced memories were permanently demoted as
out_of_context for MCP agents even in exactly the right directory. The
slim recall tool now accepts context_path and threads it through to
applicability evaluation, with a contract test proving a path-fenced
memory reports matched with the path and out_of_context without it.
RRF(lexical, semantic) fusion, Okapi BM25 in place of the field-weighted
lexical scorer, and deterministic HippoRAG-style entity activation (one-
step spread over a speaker/proper-noun graph) all measured below the
shipped ranking on the LoCoMo retrieval track (best challenger hit@10
0.691 vs shipped 0.737). No product change: the current ranking survived
every challenger this cycle. Multi-hop evidence recall (0.350) remains
the known headroom and is honestly framed as agent-side iterative recall
territory rather than memory-layer reasoning.
…call

The week's retrieval study converged on one architecture change that
passed the two-benchmark gate (lift LoCoMo AND hold the bundled suite):
a tiny local ONNX cross-encoder (ms-marco-MiniLM-L-6-v2, 0.08 GB) that
re-orders the top 50 recall candidates by reading each (query, memory)
pair. Its order is BLENDED with the retrieval order via reciprocal-rank
fusion, never substituted — pure reranking collapsed hit@1 0.38 -> 0.18
in ablation by promoting topically related non-evidence memories.

Measured on the default embedder:
- LoCoMo: any-evidence hit@10 0.737 -> 0.794, evidence recall@10
  0.660 -> 0.717, multi-hop evidence recall 0.350 -> 0.403 (the number
  no cheap structural trick had moved)
- bundled 1,176-case suite: token-overlap hit@1 0.749 -> 0.839,
  pure-paraphrase hit@5 0.338 -> 0.436

Product shape: pip install "link-mcp[rerank]"; applies only to explicit
CLI recall and MCP recall (~0.5 s at 50 candidates) — session hooks and
briefs never pay the latency. Same guarantees as the semantic tiers:
offline-only model load, LINK_RERANK=off disables, LINK_RERANK_MODEL
overrides. Results carry rerank: blended so the re-ordering is visible.

Also documented from the same study: embedding-model rankings invert by
text shape (nomic wins conversational archives, MiniLM wins claim-shaped
memory pages), so the default embedder stays MiniLM and nomic becomes
the documented LINK_SEMANTIC_MODEL alternative.
Two gaps from the honest self-audit:

1. The rerank tier shipped without any user-facing setup: the offline
   guard (correctly) never downloads, and nothing fetched the model, so
   installing link-mcp[rerank] produced silently nothing. lnk semantic
   --setup now fetches the rerank model alongside the embedder (the one
   sanctioned download moment, sharing the same offline-guard helper),
   and lnk semantic status reports the tier's state with the exact next
   command in every configuration.

2. lnk --help was a 60-command brace wall — the single worst first
   impression in the product. Commands now render in seven task-shaped
   groups with 'New? Run: link.py try' up top; per-command --help is
   unchanged. A guard test asserts every registered command appears in
   exactly one group and no group lists a ghost, so future commands
   cannot silently vanish from help.
The context/rerank improvements were selected by scores on the same
query set we report — LoCoMo has no held-out split. Say so plainly:
the deltas are dev-set results, mitigated by the two-benchmark gate
(every change also had to hold the bundled suite), fully rerunnable.
The 1.7 sibling of the aha demo: one 18-second terminal scene showing
the arc no competitor can demo — a new memory conflicts with an old
one, Link refuses to silently pile up truth, --supersedes replaces it
with lineage, recall returns only the current truth, and --as-of still
answers what was true back then. Self-contained animated SVG in the
same visual language as link-aha.svg, generated by script (keyframe
math reproducible), registered in REQUIRED_ASSETS, embedded under the
existing demo on getting-started. Verified rendering in the browser.
Same placement pattern as the aha demo: the new figure is cloned from
the existing JSON-escaped block by string transformation, so the
escaping conventions are identical by construction (the hand-editing
of this template is what broke the page once before). Verified in the
browser: JSON parses, zero console errors, both animations load.
The GIF twin of link-truth.svg, rendered with vhs from the repo's
development runtime (brew lnk predates --supersedes): a backdated
deploy decision, a revision that trips the conflict detector for real
(Possible conflicting memory found · revises_existing_claim), the
--supersedes replacement, recall returning only the current truth, and
--as-of 2026-05-01 returning the old decision honestly labeled
Recall: disabled. Every frame is genuine product output.

Also caught by recording: the SVG's original phrasing (deploys moved
to Tuesdays) did NOT trip the conflict detector — no revision cue.
Both animations now use phrasing the detector actually fires on, so
the marketing shows exactly what the product does. The SVG generator
is checked in (scripts/generate_truth_svg.py) and both assets are
registered in REQUIRED_ASSETS.
Walking the released 1.6.0 flow end to end (brew install, fresh HOME)
surfaced six first-session frictions, fixed here:

1. Workspace-consuming commands run with no target in a directory that
   has no Link wiki now fall back to the default workspace
   (LINK_WORKSPACE or ~/link) with a one-line stderr notice, so
   onboard followed by a pathless remember/recall just works. Creator
   commands (init, demo, try, proof, onboard) never redirect, an
   explicit target always wins, and a cwd wiki takes precedence.
   Tests cover all three rules.

2. When a lnk launcher on PATH runs this same runtime (e.g. the
   Homebrew install), generated commands — including the viewer's
   copy buttons — now say "lnk ..." instead of the interpreter and
   install path. Tested with a synthetic shim.

3. remember with no workspace anywhere now points at onboard and
   explains the pathless fallback instead of dead-ending.

4. The landing page now serves its pitch (headline, what Link does,
   install command, links, the PyPI name link-mcp) to text fetchers,
   LLM crawlers, and noscript readers instead of "Unpacking...";
   verified via raw curl and hydrated browser.

5. The hero install command appends "lnk try" so the blessed first
   step is unmissable.

6. The Memory Dashboard explains what review means: unreviewed
   memories recall as provisional; reviewing earns full trust.
Maintainability pass so the project onboards a second contributor
without a walkthrough:

- ARCHITECTURE.md: components, the frontmatter-is-the-schema data
  model, both review-gated write paths, the recall pipeline, the five
  invariants a change must not break, the guard scripts, and where to
  add commands/fields/tools.

- .mailmap unifies the three git identity spellings in history.

- Dependency ceilings (mcp>=1.0.0,<2; model2vec and fastembed extras
  capped <1) so a breaking upstream major cannot break every fresh
  install overnight.

- Static typing adopted via the ratchet pattern: lenient mypy config
  (mypy.ini) plus scripts/check_type_ratchet.py pinning the current
  387-error count. CI fails when the count rises; the baseline only
  gets lowered. The referee environment is defined exactly (bare
  interpreter, mypy>=1.20,<1.21, wired into the CI lint job) because
  installed dependencies change what mypy resolves.
Two additions from a research pass on where Link's design meets the
current memory literature:

1. Abstention contract. When someone asks about something the memory
   never contained, the correct behavior is to say so — not to let an
   agent dress a weak match up as an answer (the failure mode
   LongMemEval's abstention category scores, and most retrieval evals
   ignore). recall_abstention() turns evidence Link already computes
   (confidence labels, match kinds) into an explicit verdict:
   - MCP recall (slim and full surfaces) now returns an `abstention`
     object; recommended=true means nothing here is strong enough to
     assert from.
   - The MCP instructions teach agents that "my memory doesn't cover
     that" is correct behavior, not failure.
   - CLI recall prints a weak-match warning above hint-grade results.

2. Research context in benchmarks/RESULTS.md: independent academic
   support for Link's two most-questioned choices. arXiv:2601.00821
   finds verbatim chunks beat LLM-extracted artifacts by 15.9-22
   points (LoCoMo / LongMemEval-S) — "retrieval accuracy tracks how
   far the representation departs from the source" — with the winning
   hybrid being verbatim text plus distilled artifacts, i.e. Link's
   raw-sources-plus-reviewed-memories layout. arXiv:2603.15599 finds
   ranking beats graph structure for conversational memory retrieval,
   consistent with our own rejected entity-graph ablation.
LoCoMo full 1,540: Link 84.8 (haiku answerer+judge, top-50) vs mem0 v3
platform 83.2 under the same judge (their gpt-5 answers re-judged with
the harness's own prompt). LongMemEval full 500: 78.0 with the
answerer confound stated; judge-free retrieval decomposition shows
evidence in context for 99.4% of questions (96% of failures
answerer-side).
`lnk verify-mcp claude-code` previously treated the agent name as a
directory and verified a guessed runtime. It now reads the agent's
actual config file, extracts the configured Link server (JSON agents
and Codex TOML), and verifies exactly that python and wiki — with a
clear pointer to `lnk connect <agent> --write` when nothing is
configured. Plain path mode is unchanged.
"hey, before we start — from now on I only push to develop" now
stores as "from now on I only push to the develop branch": leading
interjections are stripped and a short pre-dash lead-in is dropped
when the remainder still classifies on its own; otherwise the full
text wins. Hygiene benchmark holds at 0 junk.
The Homebrew runtime python refuses direct pip installs (PEP 668), so
every printed `pip install "link-mcp[semantic]"` line was dead on a
fresh Mac and brew users had no working path to semantic or rerank.

`lnk semantic --setup` now detects the externally-managed runtime,
provisions the extras into Link's managed venv (~/.link-mcp-venv,
pinned to Link's version), and reruns the setup under that python —
one command from lexical-only to hybrid + rerank. Status guidance and
verify-mcp stop printing pip commands that the interpreter would
refuse and point at the managed-venv path instead.
Three scenes in the truth-animation visual language: a preference said
once in a normal agent session is captured by the session hooks as a
proposal, one review command makes it durable, and a brand-new
terminal the next day answers from it — ending on the memory's file
path. Generated deterministically by scripts/generate_remembers_svg.py.
All three demo SVGs (remembers, truth, aha) now render through a
shared SMIL emitter (scripts/svg_anim.py) instead of CSS keyframes —
SMIL is the animation dialect that runs most reliably inside <img>
embeds. The closing scene of each demo is also the SVG's t=0 state,
so contexts that freeze image animations (reduced motion, strict
embeds) show the payoff — the recalled memory and its file path —
instead of an empty terminal, and the loop point becomes seamless.
link-aha gains a generator (data extracted from the hand-written
original). Homepage: link-remembers leads the How-it-works figures;
README: it replaces the static site screenshot as the hero.
Session hooks run the runtime copy inside each workspace, which
silently stays old after `brew upgrade`. `lnk health`/`status` now
warn (code stale_runtime) with the exact refresh command; doctor,
init, and onboard refresh the copy as a safe repair; and
`connect --hooks --write` refreshes on version drift, not only for
pre-hooks runtimes. Newer workspace copies (dogfooded source
checkouts) are never downgraded.
Both jobs assert the runner cannot import link_mcp first (the 1.6
out-of-box MCP bug was invisible on a polluted dev machine), then walk
the full fresh-user path: onboard --agent claude-code --hooks --write,
venv self-provisioning (local wheel offered via PIP_FIND_LINKS so the
pinned version resolves before it exists on PyPI), the session-start
brief, and a live MCP stdio handshake from the provisioned venv.
The `context` primitive (origin-adjacent text that helps recall find a
memory but is never part of its claim) was only populated by the LoCoMo
benchmark adapter, where the ±1-neighbor window lifted hit@10
0.685→0.737. Now the product writes it too: session-end proposals carry
the neighboring sentences around the claim's origin, accept-capture
threads them into the page's context frontmatter (bounded at 600
chars), and the explicit paths gain `lnk remember --context` plus a
`context` parameter on both MCP remember tools. The paste-ready shell
command stays clean — context rides the structured paths only. Claims
remain context-free everywhere: echo/duplicate/conflict checks, slim
output, and the visible page body.
…ir proposals

Dogfooding LinkBar surfaced a password saved as memory and echoing
from the activity log. Three core fixes:

- remember (CLI + both MCP tools) refuses credential-shaped text —
  token patterns plus a conservative password heuristic — with
  --allow-secret as the explicit override. Memory pages are plain
  files injected into every agent session; a credential there leaks
  by design.
- forget-memory now scrubs the memory's title/name from past log
  entries, re-anchors the tamper-evident hash chain, and declares the
  redaction as its own log entry; integrity verification passes.
- capture-inbox items carry mined proposal previews (what Accept will
  save), secrets redacted — no more blind-accepting "Agent session
  notes".
Research into how the hook grabs session context surfaced three
misalignments, now fixed:

- Standing rules survived: extract_transcript_text gains keep_head, and
  the hook mines a head+tail window (9k) instead of recency-only 6k. A
  rule stated at the start of a long session no longer falls off the
  front. Verified live on an 82-turn session.
- Correct attribution at accept time: the capture records a
  ## Proposal Source block (the user's own turns); accept-capture and
  the inbox preview mine from it, so the assistant's prose beneath the
  notes is never re-proposed as the user's preference. Closes a
  regression the earlier hook-time fix didn't cover.
- The pipeline is reviewable: session-end persists a decision trail
  (## How Link Read This Session), surfaced on the dashboard Captures
  page (proposals + collapsible trail) and in capture-inbox --json
  with a mined_from_user_turns flag.
…ures

Adversarial testing of the now-automatic pipeline: 8 distinct
session-end hooks fired simultaneously produced only 7 captures. Root
cause is a check-then-write TOCTOU in capture_filename — hooks that end
in the same second share the timestamp and default "Agent session
notes" title, collide on the same base name, and the existence-check
counter loop lets two hooks pick the same path so one clobbers the
other. Real trigger: closing several agent terminals at once, or
parallel agents.

capture_filename now claims each name atomically with O_CREAT|O_EXCL
and hands the caller a reserved placeholder to overwrite. Verified:
12/12 concurrent captures now land; regression test drives 32 claims
across 16 threads at one timestamp+title and asserts 32 distinct
files.
Adversarial testing: `--applies-when project:::::garbage[[[` was
accepted and stored verbatim. Recall already normalized both sides, so
it matched a project slugifying to `garbage` — correct, but the stored
condition displayed the ugly raw form, and a value slugifying to empty
(`project:!!!`) became a silently-dead scope that never applies.

parse_applies_when now normalizes project arguments to the slug recall
compares against (so the condition matches what it shows), and rejects
values with no usable slug. path:/task: arguments stay freeform.
Adversarial fuzzing of the slim MCP surface (540 garbage/hostile calls
across every tool and argument) found two real write-path bugs:

- Frontmatter field injection: frontmatter_string escaped quotes and
  backslashes but passed newlines through, so a title containing
  "recall\nremember: chained" wrote an injected `remember:` field the
  parser then honored, and a "---\ntitle: injected" payload replaced
  the title entirely. Values now collapse whitespace runs and drop
  non-printable characters, making injected frontmatter inert text.
  Pages written from those payloads previously failed `lnk validate`
  (5 errors); the same fuzz now leaves a fully valid workspace.

- Uncapped slugs: a 300k-char title crashed remember with a raw
  OSError (File name too long). slugify caps at 80 chars, cut at a
  word boundary.

Also verified under fuzz: supersedes cycles terminate instantly in
recall and as-of; path traversal, corrupt caches/indexes, truncated
pages, and hostile queries all degrade cleanly.
README table and homepage (SEO fallback, LoCoMo card, footnote) now
carry the completed judge program: LoCoMo confirmed by the independent
Hunyuan 3 judge (85.5 vs 83.5), and LongMemEval re-judged in both
directions under the same neutral referee (mem0's gpt-5 answers 91.0,
Link's budget-model answers 80.6) with the answerer-effect framing —
we publish the number we lose alongside the ones we win.
Cold-user walkthrough found the automatic loop's worst first impression:
a one-click Accept (LinkBar) and accept-capture --index 1 landed on
whichever proposal came first in the transcript, which is often a
throat-clearing preamble ("I want to set some conventions…") rather
than the rule ("From now on I only deploy on Fridays…"). Both classify
as preferences at score 90, so confidence alone can't separate them.

- memory_durability_rank scores concrete directives (an action verb, or
  only/always/never/by default) above statements that are merely about
  making rules (want/need to set/establish + conventions/guidelines/…).
  propose_memories_from_text stable-sorts by it, so index 1 is the
  substance. Bare temporal phrases ("from now on", "going forward")
  don't count as concrete — they attach to preambles just as easily.
  Ordering only; every proposal still appears for review.
- README Quick Start collapses three parallel entry points (proof / try
  + serve / onboard x5) into one two-command hero path.
- lnk onboard collapses a fresh workspace's dozen scaffold lines into a
  one-line summary; targeted repairs on an existing workspace still list.
…nostics

First real CI run of the 1.7 work on the release PR surfaced four
failures; all addressed:

- Type ratchet (lint): my 1.7 additions raised mypy 387→398. Narrowed
  the object-typed locals in the code I touched (capture_records
  proposal loop, render_capture_inbox_text, render_redact_capture_text,
  the segment score cast) — behavior-preserving, now 385 ≤ 387.
- windows-smoke: two tests hardcoded POSIX assumptions.
  test_existing_venv_is_used_... created venv/bin/python, but the
  runtime (correctly) looks under Scripts/ on Windows — the test now
  uses default_mcp_venv_python() so it writes where the runtime looks.
  test_shim_on_path_... builds a #!/bin/sh Homebrew shim with a `:`
  PATH; that scenario can't exist on Windows, so it's skipped there.
- windows-cold-walk: PowerShell captures multi-line stdout as a string
  array, so `$out -notmatch "Link memory"` returned the non-matching
  lines (truthy) and tripped even though the brief contained it.
  Join with -join "`n" first.
- macos-clean-install MCP smoke: could not reproduce locally (demo
  wiki, mcp 1.28.1, clean venv, Python 3.12, FTS on/off all pass), so
  the fix is diagnostic + robustness: unwrap the anyio ExceptionGroup
  so CI prints the real leaf error instead of "TaskGroup (1
  sub-exception)", and assert the demo page is present in search
  results rather than ranked exactly #1 (a smoke test shouldn't pin a
  specific sqlite bm25 order).
The diagnostic unwrap from the last commit revealed the real cause on
both remaining failures: "slim recall did not return the expected query
packet" — the recall assertions pinned the demo page as the exact #1
result (`wiki.primary == "agent-memory"`), which varies by the
environment's search ranking.

- Both query_link (full) and recall (slim) now assert the demo page is
  present among the recalled pages, not ranked exactly first — the
  correct smoke check (is it findable), with the actual found/primary/
  pages values in the error for future diagnosis.
- windows-cold-walk pointed the smoke at the freshly-onboarded (empty)
  workspace, which has no demo content, so recall legitimately found
  nothing. It now builds a demo wiki for the smoke while still driving
  the provisioned venv python — which is what that step verifies.

Verified: full and slim pass against the demo on Python 3.12 and 3.14
clean venvs; an empty wiki now fails with a clear one-line diagnostic.
The diagnostic revealed the macos-clean-install failure was
"slim recall found nothing (found=False, pages=[])" — while the full
smoke on the same wiki found the demo page. The full smoke exercises
mutating tools (rebuild_index, migrate_wiki, backup_wiki) after its
read asserts, leaving the shared wiki in a state where the subsequent
slim run's recall returned empty on the runner (not reproducible on
local macOS/Linux, so it's runner-environment-specific).

The runner's own evidence shows recall works on a pristine demo (the
full smoke's query_link — identical core to recall — found the page),
so isolating each surface to its own fresh demo wiki fixes it. Matches
the windows-cold-walk fix (dedicated demo, not the shared/onboarded
workspace).

Follow-up worth investigating post-release: whether rebuild_index or
migrate_wiki can leave a wiki where recall finds nothing under some
filesystem/sqlite conditions — a real user could hit that. Does not
reproduce locally.
Isolation (fresh demo per surface) did NOT fix the macOS slim recall
failure — still found=False, pages=[]. So it is not cross-contamination
from the full smoke's mutations; the slim server returns nothing on a
pristine demo, which does not reproduce on local macOS 26 / Linux /
Python 3.12 across clean venvs.

The slim recall assertion now reports the server's own counts
(status.content_page_count, page_count, wiki.search_count, strategy),
and the CI step prints the demo's page count before smoking. The next
run distinguishes the two hypotheses definitively:
  - content_pages=0  -> the demo wiki is empty on the runner (a demo /
    packaging issue), fix the fixture/CLI
  - content_pages>0 + search_count=0 -> recall/FTS returns nothing
    despite pages present (a runner-specific search/sqlite bug)
The enhanced diagnostic pinned the macos-clean-install failure exactly:
the slim recall saw status.content_pages=0, page_count=2 — an EMPTY
demo wiki. The full smoke's demo is fully populated (it passes), but a
SECOND `link.py demo` call on the macOS runner produced a bare
scaffold with no content pages. Not reproducible on local macOS 26 /
Linux / Python 3.12 (a second demo there yields the full 16 pages), so
it is runner-environment-specific.

Fix without relying on a second demo: one demo, run the read-only slim
smoke FIRST against the pristine wiki, then the full smoke (which
exercises mutating tools: rebuild_index, migrate_wiki, backup_wiki)
last. Verified locally: slim-then-full on one demo both pass.

FLAGGED for real investigation (not a release blocker; unreproducible
locally): why does a repeated `link.py demo` emit an empty wiki on the
macOS GitHub runner? If a real user can hit an empty demo on a second
run, that is a genuine bug — the diagnostic (status.content_pages) is
now in the smoke to catch it if it recurs.
Slim-first did not help — the slim smoke on the pristine demo still saw
content_pages=0, page_count=2. So it is the demo wiki itself that is
empty/uncounted on the macOS runner, not an ordering or recall issue.

page_count=2 is a real puzzle: DEMO_FILES writes 16 wiki files in a
single forward loop with wiki/index.md and wiki/log.md LAST, so seeing
only those two means either the other 14 were never written, or they
exist on disk but the cache/search counts them as zero. The macOS smoke
step now prints the demo output, lists every wiki *.md file, prints
status, and hard-fails with "DEMO IS EMPTY" if fewer than 10 pages are
on disk — so the next run distinguishes not-written from not-indexed
in one shot. Not reproducible locally (16 pages on 3.12 and 3.14).
…orkspace

Root cause of the macos-clean-install failure, finally: the failing
step was "MCP server answers over stdio from the provisioned venv",
which ran the smoke against $HOME/link/wiki — the ONBOARDED workspace,
which onboard intentionally creates empty. The smoke asserts demo
content is recallable, so it correctly reported content_pages=0. This
is the exact twin of the windows-cold-walk bug already fixed at its
sibling step; the macOS copy was missed.

Every "the demo is empty / search returns nothing" hypothesis from the
prior commits was a misread: that smoke was never pointed at a demo,
and there is no demo-generation, search, or packaging bug — the demo
builds 16 pages everywhere. Fix: create a demo wiki and smoke it with
the provisioned venv python (mirrors Windows). Also reverted the
debug instrumentation on the package job's demo-smoke step, which was
passing all along.
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