Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 31e4764

Browse files
Merge remote-tracking branch 'origin/main' into posthog-code/share-cloud-task-foundations
# Conflicts: # packages/agent/src/gateway-models.test.ts
2 parents ad560d6 + bf27356 commit 31e4764

116 files changed

Lines changed: 7723 additions & 2008 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/code/snapshots.yml‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -344,14 +344,6 @@ snapshots:
344344
hash: v1.k4693efd2.602ee87d6d7a745d4b09d9e49bd12a70e52299b78ca97a48b5ca73a01532d801.F_1XKDzCxzMm_opEt9FvzczBb_2n-vyvOyGFXHGJmTk
345345
features-sessions-conversationview--with-pending-prompt--light:
346346
hash: v1.k4693efd2.1636dfc3ee0dd9d28065166f2fff0193f2d499692c74749d3afa7585aae898b5.YxTPa4CsqI2YuGLUdck8340ITZQrpna_C6z-SgII4Ws
347-
features-sessions-scrollbarrail--pure--dark:
348-
hash: v1.k4693efd2.6baec21b1942848a3a376d2b90ca106b2c1b94de74382b4cd05673695d2649ee.UDDPtLDf1J2E3d3Oiy9is77Nm5xRSIfB7q5z0uUZRSQ
349-
features-sessions-scrollbarrail--pure--light:
350-
hash: v1.k4693efd2.33f02943fdbb1ea9774881f8909b580ab03ba02749614c533af1bf51dd817dca.ieP2bhDz0U_PTB4M6Zplvit0XlSTFiUn083GYBFNuC0
351-
features-sessions-scrollbarrail--scrollable-conversation--dark:
352-
hash: v1.k4693efd2.203b62b52d6365d6ec222f7529c35b3f46b17cd07ca792a8ce2de594af4d589b.3dfpnVM8yxor1PNO5aPk0pxn5THZjK4otA6mbz3XhtA
353-
features-sessions-scrollbarrail--scrollable-conversation--light:
354-
hash: v1.k4693efd2.fa7ef9cbdb09aef6108efcd3606cee32d889b3ad3b8f8be1ac637aed4e80cc72.0XcIKzM-sZHVG9hTZkAHdnBIPSrBiKmjq56QRQ6nasc
355347
features-sessions-toolcallblock--create-new-file--dark:
356348
hash: v1.k4693efd2.8847f96037682460aba47f0e0f5113037918235fc98596933f2d1d06fd6f8f92.SHetexoVOtozsZGZQMcVbmpBlbk7M5D5Q0R_TJvPnyc
357349
features-sessions-toolcallblock--create-new-file--light:
@@ -569,17 +561,17 @@ snapshots:
569561
git-dialogs--sync-success--light:
570562
hash: v1.k4693efd2.8d79d77c9338aeaa73734836f0e17fde987f6d3239914f4013889d3110e5088d.Rq-w2o1uhes_a5hUWqMXeiDkfnoPCZPt932LkEQXCco
571563
loops-loopslistview--comprehensive--dark:
572-
hash: v1.k4693efd2.3c2a6b87092674a3d395a6f5d36889ea231696d788b159f357be0926f3804c8a.Ihzlq7XIy9rRoA7DTbpLePlBhjP34o8ppgP__NXx-Yw
564+
hash: v1.k4693efd2.b9223e477701b5ef4587310a1de71500dd5214222531648ef76d66766b049a9f.2lckIm16WmmwZv1xVZnUL42InIHDzemQwU3EJzzw9_U
573565
loops-loopslistview--comprehensive--light:
574-
hash: v1.k4693efd2.42c2994def56651de08f5be20e98dbe67bfbdb03e7edd8bdf008401468c1653a.EnH7NwNkvclS7g5Htf7OCZuYEm7fI5W6hEr4DmwZC8I
566+
hash: v1.k4693efd2.1f172c831aefdfd4b3bae97af84f80aae3c09c4919f6b1b68315240cc482af50.hnrwbv8il_Ay6DcYxmQXNtxKU3rqOIWAkjnGB8n4AT4
575567
loops-loopslistview--long-mixed-list--dark:
576-
hash: v1.k4693efd2.384486583f8d9b66a4bf1d9cc45036a07310cb9e1943ea5b52ff466f55a03d7b.F_sXrJ13XWHzAF8yhoKun_k4BY8Jcb3EDtT0L1I6nKw
568+
hash: v1.k4693efd2.654250bc74fff46c496747eb13031e0e7a7ef264be5bb7be41451d82a497a515.vvrxXbbABkVY3hcHS3oThQ339JFgwIzB8E4n1usVWf0
577569
loops-loopslistview--long-mixed-list--light:
578-
hash: v1.k4693efd2.d80a0c55c4490276df53f781ac88ed9d9c6ec282bc14ddb80140b3ddcc44d32f.L6hw9ZixUiyTFKOybYzmjfUSIOayvmhueMTcmYt-IWo
570+
hash: v1.k4693efd2.a8e53087efdaf58e351809775aaf410f53a781a97e10e973ee53fd0db2f94e73.7GcprWAYAgATLtNckNBd-44EyctqKNcb4FXiHA08QJs
579571
loops-loopslistview--with-builder-sessions--dark:
580-
hash: v1.k4693efd2.bc59812bf49ff161cf20e75dcf043367ae3d2817aa2c2cf91c91d9771c6b62f8.yOBz-llmwDVay7GIHlMwlMT3EuKjkoW0Vwmwvi4noCM
572+
hash: v1.k4693efd2.84cfc28bedc22a6728ba4ecf69274ab03537faeed5333775991716bf0b5f439e.fHsD0XKPnud-oDM4PLJ7edCbMnVcW6uVMdMHBYp3K08
581573
loops-loopslistview--with-builder-sessions--light:
582-
hash: v1.k4693efd2.fb97e3fe3205ddf71d98c4e35446c73e394aaa9761d3bb35b146ddafc438df4d.UUEWc0G12NQibZTyskIHGS62mB36gKJ594GrJp1E9II
574+
hash: v1.k4693efd2.fb6c9b85bd660a6be4645cf7ba8fe3ab08a169e3823e5354325b7d7ecbe13aba.4FbKrdxiUN1PZOGwFa_kl7Rpz3Z8DnTVNJvUM_3FIp0
583575
scouts-scoutsfleetlist--filtered-to-you--dark:
584576
hash: v1.k4693efd2.a3af6e76ef36598eaa347bedc4430878ed62754660166398e0576a9978cd084b.x3Ky-O6HOw0srWdOmnnKJwFNpmGmQVWip0t7CwVaRXY
585577
scouts-scoutsfleetlist--filtered-to-you--light:

‎apps/mobile/src/features/tasks/composer/options.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,16 @@ export const MODELS: ModelOption[] = [
4242
description: "Newest, most capable",
4343
supportsReasoning: true,
4444
},
45+
{
46+
value: "claude-opus-5",
47+
label: "Claude Opus 5",
48+
description: "Most capable, slower",
49+
supportsReasoning: true,
50+
},
4551
{
4652
value: "claude-opus-4-8",
4753
label: "Claude Opus 4.8",
48-
description: "Most capable, slower",
54+
description: "Previous Opus generation",
4955
supportsReasoning: true,
5056
},
5157
{

‎apps/mobile/src/lib/githubIssueUrl.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,28 @@ export interface ParsedGithubIssueUrl {
99
}
1010

1111
const GITHUB_ISSUE_URL_PATTERN =
12-
/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(issues|pull)\/(\d+)(?:[/?#].*)?$/;
12+
/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(issues|pull)\/(\d+)([/?#].*)?$/;
1313

1414
export function parseGithubIssueUrl(text: string): ParsedGithubIssueUrl | null {
1515
const trimmed = text.trim();
1616
const match = trimmed.match(GITHUB_ISSUE_URL_PATTERN);
1717
if (!match) return null;
1818

19-
const [, owner, repo, segment, rawNumber] = match;
19+
const [, owner, repo, segment, rawNumber, suffix = ""] = match;
2020
const number = Number(rawNumber);
2121
if (!Number.isInteger(number) || number <= 0) return null;
2222

2323
const kind: GithubRefKind = segment === "pull" ? "pr" : "issue";
24+
// A fragment like #discussion_r123 anchors a specific comment, so keep the
25+
// whole suffix — anchors such as #r123 only resolve on the /files subpage
26+
// they were copied from. Without a fragment the tab/query suffix is noise.
27+
const hashIndex = suffix.indexOf("#");
28+
const hasFragment = hashIndex !== -1 && hashIndex < suffix.length - 1;
2429
return {
2530
kind,
2631
owner,
2732
repo,
2833
number,
29-
normalizedUrl: `https://github.com/${owner}/${repo}/${segment}/${number}`,
34+
normalizedUrl: `https://github.com/${owner}/${repo}/${segment}/${number}${hasFragment ? suffix : ""}`,
3035
};
3136
}

‎packages/agent/README.md‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ Required environment variables (validated by zod in `src/server/bin.ts`):
171171
- `POSTHOG_PERSONAL_API_KEY` — API key for PostHog requests
172172
- `POSTHOG_PROJECT_ID` — numeric project ID
173173

174+
Optional run telemetry (the logs pair must both be set, otherwise telemetry stays off):
175+
176+
- `POSTHOG_AGENT_OTEL_LOGS_URL` — full OTLP logs URL for run metadata, e.g. `https://us.i.posthog.com/i/v1/logs`
177+
- `POSTHOG_AGENT_OTEL_LOGS_TOKEN` — project API key of the telemetry project
178+
- `POSTHOG_AGENT_OTEL_TRACES_URL` — full OTLP traces URL, e.g. `https://us.i.posthog.com/i/v1/traces`; additionally enables one APM trace per run
179+
180+
When set, `AgentServer` ships an allowlisted metadata subset of the session log (run/turn/tool lifecycle, usage, error provenance — never message content, tool arguments, or raw error text; see `src/otel-telemetry.ts`) to PostHog Logs, tagged with `service.name=posthog-code-agent` and `run_id`/`task_id`/`team_id`/`user_id`/`distinct_id` resource attributes so cloud runs are filterable per user. With the traces URL set, each run also produces an APM trace (`task_run` root span, a `turn` span per prompt, a `tool_call:<kind>` span per tool call; see `src/otel-trace-builder.ts`), and log records carry the matching trace/span ids so Logs and APM cross-link.
181+
182+
The `task_run` root span is ended and exported at the run's in-process terminal point — a background run's prompt settling, a terminal failure, or session cleanup (`close`). It cannot wait for teardown: agent-server is an exec'd process inside the sandbox, so `docker stop` / Modal terminate kill it without SIGTERM ever arriving, and a span still open at that moment is lost. Interactive sessions that end via hard teardown (e.g. inactivity timeout) therefore lose the root span; turn/tool spans and logs still assemble under the same trace id.
183+
174184
## Agent SDK
175185

176186
The `Agent` class (`src/agent.ts`) is the entrypoint for local/programmatic usage. It handles LLM gateway configuration, log writer setup, and model filtering — then delegates to `createAcpConnection()`.
@@ -209,12 +219,9 @@ Logs serve two purposes: real-time observability and session resume. Every ACP m
209219

210220
### Writing logs
211221

212-
`SessionLogWriter` (`src/session-log-writer.ts`) is a per-session multiplexer that buffers raw ndJson lines. On flush (auto-scheduled 500ms after writes, or explicit), it dispatches to whichever backend is configured:
213-
214-
- **OTEL** (`src/otel-log-writer.ts`) — preferred path. Creates an OpenTelemetry `LoggerProvider` per session with resource attributes (`task_id`, `run_id`, `device_type`) set once and indexed via `resource_fingerprint`. Each ndJson line is emitted as an OTEL log record with an `event_type` attribute (the ACP method name) and exported via OTLP HTTP to PostHog's `/i/v1/agent-logs` endpoint. Batch flush interval defaults to 500ms.
215-
- **Legacy S3** — falls back to `PostHogAPIClient.appendTaskRunLog()`, which POSTs batched `StoredNotification` entries to the Django API. The API stores them as the task run's `log_url`.
222+
`SessionLogWriter` (`src/session-log-writer.ts`) is a per-session multiplexer that buffers raw ndJson lines. On flush (auto-scheduled 500ms after writes, or explicit), it POSTs batched `StoredNotification` entries to the Django API via `PostHogAPIClient.appendTaskRunLog()`; the API stores them in S3 as the task run's `log_url`. This S3 log is the source of truth for the full transcript and for session resume.
216223

217-
Both backends can be active simultaneously — OTEL for fast indexed queries, S3 for full log download.
224+
Independently of that flush cycle, the writer tees every parsed non-chunk entry to optional `SessionLogSink`s at append time. In cloud, `OtelRunTelemetry` (`src/otel-telemetry.ts`) is wired as a sink and exports an allowlisted metadata subset (run/turn/tool lifecycle, usage, error provenance — never message content, tool arguments, or raw error text) to PostHog Logs, plus an APM trace per run when configured. See "Optional run telemetry" above for the env vars and behavior. Sink failures can never break S3 log persistence.
218225

219226
### Resuming from logs
220227

‎packages/agent/package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,13 @@
163163
"@earendil-works/pi-coding-agent": "catalog:",
164164
"@hono/node-server": "^1.19.9",
165165
"@openai/codex": "0.144.0",
166+
"@opentelemetry/api": "^1.9.1",
166167
"@opentelemetry/api-logs": "^0.208.0",
167168
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
169+
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
168170
"@opentelemetry/resources": "^2.0.0",
169171
"@opentelemetry/sdk-logs": "^0.208.0",
172+
"@opentelemetry/sdk-trace-base": "^2.8.0",
170173
"@opentelemetry/semantic-conventions": "^1.28.0",
171174
"@types/jsonwebtoken": "^9.0.10",
172175
"commander": "^14.0.2",

‎packages/agent/src/adapters/claude/session/jsonl-hydration.test.ts‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,37 @@ describe("hydrateSessionJsonl", () => {
15951595
};
15961596
}
15971597

1598+
it("returns the selected conversation when hydrating from logs", async () => {
1599+
const posthogAPI = {
1600+
getTaskRun: vi.fn().mockResolvedValue({ log_url: "https://logs.test" }),
1601+
fetchTaskRunLogs: vi.fn().mockResolvedValue([
1602+
entry("user_message", {
1603+
content: { type: "text", text: "previous request" },
1604+
}),
1605+
]),
1606+
} as unknown as PostHogAPIClient;
1607+
const log = { info: vi.fn(), warn: vi.fn() };
1608+
1609+
const result = await hydrateSessionJsonl({
1610+
sessionId,
1611+
cwd,
1612+
taskId: "t1",
1613+
runId: "r1",
1614+
posthogAPI,
1615+
log,
1616+
});
1617+
1618+
expect(result).toEqual({
1619+
hasSession: true,
1620+
conversation: [
1621+
{
1622+
role: "user",
1623+
content: [{ type: "text", text: "previous request" }],
1624+
},
1625+
],
1626+
});
1627+
});
1628+
15981629
it("sanitizes an existing file and skips S3 hydration", async () => {
15991630
const file = await writeSessionFile();
16001631
const { posthogAPI, log } = makeDeps();
@@ -1608,7 +1639,7 @@ describe("hydrateSessionJsonl", () => {
16081639
log,
16091640
});
16101641

1611-
expect(result).toBe(true);
1642+
expect(result).toEqual({ hasSession: true });
16121643
expect(
16131644
(posthogAPI as unknown as { getTaskRun: ReturnType<typeof vi.fn> })
16141645
.getTaskRun,
@@ -1635,7 +1666,7 @@ describe("hydrateSessionJsonl", () => {
16351666
log,
16361667
});
16371668

1638-
expect(result).toBe(true);
1669+
expect(result).toEqual({ hasSession: true });
16391670
expect(log.warn).toHaveBeenCalledWith(
16401671
"Failed to sanitize existing session JSONL",
16411672
expect.anything(),

‎packages/agent/src/adapters/claude/session/jsonl-hydration.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ import { isEmptyContentBlock } from "../../../utils/acp-content";
1010
import { neutralizeUnprocessableImages } from "../image-sanitization";
1111
import { supports1MContext } from "./models";
1212

13-
interface ConversationTurn {
13+
export interface ConversationTurn {
1414
role: "user" | "assistant";
1515
content: ContentBlock[];
1616
toolCalls?: ToolCallInfo[];
1717
}
1818

19+
export interface HydrateSessionJsonlResult {
20+
hasSession: boolean;
21+
conversation?: ConversationTurn[];
22+
}
23+
1924
interface ToolCallInfo {
2025
toolCallId: string;
2126
toolName: string;
@@ -681,7 +686,7 @@ export async function hydrateSessionJsonl(params: {
681686
permissionMode?: string;
682687
posthogAPI: PostHogAPIClient;
683688
log: HydrationLog;
684-
}): Promise<boolean> {
689+
}): Promise<HydrateSessionJsonlResult> {
685690
const { posthogAPI, log } = params;
686691

687692
try {
@@ -702,21 +707,21 @@ export async function hydrateSessionJsonl(params: {
702707
error: err instanceof Error ? err.message : String(err),
703708
});
704709
}
705-
return true;
710+
return { hasSession: true };
706711
} catch {
707712
// File doesn't exist, proceed with hydration
708713
}
709714

710715
const taskRun = await posthogAPI.getTaskRun(params.taskId, params.runId);
711716
if (!taskRun.log_url) {
712717
log.info("No log URL, skipping JSONL hydration");
713-
return false;
718+
return { hasSession: false };
714719
}
715720

716721
const entries = await posthogAPI.fetchTaskRunLogs(taskRun);
717722
if (entries.length === 0) {
718723
log.info("No S3 log entries, skipping JSONL hydration");
719-
return false;
724+
return { hasSession: false };
720725
}
721726

722727
const entryCounts: Record<string, number> = {};
@@ -742,7 +747,7 @@ export async function hydrateSessionJsonl(params: {
742747

743748
if (allTurns.length === 0) {
744749
log.info("No conversation to hydrate, skipping JSONL hydration");
745-
return false;
750+
return { hasSession: false };
746751
}
747752

748753
const maxTokens = supports1MContext(params.model ?? "")
@@ -774,12 +779,12 @@ export async function hydrateSessionJsonl(params: {
774779
turns: conversation.length,
775780
lines: jsonlLines.length,
776781
});
777-
return true;
782+
return { hasSession: true, conversation };
778783
} catch (err) {
779784
log.warn("Failed to hydrate session JSONL, continuing", {
780785
sessionId: params.sessionId,
781786
error: err instanceof Error ? err.message : String(err),
782787
});
783-
return false;
788+
return { hasSession: false };
784789
}
785790
}

‎packages/agent/src/adapters/claude/session/models.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ describe("toSdkModelId", () => {
3030
expect(toSdkModelId("claude-sonnet-5")).toBe("claude-sonnet-5");
3131
});
3232

33+
it("passes claude-opus-5 through unchanged (no SDK alias)", () => {
34+
expect(toSdkModelId("claude-opus-5")).toBe("claude-opus-5");
35+
});
36+
3337
it("passes deprecated gateway IDs through unchanged", () => {
3438
expect(toSdkModelId("claude-opus-4-6")).toBe("claude-opus-4-6");
3539
expect(toSdkModelId("claude-sonnet-4-5")).toBe("claude-sonnet-4-5");
@@ -67,6 +71,13 @@ describe("model capability flags", () => {
6771
xhighEffort: true,
6872
mcpInjection: true,
6973
},
74+
{
75+
modelId: "claude-opus-5",
76+
oneMContext: true,
77+
effort: true,
78+
xhighEffort: true,
79+
mcpInjection: true,
80+
},
7081
{
7182
modelId: "claude-sonnet-4-6",
7283
oneMContext: true,
@@ -123,6 +134,7 @@ describe("resolveEffortForModel", () => {
123134
["claude-fable-5", undefined, "high"],
124135
["claude-opus-4-8", undefined, "high"],
125136
["claude-opus-4-7", undefined, "high"],
137+
["claude-opus-5", undefined, "high"],
126138
["claude-sonnet-4-6", undefined, "high"],
127139
["claude-sonnet-5", undefined, "high"],
128140
["@cf/zai-org/glm-5.2", undefined, "high"],

‎packages/agent/src/adapters/claude/session/models.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function toSdkModelId(modelId: string): string {
2424
const MODELS_WITH_1M_CONTEXT = new Set([
2525
"claude-opus-4-7",
2626
"claude-opus-4-8",
27+
"claude-opus-5",
2728
"claude-sonnet-4-6",
2829
"claude-sonnet-5",
2930
"claude-fable-5",
@@ -46,6 +47,7 @@ const EXTENDED_EFFORT_LEVELS: readonly EffortLevel[] = [
4647
const MODEL_EFFORT_LEVELS: Readonly<Record<string, readonly EffortLevel[]>> = {
4748
"claude-opus-4-7": EXTENDED_EFFORT_LEVELS,
4849
"claude-opus-4-8": EXTENDED_EFFORT_LEVELS,
50+
"claude-opus-5": EXTENDED_EFFORT_LEVELS,
4951
"claude-sonnet-4-6": STANDARD_EFFORT_LEVELS,
5052
"claude-sonnet-5": EXTENDED_EFFORT_LEVELS,
5153
"claude-fable-5": EXTENDED_EFFORT_LEVELS,

‎packages/agent/src/adapters/error-classification.test.ts‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { describe, expect, it } from "vitest";
2-
import { classifyAgentError } from "./error-classification";
2+
import {
3+
classifyAgentError,
4+
isPromptTooLongError,
5+
} from "./error-classification";
36

47
describe("classifyAgentError", () => {
58
it.each([
@@ -22,6 +25,8 @@ describe("classifyAgentError", () => {
2225
["API Error: 429 rate limited", "upstream_provider_failure"],
2326
["API Error: 529 overloaded", "upstream_provider_failure"],
2427
["API Error: 400 invalid request", "agent_error"],
28+
// 413 is a hard client rejection, never a transient upstream failure.
29+
["API Error: 413 Payload Too Large", "agent_error"],
2530
[
2631
"Connection closed mid-response without the API Error prefix",
2732
"agent_error",
@@ -32,3 +37,40 @@ describe("classifyAgentError", () => {
3237
expect(classifyAgentError(message)).toBe(expected);
3338
});
3439
});
40+
41+
describe("isPromptTooLongError", () => {
42+
it.each([
43+
[
44+
'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 214431 tokens > 204698 maximum"}}',
45+
true,
46+
],
47+
[
48+
'API Error: 413 {"error":{"message":"litellm.ContextWindowExceededError: The estimated number of input and maximum output tokens (262334) exceeded this model context window limit (262144)","code":"5021"}}',
49+
true,
50+
],
51+
// Must match without the "API Error: 413" prefix.
52+
[
53+
"litellm.ContextWindowExceededError: The estimated number of input and maximum output tokens (262334) exceeded this model context window limit (262144)",
54+
true,
55+
],
56+
// The ACP-wrapped shape the agent-server catch actually sees.
57+
[
58+
'Internal error: API Error: 413 {"error":{"message":"exceeded this model context window limit (262144)"}}',
59+
true,
60+
],
61+
// Any gateway 413 means an oversized payload, whatever the body text.
62+
["API Error: 413 Payload Too Large", true],
63+
// Pins the 413 matcher's i flag.
64+
["api error: 413 payload too large", true],
65+
["API Error: 429 rate limited", false],
66+
["API Error: 400 invalid request", false],
67+
["some unrelated failure", false],
68+
] as const)("detects %j as %s", (message, expected) => {
69+
expect(isPromptTooLongError(new Error(message))).toBe(expected);
70+
});
71+
72+
it("handles non-Error inputs", () => {
73+
expect(isPromptTooLongError({ message: "prompt is too long" })).toBe(true);
74+
expect(isPromptTooLongError(undefined)).toBe(false);
75+
});
76+
});

0 commit comments

Comments
 (0)