# Files changed · PR \#22775 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22775/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(server\): preserve usage-limit recovery across empty wake runs](https://git.cafe/pingdotgg/t3code/pull/22775.md)

Version: 29

Head commit: 824b0ba8ec6ef6999b3da40106f381dc7825db94

Base commit: 8acbefefc51b4d281dcd04108c82baa1ab902b0e

Comparison base: 8acbefefc51b4d281dcd04108c82baa1ab902b0e

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/scripts/usage-limit-web-evidence-record.mjs

Status: added; +102; −0

```diff
@@ -1,0 +1,102 @@
+// Records one seeded usage-limit evidence thread (see usage-limit-web-evidence-seed.ts) at 1440 light.
+//   node usage-limit-web-evidence-record.mjs <pairingUrl> <row> <outDir> <label> <action> [seconds]
+// action: watch (observe only) | resume (click the composer's "Resume thread" if present) |
+// arm (click the banner's "Resume at reset").
+// Writes <row>-<label>.webm plus a .captions.json of UTC-stamped steps. Usage limits are SYNTHETIC.
+import * as NodeModule from "node:module";
+import * as NodeFS from "node:fs";
+import * as NodePath from "node:path";
+
+const [pairingUrl, row, outDir, label, action = "watch", seconds = "20"] = process.argv.slice(2);
+if (!pairingUrl || !row || !outDir || !label) throw new Error("missing arguments");
+const require = NodeModule.createRequire(
+  NodePath.resolve(import.meta.dirname, "../../web/package.json"),
+);
+const { chromium } = require("playwright");
+const manifest = process.env.SEED_HOME
+  ? JSON.parse(NodeFS.readFileSync(`${process.env.SEED_HOME}/fixture-${row}.json`, "utf8"))
+  : null;
+const captions = [];
+const start = Date.now();
+const caption = (text) => {
+  const entry = {
+    t: (Date.now() - start) / 1000,
+    utc: new Date().toISOString(),
+    resetAt: manifest?.resetAt ?? null,
+    text,
+  };
+  captions.push(entry);
+  console.log(JSON.stringify(entry));
+};
+const browser = await chromium.launch();
+const pair = await browser.newContext({ viewport: { width: 1440, height: 900 } });
+const pairPage = await pair.newPage();
+await pairPage.goto(pairingUrl);
+await pairPage.waitForURL((url) => !url.pathname.startsWith("/pair"), { timeout: 180_000 });
+const title = pairPage.getByText(new RegExp(`^${row} `)).first();
+await title.waitFor({ timeout: 60_000 });
+await title.click();
+await pairPage.waitForURL((url) => url.pathname.includes(encodeURIComponent(`evidence:${row}`)), {
+  timeout: 60_000,
+});
+const threadUrl = pairPage.url();
+const storageState = await pair.storageState();
+await pair.close();
+NodeFS.mkdirSync(outDir, { recursive: true });
+const context = await browser.newContext({
+  viewport: { width: 1440, height: 900 },
+  colorScheme: "light",
+  storageState,
+  recordVideo: { dir: outDir, size: { width: 1440, height: 900 } },
+});
+const page = await context.newPage();
+caption(
+  `[${label}] open ${row}: SYNTHETIC usage-limit fixture seeded (reset ${manifest?.resetAt ?? "?"})`,
+);
+await page.goto(threadUrl);
+await page.getByText("Work on this.").first().waitFor({ timeout: 120_000 });
+await page.waitForTimeout(3000);
+const banner = async () =>
+  (await page.getByText("Usage limit reached", { exact: true }).count()) > 0
+    ? "banner shown"
+    : "no banner";
+caption(`[${label}] loaded: ${await banner()}; sidebar row ${row}`);
+if (action === "arm") {
+  const arm = page.getByRole("button", { name: "Resume at reset" });
+  if ((await arm.count()) > 0) {
+    caption(`[${label}] click "Resume at reset"`);
+    await arm.first().click();
+    await page.waitForTimeout(4000);
+    const cancel = await page.getByRole("button", { name: "Cancel auto-resume" }).count();
+    caption(`[${label}] after click: "Cancel auto-resume" ${cancel > 0 ? "shown" : "not shown"}`);
+  } else {
+    caption(`[${label}] "Resume at reset" button absent; nothing to click`);
+  }
+}
+if (action === "resume") {
+  const resume = page.getByRole("button", { name: /Resume thread/i });
+  if ((await resume.count()) > 0) {
+    caption(`[${label}] click "Resume thread"`);
+    await resume.first().click();
+    await page.waitForTimeout(6000);
+    caption(
+      `[${label}] after click: ${await banner()}; continuation message ${(await page.getByText("Continue where you left off").count()) > 0 ? "visible" : "not visible"}`,
+    );
+  } else {
+    caption(`[${label}] "Resume thread" button absent; nothing to click`);
+  }
+}
+const end = Date.now() + Number(seconds) * 1000;
+while (Date.now() < end) {
+  await page.waitForTimeout(5000);
+  caption(`[${label}] ${await banner()}`);
+}
+const video = page.video();
+await context.close();
+const file = NodePath.join(outDir, `${row}-${label}.webm`);
+NodeFS.renameSync(await video.path(), file);
+NodeFS.writeFileSync(
+  file.replace(/\.webm$/, ".captions.json"),
+  JSON.stringify({ browser: browser.version(), threadUrl, captions }, null, 2),
+);
+await browser.close();
```

## apps/server/scripts/usage-limit-web-evidence-seed-all.sh

Status: added; +13; −0

```diff
@@ -1,0 +1,13 @@
+#!/usr/bin/env bash
+# Usage: seed-all.sh <checkout> <home>  (dev server for <home> must be stopped)
+set -euo pipefail
+cd "$1"; H="$2"
+S() { node apps/server/scripts/usage-limit-web-evidence-seed.ts --home "$H" "$@" | grep '"kind":"shell"' | cut -c1-300; }
+S --row W0 --stage init --title "W0 limited, no wake"
+S --row W1 --stage init --title "W1 wake completed before reset"; S --row W1 --stage wake
+S --row W2 --stage init --arm --title "W2 armed, wake completed before reset"; S --row W2 --stage wake
+S --row W4 --stage init --title "W4 resume after wake"; S --row W4 --stage wake
+S --row W5 --stage init --arm --reset-delay-ms "${W5_DELAY:-540000}" --title "W5 armed, crosses reset"; S --row W5 --stage wake
+S --row W8 --stage init --title "W8 user turn before reset"; S --row W8 --stage user
+S --row W7 --stage init --reset-delay-ms 20000 --title "W7 wake completed after reset"
+sleep 23; S --row W7 --stage wake
```

## apps/server/scripts/usage-limit-web-evidence-seed.ts

Status: added; +480; −0

```diff
@@ -1,0 +1,480 @@
+// @effect-diagnostics nodeBuiltinImport:off globalDate:off globalDateInEffect:off globalFetchInEffect:off schemaSyncInEffect:off globalConsole:off preferSchemaOverJson:off - Offline development seed writes synthetic history into a disposable home.
+import * as NodeFS from "node:fs";
+import * as NodeAssert from "node:assert/strict";
+import * as NodeChildProcess from "node:child_process";
+import * as NodePath from "node:path";
+import * as NodeUtil from "node:util";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as DateTime from "effect/DateTime";
+import * as Option from "effect/Option";
+import * as Stream from "effect/Stream";
+import {
+  CommandId,
+  EventId,
+  MessageId,
+  ProjectId,
+  ThreadId,
+  TurnItemId,
+  ProviderInstanceId,
+  ProviderDriverKind,
+  CheckpointId,
+  CheckpointScopeId,
+  type OrchestrationV2ThreadProjection,
+} from "@t3tools/contracts";
+import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
+import type { ProviderInstance } from "@t3tools/provider-core/server/driver";
+import * as ServerConfig from "../src/config.ts";
+import * as Sqlite from "../src/persistence/Sqlite.ts";
+import * as ApplicationEvents from "../src/persistence/OrchestrationEventStore.ts";
+import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts";
+import * as GitWorkflow from "../src/git/GitWorkflowService.ts";
+import * as ProjectService from "../src/project/ProjectService.ts";
+import * as ServerSettings from "../src/serverSettings.ts";
+import * as McpSessionRegistryTestkit from "../src/mcp/McpSessionRegistry.testkit.ts";
+import * as ProviderRegistry from "../src/provider/ProviderInstanceRegistry.ts";
+import * as SourceControlRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts";
+import * as VcsRegistry from "../src/vcs/VcsDriverRegistry.ts";
+import * as VcsProcess from "../src/vcs/VcsProcess.ts";
+import * as RuntimeLayer from "../src/orchestration-v2/runtimeLayer.ts";
+import * as Orchestrator from "../src/orchestration-v2/Orchestrator.ts";
+import * as EventSink from "../src/orchestration-v2/EventSink.ts";
+import * as ProjectionStore from "../src/orchestration-v2/ProjectionStore.ts";
+import * as ProjectStore from "../src/orchestration-v2/ProjectStore.ts";
+import { CodexProviderCapabilitiesV2 } from "../src/orchestration-v2/Adapters/CodexAdapterV2.ts";
+import { limitRecoveryCommand } from "../src/orchestration-v2/UsageLimitRecoveryWorker.ts";
+
+// Seeds SYNTHETIC usage-limit threads into a disposable T3 home for web evidence of the
+// usage-limit recovery banner. Run it from the checkout under test with its dev server stopped:
+//   node apps/server/scripts/usage-limit-web-evidence-seed.ts --home /tmp/<dir>/.t3 --row W1 --stage init
+// Rows: W0 limited only; W1 notification wake completes before reset; W2 as W1 but armed;
+// W4q as W1 plus a held queued message; W7 wake completes after reset (needs --reset-delay-ms
+// small, then stage "wake" after the reset); W8 a user turn completes before reset.
+// Stages: init (source run fails with a synthetic usage_limit), wake (notification run completes),
+// wake-running (notification run left running), finish (complete the newest run), user (user turn
+// completes). The provider is a Codex mock peer configured by init; no real quota is involved.
+const { values } = NodeUtil.parseArgs({
+  options: {
+    home: { type: "string" },
+    row: { type: "string" },
+    stage: { type: "string" },
+    "reset-delay-ms": { type: "string", default: String(30 * 60_000) },
+    arm: { type: "boolean", default: false },
+    "hold-queue": { type: "boolean", default: false },
+    title: { type: "string" },
+  },
+});
+NodeAssert.ok(values.home && values.row && values.stage, "--home, --row and --stage are required");
+const home = NodePath.resolve(values.home);
+NodeAssert.ok(
+  home.startsWith("/tmp/") &&
+    !home.includes("/.t3/userdata/") &&
+    home !== NodePath.resolve(process.env.HOME ?? "~", ".t3"),
+  "Home must be a disposable directory under /tmp, never live T3 state",
+);
+const row = values.row;
+const stage = values.stage;
+const failure = {
+  class: "usage_limit" as const,
+  message: "Plan limit reached. (SYNTHETIC FIXTURE)",
+  code: "usageLimitExceeded",
+  retryable: null,
+};
+const log = (kind: string, data: unknown) =>
+  console.log(
+    JSON.stringify({
+      kind,
+      boundary: "OFFLINE SEED (server stopped)",
+      row,
+      stage,
+      at: new Date().toISOString(),
+      data,
+    }),
+  );
+
+if (stage === "init") {
+  const repoRoot = NodePath.resolve(import.meta.dirname, "../../..");
+  NodeFS.mkdirSync(`${home}/userdata`, { recursive: true });
+  NodeFS.writeFileSync(`${home}/DISPOSABLE`, "Synthetic usage-limit evidence; safe to delete\n");
+  const workspace = `${home}/workspace-${row}`;
+  if (!NodeFS.existsSync(workspace)) {
+    NodeFS.mkdirSync(workspace, { recursive: true });
+    NodeChildProcess.execFileSync("git", ["init", "-q", workspace]);
+    NodeChildProcess.execFileSync("git", [
+      "-C",
+      workspace,
+      "-c",
+      "user.name=Evidence",
+      "-c",
+      "user.email=evidence@localhost",
+      "commit",
+      "--allow-empty",
+      "-qm",
+      "fixture",
+    ]);
+  }
+  const wire = JSON.parse(
+    NodeFS.readFileSync(
+      `${repoRoot}/apps/server/src/provider/testFixtures/codexMultiAgentWire.json`,
+      "utf8",
+    ),
+  );
+  // The dev server reads this through T3_CODEX_COLLAB_SCRIPT; holdTurnOpen keeps a continuation visible.
+  NodeFS.writeFileSync(
+    `${home}/mock.json`,
+    JSON.stringify({ rootThreadId: wire.rootThreadId, notifications: [], holdTurnOpen: true }),
+  );
+  NodeFS.writeFileSync(
+    `${home}/userdata/settings.json`,
+    JSON.stringify({
+      autoResumeLimitedThreads: false,
+      snoozeLimitedThreads: false,
+      providerInstances: {
+        codex: {
+          driver: "codex",
+          enabled: true,
+          displayName: "Synthetic fixture",
+          config: {
+            binaryPath: `${repoRoot}/apps/server/src/provider/testFixtures/codexCollabMockPeer.sh`,
+            homePath: `${home}/codex-home`,
+          },
+        },
+      },
+    }),
+  );
+  NodeFS.mkdirSync(`${home}/codex-home`, { recursive: true });
+}
+
+const config = ServerConfig.layerTest(process.cwd(), home);
+const platform = Layer.merge(
+  NodeServices.layer,
+  Layer.mock(SourceControlRegistry.SourceControlProviderRegistry)({
+    resolveLink: () => Effect.die("unused"),
+  }),
+);
+const modelSelection = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" };
+const driver = ProviderDriverKind.make("codex");
+const instance = {
+  instanceId: modelSelection.instanceId,
+  driverKind: driver,
+  continuationIdentity: { driverKind: driver, continuationKey: "codex:test" },
+  displayName: "Synthetic Codex",
+  enabled: true,
+  snapshot: { getSnapshot: Effect.succeed({}) } as unknown as ProviderInstance["snapshot"],
+  textGeneration: {} as ProviderInstance["textGeneration"],
+  orchestrationAdapter: {
+    instanceId: modelSelection.instanceId,
+    driver,
+    getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
+    planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),
+    openSession: () => Effect.die("offline fixture must not open a provider"),
+  } as ProviderInstance["orchestrationAdapter"],
+} satisfies ProviderInstance;
+const registry = Layer.succeed(ProviderRegistry.ProviderInstanceRegistry, {
+  getInstance: () => Effect.succeed(instance),
+  listInstances: Effect.succeed([instance]),
+  listUnavailable: Effect.succeed([]),
+  streamChanges: Stream.empty,
+  subscribeChanges: Effect.never,
+});
+const vcs = VcsRegistry.layer.pipe(
+  Layer.provide(VcsProcess.layer),
+  Layer.provide(config),
+  Layer.provide(platform),
+);
+const layers = Layer.mergeAll(
+  RuntimeLayer.layer,
+  RuntimeLayer.layerEventSink,
+  ProjectionStore.layer,
+  ProjectStore.layer,
+  ApplicationEvents.layer,
+).pipe(
+  Layer.provide(McpSessionRegistryTestkit.layer),
+  Layer.provide(McpProviderSessions.layer),
+  Layer.provide(Sqlite.layerConfig),
+  Layer.provide(CheckpointStore.layer.pipe(Layer.provide(vcs))),
+  Layer.provide(ServerSettings.layerTest()),
+  Layer.provide(registry),
+  Layer.provide(
+    Layer.mock(GitWorkflow.GitWorkflowService)({
+      pruneWorktrees: () => Effect.void,
+      createWorktree: () => Effect.die("unused"),
+    }),
+  ),
+  Layer.provide(
+    Layer.mock(ProjectService.ProjectService)({ getById: () => Effect.succeed(Option.none()) }),
+  ),
+  Layer.provide(config),
+  Layer.provide(platform),
+);
+
+const threadId = ThreadId.make(`evidence:${row}`);
+const projectId = ProjectId.make(`evidence:project:${row}`);
+const manifestPath = `${home}/fixture-${row}.json`;
+
+await Effect.runPromise(
+  Effect.gen(function* () {
+    const orchestrator = yield* Orchestrator.OrchestratorV2;
+    const sink = yield* EventSink.EventSinkV2;
+    const projections = yield* ProjectionStore.ProjectionStoreV2;
+    const write = Effect.fnUntraced(function* (events: Parameters<typeof sink.write>[0]["events"]) {
+      log(
+        "synthetic-events",
+        events.map((event) => event.type),
+      );
+      yield* sink.write({ events });
+    });
+    const completeRun = Effect.fnUntraced(function* (
+      run: OrchestrationV2ThreadProjection["runs"][number],
+      text: string,
+    ) {
+      const now = yield* DateTime.now;
+      const base = {
+        threadId,
+        runId: run.id,
+        nodeId: run.rootNodeId,
+        providerThreadId: null,
+        providerTurnId: null,
+        nativeItemRef: null,
+        parentItemId: null,
+        status: "completed" as const,
+        title: null,
+        startedAt: now,
+        completedAt: now,
+        updatedAt: now,
+      };
+      yield* write([
+        {
+          id: EventId.make(`evidence:output:${row}:${run.id}`),
+          type: "turn-item.updated",
+          threadId,
+          occurredAt: now,
+          payload: {
+            ...base,
+            id: TurnItemId.make(`evidence:output:${row}:${run.id}`),
+            ordinal: 2,
+            type: "assistant_message",
+            messageId: MessageId.make(`evidence:output:${row}:${run.id}`),
+            text,
+            streaming: false,
+          },
+        },
+        {
+          id: EventId.make(`evidence:checkpoint:${row}:${run.id}`),
+          type: "turn-item.updated",
+          threadId,
+          occurredAt: now,
+          payload: {
+            ...base,
+            id: TurnItemId.make(`evidence:checkpoint:${row}:${run.id}`),
+            ordinal: 3,
+            type: "checkpoint",
+            checkpointId: CheckpointId.make(`evidence:checkpoint:${row}:${run.id}`),
+            scopeId: CheckpointScopeId.make(`evidence:scope:${row}:${run.id}`),
+            files: [],
+          },
+        },
+        {
+          id: EventId.make(`evidence:completed:${row}:${run.id}`),
+          type: "run.updated",
+          threadId,
+          occurredAt: now,
+          payload: {
+            ...run,
+            status: "completed",
+            startedAt: run.startedAt ?? now,
+            completedAt: now,
+          },
+        },
+      ]);
+    });
+    const dispatchFollowUp = Effect.fnUntraced(function* (kind: "notification" | "user") {
+      yield* orchestrator.dispatch({
+        type: "message.dispatch",
+        commandId: CommandId.make(`evidence:${kind}:${row}`),
+        threadId,
+        messageId: MessageId.make(`evidence:${kind}:${row}`),
+        text:
+          kind === "notification"
+            ? "Background task finished; no further work is needed."
+            : "Summarise what you changed so far.",
+        ...(kind === "notification"
+          ? {
+              notification: {
+                source: { kind: "background_task" as const },
+                outcome: "completed" as const,
+                summary: "Background task finished",
+              },
+            }
+          : {}),
+        attachments: [],
+        dispatchMode: { type: "queue_after_active" },
+        createdBy: kind === "notification" ? "agent" : "user",
+        creationSource: kind === "notification" ? "provider" : "web",
+      });
+      return (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)!;
+    });
+
+    if (stage === "init") {
+      const createdAt = DateTime.formatIso(yield* DateTime.now);
+      yield* (yield* ProjectStore.ProjectStoreV2).apply({
+        sequence: 0,
+        eventId: EventId.make(`evidence:project:${row}`),
+        aggregateKind: "project",
+        aggregateId: projectId,
+        occurredAt: createdAt,
+        commandId: null,
+        causationEventId: null,
+        correlationId: null,
+        metadata: {},
+        type: "project.created",
+        payload: {
+          projectId,
+          title: `Usage-limit evidence ${row}`,
+          workspaceRoot: `${home}/workspace-${row}`,
+          defaultModelSelection: modelSelection,
+          scripts: [],
+          createdAt,
+          updatedAt: createdAt,
+        },
+      });
+      yield* orchestrator.dispatch({
+        type: "thread.create",
+        commandId: CommandId.make(`evidence:create:${row}`),
+        threadId,
+        projectId,
+        title: values.title ?? `${row} synthetic usage limit`,
+        modelSelection,
+        runtimeMode: "full-access",
+        interactionMode: "default",
+        branch: null,
+        worktreePath: null,
+        createdBy: "user",
+        creationSource: "web",
+      });
+      yield* orchestrator.dispatch({
+        type: "message.dispatch",
+        commandId: CommandId.make(`evidence:source:${row}`),
+        threadId,
+        messageId: MessageId.make(`evidence:source:${row}`),
+        text: "Work on this.",
+        attachments: [],
+        dispatchMode: { type: "defer_start" },
+        createdBy: "user",
+        creationSource: "web",
+      });
+      const source = (yield* orchestrator.getThreadProjection(threadId)).runs[0]!;
+      const now = yield* DateTime.now;
+      const resetAt = new Date(
+        DateTime.toEpochMillis(now) + Number(values["reset-delay-ms"]),
+      ).toISOString();
+      yield* write([
+        {
+          id: EventId.make(`evidence:failed:${row}`),
+          type: "run.updated",
+          threadId,
+          occurredAt: now,
+          payload: { ...source, status: "failed", completedAt: now },
+        },
+        {
+          id: EventId.make(`evidence:error:${row}`),
+          type: "turn-item.updated",
+          threadId,
+          occurredAt: now,
+          payload: {
+            id: TurnItemId.make(`evidence:error:${row}`),
+            type: "error",
+            threadId,
+            runId: source.id,
+            nodeId: source.rootNodeId,
+            providerThreadId: null,
+            providerTurnId: null,
+            nativeItemRef: null,
+            parentItemId: null,
+            ordinal: 2,
+            status: "failed",
+            title: "Usage limit reached",
+            startedAt: now,
+            completedAt: now,
+            updatedAt: now,
+            failure: { ...failure, resetAt },
+          },
+        },
+      ]);
+      if (values["hold-queue"]) {
+        yield* orchestrator.dispatch({
+          type: "message.dispatch",
+          commandId: CommandId.make(`evidence:queue:${row}`),
+          threadId,
+          messageId: MessageId.make(`evidence:queue:${row}`),
+          text: "Run after recovery.",
+          attachments: [],
+          dispatchMode: { type: "queue_after_active" },
+          createdBy: "user",
+          creationSource: "web",
+        });
+        const queued = (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)!;
+        yield* write([
+          {
+            id: EventId.make(`evidence:held:${row}`),
+            type: "run.updated",
+            threadId,
+            occurredAt: now,
+            payload: { ...queued, status: "queued", queuePosition: 1, queueHeld: true },
+          },
+        ]);
+      }
+      if (values.arm) {
+        const candidate = (yield* projections.getLimitRecoveryCandidates({
+          now,
+          autoResume: true,
+          snooze: false,
+        })).find((c) => c.id === threadId)!;
+        const arm = limitRecoveryCommand(candidate, true, DateTime.toEpochMillis(now), false);
+        if (arm?.type !== "thread.metadata.update" || !arm.limitRecovery)
+          throw new Error("Missing arm");
+        yield* orchestrator.autoArmLimitRecovery({
+          commandId: arm.commandId,
+          threadId,
+          limitRecovery: arm.limitRecovery,
+        });
+      }
+      NodeFS.writeFileSync(
+        manifestPath,
+        JSON.stringify({ row, threadId, sourceRunId: source.id, resetAt }),
+      );
+    }
+    if (stage === "wake") {
+      yield* completeRun(yield* dispatchFollowUp("notification"), "No further work is needed.");
+    }
+    if (stage === "wake-running") {
+      const wake = yield* dispatchFollowUp("notification");
+      const now = yield* DateTime.now;
+      yield* write([
+        {
+          id: EventId.make(`evidence:running:${row}`),
+          type: "run.updated",
+          threadId,
+          occurredAt: now,
+          payload: { ...wake, status: "running", startedAt: now, completedAt: null },
+        },
+      ]);
+    }
+    if (stage === "finish") {
+      const runs = (yield* orchestrator.getThreadProjection(threadId)).runs;
+      yield* completeRun(runs.at(-1)!, "No further work is needed.");
+    }
+    if (stage === "user") {
+      yield* completeRun(
+        yield* dispatchFollowUp("user"),
+        "Here is a summary of the changes so far.",
+      );
+    }
+    const shell = (yield* orchestrator.getShellSnapshot()).threads.find((t) => t.id === threadId);
+    log("shell", {
+      latestRunId: shell?.latestRunId ?? null,
+      limitRecovery: shell?.limitRecovery ?? null,
+    });
+  }).pipe(Effect.provide(layers), Effect.scoped),
+);
```

## apps/server/scripts/usage-limit-web-evidence-stills.mjs

Status: added; +65; −0

```diff
@@ -1,0 +1,65 @@
+// Captures web stills of the seeded usage-limit evidence threads (see usage-limit-web-evidence-seed.ts).
+//   node usage-limit-web-evidence-stills.mjs <pairingUrl> <outDir> <label> [rows]
+// Resolves playwright from apps/web. Every usage-limit failure shown is a SYNTHETIC fixture.
+import * as NodeModule from "node:module";
+import * as NodeFS from "node:fs";
+import * as NodePath from "node:path";
+
+const [pairingUrl, outDir, label, rowList = "W0,W1,W2,W7,W8"] = process.argv.slice(2);
+if (!pairingUrl || !outDir || !label)
+  throw new Error("usage: <pairingUrl> <outDir> <label> [rows]");
+const require = NodeModule.createRequire(
+  NodePath.resolve(import.meta.dirname, "../../web/package.json"),
+);
+const { chromium } = require("playwright");
+const rows = rowList.split(",");
+const variants = [
+  { width: 1440, height: 900, theme: "light" },
+  { width: 1440, height: 900, theme: "dark" },
+  { width: 390, height: 844, theme: "light" },
+  { width: 390, height: 844, theme: "dark" },
+];
+NodeFS.mkdirSync(outDir, { recursive: true });
+const browser = await chromium.launch();
+const origin = new URL(pairingUrl).origin;
+const pairContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
+const page = await pairContext.newPage();
+await page.goto(pairingUrl);
+await page.waitForURL((url) => !url.pathname.startsWith("/pair"), { timeout: 180_000 });
+const urls = {};
+for (const row of rows) {
+  const title = page.getByText(new RegExp(`^${row} `)).first();
+  await title.waitFor({ timeout: 60_000 });
+  await title.click();
+  await page.waitForURL((url) => url.pathname.includes(encodeURIComponent(`evidence:${row}`)), {
+    timeout: 60_000,
+  });
+  urls[row] = page.url();
+}
+const storageState = await pairContext.storageState();
+await pairContext.close();
+const env = { browser: `chromium ${browser.version()}`, node: process.version, origin, label };
+const shots = [];
+for (const variant of variants) {
+  const context = await browser.newContext({
+    viewport: { width: variant.width, height: variant.height },
+    colorScheme: variant.theme,
+    storageState,
+  });
+  const tab = await context.newPage();
+  for (const row of rows) {
+    await tab.goto(urls[row]);
+    await tab.waitForLoadState("networkidle").catch(() => {});
+    await tab.waitForTimeout(2500);
+    const file = NodePath.join(outDir, `${row}-${variant.width}-${variant.theme}-${label}.png`);
+    await tab.screenshot({ path: file });
+    shots.push({ row, ...variant, file, at: new Date().toISOString() });
+  }
+  await context.close();
+}
+NodeFS.writeFileSync(
+  NodePath.join(outDir, `shots-${label}.json`),
+  JSON.stringify({ env, urls, shots }, null, 2),
+);
+await browser.close();
+console.log(JSON.stringify({ env, count: shots.length }));
```

## apps/server/src/orchestration-v2/Orchestrator.ts

Status: modified; +17; −11

```diff
@@ -1041,7 +1041,7 @@
           "contextTransfers",
         ],
         {
-          turnItemTypes: ["user_message", "error"],
+          turnItemTypes: ["user_message", "error", "notification"],
           messageRoles: ["user"],
           omitSettledLeafNodes: true,
         },
@@ -2961,7 +2961,7 @@
       const projection = yield* loadProjectionForCommand(
         command,
         ["runs", "runtimeRequests", "turnItems"],
-        { turnItemTypes: ["error"] },
+        { turnItemTypes: ["user_message", "error", "notification"] },
       );
       const run = usageLimitBlockedRun(projection.runs, projection.turnItems, null);
       const failure = latestRootProviderFailure(run, projection.turnItems);
@@ -4814,7 +4814,10 @@
         source === undefined ||
         (source.status !== "interrupted" &&
           !(source.status === "failed" && limited?.class === "usage_limit")) ||
+        (source.status === "failed"
+          ? usageLimitBlockedRun(projection.runs, projection.turnItems, null)
-        latestExecutedRun(projection.runs)?.id !== source.id ||
+          : latestExecutedRun(projection.runs)
+        )?.id !== source.id ||
         projection.thread.archivedAt !== null ||
         projection.thread.deletedAt !== null ||
         projection.runtimeRequests.some((request) => request.status === "pending")
@@ -9141,14 +9144,17 @@
     const { runs, subagents } = input.projection;
     let { thread } = input.projection;
     let recovery = thread.limitRecovery;
+    const { turnItems } = yield* projectionStore
+      .getThreadRecords(thread.id, ["turnItems"], {
+        turnItemTypes: ["user_message", "notification", "error"],
+      })
-    const latest = latestExecutedRun(runs);
+      .pipe(mapDispatchError(input.command));
+    const latest = usageLimitBlockedRun(runs, turnItems, null) ?? latestExecutedRun(runs);
     if (latest?.status === "failed") {
-      const { turnItems } = yield* projectionStore
-        .getThreadRecords(thread.id, ["turnItems"], {
-          turnItemTypes: ["error"],
-          turnItemRunIds: [latest.id],
-        })
+      // Stop must also reach the limited source hidden behind completed wakes.
+      if (latest.id !== latestExecutedRun(runs)?.id) {
+        yield* markStoppedRun({ ...input, thread, run: latest });
+      }
-        .pipe(mapDispatchError(input.command));
       const failure = latestRootProviderFailure(latest, turnItems);
       if (
         failure?.class === "usage_limit" &&
@@ -11444,7 +11450,7 @@
         const projection = yield* loadProjectionForCommand(
           command,
           ["runs", "turnItems", "providerSessions", "messages"],
+          { turnItemTypes: ["user_message", "error", "notification"], messageRoles: ["user"] },
-          { turnItemTypes: ["error"], messageRoles: ["user"] },
         );
         if (projection.thread.archivedAt !== null || projection.thread.deletedAt !== null) {
           return yield* new OrchestratorDispatchError({
```

## apps/server/src/orchestration-v2/ProjectionStore.test.ts

Status: modified; +2; −1

```diff
@@ -3803,7 +3803,8 @@
           (detail) =>
             /^SEARCH (?:item|error) /.test(detail) && !detail.includes("(turn_item_id=?)"),
         );
+        // Recovery selection also reads the latest failed source before skipping notification wakes.
+        assert.lengthOf(errorLookups, marker.includes("blocking") ? 3 : 2, marker);
-        assert.lengthOf(errorLookups, marker.includes("blocking") ? 2 : 1, marker);
         for (const errorLookup of errorLookups) {
           assert.include(errorLookup, "turn_items_failed_error_idx");
           assert.include(errorLookup, "(thread_id=? AND run_id=? AND node_id=?)");
```

## apps/server/src/orchestration-v2/ProjectionStore.ts

Status: modified; +70; −2

```diff
@@ -3783,6 +3783,23 @@
               LIMIT 1
             ) AS last_error
           FROM orchestration_v2_projection_threads t
+          LEFT JOIN orchestration_v2_projection_runs latest_failure ON latest_failure.run_id = (
+            SELECT failed.run_id FROM orchestration_v2_projection_runs failed
+            WHERE failed.thread_id = t.thread_id AND failed.status = 'failed'
+            ORDER BY failed.completed_at IS NULL DESC, failed.completed_at DESC,
+              failed.ordinal DESC, failed.run_id DESC
+            LIMIT 1
+          )
+          LEFT JOIN orchestration_v2_projection_turn_items latest_failure_error
+            ON latest_failure_error.turn_item_id = (
+              SELECT error.turn_item_id FROM orchestration_v2_projection_turn_items error
+              WHERE error.thread_id = t.thread_id
+                AND error.run_id = latest_failure.run_id
+                AND error.node_id IS json_extract(latest_failure.payload_json, '$.rootNodeId')
+                AND error.type = 'error' AND error.status = 'failed'
+              ORDER BY error.updated_at DESC, error.ordinal DESC, error.turn_item_id DESC
+              LIMIT 1
+            )
           INNER JOIN orchestration_v2_projection_runs r ON r.run_id = (
             SELECT latest.run_id FROM orchestration_v2_projection_runs latest
             WHERE latest.thread_id = t.thread_id
@@ -3791,7 +3808,24 @@
                 latest.status = 'cancelled'
                 AND json_extract(latest.payload_json, '$.startedAt') IS NULL
               )
-            -- latestExecutedRun: the run that ended last (runRanAfter).
+              AND NOT (
+                latest.status = 'completed'
+                  AND COALESCE(
+                    json_extract(latest_failure_error.payload_json, '$.failure.class') = 'usage_limit'
+                    AND julianday(latest.completed_at) <= julianday(
+                      json_extract(latest_failure_error.payload_json, '$.failure.resetAt')
+                    ), 0
+                  ) AND (
+                  SELECT wake.type FROM orchestration_v2_projection_turn_items wake
+                  WHERE wake.run_id = latest.run_id
+                    AND wake.node_id IS NOT NULL
+                    AND wake.node_id IS json_extract(latest.payload_json, '$.rootNodeId')
+                    AND wake.type IN ('user_message', 'notification')
+                  ORDER BY wake.ordinal
+                  LIMIT 1
+                ) IS 'notification'
+              )
+            -- usageLimitBlockedRun: only notification wakes completed before reset preserve the limit.
             ORDER BY latest.completed_at IS NULL DESC, latest.completed_at DESC,
               latest.ordinal DESC, latest.run_id DESC
             LIMIT 1
@@ -5803,6 +5837,23 @@
               ORDER BY candidate.ordinal DESC, candidate.run_id DESC
               LIMIT 1
             )
+            LEFT JOIN orchestration_v2_projection_runs latest_failure ON latest_failure.run_id = (
+              SELECT failed.run_id FROM orchestration_v2_projection_runs failed
+              WHERE failed.thread_id = t.thread_id AND failed.status = 'failed'
+              ORDER BY failed.completed_at IS NULL DESC, failed.completed_at DESC,
+                failed.ordinal DESC, failed.run_id DESC
+              LIMIT 1
+            )
+            LEFT JOIN orchestration_v2_projection_turn_items latest_failure_error
+              ON latest_failure_error.turn_item_id = (
+                SELECT error.turn_item_id FROM orchestration_v2_projection_turn_items error
+                WHERE error.thread_id = t.thread_id
+                  AND error.run_id = latest_failure.run_id
+                  AND error.node_id IS json_extract(latest_failure.payload_json, '$.rootNodeId')
+                  AND error.type = 'error' AND error.status = 'failed'
+                ORDER BY error.updated_at DESC, error.ordinal DESC, error.turn_item_id DESC
+                LIMIT 1
+              )
             LEFT JOIN orchestration_v2_projection_runs blocked ON blocked.run_id = (
               SELECT candidate.run_id
               FROM orchestration_v2_projection_runs candidate
@@ -5812,7 +5863,24 @@
                   candidate.status = 'cancelled'
                   AND json_extract(candidate.payload_json, '$.startedAt') IS NULL
                 )
-              -- latestExecutedRun: the run that ended last (runRanAfter).
+                AND NOT (
+                  candidate.status = 'completed'
+                  AND COALESCE(
+                    json_extract(latest_failure_error.payload_json, '$.failure.class') = 'usage_limit'
+                    AND julianday(candidate.completed_at) <= julianday(
+                      json_extract(latest_failure_error.payload_json, '$.failure.resetAt')
+                    ), 0
+                  ) AND (
+                    SELECT wake.type FROM orchestration_v2_projection_turn_items wake
+                    WHERE wake.run_id = candidate.run_id
+                      AND wake.node_id IS NOT NULL
+                      AND wake.node_id IS json_extract(candidate.payload_json, '$.rootNodeId')
+                      AND wake.type IN ('user_message', 'notification')
+                    ORDER BY wake.ordinal
+                    LIMIT 1
+                  ) IS 'notification'
+                )
+              -- usageLimitBlockedRun: only notification wakes completed before reset preserve the limit.
               ORDER BY candidate.completed_at IS NULL DESC, candidate.completed_at DESC,
                 candidate.ordinal DESC, candidate.run_id DESC
               LIMIT 1
```

## apps/server/src/orchestration-v2/runtimeLayer.test.ts

Status: modified; +262; −20

```diff
@@ -1,3 +1,7 @@
+import {
+  latestExecutedRun,
+  usageLimitBlockedRun,
+} from "@t3tools/shared/orchestrationV2ThreadError";
 import { SourceControlProviderKind } from "@t3tools/contracts";
 import { limitRecoveryCommand } from "./UsageLimitRecoveryWorker.ts";
 import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
@@ -9,6 +13,7 @@
   type ApplicationStoredEvent,
   CheckpointId,
   CheckpointRef,
+  CheckpointScopeId,
   CommandId,
   ContextTransferId,
   EventId,
@@ -5470,7 +5475,7 @@
 });
 
 it.layer(layerTest)("usage-limit recovery", (it) => {
-  it.effect.each(["interrupted", "usage_limit"] as const)(
+  it.effect.each(["interrupted", "usage_limit", "usage_limit_wake"] as const)(
     "manually resumes an %s run ahead of its queued message only once",
     (reason) =>
       Effect.gen(function* () {
@@ -5553,7 +5558,7 @@
           ],
         });
         let scheduledResume: ReturnType<typeof limitRecoveryCommand> = null;
-        if (reason === "usage_limit") {
+        if (reason !== "interrupted") {
           const resetAt = DateTime.formatIso(DateTime.add(now, { minutes: 1 }));
           yield* events.write({
             events: [
@@ -5589,24 +5594,97 @@
               },
             ],
           });
+          if (reason === "usage_limit_wake") {
+            yield* TestClock.adjust("2 seconds");
+            yield* orchestrator.dispatch({
+              type: "message.dispatch",
+              commandId: CommandId.make("manual-resume:wake"),
+              threadId,
+              messageId: MessageId.make("manual-resume:wake"),
+              text: "Background task finished.",
+              attachments: [],
+              notification: {
+                source: { kind: "background_task" },
+                outcome: "completed",
+                summary: "Background task finished",
+              },
+              dispatchMode: { type: "queue_after_active" },
+              createdBy: "agent",
+              creationSource: "provider",
+            });
+            const wake = (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)!;
+            const completedAt = yield* DateTime.now;
+            yield* events.write({
+              events: [
+                {
+                  id: EventId.make("manual-resume:wake-running"),
+                  type: "run.updated",
+                  threadId,
+                  occurredAt: completedAt,
+                  payload: {
+                    ...wake,
+                    status: "running",
+                    startedAt: completedAt,
+                    completedAt: null,
+                  },
+                },
+              ],
+            });
+            const activeWakeResume = yield* orchestrator
+              .dispatch({
+                type: "message.dispatch",
+                commandId: CommandId.make("manual-resume:active-wake"),
+                threadId,
+                messageId: MessageId.make("manual-resume:active-wake"),
+                manualContinuationOfRunId: source.id,
+                text: "Continue where you left off.",
+                attachments: [],
+                dispatchMode: { type: "start_immediately" },
+                createdBy: "user",
+                creationSource: "web",
+              })
+              .pipe(Effect.exit);
+            assert.equal(activeWakeResume._tag, "Failure");
+            yield* events.write({
+              events: [
+                {
+                  id: EventId.make("manual-resume:wake-completed"),
+                  type: "run.updated",
+                  threadId,
+                  occurredAt: completedAt,
+                  payload: { ...wake, status: "completed", startedAt: completedAt, completedAt },
+                },
+              ],
+            });
+          }
           const shell = (yield* orchestrator.getShellSnapshot()).threads.find(
             (thread) => thread.id === threadId,
           )!;
+          if (reason === "usage_limit_wake") {
+            assert.isNull(limitRecoveryCommand(shell, false, DateTime.toEpochMillis(now)));
+            yield* TestClock.adjust("1 minute");
+          } else {
-          yield* orchestrator.dispatch(
-            limitRecoveryCommand(shell, true, DateTime.toEpochMillis(now))!,
-          );
-          const armed = (yield* orchestrator.getShellSnapshot()).threads.find(
+            yield* orchestrator.dispatch(
+              limitRecoveryCommand(shell, true, DateTime.toEpochMillis(now))!,
+            );
+            const armed = (yield* orchestrator.getShellSnapshot()).threads.find(
+              (thread) => thread.id === threadId,
+            )!;
+            scheduledResume = limitRecoveryCommand(armed, true, Date.parse(resetAt));
+            assert.isNotNull(scheduledResume);
+          }
-            (thread) => thread.id === threadId,
-          )!;
-          scheduledResume = limitRecoveryCommand(armed, true, Date.parse(resetAt));
-          assert.isNotNull(scheduledResume);
         }
+        const current = yield* orchestrator.getThreadProjection(threadId);
+        const selected =
+          usageLimitBlockedRun(current.runs, current.turnItems, null) ??
+          latestExecutedRun(current.runs);
+        assert.equal(selected?.id, source.id);
         const resume = (suffix: string) => ({
           type: "message.dispatch" as const,
           commandId: CommandId.make(`manual-resume:${suffix}:${reason}`),
           threadId,
           messageId: MessageId.make(`manual-resume:${suffix}:${reason}`),
-          manualContinuationOfRunId: source.id,
+          manualContinuationOfRunId: selected!.id,
           text: "Continue where you left off.",
           attachments: [],
           dispatchMode: { type: "start_immediately" as const },
@@ -5615,9 +5693,11 @@
         });
         yield* orchestrator.dispatch(resume("first"));
         const after = yield* orchestrator.getThreadProjection(threadId);
-        assert.lengthOf(after.runs, 3);
+        const expectedRuns = reason === "usage_limit_wake" ? 4 : 3;
+        assert.lengthOf(after.runs, expectedRuns);
         assert.equal(after.runs[1]?.status, "queued");
-        assert.equal(after.runs[2]?.status, "starting");
+        assert.isFalse(after.runs[1]?.queueHeld ?? false);
+        assert.equal(after.runs.at(-1)?.status, "starting");
         assert.equal(
           (yield* orchestrator.dispatch(resume("second")).pipe(Effect.exit))._tag,
           "Failure",
@@ -5626,7 +5706,18 @@
           yield* TestClock.adjust("1 minute");
           yield* orchestrator.dispatch(scheduledResume);
         }
+        assert.lengthOf((yield* orchestrator.getThreadProjection(threadId)).runs, expectedRuns);
+        if (reason === "usage_limit_wake") {
+          yield* orchestrator.dispatch({
+            type: "queue.resume",
+            commandId: CommandId.make("manual-resume:release-queue"),
-        assert.lengthOf((yield* orchestrator.getThreadProjection(threadId)).runs, 3);
+            threadId,
+          });
+          const released = yield* orchestrator.getThreadProjection(threadId);
+          assert.equal(released.runs[1]?.id, queued.id);
+          assert.equal(released.runs[1]?.status, "queued");
+          assert.isFalse(released.runs[1]?.queueHeld ?? false);
+        }
       }),
   );
 
@@ -5644,6 +5735,10 @@
     "expired-snooze",
     "wake",
     "stop",
+    "completed-wake-before-arm",
+    "completed-wake-after-arm",
+    "completed-wake-stop-before-arm",
+    "completed-wake-postreset",
     "stop-before-arm",
     "stop-before-arm-mismatched-recovery",
   ] as const)("guards a scheduled usage-limit continuation against %s", (scenario) =>
@@ -5773,9 +5868,100 @@
         assert.equal(resumeHeldQueue._tag, "Failure");
         assert.isTrue((yield* orchestrator.getThreadProjection(threadId)).runs[1]?.queueHeld);
       }
+      const completedWake = scenario.startsWith("completed-wake-");
+      const finishBackgroundWake = Effect.fnUntraced(function* () {
+        yield* TestClock.adjust("2 seconds");
+        yield* orchestrator.dispatch({
+          type: "message.dispatch",
+          commandId: CommandId.make(`recovery:background-wake:${scenario}`),
+          threadId,
+          messageId: MessageId.make(`recovery:background-wake:${scenario}`),
+          text: "Background task finished; no further work is needed.",
+          attachments: [],
-      const shell = (yield* orchestrator.getShellSnapshot()).threads.find(
-        (thread) => thread.id === threadId,
-      )!;
+          notification: {
+            source: { kind: "background_task" },
+            outcome: "completed",
+            summary: "Background task finished",
+          },
+          dispatchMode: { type: "queue_after_active" },
+          createdBy: "agent",
+          creationSource: "provider",
+        });
+        const wake = (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)!;
+        const completedAt = yield* DateTime.now;
+        const itemBase = {
+          threadId,
+          runId: wake.id,
+          nodeId: wake.rootNodeId,
+          providerThreadId: null,
+          providerTurnId: null,
+          nativeItemRef: null,
+          parentItemId: null,
+          status: "completed" as const,
+          title: null,
+          startedAt: completedAt,
+          completedAt,
+          updatedAt: completedAt,
+        };
+        yield* events.write({
+          events: [
+            {
+              id: EventId.make(`recovery:wake-notice:${scenario}`),
+              type: "turn-item.updated",
+              threadId,
+              occurredAt: completedAt,
+              payload: {
+                ...itemBase,
+                id: TurnItemId.make(`recovery:wake-notice:${scenario}`),
+                ordinal: 2,
+                ...(scenario === "completed-wake-postreset"
+                  ? {
+                      type: "assistant_message" as const,
+                      messageId: MessageId.make(`recovery:wake-output:${scenario}`),
+                      text: "Finished the remaining implementation and verified the result.",
+                      streaming: false,
+                    }
+                  : { type: "system_notice" as const, message: "No further work is needed." }),
+              },
+            },
+            {
+              id: EventId.make(`recovery:wake-checkpoint:${scenario}`),
+              type: "turn-item.updated",
+              threadId,
+              occurredAt: completedAt,
+              payload: {
+                ...itemBase,
+                id: TurnItemId.make(`recovery:wake-checkpoint:${scenario}`),
+                ordinal: 3,
+                type: "checkpoint",
+                checkpointId: CheckpointId.make(`recovery:wake-checkpoint:${scenario}`),
+                scopeId: CheckpointScopeId.make(`recovery:wake-scope:${scenario}`),
+                files: [],
+              },
+            },
+            {
+              id: EventId.make(`recovery:wake-completed:${scenario}`),
+              type: "run.updated",
+              threadId,
+              occurredAt: completedAt,
+              payload: { ...wake, status: "completed", startedAt: completedAt, completedAt },
+            },
+          ],
+        });
+      });
+      if (scenario === "completed-wake-before-arm" || scenario === "completed-wake-stop-before-arm")
+        yield* finishBackgroundWake();
+      const projections = yield* ProjectionStore.ProjectionStoreV2;
+      const shell = completedWake
+        ? (yield* projections.getLimitRecoveryCandidates({
+            now: yield* DateTime.now,
+            autoResume: true,
+            snooze: false,
+          })).find((row) => row.id === threadId)!
+        : (yield* orchestrator.getShellSnapshot()).threads.find(
+            (thread) => thread.id === threadId,
+          )!;
+      assert.isDefined(shell, "a completed background wake must leave the limited run armable");
       assert.isNull(limitRecoveryCommand(shell, false, DateTime.toEpochMillis(now)));
       if (scenario === "invalid-snooze") {
         const result = yield* orchestrator
@@ -5802,7 +5988,9 @@
         limitRecovery: arm.limitRecovery,
       };
       const stopBeforeArm =
-        scenario === "stop-before-arm" || scenario === "stop-before-arm-mismatched-recovery";
+        scenario === "stop-before-arm" ||
+        scenario === "stop-before-arm-mismatched-recovery" ||
+        scenario === "completed-wake-stop-before-arm";
       if (stopBeforeArm) {
         if (scenario === "stop-before-arm-mismatched-recovery") {
           const current = yield* orchestrator.getThreadProjection(threadId);
@@ -5867,6 +6055,46 @@
         snooze,
         requestId: armCommand.commandId,
       });
+      if (scenario === "completed-wake-postreset") {
+        const staleResume = limitRecoveryCommand(armedShell, true, Date.parse(resetAt));
+        assert(staleResume?.type === "message.dispatch");
+        yield* TestClock.adjust("1 minute");
+        yield* finishBackgroundWake();
+        assert.isUndefined(
+          (yield* projections.getLimitRecoveryCandidates({
+            now: yield* DateTime.now,
+            autoResume: true,
+            snooze: false,
+          })).find((row) => row.id === threadId),
+        );
+        const successful = (yield* orchestrator.getShellSnapshot()).threads.find(
+          (thread) => thread.id === threadId,
+        )!;
+        assert.equal(successful.status, "completed");
+        assert.isNull(successful.lastErrorClass);
+        yield* orchestrator.dispatch(staleResume);
+        const after = yield* orchestrator.getThreadProjection(threadId);
+        assert.lengthOf(after.runs, 2);
+        assert.isNull(usageLimitBlockedRun(after.runs, after.turnItems, null));
+        const rejected = yield* orchestrator
+          .dispatch({
+            ...staleResume,
+            commandId: CommandId.make("recovery:stale-manual-postreset"),
+            messageId: MessageId.make("recovery:stale-manual-postreset"),
+            usageLimitContinuationOfRunId: undefined,
+            manualContinuationOfRunId: run.id,
+          })
+          .pipe(Effect.exit);
+        assert.equal(rejected._tag, "Failure");
+        return;
+      }
+      if (scenario === "completed-wake-after-arm") {
+        yield* finishBackgroundWake();
+        armedShell = (yield* orchestrator.getShellSnapshot()).threads.find(
+          (thread) => thread.id === threadId,
+        )!;
+        assert.equal(armedShell.limitRecovery?.runId, run.id);
+      }
       if (snooze)
         assert.equal(DateTime.toEpochMillis(armedShell.snoozedUntil!), Date.parse(resetAt));
       if (scenario === "cancel-resume-keep-snooze") {
@@ -5981,14 +6209,26 @@
       });
       assert.lengthOf(
         (yield* orchestrator.getThreadProjection(threadId)).runs,
+        scenario === "queued-resume" || completedWake ? 2 : 1,
-        scenario === "queued-resume" ? 2 : 1,
       );
       yield* TestClock.adjust("1 minute");
+      const resumeSource = completedWake
+        ? (yield* projections.getLimitRecoveryCandidates({
+            now: yield* DateTime.now,
+            autoResume: true,
+            snooze: false,
+          })).find((row) => row.id === threadId)!
+        : armedShell;
+      assert.isDefined(resumeSource, "a completed background wake must remain due for recovery");
       const resume = limitRecoveryCommand(
-        armedShell,
+        resumeSource,
         true,
         DateTime.toEpochMillis(yield* DateTime.now),
       );
+      if (completedWake) {
+        assert(resume?.type === "message.dispatch");
+        assert.equal(resume.usageLimitContinuationOfRunId, run.id);
+      }
       if (autoResume && scenario !== "cancel-resume-keep-snooze") assert.isNotNull(resume);
       else assert.isNull(resume);
       if (scenario === "snooze-race") {
@@ -6114,6 +6354,7 @@
         after.runs,
         before.runs.length +
           (scenario === "resume" ||
+          completedWake ||
           stopBeforeArm ||
           scenario === "queued-resume" ||
           scenario === "independent-patches" ||
@@ -6125,6 +6366,7 @@
         after.messages,
         before.messages.length +
           (scenario === "resume" ||
+          completedWake ||
           stopBeforeArm ||
           scenario === "queued-resume" ||
           scenario === "independent-patches" ||
```

## apps/web/src/components/ChatView.tsx

Status: modified; +4; −1

```diff
@@ -42,6 +42,7 @@
 import {
   latestExecutedRun,
   latestRootProviderFailure,
+  usageLimitBlockedRun,
 } from "@t3tools/shared/orchestrationV2ThreadError";
 import type { UsageLimitSourceSnapshots } from "@t3tools/contracts";
 import { feedbackBannerItem } from "./chat/ComposerFeedback";
@@ -755,7 +756,9 @@
       true;
   const resumableRunId = (() => {
     if (!isServerThread || serverProjection === null) return null;
+    const run =
+      usageLimitBlockedRun(serverProjection.runs, serverProjection.turnItems, null) ??
-    const run = latestExecutedRun(serverProjection.runs);
+      latestExecutedRun(serverProjection.runs);
     if (run?.status === "interrupted") return run.id;
     return run?.status === "failed" &&
       serverRuntime?.lastErrorClass === "usage_limit" &&
```

## knip.jsonc

Status: modified; +3; −0

```diff
@@ -43,6 +43,9 @@
         "scripts/measure-pr-preview.ts",
         "scripts/probe-claude-fork-local-rollback-replay.ts",
         "scripts/update-test-shard-weights.ts",
+        "scripts/usage-limit-web-evidence-record.mjs",
+        "scripts/usage-limit-web-evidence-seed.ts",
+        "scripts/usage-limit-web-evidence-stills.mjs",
         "scripts/verify-background-live.ts",
         "src/provider/testFixtures/*.mjs",
         // graphqlgen reads its config.
```

## packages/client-runtime/src/state/threadExecution.test.ts

Status: modified; +64; −0

```diff
@@ -138,6 +138,7 @@
       failure: {
         class: "usage_limit" as const,
         message: "Plan limit reached",
+        resetAt: DateTime.formatIso(DateTime.add(now, { minutes: 1 })),
         code: "usageLimitExceeded",
         retryable: null,
       },
@@ -168,6 +169,69 @@
         turnItems: [{ ...item, failure: { ...item.failure, class: "provider_error" as const } }],
       }),
     ).toMatchObject({ status: "queued", lastErrorClass: null });
+
+    const wake = {
+      ...run("wake", 4, "completed"),
+      rootNodeId: NodeId.make("wake-root"),
+      completedAt: DateTime.add(now, { seconds: 1 }),
+    };
+    const notification = {
+      ...item,
+      id: TurnItemId.make("wake-notification"),
+      runId: wake.id,
+      nodeId: wake.rootNodeId,
+      type: "notification" as const,
+      status: "completed" as const,
+      source: { kind: "background_task" as const },
+      outcome: "completed" as const,
+      summary: "Background work finished",
+    };
+    const woke = { ...projection, runs: [failed, wake], turnItems: [item, notification] };
+    expect(
+      deriveThreadRuntime({
+        ...woke,
+        runs: [failed, { ...wake, completedAt: DateTime.add(now, { minutes: 2 }) }],
+      }),
+    ).toMatchObject({ status: "completed", lastErrorClass: null });
+    expect(
+      usageLimitBlockedRun(
+        [failed, { ...wake, completedAt: DateTime.add(now, { minutes: 1 }) }],
+        woke.turnItems,
+        null,
+      )?.id,
+    ).toBe(failed.id);
+    expect(deriveThreadRuntime(woke)).toMatchObject({
+      status: "failed",
+      lastErrorClass: "usage_limit",
+    });
+    expect(
+      deriveThreadRuntime({ ...woke, runs: [failed, { ...wake, status: "running" }] }),
+    ).toMatchObject({ status: "running", lastErrorClass: null });
+    expect(deriveThreadRuntime({ ...woke, turnItems: [item] })).toMatchObject({
+      status: "completed",
+      lastErrorClass: null,
+    });
+    expect(
+      deriveThreadRuntime({
+        ...woke,
+        turnItems: [item, { ...notification, nodeId: NodeId.make("child") }],
+      }),
+    ).toMatchObject({ status: "completed", lastErrorClass: null });
+    const userInput = {
+      ...notification,
+      id: TurnItemId.make("wake-user-input"),
+      ordinal: 0,
+      type: "user_message" as const,
+      messageId: wake.userMessageId,
+      inputIntent: "turn_start" as const,
+      text: "Continue",
+      attachments: [],
+      createdBy: "user" as const,
+      creationSource: "web" as const,
+    };
+    expect(
+      deriveThreadRuntime({ ...woke, turnItems: [notification, item, userInput] }),
+    ).toMatchObject({ status: "completed", lastErrorClass: null });
   });
 
   it("keeps live activity attached to an executing run when a newer run is queued", () => {
```

## packages/shared/src/orchestrationV2ThreadError.ts

Status: modified; +26; −1

```diff
@@ -80,7 +80,32 @@
   turnItems: ReadonlyArray<OrchestrationV2TurnItem>,
   sessionError: string | null,
 ): OrchestrationV2Run | null {
+  const failed = latestExecutedRun(runs.filter((run) => run.status === "failed"));
+  const failure = latestRootProviderFailure(failed, turnItems);
+  if (failure?.class !== "usage_limit") return null;
+  const resetAt = failure.resetAt == null ? NaN : Date.parse(failure.resetAt);
+  // Before reset, a completed notification wake cannot supersede the limited work.
+  // After reset, success is authoritative even when the input was a notification.
+  const firstInputs = new Map<
+    NonNullable<OrchestrationV2TurnItem["nodeId"]>,
+    Extract<OrchestrationV2TurnItem, { type: "user_message" | "notification" }>
+  >();
+  for (const item of turnItems) {
+    if (item.nodeId === null || (item.type !== "user_message" && item.type !== "notification"))
+      continue;
+    const first = firstInputs.get(item.nodeId);
+    if (first === undefined || item.ordinal < first.ordinal) firstInputs.set(item.nodeId, item);
+  }
+  const executed = latestExecutedRun(
+    runs.filter(
+      (run) =>
+        run.status !== "completed" ||
+        run.completedAt === null ||
+        !(DateTime.toEpochMillis(run.completedAt) <= resetAt) ||
+        run.rootNodeId === null ||
-  const executed = latestExecutedRun(runs);
+        firstInputs.get(run.rootNodeId)?.type !== "notification",
+    ),
+  );
   if (executed?.status !== "failed") return null;
   const summary = threadErrorSummary(latestRootProviderFailure(executed, turnItems), sessionError);
   return summary.lastErrorClass === "usage_limit" ? executed : null;
```
