Skip to content

read_channel has no time window, so a scheduled digest reconstructs "the last 24h" from a UTC pod clock — and silently reported an empty channel #92

Description

@shrimalmadhur

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions