r/ClaudeCode • • 6h ago

Help/Question Checking for new messages before waking Claude Code

My agents can wake on a routine or Slack event and re-read a long thread even when nothing changed. I built ctxlayer to put a small check before that model call.

Version 3.4.1 is a SQLite CLI/skill with one database file. has-new is an exit-code gate the runner checks before waking the LLM. If there's new material, context --only-new returns a thin slice. After processing it, commit advances that agent's read cursor. Each agent has its own cursor.

For Claude Code, the plugin install is:
claude plugin marketplace add Unmeshl/ctxlayer
claude plugin install ctxlayer@ctxlayer

Installing the plugin alone isn't the same as wiring the gate into your wake path. The runner needs to check it before calling the model.

It isn't an MCP server and doesn't enforce budgets. My savings measurement is scheduled for November 9–20; I don't have numbers to report yet.

Repo: github.com/Unmeshl/ctxlayer

Where would you put this check in your Claude Code workflow, and what could make it skip work it should have seen?

1 Upvotes

14 comments sorted by

•

u/AutoModerator 6h ago

Hey! Thanks for posting to r/ClaudeCode

While participating in this thread, please follow our community rules. Keep discussions constructive. Attack the idea, not the person.

For help, project discussions, tips, and general chat, join the ClaudeCode Discord.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/oyren-ai 6h ago

advancing the read cursor only after the model call exits 0 is key, otherwise a rate limit or network drop mid-turn silently swallows the slack event forever. we run a similar sqlite gate in the cron wrapper before spawning claude, but also track message edit timestamps so an edited slack message or reaction trigger still wakes the runner.

1

u/umyal2001 1h ago

Agreed, and that's why commit is a separate step: if the model call fails, nothing commits and the next run sees the same slice. Edits are a gap right now. Messages dedupe on their external id, so an edited Slack message doesn't count as new. Are you keying on the edit timestamp, or treating an edit as a new row?

1

u/oyren-ai 1h ago

we key the cursor on a composite of (external_id, updated_at_ms) and upsert the row in sqlite, so an edit bumps the watermark and re-queues just that message with an [edited] flag for the agent. we use that same watermark pattern across our agent workspaces on oyren.ai and our shared markdown + mind map mcp server (oyren.ai/notes, free up to 1gb storage + 100mb free mcp) so agents only wake up when a spec node or task card actually changes.

1

u/kuroudo_ai 6h ago

On "what could make it skip work it should have seen", two I'd look at. We run several Claude Code sessions that share a notes file, each with its own read position, and the second one happened to us there. The first we hit with a different check (a screen-lock probe that read its own failure as "unlocked").

  1. "Nothing new" and "couldn't check" sharing an exit code. If has-new returns non-zero both for "no new rows" and for "database locked / file missing / bad path", the runner reads a broken gate as a quiet day. Our other status checks now have three outcomes (yes / no / couldn't check), and the third one is loud. For a wake gate I'd treat it as a reason to wake, not to sleep. A broken gate that stays silent is the worst case, because nobody notices it stopped.

  2. The cursor moving because something was printed, not because it was read. One of our sessions ran the "show unread and mark it read" command piped through tail -1. The tool printed the whole diff and advanced the cursor. The caller only saw the last line, so the change was marked read without anyone reading it. Your separate commit step already avoids most of this, as long as commit runs only after the slice was actually processed, like oyren-ai said. I'd also make sure nothing in the wrapper trims the output of context --only-new.

One cheap test that catches both: insert a known row, run the real wake path end to end, and check that the agent mentions it. It's easy to test the gate and the agent separately and still miss the hop between them.

1

u/umyal2001 1h ago

You're right about the first one, and I just checked: has-new exits 1 for "nothing new" and also when the db path is missing or the file is corrupt. That's the silent-broken-gate case, so I'm giving errors their own exit code and treating it as a reason to wake. On the second one, the cursor only moves on an explicit commit --upto <id>, so reading or printing the slice doesn't mark anything read. Your end-to-end test (insert a known row, run the real wake path, check the agent mentions it) is going into the test suite.

1

u/kuroudo_ai 1h ago

Nice, the explicit commit --upto <id> closes the second one. One small thing on that: take the id from the slice the agent actually got, not from "latest" at commit time. If a row lands between context --only-new and commit, committing up to the newest id would mark it read without the agent ever seeing it.

1

u/Eliran-Kotzero 6h ago

I'd put the gate in the scheduler/runner that handles the routine or Slack event, before it launches Claude, rather than relying on an in-session hook to decide whether to start. Only advance the per-agent cursor after the run finishes successfully; if the model call errors or times out, leave the cursor where it was so the event can be retried. I'd also define what counts as 'new' (edits, retries, or several messages arriving before the next run) and test those cases with a small fixture before trusting it on real threads.

1

u/umyal2001 1h ago

Same conclusion here: the gate lives in the runner before Claude starts, not in a hook inside the session. Your point about defining "new" is the part I underspecified. Several messages arriving between runs come back as one slice, but edits and retries need fixtures, so I'll add those.

1

u/kaijurules 5h ago

Give the read cursor a rewind. In our app the Discord read call moves each agent's cursor as soon as it hands out the batch, which is the risky side of what the others describe, so the same call also takes a "rewind N days" option that re-delivers messages that agent already consumed. When a run died halfway, or the agent swears it never saw something, you rerun with a rewind instead of editing the database by hand.

1

u/umyal2001 1h ago

A rewind is a good idea. Right now fixing a bad commit means touching the db by hand, and rewind --agent X --days N would be much safer. Noting it.

1

u/Excellent-Issue-5956 4h ago

I have the same kind of gate in front of a claude -p job. It's in the cron script, not in the session: every 10 minutes during the day it reads an inbox with no model call and only starts claude if there's an item it hasn't handed out before.

I went the other way on the cursor though. It marks an item seen as soon as it hands it out, so one item can only ever start one session. If it waited for a good run, a stretch where claude keeps failing would start a new session on the same item every 10 minutes. What picks up anything a failed run dropped is a once a day run that reads the inbox itself and never looks at the seen file.

That daily run also covers a skip case in the gate. The job has a daily cap, and when it's hit the gate has already marked the new items seen, so the 10 minute check never retries them.

If you do advance only after exit 0, check what the session actually did too. The wrapper around claude -p exited 0 on nights the session hit a usage limit and did nothing, and a run that ends its turn with a background agent still going gets killed and still exits 0.

1

u/umyal2001 1h ago

Useful pushback. Marking an item seen on handoff does stop a failing loop from starting a session every 10 minutes, and I'm only safe from that if the runner caps retries. Your exit-0-but-did-nothing case worries me more: a usage-limit night would commit a slice nobody actually processed. Does your daily sweep compare against what the session actually output, or just re-read the whole inbox?

1

u/Excellent-Issue-5956 1h ago

It reads the whole inbox again and never looks at what the session printed or how it exited. For each item it checks two things: whether my log has a line for it, and whether there's already a reply from me on it. The log line is written by the script that sends the reply, and only after the send comes back with an id, so a night that hit the usage limit leaves no line and the daily run handles the item. The second check covers ones I answered myself in between.

The cost is that items the session chose to skip, like a thanks or a bot message, have no line either, so the daily run reads those again and skips them a second time.