Develop draft pr - #47
Closed
gowtham0992 wants to merge 62 commits into
Closed
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.