Skip to the content.

Event Contract

This document is the public contract for the JSON event streams the toolkit emits. Anyone building a frontend (Telegram bot, web UI, Discord, MCP server, …) should be able to wrap the workflow using this document alone — no source-reading required.

There are two streams, intentionally separate, each with its own vocabulary and purpose.

Stream File Vocabulary Purpose
Workflow events <out_dir>/events.jsonl (per-request) + stdout when --json-events is set stage: "<name>" Chatty agent-facing stream. Says “what part of the workflow is happening right now.” Tail-able.
Audit log requests/<request_id>/audit.jsonl event: "<name>" Forensic record. Says “what discrete happened-thing got recorded.” Append-only at the application level (O_APPEND + flock).

The two streams overlap but are NOT redundant. Some workflow events have audit twins (e.g. stage: "uploaded"event: "upload_completed"). Some workflow events don’t (e.g. triage, render, history_hint — observational only). Some audit events have no workflow twin (e.g. all the Stage 1/2 gate events — they fire in u1_print_start_gate.py, not the workflow). The twin-pair table at the bottom of this doc lists which is which.

The two vocabularies exist because the streams answer different questions:

Don’t conflate them. A consumer that wants to render a live progress UI subscribes to events.jsonl. A consumer that wants to reconstruct what happened to a finished request reads audit.jsonl.


Lifecycle cheat-sheet

A normal --yes upload+start run from a fresh STL emits events in this order. (Items in brackets are conditional / depend on operator answers + collisions.)

WORKFLOW STREAM                            AUDIT STREAM
==========================================================================
request_created                            request_created
   triage
   [render × 1-2]
   [orient_analysis]
   [history_hint]
   [need_input × 0-5]                      (no audit twins for need_input)
   [awaiting_input — only without --yes]
COMMIT (only when --yes / --upload-only):
   [supports_override]
   [slice_reused — collision short-circuit]
   slicing                                 slicing_completed
   [warning]
   summary
   uploaded                                upload_completed
   readiness_card                          readiness_card_emitted
   next_action_required (Stage 1 cmd)
                                           — agent then runs u1_print_start_gate.py —
                                           stage1_photo_captured / stage1_photo_failed
                                           — operator approves photo, agent runs Stage 2 —
                                           [stage2_token_invalid]
                                           [stage2_preflight_blocked]
                                           start_safety_check_passed | start_safety_check_failed
                                           [stage2_sanity_capture_failed]
                                           print_started  ← terminal success

Resume path (operator re-uploads same STL after agent lost context):

request_resumed                            request_resumed
   [phase-aware skip — Phase 2 design]
   readiness_card_resumed                  readiness_card_replayed_from_resume
   next_action_required (Stage 1 cmd)
                                           ... same Stage 1 + Stage 2 events as above ...

Upload-only path (operator chose “Upload only” at the Upload? prompt):

   ... (analysis + slice + uploaded same as above) ...
   readiness_card                          upload_only_complete
   complete                                ← terminal

Workflow events — stage: vocabulary

Each entry: event name, payload fields, when it fires. Required fields marked plainly; optional fields are in italic. Field types: paths are absolute strings; numerics are JSON numbers; everything else is a string unless noted.

Lifecycle

stage: "request_created"

First emitted on a fresh STL (no on-disk recovery match found).

stage: "request_resumed"

Fires when content-hash recovery matched an in-flight request on disk (Phase 2 design — same STL bytes → same request_id).

stage: "readiness_card_resumed"

Fires when phase-aware skip short-circuits past the slice/upload prompts because the prior run already reached awaiting_start_approval. Payload is a copy of the prior readiness_card event with stage renamed and resumed_from_phase added.

Analysis

These are observational — they describe what the workflow learned about the model. No audit twins; the analysis itself doesn’t change print state.

stage: "triage"

First emitted after the source STL is parsed.

stage: "render"

Preview images. Multiple may fire per run (source view, auto-oriented view, slicer preview).

stage: "orient_analysis"

Comparison of the two orientations. Fires when both got rendered.

stage: "history_hint"

Surfaces prior-print history for this tool/nozzle to inform the preset recommendation. Always emitted (may carry an empty per_tool).

Decision

stage: "need_input"

The workflow has reached a decision point and is exiting. Each event surfaces one question. Every option carries a fully-formed next_command the agent should tool-call verbatim when the operator picks it.

After every need_input, an awaiting_input event fires before the workflow exits, so consumers tailing the stream know the workflow process has terminated and the next turn requires a tool-call.

stage: "awaiting_input"

Sentinel that the workflow process has exited awaiting the operator’s answer.

Commit

(Only fires when --yes or --upload-only is present.)

stage: "supports_override"

Fires once when the supports answer materialized a temp profile with the enable_support flag overridden.

stage: "slice_reused"

Cache hit: a prior slice was reused because the gcode for this filename + profile combo already existed.

stage: "slicing"

Bare event marking the start of a real Orca slice. No fields.

stage: "warning"

Slicer emitted geometric warnings (overhang, cantilever) in its output.

stage: "summary"

Slice complete; metrics + preview ready for the operator to see.

stage: "uploaded"

Upload to Moonraker completed (or detected a collision / transport error). The payload spreads _real_upload’s result, which carries many fields depending on outcome.

stage: "cancelled"

Workflow cancelled by operator (--cancel, or Cancel chosen at Upload?/filename_collision).

Readiness + dispatch

stage: "readiness_card"

Consolidated final-decision summary the agent surfaces to the operator before Stage 1. Carries everything the agent + operator need to make the start decision.

stage: "next_action_required"

Imperative signal: tool-call this command verbatim, no operator question, no narrative preamble.

Fires after readiness_card to push the Stage 1 dispatch, and after need_input in some recovery paths.

stage: "complete"

Terminal event for the upload-only path.

stage: "setup_required"

Operator-environment problem detected; workflow halted with remediation guidance.


Audit events — event: vocabulary

Audit rows have a common shape:

{"seq": 1, "ts": "2026-06-27T10:42:00+00:00", "request_id": "u1_...", "event": "<name>", "operator": "telegram:brent", "details": {...}}

Only the fields under details are listed below.

Lifecycle audit twins

Event Fires when details fields
event: "request_created" New request created (workflow) model_file, model_hash
event: "request_resumed" Content-hash recovery matched resumed_from, request_revision
event: "readiness_card_replayed_from_resume" Phase-aware skip fired printer_storage_filename, request_revision

Commit audit twins

Event Fires when details fields
event: "slicing_completed" After real Orca slice produced gcode gcode_hash, estimated_time, estimated_filament_g
event: "upload_completed" After upload to Moonraker uploaded_filename, moonraker_upload_ok, dry_run
event: "readiness_card_emitted" Readiness card built (upload+start path) printer_storage_filename, gcode_hash, request_revision
event: "upload_only_complete" Readiness card built (upload-only path) printer_storage_filename, gcode_hash, request_revision

The gcode_hash + request_revision on readiness_card_emitted is what can_start() (Phase 3b) consumes to verify the plan hasn’t drifted between the operator’s review and Stage 2 dispatch.

Start gate (Stage 1)

These fire from scripts/u1_print_start_gate.py and have no workflow twin — they exist only in the audit stream.

Event Fires when details fields
event: "stage1_photo_captured" Real bed photo captured + approval token written snapshot_path, approval_token
event: "stage1_photo_failed" Camera unreachable or photo verifiably dark error, is_mock

Start gate (Stage 2)

Event Fires when details fields
event: "stage2_token_invalid" Operator-supplied approval token invalid/expired reason
event: "stage2_preflight_blocked" Preflight re-check failed at Stage 2 blockers (list)
event: "stage2_sanity_capture_failed" Sanity-only fresh photo unusable error, is_mock, brightness_check
event: "start_safety_check_passed" can_start() returned ok request_revision, gcode_hash
event: "start_safety_check_failed" can_start() refused reason, current_revision, current_gcode_hash
event: "print_started" Moonraker /printer/print/start accepted printer_storage_filename, request_revision, gcode_hash

Stage 2 emits exactly ONE of: print_started (success), or a *_failed / *_invalid / *_blocked row (refusal). Stage 1 emits exactly ONE of stage1_photo_captured (success) or stage1_photo_failed (camera problem).

Pre-start grace period (Stage 2, v2.1.0)

After every safety check passes and BEFORE the HTTP call to the printer, Stage 2 opens a cancel window (default 120s; U1_GRACE_PERIOD_SECONDS=0 or --grace-seconds 0 disables). All rows land in audit.jsonl:

Event Fires when details fields
event: "pre_start_grace_period_started" Window opens grace_seconds, cancel_marker
event: "pre_start_grace_notify_sent" $U1_GRACE_NOTIFY_CMD exited 0 exit_code, stderr_tail
event: "pre_start_grace_notify_failed" Notify command failed/timed out (window still runs) exit_code, stderr_tail
event: "pre_start_grace_cancelled" Cancel marker appeared — no HTTP call. Checked every second, again after the final tick, and once more immediately before the start call cancel_marker, cancelled_after_wait_s
event: "pre_start_grace_period_expired" Window closed with no cancel → proceeding to start grace_seconds, proceeded_to_start
event: "gate_operator_unknown" Stage ran with no operator identity set (unknown:gate) — allowed, loudly audited note
event: "gate_refused_test_operator" Fence 1: operator has a test-flavored prefix (smoke: / test: / dry: / mock: / fixture:) — refused before any Moonraker call prefix_match

A grace-cancel refusal payload carries a recovery block (instruction + stage1_command): the slice and upload are still valid, so the cheap path back is a fresh Stage 1 (new photo + new yes), not a workflow re-run.


Twin-pair table (quick cross-reference)

Workflow stream stage: Audit stream event:
request_created request_created
request_resumed request_resumed
readiness_card_resumed readiness_card_replayed_from_resume
slicing slicing_completed
uploaded upload_completed
readiness_card (upload+start path) readiness_card_emitted
complete (upload-only path) upload_only_complete
triage, render, orient_analysis, history_hint — (observational only)
need_input, awaiting_input — (decision-flow only)
supports_override, slice_reused, warning, summary, cancelled, setup_required, next_action_required — (workflow-internal signals)
stage1_photo_captured, stage1_photo_failed, stage2_token_invalid, stage2_preflight_blocked, stage2_sanity_capture_failed, start_safety_check_passed, start_safety_check_failed, print_started (all gate-only)

How to consume each stream

Workflow events (events.jsonl + stdout when --json-events is set)

Audit log (requests/<request_id>/audit.jsonl)


Multi-part kit events (stage: — v2.1.0)

Emitted by the kit path. A zip with >1 STL is auto-detected; the single workflow emits kit_detected and the rest come from u1_kit_workflow.py.

Audit twins: kit_ingested, kit_sliced, kit_readiness_card_emitted, kit_slice_failed, kit_upload_failed, post_confirm_flags_backfilled, stage1_token_adopted_from_sidecar (event: vocabulary, in audit.jsonl).

Form mode (v2.2 — button UX with file handoff)

With --interaction-mode form (or U1_INTERACTION_MODE=form), the kit workflow emits ONE consolidated need_input with key: "kit_form" instead of the staged turns:

The handoff is the point: the adapter’s buttons collect answers at the GATEWAY, which writes <answers_dir>/<form_id>.json (U1_FORM_ANSWERS_DIR, default <data_dir>/form_answers). The workflow redeems the file — single-use (consumed on read), bound to the persisted form_id (mismatch → form_rejected). Answer content never passes through the model in either direction; the model relays one opaque id. Audit rows: kit_form_emitted, form_answers_file_redeemed, form_answers_rejected. The staged text flow remains the default and is unchanged.


Versioning

The event contract is additive: new events can appear; existing events’ field set can grow with optional fields. Consumers should ignore unknown stages and unknown fields.

Two changes would be breaking and require a major-version bump:

  1. Renaming an existing event (e.g. stage: "uploaded"stage: "upload_complete").
  2. Removing a previously-required field from an existing event.

v2.1.0 added kit + plates as optional additive fields on request.json and the kit events above — additive, so no version bump (schema_version stays 1). A breaking change (rename/removal) would bump schema_version in lockstep so consumers can branch on it.


Cross-references