Skip to content

Commit ee97ec3

Browse files
PR Batch Testercodex
andcommitted
fix(clients): preserve unrelated usage-limit messages
Restrict Claude pause notices to the failed root provider turn and match complete CLI limit notices so ordinary assistant output stays visible. Co-authored-by: Codex <[email protected]>
1 parent 49e86b9 commit ee97ec3

2 files changed

Lines changed: 57 additions & 16 deletions

File tree

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

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type OrchestrationV2ExecutionNode,
1414
type OrchestrationV2Run,
1515
type OrchestrationV2RunStatus,
16+
type OrchestrationV2TurnItem,
1617
} from "@t3tools/contracts";
1718
import * as DateTime from "effect/DateTime";
1819
import { usageLimitBlockedRun } from "@t3tools/shared/orchestrationV2ThreadError";
@@ -592,6 +593,7 @@ describe("usage-limit notice rows", () => {
592593
});
593594
const notice = (id: string, ordinal: number, nativeId: string) => ({
594595
...base,
596+
nodeId: limited.rootNodeId,
595597
id: TurnItemId.make(id),
596598
ordinal,
597599
nativeItemRef: {
@@ -613,9 +615,9 @@ describe("usage-limit notice rows", () => {
613615
assistant("cli-notice", 5, "You've hit your 5-hour limit · resets 7:10am (Asia/Kolkata)"),
614616
error("limit-error", 6),
615617
];
616-
const swap = (index: number, item: (typeof turnItems)[number]) =>
618+
const swap = (index: number, item: OrchestrationV2TurnItem) =>
617619
turnItems.map((current, position) => (position === index ? item : current));
618-
const rows = (items: ReadonlyArray<(typeof turnItems)[number]>) =>
620+
const rows = (items: ReadonlyArray<OrchestrationV2TurnItem>) =>
619621
items.map((item, position) => ({
620622
position,
621623
visibility: "local" as const,
@@ -628,7 +630,10 @@ describe("usage-limit notice rows", () => {
628630
projection.visibleTurnItems,
629631
deriveUsageLimitNoticeRows(projection),
630632
).map((row) => row.item.id);
631-
const projection = (runs: ReadonlyArray<OrchestrationV2Run>, items = turnItems) => ({
633+
const projection = (
634+
runs: ReadonlyArray<OrchestrationV2Run>,
635+
items: ReadonlyArray<OrchestrationV2TurnItem> = turnItems,
636+
) => ({
632637
...v2Projection,
633638
runs,
634639
turnItems: items,
@@ -638,6 +643,16 @@ describe("usage-limit notice rows", () => {
638643
it("leaves the recovery notice as the only account of a usage-limit pause", () => {
639644
const kept = ["useful", "unrelated-notice", "child-error"];
640645
expect(visibleIds(projection([limited]))).toEqual(kept);
646+
for (const text of [
647+
"You've hit your session limit",
648+
"You've hit your session limit · resets 11:10am (Australia/Sydney)",
649+
"You're out of extra usage",
650+
"Claude AI usage limit reached.",
651+
]) {
652+
expect(visibleIds(projection([limited], swap(4, assistant("cli-notice", 5, text))))).toEqual(
653+
kept,
654+
);
655+
}
641656
// Follow-ups that never started do not end the pause.
642657
expect(visibleIds(projection([limited, run("queued", 2, "queued")]))).toEqual(kept);
643658
expect(
@@ -654,14 +669,37 @@ describe("usage-limit notice rows", () => {
654669
});
655670

656671
it("keeps the agent's own words in the limited turn", () => {
657-
const reply = assistant(
658-
"cli-notice",
659-
5,
672+
for (const text of [
660673
"I've hit your limit on retries, so here is the plan.",
661-
);
662-
expect(visibleIds(projection([limited], swap(4, reply)))).toContain("cli-notice");
674+
"You've hit your configured retry limit; reduce maxRetries and rerun the build.",
675+
"Claude AI usage limit reached, but the changes are saved in src/parser.ts.",
676+
"You've hit your 5-hour limit · resets 7:10am. The changes are saved.",
677+
]) {
678+
expect(
679+
visibleIds(projection([limited], swap(4, assistant("cli-notice", 5, text)))),
680+
).toContain("cli-notice");
681+
}
663682
const earlierTurn = { ...turnItems[4]!, providerTurnId: ProviderTurnId.make("earlier-turn") };
664683
expect(visibleIds(projection([limited], swap(4, earlierTurn)))).toContain("cli-notice");
684+
const pause = turnItems[1]!;
685+
for (const item of [
686+
{ ...pause, nodeId: NodeId.make("child") },
687+
{ ...pause, providerTurnId: ProviderTurnId.make("earlier-turn") },
688+
{ ...pause, providerTurnId: null },
689+
{
690+
...pause,
691+
nativeItemRef: { ...pause.nativeItemRef!, driver: ProviderDriverKind.make("codex") },
692+
},
693+
{
694+
...pause,
695+
nativeItemRef: {
696+
...pause.nativeItemRef!,
697+
nativeId: "usage-limit:earlier-turn:five_hour:1",
698+
},
699+
},
700+
]) {
701+
expect(visibleIds(projection([limited], swap(1, item)))).toContain("pause");
702+
}
665703
});
666704

667705
it("returns the rows to history once another run executes or the failure is not a usage limit", () => {

‎packages/client-runtime/src/state/threadExecution.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ export interface UsageLimitNoticeRows {
299299

300300
// Claude Code's own limit messages, such as "You've hit your 5-hour limit · resets 7:10am".
301301
const PROVIDER_LIMIT_NOTICE =
302-
/^(?:You[’']ve hit your [^\n]*\blimit\b|You[’']re out of extra usage\b|Claude AI usage limit reached\b)[^\n]*$/i;
302+
/^(?:You[’']ve hit your (?:(?:5-hour|session|weekly|usage) )?limit|You[’']re out of extra usage|Claude AI usage limit reached)(?: · resets (?:[1-9]|1[0-2])(?::[0-5]\d)?(?:am|pm)(?: \([A-Za-z_+-]+(?:\/[A-Za-z_+-]+)+\))?)?\.?$/i;
303303

304304
/**
305305
* The timeline rows that repeat a usage-limit pause while its recovery notice shows: the run's
@@ -327,20 +327,23 @@ export function deriveUsageLimitNoticeRows(
327327
) {
328328
itemIds.add(item.id);
329329
if (item.failure === failure) failedProviderTurnId = item.providerTurnId;
330-
} else if (
331-
item.type === "system_notice" &&
332-
item.nativeItemRef?.nativeId?.startsWith("usage-limit:") === true
333-
) {
334-
itemIds.add(item.id);
335330
}
336331
}
337332
let providerNotice: ThreadStructureTurnItem | null = null;
338333
if (failedProviderTurnId !== null) {
339334
for (const item of projection.turnItems) {
335+
if (item.runId !== run.id || item.providerTurnId !== failedProviderTurnId) continue;
336+
if (
337+
item.type === "system_notice" &&
338+
item.nodeId === run.rootNodeId &&
339+
item.parentItemId === null &&
340+
item.nativeItemRef?.driver === "claudeAgent" &&
341+
item.nativeItemRef.nativeId?.startsWith(`usage-limit:${failedProviderTurnId}:`) === true
342+
) {
343+
itemIds.add(item.id);
344+
}
340345
if (
341346
item.type === "assistant_message" &&
342-
item.runId === run.id &&
343-
item.providerTurnId === failedProviderTurnId &&
344347
item.parentItemId === null &&
345348
(providerNotice === null || item.ordinal > providerNotice.ordinal)
346349
) {

0 commit comments

Comments
 (0)