Skip to main content

ADR-037 β€” Runtime is read-only toward Specialist assets; improvement loop proposes, humans approve

Status: Accepted (2026-07-09; P4.2 shipped β€” proposal flow live, legacy direct-ingest removed) Date: 2026-07-07 Closes: OQ-205 (agent skill write-back at M2) Refs: PRD prd-specialist-value-output Q1/T0.4, ADR-033 L6, ADR-008 (KB architecture) Related issue: #227

Thesis: the product's core claim is human-vetted quality. Any path where AI output rewrites the assets that govern future AI output β€” Soul, Skills, or KB β€” without a human in the loop is a self-amplifying feedback channel that undercuts that claim. The runtime is therefore read-only toward Specialist assets; the improvement loop may only propose, and a human (AM/Expert) approval is what applies.


Context​

OQ-205 asked whether a Specialist agent should ever write persona content (SOUL.md, skills) back to storage after Expert correction, with Option A ("read-only forever") as the proposed default. While that question sat open, one write-back path already shipped and contradicts it:

api/src/haystack/jobs/correction-refinement.processor.ts takes every unprocessed Expert correction, has an LLM categorise it (applies_to, key_learning, write_to_kb), and β€” when the LLM says write_to_kb: true β€” directly ingests a generated markdown document into the org's KB (HaystackIngestionService.upload), or appends to an existing correction doc. No human sees the generated KB entry before it starts influencing retrieval for live drafts.

Why this violates the default:

  • No quality gate at the exact point quality matters. The Expert reviewed the reply; nobody reviews the generalised lesson the LLM extracted from it. A mis-generalised lesson ("always promise refunds within 24h") silently steers every future draft for that org.
  • Prompt-injection surface. The categorising LLM reads client-authored message content (#3190 hardened the prompt, but the write decision is still model-made). A poisoned inbound message steering write_to_kb is a KB-poisoning primitive.
  • Unauditable provenance. KB docs created this way look like admin_upload source; operators cannot distinguish human-curated knowledge from AI-self-written knowledge.

Decision​

  1. OQ-205 Option A β€” the runtime never writes Soul/Skill/KB assets. Specialist assets (SOUL.md, skills, KB documents, prompt templates) are operator-managed. No agent or in-process AI pipeline mutates them as a side effect of handling traffic.
  2. The improvement loop outputs proposals only. Expert corrections, edit-ratio signals, and any future learning pipeline produce proposals; a human (AM or Expert) approval step is what applies a proposal to a Specialist asset. Approval carries an audit trail.
  3. Transition (expand-contract, complete): the legacy correction→KB auto-ingest in correction-refinement.processor.ts was gated behind the feature flag correction_auto_ingest_enabled (default OFF) during the expand phase. That flag AND the entire correction-refinement.processor.ts direct-ingest pipeline are now removed — P4.2a (LoopProposalProducerService) + P4.2b (approval UI) shipped, and P4.2c executed this contract step. P4.2a's producer consumes the unprocessed backlog (corrections.processed_at IS NULL) as human-approved kb_golden_answer DRAFT proposals; its backlog query is deliberately flag-independent, so nothing is lost.

Consequences​

  • Corrections accumulate unprocessed until P4.2. Accepted: the rows are durable (corrections.processed_at IS NULL is the queue), the few-shot path (correction_fewshot_enabled, ADR-033 P1) reads corrections directly and is unaffected, and the alternative β€” continuing to self-apply β€” is worse than a delay.
  • Emergency path exists but is loud. An org that demonstrably needs auto-ingest before P4.2 can have the flag flipped ON per-org via the Ops feature-flag surface; every change requires a reason and lands in feature_flag_audit, so exceptions are explicit and reviewable.
  • KB stops growing from corrections by default. AMs/Experts who relied on the automatic refinement docs must curate KB entries manually until P4.2.
  • Gate short-circuits before the categorisation LLM call β€” flag OFF also spends zero LLM budget on corrections.

Alternatives considered​

  • Option B β€” PR-promotion only (agent proposes a patch, human approves in the platform UI). This is the end-state (Decision 2 / P4.2); rejected as the immediate stopgap because building the proposal + approval surface is P4.2-sized work, while the flag gate ships now and loses nothing.
  • Option C β€” audited agent write-back (agent writes in place; every write versioned and logged). Rejected: audit-after-the-fact is not a quality gate. The write has already influenced live drafts by the time a human reads the log β€” at the exact moment quality is the product's core claim, review must be before effect, not after.

Verification​

  • api/test/loop-proposal-producer.spec.ts β€” LoopProposalProducerService.runOnce consumes the corrections.processed_at IS NULL backlog as kb_golden_answer DRAFT proposals and marks each correction processed, independent of any feature flag (the human-approved proposal flow that replaces the legacy direct-ingest). Includes the migrated corrections.processed_at column schema guarantee (nullable, no default) the producer's backlog selector depends on.
  • api/src/feature-flags/feature-flag.service.spec.ts β€” correction_auto_ingest_enabled is no longer in FLAG_KEYS (the flag and the correction-refinement.processor.ts direct-ingest pipeline it gated were removed in P4.2c).