| author | |
| committer | |
| log | 6530de0339d2ca49957d0039133c49e3a08557f7 |
| tree | 5e98cd0e6938f1c38e09c4df3a23b464684e2403 |
| parent | 75bf92e652f72cb0062c102236aeb130d1d714b3 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>12 files changed, 187 insertions(+), 10 deletions(-)
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+5| ... | ... | @@ -84,6 +84,7 @@ describe("CheckpointDiffQuery.layer", () => { |
| 84 | 84 | getShellSnapshot: () => |
| 85 | 85 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), |
| 86 | 86 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 87 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 87 | 88 | getArchivedShellSnapshot: () => |
| 88 | 89 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), |
| 89 | 90 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), |
| ... | ... | @@ -201,6 +202,7 @@ describe("CheckpointDiffQuery.layer", () => { |
| 201 | 202 | getShellSnapshot: () => |
| 202 | 203 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), |
| 203 | 204 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 205 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 204 | 206 | getArchivedShellSnapshot: () => |
| 205 | 207 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), |
| 206 | 208 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), |
| ... | ... | @@ -293,6 +295,7 @@ describe("CheckpointDiffQuery.layer", () => { |
| 293 | 295 | getShellSnapshot: () => |
| 294 | 296 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), |
| 295 | 297 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 298 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 296 | 299 | getArchivedShellSnapshot: () => |
| 297 | 300 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), |
| 298 | 301 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), |
| ... | ... | @@ -370,6 +373,7 @@ describe("CheckpointDiffQuery.layer", () => { |
| 370 | 373 | getShellSnapshot: () => |
| 371 | 374 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), |
| 372 | 375 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 376 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 373 | 377 | getArchivedShellSnapshot: () => |
| 374 | 378 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), |
| 375 | 379 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), |
| ... | ... | @@ -432,6 +436,7 @@ describe("CheckpointDiffQuery.layer", () => { |
| 432 | 436 | getShellSnapshot: () => |
| 433 | 437 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), |
| 434 | 438 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 439 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 435 | 440 | getArchivedShellSnapshot: () => |
| 436 | 441 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), |
| 437 | 442 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), |
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+1| ... | ... | @@ -435,6 +435,7 @@ describe("OrchestrationEngine", () => { |
| 435 | 435 | updatedAt: projectionSnapshot.updatedAt, |
| 436 | 436 | }), |
| 437 | 437 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 438 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 438 | 439 | getArchivedShellSnapshot: () => |
| 439 | 440 | Effect.succeed({ |
| 440 | 441 | snapshotSequence: projectionSnapshot.snapshotSequence, |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+71| ... | ... | @@ -3503,6 +3503,77 @@ it.effect("omits foreign-host PRs from legacy snapshots while preserving native |
| 3503 | 3503 | }).pipe(Effect.provide(layer)); |
| 3504 | 3504 | }); |
| 3505 | 3505 | |
| 3506 | it.effect( | |
| 3507 | "lists linked threads like the shell snapshot, in one query and without identities", | |
| 3508 | () => { | |
| 3509 | const resolved: string[] = []; | |
| 3510 | const layer = OrchestrationProjectionSnapshotQueryLive.pipe( | |
| 3511 | Layer.provide(ThreadBackgroundLiveness.layer), | |
| 3512 | Layer.provide(ThreadPlanProgress.layer), | |
| 3513 | Layer.provide( | |
| 3514 | Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, { | |
| 3515 | resolve: (root) => | |
| 3516 | Effect.sync(() => { | |
| 3517 | resolved.push(root); | |
| 3518 | return null; | |
| 3519 | }), | |
| 3520 | }), | |
| 3521 | ), | |
| 3522 | Layer.provideMerge(SqlitePersistenceMemory), | |
| 3523 | ); | |
| 3524 | return Effect.gen(function* () { | |
| 3525 | const sql = yield* SqlClient.SqlClient; | |
| 3526 | const query = yield* ProjectionSnapshotQuery; | |
| 3527 | yield* sql`INSERT INTO projection_projects (project_id, title, workspace_root, scripts_json, created_at, updated_at) | |
| 3528 | VALUES ('p1', 'One', '/one', '[]', '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z'), | |
| 3529 | ('p2', 'Two', '/two', '[]', '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z')`; | |
| 3530 | yield* sql`INSERT INTO projection_threads (thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, created_at, updated_at, archived_at, deleted_at, settled_override, settled_at) | |
| 3531 | VALUES | |
| 3532 | ('t-late', 'p1', 'Late', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', '2026-09-03T00:00:00Z', '2026-09-03T00:00:00Z', NULL, NULL, 'settled', '2026-09-04T00:00:00Z'), | |
| 3533 | ('t-early', 'p2', 'Early', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z', NULL, NULL, NULL, NULL), | |
| 3534 | ('t-first', 'p1', 'First', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', '2026-09-02T00:00:00Z', '2026-09-02T00:00:00Z', NULL, NULL, NULL, NULL), | |
| 3535 | ('t-plain', 'p1', 'Plain', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', '2026-09-02T00:00:00Z', '2026-09-02T00:00:00Z', NULL, NULL, NULL, NULL), | |
| 3536 | ('t-archived', 'p1', 'Archived', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', '2026-09-02T00:00:00Z', '2026-09-02T00:00:00Z', '2026-09-05T00:00:00Z', NULL, NULL, NULL), | |
| 3537 | ('t-deleted', 'p1', 'Deleted', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', '2026-09-02T00:00:00Z', '2026-09-02T00:00:00Z', NULL, '2026-09-05T00:00:00Z', NULL, NULL)`; | |
| 3538 | yield* sql`INSERT INTO projection_thread_pull_requests (thread_id, host, repository, number, url, source, linked_at, snapshot_json) | |
| 3539 | VALUES | |
| 3540 | ('t-late', 'github.com', 'acme/web', 3, 'https://github.com/acme/web/pull/3', 'manual', '2026-09-03T00:00:00Z', NULL), | |
| 3541 | ('t-early', 'github.com', 'acme/api', 4, 'https://github.com/acme/api/pull/4', 'agent', '2026-09-01T00:00:00Z', NULL), | |
| 3542 | ('t-first', 'github.com', 'acme/web', 2, 'https://github.com/acme/web/pull/2', 'stack-dismissed', '2026-09-02T00:00:00Z', NULL), | |
| 3543 | ('t-first', 'github.com', 'acme/web', 1, 'https://github.com/acme/web/pull/1', 'created', '2026-09-02T00:00:00Z', | |
| 3544 | '{"state":"open","title":"One","headBranch":"one","baseBranch":"main","isDraft":false,"updatedAt":null,"syncedAt":"2026-09-02T00:00:00Z"}'), | |
| 3545 | ('t-archived', 'github.com', 'acme/web', 5, 'https://github.com/acme/web/pull/5', 'manual', '2026-09-02T00:00:00Z', NULL), | |
| 3546 | ('t-deleted', 'github.com', 'acme/web', 6, 'https://github.com/acme/web/pull/6', 'manual', '2026-09-02T00:00:00Z', NULL)`; | |
| 3547 | const expected = (yield* query.getShellSnapshot()).threads | |
| 3548 | .filter((thread) => thread.pullRequests.length > 0) | |
| 3549 | .map(({ id, projectId, settledOverride, settledAt, pullRequests }) => ({ | |
| 3550 | id, | |
| 3551 | projectId, | |
| 3552 | settledOverride, | |
| 3553 | settledAt, | |
| 3554 | pullRequests, | |
| 3555 | })); | |
| 3556 | resolved.length = 0; | |
| 3557 | ||
| 3558 | const counter = makeSqlStatementCounter(); | |
| 3559 | const threads = yield* query | |
| 3560 | .listThreadsWithPullRequests() | |
| 3561 | .pipe(Effect.withTracer(counter.tracer)); | |
| 3562 | assert.deepStrictEqual( | |
| 3563 | threads.map((thread) => [thread.id, thread.pullRequests.map((link) => link.number)]), | |
| 3564 | [ | |
| 3565 | ["t-first", [1, 2]], | |
| 3566 | ["t-late", [3]], | |
| 3567 | ["t-early", [4]], | |
| 3568 | ], | |
| 3569 | ); | |
| 3570 | assert.deepStrictEqual(threads, expected); | |
| 3571 | assert.strictEqual(counter.count(), 1); | |
| 3572 | assert.deepStrictEqual(resolved, []); | |
| 3573 | }).pipe(Effect.provide(layer)); | |
| 3574 | }, | |
| 3575 | ); | |
| 3576 | ||
| 3506 | 3577 | projectionSnapshotLayer("ProjectionSnapshotQuery activities by kind", (it) => { |
| 3507 | 3578 | it.effect("lists one kind across active threads only, without hydrating the threads", () => |
| 3508 | 3579 | Effect.gen(function* () { |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+66| ... | ... | @@ -77,6 +77,7 @@ import { |
| 77 | 77 | type ProjectionSnapshotCounts, |
| 78 | 78 | type ProjectionThreadCheckpointContext, |
| 79 | 79 | type ProjectionThreadDetailQuery, |
| 80 | type ProjectionThreadPullRequests, | |
| 80 | 81 | type ProjectionSnapshotQueryShape, |
| 81 | 82 | } from "../Services/ProjectionSnapshotQuery.ts"; |
| 82 | 83 | |
| ... | ... | @@ -802,6 +803,41 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 802 | 803 | `, |
| 803 | 804 | }); |
| 804 | 805 | |
| 806 | // One row per link, in the shell snapshot's thread order and link order. | |
| 807 | const listActiveThreadPullRequestSyncRows = SqlSchema.findAll({ | |
| 808 | Request: Schema.Void, | |
| 809 | Result: ProjectionThreadPullRequestDbRowSchema.mapFields( | |
| 810 | Struct.assign({ | |
| 811 | projectId: ProjectionThread.fields.projectId, | |
| 812 | settledOverride: ProjectionThread.fields.settledOverride, | |
| 813 | settledAt: ProjectionThread.fields.settledAt, | |
| 814 | }), | |
| 815 | ), | |
| 816 | execute: () => | |
| 817 | sql` | |
| 818 | SELECT | |
| 819 | links.thread_id AS "threadId", | |
| 820 | threads.project_id AS "projectId", | |
| 821 | threads.settled_override AS "settledOverride", | |
| 822 | threads.settled_at AS "settledAt", | |
| 823 | links.host, | |
| 824 | links.repository, | |
| 825 | links.number, | |
| 826 | links.url, | |
| 827 | links.source, | |
| 828 | links.linked_at AS "linkedAt", | |
| 829 | links.snapshot_json AS "snapshot", | |
| 830 | links.stack_json AS "stack" | |
| 831 | FROM projection_thread_pull_requests links | |
| 832 | INNER JOIN projection_threads threads | |
| 833 | ON threads.thread_id = links.thread_id | |
| 834 | WHERE threads.deleted_at IS NULL | |
| 835 | AND threads.archived_at IS NULL | |
| 836 | ORDER BY threads.project_id ASC, threads.created_at ASC, threads.thread_id ASC, | |
| 837 | links.linked_at ASC, links.number ASC | |
| 838 | `, | |
| 839 | }); | |
| 840 | ||
| 805 | 841 | const listArchivedThreadPullRequestRows = SqlSchema.findAll({ |
| 806 | 842 | Request: Schema.Void, |
| 807 | 843 | Result: ProjectionThreadPullRequestDbRowSchema, |
| ... | ... | @@ -2783,6 +2819,35 @@ pending_approval_requests AS ( |
| 2783 | 2819 | }), |
| 2784 | 2820 | ); |
| 2785 | 2821 | |
| 2822 | const listThreadsWithPullRequests: ProjectionSnapshotQueryShape["listThreadsWithPullRequests"] = | |
| 2823 | () => | |
| 2824 | listActiveThreadPullRequestSyncRows(undefined).pipe( | |
| 2825 | Effect.map((rows) => { | |
| 2826 | const threads = new Map< | |
| 2827 | ThreadId, | |
| 2828 | ProjectionThreadPullRequests & { readonly pullRequests: Array<ThreadPullRequestLink> } | |
| 2829 | >(); | |
| 2830 | for (const row of rows) { | |
| 2831 | const thread = threads.get(row.threadId) ?? { | |
| 2832 | id: row.threadId, | |
| 2833 | projectId: row.projectId, | |
| 2834 | settledOverride: row.settledOverride, | |
| 2835 | settledAt: row.settledAt, | |
| 2836 | pullRequests: [], | |
| 2837 | }; | |
| 2838 | thread.pullRequests.push(mapPullRequestRow(row)); | |
| 2839 | threads.set(row.threadId, thread); | |
| 2840 | } | |
| 2841 | return [...threads.values()]; | |
| 2842 | }), | |
| 2843 | Effect.mapError( | |
| 2844 | toPersistenceSqlOrDecodeError( | |
| 2845 | "ProjectionSnapshotQuery.listThreadsWithPullRequests:query", | |
| 2846 | "ProjectionSnapshotQuery.listThreadsWithPullRequests:decodeRows", | |
| 2847 | ), | |
| 2848 | ), | |
| 2849 | ); | |
| 2850 | ||
| 2786 | 2851 | const getArchivedShellSnapshot: ProjectionSnapshotQueryShape["getArchivedShellSnapshot"] = () => |
| 2787 | 2852 | sql |
| 2788 | 2853 | .withTransaction( |
| ... | ... | @@ -3778,6 +3843,7 @@ pending_approval_requests AS ( |
| 3778 | 3843 | listActivitiesByKind, |
| 3779 | 3844 | getSnapshot, |
| 3780 | 3845 | getShellSnapshot, |
| 3846 | listThreadsWithPullRequests, | |
| 3781 | 3847 | getArchivedShellSnapshot, |
| 3782 | 3848 | getDeletedWorktreeThreads, |
| 3783 | 3849 | searchThreads, |
apps/server/src/orchestration/PullRequestSyncReactor.test.ts+13-1| ... | ... | @@ -172,6 +172,7 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options: |
| 172 | 172 | const snapshots = yield* Ref.make(options.snapshot); |
| 173 | 173 | const events = yield* PubSub.unbounded<OrchestrationEvent>(); |
| 174 | 174 | const snapshotReads = yield* Queue.unbounded<void>(); |
| 175 | const shellSnapshotReads = yield* Ref.make(0); | |
| 175 | 176 | const syncCommands = yield* Ref.make<ReadonlyArray<SyncCommand>>([]); |
| 176 | 177 | const linkCommands = yield* Ref.make<ReadonlyArray<LinkCommand>>([]); |
| 177 | 178 | const summaryCalls = yield* Ref.make<ReadonlyArray<PullRequestRef>>([]); |
| ... | ... | @@ -209,8 +210,16 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options: |
| 209 | 210 | |
| 210 | 211 | const dependencies = Layer.mergeAll( |
| 211 | 212 | Layer.mock(ProjectionSnapshotQuery)({ |
| 213 | listThreadsWithPullRequests: () => | |
| 214 | Queue.offer(snapshotReads, undefined).pipe( | |
| 215 | Effect.andThen(Ref.get(snapshots)), | |
| 216 | Effect.map((snapshot) => snapshot.threads), | |
| 217 | ), | |
| 212 | 218 | getShellSnapshot: () => |
| 213 | Queue.offer(snapshotReads, undefined).pipe(Effect.andThen(Ref.get(snapshots))), | |
| 219 | Ref.update(shellSnapshotReads, (count) => count + 1).pipe( | |
| 220 | Effect.andThen(Queue.offer(snapshotReads, undefined)), | |
| 221 | Effect.andThen(Ref.get(snapshots)), | |
| 222 | ), | |
| 214 | 223 | }), |
| 215 | 224 | Layer.mock(PullRequestService)({ |
| 216 | 225 | summary, |
| ... | ... | @@ -235,6 +244,7 @@ const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options: |
| 235 | 244 | activation, |
| 236 | 245 | snapshots, |
| 237 | 246 | snapshotReads, |
| 247 | shellSnapshotReads, | |
| 238 | 248 | syncCommands, |
| 239 | 249 | linkCommands, |
| 240 | 250 | summaryCalls, |
| ... | ... | @@ -511,6 +521,8 @@ describe("PullRequestSyncReactor", () => { |
| 511 | 521 | ], |
| 512 | 522 | ); |
| 513 | 523 | assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); |
| 524 | // Reads only linked threads, never the full shell snapshot of every thread. | |
| 525 | assert.strictEqual(yield* Ref.get(fixture.shellSnapshotReads), 0); | |
| 514 | 526 | }).pipe(Effect.provide(fixture.layer)); |
| 515 | 527 | }), |
| 516 | 528 | ), |
apps/server/src/orchestration/PullRequestSyncReactor.ts+7-9| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import { siblingPullRequestUrl } from "@t3tools/shared/changeRequestUrl"; |
| 2 | 2 | import { |
| 3 | 3 | CommandId, |
| 4 | type OrchestrationThreadShell, | |
| 5 | 4 | type PullRequestSummary, |
| 6 | 5 | type ThreadPullRequestKey, |
| 7 | 6 | type ThreadPullRequestLink, |
| ... | ... | @@ -36,7 +35,7 @@ const SLOW_SYNC_INTERVAL_MS = 15 * 60 * 1_000; |
| 36 | 35 | type SnapshotFields = Omit<ThreadPullRequestSnapshot, "syncedAt">; |
| 37 | 36 | |
| 38 | 37 | interface LinkEntry { |
| 39 | readonly thread: OrchestrationThreadShell; | |
| 38 | readonly thread: ProjectionSnapshotQuery.ProjectionThreadPullRequests; | |
| 40 | 39 | readonly link: ThreadPullRequestLink; |
| 41 | 40 | } |
| 42 | 41 | |
| ... | ... | @@ -104,15 +103,15 @@ function stacksEqual( |
| 104 | 103 | ); |
| 105 | 104 | } |
| 106 | 105 | |
| 107 | function isUnsettled(thread: OrchestrationThreadShell): boolean { | |
| 106 | function isUnsettled(thread: ProjectionSnapshotQuery.ProjectionThreadPullRequests): boolean { | |
| 108 | 107 | return thread.settledOverride !== "settled" && thread.settledAt === null; |
| 109 | 108 | } |
| 110 | 109 | |
| 111 | 110 | /** |
| 112 | 111 | * Keeps every thread ↔ pull request link's host snapshot current. One sweep a minute reads |
| 113 | * the shell snapshot, groups visible links by pull request so the host is asked once per PR | |
| 114 | * no matter how many threads share it, and writes back only what changed. Native stacks the | |
| 115 | * host reports are auto-linked to the thread as `source: "stack"`. | |
| 112 | * only the active threads that have links, groups visible links by pull request so the host | |
| 113 | * is asked once per PR no matter how many threads share it, and writes back only what | |
| 114 | * changed. Native stacks the host reports are auto-linked to the thread as `source: "stack"`. | |
| 116 | 115 | */ |
| 117 | 116 | export class PullRequestSyncReactor extends Context.Service< |
| 118 | 117 | PullRequestSyncReactor, |
| ... | ... | @@ -153,14 +152,13 @@ export const make = Effect.gen(function* () { |
| 153 | 152 | Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.logWarning(message, fields); |
| 154 | 153 | |
| 155 | 154 | const sweep = Effect.fn("PullRequestSyncReactor.sweep")(function* (requestedKey?: string) { |
| 156 | const snapshot = yield* snapshots.getShellSnapshot(); | |
| 155 | const threads = yield* snapshots.listThreadsWithPullRequests(); | |
| 157 | 156 | const now = yield* DateTime.now; |
| 158 | 157 | const nowMs = DateTime.toEpochMillis(now); |
| 159 | 158 | const nowIso = DateTime.formatIso(now); |
| 160 | 159 | |
| 161 | 160 | const groups = new Map<string, Array<LinkEntry>>(); |
| 162 | for (const thread of snapshot.threads) { | |
| 163 | if (thread.archivedAt !== null) continue; | |
| 161 | for (const thread of threads) { | |
| 164 | 162 | for (const link of visibleThreadPullRequests(thread.pullRequests)) { |
| 165 | 163 | const key = threadPullRequestKeyOf(link); |
| 166 | 164 | const entries = groups.get(key) ?? []; |
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+16| ... | ... | @@ -64,6 +64,12 @@ export interface ProjectionFullThreadDiffContext { |
| 64 | 64 | readonly toCheckpointRef: CheckpointRef | null; |
| 65 | 65 | } |
| 66 | 66 | |
| 67 | /** The thread fields pull request sync reads, for a thread with at least one link. */ | |
| 68 | export type ProjectionThreadPullRequests = Pick< | |
| 69 | OrchestrationThreadShell, | |
| 70 | "id" | "projectId" | "settledOverride" | "settledAt" | "pullRequests" | |
| 71 | >; | |
| 72 | ||
| 67 | 73 | export interface ProjectionThreadDetailQuery { |
| 68 | 74 | /** |
| 69 | 75 | * Limit activities before SQLite returns and decodes their payloads. |
| ... | ... | @@ -131,6 +137,16 @@ export interface ProjectionSnapshotQueryShape { |
| 131 | 137 | ProjectionRepositoryError |
| 132 | 138 | >; |
| 133 | 139 | |
| 140 | /** | |
| 141 | * Read active (not deleted, not archived) threads that have at least one pull | |
| 142 | * request link, in shell snapshot order. Skips repository identity, so no | |
| 143 | * legacy `linkedPullRequest` is derived. | |
| 144 | */ | |
| 145 | readonly listThreadsWithPullRequests: () => Effect.Effect< | |
| 146 | ReadonlyArray<ProjectionThreadPullRequests>, | |
| 147 | ProjectionRepositoryError | |
| 148 | >; | |
| 149 | ||
| 134 | 150 | /** Durable worktree ownership retained after thread deletion, including across restarts. */ |
| 135 | 151 | readonly getDeletedWorktreeThreads: () => Effect.Effect< |
| 136 | 152 | ReadonlyArray<{ |
apps/server/src/project/AgentSessionScanner.test.ts+1| ... | ... | @@ -48,6 +48,7 @@ const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray< |
| 48 | 48 | updatedAt: "2026-01-01T00:00:00.000Z", |
| 49 | 49 | }), |
| 50 | 50 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 51 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 51 | 52 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 52 | 53 | getSnapshotSequence: () => Effect.die("unused"), |
| 53 | 54 | getCounts: () => Effect.die("unused"), |
apps/server/src/project/ProjectSetupScriptRunner.test.ts+1| ... | ... | @@ -34,6 +34,7 @@ const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) => |
| 34 | 34 | getSnapshot: () => Effect.die("unused"), |
| 35 | 35 | getShellSnapshot: () => Effect.die("unused"), |
| 36 | 36 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 37 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 37 | 38 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 38 | 39 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 1 }), |
| 39 | 40 | getCounts: () => Effect.die("unused"), |
apps/server/src/provider/Layers/ProviderService.test.ts+1| ... | ... | @@ -4969,6 +4969,7 @@ describe("agent browser access", () => { |
| 4969 | 4969 | getSnapshot: () => Effect.die("unused"), |
| 4970 | 4970 | getShellSnapshot: () => Effect.die("unused"), |
| 4971 | 4971 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 4972 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 4972 | 4973 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 4973 | 4974 | getSnapshotSequence: () => Effect.die("unused"), |
| 4974 | 4975 | getCounts: () => Effect.die("unused"), |
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+1| ... | ... | @@ -239,6 +239,7 @@ describe("ProviderSessionReaper", () => { |
| 239 | 239 | getSnapshot: () => Effect.die("unused"), |
| 240 | 240 | getShellSnapshot: () => Effect.die("unused"), |
| 241 | 241 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 242 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 242 | 243 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 243 | 244 | getSnapshotSequence: () => |
| 244 | 245 | Effect.succeed({ snapshotSequence: input.readModel.snapshotSequence }), |
apps/server/src/serverRuntimeStartup.test.ts+4| ... | ... | @@ -169,6 +169,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa |
| 169 | 169 | getSnapshot: () => Effect.die("unused"), |
| 170 | 170 | getShellSnapshot: () => Effect.die("unused"), |
| 171 | 171 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 172 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 172 | 173 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 173 | 174 | getSnapshotSequence: () => Effect.die("unused"), |
| 174 | 175 | getCounts: () => Effect.die("unused"), |
| ... | ... | @@ -298,6 +299,7 @@ it.effect.each([ |
| 298 | 299 | getSnapshot: () => Effect.die("unused"), |
| 299 | 300 | getShellSnapshot: () => Effect.die("unused"), |
| 300 | 301 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 302 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 301 | 303 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 302 | 304 | getSnapshotSequence: () => Effect.die("unused"), |
| 303 | 305 | getCounts: () => Effect.die("unused"), |
| ... | ... | @@ -385,6 +387,7 @@ it.effect( |
| 385 | 387 | getSnapshot: () => Effect.die("unused"), |
| 386 | 388 | getShellSnapshot: () => Effect.die("unused"), |
| 387 | 389 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 390 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 388 | 391 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 389 | 392 | getSnapshotSequence: () => Effect.die("unused"), |
| 390 | 393 | getCounts: () => Effect.die("unused"), |
| ... | ... | @@ -450,6 +453,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa |
| 450 | 453 | getSnapshot: () => Effect.die("unused"), |
| 451 | 454 | getShellSnapshot: () => Effect.die("unused"), |
| 452 | 455 | getDeletedWorktreeThreads: () => Effect.die("unused"), |
| 456 | listThreadsWithPullRequests: () => Effect.die("unused"), | |
| 453 | 457 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 454 | 458 | getSnapshotSequence: () => Effect.die("unused"), |
| 455 | 459 | getCounts: () => Effect.die("unused"), |