diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c53222..c0a62012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ Entries that change an on-disk format or a response shape say so. ## [Unreleased] ### Added +- `POST /contexts/{name}/promote` and the `promote` MCP tool (#466 + S2, ADR 0018): graph-path memory promotion — the named scratch + sources move into an established destination context as the + export/import round trip in one call, no LLM anywhere in the path. + Each source moves whole (passage, `date`, tags, only its own share + of every edge's weight; aliases exactly when their canonical is + live in the promoted slice, the rest counted in `aliases_dropped`), + source ids survive so promoted citations still name the session, + and applying is per-source retract-then-apply — re-promotion is + idempotent. The destination is never created and its own schema + judges the incoming batches; a missing source id refuses the whole + request path-addressed. After a real apply the destination's + consolidation audit (all three checks, default ceilings) rides back + under `audit` — candidates to judge, never applied — and + `?dry_run=true` previews the same `batches` shape with nothing + written. Write role, `retract_source`'s classification; the + destination named in the body is scope-checked like `/import`'s + body contexts. + - `taguru extract --source-id ID`, `--date WHEN`, `--tag TAG` (#466 S1, ADR 0017): bake the promotion runbook's source conventions into the written batch — the `session:{agent}:{id}` header source (with @@ -69,6 +88,16 @@ Entries that change an on-disk format or a response shape say so. not gated, mirroring `sdk/python-langchain`. ### Fixed +- `POST /import?dry_run=true` now seeds each batch's checks with what + the batches before it would intern and create, so two spurious + mid-stream refusals the real import never raises are gone: an + `UnknownCanonical` alias rejection when a stream's aliases trail + their canonicals (every export — aliases ride the last batch), and + a `no_context` refusal on every post-first batch of a restore into + a fresh context name (the create block rides only the first batch). + Cross-batch alias conflicts remain un-predicted — that direction + only lets a preview pass what the real run would refuse, like the + capacity caps the preview contract already documents as advisory. - TypeScript SDK: ported the Python SDK hardening that had not reached the TypeScript twin — `contexts.delete`/`groups.delete` no longer auto-retry after an ambiguous transport failure (the retry turned an diff --git a/adr/0018-graph-path-promotion.md b/adr/0018-graph-path-promotion.md new file mode 100644 index 00000000..831085a2 --- /dev/null +++ b/adr/0018-graph-path-promotion.md @@ -0,0 +1,126 @@ +# 0018. Graph-path promotion as a server verb (MCP `promote`) + +- **Status**: Accepted +- **Date**: 2026-08-09 +- **Issue**: #466 (S2) +- **Related**: #465 (the runbook this bundles), ADR 0012 (the audit it + runs and the explicit-forgetting posture it keeps), ADR 0017 (S1, + the extract flags that feed this), ADR 0009 §13 (the credential + boundary that decides CLI vs server), ADR 0005 (the batch contract + the transfer rides) +- **Supersedes**: — / **Superseded by**: — + +Once Accepted, this document's Decision is immutable: a changed decision gets a +new `adr/000N-*.md` that names this one in *Supersedes*, never an edit here. + +## 1. Scope + +One server verb — `POST /contexts/{name}/promote`, advertised as the +MCP tool `promote` — that moves named scratch sources into a permanent +context over the **graph path**: the scratch's already-structured +associations, passages, dates, and tags, promoted without +re-extraction and therefore without an LLM or any model credential. +Out of scope: the text path (session notes → `taguru extract` → import +— needs model credentials the server never holds, so it stays a CLI +composition; a preset verb is #466 S3 if ever needed), applying audit +judgments, and retiring the promoted scratch. + +## 2. Context + +The #466 rehearsal measured one promotion at 8–10 manual operations; +S1 (ADR 0017) removed the hand-editing from the extract half. What +remains mechanical is the transfer itself: export the scratch, keep +only the keeper sources, re-head every batch at the permanent context, +import, audit the landing zone. Every step is an existing server +operation — which is exactly why the rehearsal's re-evaluation +concluded the graph path can be a server verb while the text path +cannot: no step needs a model credential, so bundling them behind +`/mcp` respects ADR 0009 §13 as-is. The judgment points the rehearsal +proved irreducible — WHICH sources to promote, and what to do with +audit candidates — stay with the calling agent on both paths. + +## 3. Decision + +**`promote` IS export → filter → re-head → import → audit, in one +request, built from the same machinery the manual procedure uses.** +Body: `{into, sources[], audit?}`; `?dry_run=true` previews. + +1. **The transfer is the export/import round trip, not a third write + path.** The scratch's [`ExportSnapshot`] is filtered to the named + sources — each association keeps only their attributions, its + count/weight recomputed from what is kept, edges left with nothing + drop — then rendered as an ordinary import stream headed at `into` + and applied batch by batch with `POST /import`'s own + retract-then-apply. Re-promoting the same sources is therefore + idempotent, aliases are carried exactly when their canonical is + live in the promoted slice (the render's standing rule — the count + of dropped ones rides the response), the unsourced residual cannot + travel (filtering makes attributed count equal total by + construction), and the scratch's schema never installs into the + destination (the snapshot's schema is cleared; the destination's + own schema judges the incoming batches instead, refusing in + `strict` exactly as an import would). +2. **Provenance travels whole.** Source ids, `stored_at`, `date`, and + tags ride the stream verbatim, so a promoted fact's citation still + names the session that produced it and every windowed read keeps + working — the runbook's provenance promise, now enforced by + construction rather than by operator care. +3. **Promote never creates and never retires.** The destination must + already exist (checked up front, and the stream's create block is + stripped so a context deleted mid-request refuses instead of + resurrecting under the scratch's meta — promotion lands in an + established context, never silently mints one). The promoted + scratch stays until the agent explicitly retracts it — + `retract_source`/context deletion remain the runbook's step 5; + forgetting stays an explicit operation (ADR 0012's posture). +4. **Missing sources refuse whole, before anything applies.** A + mistyped session id under retract-then-apply would otherwise + no-op silently; instead every requested id must exist in the + scratch (as a passage or a live attribution) or the request refuses + naming the absentees, `nothing_written`. +5. **The audit is bundled, its judgments are not.** After a real + apply, the destination gets the same merge/contradiction/staleness + computation `audit_consolidation` runs (all three checks, default + ceilings), riding back as `audit` — candidates with fingerprints, + never applications; `audit: false` opts out for a large destination + and `dry_run` skips it (nothing landed to audit). Tuned re-runs + stay one `audit_consolidation` call away, fingerprint reuse intact. +6. **Write role, both contexts checked.** Promote is an ingest-loop + verb — `retract_source`'s classification, not `/import`'s Admin + (it cannot create contexts and carries no group or schema records). + The route check covers the scratch; the handler checks the + key's grant on `into` before anything applies, `/import`'s + body-context discipline. Through `taguru router` the request + proxies whole to the shard owning the scratch, so a destination + living on another shard refuses there (`no_context`) — promotion + through the router requires the pair on one shard, a documented + divergence in route.rs's own list. + +## 4. Consequences + +- The runbook's steps 3–4 become one call on the graph path, and step + 2 disappears from it entirely (no extract when the scratch's + structure is already right); with S1, a session already written in + structured form promotes with: review → `promote` → judge audit + candidates → `retract_source`. The irreducible judgment points are + now the ONLY manual steps. +- No new credential surface and no new write semantics: everything the + verb does was already expressible with existing operations, so the + security review surface is composition, not new capability. +- The response reuses `/import`'s per-batch outcome shape and the + audit's section shapes — clients that parse either parse this. +- Building the dry run exposed two standing `/import?dry_run` defects + the same change fixes at the root: a preview held no cross-batch + state, so a restore whose aliases trail their canonicals (every + export — aliases ride the last batch) and every post-first batch of + a fresh-name restore (the create block rides only the first) + refused spuriously where the real import applies cleanly. Previews + now seed each batch's checks with what the batches before it would + intern and create. Cross-batch alias CONFLICTS remain un-predicted — + that gap only lets a preview pass what a real run would refuse, the + advisory direction the capacity caps already occupy. +- Not in this split, deliberately: promoting BETWEEN servers (export's + file form already covers migration), a `since`/`until` or tag filter + choosing sources server-side (the agent already holds + `list_sources`), and the text-path preset (#466 S3, if S1 plus this + proves insufficient). diff --git a/docs/promotion.html b/docs/promotion.html index 52c4b490..ed5a2a14 100644 --- a/docs/promotion.html +++ b/docs/promotion.html @@ -113,6 +113,22 @@

The promotion procedure

  • Review the scratch context: recall/query, or taguru communities --context scratch-… for a themed overview once the scratch has grown.
  • +
  • Graph path — one call when the structure is already right (ADR 0018): + when the keepers are the scratch's own structured associations (written by the + session itself, nothing left to extract), the promote MCP tool + (POST /contexts/{scratch}/promote, body + {into, sources, audit?}, ?dry_run=true to preview) + bundles steps 4–5: each named source moves whole — passage, date, + tags, and only its own share of every edge's weight — with no LLM + anywhere in the path. Source ids survive, so promoted citations still name the + session; per-source retract-then-apply makes re-promotion idempotent; the + destination must already exist and its own schema judges the incoming batches; + the landing-zone audit rides back in the response (audit: false + opts out), and a dry run previews the transfer batches only, with nothing + written and no audit. What stays yours is exactly the judgment: + which sources (step 1's review), the audit's candidates (step 5's rubric), + and retirement (step 6). Steps 3–4 below are the text path, needed only + when the keepers exist as prose that still wants extraction.
  • Extract the keepers with taguru extract over the session passages (or hand-write the batch), targeting the permanent context — keeping the session:{agent}:{id} source ids and the dates: @@ -143,8 +159,9 @@

    The promotion procedure

    What not to do