Skip to content

fix(server): keep a dated snooze on awake threads; keep a scheduled model until a default turn takes it - #97

Open
chrisohearn wants to merge 1 commit into
r4/mcp-tool-reliabilityfrom
r4/w4-tools-followups
Open

chrisohearn wants to merge 1 commit into
r4/mcp-tool-reliabilityfrom
r4/w4-tools-followups

Conversation

@chrisohearn

Copy link
Copy Markdown
Owner

Follow-up to #94 (ledger r4/w4-tools), from its delta review. This PR is stacked on #94's branch, and GitHub retargets it to integration/mcp-events-20261109 when #94 merges. Its diff is one commit.

In every acceptance check, <deploy_ms> is the deploy time in epoch milliseconds.

1. A default agent send no longer erases a dated snooze on a thread that counts as awake

Problem. A thread can count as awake while its snooze is still stored: a pending request, a waiting guest message, or a run that failed after the snooze. isSnoozed then returns false. #94 used that result for two decisions: whether to hold the message, and whether to end the snooze. So a default agent send (respectSnooze: true) to such a thread cleared the stored snooze for good. This is the same class as the incident: a dated hold ended by an agent that never meant to end it.

Change. The two decisions are now separate in Orchestrator.ts:

  • keepsSnooze: the sender respects the snooze, the message is not an automatic wake, the stored date is in the future, and it isn't a usage-limit snooze. When true, the stored snooze is never cleared.
  • respectsSnooze: keepsSnooze, the thread has run before, and isSnoozed is true. When true, the turn is held as before.

A thread that counts as awake, or has never run, gets the message at once and keeps its dated hold. t3_thread_send reports this as a delivered warning: "It counts as awake for now (a pending request or an error), so the message was delivered; the snooze stays."

Test. runtimeLayer.test.ts, "delivers to a snoozed thread that counts as awake without ending its snooze": a background wake fails after the snooze, then a respect-snooze send starts at once and the stored snoozedUntil stays 2099-01-01. On #94's head the snooze is cleared.

2. A scheduled model change stays until a default-model turn takes it

Problem. #94 cleared the schedule once no queued default-model run from before the change remained. When only explicit picks were left ahead, the change was dropped before any default turn used it. The explicit pick then ran and moved the thread to its model, so later default messages got the older pick instead of the scheduled model.

Change. threadAtRunStart now clears the schedule only in two cases:

  • a default-model turn starts and nothing queued before the change still waits, explicit picks included;
  • a turn starts that names a model after the change: an explicit pick queued later, or a new non-server message with modelSelection. That newer instruction replaces the change.

An explicit pick queued before the change runs with its own model and leaves the change pending. Server continuations that repeat an earlier run's model don't count as naming one.

Test. The scheduled-change case in runtimeLayer.test.ts now runs to the end: two default runs and one explicit pick queued before the change, and one default message after it.

  • The change survives the default runs while the pick still waits.
  • The pick starts with its own model, and the change stays pending.
  • The next default turn starts with the scheduled model and clears the change.

On #94's head the change is gone after the second boundary.

3. Test: a later completed run releases snooze-held turns only once the thread is awake

runtimeLayer.test.ts, "a later completed run releases snooze-held messages only once the thread is awake":

  • a background wake completes while the thread sleeps, and the held turn stays held and deferred;
  • after an unsnooze, the next completed run releases it, as a usage limit's resume does;
  • the released turn loses snoozeDeferred.

This is the path a usage-limit resume takes: handleRunUpdate runs releaseUnattendedHolds.

Acceptance (read-only)

sqlite3 -readonly ~/.t3/userdata/statev2.sqlite "select thread_id, json_extract(payload_json,'$.snoozedUntil') from orchestration_v2_projection_threads where json_extract(payload_json,'$.snoozedUntil') > strftime('%Y-%m-%dT%H:%M:%fZ','now') and deleted_at is null"
sqlite3 -readonly ~/.t3/userdata/statev2.sqlite "select json_extract(payload_json,'$.scheduledModelSelection') from orchestration_v2_projection_threads where thread_id like 'mcp:launch:be52150e%'"

Pass:

  • threads with a dated snooze keep it across agent sends after deploy, with no thread.unsnoozed event whose actor is an MCP send with respectSnooze left at its default;
  • a schedule on the inbox clears only after a default-model run starts with it.

Notes

  • Docs: decision record 0017, docs/user/outside-agents.md and docs/orchestration-v2/orchestrator-mcp-server.md are updated for both changes.

  • Agent context: the new warning text is in a tool result, not the tool list, so the budget is unchanged. No migration.

  • Checks: under the resource rule, I ran only focused checks, under the shared lock with two workers:

    • the apps/server typecheck;
    • the five touched runtimeLayer cases (the two new tests, the scheduled change, the respect-snooze hold, and the post-only case);
    • OrchestratorMcpService, SnoozeDeferredDelivery, Orchestrator.control-reads, core and AgentContextBudget (158 tests).

    Hosted CI is the full gate.

🤖 Generated with Claude Code

… model for default turns

From the PR #94 delta review:
- A default agent send to a thread that counted as awake only because of
  a pending request, guest message or error erased its dated snooze. It
  now delivers and keeps the stored snooze. So does a thread that has
  never run. The send's warning says it was delivered.
- A scheduled model change was cleared once only explicit picks were
  ahead of it, so a later default message got the older pick. It now
  stays until a default-model turn starts with it, after everything
  queued before it, and a model named after the change replaces it.
- Tests: a later completed run, such as a usage limit's resume,
  releases snooze-held turns only once the thread is awake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-13T04:57:12.838290Z
We couldn’t complete this review. Comment @codex review to try again.
a267c97 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants