from message to receipt
The clearest demo of netsky is not a screenshot. It is one message entering the system and staying explainable all the way through.
A text can arrive from the web form or from iMessage. After that, the path converges. Netsky records the message, records delivery, queues the input for manager0, and updates the database-backed surfaces the web app reads (src/crates/netsky-web/src/lib.rs:201-223, src/crates/netsky-comms/src/lib.rs:249-320, src/crates/netsky-daemon/src/lib.rs:1047-1074).
For iMessage-originated threads, the daemon can also bridge a later manager reply back to that chat when the message metadata still carries the original chat id (src/crates/netsky-daemon/src/lib.rs:720-755, src/crates/netsky-comms/src/lib.rs:155-199).
flowchart LR
web[web form]
imessage[iMessage]
messages[(messages)]
deliveries[(deliveries)]
inputs[(actor_inputs)]
manager[manager0]
tasks[(tasks)]
notes[(notes)]
logs[(logs)]
dashboard[web dashboard]
reply[outbound reply]
web --> messages
imessage --> messages
messages --> deliveries
messages --> inputs
inputs --> manager
manager --> tasks
manager --> notes
manager --> logs
tasks --> dashboard
notes --> dashboard
logs --> dashboard
manager --> reply
That is the metadata spine in practical terms. It is not “the database somewhere in the background.” It is the path that keeps the system legible after the message scrolls away.
two front doors, one ingress path #
The web app exposes POST /api/user-message. It trims the body, enforces a size limit, and forwards the text to the daemon as Request::UserMessage { source: "web", body } (src/crates/netsky-web/src/lib.rs:201-223).
The iMessage side is a poller, not a webhook. Every two seconds the daemon asks netsky-comms to read new rows from the local Messages database. Only allowed chats are imported. Each imported row becomes a messages record addressed to manager0, and each one gets a deliveries record showing that the database accepted it and whether channel injection succeeded (src/crates/netsky-daemon/src/lib.rs:665-691, src/crates/netsky-comms/src/lib.rs:249-320).
That convergence matters. Web text and phone text are not two different products with two different truth stores. They are two ingress paths into the same operational record.
what the spine actually stores #
The shared schema is small enough to name directly.
actors: who exists, what kind of runtime each one is, whether it is live, and where it is running.sessions: the current and historical runtime instances behind those actors.messages: the inbound and outbound communication record.deliveries: whether a given message made it to a given target and by which method.actor_inputs: the queue that binds a durable inbound message to a specific live thread.tasks: bounded work with priority, estimate, assigned actor, and status.notes: compact handoff context inwhat,why,how,nextform.logs: the event stream the web app can summarize into a live feed.
Those tables are declared directly in netsky-db, and the matching records live in netsky-client, so the daemon, CLI, and web app all speak the same shapes (src/crates/netsky-db/src/lib.rs:109-229, src/crates/netsky-client/src/lib.rs:175-259).
This is the important distinction from “chat history.” A chat transcript can tell you what was said. This spine can tell you what the system did with it.
where manager0 fits #
manager0 is not the database. It is the first active reader and writer on top of it.
When the daemon queues a message for manager0, it does two things. First, it keeps the original messages row. Second, it writes an actor_inputs row bound to the current Codex thread for that actor (src/crates/netsky-daemon/src/lib.rs:1047-1074, src/crates/netsky-agent/src/lib.rs:278-315, src/crates/netsky-db/src/lib.rs:746-863).
That split is load-bearing.
The messages row is the durable record that something came in. The actor_inputs row is the live queue entry that says which running session should consume it next. If the session changes, the durable message still exists. If the transport changes, the queue still exists. The system does not have to infer either from terminal text.
The same pattern shows up again when work branches outward. Starting a worker creates a real task record, marks it in-progress, upserts the manager and worker actors, and then submits the task text into the worker session (src/crates/netsky-agent/src/lib.rs:203-260, src/crates/netsky-db/src/lib.rs:520-648).
That is why the queue page and the chat thread can stay consistent. They are not reconstructed from prose after the fact. They are parallel views over the same underlying records.
why the web app can stay thin #
The web app does not maintain a separate interpretation layer.
Its dashboard snapshot reads status, actors, sessions, tasks, messages, and logs from the database, then assembles the overview and manager0 surfaces from those reads (src/crates/netsky-web/src/lib.rs:139-155, src/crates/netsky-web/src/lib.rs:293-377). The status call even reports the database path and object counts directly from the same backend (src/crates/netsky-db/src/lib.rs:1188-1202).
One concrete route shows the shape. GET /api/dashboard returns one snapshot object with status, manager, queue, feed, tasks, messages, conversation, and logs fields, all assembled from the same database-backed records rather than from a separate cache or hand-maintained page model (src/crates/netsky-web/src/lib.rs:146-154, src/crates/netsky-web/src/lib.rs:293-377).
{ "status": { "...": "daemon + counts + health" }, "manager": { "...": "manager0 summary" }, "queue": { "...": "open work summary" }, "feed": [], "tasks": [], "messages": [], "conversation": [], "logs": [] }
That is a much stronger setup than a dashboard that screenscrapes terminal panes or polls one runtime’s memory. A page refresh, a daemon restart, and a fresh session can still recover the same recent message, task, and log history.
The current overview screenshot is useful here because it is not a second artifact. It is one HTML view over that same snapshot.

The overview route is already reading the same status, task, message, and log trail the receipt idea depends on.
It also means the public side can get more exact. A post can cite the same route shape and the same record model the operator UI uses. A future report page can link to the same task and message IDs the daemon already records. The narrative layer and the operational layer stop drifting apart.
where the receipt comes from #
The “receipt” idea is just this spine made visible.
If one inbound message already has a message row, delivery rows, an actor-input binding, nearby tasks, nearby notes, and nearby logs, then the missing piece is not collection. It is presentation.
A useful receipt page should open any one of those objects and let the rest of the chain be traversed without guessing:
- message to task
- task to actor
- actor to session
- session to note or log
- reply back to the original message
The current system is closer to that than it first appears. The write path already exists. The web app already reads the right records. What is still missing is deeper linking and object-level drill-down.
That is why the next useful pass is not “more dashboard.” It is turning one message path into one inspectable trail.