Everyone who has ever kept a timesheet knows the Friday afternoon ritual: staring at a week you definitely worked, trying to remember which of it was the migration and which was the bug that ate Tuesday. The hours were real. The record of which task they belonged to was never written down, because writing it down meant stopping work to press a button.
TimerOS has tracked the hours automatically since launch. What it could not do until this month was say what they were for. That shipped across v1.3 — the desktop app now matches tracked time to your own open tasks while you work, on your machine. This post is about how it decides, and more usefully, about when it decides to say nothing at all.
Why guessing is the failure mode
The obvious way to build this is to take whatever the person is looking at, find the closest-sounding task, and book the time. It demos beautifully and it is quietly corrosive, because a timesheet that is 80% right is worse than one that is 60% filled. The 20% is invisible — it looks exactly like the correct rows — so you either audit everything, which costs more than filling it in did, or you bill a client for work that happened on a different job.
A wrong booking is more expensive than a missing one, because you cannot see it.
So the matcher is built to abstain. It has to clear a real evidence bar before it books anything, and when it cannot, the time simply stays productive and unattributed. Unassigned time is a normal outcome here, not a failure state, and the app does not nag you about it.
What it actually reads
One signal: the title of the window you are looking at. Not the application name, not the browser address. Those two feed the separate productive-versus-idle classification and carry no weight in matching a task — the host parameter is passed into the scoring function and deliberately ignored.
That title is tokenised and scored against a corpus built from your own open tasks and subtasks. Words from a task’s title count at full weight; words from its description count at 40%, and can only ever raise a score, never dilute it. Rarer words count for more than common ones, and about 130 generic words — fix, bug, update, task — are stripped out entirely, so “Fix login bug” and “Fix export bug” are separated by the words that actually differ.
The evidence bar
Before anything is booked, a candidate has to pass all of these:
- Two matching content words — or one that is unique. A single shared generic word can never carry a match. One word will do it only if that word appears in exactly one task in your whole list, which is what makes ticket numbers work.
- A score of 0.45, measured as weighted coverage of the task’s own title words.
- A winning margin of 0.15 over the runner-up. Two similar tasks that score closely produce no booking at all, rather than a coin flip. This is the rule that does the most work.
- Sixty seconds of agreement. The same task has to keep winning across repeated checks spanning a full minute before its first booking. Alt-tabbing through something for fifteen seconds books nothing.
Two thumbs on the scale
Coverage alone ignores everything you already know about the work, so two priors adjust it. A task marked in progress is weighted up by a quarter. And a task’s dates matter: work happening inside its scheduled window gets a modest lift, while a task that is more than a week from starting, or a fortnight past due, is weighted down to 60%. The date is taken from the same trusted clock the shift logic uses, so changing your machine’s timezone does not move it.
Staying still, and letting go
The other half of the problem is not accuracy, it is stability. A matcher that re-decides every five seconds produces a day chopped into ninety fragments, which is technically detailed and practically useless.
So a match latches. Once a task holds, a different task has to out-score it continuously for two full minutes before the booking switches — a quick glance at another ticket does not derail the block you are in. And a latch that has gone thirty minutes without fresh confirmation lets go by itself, so a task you finished at eleven does not keep quietly collecting your afternoon.
There is a second, quieter channel. When you are inside TimerOS itself with a task, incident or change request open on screen for twenty seconds, that counts as context directly — no guessing required, because you are literally looking at the thing. It is the only route by which incidents and change requests get attributed at all.
What leaves your machine
All of the above happens in the desktop app, in Rust, on your own computer. The corpus is built locally from your assigned work, the scoring is local, the latch is local.
What syncs is a map of task IDs to second counts — the same shape as the rest of the day’s hours. Window titles are not transmitted, because the thing that needed to read them already finished reading them before anything left the machine. That is the same architecture as the classification itself, which is written up separately.
Worth saying plainly: none of this is AI. There is no model in the matching path, no inference server, nothing that needs a GPU. It is a deterministic scoring function — the same input produces the same output every time, and it behaves identically whether your tracking mode is Automatic or Rule-based. That is not modesty. Determinism is why we can tell you the exact thresholds in this post, and why you can predict what it will do.
How this differs from auto-tracking elsewhere
Background capture is not new, and we should not pretend it is. Both Toggl Track and Clockify record activity without a timer running. The difference is what arrives at the other end.
Neither approach is obviously right. Record-and-review never books anything wrong, because it never books anything — the cost lands on you on Friday. Matching moves that cost to a machine and accepts that it will sometimes be wrong, which is why the review panel and the undo matter more than the matcher does.
What we deliberately did not do
- No project-level fallback. The first version booked to the project when it could not identify a task. We removed that five hours later, in v1.3.1. “Some time, on roughly the right project” is exactly the plausible-looking noise that makes a timesheet untrustworthy.
- No nagging. There is an Unassigned row showing time that matched nothing, with a one-click assign if you want it. It does not turn red, and it does not follow you around.
- No silent overwriting. Tracked hours appear on a task on their own, but a number you typed always wins over a number that was derived. The task shows which it is.
- No route to your invoice. Attributed hours do not flow into billing yet. Given that a wrong booking would become a wrong invoice, that ordering is deliberate — the matcher earns billing later, once beta has taught us where it is weak.
The goal was never a timesheet that fills itself in perfectly. It was to move the work from remembering to checking — because you can check a list in two minutes on Friday, and you were never going to remember Tuesday.