Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions AGENT_COORDINATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,11 @@ Server Error) for an unknown slug.
## Active claims (who is editing what RIGHT NOW)

- **2026-07-14 · Codex release captain →**
`convex/domains/agents/fastAgentPanelStreaming.ts#runtime-tier-fallback`,
`convex/domains/agents/orchestrator/queueProtocol.ts#run-terminal-summary`,
`convex/domains/integrations/billing/rateLimiting.ts#internal-usage-accounting`,
`convex/tools/media/linkupSearch.ts#abort-signal`,
`convex/schema.ts#llm-usage-reservation-fields`,
`convex/domains/agents/fastAgentPanelStreaming.ts#provider-message-adapter`,
`src/features/agents/components/FastAgentPanel/FastAgentPanel.tsx#terminal-run-error`,
`.github/workflows/ci.yml#runtime-smoke`, and focused tests · route automatic agent turns onto a tier-eligible fallback and
surface terminal worker errors instead of rendering silence · branch
`fix/agent-tier-fallback-errors`.
and focused tests · production dogfood after #529 selected the correct
tier-eligible model but exposed an empty provider-message list plus an internal
error type in the terminal alert · branch `codex/fast-agent-empty-messages`.

- **2026-06-03 · Claude →** `convex/*#handoff-token`, `src/.../ScratchnodePrivateBridge`,
`src/App.tsx#events-private-route`, `public/proto/home-v5.html#private-handoff` ·
Expand Down
116 changes: 116 additions & 0 deletions convex/__tests__/agentRuntimeTierFallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getTierSelectionFailureReason,
getRuntimeAccessTokenEstimate,
planMeteredProviderStep,
requireRuntimePromptText,
resolveRuntimeBillingContext,
selectTierEligibleRuntimeModel,
type TierModelCheck,
Expand Down Expand Up @@ -301,7 +302,98 @@ describe("cumulative metered provider token planning", () => {
});
});

describe("queued runtime prompt validation", () => {
const promptMessage = {
_id: "prompt-production-shape",
threadId: "thread-production-shape",
status: "success",
message: {
role: "user",
content: "Reply with exactly TIER_OK and nothing else.",
},
};

it("carries the exact production-shaped user prompt across the bounded history boundary", () => {
expect(
requireRuntimePromptText({
messages: [promptMessage],
promptMessageId: promptMessage._id,
threadId: promptMessage.threadId,
}),
).toBe("Reply with exactly TIER_OK and nothing else.");
});

it.each([
{ label: "missing", messages: [] },
{
label: "blank",
messages: [{ ...promptMessage, message: { role: "user", content: " " } }],
},
{
label: "wrong thread",
messages: [{ ...promptMessage, threadId: "thread-other" }],
},
{
label: "non-user",
messages: [
{
...promptMessage,
message: { ...promptMessage.message, role: "assistant" },
},
],
},
{
label: "failed",
messages: [{ ...promptMessage, status: "failed" }],
},
{
label: "missing status",
messages: [{ ...promptMessage, status: undefined }],
},
])("fails closed for a $label prompt row", ({ messages }) => {
expect(() =>
requireRuntimePromptText({
messages,
promptMessageId: promptMessage._id,
threadId: promptMessage.threadId,
}),
).toThrow("Runtime prompt message is missing or empty");
});
});

describe("streamAsync tier-gate wiring", () => {
it("validates an exact prompt before routing and carries it explicitly with zero history", () => {
const actionStart = streamingSource.indexOf("export const streamAsync");
const actionEnd = streamingSource.indexOf(
"export const generateDocumentContent",
actionStart,
);
const actionSource = streamingSource.slice(actionStart, actionEnd);
const promptGuard = actionSource.indexOf(
"const promptText = requireRuntimePromptText({",
);
const tierGate = actionSource.indexOf(
"const tierSelection = await selectTierEligibleRuntimeModel",
);
const reservation = actionSource.indexOf(
"await reserveRuntimeModelAccess(model, attemptKey)",
);
const providerCall = actionSource.indexOf("result = await agent.streamText(");
const providerCallEnd = actionSource.indexOf(
"if (attemptTimedOut)",
providerCall,
);
const providerCallSource = actionSource.slice(providerCall, providerCallEnd);

expect(promptGuard).toBeGreaterThanOrEqual(0);
expect(tierGate).toBeGreaterThan(promptGuard);
expect(reservation).toBeGreaterThan(tierGate);
expect(providerCall).toBeGreaterThan(reservation);
expect(providerCallSource).toContain("promptMessageId: args.promptMessageId");
expect(providerCallSource).toContain("prompt: promptText");
expect(providerCallSource).toContain("recentMessages: 0");
});

it("removes the unmetered dynamic-prompt LLM path from queued streaming", () => {
const actionStart = streamingSource.indexOf("export const streamAsync");
const preflight = streamingSource.indexOf(
Expand Down Expand Up @@ -459,6 +551,30 @@ describe("agent run presentation", () => {
);
});

it("maps custom SDK wrappers and empty-message internals to a safe retry hint", () => {
expect(
formatPublicAgentRunError(
"AgentStreamFailedError: Invalid prompt: messages must not be empty\n at stream (/convex/file.ts:1:1)",
),
).toBe("The agent could not start this request. Please try again.");
expect(
formatPublicAgentRunError(
"Uncaught RuntimePromptError: Runtime prompt message is missing or empty (request id: qa-1)",
),
).toBe("The agent could not start this request. Please try again.");
});

it("maps bare error-class wrappers to the generic public failure", () => {
expect(formatPublicAgentRunError("Error\n at handler")).toBe(
"The agent run failed before producing a response.",
);
expect(
formatPublicAgentRunError(
"Uncaught AgentStreamFailedError\n at stream (/convex/file.ts:1:1)",
),
).toBe("The agent run failed before producing a response.");
});

it("projects terminal error metadata without exposing database-only fields", () => {
const projection = projectAgentRunPresentation({
_id: "run-qa",
Expand Down
9 changes: 8 additions & 1 deletion convex/domains/agents/agentRunPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,18 @@ export function formatPublicAgentRunError(
while (message && message !== previous) {
previous = message;
message = message
.replace(/^Error:\s*/i, "")
.replace(/^Uncaught\s+/i, "")
.replace(/^(?:[A-Za-z_$][\w$.-]*Error|Error)\s*(?::\s*|$)/i, "")
.trim();
}

if (
/^Invalid prompt:\s*messages must not be empty\b/i.test(message) ||
/^Runtime prompt message is missing or empty\b/i.test(message)
) {
return "The agent could not start this request. Please try again.";
}

if (!message) return "The agent run failed before producing a response.";
return message.slice(0, MAX_PUBLIC_RUN_ERROR_LENGTH);
}
Expand Down
29 changes: 15 additions & 14 deletions convex/domains/agents/fastAgentPanelStreaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
getTierSelectionFailureReason,
getRuntimeAccessTokenEstimate,
planMeteredProviderStep,
requireRuntimePromptText,
resolveRuntimeBillingContext,
selectTierEligibleRuntimeModel,
} from "./runtimeTierFallback";
Expand Down Expand Up @@ -250,18 +251,6 @@ async function listThreadMessages(ctx: any, threadId: string, numItems = 30): Pr
return (result?.page ?? result ?? []) as any[];
}

function findPromptMessageText(messages: any[], promptMessageId?: string, fallbackPrompt?: string): string {
if (promptMessageId) {
const found = messages.find(
(message: any) => String(message?.messageId ?? message?.id ?? message?._id ?? "") === promptMessageId,
);
if (typeof found?.text === "string" && found.text.trim()) {
return found.text;
}
}
return String(fallbackPrompt ?? "").trim();
}

async function buildRuntimeDailyBriefSummary(ctx: any): Promise<string | null> {
try {
const brief = await ctx.runQuery(api.domains.research.dailyBriefMemoryQueries.getLatestMemory, {});
Expand Down Expand Up @@ -3295,16 +3284,23 @@ export const streamAsync = internalAction({
summary?: string;
messageId?: string;
} | undefined;
const [threadMessages, persistedScratchpad] = await Promise.all([
const [threadMessages, promptMessages, persistedScratchpad] = await Promise.all([
listThreadMessages(ctx, args.threadId, 32),
ctx.runQuery(components.agent.messages.getMessagesByIds, {
messageIds: [args.promptMessageId],
}),
ctx.runQuery(
internal.domains.agents.agentScratchpads.getByAgentThreadInternal,
{
agentThreadId: args.threadId,
},
),
]);
const promptText = findPromptMessageText(threadMessages, args.promptMessageId);
const promptText = requireRuntimePromptText({
messages: promptMessages,
promptMessageId: args.promptMessageId,
threadId: args.threadId,
});
previousCompactContext =
(persistedScratchpad?.scratchpad?.compactContext as typeof previousCompactContext) ?? undefined;
let workingSetContext:
Expand Down Expand Up @@ -4051,6 +4047,11 @@ export const streamAsync = internalAction({
{ threadId: args.threadId },
{
promptMessageId: args.promptMessageId,
// recentMessages stays at zero so the queued runtime cannot silently
// expand provider context. Supplying the exact validated prompt keeps
// the SDK input non-empty; promptMessageId prevents the SDK from
// saving a duplicate user message.
prompt: promptText,
system: attemptSystemPrompt,
abortSignal: attemptController.signal,
providerOptions,
Expand Down
63 changes: 63 additions & 0 deletions convex/domains/agents/runtimeTierFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,69 @@ export type RuntimeBillingContext =
| "anonymous_user"
| "trusted_evaluation";

type RuntimePromptMessage = {
_id?: unknown;
id?: unknown;
messageId?: unknown;
threadId?: unknown;
role?: unknown;
status?: unknown;
text?: unknown;
content?: unknown;
message?: {
role?: unknown;
content?: unknown;
};
};

/**
* Resolve the exact queued user prompt before routing or reserving provider
* budget. The component query supplying these rows is already scoped to the
* requested message ID; the additional thread/role/status checks make that
* boundary fail closed if queue state is stale or malformed.
*/
export function requireRuntimePromptText(args: {
messages: readonly (RuntimePromptMessage | null | undefined)[];
promptMessageId: string;
threadId: string;
}): string {
const expectedMessageId = args.promptMessageId.trim();
const expectedThreadId = args.threadId.trim();
const promptMessage = args.messages.find((message) => {
const messageId = String(
message?._id ?? message?.messageId ?? message?.id ?? "",
);
return messageId === expectedMessageId;
});
const role = String(
promptMessage?.role ?? promptMessage?.message?.role ?? "",
).trim();
const status = String(promptMessage?.status ?? "").trim();
const threadId = String(promptMessage?.threadId ?? "").trim();
const content =
typeof promptMessage?.text === "string"
? promptMessage.text
: typeof promptMessage?.content === "string"
? promptMessage.content
: typeof promptMessage?.message?.content === "string"
? promptMessage.message.content
: "";

if (
!expectedMessageId ||
!expectedThreadId ||
!promptMessage ||
threadId !== expectedThreadId ||
role !== "user" ||
status !== "success" ||
!content.trim()
) {
throw new Error("Runtime prompt message is missing or empty");
}

return content.trim();
}

export type MeteredProviderStep = {
usage?: {
totalTokens?: number;
Expand Down
Loading