| author | |
| committer | |
| log | 4d73e1d5cdd32dbfb470e9422421cc40475d5e99 |
| tree | bb302023136c2d6abba27b43edab2bc0eddef565 |
| parent | d765d03cb7f980794c409c38e6ea5e25d89fde33 |
| signature | Signed by PGP key B5690EEEBB952194 |
10 files changed, 186 insertions(+), 2 deletions(-)
apps/server/src/orchestration-v2/Orchestrator.ts+21-2| ... | ... | @@ -3251,6 +3251,27 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 3251 | 3251 | }; |
| 3252 | 3252 | } |
| 3253 | 3253 | })(); |
| 3254 | if ( | |
| 3255 | command.type === "thread.pull-request-link.sync" || | |
| 3256 | command.type === "thread.pull-request-watch.sync" | |
| 3257 | ) { | |
| 3258 | if (updatedThread === thread) return; | |
| 3259 | const link = threadPullRequestsOf(updatedThread).find((candidate) => | |
| 3260 | threadPullRequestKeysEqual(candidate, normalizeThreadPullRequestKey(command)), | |
| 3261 | ); | |
| 3262 | if (link === undefined) return; | |
| 3263 | yield* emit( | |
| 3264 | events, | |
| 3265 | command, | |
| 3266 | )({ | |
| 3267 | type: "thread.pull-request-link-updated", | |
| 3268 | threadId: command.threadId, | |
| 3269 | providerInstanceId: updatedThread.providerInstanceId, | |
| 3270 | occurredAt: now, | |
| 3271 | payload: link, | |
| 3272 | }); | |
| 3273 | return; | |
| 3274 | } | |
| 3254 | 3275 | const eventType = (() => { |
| 3255 | 3276 | switch (command.type) { |
| 3256 | 3277 | case "thread.archive": |
| ... | ... | @@ -3282,9 +3303,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 3282 | 3303 | return "thread.metadata-updated" as const; |
| 3283 | 3304 | case "thread.pull-request.link": |
| 3284 | 3305 | case "thread.pull-request.unlink": |
| 3285 | case "thread.pull-request-link.sync": | |
| 3286 | 3306 | case "thread.pull-request.watch": |
| 3287 | case "thread.pull-request-watch.sync": | |
| 3288 | 3307 | case "thread.pull-request.sync": |
| 3289 | 3308 | return "thread.pull-request-synced" as const; |
| 3290 | 3309 | case "thread.runtime-mode.set": |
apps/server/src/orchestration-v2/ProjectionStore.ts+15| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { applyThreadPullRequestLinkUpdate } from "@t3tools/shared/threadPullRequests"; | |
| 1 | 2 | import { projectTurnItemForWire } from "./WireProjection.ts"; |
| 2 | 3 | import * as Stream from "effect/Stream"; |
| 3 | 4 | import { makeThreadFind, findProjectedThreadItems } from "./ThreadFind.ts"; |
| ... | ... | @@ -718,6 +719,11 @@ export function applyToProjection( |
| 718 | 719 | }; |
| 719 | 720 | |
| 720 | 721 | switch (event.type) { |
| 722 | case "thread.pull-request-link-updated": | |
| 723 | return { | |
| 724 | ...base, | |
| 725 | thread: applyThreadPullRequestLinkUpdate(projection.thread, event.payload), | |
| 726 | }; | |
| 721 | 727 | case "thread.created": |
| 722 | 728 | case "thread.archived": |
| 723 | 729 | case "thread.unarchived": |
| ... | ... | @@ -1823,6 +1829,15 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> = |
| 1823 | 1829 | const apply: ProjectionStoreV2Shape["apply"] = (event) => |
| 1824 | 1830 | Effect.gen(function* () { |
| 1825 | 1831 | switch (event.type) { |
| 1832 | case "thread.pull-request-link-updated": { | |
| 1833 | const thread = yield* getThread(event.threadId); | |
| 1834 | yield* apply({ | |
| 1835 | ...event, | |
| 1836 | type: "thread.pull-request-synced", | |
| 1837 | payload: applyThreadPullRequestLinkUpdate(thread, event.payload), | |
| 1838 | }); | |
| 1839 | break; | |
| 1840 | } | |
| 1826 | 1841 | case "thread.created": |
| 1827 | 1842 | case "thread.archived": |
| 1828 | 1843 | case "thread.unarchived": |
apps/server/src/orchestration-v2/PullRequestSyncReactor.ts+2| ... | ... | @@ -440,6 +440,8 @@ export const make = Effect.gen(function* () { |
| 440 | 440 | yield* forkParked( |
| 441 | 441 | Stream.runForEach(events, (event) => { |
| 442 | 442 | switch (event.type) { |
| 443 | case "thread.pull-request-link-updated": | |
| 444 | return event.payload.snapshot === null ? requestSync(event.payload) : Effect.void; | |
| 443 | 445 | case "thread.pull-request-synced": |
| 444 | 446 | return Effect.forEach( |
| 445 | 447 | visibleThreadPullRequests(event.payload.pullRequests ?? []).filter( |
apps/server/src/orchestration-v2/ThreadSettlementService.ts+1| ... | ... | @@ -606,6 +606,7 @@ export const make = Effect.gen(function* () { |
| 606 | 606 | switch (event.type) { |
| 607 | 607 | case "thread.settled": |
| 608 | 608 | return cleanUpSettledThread(event.threadId); |
| 609 | case "thread.pull-request-link-updated": | |
| 609 | 610 | case "thread.pull-request-synced": |
| 610 | 611 | case "provider-session.detached": |
| 611 | 612 | return worker.enqueue(event.threadId); |
apps/server/src/orchestration-v2/runtimeLayer.test.ts+87| ... | ... | @@ -4860,6 +4860,93 @@ it.layer(layerSharedApplicationDataPlaneTest)("visited projection", (it) => { |
| 4860 | 4860 | ); |
| 4861 | 4861 | }); |
| 4862 | 4862 | |
| 4863 | it.layer(Layer.fresh(layerSharedApplicationDataPlaneTest))("compact PR events", (it) => { | |
| 4864 | it.effect("persists only the changed PR link and replays it without losing other links", () => | |
| 4865 | Effect.gen(function* () { | |
| 4866 | const orchestrator = yield* Orchestrator.OrchestratorV2; | |
| 4867 | const maintenance = yield* ProjectionMaintenance.ProjectionMaintenanceV2; | |
| 4868 | const sql = yield* SqlClient.SqlClient; | |
| 4869 | const threadId = ThreadId.make("compact-pr-sync"); | |
| 4870 | yield* orchestrator.dispatch({ | |
| 4871 | type: "thread.create", | |
| 4872 | createdBy: "user", | |
| 4873 | creationSource: "web", | |
| 4874 | commandId: CommandId.make("compact-pr-create"), | |
| 4875 | threadId, | |
| 4876 | projectId: ProjectId.make("compact-pr-project"), | |
| 4877 | title: "PR sync", | |
| 4878 | modelSelection, | |
| 4879 | runtimeMode: "full-access", | |
| 4880 | interactionMode: "default", | |
| 4881 | branch: null, | |
| 4882 | worktreePath: null, | |
| 4883 | }); | |
| 4884 | const key = { host: "github.com", repository: "pingdotgg/t3code" }; | |
| 4885 | for (const number of [1, 2, 3]) { | |
| 4886 | yield* orchestrator.dispatch({ | |
| 4887 | type: "thread.pull-request.link", | |
| 4888 | commandId: CommandId.make(`compact-pr-link-${number}`), | |
| 4889 | threadId, | |
| 4890 | ...key, | |
| 4891 | number, | |
| 4892 | url: `https://github.com/pingdotgg/t3code/pull/${number}`, | |
| 4893 | source: "manual", | |
| 4894 | }); | |
| 4895 | } | |
| 4896 | const before = yield* orchestrator.getThreadShell(threadId); | |
| 4897 | yield* orchestrator.dispatch({ | |
| 4898 | type: "thread.pull-request-link.sync", | |
| 4899 | commandId: CommandId.make("compact-pr-sync"), | |
| 4900 | threadId, | |
| 4901 | ...key, | |
| 4902 | number: 2, | |
| 4903 | snapshot: { | |
| 4904 | state: "open", | |
| 4905 | title: "Updated PR", | |
| 4906 | headBranch: "feature/pr", | |
| 4907 | baseBranch: "main", | |
| 4908 | isDraft: false, | |
| 4909 | updatedAt: null, | |
| 4910 | syncedAt: "2026-10-13T12:00:00.000Z", | |
| 4911 | }, | |
| 4912 | stack: { | |
| 4913 | kind: "native", | |
| 4914 | id: "updated", | |
| 4915 | number: 2, | |
| 4916 | url: "https://github.com/pingdotgg/t3code/pull/2", | |
| 4917 | base: "main", | |
| 4918 | layers: [], | |
| 4919 | }, | |
| 4920 | }); | |
| 4921 | const rows = yield* sql<{ event_type: string; payload_json: string }>` | |
| 4922 | SELECT event_type, payload_json FROM orchestration_events | |
| 4923 | WHERE command_id = ${"compact-pr-sync"} | |
| 4924 | `; | |
| 4925 | assert.equal(rows.length, 1); | |
| 4926 | assert.equal(rows[0]?.event_type, "thread.pull-request-link-updated"); | |
| 4927 | const payload = JSON.parse(rows[0]!.payload_json); | |
| 4928 | assert.equal(payload.number, 2); | |
| 4929 | assert.isUndefined(payload.pullRequests); | |
| 4930 | const after = yield* orchestrator.getThreadShell(threadId); | |
| 4931 | assert.equal(after?.pullRequests?.length, 3); | |
| 4932 | assert.deepEqual(after?.pullRequests?.[0], before?.pullRequests?.[0]); | |
| 4933 | assert.deepEqual(after?.pullRequests?.[2], before?.pullRequests?.[2]); | |
| 4934 | assert.equal(after?.pullRequests?.[1]?.stack?.id, "updated"); | |
| 4935 | assert.isTrue((yield* maintenance.rebuild).valid); | |
| 4936 | assert.deepEqual( | |
| 4937 | (yield* orchestrator.getThreadShell(threadId))?.pullRequests, | |
| 4938 | after?.pullRequests, | |
| 4939 | ); | |
| 4940 | yield* maintenance.compactEventStore; | |
| 4941 | assert.isTrue((yield* maintenance.rebuild).valid); | |
| 4942 | assert.deepEqual( | |
| 4943 | (yield* orchestrator.getThreadShell(threadId))?.pullRequests, | |
| 4944 | after?.pullRequests, | |
| 4945 | ); | |
| 4946 | }), | |
| 4947 | ); | |
| 4948 | }); | |
| 4949 | ||
| 4863 | 4950 | it.layer(layerSharedApplicationDataPlaneTest)("shared application data plane", (it) => { |
| 4864 | 4951 | it.effect("orders retained project transactions and V2 thread transactions in one source", () => |
| 4865 | 4952 | Effect.gen(function* () { |
apps/server/src/relay/AgentAwarenessRelay.ts+1| ... | ... | @@ -97,6 +97,7 @@ export function shouldPublishAgentAwarenessEvent( |
| 97 | 97 | case "thread.unarchived": |
| 98 | 98 | case "thread.deleted": |
| 99 | 99 | case "thread.metadata-updated": |
| 100 | case "thread.pull-request-link-updated": | |
| 100 | 101 | case "thread.pull-request-synced": |
| 101 | 102 | case "thread.model-selection-updated": |
| 102 | 103 | case "thread.provider-switched": |
packages/client-runtime/src/state/orchestrationV2Projection.test.ts+29| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | import { |
| 3 | type ThreadPullRequestLink, | |
| 3 | 4 | type OrchestrationV2DomainEvent, |
| 4 | 5 | type OrchestrationV2Run, |
| 5 | 6 | type OrchestrationV2ThreadProjection, |
| ... | ... | @@ -105,6 +106,34 @@ const emptyProjection = { |
| 105 | 106 | } as OrchestrationV2ThreadProjection; |
| 106 | 107 | |
| 107 | 108 | describe("applyOrchestrationV2ProjectionEvent", () => { |
| 109 | it("applies a compact PR sync without replacing other links or thread metadata", () => { | |
| 110 | const link = (number: number): ThreadPullRequestLink => ({ | |
| 111 | host: "github.com", | |
| 112 | repository: "pingdotgg/t3code", | |
| 113 | number, | |
| 114 | url: `https://github.com/pingdotgg/t3code/pull/${number}`, | |
| 115 | source: "manual", | |
| 116 | linkedAt: DateTime.formatIso(now), | |
| 117 | snapshot: null, | |
| 118 | stack: null, | |
| 119 | }); | |
| 120 | const links = [link(1), link(2)]; | |
| 121 | const projection = { | |
| 122 | ...emptyProjection, | |
| 123 | thread: { ...emptyProjection.thread, pullRequests: links }, | |
| 124 | }; | |
| 125 | const updated = { ...links[1]!, source: "agent" as const }; | |
| 126 | const next = applyOrchestrationV2ProjectionEvent(projection, { | |
| 127 | id: "compact-sync" as OrchestrationV2DomainEvent["id"], | |
| 128 | type: "thread.pull-request-link-updated", | |
| 129 | threadId, | |
| 130 | occurredAt: now, | |
| 131 | payload: updated, | |
| 132 | }); | |
| 133 | expect(next?.thread).toEqual({ ...projection.thread, pullRequests: [links[0], updated] }); | |
| 134 | expect(next?.thread.pullRequests?.[0]).toBe(links[0]); | |
| 135 | }); | |
| 136 | ||
| 108 | 137 | it("keeps live token usage when the terminal provider turn omits it", () => { |
| 109 | 138 | const providerTurnId = ProviderTurnId.make("provider-turn-reducer"); |
| 110 | 139 | const running = { |
packages/client-runtime/src/state/orchestrationV2Projection.ts+6| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { applyThreadPullRequestLinkUpdate } from "@t3tools/shared/threadPullRequests"; | |
| 1 | 2 | import type { |
| 2 | 3 | OrchestrationV2DomainEvent, |
| 3 | 4 | OrchestrationV2Run, |
| ... | ... | @@ -201,6 +202,11 @@ export function applyOrchestrationV2ProjectionEvent( |
| 201 | 202 | const latestLocalTurnOrdinal = options?.latestLocalTurnOrdinal; |
| 202 | 203 | const base = { ...projection, updatedAt: event.occurredAt }; |
| 203 | 204 | switch (event.type) { |
| 205 | case "thread.pull-request-link-updated": | |
| 206 | return { | |
| 207 | ...base, | |
| 208 | thread: applyThreadPullRequestLinkUpdate(projection.thread, event.payload), | |
| 209 | }; | |
| 204 | 210 | case "thread.created": |
| 205 | 211 | case "thread.archived": |
| 206 | 212 | case "thread.unarchived": |
packages/contracts/src/orchestrationV2.ts+10| ... | ... | @@ -1765,6 +1765,11 @@ export const OrchestrationV2DomainEvent = Schema.Union([ |
| 1765 | 1765 | ]), |
| 1766 | 1766 | payload: OrchestrationV2AppThread, |
| 1767 | 1767 | }), |
| 1768 | Schema.Struct({ | |
| 1769 | ...OrchestrationV2EventBase.fields, | |
| 1770 | type: Schema.Literal("thread.pull-request-link-updated"), | |
| 1771 | payload: ThreadPullRequestLink, | |
| 1772 | }), | |
| 1768 | 1773 | Schema.Struct({ |
| 1769 | 1774 | ...OrchestrationV2EventBase.fields, |
| 1770 | 1775 | type: Schema.Literal("run.created"), |
| ... | ... | @@ -2585,6 +2590,11 @@ export const OrchestrationV2DomainEventJson = Schema.Union([ |
| 2585 | 2590 | ]), |
| 2586 | 2591 | payload: OrchestrationV2AppThreadJson, |
| 2587 | 2592 | }), |
| 2593 | Schema.Struct({ | |
| 2594 | ...OrchestrationV2JsonEventBaseFields, | |
| 2595 | type: Schema.Literal("thread.pull-request-link-updated"), | |
| 2596 | payload: ThreadPullRequestLink, | |
| 2597 | }), | |
| 2588 | 2598 | Schema.Struct({ |
| 2589 | 2599 | ...OrchestrationV2JsonEventBaseFields, |
| 2590 | 2600 | type: Schema.Literal("run.created"), |
packages/shared/src/threadPullRequests.ts+14| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import type { |
| 2 | OrchestrationV2AppThread, | |
| 2 | 3 | RepositoryIdentity, |
| 3 | 4 | SourceControlProviderKind, |
| 4 | 5 | ThreadLinkedPullRequest, |
| ... | ... | @@ -354,3 +355,16 @@ export function threadPullRequestsOf(thread: { |
| 354 | 355 | }, |
| 355 | 356 | ]; |
| 356 | 357 | } |
| 358 | ||
| 359 | /** Apply a single-link sync without copying unrelated PRs into the event history. */ | |
| 360 | export function applyThreadPullRequestLinkUpdate( | |
| 361 | thread: OrchestrationV2AppThread, | |
| 362 | updated: ThreadPullRequestLink, | |
| 363 | ): OrchestrationV2AppThread { | |
| 364 | return { | |
| 365 | ...thread, | |
| 366 | pullRequests: threadPullRequestsOf(thread).map((link) => | |
| 367 | threadPullRequestKeysEqual(link, updated) ? updated : link, | |
| 368 | ), | |
| 369 | }; | |
| 370 | } |