diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts index 7b33d6f6ec91..e55812f5fe95 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts @@ -157,6 +157,7 @@ function makeHarness(options: HarnessOptions = {}) { snapshot: Effect.die("unused"), getShell: () => Effect.die("unused"), listShells: () => Effect.die("unused"), + listResolvedShells: () => Effect.die("unused"), }), Layer.mock(GitWorkflow.GitWorkflowService)({ createWorktree, diff --git a/apps/server/src/project/ProjectService.test.ts b/apps/server/src/project/ProjectService.test.ts index 98e576a6fe9b..cfa55544f393 100644 --- a/apps/server/src/project/ProjectService.test.ts +++ b/apps/server/src/project/ProjectService.test.ts @@ -803,3 +803,55 @@ it.effect("rejects an update that waited on the lock while its project was delet ); }).pipe(Effect.provide(ProjectServiceDependenciesLayer)), ); + +it.effect("lists resolved shells for changed roots without requesting enrichment", () => + Effect.gen(function* () { + const enrichment = yield* ProjectEnrichmentService.ProjectEnrichmentService; + const enrichmentCalls: Array = []; + const record = (method: string, workspaceRoot: string) => + Effect.sync(() => enrichmentCalls.push(`${method} ${workspaceRoot}`)); + const service = yield* ProjectService.make.pipe( + Effect.provideService(ProjectEnrichmentService.ProjectEnrichmentService, { + ...enrichment, + peek: (root) => record("peek", root).pipe(Effect.andThen(enrichment.peek(root))), + request: (root) => record("request", root).pipe(Effect.andThen(enrichment.request(root))), + getAvailable: (root) => + record("getAvailable", root).pipe(Effect.andThen(enrichment.getAvailable(root))), + }), + ); + for (const name of ["a", "b", "c"]) { + yield* service.create({ + commandId: CommandId.make(`command:project:resolved:${name}`), + projectId: ProjectId.make(`project:resolved-${name}`), + title: name, + workspaceRoot: `/work/resolved-${name}`, + }); + } + enrichmentCalls.length = 0; + + const identity = { + canonicalKey: "github.com/t3tools/resolved-a", + locator: { + source: "git-remote" as const, + remoteName: "origin", + remoteUrl: "git@github.com:t3tools/resolved-a.git", + }, + rootPath: "/work/resolved-a", + }; + const shells = yield* service.listResolvedShells( + new Map([ + ["/work/resolved-a", identity], + ["/work/resolved-c", null], + ]), + ); + + assert.deepEqual( + shells.map((shell) => [shell.workspaceRoot, shell.repositoryIdentity?.canonicalKey ?? null]), + [ + ["/work/resolved-a", "github.com/t3tools/resolved-a"], + ["/work/resolved-c", null], + ], + ); + assert.deepEqual(enrichmentCalls, []); + }).pipe(Effect.provide(ProjectServiceDependenciesLayer)), +); diff --git a/apps/server/src/project/ProjectService.ts b/apps/server/src/project/ProjectService.ts index 5d0e6b68e626..6e4ebef8d9ca 100644 --- a/apps/server/src/project/ProjectService.ts +++ b/apps/server/src/project/ProjectService.ts @@ -6,6 +6,7 @@ import { type ProjectCreatePayload, type ProjectUpdatePayload, type ProjectSnapshot, + type RepositoryIdentity, type ThreadId, } from "@t3tools/contracts"; import * as Context from "effect/Context"; @@ -141,6 +142,15 @@ export class ProjectService extends Context.Service< readonly listShells: (options?: { readonly projectIds?: ReadonlyArray; }) => Effect.Effect, ProjectOperationError>; + /** + * Active shells for the workspace roots an enrichment refresh resolved, + * carrying those identities. Requests no enrichment: re-enriching here + * would re-request every expired root, and each resolution publishes + * another refresh. + */ + readonly listResolvedShells: ( + identities: ReadonlyMap, + ) => Effect.Effect, ProjectOperationError>; } >()("t3/project/ProjectService") {} @@ -539,6 +549,23 @@ export const make = Effect.gen(function* () { return yield* Effect.forEach(shells, enrichShell, { concurrency: 16 }); }); + const listResolvedShells: ProjectService["Service"]["listResolvedShells"] = Effect.fn( + "ProjectService.listResolvedShells", + )(function* (identities) { + const shells = yield* projects + .listShells() + .pipe( + Effect.mapError( + (cause) => new ProjectOperationError({ operation: "list-projects", cause }), + ), + ); + return shells.flatMap((shell) => + identities.has(shell.workspaceRoot) + ? [{ ...shell, repositoryIdentity: identities.get(shell.workspaceRoot) ?? null }] + : [], + ); + }); + const snapshot = Effect.gen(function* () { const rows = yield* projects .list() @@ -564,6 +591,7 @@ export const make = Effect.gen(function* () { snapshot, getShell, listShells, + listResolvedShells, }); }); diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts index 30ada288bd76..e2a2ab690e18 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.test.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.test.ts @@ -254,6 +254,7 @@ const makeTestRelay = Effect.fnUntraced(function* ( snapshot: Effect.succeed({ projects: [] } as never), getShell: unused, listShells: unused, + listResolvedShells: unused, getById: () => Effect.succeed( Option.some({ diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 2a0006ddb24b..8af313ead12a 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -948,23 +948,16 @@ export const subscribeOrchestrationV2Shell = Effect.fn("ws.orchestrationV2.subsc const enrichmentRefreshes = Stream.fromSubscription(enrichmentChanges).pipe( Stream.filter((change) => change.repositoryIdentityResolved), Stream.groupedWithin(64, Duration.millis(25)), - // Build the refresh from the identities the changes carry. Re-enriching - // every project here re-requested each expired root, whose resolution - // published again, so one expiry kept every subscriber reloading every - // project's metadata once a minute. Stream.mapEffect((changes) => Effect.gen(function* () { - const identities = new Map( - Array.from(changes, (change) => [ - change.workspaceRoot, - change.enrichment.repositoryIdentity, - ]), - ); const snapshotSequence = yield* applicationEvents.latestApplicationSequence; - const changedProjects = (yield* projects.listShells()).flatMap((project) => - identities.has(project.workspaceRoot) - ? [{ ...project, repositoryIdentity: identities.get(project.workspaceRoot) ?? null }] - : [], + const changedProjects = yield* projectService.listResolvedShells( + new Map( + Array.from(changes, (change) => [ + change.workspaceRoot, + change.enrichment.repositoryIdentity, + ]), + ), ); return shellStreamItemFromEnrichmentRefresh({ snapshot: {