> ## Documentation Index
> Fetch the complete documentation index at: https://docs.interpscout.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Briefing Pipeline

> From a queued request to a saved, gated pre-meeting briefing

A briefing request looks simple from the teacher's side — fill in a topic, maybe a speaker and an organization, submit. What happens between submit and a finished document is a queue, a research fan-out across several angles, a chunked writing discipline, and a save gate that will reject a briefing that isn't actually usable.

<div style={{position: "relative", width: "100%", maxWidth: "980px", margin: "2rem auto"}}>
  <svg viewBox="0 0 1500 580" style={{width: "100%", height: "auto", display: "block"}} role="img" aria-label="Briefing lifecycle: queued, then running with a heartbeat before every search, then chunked writing at up to 1,500 characters per chunk, then save gates requiring at least 800 characters, at least 3 real links, and the row still running, then done. A gate rejection sends the row back to chunked writing to revise. If running goes more than 6 minutes without a heartbeat, the row is reclaimed for orphan recovery and requeued, up to 8 attempts with the last two in simplified mode, looping back to queued; past the attempt limit it fails with an honest note.">
    <path d="M1060 164 C1060 90, 740 90, 740 168" stroke="#C8A55B" strokeWidth="1.6" strokeDasharray="5 5" fill="none" />

    <path d="M740 168 L735.5 155 L744.5 155 Z" fill="#C8A55B" />

    <path d="M625 455 C625 515, 150 515, 150 245" stroke="#B5763F" strokeWidth="1.6" strokeDasharray="5 5" fill="none" />

    <path d="M150 232 L154.5 245 L145.5 245 Z" fill="#B5763F" />

    <path d="M242.0 200.0 L314.0 200.0" stroke="#8A8378" strokeWidth="2" fill="none" />

    <path d="M327.0 200.0 L314.0 204.5 L314.0 195.5 Z" fill="#8A8378" />

    <path d="M532.0 200.0 L604.0 200.0" stroke="#8A8378" strokeWidth="2" fill="none" />

    <path d="M617.0 200.0 L604.0 204.5 L604.0 195.5 Z" fill="#8A8378" />

    <path d="M862.0 200.0 L929.0 200.0" stroke="#8A8378" strokeWidth="2" fill="none" />

    <path d="M942.0 200.0 L929.0 204.5 L929.0 195.5 Z" fill="#8A8378" />

    <path d="M1177.0 200.0 L1244.0 200.0" stroke="#8A8378" strokeWidth="2" fill="none" />

    <path d="M1257.0 200.0 L1244.0 204.5 L1244.0 195.5 Z" fill="#8A8378" />

    <path d="M430.0 239.0 L430.0 369.0" stroke="#B5763F" strokeWidth="2" fill="none" />

    <path d="M430.0 382.0 L425.5 369.0 L434.5 369.0 Z" fill="#B5763F" />

    <path d="M532.0 420.0 L604.0 420.0" stroke="#B5763F" strokeWidth="2" fill="none" />

    <path d="M617.0 420.0 L604.0 424.5 L604.0 415.5 Z" fill="#B5763F" />

    <path d="M862.0 420.0 L954.0 420.0" stroke="#B5763F" strokeWidth="2" fill="none" />

    <path d="M967.0 420.0 L954.0 424.5 L954.0 415.5 Z" fill="#B5763F" />

    <rect x="65.0" y="168.0" width="170" height="64" rx="12" fill="#8A8378" fillOpacity="0.10" stroke="#8A8378" strokeWidth="1.4" />

    <rect x="335.0" y="168.0" width="190" height="64" rx="12" fill="#8A8378" fillOpacity="0.10" stroke="#8A8378" strokeWidth="1.4" />

    <rect x="625.0" y="168.0" width="230" height="64" rx="12" fill="#8A8378" fillOpacity="0.10" stroke="#8A8378" strokeWidth="1.4" />

    <rect x="950.0" y="164.0" width="220" height="72" rx="14" fill="#C8A55B" fillOpacity="0.16" stroke="#C8A55B" strokeWidth="1.6" />

    <rect x="1265.0" y="168.0" width="150" height="64" rx="12" fill="#8A8378" fillOpacity="0.10" stroke="#8A8378" strokeWidth="1.4" />

    <rect x="335.0" y="390.0" width="190" height="60" rx="12" fill="#B5763F" fillOpacity="0.12" stroke="#B5763F" strokeWidth="1.4" />

    <rect x="625.0" y="385.0" width="230" height="70" rx="12" fill="#B5763F" fillOpacity="0.12" stroke="#B5763F" strokeWidth="1.4" />

    <rect x="975.0" y="390.0" width="170" height="60" rx="12" fill="#B5763F" fillOpacity="0.22" stroke="#B5763F" strokeWidth="1.6" />
  </svg>

  <div style={{position: "absolute", left: "60.0%", top: "13.4%", transform: "translate(-50%,-50%)", fontSize: "11.5px", fontStyle: "italic", color: "#8A8378", whiteSpace: "nowrap"}}>gate rejects → revise</div>
  <div style={{position: "absolute", left: "25.8%", top: "91.0%", transform: "translate(-50%,-50%)", fontSize: "11.5px", fontStyle: "italic", color: "#8A8378", whiteSpace: "nowrap"}}>retries</div>
  <div style={{position: "absolute", left: "60.5%", top: "60.0%", transform: "translate(-50%,-100%)", fontSize: "11.5px", fontStyle: "italic", color: "#8A8378", whiteSpace: "nowrap"}}>over limit</div>

  <div style={{position: "absolute", left: "10.00%", top: "34.48%", transform: "translate(-50%,-50%)", textAlign: "center", width: "11.3%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Queued</div>
  </div>

  <div style={{position: "absolute", left: "28.67%", top: "34.48%", transform: "translate(-50%,-50%)", textAlign: "center", width: "12.6%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Running</div>
    <div style={{fontSize: "10.5px", color: "#8A8378", marginTop: "2px", lineHeight: 1.25}}>heartbeat before every search</div>
  </div>

  <div style={{position: "absolute", left: "49.33%", top: "34.48%", transform: "translate(-50%,-50%)", textAlign: "center", width: "15.3%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Chunked writing</div>
    <div style={{fontSize: "10.5px", color: "#8A8378", marginTop: "2px", lineHeight: 1.25}}>≤1,500 chars/chunk · every 1–2 chunks</div>
  </div>

  <div style={{position: "absolute", left: "70.67%", top: "34.48%", transform: "translate(-50%,-50%)", textAlign: "center", width: "14.7%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Save gates</div>
    <div style={{fontSize: "10.5px", color: "#8A8378", marginTop: "2px", lineHeight: 1.25}}>≥800 chars · ≥3 links · still running</div>
  </div>

  <div style={{position: "absolute", left: "89.33%", top: "34.48%", transform: "translate(-50%,-50%)", textAlign: "center", width: "10.0%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Done</div>
  </div>

  <div style={{position: "absolute", left: "28.67%", top: "72.41%", transform: "translate(-50%,-50%)", textAlign: "center", width: "12.6%"}}>
    <div style={{fontWeight: 700, fontSize: "13.5px"}}>Orphan recovery</div>
    <div style={{fontSize: "10.5px", color: "#8A8378", marginTop: "2px", lineHeight: 1.25}}>no heartbeat > 6 min</div>
  </div>

  <div style={{position: "absolute", left: "49.33%", top: "72.41%", transform: "translate(-50%,-50%)", textAlign: "center", width: "15.3%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Requeue</div>
    <div style={{fontSize: "10.5px", color: "#8A8378", marginTop: "2px", lineHeight: 1.25}}>≤8 attempts · last two simplified</div>
  </div>

  <div style={{position: "absolute", left: "70.67%", top: "72.41%", transform: "translate(-50%,-50%)", textAlign: "center", width: "11.3%"}}>
    <div style={{fontWeight: 700, fontSize: "14.5px"}}>Failed</div>
    <div style={{fontSize: "11px", color: "#8A8378", marginTop: "2px"}}>with an honest note</div>
  </div>
</div>

*Gray traces the happy path; terracotta traces the orphan-recovery and retry path; gold marks the save gate — the only place a briefing is judged worth keeping. A row only fails after 8 requeue attempts, the last two already stripped to a simplified, no-further-reading mode.*

## Queue and dispatch

A submitted request is written to Postgres as `status: queued`. A cron-driven dispatcher claims queued rows on a schedule, marks the claimed row `running`, and hands it to the Briefing agent on the durable agent runtime. The web app doesn't wait around for the agent to finish — it dispatches and moves on; completion is observed later, either by the agent's own save call marking the row `done`, or by the same heartbeat-aware recovery that watches course production.

## Research, fanned out across angles

The agent researches a fixed set of angles, skipping any whose input field is empty:

1. **Domain crash course** — what this field is, explained plainly, and its core concepts.
2. **Institution profile** — background, reputation, and recent moves of the host organization.
3. **Speaker profile** — recent public statements and notable positions.
4. **Controversies and hot topics** — what practitioners are debating recently, anchored to a date the dispatcher injects mechanically so the agent can't misjudge "recent" by guessing the wrong year.
5. **Bilingual term table** — the heaviest angle by design, and the interpreter's core asset from the briefing. The agent actively searches authoritative bilingual sources (government white papers, Ministry of Foreign Affairs bilingual transcripts, UN parallel texts) for each concept, and grades every rendering official or reference depending on whether it actually found a citable bilingual source — never upgraded on confidence alone.
6. **Further reading** — three to eight entries, every one a real, working link the agent actually fetched or searched to, never a placeholder.

This loosely follows Setton & Dawrant's account of what an interpreter needs before a real assignment: domain-specific knowledge (what the field is) and encounter-specific knowledge (who's hosting, who's speaking, what's contested right now).

## Writing in chunks, not one shot

The agent doesn't compose the whole document in memory and hand it over in a single write. It appends the briefing to a sandbox file in chunks — roughly 1,500 characters at a time — and reports progress after every chunk or two. This exists because writing a multi-thousand-character document in one model turn, with no intermediate signal, looks identical from the outside to a stalled agent: nothing observable happens for minutes at a time. Chunked writing turns "no visible progress for ten minutes" into "a heartbeat every thirty seconds," which is what makes it possible to tell a slow-but-alive agent apart from a dead one.

## The save gate

When the file is complete, the agent calls a save tool that reads the finished file and applies gates before it's allowed to reach `done`:

* **A minimum length** — a briefing under roughly 800 characters is judged too thin to be useful and rejected rather than saved.
* **A further-reading floor** — at least three real, clickable links, or the save is rejected. A briefing with zero further reading fails this gate outright; the normal mode always requires it (a simplified fallback mode, used only after repeated retries, is explicitly exempt and says so in the document itself).
* **A running-status write guard** — the save only succeeds if the row is still `running` when it commits. This exists specifically to stop an old, previously-orphaned session from overwriting a briefing that a newer retry already finished (see [Truth Layer](/internals/truth-layer) for why that guard was added).

A rejected save isn't final — the agent reads the reason, edits the file, and resubmits within the same session.

## Heartbeat and self-healing

A briefing row is heartbeat-aware, not wall-clock-timed from when it started: as long as the agent keeps updating progress, the row is never reclaimed as orphaned, no matter how long real research and writing takes. Only once progress goes quiet past a threshold does the row get requeued. Retries are capped; the last two attempts before the cap switch to a simplified mode — two core angles only, no further-reading requirement — that trades completeness for a real chance of finishing rather than exhausting the retry budget chasing a full result.
