Durable event journal and replay
Durable event journal and replay for Q integrations.
Q3.2 gives versioned answers and managed agent runs one PostgreSQL event journal. Apply 20261008_000000_event_journal before deploying the matching API and workers. The prepared Python and TypeScript 1.1.0 packages include managed-agent framing, duplicate suppression and bounded readers; publication remains a separate step.
Managed agent observation
Save the run receipt, then call GET /v1/agents/{agent_id}/runs/{run_id}/stream?stream_key=... using the original principal. Managed streams return X-Q-Stream-Version: 1, X-Q-Run-ID, X-Q-Replay-Expires-At and the usual run/stream-key headers. An already-terminal journal also returns X-Q-Terminal-Cursor. All these headers are exposed to browser clients.
Each event has version, type, run_id, sequence, cursor, revision and payload. SSE id equals cursor (run_id:sequence); SSE event equals type. Save the last fully applied cursor with your application state. On reconnect, supply it as Last-Event-ID. A numeric cursor from a legacy stream is invalid for a managed stream. GET never restarts a workflow or performs a tool action.
receipt = await q.agents.runs.start(agent_id, {
"prompt": "Answer using the authorised evidence",
"mode": "ask", "durable_temporal": True,
}, idempotency_key=creation_key)
async for event in q.agents.runs.stream(
agent_id, receipt["run_id"], stream_key=receipt["stream_key"],
last_event_id=saved_cursor,
):
# Apply state and save event["cursor"] together in your own durable transaction.
if event["type"] == "revision":
draft = event["payload"]["answer"]
elif event["type"] == "token":
draft += event["payload"].get("delta", "")
elif event["type"] == "done":
outcome = event["payload"]["status"]
saved_cursor = event["cursor"]TypeScript uses q.agents.runs.stream(agentId, runId, { streamKey, lastEventId }). Both primary readers validate run identity, frame limits, sequence gaps and revision changes, and suppress duplicate sequences within that reader. They propagate HTTP/protocol failures; applications reconnect using the saved cursor. They do not automatically POST again or promise exactly-once application side effects. Cursor/state persistence is the consumer's responsibility.
An activity retry or input resume appends revision with replace: true, a reason and committed answer text. Replace your draft and clear stale provisional progress/evidence; never append that checkpoint text again. Other progress/tool events remain informational. Unknown event types advance the cursor without changing your result.
done ends the journal and carries status (completed, failed, expired or cancelled), row_version, result_url, and normally result/usage. Replace provisional output with the final result. result_url is relative to your configured API prefix. An oversized final result omits result; fetch the authenticated run resource. The terminal frame is recoverable from committed run state even if the worker died before publishing it. Two API instances reconstruct the same event, including its ID. EOF alone never establishes success. A reconnect at the already-consumed terminal cursor closes cleanly.
Bounds and errors
- One stream retains at most 1,024 events and 8 MiB of encoded frames; a frame is at most 262,144 bytes. Each database read returns at most 16 events. Subscribers pull pages and keep no growing server queue.
- Answer expiry remains creation plus 900 seconds. Managed-agent expiry is its original deadline plus 3,600 seconds. Reconnecting does not extend either deadline. Expired answer metadata returns 410 until cleanup, then 404; managed-run identity can still establish expiry after cleanup.
- Cursor errors are explicit: malformed/cross-run cursor 400; cursor ahead 409; retention gap or expiry 410. A slow consumer can exhaust retention. Reconcile
GET /v1/agents/{agent_id}/runs/{run_id}and inspect status; do not invent missing progress or repeat actions. - Original tenant/principal, current workspace/delegation/space access and captured document revisions are checked before headers and each page. Revoked or changed sources invalidate replay. External, memory and other evidence without a revision contract can make reconnect unavailable (
stream_evidence_requires_refetch). Bytes already delivered cannot be revoked. - Public run status, run lists and agent recent-run projections preserve lifecycle fields while returning
result_json: nullandresult_access_errorwhen result evidence cannot be reauthorised. This also protects a large-result fallback and retained results after journal cleanup. A redacted result is not an execution failure. - Database/evidence unavailability fails closed. Before headers, normal error envelopes apply; after headers the connection closes and a subsequent GET gives the current error. Observer disconnect only stops observation.
Comment keepalives do not consume sequence numbers. Answer producer heartbeats still maintain its lease. A synchronous answer producer lost for 20 seconds returns stream_interrupted; the journal does not restart it.
Operations and compatibility
Give scoped API/worker roles CRUD access to both journal tables with tenant RLS enabled. Set DURABLE_AGENT_RECONCILE_TENANTS for all assigned tenants, including tenants using only answer streams. The worker sweep prunes up to 100 expired streams per tenant per pass; python -m scripts.agent_runs --tenant authorised-tenant performs the same bounded cleanup alongside lifecycle reconciliation. Repeat an operator sweep while backlog remains. Expiry denies replay even if cleanup is delayed. Include these tables in the existing tenant data deletion and database backup/retention policy.
Drain old answer streams (up to 900 seconds) and coordinate API/worker rollout. Existing Redis histories are not imported; affected old answer cursors return 404. Managed journals are created lazily; do not promise recovery of pre-upgrade events. Downgrade waits until all journal replay windows expire. Production connection budgets, polling load and retention cleanup need staging qualification.
Inline agents, unversioned answers, legacy Temporal activities and separate gateway/job streams remain explicit best-effort adapters. They do not advertise the managed version-one handshake. Go/CLI generic transports can observe the wire contract but do not gain a typed agent replay reducer here. External action journalling is Q3.3; journal delivery alone does not make tool execution exactly once.