authorgravatar for 142208891+realhasanshoaib@users.noreply.github.comHasan Shoaib <142208891+realhasanshoaib@users.noreply.github.com> 2026-10-14 00:21:47+04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 13:21:47-07:00
log84cb13412ebebd6533e8a3de1579c40ae001573e
treeda12d92875727dce27b0e1988b739cc26c222097
parentfb8d0fc19c27ff860bdefb7dcfe69eae546f837c
signature Signed by PGP key B5690EEEBB952194

fix(server): failed threads no longer scan every tool item for errors (#18628)


7 files changed, 32 insertions(+), 4 deletions(-)

apps/server/src/orchestration-v2/ProjectionSettlement.test.ts+1-1
......@@ -520,7 +520,7 @@ it.effect("shell failure lookups stay on the thread's own turn items", () =>
520520 // node_id, so a node_ordinal lookup walks the whole history once per thread.
521521 const itemLookups = plan.filter((row) => row.detail.startsWith("SEARCH item "));
522522 assert.lengthOf(itemLookups, 2);
523 assert.isTrue(itemLookups.every((row) => row.detail.includes("turn_items_thread_run_idx")));
523 assert.isTrue(itemLookups.every((row) => row.detail.includes("turn_items_failed_error_idx")));
524524 // The pending secret request lookup is bounded the same way.
525525 const secretLookups = plan.filter((row) => row.detail.startsWith("SEARCH secret "));
526526 assert.lengthOf(secretLookups, 1);
apps/server/src/orchestration-v2/ProjectionStore.test.ts+9
......@@ -3426,6 +3426,15 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => {
34263426 );
34273427 assert.isAbove(sessionLookup, bindingLookup, marker);
34283428 assert.include(plan[sessionLookup], "(provider_session_id=?)");
3429 const errorLookups = plan.filter(
3430 (detail) =>
3431 /^SEARCH (?:item|error) /.test(detail) && !detail.includes("(turn_item_id=?)"),
3432 );
3433 assert.lengthOf(errorLookups, marker.includes("blocking") ? 2 : 1, marker);
3434 for (const errorLookup of errorLookups) {
3435 assert.include(errorLookup, "turn_items_failed_error_idx");
3436 assert.include(errorLookup, "(thread_id=? AND run_id=? AND node_id=?)");
3437 }
34293438 }
34303439 }),
34313440 );
apps/server/src/orchestration-v2/ProjectionStore.ts-2
......@@ -5214,7 +5214,6 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
52145214 (
52155215 SELECT item.payload_json
52165216 FROM orchestration_v2_projection_turn_items item
5217 INDEXED BY orchestration_v2_projection_turn_items_thread_run_idx
52185217 INNER JOIN orchestration_v2_projection_runs r ON r.run_id = item.run_id
52195218 WHERE r.run_id = presented.run_id
52205219 AND r.status = 'failed'
......@@ -5231,7 +5230,6 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
52315230 (
52325231 SELECT item.payload_json
52335232 FROM orchestration_v2_projection_turn_items item
5234 INDEXED BY orchestration_v2_projection_turn_items_thread_run_idx
52355233 WHERE item.thread_id = t.thread_id
52365234 AND item.run_id = blocked.run_id
52375235 AND item.type = 'error' AND item.status = 'failed'
apps/server/src/persistence/Migrations.ts+2
......@@ -75,6 +75,7 @@ import Migration0058 from "./Migrations/058_WebhookRelayDeliveries.ts";
7575import Migration0059 from "./Migrations/059_McpAppModelContext.ts";
7676import Migration0060 from "./Migrations/060_ThreadSnapshotWindowIndexes.ts";
7777import Migration0061 from "./Migrations/061_PullRequestReviewGuides.ts";
78import Migration0062 from "./Migrations/062_ThreadFailureLookupIndex.ts";
7879
7980/**
8081 * Migration loader with all migrations defined inline.
......@@ -150,6 +151,7 @@ export const migrationEntries = [
150151 [59, "McpAppModelContext", Migration0059],
151152 [60, "ThreadSnapshotWindowIndexes", Migration0060],
152153 [61, "PullRequestReviewGuides", Migration0061],
154 [62, "ThreadFailureLookupIndex", Migration0062],
153155] as const;
154156
155157export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const);
apps/server/src/persistence/Migrations/055_OrchestrationV2.test.ts+3-1
......@@ -13,7 +13,7 @@ layer("055_OrchestrationV2", (it) => {
1313 Effect.sync(() => {
1414 assert.deepStrictEqual(
1515 migrationEntries.map(([id]) => id),
16 Array.from({ length: 61 }, (_, index) => index + 1),
16 Array.from({ length: 62 }, (_, index) => index + 1),
1717 );
1818 }),
1919 );
......@@ -33,6 +33,7 @@ layer("055_OrchestrationV2", (it) => {
3333 [59, "McpAppModelContext"],
3434 [60, "ThreadSnapshotWindowIndexes"],
3535 [61, "PullRequestReviewGuides"],
36 [62, "ThreadFailureLookupIndex"],
3637 ]);
3738 assert.deepStrictEqual(yield* runMigrations(), []);
3839
......@@ -60,6 +61,7 @@ layer("055_OrchestrationV2", (it) => {
6061 { migration_id: 59, name: "McpAppModelContext" },
6162 { migration_id: 60, name: "ThreadSnapshotWindowIndexes" },
6263 { migration_id: 61, name: "PullRequestReviewGuides" },
64 { migration_id: 62, name: "ThreadFailureLookupIndex" },
6365 ]);
6466
6567 const tables = yield* sql<{ readonly name: string }>`
apps/server/src/persistence/Migrations/062_ThreadFailureLookupIndex.ts created+15
......@@ -0,0 +1,15 @@
1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/sql/SqlClient";
3
4export default Effect.gen(function* () {
5 const sql = yield* SqlClient.SqlClient;
6
7 // Shell and limit-recovery reads need one root error, not every item in a run.
8 yield* sql`
9 CREATE INDEX orchestration_v2_projection_turn_items_failed_error_idx
10 ON orchestration_v2_projection_turn_items(
11 thread_id, run_id, node_id, updated_at DESC, ordinal DESC, turn_item_id DESC
12 )
13 WHERE type = 'error' AND status = 'failed'
14 `;
15});
apps/server/src/persistence/reconcileV2PreviewMigration.test.ts+2
......@@ -42,6 +42,7 @@ describe("V2 preview upgrade", () => {
4242 [59, "McpAppModelContext"],
4343 [60, "ThreadSnapshotWindowIndexes"],
4444 [61, "PullRequestReviewGuides"],
45 [62, "ThreadFailureLookupIndex"],
4546 ]);
4647 assert.deepStrictEqual(yield* runMigrations(), []);
4748 assert.deepStrictEqual(yield* sql`SELECT * FROM orchestration_v2_legacy_imports`, imports);
......@@ -126,6 +127,7 @@ describe("V2 preview upgrade", () => {
126127 [59, "McpAppModelContext"],
127128 [60, "ThreadSnapshotWindowIndexes"],
128129 [61, "PullRequestReviewGuides"],
130 [62, "ThreadFailureLookupIndex"],
129131 ]);
130132 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
131133 );