work / conversation simulator
The gaps aren’t simulated. They’re real.
Synthetic conversation almost always reads as fake, and not because the sentences are bad. It reads as fake because everything happens — no dead hours, no thread that dies and gets necroposted a week later, no 3am. So this one doesn’t model elapsed time at all. It runs from cron, most invocations post nothing, and the silence between messages is literal silence.
529 lines of python · 35 personas · circadian availability curves · thread heat, fatigue, dormancy
personal tool · working · fictional cast
the idea
The obvious way to build this is a loop: generate a conversation, insert plausible-looking delays, write it out. It produces something that looks right in a screenshot and wrong the moment you scroll. Every thread gets a reply. Nothing is ever abandoned. The delays are drawn from a distribution, which means they are regular in a way real ones never are.
The move that makes this work is refusing to own the clock. The program is stateless between runs: cron fires it every ten minutes, it loads state, asks whether anyone would plausibly speak right now, usually concludes no, saves, and exits. Nobody is scheduling a quiet period — the quiet is what happens when the answer is no eighty times in a row. Which is also what produces the artifacts you cannot fake by inserting delays: a thread that goes dormant because nobody happened to be awake and interested at the same moment, and gets revived four days later by someone who just got to it.
the timing model
Everything interesting is in deciding whether and who. The model writes the words; the engine decides if there are any.
The 0.02 is the detail I’d point at. Setting a sleeping person’s speaking probability to zero is cleaner, more defensible on paper, and wrong — it produces nights that are perfectly empty, which is its own tell. Real channels have one message at 4am from someone who shouldn’t be awake. The floor is small enough to be rare and non-zero because reality is.
Threads cool on a half-life rather than a timeout, so liveliness decays continuously instead of falling off a cliff, and the busiest thread is usually — but not always — the one that gets the next reply. Fatigue exists because real conversations exhaust their topic before they run out of participants.
the part I got wrong first
One constant controls how strongly the channel sleeps at night, and its correct value turns out to depend on something that has nothing to do with the code: how geographically concentrated the cast is.
Put everyone in one city and the raw ratio between the busiest and quietest hour is roughly 9× — the day/night contrast is already there and barely needs shaping. Spread the same people across many timezones and the ratio collapses to about 2×, because someone is always awake somewhere. Feed that flat curve through the mild exponent and you get a channel that never sleeps; it needs roughly 3.0 to produce visible nights at all.
Getting it wrong doesn’t crash anything. It produces a channel that is subtly, unfixably wrong-feeling, and it is genuinely hard to trace back to one number. So the constant carries the measurement that produced it and the instruction for re-deriving it after any change to where people live — because a magic number without its method is a landmine for whoever touches it next, and that person is usually me.
The same instinct is why a person with no timezone set inherits the machine’s. It looks lazy and it is deliberate: the local clock tracks daylight saving for free, and a hardcoded UTC offset does not — it runs the entire channel an hour off for half the year, silently, in a way no test would flag.
what it isn’t
It is a personal tool that works, not a product, and the cast is 35 invented people. I have not run a blind test on whether the output passes for human conversation, so this page does not claim it does — the timing model is principled and defensible; whether it is convincing is an unmeasured question and stays labelled as one.
Two things I’d flag to anyone running something like this. Secrets live in one gitignored file that the program reads itself, so they stay out of shell history, out of the crontab, and out of a screenshot — and it warns on startup if that file is readable by anyone else. And a preflight check exists specifically to catch the failure that otherwise surfaces days later: a perfectly valid token, a real channel, and a bot that was never invited to it. It exits nonzero so the scheduled install can be gated on it. Both are the same lesson as everything else here — the failure that hurts is the one that looks like success.
why it’s on this site
Because the whole thing is one honesty argument in code. The easy version fakes the gaps and looks right in a screenshot; the honest version gives up control of the clock and accepts that most runs produce nothing. It is the same reason the correspondent layer is stamped Demo rather than Live: a synthetic world is only useful if you are exact about which parts of it are real.