Skip to content

feat(agents): the desk remembers — agents check precedent before acting - #1

Merged
ricauts merged 1 commit into
mainfrom
feat/desk-memory-tools
Aug 14, 2026
Merged

feat(agents): the desk remembers — agents check precedent before acting#1
ricauts merged 1 commit into
mainfrom
feat/desk-memory-tools

Conversation

@ricauts

@ricauts ricauts commented Aug 14, 2026

Copy link
Copy Markdown
Owner

What was ported

A resolver had no memory: every ticket started from zero even when the same fault had been solved last month. Three read-only tools give it one, in src/lib/ai/tools/history.ts, with the pure ranking/redaction core in src/lib/ai/ticket-history.ts:

  • search_tickets — ranks past tickets on relevance across title, description and the resolution someone actually recorded, preferring tickets that reached an outcome. Filterable by category or to resolved-only.
  • read_ticket — one past ticket in full: the request, the replies sent, which tools the agent used, the resolution note.
  • requester_history — what else this person has filed, so the third replacement dock in two months reads as a hardware fault, not a new request.

All three are risk LOW, no approval — they only read.

Where the idea came from

Paperclip's MCP server and its packages/skills-catalog progressive-disclosure pattern: a cheap catalogue you search first, with the expensive full read behind a second call. Claude Code's search-then-read tool pairing is the other half. No upstream code was copied — the ranking (stemming, per-field weighting, settled-ticket bonus) and the redaction rule were written against Servo's own schema, so THIRD-PARTY.md needs no entry.

Why this, and why now

It is the ROADMAP's "knowledge for agents" goal at a fraction of the cost: no embedding model, no vector store, no new dependency, and it works offline on the SQLite that already ships with the app.

Design decisions

  • Ranking in memory, not SQL. SQLite has no relevance scoring and Servo ships without an FTS extension so self-hosting stays a one-liner. The tools fetch a bounded 60-row candidate window and score it in TypeScript — which also makes the ranking unit-testable.
  • Requester redaction. mayRevealRequester() reveals a name or email only when the past ticket belongs to the same requester as the one being worked; otherwise the hit reads another requester (withheld). An MCP caller has no ticket in context, so it sees every identity withheld. Precedent is useful; other people's identities are not, and an agent must not quote one person's details back to another.
  • The MCP server lost its naive search_tickets. src/lib/mcp.ts carried its own unranked title/description LIKE search. Deleted, so the registry tool is served instead — external clients now get the same ranked, redaction-aware results the agents get.
  • Upgrades stay non-destructive. ensureToolPolicies() backfills the three policy rows, so the default resolver gains the tools without a reseed. Specialists an admin has edited are never rewritten, so their allowlists are extended from Agents → Tools — documented in the user guide. The bundled agents/*.md carry the tools for fresh installs.
  • The mock provider exercises it. MockProvider reads the tool list it is handed and opens its script with search_tickets when the tool is granted, so the offline demo shows precedent-checking without an API key.
  • No new configuration. No env vars; the three tools appear in Settings → Tools like every other tool and can be disabled there.

How it was validated

Nothing here is claimed on inspection.

  • npm ci · npm run typecheck clean · npm run build succeeds.
  • npm test: 93 passing — 41 pre-existing (all still green) + 52 new across tests/ticket-history.test.ts (ranking, stemming, resolution extraction, redaction, formatting) and tests/history-tools.test.ts (the tool contract against a stubbed Prisma client: query shape, limits, redaction, and "return the error as text, never throw").
  • npm run setup on a fresh database: 21 tool policies, 4 profiles seeded with the new tools. Re-run on a populated database: 21 policies backfilled, 0 profiles overwritten — the upgrade path.
  • End to end through the deterministic mock provider against a real SQLite database (the cloud has no API keys). Ravi's VPN ticket was resolved last month; Dana files the same symptom. The resolver opened with search_tickets, found #1001, read back "Reissued the VPN certificate and pinned the client to the EU gateway" — and neither Ravi's name nor his email appeared anywhere in the result.
  • MCP surface listed to confirm search_tickets, read_ticket and requester_history are served.

One real bug surfaced during testing and was fixed: "resetting" stemmed to resett, matching neither reset nor anything useful. The stemmer now undoes the consonant English doubles before -ing/-ed.

What was rejected

  • Paperclip packages/adapters/* (claude-local, codex-local, cursor-cloud, gemini-local, hermes…) — they adapt coding-agent CLIs and assume Paperclip's mutable server+UI dual registry and its agent-hire model. Servo's BYOK layer already covers Anthropic- and OpenAI-compatible endpoints from Settings, which is the part that pays.
  • Paperclip packages/plugins/* — worker isolation, a manifest format and a plugin SDK are a large surface already served in Servo by custom HTTP tools plus the MCP server. Infrastructure, not breadth.
  • Paperclip packages/db — its own schema and migration story; Servo is Prisma + SQLite with the string unions in src/lib/types.ts as the source of truth.

docs/PORTING-LEDGER.md is created in this PR and records all of the above, plus the candidates for the next run (agent skills as versionable files; fetch_url behind the egress allowlist; knowledge-gap mining on top of desk memory).

🤖 Generated with Claude Code

https://claude.ai/code/session_016L84GW5HDSnb5DH93F8era


Generated by Claude Code

A resolver had no memory: every ticket started from zero even when the same
fault had been solved last month. Three read-only tools give it one.

- search_tickets ranks past tickets on relevance — title, description and the
  resolution someone actually recorded — preferring tickets that reached an
  outcome, filterable by category or to resolved-only.
- read_ticket opens one in full: the request, the replies sent, the tools the
  agent used, the resolution note.
- requester_history lists what else this person has filed, so the third
  replacement dock in two months reads as a hardware fault, not a new request.

Precedent without the privacy leak: another requester's name and email are
withheld unless the past ticket belongs to the same person, so nothing can be
quoted back to the wrong requester. An MCP caller, having no ticket in
context, sees every identity withheld.

Ranking is scored in memory over a bounded candidate window rather than in
SQL: SQLite has no relevance scoring and Servo ships without an FTS extension
so self-hosting stays a one-liner — and the scoring becomes unit-testable.

The MCP server's own unranked search_tickets is deleted in favour of the
registry tool, so external clients get the same ranked, redaction-aware
results the agents get. The mock provider reads the tool list it is handed and
opens its script with search_tickets when granted, so the offline demo shows
precedent-checking without an API key.

All three are LOW risk and need no approval; policy rows backfill on upgrade,
so the default resolver gains them without a reseed. Specialists an admin has
edited are never rewritten — they opt in from Agents -> Tools, as documented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016L84GW5HDSnb5DH93F8era
@ricauts
ricauts merged commit d3fd423 into main Aug 14, 2026
1 check 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.

2 participants