Inspiration
Painting a bedroom is about seven hours of work. It takes most of a weekend.
The gap is dwell: filler going off, a coat drying, grout curing, dough proving. Time the job needs and you do not. It is not on the tin as a task, it cannot be hurried, and it is the only part of the job that runs while you are asleep.
That is why people get this wrong in a specific, repeatable way. They look at a six-hour job, see a free Saturday, and start at two in the afternoon. The first coat goes on at six, and it is Sunday lunchtime before they find out the weekend was never long enough.
An assistant standing in the room can hold that, and no instruction sheet can — because the answer depends on which hours you have and how cold your room is today. That is an Alexa+ problem, not a website problem.
What it does
You tell it what you are doing and when you are actually free. It works out when every piece of work happens, and then survives contact with reality.
"Six hours and fifty-five minutes of actual work, spread over a day and a half — most of that is waiting for things to dry. You stop tomorrow afternoon at quarter to five, and it is usable tomorrow evening at five past eight. Glossing the skirting will not fit — leave that for another day. First job, at 9am: clear the room and put sheets down. Job code is quarry sixty-five."
Then, over the following days:
| You say | What happens |
|---|---|
| "Done the filling." | Recorded at the real time; everything downstream reschedules from there. |
| "It's freezing in here and the windows are shut." | Drying times stretch, and it tells you how far back that pushes the finish. |
| "Where was I?" | Three days later, with no arguments — the MCP session remembers the job. |
| "Will the room be usable by Sunday teatime?" | A real answer, including "no, and dropping work will not help." |
| "What do I need to buy?" | Quantities from the room's size, rounded up to tin sizes, minus the cupboard. |
Two things it gets right that a chatbot does not
It knows the difference between finishing and being finished. "Done by six" is ambiguous: the second coat goes on at half four, the room is not usable until half eight, and you are free from half four. Ask about a deadline and it establishes which you meant — because you can drop work to hit one of those and you cannot hurry the other.
It refuses to suggest a sacrifice that buys nothing. Ask if you can finish by Sunday seven and the honest answer is often no — and skipping the gloss will not change that, because the finish is set by the last coat needing four hours to dry, not by how much there is to do. Software that tells you to drop a coat of paint for nothing is worse than software that says nothing.
How I built it
A self-hosted MCP server, spec 2025-11-25, over Streamable HTTP, on a single endpoint.
- POST takes one JSON-RPC message. Requests get
application/json; notifications get202with no body.initializereturns anMcp-Session-Id; every later request carries it, and an unknown one gets a404so the client knows to start again. - GET opens an SSE stream, primed with an event id and an empty
datafield as the spec asks, with aretryfield so the client polls back rather than the server holding a socket open all afternoon. It has something real to push: the moment a coat finishes drying, it says so, unprompted. - DELETE ends the session.
Originis validated, and a present-but-untrusted one gets a403— which is what stops a random page driving somebody's server through their browser.
Seven tools, each declaring an outputSchema and returning structuredContent alongside text written to be read out loud, plus a dwell/ui hint in _meta telling a client which of four cards to draw. The three procedures are also exposed as MCP resources (dwell://procedure/…).
list_jobs · start_job · whats_next · mark_done · change_the_plan · can_i_finish_by · what_to_buy
The engine is the part that earns it. Work has to land inside the hours you are free; dwell runs on the wall clock regardless, through the night. You are one person and there is one roller. Some steps must wait for the one before to cure; others only need it worked — you pull masking tape while the last coat is still soft, and waiting for it to cure is how you tear the paint off with the tape. Work is contiguous on purpose: you do not paint half a wall, break for four hours and come back, because the edge dries and it shows.
It is critical-path list scheduling under calendar and resource constraints. Among the steps that are ready it starts the one with the longest chain behind it, with compulsory work always ahead of optional, so the gloss never takes the last slot on a Sunday and pushes a second coat out of the plan. It also reports why each step sits where it does: after the one before, waiting on a cure, the kit was in use, or simply that you were not free until then.
The client is a stand-in for an Echo Show. The whole visual system is one idea: ochre is you, hands on it in the room; slate is the job getting on without you. The strip along the bottom is the entire job drawn that way — solid where your hands are on it, hatched where it is drying, night shaded because that is when most of the drying happens. The clock under the screen is not a mock: it is the now argument on a real tool call, so a two-day story fits a two-minute demo.
Stack: dependency-free JavaScript. No build step, no bundler, no framework, and no web font. The scheduler runs unchanged in Deno on Supabase Edge Functions and in Node for the tests. State lives in Postgres, in two tables with row level security on and no policies at all — nothing reaches them except the Edge Function, so the data surface of the whole system is exactly seven tools.
Challenges I ran into
Two kinds of "done". The first version had one finish time and it was quietly wrong for every deadline question anyone would actually ask. Splitting hands_off_at from usable_at changed the product more than any other single change.
Dropping work that buys nothing. The first can_i_finish_by dutifully dropped optional steps until it ran out, then reported a pile of sacrifices — even when they moved the finish by zero minutes, because the constraint was drying time, not workload. Now it checks whether the trimming bought anything and, when it did not, hands back the untrimmed plan and names what is actually in the way.
Dependencies are not all the same. Modelling every dependency as "wait for the cure" put four hours between the last coat and pulling the tape, which is exactly backwards — the tape has to come off while the paint is soft. That needed a second, weaker edge in the graph (follows), and it changed the schedule by hours.
An optional step stole a compulsory slot. The critical-path heuristic gave the gloss a long tail, so it grabbed the last free hours on a Sunday and pushed a second coat of emulsion out of the plan entirely. One extra sort key fixed it, but only after the plan looked plausible and was wrong.
Every one of these was found by reading the plan the thing produced and asking whether a decorator would agree with it.
Accomplishments that I'm proud of
225 assertions, and half of them are protocol. tests/server.test.js drives the server through real Request/Response objects on the same code path Deno serves: session headers, 202 on notifications, 404 on an unknown session, 403 on a bad origin, 400 on an unsupported protocol version, the primed SSE event — and then lives through a whole job, planned on Saturday, half finished, weather turning, resumed on Sunday.
server/app.js is a plain Request -> Response function with the store injected, and server/index.ts is five lines of Deno on top. That split is why the entire MCP surface could be exercised on a laptop before anything was deployed — and why node server/local.js runs the whole product, client and server, with no cloud account at all.
And the engine does not know what paint is. It knows work, dwell and dependency — which is why a sourdough loaf schedules through it unchanged. The third procedure in the library is there to prove exactly that.
What I learned
That the interesting constraint in a voice product is not the microphone, it is the absence of scrollback. Everything the device says is gone the moment it is said, so a spoken plan cannot be a list — it has to be one thing at a time, with the number you will act on placed last, where it lands. Writing whats_next to return one instruction instead of a summary was the change that made it feel like an assistant instead of an app.
And that MCP's session id is a product feature, not plumbing. It is the entire mechanism behind "where was I" working three days later with no arguments, which is why whats_next has an empty required list.
What's next for Dwell
More procedures — they are data, not code, so a new one is a table of steps and dwell times, not a change to the scheduler. Real weather instead of asking. A shopping list that becomes a basket. And the shape most obviously missing: a second pair of hands, because every number in the scheduler assumes there is only one.
Built With
- alexa
- deno
- edge-functions
- javascript
- json-rpc
- mcp
- model-context-protocol
- node.js
- postgresql
- scheduling
- server-sent-events
- streamable-http
- supabase
- voice-ui
Log in or sign up for Devpost to join the conversation.