Skip to content

Release 2.3.0 - #60

Merged
gowtham0992 merged 21 commits into
mainfrom
develop
Aug 15, 2026
Merged

Release 2.3.0 #60
gowtham0992 merged 21 commits into
mainfrom
develop

Conversation

@gowtham0992

Copy link
Copy Markdown
Owner

Fixed (reported by @SparklesKitchen)

Added

  • lnk handoff: switch agents mid-task and lose nothing. Standalone packets pushed into the next session on any agent, via session hooks and the MCP first response.
  • Proactive guard: a saved constraint surfaces the moment a prompt would violate it, on Claude Code's prompt hook and every MCP recall path, with a 45-minute cooldown.
  • Switch-intent nudge: offers a handoff when a prompt announces a stop or a tool switch.
  • lnk import: bring memory home from Claude Code, Cursor, Codex, or a ChatGPT paste, all review-gated.
  • Bulk review: accept-capture --all and delete-capture --all.
  • pip installs now ship the full CLI (lnk), which finally makes Windows a real install path.
  • Meaning-based recall set up by default: hit@1 0.589 to 0.703.

Changed

  • The MCP session brief is bounded: first-recall cost drops from 11,269 tokens to 2,313.

2.2.1's RESULTS.md published an honest asterisk: the first MCP response
of a session carried the full memory brief (~16.5k chars), making it
the largest packet Link sends - 11,269 tokens on the benchmark corpus
against a 1,954-token steady state. "Bounding it is next" was the
public promise. This is that.

_compact_session_brief digests the full brief down to what an agent
needs to behave well at session start: typed memory claims trimmed to
200 chars, active/review counts, and one guidance note pointing at
recall for everything else. A hard 4,000-character budget is enforced
in code - trailing memories are dropped until the digest fits, with a
truncated flag - so the bound holds by construction, not by hope.

Measured on the same corpus: first recall 2,313 tokens, steady state
1,954 - brief overhead down from ~9,300 tokens to ~360. On the demo
wiki the digest is 956 chars carrying every memory. Retrieval recording
still uses the full memory-name list, so lnk wins counts are unchanged.

The budget is pinned by a test (digest must serialize under
SESSION_BRIEF_MAX_CHARS), eval_token_economics.py measures the real MCP
surface on every run, and README + RESULTS.md now state the bounded
number instead of the asterisk.

1010 tests, ruff clean, ratchet 385, hygiene green.
Three small findings from the 2.2.1 post-release audit, none
user-facing, all the kind of thing a meticulous adopter notices:

1. The MCP handshake reported serverInfo.version as the SDK version
   (e.g. 1.29.0) because FastMCP has no version parameter. The
   underlying lowlevel Server does; set it defensively via getattr so
   the FakeFastMCP test stub (and any SDK that renames the private
   attribute) degrades to the old behavior instead of crashing - the
   first attempt broke 69 contract tests by assuming the attribute.
   Real handshake now reports 2.2.1.

2. LinkBar Info.plist gains CFBundleVersion alongside
   CFBundleShortVersionString - macOS update and launch-services paths
   key off the build number, and it was absent entirely.

3. packaging/linkbar.rb was a 1.0.0-era snapshot and
   packaging/RELEASE-linkbar.md described the 2.0.0 release. The cask
   template is now generated from the live tap with placeholders (so it
   cannot rot into a stale pinned version), and RELEASE-linkbar.md is
   reduced to the LinkBar-specific mechanics with a pointer to the
   release recipe memory that actually gets exercised.

1010 tests, ruff clean, ratchet 385, hygiene green.
The user noticed the banner said "Link 2.2.1 is out" followed by
"secret gate, capture provenance, ranked proposals" - feature words
from a 1.x release. prepare_release.py stamps the version into the
banner but nothing owned the words, so they rotted invisibly across
four releases.

Two fixes:
- The live banner now describes 2.2 ("sync with no server, temporal
  recall, proof your agents use it").
- prepare_release.py takes --banner "feature words" and substitutes
  them after "is out" (HOMEPAGE_BANNER_WORDS_RE over the JS-escaped
  template); omitting --banner prints a loud stderr warning naming
  exactly what will rot. Verified the substitution against the real
  homepage template and the release recipe memory now says to always
  pass it.

1010 tests, ruff clean, ratchet 385, hygiene green.
The 2.3 flagship, built from the deep-research round. The most
universal pain of multi-agent work is the switch: a rate limit hits
mid-task (Pro exhausts after ~12 heavy prompts; the community
documents 20-40% of every session's tokens going to context
re-establishment), and the hand-rolled fix - HANDOFF.md files - is
widespread enough to have a dictionary entry. Anthropic shipped
cross-session messaging, but intra-Claude only. Cross-vendor is the
whitespace only an agent-agnostic layer can fill.

link_core/handoff.py: standalone packets under raw/handoffs/ (task,
state, explicit next steps; "no return path" - the receiving session
cannot ask the old one what it meant). Chained breadcrumbs to the
previous handoff, automatic expiry (48h), machine-local (raw/ never
syncs), secret-redacted at write - including the title and filename
slug, a leak the new tests caught (first line of a note can carry the
secret).

Delivery is push, never pull, per the research's core design rule:
- session-start hooks open with the HANDOFF WAITING block, above
  everything else
- the MCP first-response digest carries handoff_waiting first - and
  delivers even when the store has zero memories (the demo caught the
  digest bailing on empty stores and swallowing the handoff)
CLI: lnk handoff "note" --task --next (repeatable) --from; lnk handoffs
[--clear FILE]. Agent instructions (shared template + embedded
constant + LINK.md) teach: resume first, clear after pickup, offer a
handoff when the user stops or switches.

Bulk review (the review-debt fix riding along): accept-capture --all
accepts every proposal with duplicates/conflicts skipped and reported,
clears the capture only when nothing was skipped; delete-capture --all
--confirm clears the pending inbox with dismissals recorded and the
preview NAMES the workspace it will hit. Fixed en route: with --all the
positional is always the target directory - the previous parse could
route a destructive bulk delete to the default workspace fallback
(caught in testing against a scratch dir; regression test pins it).

Verified live end to end: a claude-code handoff written in one session
opened the next session on both surfaces (hook text starts with
HANDOFF WAITING; MCP first response carries the same block); bulk
accept took a 4-proposal import to 4 memories and cleared the capture.

1019 tests (+9), ruff clean, ratchet 385, contracts and hygiene green.
The deepest weakness in the research round was B1: mid-session, memory
only arrives if the agent thinks to ask, and mostly it does not. The
session-start brief is a snapshot; forty minutes in, the user types
"let's deploy payments on friday" and the Tuesdays-only memory sits
unread. This is the per-prompt check that catches exactly that moment.

link_core/guard.py: fires only when BOTH hold - the memory is
constraint-shaped (never/always/only/do not/must not: the class whose
violation hurts) AND the prompt overlaps it strongly (the same lexical
ranker recall uses, confidence gate + score floor so a single stray
shared token can never interrupt). Silence is the normal, correct
output; precision-first because a naggy guard is a dead guard.

Wiring: Claude Code's UserPromptSubmit hook runs `lnk hook
prompt-check` (10s timeout, measured 82ms; no model load). Other
agents' configs are untouched - they have no prompt event. Firings are
recorded in the usage ledger as a new "guard" kind, so lnk wins counts
the guard working.

Live matrix verified: "lets deploy the payments service on friday"
fires the Tuesdays constraint by name; "force push this to the shared
branch" fires the force-push rule; haiku requests, short prompts, and
one-token overlaps stay silent; non-constraint preferences never
interrupt even on strong overlap.

7 new tests (precision matrix + hook-plan wiring both ways). 1026
tests, ruff clean, ratchet 385, contracts and hygiene green.
The handoff needs a face. A waiting handoff now opens the Inbox tab as
a rust-tinted banner above everything else - title, age, source agent,
body preview with next steps, and a Clear button for after pickup.
Fed by lnk handoffs --json in the fast refresh batch, updated the
moment a handoff lands via a directory watcher on raw/handoffs, and
older pending handoffs are counted below the newest. Snapshot-verified
against a live workspace; zero warnings.
Two remaining items from the deep-research round.

1. The handoff suggests itself at the right moment. Session-end hook
   output is invisible (the session is over), so the trigger lives in
   the per-prompt hook that already sees every prompt: when one
   announces a stop or a tool switch ("switching to codex", "hit my
   rate limit", "continue this tomorrow", "stopping here"), the hook
   nudges the agent to offer lnk handoff before ending. Same
   precision-first discipline as the guard - the agent-names gate keeps
   "switching to a recursive approach" and "continue with the refactor"
   silent; a 17-case matrix is pinned in tests. Constraint reminders
   outrank the nudge when both would fire.

2. lnk setup provisions the fast semantic tier by default (B3): the
   lexical-vs-fast gap (hit@1 0.589 vs 0.703) is the biggest quality
   difference a new install feels, and the cost is one ~30 MB local
   model fetched during the explicit setup command with a clear
   message. Recall itself still never touches the network. Fast tier
   ONLY on the default path - quality and rerank (~200 MB more) stay
   explicit opt-ins - and only where Link owns the environment (the
   managed venv / externally-managed pythons); user-managed pythons
   keep the hint. --no-semantic opts out. First wiring naively called
   the full semantic setup, which would have pulled all three tiers on
   a cold install; caught and scoped before commit.

1029 tests (+3), ruff clean, ratchet 385, contracts and hygiene green.
Self-review caught the flaw in my own precision story: the guard had
no memory of its own firings, so ten prompts about deployment meant
ten identical Tuesday reminders - the exact nagging the design vowed
against. The usage ledger already records every guard firing with the
memory name and timestamp, so recently_guarded() walks it backward
(events are append-ordered; past the 45-minute floor it stops) and the
prompt-check hook skips a constraint it surfaced within the window. A
different constraint can still fire immediately. Live-verified: first
deploy prompt fires, the follow-up seconds later is silent.
Three fixes from the room-for-betterment review:

1. Guard coverage was 1 of 9 (only Claude Code has a prompt hook). But
   hookless agents recall, and a recall query paraphrases the request -
   so the guard now rides all three MCP recall paths (slim query mode,
   slim memory mode, full recall_memory), adding a "guard" field to the
   packet on a strong constraint overlap. The cooldown is shared with
   the prompt hook through the usage ledger, so the two surfaces never
   nag in stereo. Live-verified: a hookless recall for "deploy payments
   friday" carries the Tuesdays constraint; the follow-up is quiet.

2. Track 7, the honest slice: eval_token_economics now measures what a
   delivered handoff costs the receiving session (109 tokens on the
   benchmark packet). The savings side - community-reported 20-400f
   session tokens on re-establishment - needs real-agent studies, and
   the benchmark says so explicitly rather than faking it with a
   synthetic proxy.

3. README benchmark row updated for the new default: pip stays lexical
   0.589, brew + lnk setup now lands the fast tier at 0.703, quality
   and rerank remain opt-in - so the row stays honest in both
   directions after the fast-tier-default change.

1031 tests (+1), ruff clean, ratchet 385, hygiene green.
Discovered during the fix-all triage: the wheel only exposed the MCP
server ([project.scripts] link-mcp) - link.py was never packaged, so
every pip user (which means every Windows user, who has no brew) got a
memory server with no lnk. The "Windows support" gap was never about
winget manifests; it was this.

Fix without a refactor: hatch force-include ships the repo-root
link.py as the importable link_cli module behind a new `lnk` console
script, and serve.py / LINK.md / .linkignore ride along as data so
workspaces still receive their runtime files - copy_runtime_files now
falls back to link_cli.py as the source for the workspace link.py
copy.

Verified against a cold venv with the built wheel: lnk --version, init,
remember, recall, handoff, handoffs all work, and the workspace
receives LINK.md, link.py, and serve.py. Two tests pin the packaging
(console script + force-includes declared; runtime-copy fallback).

1033 tests (+2), ruff clean, ratchet 385, hygiene green.
)

First external bug reports, both with root cause and verified fixes.

#58 is the serious one: LinkBar crashed at launch on every machine
except the build host, since 1.0.0. SPM generates a Bundle.module
accessor that fatalErrors when it cannot find its resource bundle, and
it only checks the app root and the absolute build path compiled into
the binary. bundle.sh put the bundle in Contents/Resources - neither
location - so a packaged app always died. It looked fine here only
because the maintainer machine has that build path. LSUIElement means
no window, so the failure read as "the app never opened".

Structurally invisible to our own testing: every snapshot ran the dev
binary, and every manual check ran on the machine that satisfies
candidate 2. Reproduced by hiding the build directory and running the
installed binary, then fixed by never touching Bundle.module - a small
resource lookup searches Bundle.main, the SPM bundle under Resources,
the app root, and the executable directory, returning nil rather than
crashing. The resource copy lost its 2>/dev/null || true so a failed
copy fails the build. Verified: freshly bundled app launches and
renders with every build path hidden.

#59: build_wiki_cache assigned raw forward links per stem, so a nested
sources/vendor-docs/INDEX.md erased root index.md edges; rebuild wrote
an incomplete _backlinks.json that validate then called stale. Now
merged, matching what build_backlinks and validation already do.
Proven by A/B on the reporter fixture: with the fix agent-memory has
its index backlink, without it the edge is gone.

Three regression tests: stem-merge in the cache, no Swift source may
touch Bundle.module, and the resource copy may not swallow failures.
1036 tests, ruff clean, ratchet 385, hygiene green.
…idden

The two LinkBar tests from the fix commit are static text checks (no
Bundle.module in source, no || true on the resource copy). They pin the
cause, but nothing actually launched the packaged app - and the whole
lesson of #58 is that only running the artifact with the build
environment unavailable can catch that class of bug. My reply on the
issue said the packaged path was exercised; it was exercised by hand,
not by anything repeatable. This makes it true.

scripts/smoke_linkbar_packaged.py bundles the app, moves every
*_LinkBar.bundle under .build out of the way (simulating a machine that
never built anything), launches the packaged binary in snapshot mode,
requires a render, and restores the bundles in a finally block. It
skips cleanly off macOS or without a swift toolchain.

Proven in both directions: against the pre-fix files it fails with the
exact reported fatal error; against the fix it renders. Wired into the
macOS CI job and py_compile list, plus a unit test asserting the
harness exists and is referenced by the workflow - so it cannot quietly
stop running.

1037 tests, ruff clean, ratchet 385, hygiene green.
prepare_release.py stamps the six release files (with --banner words this
time, so the homepage banner describes 2.3 instead of an older release).
LinkBar bumps to 1.3.0 in DesignSystem.swift and bundle.sh. Release zip
built: LinkBar-1.3.0.zip sha256
2a390edd628bdc796a856841a1b61e102d00d801d029cb66c8667e6c45d45b66.

All gates green on the stamped tree: packaged-launch smoke (the #58
harness), 1037 tests, ruff, no-identity sync sim, ratchet 385, hygiene,
contracts.
Brings the Bar CI investigation collector onto develop, along with the
release-hygiene exemption for it and the token-benchmark FTS cache fix.

Resolves the py_compile list by keeping both entries: bar_investigate.py
from main and smoke_linkbar_packaged.py from develop.
@github-actions

Copy link
Copy Markdown

Bar CI investigation

Run 31907106513 · attempt 1

windows-smoke

Failed step: Run broad Windows tests
Diagnosis: PermissionError due to file being used by another process
Confidence: 90%
Remaining uncertainty: The error message clearly indicates a PermissionError, but the root cause of the file being used by another process is not explicitly stated in the evidence.
Evidence: E-LINK-001, E-LINK-002, E-LINK-003
Open protected investigation

package

Failed step: Build link-mcp
Diagnosis: The CI failure is due to a FileNotFoundError caused by a missing file specified in the URL removed configuration.
Confidence: 90%
Remaining uncertainty: The uncertainty in this diagnosis is due to the fact that the exact reason for the missing '.linkignore' file is not clear. It could be due to a mistake in the URL removed configuration or a proble
Evidence: E-LINK-001, E-LINK-003
Open protected investigation

A recall builds the persistent FTS index under .link-cache, which holds
an open SQLite handle. POSIX unlinks open files happily, so a test that
left the cache open still cleaned up on macOS and Linux; Windows failed
at TemporaryDirectory teardown with WinError 32 on page-fts-v1.sqlite.
That is a platform-only failure invisible to everyone developing here.

tests/mcp_harness.py drives the server through a context manager that
always closes the cache on exit, so the next MCP test cannot reintroduce
this by forgetting. The guard, handoff, and usage tests now use it.
python -m build deliberately builds the sdist first and then builds the
wheel from the extracted sdist, where no parent directory exists. The
force-include entries referenced ../link.py and friends, which resolve
in the source tree but not inside that extracted sdist, so CI failed
with "Forced include not found: .linkignore".

A hatchling build hook copies the four repo-root runtime files into the
package before file collection, so both builds see local paths: from the
source tree it copies from ../, and from an sdist the copies are already
there. The copies are gitignored build artifacts; the repo root stays
the single source of truth, and the hook fails the build if a file is
missing from both places rather than shipping a wheel without the CLI.
@gowtham0992
gowtham0992 merged commit d745520 into main Aug 15, 2026
10 checks passed
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