| author | |
| committer | |
| log | 99efaeab58297c7e346ee91a4f28044faed03c59 |
| tree | 314058bc2126cc844e1c0056a9f17760f5c9cd46 |
| parent | daafcc4a972b25eb762962cb72575b1a2e583d61 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>7 files changed, 102 insertions(+), 15 deletions(-)
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+61| ... | ... | @@ -21,6 +21,7 @@ import * as FileSystem from "effect/FileSystem"; |
| 21 | 21 | import * as Layer from "effect/Layer"; |
| 22 | 22 | import * as Path from "effect/Path"; |
| 23 | 23 | import * as Schema from "effect/Schema"; |
| 24 | import * as Tracer from "effect/Tracer"; | |
| 24 | 25 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 25 | 26 | |
| 26 | 27 | import { makeSqlStatementCounter } from "../../../integration/SqlStatementCounter.integration.ts"; |
| ... | ... | @@ -114,6 +115,66 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-curs |
| 114 | 115 | }, |
| 115 | 116 | ); |
| 116 | 117 | |
| 118 | it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-cleanup-span-")))( | |
| 119 | "OrchestrationProjectionPipeline attachment cleanup span", | |
| 120 | (it) => { | |
| 121 | it.effect("runs attachment cleanup only for events that remove attachments", () => | |
| 122 | Effect.gen(function* () { | |
| 123 | const projectionPipeline = yield* OrchestrationProjectionPipeline; | |
| 124 | const eventStore = yield* OrchestrationEventStore; | |
| 125 | let cleanupSpans = 0; | |
| 126 | const tracer = Tracer.make({ | |
| 127 | span: (options) => { | |
| 128 | if (options.name === "applyAttachmentSideEffects") cleanupSpans += 1; | |
| 129 | return new Tracer.NativeSpan(options); | |
| 130 | }, | |
| 131 | }); | |
| 132 | const now = "2026-01-01T00:00:00.000Z"; | |
| 133 | const projectId = ProjectId.make("project-cleanup-span"); | |
| 134 | const threadId = ThreadId.make("thread-cleanup-span"); | |
| 135 | ||
| 136 | const projectCreated = yield* eventStore.append({ | |
| 137 | type: "project.created", | |
| 138 | eventId: EventId.make("evt-cleanup-span-project"), | |
| 139 | aggregateKind: "project", | |
| 140 | aggregateId: projectId, | |
| 141 | occurredAt: now, | |
| 142 | commandId: CommandId.make("cmd-cleanup-span-project"), | |
| 143 | causationEventId: null, | |
| 144 | correlationId: null, | |
| 145 | metadata: {}, | |
| 146 | payload: { | |
| 147 | projectId, | |
| 148 | title: "Cleanup span project", | |
| 149 | workspaceRoot: "/tmp/project-cleanup-span", | |
| 150 | defaultModelSelection: null, | |
| 151 | scripts: [], | |
| 152 | createdAt: now, | |
| 153 | updatedAt: now, | |
| 154 | }, | |
| 155 | }); | |
| 156 | yield* projectionPipeline.projectEvent(projectCreated).pipe(Effect.withTracer(tracer)); | |
| 157 | assert.strictEqual(cleanupSpans, 0); | |
| 158 | ||
| 159 | const threadDeleted = yield* eventStore.append({ | |
| 160 | type: "thread.deleted", | |
| 161 | eventId: EventId.make("evt-cleanup-span-thread-delete"), | |
| 162 | aggregateKind: "thread", | |
| 163 | aggregateId: threadId, | |
| 164 | occurredAt: now, | |
| 165 | commandId: CommandId.make("cmd-cleanup-span-thread-delete"), | |
| 166 | causationEventId: null, | |
| 167 | correlationId: null, | |
| 168 | metadata: {}, | |
| 169 | payload: { threadId, deletedAt: now }, | |
| 170 | }); | |
| 171 | yield* projectionPipeline.projectEvent(threadDeleted).pipe(Effect.withTracer(tracer)); | |
| 172 | assert.strictEqual(cleanupSpans, 1); | |
| 173 | }), | |
| 174 | ); | |
| 175 | }, | |
| 176 | ); | |
| 177 | ||
| 117 | 178 | it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-import-shell-")))( |
| 118 | 179 | "imported thread shell projection", |
| 119 | 180 | (it) => { |
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+7-8| ... | ... | @@ -1991,13 +1991,6 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti |
| 1991 | 1991 | |
| 1992 | 1992 | const applyAttachmentSideEffects = Effect.fn("applyAttachmentSideEffects")( |
| 1993 | 1993 | function* (event: OrchestrationEvent, sideEffects: AttachmentSideEffects) { |
| 1994 | if ( | |
| 1995 | sideEffects.deletedThreadIds.size === 0 && | |
| 1996 | sideEffects.prunedThreadRelativePaths.size === 0 | |
| 1997 | ) { | |
| 1998 | return; | |
| 1999 | } | |
| 2000 | ||
| 2001 | 1994 | const deletedThreadIds = new Set<string>(); |
| 2002 | 1995 | for (const threadId of sideEffects.deletedThreadIds) { |
| 2003 | 1996 | const recreatedLater = yield* eventStore.hasEventAfter({ |
| ... | ... | @@ -2113,9 +2106,15 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti |
| 2113 | 2106 | ); |
| 2114 | 2107 | }), |
| 2115 | 2108 | ); |
| 2109 | const hasCleanup = | |
| 2110 | attachmentSideEffects.deletedThreadIds.size > 0 || | |
| 2111 | attachmentSideEffects.prunedThreadRelativePaths.size > 0; | |
| 2116 | 2112 | // Return the cleanup effect so the caller runs it after the outer transaction commits. |
| 2113 | // Most events have no cleanup, so they skip the call and write no cleanup span. | |
| 2117 | 2114 | // @effect-diagnostics-next-line returnEffectInGen:off |
| 2118 | return applyAttachmentSideEffects(event, attachmentSideEffects).pipe(Effect.asVoid); | |
| 2115 | return hasCleanup | |
| 2116 | ? applyAttachmentSideEffects(event, attachmentSideEffects).pipe(Effect.asVoid) | |
| 2117 | : Effect.void; | |
| 2119 | 2118 | }, |
| 2120 | 2119 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 2121 | 2120 | Effect.provideService(Path.Path, path), |
apps/server/src/preview/PortScanner.test.ts+21| ... | ... | @@ -18,6 +18,7 @@ import * as Layer from "effect/Layer"; |
| 18 | 18 | import * as PlatformError from "effect/PlatformError"; |
| 19 | 19 | import * as Scope from "effect/Scope"; |
| 20 | 20 | import * as TestClock from "effect/testing/TestClock"; |
| 21 | import * as Tracer from "effect/Tracer"; | |
| 21 | 22 | import { expect } from "vite-plus/test"; |
| 22 | 23 | import { FetchHttpClient } from "effect/unstable/http"; |
| 23 | 24 | |
| ... | ... | @@ -441,6 +442,26 @@ effectIt.effect("stops probing a subscriber's configured paths after its scope c |
| 441 | 442 | }).pipe(Effect.scoped, Effect.provide(layer)); |
| 442 | 443 | }); |
| 443 | 444 | |
| 445 | effectIt.effect("writes no poll span while no client retains the scanner", () => { | |
| 446 | let pollSpans = 0; | |
| 447 | const tracer = Tracer.make({ | |
| 448 | span: (options) => { | |
| 449 | if (options.name === "PortDiscovery.pollTick") pollSpans += 1; | |
| 450 | return new Tracer.NativeSpan(options); | |
| 451 | }, | |
| 452 | }); | |
| 453 | const layer = makeProbeFailureLayer(processProbeFailure); | |
| 454 | ||
| 455 | return Effect.gen(function* () { | |
| 456 | const scanner = yield* PortScanner.PortDiscovery; | |
| 457 | yield* TestClock.adjust(Duration.seconds(15)); | |
| 458 | expect(pollSpans).toBe(0); | |
| 459 | ||
| 460 | yield* scanner.retain; | |
| 461 | expect(pollSpans).toBe(1); | |
| 462 | }).pipe(Effect.scoped, Effect.provide(layer), Effect.withTracer(tracer)); | |
| 463 | }); | |
| 464 | ||
| 444 | 465 | effectIt.effect("uses the current configured fragment when readiness comes from cache", () => { |
| 445 | 466 | const requests: string[] = []; |
| 446 | 467 | const fetchFn = ((input: Parameters<typeof globalThis.fetch>[0]) => { |
apps/server/src/preview/PortScanner.ts+6-4| ... | ... | @@ -550,7 +550,6 @@ export const make = Effect.gen(function* PortDiscoveryMake() { |
| 550 | 550 | |
| 551 | 551 | const pollTick = Effect.fn("PortDiscovery.pollTick")( |
| 552 | 552 | function* () { |
| 553 | if ((yield* Ref.get(stateRef)).retainCount <= 0) return; | |
| 554 | 553 | const configuredUrls = [ |
| 555 | 554 | ...new Set( |
| 556 | 555 | [...(yield* Ref.get(stateRef)).listeners.values()].flatMap( |
| ... | ... | @@ -579,9 +578,12 @@ export const make = Effect.gen(function* PortDiscoveryMake() { |
| 579 | 578 | ), |
| 580 | 579 | ); |
| 581 | 580 | |
| 582 | // Single layer-scoped polling fiber. Ticks are no-ops when no client is | |
| 583 | // currently retained, so the cost is one Ref.get every POLL_INTERVAL. | |
| 584 | yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL)))); | |
| 581 | // Single layer-scoped polling fiber. Ticks skip the scan and its span when no | |
| 582 | // client is currently retained, so the cost is one Ref.get every POLL_INTERVAL. | |
| 583 | const pollIfRetained = Ref.get(stateRef).pipe( | |
| 584 | Effect.flatMap((state) => (state.retainCount > 0 ? pollTick() : Effect.void)), | |
| 585 | ); | |
| 586 | yield* Effect.forkScoped(pollIfRetained.pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL)))); | |
| 585 | 587 | |
| 586 | 588 | const acquireRetention = Effect.fn("PortDiscovery.retain")(function* () { |
| 587 | 589 | const wasIdle = yield* Ref.modify(stateRef, (state) => [ |
apps/server/src/processRunner.ts+2-1| ... | ... | @@ -171,7 +171,8 @@ export const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsComman |
| 171 | 171 | }, |
| 172 | 172 | ); |
| 173 | 173 | |
| 174 | const collectText = Effect.fn("processRunner.collectText")(function* (input: { | |
| 174 | // Untraced: no attributes, and its time is the runProcessCore span. Errors fail that span. | |
| 175 | const collectText = Effect.fnUntraced(function* (input: { | |
| 175 | 176 | readonly command: string; |
| 176 | 177 | readonly args: ReadonlyArray<string>; |
| 177 | 178 | readonly cwd?: string | undefined; |
apps/server/src/vcs/GitVcsDriverCore.ts+3-1| ... | ... | @@ -549,7 +549,9 @@ function trace2ChildKey(record: Record<string, unknown>): string | null { |
| 549 | 549 | const Trace2Record = Schema.Record(Schema.String, Schema.Unknown); |
| 550 | 550 | const decodeTrace2Record = decodeJsonResult(Trace2Record); |
| 551 | 551 | |
| 552 | const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* ( | |
| 552 | // Untraced because it runs on every git spawn and returns at once without hook | |
| 553 | // callbacks. Its errors fail the runGitCommand span. | |
| 554 | const createTrace2Monitor = Effect.fnUntraced(function* ( | |
| 553 | 555 | input: Pick<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">, |
| 554 | 556 | progress: GitVcsDriver.ExecuteGitProgress | undefined, |
| 555 | 557 | ): Effect.fn.Return< |
packages/shared/src/shell.ts+2-1| ... | ... | @@ -681,7 +681,8 @@ export const resolveCommandPath = Effect.fn("shell.resolveCommandPath")(function |
| 681 | 681 | }); |
| 682 | 682 | }); |
| 683 | 683 | |
| 684 | export const resolveSpawnCommand = Effect.fn("shell.resolveSpawnCommand")(function* ( | |
| 684 | // Untraced because it runs before most spawns and returns at once off Windows. | |
| 685 | export const resolveSpawnCommand = Effect.fnUntraced(function* ( | |
| 685 | 686 | command: string, |
| 686 | 687 | args: ReadonlyArray<string>, |
| 687 | 688 | options: CommandAvailabilityOptions = {}, |