Skip to content

Tether Interaction

You are the thread back to base reality. Psychonauts work autonomously within the annihilation, but you have final say over questions, drifts, pipeline mutations, and code grounding.

When a psychonaut needs human input, it invokes the ask_user tool, which calls QuestionQueue.ask/2. This blocks the psychonaut until the Tether answers or the question times out.

%Annihilation.Tether.Question{
id: "q_abc123",
agent_id: "agent_xyz",
bead_id: 42,
burst_id: "burst_20260227T103045_001",
text: "Should I use PostgreSQL or SQLite for the user store?",
context: "The requirements mention multi-region...",
priority: :normal, # :critical | :high | :normal | :low
timeout: 120_000, # 2 minutes default
status: :pending, # :pending | :answered | :timed_out
answer: nil,
asked_at: ~U[2026-02-27 10:30:45Z],
answered_at: nil
}

Questions are sorted by priority (critical > high > normal > low), then by submission time (oldest first). The TUI Tether mode shows them in this order.

  1. Psychonaut calls ask_user tool -> QuestionQueue.ask/2 blocks the agent process
  2. Question appears in TUI Tether mode with priority badge
  3. Event broadcast: "tether:reaching" -> {:question_asked, question}
  4. Answer path: Tether types response -> QuestionQueue.answer/3 -> agent unblocks with {:ok, answer_text}
  5. Timeout path: Timer fires -> agent receives {:timed_out, question} -> agent proceeds with an assumption (drift)
QuestionQueue.answer(question_id, "Use PostgreSQL for multi-region support")
# -> :ok (answered in time)
# -> {:late, question} (question already timed out -- triggers late beacon handling)
# -> {:error, :not_found}

When a psychonaut times out waiting for an answer, it records its assumption in the AssumptionsLedger.

Each drift records:

  • agent_id, bead_id, burst_id — context
  • question_id — the original timed-out question
  • text — what the psychonaut assumed
  • reason — why it chose this assumption
  • status:drifting | :confirmed | :rejected | :superseded
  • correction_bead_id — created when rejected or superseded
Question times out
|
v
:drifting (assumption recorded)
|
+----+----+----+
| | |
v v v
:confirmed :rejected :superseded
(Tether (Tether (late beacon
agrees) disagrees) arrived)

Drifts are persisted as append-only JSONL at .annihilation/assumptions.jsonl. The AssumptionsLedger GenServer maintains an in-memory index loaded from file on startup. Every mutation (create, update) is appended as a new line.

The Tether can act on drifts via DriftReview:

DriftReview.ground(drift_id, note: "Good assumption")

Confirms the psychonaut’s assumption was correct. Sets status to :confirmed. Broadcasts {:drift_confirmed, info} on "tether:drifts".

DriftReview.reject(drift_id, "Should have used PostgreSQL, not SQLite")

The assumption was wrong. Creates a correction bead automatically:

  • Title: “Correct rejected drift: {question_summary}”
  • Labels: ["drift-correction", "tether-rejected"]
  • Priority: 1 (high)
  • Description includes the original assumption, reason, and Tether’s correction

Sets status to :rejected. The correction bead will be picked up in the next burst.

DriftReview.note(drift_id, "Worth discussing in the next standup")

Adds a note without changing status. Notes can be added to drifts in any status.

When the Tether answers a question after it has timed out, the LateBeaconHandler processes it:

  1. QuestionQueue.answer/3 returns {:late, question}
  2. Event broadcast: "tether:reaching" -> {:late_answer, question, answer_text}
  3. LateBeaconHandler receives the event
  4. Looks up the corresponding drift via AssumptionsLedger.find_by_question_id/2
  5. Creates a correction bead (similar to rejection, but with “superseded” context)
  6. Updates drift status to :superseded
  7. Broadcasts {:drift_superseded, info} on "tether:drifts"

If the drift does not exist yet (race condition), the late answer is buffered in pending_late_answers and applied when the drift creation event arrives.

config/config.exs
config :annihilation, :assumptions,
question_timeout: 120_000, # 2 minutes
auto_create_correction_beads: true # auto-create beads on late answers

Pipeline mutations (via set_pipeline and create_pipeline tools) go through the Pipeline.GroundingQueue:

  1. Psychonaut proposes a pipeline change
  2. Mutation enters the grounding queue with a timeout (default 2 minutes)
  3. Tether can:
    • {:approve, _} — apply as-is
    • {:reject, reason} — do not apply
    • {:modify, modified_value} — apply with modifications
  4. If timeout expires, the mutation drifts — applied as an assumption and recorded in the AssumptionsLedger

Pipeline drifts can be reviewed via PipelineDriftReview using the same ground/reject pattern.