Realtime (notify, not poll)
Use await, input, and render so agents and users react over the same stream.
A worker-less shell app can still be two-way. The browser view sends input events. An
agent parks on await, gets notified, computes, then publishes render state. The next
viewer repaints from current_state.
screen select -> POST input/select -> GET await wakes -> agent computes -> POST render/main -> shell rendersAwait
GET /apps/{id}/await?cursor=<n> parks until one event arrives or the await window ends.
The default replay window is 500 events or 15 minutes, with a 25 second await. A timeout
still returns ok:true, an empty events array, and the current state.
# Await before the user action. The request parks until an event arrives or the await window ends.
# docjourney: step=await-select
curl -s "https://t.tini.works/apps/journey/await?cursor=0" \
-H "authorization: Bearer <short-key>"
# -> {"ok":true,"cursor_status":"ok","replay_available":true,"events":[{"kind":"input","name":"select","payload":"{\"choice\":\"ops\"}","cursor":1,...}],"current_state":{},"next_cursor":1}Cursor failures are explicit:
| Shape | Meaning |
|---|---|
{"ok":false,"type":"cursor_too_old","cursor_status":"too_old","first_retained_seq":...,"last_seq":...,"next_cursor":...} | The cursor fell out of the 500 event or 15 minute replay window. |
{"ok":false,"type":"replay_unavailable","cursor_status":"js_disabled","replay_available":false,"events":[],"current_state":...} | JetStream replay is unavailable for this account or host. |
Input
POST /apps/{id}/input/{event} publishes a user or agent event. If payload is a string,
it is used as-is. Otherwise its raw JSON is published as a string.
# Send the selected value.
# docjourney: step=select-input
curl -s -X POST "https://t.tini.works/apps/journey/input/select" \
-H "authorization: Bearer <short-key>" \
-H "content-type: application/json" \
-d '{"payload":"{\"choice\":\"ops\"}"}'
# -> {"ok":true,"published":true,"app":"journey","kind":"input","name":"select","subject":"app.journey.session.main.input.select","cursor":1,"replay_available":true}Render
POST /apps/{id}/render/{region} publishes view state and persists that region as
current state. Late joiners and later await calls can repaint without polling a custom
API.
# Transform the selected input into render state, then publish that state.
# docjourney: step=transform-render
curl -s -X POST "https://t.tini.works/apps/journey/render/main" \
-H "authorization: Bearer <short-key>" \
-H "content-type: application/json" \
-d '{"payload":"{\"selected\":\"ops\",\"source\":\"docjourney\"}"}'
# -> {"ok":true,"published":true,"app":"journey","kind":"render","name":"main","subject":"app.journey.session.main.view.main","cursor":2,"replay_available":true}# Read the current render state without polling a custom API.
# docjourney: step=await-current-state
curl -s "https://t.tini.works/apps/journey/await?cursor=2" \
-H "authorization: Bearer <short-key>"
# -> {"ok":true,"cursor_status":"ok","replay_available":true,"events":[],"current_state":{"main":"{\"selected\":\"ops\",\"source\":\"docjourney\"}"},"next_cursor":2}