What happened
A prompt job scheduled at 18:00 America/Los_Angeles reads a channel with
read_channel(limit: 200) and is told by its skill to keep the messages from
the last 24 hours. Computing that window is left entirely to the model, and
nothing in the turn makes it deterministic:
- The pod runs UTC. The job fires at 01:00 UTC (02:00 in winter) on the next
calendar day, so the date the harness injects is already tomorrow relative to
the job's own timezone.
- The skill body says "it is 18:00 Pacific."
The model combined the two into "18:00 Pacific on <the UTC date>" — 24 hours
in the future — so its window opened after the newest message in the channel.
From its own transcript:
Current date is 2026-09-14 (from the system reminder)... The current time is
2026-09-14 18:00 PT = 2026-09-15 01:00 UTC. Last 24 hours = from 2026-09-14
01:00 UTC onwards. The most recent message is 2026-09-14 00:02:44 UTC. There
are NO messages in the last 24 hours.
The read itself was perfect: 200 messages, more: false, of which 99 were
inside the true window, from 10 distinct authors. The digest posted "nothing
reported in the last 24h" over one of the busiest days the channel has had.
Nothing anywhere reported a problem. read_channel returned outcome=ok, the
turn returned PASS, and the post succeeded. The only artifact is a summary
that says the opposite of the truth.
It is also not a one-off. At that fire time the UTC date is always a day ahead
of the job's own timezone, so the trap is armed every single night; it happened
to land on the run after the digest was pointed at a channel people read.
Why steering cannot fix this properly
We patched the skill to run date -u -d '24 hours ago' +%FT%TZ and use the
output verbatim, which removes the arithmetic and the timezone conversion. Two
things it cannot remove:
- The model still hand-filters the whole read. At 200 messages the result
is ~70KB, which spills to a tool-result file, and the base image has no jq
and no python3 — so the model slices it with grep. That consumed most of
an 88-second turn to produce one line.
limit is a count, not a window. The Nostr adapter returns more: false
unconditionally (one REQ, and EOSE means the relay sent everything it had
for that filter). So a channel busier than limit inside the window still
reports as fully read, and authors that were simply never paged back to
render as quiet. A count-bounded read cannot answer a time-bounded question.
Both are the same root cause: the window is reconstructed by the model instead
of being enforced by the host.
Proposed fix
Give read_channel a time window, owned by the host clock:
read_channel(surface, channel, limit?, withinHours?)
withinHours rather than a caller-supplied since timestamp on purpose — the
caller then never produces a clock reading at all, so there is no second place
to misread one. The model cannot get the window wrong because it never sees a
message outside it.
Both adapters already have the primitive:
| Adapter |
Mechanism |
Today |
| Nostr |
Filter.since (epoch seconds) |
already set on subscription filters; readChannel's query does not use it |
| Slack |
conversations.history oldest |
hardcoded "0" |
Worth having alongside it: make more answer the question the caller is
actually asking — "did I see the whole window" — rather than "did the relay
finish this filter". With a window that distinction becomes meaningful and
checkable.
Knock-on benefits beyond correctness: the payload shrinks to what is in scope
(~99 messages instead of 200 here), which keeps it inline instead of spilling to
a file the model has to slice by hand, and drops the turn's token cost.
Observed on toolkit 0.5.2.
What happened
A
promptjob scheduled at18:00 America/Los_Angelesreads a channel withread_channel(limit: 200)and is told by its skill to keep the messages fromthe last 24 hours. Computing that window is left entirely to the model, and
nothing in the turn makes it deterministic:
calendar day, so the date the harness injects is already tomorrow relative to
the job's own timezone.
The model combined the two into "18:00 Pacific on
<the UTC date>" — 24 hoursin the future — so its window opened after the newest message in the channel.
From its own transcript:
The read itself was perfect: 200 messages,
more: false, of which 99 wereinside the true window, from 10 distinct authors. The digest posted "nothing
reported in the last 24h" over one of the busiest days the channel has had.
Nothing anywhere reported a problem.
read_channelreturnedoutcome=ok, theturn returned
PASS, and the post succeeded. The only artifact is a summarythat says the opposite of the truth.
It is also not a one-off. At that fire time the UTC date is always a day ahead
of the job's own timezone, so the trap is armed every single night; it happened
to land on the run after the digest was pointed at a channel people read.
Why steering cannot fix this properly
We patched the skill to run
date -u -d '24 hours ago' +%FT%TZand use theoutput verbatim, which removes the arithmetic and the timezone conversion. Two
things it cannot remove:
is ~70KB, which spills to a tool-result file, and the base image has no
jqand no
python3— so the model slices it withgrep. That consumed most ofan 88-second turn to produce one line.
limitis a count, not a window. The Nostr adapter returnsmore: falseunconditionally (one REQ, and EOSE means the relay sent everything it had
for that filter). So a channel busier than
limitinside the window stillreports as fully read, and authors that were simply never paged back to
render as quiet. A count-bounded read cannot answer a time-bounded question.
Both are the same root cause: the window is reconstructed by the model instead
of being enforced by the host.
Proposed fix
Give
read_channela time window, owned by the host clock:withinHoursrather than a caller-suppliedsincetimestamp on purpose — thecaller then never produces a clock reading at all, so there is no second place
to misread one. The model cannot get the window wrong because it never sees a
message outside it.
Both adapters already have the primitive:
Filter.since(epoch seconds)readChannel's query does not use itconversations.historyoldest"0"Worth having alongside it: make
moreanswer the question the caller isactually asking — "did I see the whole window" — rather than "did the relay
finish this filter". With a window that distinction becomes meaningful and
checkable.
Knock-on benefits beyond correctness: the payload shrinks to what is in scope
(~99 messages instead of 200 here), which keeps it inline instead of spilling to
a file the model has to slice by hand, and drops the turn's token cost.
Observed on toolkit 0.5.2.