authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-06 17:51:52-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 17:51:52-07:00
logd021f57bf3a500e419e800c3eb957568bed8f713
tree2ab961a6c79cc433cc5ffdedda3dee6395fdb6af
parent517188b3caa135bc09ec0a4f93474ef17c8b5c4f
signature Signed by PGP key B5690EEEBB952194

refactor(server): instrument WS RPCs in group middleware (#15548)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

5 files changed, 1437 insertions(+), 2008 deletions(-)

apps/server/src/observability/RpcInstrumentation.test.ts+305-282
......@@ -1,313 +1,336 @@
11import { assert, describe, it } from "@effect/vitest";
2import { WS_METHODS } from "@t3tools/contracts";
2import {
3 AuthOrchestrationOperateScope,
4 AuthOrchestrationReadScope,
5 type AuthEnvironmentScope,
6 ScheduledTaskError,
7 ScheduledTaskId,
8 WS_METHODS,
9 WsRpcGroup,
10} from "@t3tools/contracts";
11import * as Deferred from "effect/Deferred";
312import * as Duration from "effect/Duration";
413import * as Effect from "effect/Effect";
514import * as Exit from "effect/Exit";
615import * as Fiber from "effect/Fiber";
16import * as Layer from "effect/Layer";
717import * as Metric from "effect/Metric";
18import * as Option from "effect/Option";
819import * as Stream from "effect/Stream";
920import * as Tracer from "effect/Tracer";
21import * as RpcTest from "effect/rpc/RpcTest";
1022import * as TestClock from "effect/testing/TestClock";
1123
12import {
13 observeRpcEffect,
14 observeRpcStream,
15 observeRpcStreamEffect,
16} from "./RpcInstrumentation.ts";
17
18const hasMetricSnapshot = (
19 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
20 id: string,
21 attributes: Readonly<Record<string, string>>,
22) =>
23 snapshots.some(
24 (snapshot) =>
25 snapshot.id === id &&
26 Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value),
27 );
28
29const findHistogramSnapshot = (
30 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
31 id: string,
32 attributes: Readonly<Record<string, string>>,
33) =>
34 snapshots.find(
35 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Histogram" }> =>
36 snapshot.type === "Histogram" &&
37 snapshot.id === id &&
38 Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value),
39 );
40
41const collectSpanNames = <A, E, R>(
42 effect: Effect.Effect<A, E, R>,
43): Effect.Effect<ReadonlyArray<string>, E, R> =>
44 Effect.gen(function* () {
45 const spanNames: Array<string> = [];
46 const tracer = Tracer.make({
47 span: (options) => {
48 const span = new Tracer.NativeSpan(options);
49 const end = span.end.bind(span);
50
51 span.end = (endTime, exit) => {
52 end(endTime, exit);
53 if (span.sampled) {
54 spanNames.push(span.name);
55 }
56 };
57
58 return span;
59 },
60 });
61
62 yield* effect.pipe(Effect.withTracer(tracer));
63
64 return spanNames;
24import { RPC_REQUIRED_SCOPES } from "../auth/RpcAuthorization.ts";
25import * as RpcAuthorization from "../auth/RpcAuthorization.ts";
26import { RpcInstrumentation, rpcInstrumentationLayer } from "./RpcInstrumentation.ts";
27
28type WsRpcMethod = keyof typeof RPC_REQUIRED_SCOPES;
29
30/** The server group narrowed to `tags`, so a test only implements the handlers it calls. */
31const groupOf = <const Tags extends ReadonlyArray<WsRpcMethod>>(...tags: Tags) =>
32 WsRpcGroup.omit(
33 ...[...WsRpcGroup.requests.keys()].filter(
34 (tag): tag is Exclude<WsRpcMethod, Tags[number]> =>
35 !(tags as ReadonlyArray<string>).includes(tag),
36 ),
37 ).middleware(RpcInstrumentation);
38
39/** The middleware ws.ts installs for a connection with `scopes`. */
40const connectionMiddleware = (scopes: ReadonlyArray<AuthEnvironmentScope>) =>
41 Layer.merge(RpcAuthorization.layer(scopes), rpcInstrumentationLayer);
42const readOnlyConnection = connectionMiddleware([AuthOrchestrationReadScope]);
43const taskId = ScheduledTaskId.make("scheduled-task:instrumented");
44const rpcSpanDefaults = { "rpc.transport": "websocket", "rpc.system": "effect-rpc" };
45
46/** Runs a test with a fresh metric registry and a tracer that keeps every span it ends. */
47const withTelemetry = <A, E, R>(
48 test: (ended: ReadonlyArray<Tracer.NativeSpan>) => Effect.Effect<A, E, R>,
49) => {
50 const ended: Array<Tracer.NativeSpan> = [];
51 const tracer = Tracer.make({
52 span: (options) => {
53 const span = new Tracer.NativeSpan(options);
54 const end = span.end.bind(span);
55 span.end = (endTime, exit) => {
56 end(endTime, exit);
57 ended.push(span);
58 };
59 return span;
60 },
6561 });
66
67describe("RpcInstrumentation", () => {
68 it.effect("records success metrics for unary RPC handlers", () =>
69 Effect.gen(function* () {
70 yield* observeRpcEffect("rpc.instrumentation.success", Effect.succeed("ok"), {
71 "rpc.aggregate": "test",
72 }).pipe(Effect.withSpan("rpc.instrumentation.success.span"));
73
74 const snapshots = yield* Metric.snapshot;
75
76 assert.equal(
77 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {
78 method: "rpc.instrumentation.success",
79 outcome: "success",
80 }),
81 true,
82 );
83 assert.equal(
84 hasMetricSnapshot(snapshots, "t3_rpc_request_duration", {
85 method: "rpc.instrumentation.success",
86 }),
87 true,
88 );
89 }),
62 return test(ended).pipe(
63 Effect.scoped,
64 Effect.withTracer(tracer),
65 Effect.provideService(Metric.MetricRegistry, new Map()),
9066 );
67};
9168
92 it.effect("records failure outcomes for unary RPC handlers", () =>
93 Effect.gen(function* () {
94 yield* Effect.exit(
95 observeRpcEffect("rpc.instrumentation.failure", Effect.fail("boom"), {
96 "rpc.aggregate": "test",
97 }).pipe(Effect.withSpan("rpc.instrumentation.failure.span")),
98 );
69const rpcSpans = (ended: ReadonlyArray<Tracer.NativeSpan>) =>
70 ended.filter((span) => span.name.startsWith("ws.rpc."));
9971
100 const snapshots = yield* Metric.snapshot;
72// RpcTest keeps Effect's own RpcServer/RpcClient spans, which ws.ts turns off with
73// `disableTracing: true`. Everything else comes from the middleware or the handlers.
74const appSpans = (ended: ReadonlyArray<Tracer.NativeSpan>) =>
75 ended.filter((span) => !/^Rpc(Server|Client)\./.test(span.name));
10176
102 assert.equal(
103 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {
104 method: "rpc.instrumentation.failure",
105 outcome: "failure",
106 }),
107 true,
108 );
109 assert.equal(
110 hasMetricSnapshot(snapshots, "t3_rpc_request_duration", {
111 method: "rpc.instrumentation.failure",
112 }),
113 true,
114 );
115 }),
116 );
77const exitTag = (span: Tracer.NativeSpan | undefined) =>
78 span?.status._tag === "Ended" ? span.status.exit._tag : undefined;
11779
118 it.effect("records subscription activation metrics for stream RPC handlers", () =>
119 Effect.gen(function* () {
120 const events = yield* Stream.runCollect(
121 observeRpcStreamEffect(
122 "rpc.instrumentation.stream",
123 Effect.succeed(Stream.make("a", "b")),
124 { "rpc.aggregate": "test" },
125 ).pipe(Stream.withSpan("rpc.instrumentation.stream.span")),
126 );
127
128 assert.deepStrictEqual(Array.from(events), ["a", "b"]);
129
130 const snapshots = yield* Metric.snapshot;
131
132 assert.equal(
133 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {
134 method: "rpc.instrumentation.stream",
135 outcome: "success",
136 }),
137 true,
138 );
139 assert.equal(
140 hasMetricSnapshot(snapshots, "t3_rpc_request_duration", {
141 method: "rpc.instrumentation.stream",
142 }),
143 true,
144 );
145 }),
146 );
147
148 it.effect("records failure outcomes for direct stream RPC handlers during consumption", () =>
149 Effect.gen(function* () {
150 const exit = yield* Stream.runCollect(
151 observeRpcStream(
152 "rpc.instrumentation.stream.failure",
153 Stream.make("a").pipe(Stream.concat(Stream.fail("boom"))),
154 { "rpc.aggregate": "test" },
155 ).pipe(Stream.withSpan("rpc.instrumentation.stream.failure.span")),
156 ).pipe(Effect.exit);
157
158 assert.equal(Exit.isFailure(exit), true);
159
160 const snapshots = yield* Metric.snapshot;
161
162 assert.equal(
163 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {
164 method: "rpc.instrumentation.stream.failure",
165 outcome: "failure",
166 }),
167 true,
168 );
169 assert.equal(
170 hasMetricSnapshot(snapshots, "t3_rpc_request_duration", {
171 method: "rpc.instrumentation.stream.failure",
172 }),
173 true,
174 );
175 }),
176 );
177
178 it.effect("records direct stream durations from nanosecond clock readings", () =>
179 Effect.gen(function* () {
180 const duration = Duration.nanos(1_500_000n);
181 const events = yield* Effect.gen(function* () {
182 const fiber = yield* Stream.runCollect(
183 observeRpcStream(
184 WS_METHODS.serverGetProcessDiagnostics,
185 Stream.fromEffect(Effect.sleep(duration).pipe(Effect.as("ok"))),
186 {
187 "rpc.aggregate": "test",
188 },
189 ),
190 ).pipe(Effect.forkChild);
80const parentSpanId = (span: Tracer.NativeSpan | undefined) =>
81 span?.parent._tag === "Some" ? span.parent.value.spanId : undefined;
19182
192 yield* Effect.yieldNow;
193 yield* TestClock.adjust(duration);
194 return yield* Fiber.join(fiber);
195 }).pipe(Effect.provide(TestClock.layer()));
196
197 assert.deepStrictEqual(Array.from(events), ["ok"]);
198
199 const snapshots = yield* Metric.snapshot;
200 const snapshot = findHistogramSnapshot(snapshots, "t3_rpc_request_duration", {
201 method: WS_METHODS.serverGetProcessDiagnostics,
202 });
203
204 assert.equal(snapshot?.state.count, 1);
205 assert.equal(snapshot?.state.sum, 1.5);
206 }),
207 );
208
209 it.effect("records failure outcomes when a stream RPC effect produces a failing stream", () =>
210 Effect.gen(function* () {
211 const exit = yield* Stream.runCollect(
212 observeRpcStreamEffect(
213 "rpc.instrumentation.stream.effect.failure",
214 Effect.succeed(Stream.fail("boom")),
215 { "rpc.aggregate": "test" },
216 ).pipe(Stream.withSpan("rpc.instrumentation.stream.effect.failure.span")),
217 ).pipe(Effect.exit);
218
219 assert.equal(Exit.isFailure(exit), true);
220
221 const snapshots = yield* Metric.snapshot;
222
223 assert.equal(
224 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {
225 method: "rpc.instrumentation.stream.effect.failure",
226 outcome: "failure",
227 }),
228 true,
229 );
230 assert.equal(
231 hasMetricSnapshot(snapshots, "t3_rpc_request_duration", {
232 method: "rpc.instrumentation.stream.effect.failure",
233 }),
234 true,
235 );
236 }),
237 );
238
239 it.effect("records spans for traced stream RPC handlers", () =>
240 Effect.gen(function* () {
241 const spanNames = yield* collectSpanNames(
242 Stream.runCollect(
243 observeRpcStream(
244 "rpc.instrumentation.traced.stream",
245 Stream.fromEffect(
246 Effect.succeed("ok").pipe(Effect.withSpan("rpc.instrumentation.traced.stream.child")),
83const requestCount = (
84 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
85 method: string,
86 outcome: string,
87) =>
88 snapshots.find(
89 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Counter" }> =>
90 snapshot.type === "Counter" &&
91 snapshot.id === "t3_rpc_requests_total" &&
92 snapshot.attributes?.["method"] === method &&
93 snapshot.attributes?.["outcome"] === outcome,
94 )?.state;
95
96const requestDuration = (snapshots: ReadonlyArray<Metric.Metric.Snapshot>, method: string) =>
97 snapshots.find(
98 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Histogram" }> =>
99 snapshot.type === "Histogram" &&
100 snapshot.id === "t3_rpc_request_duration" &&
101 snapshot.attributes?.["method"] === method,
102 )?.state;
103
104describe("WS RPC instrumentation middleware", () => {
105 it.effect("records one span and request metric per call, including rejected calls", () =>
106 withTelemetry((ended) =>
107 Effect.gen(function* () {
108 const group = groupOf(
109 WS_METHODS.serverProbe,
110 WS_METHODS.scheduledTasksList,
111 WS_METHODS.serverRetryResourceTelemetry,
112 WS_METHODS.pullRequestsSubscribeRefreshes,
113 WS_METHODS.scheduledTasksSubscribe,
114 WS_METHODS.serverGetSettings,
115 );
116 const client = yield* RpcTest.makeClient(group).pipe(
117 Effect.provide(
118 Layer.mergeAll(
119 group.toLayerHandler(WS_METHODS.serverProbe, () =>
120 Effect.succeed({}).pipe(Effect.withSpan("serverProbe.child")),
121 ),
122 group.toLayerHandler(WS_METHODS.scheduledTasksList, () =>
123 Effect.annotateCurrentSpan({ "scheduled_task.id": taskId }).pipe(
124 Effect.andThen(new ScheduledTaskError({ message: "List failed." })),
125 ),
126 ),
127 group.toLayerHandler(WS_METHODS.serverRetryResourceTelemetry, () =>
128 Effect.die("authorization let a rejected call through"),
129 ),
130 group.toLayerHandler(WS_METHODS.pullRequestsSubscribeRefreshes, () =>
131 Stream.make(1, 2),
132 ),
133 group.toLayerHandler(WS_METHODS.scheduledTasksSubscribe, () =>
134 Stream.concat(
135 Stream.make({ tasks: [] }),
136 Stream.fail(new ScheduledTaskError({ message: "Subscription failed." })),
137 ),
138 ),
139 group.toLayerHandler(WS_METHODS.serverGetSettings, () => Effect.die("broken")),
140 readOnlyConnection,
247141 ),
248 { "rpc.aggregate": "test" },
249142 ),
250 ),
251 );
252
253 assert.equal(spanNames.includes("ws.rpc.rpc.instrumentation.traced.stream"), true);
254 assert.equal(spanNames.includes("rpc.instrumentation.traced.stream.child"), true);
255 }),
256 );
257
258 it.effect("does not create spans for disabled unary RPC handlers", () =>
259 Effect.gen(function* () {
260 const spanNames = yield* collectSpanNames(
261 observeRpcEffect(
262 WS_METHODS.serverGetTraceDiagnostics,
263 Effect.succeed("ok").pipe(Effect.withSpan("rpc.instrumentation.disabled.unary.child")),
264 { "rpc.aggregate": "test" },
265 ),
266 );
267
268 assert.deepStrictEqual(spanNames, []);
269 }),
143 );
144
145 assert.deepStrictEqual(yield* client[WS_METHODS.serverProbe]({}), {});
146 const listError = yield* client[WS_METHODS.scheduledTasksList]({}).pipe(Effect.flip);
147 assert.equal(listError._tag, "ScheduledTaskError");
148 // Retrying telemetry needs operate scope, so the handler must not run.
149 const rejection = yield* client[WS_METHODS.serverRetryResourceTelemetry]({}).pipe(
150 Effect.flip,
151 );
152 assert.equal(rejection._tag, "EnvironmentAuthorizationError");
153 const refreshes = yield* Stream.runCollect(
154 client[WS_METHODS.pullRequestsSubscribeRefreshes]({}),
155 );
156 assert.deepStrictEqual(Array.from(refreshes), [1, 2]);
157 const subscribeError = yield* Stream.runDrain(
158 client[WS_METHODS.scheduledTasksSubscribe]({}),
159 ).pipe(Effect.flip);
160 assert.equal(subscribeError._tag, "ScheduledTaskError");
161 const settingsExit = yield* Effect.exit(client[WS_METHODS.serverGetSettings]({}));
162 assert.isTrue(Exit.hasDies(settingsExit));
163
164 assert.deepStrictEqual(
165 rpcSpans(ended).map((span) => [span.name, Object.fromEntries(span.attributes)]),
166 [
167 [
168 "ws.rpc.server.probe",
169 {
170 ...rpcSpanDefaults,
171 "rpc.method": WS_METHODS.serverProbe,
172 "rpc.aggregate": "server",
173 },
174 ],
175 [
176 "ws.rpc.scheduledTasks.list",
177 {
178 ...rpcSpanDefaults,
179 "rpc.method": WS_METHODS.scheduledTasksList,
180 "rpc.aggregate": "scheduledTasks",
181 "scheduled_task.id": taskId,
182 },
183 ],
184 [
185 "ws.rpc.server.retryResourceTelemetry",
186 {
187 ...rpcSpanDefaults,
188 "rpc.method": WS_METHODS.serverRetryResourceTelemetry,
189 "rpc.aggregate": "server",
190 },
191 ],
192 [
193 "ws.rpc.pullRequests.subscribeRefreshes",
194 {
195 ...rpcSpanDefaults,
196 "rpc.method": WS_METHODS.pullRequestsSubscribeRefreshes,
197 "rpc.aggregate": "pull-requests",
198 },
199 ],
200 [
201 "ws.rpc.scheduledTasks.subscribe",
202 {
203 ...rpcSpanDefaults,
204 "rpc.method": WS_METHODS.scheduledTasksSubscribe,
205 "rpc.aggregate": "scheduledTasks",
206 },
207 ],
208 [
209 "ws.rpc.server.getSettings",
210 {
211 ...rpcSpanDefaults,
212 "rpc.method": WS_METHODS.serverGetSettings,
213 "rpc.aggregate": "server",
214 },
215 ],
216 ],
217 );
218 assert.deepStrictEqual(rpcSpans(ended).map(exitTag), [
219 "Success",
220 "Failure",
221 "Failure",
222 "Success",
223 "Failure",
224 "Failure",
225 ]);
226 const probeSpan = rpcSpans(ended)[0];
227 const child = ended.find((span) => span.name === "serverProbe.child");
228 assert.equal(parentSpanId(child), probeSpan?.spanId);
229
230 const snapshots = yield* Metric.snapshot;
231 assert.deepStrictEqual(
232 [
233 requestCount(snapshots, WS_METHODS.serverProbe, "success"),
234 requestCount(snapshots, WS_METHODS.scheduledTasksList, "failure"),
235 requestCount(snapshots, WS_METHODS.serverRetryResourceTelemetry, "failure"),
236 requestCount(snapshots, WS_METHODS.pullRequestsSubscribeRefreshes, "success"),
237 requestCount(snapshots, WS_METHODS.scheduledTasksSubscribe, "failure"),
238 requestCount(snapshots, WS_METHODS.serverGetSettings, "failure"),
239 ].map((state) => state?.count),
240 [1, 1, 1, 1, 1, 1],
241 );
242 for (const method of group.requests.keys()) {
243 assert.equal(requestDuration(snapshots, method)?.count, 1);
244 }
245 }),
246 ),
270247 );
271248
272 it.effect("does not create spans for disabled direct stream RPC handlers", () =>
273 Effect.gen(function* () {
274 const spanNames = yield* collectSpanNames(
275 Stream.runCollect(
276 observeRpcStream(
277 WS_METHODS.serverGetTraceDiagnostics,
278 Stream.fromEffect(
279 Effect.succeed("ok").pipe(
280 Effect.withSpan("rpc.instrumentation.disabled.stream.child"),
249 it.effect("keeps a stream's span and duration open until the subscription is interrupted", () =>
250 withTelemetry((ended) =>
251 Effect.gen(function* () {
252 const waiting = yield* Deferred.make<void>();
253 const group = groupOf(WS_METHODS.pullRequestsSubscribeRefreshes);
254 const client = yield* RpcTest.makeClient(group).pipe(
255 Effect.provide(
256 Layer.mergeAll(
257 group.toLayerHandler(WS_METHODS.pullRequestsSubscribeRefreshes, () =>
258 Stream.concat(
259 Stream.make(1),
260 Stream.fromEffect(
261 Deferred.succeed(waiting, undefined).pipe(
262 Effect.andThen(Effect.never),
263 Effect.withSpan("refreshes.wait"),
264 ),
265 ),
266 ),
281267 ),
268 readOnlyConnection,
282269 ),
283 { "rpc.aggregate": "test" },
284270 ),
285 ),
286 );
287
288 assert.deepStrictEqual(spanNames, []);
289 }),
271 );
272
273 // Wall and monotonic time disagree before the call starts, so a duration that mixes
274 // the two clocks is caught.
275 yield* TestClock.adjust(Duration.seconds(1));
276 yield* TestClock.setTime(0);
277 const consumer = yield* Stream.runDrain(
278 client[WS_METHODS.pullRequestsSubscribeRefreshes]({}),
279 ).pipe(Effect.forkChild);
280 yield* Deferred.await(waiting);
281 yield* TestClock.adjust(Duration.millis(250));
282 // A backward wall-clock correction must not shorten the measured duration.
283 yield* TestClock.setTime(0);
284 assert.deepStrictEqual(appSpans(ended), []);
285
286 // The client waits for the server to stop the call, which ends the RPC span.
287 yield* Fiber.interrupt(consumer);
288
289 const [rpcSpan] = rpcSpans(ended);
290 assert.equal(rpcSpan?.name, "ws.rpc.pullRequests.subscribeRefreshes");
291 assert.equal(exitTag(rpcSpan), "Failure");
292 const waitSpan = ended.find((span) => span.name === "refreshes.wait");
293 assert.equal(parentSpanId(waitSpan), rpcSpan?.spanId);
294
295 const snapshots = yield* Metric.snapshot;
296 assert.equal(
297 requestCount(snapshots, WS_METHODS.pullRequestsSubscribeRefreshes, "interrupt")?.count,
298 1,
299 );
300 const duration = requestDuration(snapshots, WS_METHODS.pullRequestsSubscribeRefreshes);
301 assert.equal(duration?.count, 1);
302 assert.equal(duration?.sum, 250);
303 }),
304 ),
290305 );
291306
292 it.effect("does not create spans for disabled stream effect RPC handlers", () =>
293 Effect.gen(function* () {
294 const spanNames = yield* collectSpanNames(
295 Stream.runCollect(
296 observeRpcStreamEffect(
297 WS_METHODS.serverGetTraceDiagnostics,
298 Effect.succeed(
299 Stream.fromEffect(
300 Effect.succeed("ok").pipe(
301 Effect.withSpan("rpc.instrumentation.disabled.stream.effect.consume"),
302 ),
307 it.effect("records metrics but no spans for methods with tracing disabled", () =>
308 withTelemetry((ended) =>
309 Effect.gen(function* () {
310 const group = groupOf(WS_METHODS.serverSignalProcess);
311 const client = yield* RpcTest.makeClient(group).pipe(
312 Effect.provide(
313 Layer.mergeAll(
314 group.toLayerHandler(WS_METHODS.serverSignalProcess, (input) =>
315 Effect.succeed({
316 pid: input.pid,
317 signal: input.signal,
318 signaled: true,
319 message: Option.none(),
320 }).pipe(Effect.withSpan("signalProcess.child")),
303321 ),
304 ).pipe(Effect.withSpan("rpc.instrumentation.disabled.stream.effect.create")),
305 { "rpc.aggregate": "test" },
322 connectionMiddleware([AuthOrchestrationReadScope, AuthOrchestrationOperateScope]),
323 ),
306324 ),
307 ),
308 );
325 );
326
327 const input = { pid: 4242, startTimeMs: 0, signal: "SIGINT" } as const;
328 assert.equal((yield* client[WS_METHODS.serverSignalProcess](input)).signaled, true);
309329
310 assert.deepStrictEqual(spanNames, []);
311 }),
330 assert.deepStrictEqual(appSpans(ended), []);
331 const snapshots = yield* Metric.snapshot;
332 assert.equal(requestCount(snapshots, WS_METHODS.serverSignalProcess, "success")?.count, 1);
333 }),
334 ),
312335 );
313336});
apps/server/src/observability/RpcInstrumentation.ts+219-116
......@@ -1,14 +1,198 @@
1import { WS_METHODS } from "@t3tools/contracts";
2import * as Clock from "effect/Clock";
3import * as Duration from "effect/Duration";
1import { ORCHESTRATION_V2_WS_METHODS, WS_METHODS, type WsRpcGroup } from "@t3tools/contracts";
42import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Metric from "effect/Metric";
3import * as Layer from "effect/Layer";
74import * as References from "effect/References";
8import * as Stream from "effect/Stream";
5import type * as RpcGroup from "effect/rpc/RpcGroup";
6import * as RpcMiddleware from "effect/rpc/RpcMiddleware";
97
10import { outcomeFromExit } from "./Attributes.ts";
11import { metricAttributes, rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";
8import { rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";
9
10type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];
11
12/**
13 * The `rpc.aggregate` span attribute of every WebSocket RPC. Trace queries and dashboards group by
14 * these labels, so they keep their historical values even where they differ from the method
15 * prefix. Adding an RPC to `WsRpcGroup` without a label is a type error.
16 */
17const RPC_AGGREGATES = {
18 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: "orchestrationV2",
19 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: "orchestration",
20 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: "orchestration",
21 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: "orchestration",
22 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: "orchestration",
23 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: "orchestration",
24 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: "orchestration",
25 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: "orchestrationV2",
26 [ORCHESTRATION_V2_WS_METHODS.launchThread]: "orchestration",
27 [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: "orchestration",
28 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: "orchestrationV2",
29 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: "orchestrationV2",
30 [WS_METHODS.projectsMutate]: "orchestration",
31 [WS_METHODS.serverProbe]: "server",
32 [WS_METHODS.serverGetConfig]: "server",
33 [WS_METHODS.serverRefreshProviders]: "server",
34 [WS_METHODS.serverUpdateProvider]: "server",
35 [WS_METHODS.providerAuthStart]: "provider",
36 [WS_METHODS.providerConsumeResetCredit]: "provider",
37 [WS_METHODS.providerAuthComplete]: "provider",
38 [WS_METHODS.chatGptReconnectProfile]: "provider",
39 [WS_METHODS.chatGptImportProfile]: "provider",
40 [WS_METHODS.chatGptHandoffSubscribe]: "provider",
41 [WS_METHODS.codexAuthCallbackSubscribe]: "provider",
42 [WS_METHODS.providerAuthRespond]: "provider",
43 [WS_METHODS.providerAuthCancel]: "provider",
44 [WS_METHODS.providerAuthLogout]: "provider",
45 [WS_METHODS.providerAuthSubscribe]: "provider",
46 [WS_METHODS.providerInstallStart]: "provider",
47 [WS_METHODS.providerInstallCancel]: "provider",
48 [WS_METHODS.providerInstallSubscribe]: "provider",
49 [WS_METHODS.providerInstallRemove]: "provider",
50 [WS_METHODS.serverUpdateServer]: "server",
51 [WS_METHODS.serverUpdateServerWithProgress]: "server",
52 [WS_METHODS.serverCommitDesktopUpdate]: "server",
53 [WS_METHODS.serverUpsertKeybinding]: "server",
54 [WS_METHODS.serverRemoveKeybinding]: "server",
55 [WS_METHODS.serverGetSettings]: "server",
56 [WS_METHODS.serverUpdateSettings]: "server",
57 [WS_METHODS.serverSearchAcpRegistry]: "server",
58 [WS_METHODS.serverPrepareAcpRegistryAgent]: "server",
59 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: "server",
60 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: "server",
61 [WS_METHODS.serverListAcpRegistrySessions]: "server",
62 [WS_METHODS.serverImportAcpRegistrySession]: "server",
63 [WS_METHODS.serverDeleteAcpRegistrySession]: "server",
64 [WS_METHODS.serverListAcpRegistryProviders]: "server",
65 [WS_METHODS.serverSetAcpRegistryProvider]: "server",
66 [WS_METHODS.serverDisableAcpRegistryProvider]: "server",
67 [WS_METHODS.serverLogoutAcpRegistry]: "server",
68 [WS_METHODS.serverDiscoverSourceControl]: "server",
69 [WS_METHODS.serverGetTraceDiagnostics]: "server",
70 [WS_METHODS.serverGetProcessDiagnostics]: "server",
71 [WS_METHODS.serverGetHostResources]: "server",
72 [WS_METHODS.serverGetProcessResourceHistory]: "server",
73 [WS_METHODS.serverGetResourceTelemetryHistory]: "server",
74 [WS_METHODS.serverRetryResourceTelemetry]: "server",
75 [WS_METHODS.serverGetUsageSummary]: "server",
76 [WS_METHODS.serverRefreshUsageRates]: "server",
77 [WS_METHODS.serverSignalProcess]: "server",
78 [WS_METHODS.serverReportClientActivity]: "server",
79 [WS_METHODS.serverReportHostPowerState]: "server",
80 [WS_METHODS.serverGetBackgroundPolicy]: "server",
81 [WS_METHODS.scheduledTasksList]: "scheduledTasks",
82 [WS_METHODS.scheduledTasksSubscribe]: "scheduledTasks",
83 [WS_METHODS.scheduledTasksUpsert]: "scheduledTasks",
84 [WS_METHODS.scheduledTasksSetEnabled]: "scheduledTasks",
85 [WS_METHODS.scheduledTasksDelete]: "scheduledTasks",
86 [WS_METHODS.scheduledTasksRunNow]: "scheduledTasks",
87 [WS_METHODS.scheduledTasksRotateWebhookToken]: "scheduledTasks",
88 [WS_METHODS.scheduledTasksListWebhookDeliveries]: "scheduledTasks",
89 [WS_METHODS.scheduledTasksGetWebhookDelivery]: "scheduledTasks",
90 [WS_METHODS.secretsAnswerRequest]: "secrets",
91 [WS_METHODS.cloudGetRelayClientStatus]: "cloud",
92 [WS_METHODS.cloudInstallRelayClient]: "cloud",
93 [WS_METHODS.pullRequestsList]: "pull-requests",
94 [WS_METHODS.pullRequestsListStats]: "pull-requests",
95 [WS_METHODS.pullRequestsSummary]: "pull-requests",
96 [WS_METHODS.pullRequestsRouting]: "pull-requests",
97 [WS_METHODS.pullRequestsRoutingIdentity]: "pull-requests",
98 [WS_METHODS.pullRequestsStack]: "pull-requests",
99 [WS_METHODS.pullRequestsLinkedThreads]: "pull-requests",
100 [WS_METHODS.pullRequestsDetail]: "pull-requests",
101 [WS_METHODS.pullRequestsPreview]: "pull-requests",
102 [WS_METHODS.pullRequestsChecks]: "pull-requests",
103 [WS_METHODS.pullRequestsActivity]: "pull-requests",
104 [WS_METHODS.pullRequestsThreadComments]: "pull-requests",
105 [WS_METHODS.pullRequestsDiffFileContents]: "pull-requests",
106 [WS_METHODS.pullRequestsFilesViewed]: "pull-requests",
107 [WS_METHODS.pullRequestsRunAction]: "pull-requests",
108 [WS_METHODS.pullRequestsUpdate]: "pull-requests",
109 [WS_METHODS.pullRequestsComment]: "pull-requests",
110 [WS_METHODS.pullRequestsUpdateComment]: "pull-requests",
111 [WS_METHODS.pullRequestsSubmitReview]: "pull-requests",
112 [WS_METHODS.pullRequestsReplyToThread]: "pull-requests",
113 [WS_METHODS.pullRequestsSetThreadResolution]: "pull-requests",
114 [WS_METHODS.pullRequestsSetReaction]: "pull-requests",
115 [WS_METHODS.pullRequestsSetFilesViewed]: "pull-requests",
116 [WS_METHODS.pullRequestsInvalidate]: "pull-requests",
117 [WS_METHODS.pullRequestsSubscribeRefreshes]: "pull-requests",
118 [WS_METHODS.pullRequestsReviewerCandidates]: "pull-requests",
119 [WS_METHODS.pullRequestsRequestReviewers]: "pull-requests",
120 [WS_METHODS.pullRequestsLabelCandidates]: "pull-requests",
121 [WS_METHODS.pullRequestsSetLabels]: "pull-requests",
122 [WS_METHODS.sourceControlLookupRepository]: "source-control",
123 [WS_METHODS.sourceControlCloneRepository]: "source-control",
124 [WS_METHODS.sourceControlPublishRepository]: "source-control",
125 [WS_METHODS.projectCloneStart]: "source-control",
126 [WS_METHODS.projectCloneCancel]: "source-control",
127 [WS_METHODS.projectCloneRetry]: "source-control",
128 [WS_METHODS.subscribeProjectClones]: "source-control",
129 [WS_METHODS.projectsListEntries]: "workspace",
130 [WS_METHODS.projectsReadFile]: "workspace",
131 [WS_METHODS.projectsSearchContents]: "workspace",
132 [WS_METHODS.projectsSearchEntries]: "workspace",
133 [WS_METHODS.projectsWriteFile]: "workspace",
134 [WS_METHODS.projectsEnsureScratch]: "orchestration",
135 [WS_METHODS.projectsCreateNew]: "orchestration",
136 [WS_METHODS.shellOpenInEditor]: "workspace",
137 [WS_METHODS.filesystemBrowse]: "workspace",
138 [WS_METHODS.agentSessionsScan]: "workspace",
139 [WS_METHODS.agentSessionsImport]: "workspace",
140 [WS_METHODS.assetsCreateUrl]: "workspace",
141 [WS_METHODS.assetsPersistChatAttachments]: "orchestration",
142 [WS_METHODS.attachmentsCreateUploadUrl]: "workspace",
143 [WS_METHODS.attachmentsDelete]: "workspace",
144 [WS_METHODS.providerUploadFeedback]: "provider",
145 [WS_METHODS.subscribeVcsStatus]: "vcs",
146 [WS_METHODS.subscribeWorktreeSetup]: "vcs",
147 [WS_METHODS.worktreeSetupCancel]: "vcs",
148 [WS_METHODS.subscribeResourceTelemetry]: "server",
149 [WS_METHODS.vcsRefreshStatus]: "vcs",
150 [WS_METHODS.vcsPull]: "git",
151 [WS_METHODS.gitRunStackedAction]: "vcs",
152 [WS_METHODS.gitResolvePullRequest]: "git",
153 [WS_METHODS.gitPreparePullRequestThread]: "git",
154 [WS_METHODS.vcsListRefs]: "vcs",
155 [WS_METHODS.vcsCreateWorktree]: "vcs",
156 [WS_METHODS.vcsRemoveWorktree]: "vcs",
157 [WS_METHODS.vcsCreateRef]: "vcs",
158 [WS_METHODS.vcsSwitchRef]: "vcs",
159 [WS_METHODS.vcsInit]: "vcs",
160 [WS_METHODS.reviewGetDiffPreview]: "review",
161 [WS_METHODS.reviewGetDiffFileContents]: "review",
162 [WS_METHODS.terminalOpen]: "terminal",
163 [WS_METHODS.terminalAttach]: "terminal",
164 [WS_METHODS.terminalWrite]: "terminal",
165 [WS_METHODS.terminalResize]: "terminal",
166 [WS_METHODS.terminalClear]: "terminal",
167 [WS_METHODS.terminalRestart]: "terminal",
168 [WS_METHODS.terminalClose]: "terminal",
169 [WS_METHODS.subscribeTerminalEvents]: "terminal",
170 [WS_METHODS.subscribeTerminalMetadata]: "terminal",
171 [WS_METHODS.previewOpen]: "preview",
172 [WS_METHODS.previewNavigate]: "preview",
173 [WS_METHODS.previewResize]: "preview",
174 [WS_METHODS.previewAdjust]: "preview",
175 [WS_METHODS.previewRefresh]: "preview",
176 [WS_METHODS.previewClose]: "preview",
177 [WS_METHODS.previewList]: "preview",
178 [WS_METHODS.previewClearProfile]: "preview",
179 [WS_METHODS.previewReportStatus]: "preview",
180 [WS_METHODS.subscribePreviewEvents]: "preview",
181 [WS_METHODS.subscribeDiscoveredLocalServers]: "preview",
182 [WS_METHODS.deviceConfigure]: "device",
183 [WS_METHODS.deviceTestHost]: "device",
184 [WS_METHODS.deviceList]: "device",
185 [WS_METHODS.deviceOpen]: "device",
186 [WS_METHODS.deviceClose]: "device",
187 [WS_METHODS.deviceShutdown]: "device",
188 [WS_METHODS.deviceDetail]: "device",
189 [WS_METHODS.deviceAction]: "device",
190 [WS_METHODS.subscribeDeviceState]: "device",
191 [WS_METHODS.subscribeServerConfig]: "server",
192 [WS_METHODS.subscribeServerLifecycle]: "server",
193 [WS_METHODS.subscribeAuthAccess]: "auth",
194 [WS_METHODS.subscribeBackgroundPolicy]: "server",
195} as const satisfies Readonly<Record<WsRpcMethod, string>>;
12196
13197const RPC_SPAN_PREFIX = "ws.rpc";
14198const DEFAULT_RPC_SPAN_ATTRIBUTES = {
......@@ -22,117 +206,36 @@ const RPC_METHODS_WITH_TRACING_DISABLED: ReadonlySet<string> = new Set([
22206 WS_METHODS.serverSignalProcess,
23207]);
24208
25function shouldTraceRpc(method: string): boolean {
26 return !RPC_METHODS_WITH_TRACING_DISABLED.has(method);
27}
28
29const rpcSpanAttributes = (
30 method: string,
31 traceAttributes?: Readonly<Record<string, unknown>>,
32): Record<string, unknown> => ({
33 ...DEFAULT_RPC_SPAN_ATTRIBUTES,
34 "rpc.method": method,
35 ...traceAttributes,
36});
37
38const withRpcEffectTracing = <A, E, R>(
39 method: string,
40 effect: Effect.Effect<A, E, R>,
41 traceAttributes?: Readonly<Record<string, unknown>>,
42): Effect.Effect<A, E, R> =>
43 shouldTraceRpc(method)
44 ? effect.pipe(
45 Effect.withSpan(`${RPC_SPAN_PREFIX}.${method}`, {
46 attributes: rpcSpanAttributes(method, traceAttributes),
47 }),
48 )
49 : effect.pipe(Effect.provideService(References.TracerEnabled, false));
209/**
210 * Records each WebSocket RPC's span and request metrics. `ws.ts` adds it to the server's group
211 * after `RpcScopeAuthorization`, so it wraps authorization and also records rejected calls.
212 */
213export class RpcInstrumentation extends RpcMiddleware.Service<RpcInstrumentation>()(
214 "t3/server/RpcInstrumentation",
215) {}
50216
51const withRpcStreamTracing = <A, E, R>(
52 method: string,
53 stream: Stream.Stream<A, E, R>,
54 traceAttributes?: Readonly<Record<string, unknown>>,
55): Stream.Stream<A, E, R> =>
56 shouldTraceRpc(method)
57 ? stream.pipe(
58 Stream.withSpan(`${RPC_SPAN_PREFIX}.${method}`, {
59 attributes: rpcSpanAttributes(method, traceAttributes),
60 }),
61 )
62 : stream.pipe(Stream.provideService(References.TracerEnabled, false));
63
64const recordRpcStreamMetrics = <E>(
65 method: string,
66 startedAt: bigint,
67 exit: Exit.Exit<unknown, E>,
68): Effect.Effect<void, never, never> =>
69 Effect.gen(function* () {
70 yield* Metric.update(
71 Metric.withAttributes(rpcRequestDuration, metricAttributes({ method })),
72 Duration.nanos((yield* Clock.monotonicTimeNanos) - startedAt),
73 );
74 yield* Metric.update(
75 Metric.withAttributes(
76 rpcRequestsTotal,
77 metricAttributes({
78 method,
79 outcome: outcomeFromExit(exit),
80 }),
81 ),
82 1,
83 );
84 });
217/**
218 * Wraps each WebSocket RPC call in its `ws.rpc.<method>` span and records its request counter and
219 * duration. For a stream RPC, the middleware receives the whole stream run, so the span and the
220 * metrics cover the subscription until it ends, fails, or is interrupted. Methods in
221 * `RPC_METHODS_WITH_TRACING_DISABLED` record metrics but no spans, for the call or anything it runs.
222 */
223export const rpcInstrumentationLayer = Layer.succeed(RpcInstrumentation)((effect, { rpc }) => {
224 const method = rpc._tag;
225 const measured = effect.pipe(
226 withMetrics({ counter: rpcRequestsTotal, timer: rpcRequestDuration, attributes: { method } }),
227 );
85228
86export const observeRpcEffect = <A, E, R>(
87 method: string,
88 effect: Effect.Effect<A, E, R>,
89 traceAttributes?: Readonly<Record<string, unknown>>,
90): Effect.Effect<A, E, R> => {
91 const instrumented = effect.pipe(
92 withMetrics({
93 counter: rpcRequestsTotal,
94 timer: rpcRequestDuration,
229 if (RPC_METHODS_WITH_TRACING_DISABLED.has(method)) {
230 return measured.pipe(Effect.provideService(References.TracerEnabled, false));
231 }
232 return measured.pipe(
233 Effect.withSpan(`${RPC_SPAN_PREFIX}.${method}`, {
95234 attributes: {
96 method,
235 ...DEFAULT_RPC_SPAN_ATTRIBUTES,
236 "rpc.method": method,
237 "rpc.aggregate": RPC_AGGREGATES[method as WsRpcMethod],
97238 },
98239 }),
99240 );
100
101 return withRpcEffectTracing(method, instrumented, traceAttributes);
102};
103
104export const observeRpcStream = <A, E, R>(
105 method: string,
106 stream: Stream.Stream<A, E, R>,
107 traceAttributes?: Readonly<Record<string, unknown>>,
108): Stream.Stream<A, E, R> => {
109 const instrumented = Stream.unwrap(
110 Effect.gen(function* () {
111 const startedAt = yield* Clock.monotonicTimeNanos;
112 return stream.pipe(Stream.onExit((exit) => recordRpcStreamMetrics(method, startedAt, exit)));
113 }),
114 );
115
116 return withRpcStreamTracing(method, instrumented, traceAttributes);
117};
118
119export const observeRpcStreamEffect = <A, StreamError, StreamContext, EffectError, EffectContext>(
120 method: string,
121 effect: Effect.Effect<Stream.Stream<A, StreamError, StreamContext>, EffectError, EffectContext>,
122 traceAttributes?: Readonly<Record<string, unknown>>,
123): Stream.Stream<A, StreamError | EffectError, StreamContext | EffectContext> => {
124 const instrumented = Stream.unwrap(
125 Effect.gen(function* () {
126 const startedAt = yield* Clock.monotonicTimeNanos;
127 // onError also runs when the stream is interrupted before it is produced.
128 const stream = yield* effect.pipe(
129 Effect.onError((cause) => recordRpcStreamMetrics(method, startedAt, Exit.failCause(cause))),
130 );
131 return stream.pipe(
132 Stream.onExit((streamExit) => recordRpcStreamMetrics(method, startedAt, streamExit)),
133 );
134 }),
135 );
136
137 return withRpcStreamTracing(method, instrumented, traceAttributes);
138};
241});
apps/server/src/ws.ts+902-1601
......@@ -159,11 +159,6 @@ import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
159159import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts";
160160import * as OrchestrationEventStore from "./persistence/OrchestrationEventStore.ts";
161161import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts";
162import {
163 observeRpcEffect,
164 observeRpcStream,
165 observeRpcStreamEffect,
166} from "./observability/RpcInstrumentation.ts";
167162import * as ProviderRegistry from "./provider/ProviderRegistry.ts";
168163import * as ProviderInstanceRegistry from "./provider/ProviderInstanceRegistry.ts";
169164import * as AcpRegistrySupport from "./provider/acp/AcpRegistrySupport.ts";
......@@ -215,6 +210,7 @@ import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
215210import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
216211import { requiredScopeForDeviceList, rpcAuthorizationError } from "./auth/RpcAuthorization.ts";
217212import * as RpcAuthorization from "./auth/RpcAuthorization.ts";
213import { RpcInstrumentation, rpcInstrumentationLayer } from "./observability/RpcInstrumentation.ts";
218214import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
219215import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";
220216import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";
......@@ -541,7 +537,8 @@ function projectFileFailureContext(
541537
542538const PROVIDER_STATUS_DEBOUNCE_MS = 200;
543539
544const ServerWsRpcGroup = WsRpcGroup;
540// Middleware added later wraps middleware added earlier, so instrumentation wraps authorization.
541const ServerWsRpcGroup = WsRpcGroup.middleware(RpcInstrumentation);
545542// When a resuming client's cursor is more than this many events behind the
546543// current head, skip the per-event catch-up replay and send a fresh shell
547544// snapshot instead. Replaying each intervening event costs a shell refetch;
......@@ -1808,851 +1805,595 @@ const layerWsRpc = (
18081805
18091806 const handlers = ServerWsRpcGroup.of({
18101807 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: (command) =>
1811 observeRpcEffect(
1812 ORCHESTRATION_V2_WS_METHODS.dispatchCommand,
1813 startup
1814 .enqueueCommand(
1815 // A retry also restarts the preparation work the launch owns.
1816 (command.type === "prepared-run.retry"
1817 ? threadLaunch.retryPreparation(command)
1818 : ThreadMessageIntake.dispatchCommand(
1819 ThreadManagementService.withCreationProvenance(command, {
1820 createdBy: "user",
1821 creationSource:
1822 "creationSource" in command ? command.creationSource : "web",
1823 }),
1824 )
1825 ).pipe(Effect.provide(intakeContext)),
1826 )
1827 .pipe(
1828 Effect.tap(() => recordClientCommandAnalytics(command)),
1829 Effect.map((result) => ({ sequence: result.sequence })),
1830 Effect.mapError((cause) => {
1831 const detail = userFacingDispatchErrorMessage(cause);
1832 return new OrchestrationV2DispatchCommandError({
1833 commandId: command.commandId,
1834 commandType: command.type,
1835 message: detail ?? "Failed to dispatch orchestration V2 command",
1836 ...(detail === undefined ? {} : { detail }),
1837 cause,
1838 });
1839 }),
1840 ),
1841 {
1842 "rpc.aggregate": "orchestrationV2",
1843 "orchestration_v2.command_id": command.commandId,
1844 "orchestration_v2.command_type": command.type,
1845 "orchestration_v2.thread_id":
1846 command.type === "thread.fork" || command.type === "thread.merge_back"
1847 ? command.targetThreadId
1848 : command.type === "delegated_task.request" ||
1849 command.type === "delegated_task.wake-policy" ||
1850 command.type === "delegated_task.completion-delivery.acknowledge" ||
1851 command.type === "delegated_task.completion-delivery.dispose" ||
1852 command.type === "thread.created.record"
1853 ? command.parentThreadId
1854 : command.threadId,
1855 ...(command.type === "thread.fork" || command.type === "thread.merge_back"
1856 ? { "orchestration_v2.source_thread_id": command.sourceThreadId }
1857 : {}),
1858 },
1808 Effect.annotateCurrentSpan({
1809 "orchestration_v2.command_id": command.commandId,
1810 "orchestration_v2.command_type": command.type,
1811 "orchestration_v2.thread_id":
1812 command.type === "thread.fork" || command.type === "thread.merge_back"
1813 ? command.targetThreadId
1814 : command.type === "delegated_task.request" ||
1815 command.type === "delegated_task.wake-policy" ||
1816 command.type === "delegated_task.completion-delivery.acknowledge" ||
1817 command.type === "delegated_task.completion-delivery.dispose" ||
1818 command.type === "thread.created.record"
1819 ? command.parentThreadId
1820 : command.threadId,
1821 ...(command.type === "thread.fork" || command.type === "thread.merge_back"
1822 ? { "orchestration_v2.source_thread_id": command.sourceThreadId }
1823 : {}),
1824 }).pipe(
1825 Effect.andThen(
1826 startup
1827 .enqueueCommand(
1828 // A retry also restarts the preparation work the launch owns.
1829 (command.type === "prepared-run.retry"
1830 ? threadLaunch.retryPreparation(command)
1831 : ThreadMessageIntake.dispatchCommand(
1832 ThreadManagementService.withCreationProvenance(command, {
1833 createdBy: "user",
1834 creationSource:
1835 "creationSource" in command ? command.creationSource : "web",
1836 }),
1837 )
1838 ).pipe(Effect.provide(intakeContext)),
1839 )
1840 .pipe(
1841 Effect.tap(() => recordClientCommandAnalytics(command)),
1842 Effect.map((result) => ({ sequence: result.sequence })),
1843 Effect.mapError((cause) => {
1844 const detail = userFacingDispatchErrorMessage(cause);
1845 return new OrchestrationV2DispatchCommandError({
1846 commandId: command.commandId,
1847 commandType: command.type,
1848 message: detail ?? "Failed to dispatch orchestration V2 command",
1849 ...(detail === undefined ? {} : { detail }),
1850 cause,
1851 });
1852 }),
1853 ),
1854 ),
18591855 ),
18601856 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: (input) =>
1861 observeRpcEffect(
1862 ORCHESTRATION_V2_WS_METHODS.getWorkflowScript,
1863 readWorkflowScript({ scriptPath: input.scriptPath }),
1864 { "rpc.aggregate": "orchestration" },
1865 ),
1857 readWorkflowScript({ scriptPath: input.scriptPath }),
18661858 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: (input) =>
1867 observeRpcEffect(
1868 ORCHESTRATION_V2_WS_METHODS.getTurnItem,
1869 threadManagement.getTurnItem(input).pipe(
1870 Effect.mapError(
1871 (cause) =>
1872 new OrchestrationV2GetThreadProjectionError({
1873 threadId: input.threadId,
1874 message: "Failed to load turn item",
1875 cause,
1876 }),
1877 ),
1859 threadManagement.getTurnItem(input).pipe(
1860 Effect.mapError(
1861 (cause) =>
1862 new OrchestrationV2GetThreadProjectionError({
1863 threadId: input.threadId,
1864 message: "Failed to load turn item",
1865 cause,
1866 }),
18781867 ),
1879 { "rpc.aggregate": "orchestration" },
18801868 ),
18811869 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: (input) =>
1882 observeRpcEffect(
1883 ORCHESTRATION_V2_WS_METHODS.getTurnDiff,
1884 checkpointDiffQuery.getTurnDiff(input).pipe(
1885 Effect.mapError(
1886 (cause) =>
1887 new OrchestrationGetTurnDiffError({
1888 message: "Failed to load turn diff",
1889 cause,
1890 }),
1891 ),
1870 checkpointDiffQuery.getTurnDiff(input).pipe(
1871 Effect.mapError(
1872 (cause) =>
1873 new OrchestrationGetTurnDiffError({
1874 message: "Failed to load turn diff",
1875 cause,
1876 }),
18921877 ),
1893 { "rpc.aggregate": "orchestration" },
18941878 ),
18951879 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: (input) =>
1896 observeRpcEffect(
1897 ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff,
1898 checkpointDiffQuery.getFullThreadDiff(input).pipe(
1899 Effect.mapError(
1900 (cause) =>
1901 new OrchestrationGetFullThreadDiffError({
1902 message: "Failed to load full thread diff",
1903 cause,
1904 }),
1905 ),
1880 checkpointDiffQuery.getFullThreadDiff(input).pipe(
1881 Effect.mapError(
1882 (cause) =>
1883 new OrchestrationGetFullThreadDiffError({
1884 message: "Failed to load full thread diff",
1885 cause,
1886 }),
19061887 ),
1907 { "rpc.aggregate": "orchestration" },
19081888 ),
19091889 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: (input) =>
1910 observeRpcEffect(
1911 ORCHESTRATION_V2_WS_METHODS.searchThreads,
1912 threadSearch.search(input).pipe(
1913 Effect.mapError(
1914 (cause) =>
1915 new OrchestrationSearchThreadsError({
1916 message: "Failed to search threads",
1917 cause,
1918 }),
1919 ),
1890 threadSearch.search(input).pipe(
1891 Effect.mapError(
1892 (cause) =>
1893 new OrchestrationSearchThreadsError({
1894 message: "Failed to search threads",
1895 cause,
1896 }),
19201897 ),
1921 { "rpc.aggregate": "orchestration" },
19221898 ),
19231899 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: (_input) =>
1924 observeRpcEffect(
1925 ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot,
1926 getOrchestrationV2ArchivedShellSnapshot,
1927 { "rpc.aggregate": "orchestration" },
1928 ),
1900 getOrchestrationV2ArchivedShellSnapshot,
19291901 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: (input) =>
1930 observeRpcEffect(
1931 ORCHESTRATION_V2_WS_METHODS.getThreadProjection,
1932 // Pre-pagination clients still call this compatibility endpoint.
1933 // Keep stale clients from materializing an unbounded transcript.
1934 threadManagement
1935 .getThreadSnapshotWindow(input.threadId, {
1936 rowLimit: THREAD_HISTORY_SNAPSHOT_ROW_LIMIT,
1937 })
1938 .pipe(
1939 Effect.map((snapshot) =>
1940 projectThreadProjectionForWire(
1941 buildBoundedThreadProjection({
1942 projection: snapshot.projection,
1943 snapshotSequence: snapshot.snapshotSequence,
1944 }).projection,
1902 Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe(
1903 Effect.andThen(
1904 // Pre-pagination clients still call this compatibility endpoint.
1905 // Keep stale clients from materializing an unbounded transcript.
1906 threadManagement
1907 .getThreadSnapshotWindow(input.threadId, {
1908 rowLimit: THREAD_HISTORY_SNAPSHOT_ROW_LIMIT,
1909 })
1910 .pipe(
1911 Effect.map((snapshot) =>
1912 projectThreadProjectionForWire(
1913 buildBoundedThreadProjection({
1914 projection: snapshot.projection,
1915 snapshotSequence: snapshot.snapshotSequence,
1916 }).projection,
1917 ),
1918 ),
1919 Effect.mapError(
1920 (cause) =>
1921 new OrchestrationV2GetThreadProjectionError({
1922 threadId: input.threadId,
1923 message: `Failed to load orchestration V2 thread ${input.threadId}`,
1924 cause,
1925 }),
19451926 ),
19461927 ),
1947 Effect.mapError(
1948 (cause) =>
1949 new OrchestrationV2GetThreadProjectionError({
1950 threadId: input.threadId,
1951 message: `Failed to load orchestration V2 thread ${input.threadId}`,
1952 cause,
1953 }),
1954 ),
1955 ),
1956 {
1957 "rpc.aggregate": "orchestrationV2",
1958 "orchestration_v2.thread_id": input.threadId,
1959 },
1928 ),
19601929 ),
19611930 [ORCHESTRATION_V2_WS_METHODS.launchThread]: (input) =>
1962 observeRpcEffect(
1963 ORCHESTRATION_V2_WS_METHODS.launchThread,
1964 startup
1965 .enqueueCommand(
1966 ThreadMessageIntake.launchThread({
1967 commandId: input.commandId,
1968 ...(input.threadId === undefined ? {} : { threadId: input.threadId }),
1969 ...(input.reuseExistingThread === undefined
1970 ? {}
1971 : { reuseExistingThread: input.reuseExistingThread }),
1972 projectId: input.projectId,
1973 title: input.title,
1974 ...(input.generateTitle === undefined
1975 ? {}
1976 : { generateTitle: input.generateTitle }),
1977 modelSelection: input.modelSelection,
1978 runtimeMode: input.runtimeMode,
1979 interactionMode: input.interactionMode,
1980 workspaceStrategy: input.workspaceStrategy,
1981 ...(input.initialMessage === undefined
1982 ? {}
1983 : {
1984 initialMessage: {
1985 ...(input.initialMessage.messageId === undefined
1986 ? {}
1987 : { messageId: input.initialMessage.messageId }),
1988 text: input.initialMessage.text,
1989 attachments: input.initialMessage.attachments,
1990 ...(input.initialMessage.context === undefined
1991 ? {}
1992 : { context: input.initialMessage.context }),
1993 },
1994 }),
1995 createdBy: "user",
1996 creationSource: input.creationSource ?? "web",
1997 }).pipe(Effect.provide(intakeContext)),
1998 )
1999 .pipe(
2000 Effect.tap(() =>
2001 analytics
2002 .record("client.thread.started", originProps)
2003 .pipe(
2004 Effect.andThen(
2005 input.initialMessage === undefined
2006 ? Effect.void
2007 : analytics.record("client.turn.requested", originProps),
1931 Effect.annotateCurrentSpan({
1932 "orchestration_v2.command_id": input.commandId,
1933 "orchestration_v2.project_id": input.projectId,
1934 }).pipe(
1935 Effect.andThen(
1936 startup
1937 .enqueueCommand(
1938 ThreadMessageIntake.launchThread({
1939 commandId: input.commandId,
1940 ...(input.threadId === undefined ? {} : { threadId: input.threadId }),
1941 ...(input.reuseExistingThread === undefined
1942 ? {}
1943 : { reuseExistingThread: input.reuseExistingThread }),
1944 projectId: input.projectId,
1945 title: input.title,
1946 ...(input.generateTitle === undefined
1947 ? {}
1948 : { generateTitle: input.generateTitle }),
1949 modelSelection: input.modelSelection,
1950 runtimeMode: input.runtimeMode,
1951 interactionMode: input.interactionMode,
1952 workspaceStrategy: input.workspaceStrategy,
1953 ...(input.initialMessage === undefined
1954 ? {}
1955 : {
1956 initialMessage: {
1957 ...(input.initialMessage.messageId === undefined
1958 ? {}
1959 : { messageId: input.initialMessage.messageId }),
1960 text: input.initialMessage.text,
1961 attachments: input.initialMessage.attachments,
1962 ...(input.initialMessage.context === undefined
1963 ? {}
1964 : { context: input.initialMessage.context }),
1965 },
1966 }),
1967 createdBy: "user",
1968 creationSource: input.creationSource ?? "web",
1969 }).pipe(Effect.provide(intakeContext)),
1970 )
1971 .pipe(
1972 Effect.tap(() =>
1973 analytics
1974 .record("client.thread.started", originProps)
1975 .pipe(
1976 Effect.andThen(
1977 input.initialMessage === undefined
1978 ? Effect.void
1979 : analytics.record("client.turn.requested", originProps),
1980 ),
1981 Effect.ignore,
20081982 ),
2009 Effect.ignore,
2010 ),
1983 ),
1984 Effect.map((result) => ({
1985 ...result,
1986 projection: projectThreadProjectionForWire(result.projection),
1987 })),
1988 Effect.catchTags({
1989 AttachmentClaimError: (cause) =>
1990 new OrchestrationV2ThreadLaunchError({
1991 commandId: input.commandId,
1992 projectId: input.projectId,
1993 message: cause.message,
1994 cause,
1995 }),
1996 ThreadLaunchError: (cause) =>
1997 new OrchestrationV2ThreadLaunchError({
1998 commandId: input.commandId,
1999 projectId: input.projectId,
2000 message: "Failed to launch thread",
2001 cause,
2002 }),
2003 ServerRuntimeStartupError: (cause) =>
2004 new OrchestrationV2ThreadLaunchError({
2005 commandId: input.commandId,
2006 projectId: input.projectId,
2007 message: "Failed to launch thread",
2008 cause,
2009 }),
2010 }),
20112011 ),
2012 Effect.map((result) => ({
2013 ...result,
2014 projection: projectThreadProjectionForWire(result.projection),
2015 })),
2016 Effect.catchTags({
2017 AttachmentClaimError: (cause) =>
2018 new OrchestrationV2ThreadLaunchError({
2019 commandId: input.commandId,
2020 projectId: input.projectId,
2021 message: cause.message,
2022 cause,
2023 }),
2024 ThreadLaunchError: (cause) =>
2025 new OrchestrationV2ThreadLaunchError({
2026 commandId: input.commandId,
2027 projectId: input.projectId,
2028 message: "Failed to launch thread",
2029 cause,
2030 }),
2031 ServerRuntimeStartupError: (cause) =>
2032 new OrchestrationV2ThreadLaunchError({
2033 commandId: input.commandId,
2034 projectId: input.projectId,
2035 message: "Failed to launch thread",
2036 cause,
2037 }),
2038 }),
2039 ),
2040 {
2041 "rpc.aggregate": "orchestration",
2042 "orchestration_v2.command_id": input.commandId,
2043 "orchestration_v2.project_id": input.projectId,
2044 },
2012 ),
20452013 ),
20462014 [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: (_input) =>
2047 observeRpcStreamEffect(
2048 ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell,
2049 subscribeOrchestrationV2ArchivedShell(),
2050 { "rpc.aggregate": "orchestration" },
2051 ),
2015 Stream.unwrap(subscribeOrchestrationV2ArchivedShell()),
20522016 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: (input) =>
2053 observeRpcStreamEffect(
2054 ORCHESTRATION_V2_WS_METHODS.subscribeShell,
2055 subscribeOrchestrationV2Shell(input),
2056 {
2057 "rpc.aggregate": "orchestrationV2",
2058 },
2059 ),
2017 Stream.unwrap(subscribeOrchestrationV2Shell(input)),
20602018 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: (input) =>
2061 observeRpcStreamEffect(
2062 ORCHESTRATION_V2_WS_METHODS.subscribeThread,
2063 subscribeOrchestrationV2Thread(input),
2064 {
2065 "rpc.aggregate": "orchestrationV2",
2066 "orchestration_v2.thread_id": input.threadId,
2067 },
2019 Stream.unwrap(
2020 Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe(
2021 Effect.andThen(subscribeOrchestrationV2Thread(input)),
2022 ),
20682023 ),
20692024 [WS_METHODS.scheduledTasksList]: (_input) =>
2070 observeRpcEffect(
2071 WS_METHODS.scheduledTasksList,
2072 scheduledTasks.list().pipe(Effect.map(withVisibleWebhookUrls)),
2073 { "rpc.aggregate": "scheduledTasks" },
2074 ),
2025 scheduledTasks.list().pipe(Effect.map(withVisibleWebhookUrls)),
20752026 [WS_METHODS.scheduledTasksSubscribe]: (_input) =>
2076 observeRpcStream(
2077 WS_METHODS.scheduledTasksSubscribe,
2078 scheduledTasks.subscribeList().pipe(Stream.map(withVisibleWebhookUrls)),
2079 { "rpc.aggregate": "scheduledTasks" },
2080 ),
2081 [WS_METHODS.scheduledTasksUpsert]: (input) =>
2082 observeRpcEffect(WS_METHODS.scheduledTasksUpsert, scheduledTasks.upsert(input), {
2083 "rpc.aggregate": "scheduledTasks",
2084 }),
2027 scheduledTasks.subscribeList().pipe(Stream.map(withVisibleWebhookUrls)),
2028 [WS_METHODS.scheduledTasksUpsert]: (input) => scheduledTasks.upsert(input),
20852029 [WS_METHODS.scheduledTasksSetEnabled]: (input) =>
2086 observeRpcEffect(WS_METHODS.scheduledTasksSetEnabled, scheduledTasks.setEnabled(input), {
2087 "rpc.aggregate": "scheduledTasks",
2088 "scheduled_task.id": input.id,
2089 }),
2030 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2031 Effect.andThen(scheduledTasks.setEnabled(input)),
2032 ),
20902033 [WS_METHODS.scheduledTasksDelete]: (input) =>
2091 observeRpcEffect(WS_METHODS.scheduledTasksDelete, scheduledTasks.delete(input), {
2092 "rpc.aggregate": "scheduledTasks",
2093 "scheduled_task.id": input.id,
2094 }),
2034 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2035 Effect.andThen(scheduledTasks.delete(input)),
2036 ),
20952037 [WS_METHODS.scheduledTasksRunNow]: (input) =>
2096 observeRpcEffect(WS_METHODS.scheduledTasksRunNow, scheduledTasks.runNow(input), {
2097 "rpc.aggregate": "scheduledTasks",
2098 "scheduled_task.id": input.id,
2099 }),
2038 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2039 Effect.andThen(scheduledTasks.runNow(input)),
2040 ),
21002041 [WS_METHODS.scheduledTasksRotateWebhookToken]: (input) =>
2101 observeRpcEffect(
2102 WS_METHODS.scheduledTasksRotateWebhookToken,
2103 scheduledTasks.rotateWebhookToken(input),
2104 { "rpc.aggregate": "scheduledTasks", "scheduled_task.id": input.id },
2042 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2043 Effect.andThen(scheduledTasks.rotateWebhookToken(input)),
21052044 ),
21062045 [WS_METHODS.secretsAnswerRequest]: (input) =>
2107 observeRpcEffect(WS_METHODS.secretsAnswerRequest, secretRequests.answer(input), {
2108 "rpc.aggregate": "secrets",
2109 "orchestration_v2.thread_id": input.threadId,
2110 }),
2046 Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe(
2047 Effect.andThen(secretRequests.answer(input)),
2048 ),
21112049 [WS_METHODS.scheduledTasksListWebhookDeliveries]: (input) =>
2112 observeRpcEffect(
2113 WS_METHODS.scheduledTasksListWebhookDeliveries,
2114 scheduledTasks.listWebhookDeliveries(input),
2115 { "rpc.aggregate": "scheduledTasks", "scheduled_task.id": input.id },
2050 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2051 Effect.andThen(scheduledTasks.listWebhookDeliveries(input)),
21162052 ),
21172053 [WS_METHODS.scheduledTasksGetWebhookDelivery]: (input) =>
2118 observeRpcEffect(
2119 WS_METHODS.scheduledTasksGetWebhookDelivery,
2120 scheduledTasks.getWebhookDelivery(input),
2121 { "rpc.aggregate": "scheduledTasks", "scheduled_task.id": input.id },
2122 ),
2123 [WS_METHODS.serverProbe]: (_input) =>
2124 observeRpcEffect(WS_METHODS.serverProbe, Effect.succeed({}), {
2125 "rpc.aggregate": "server",
2126 }),
2127 [WS_METHODS.serverGetConfig]: (_input) =>
2128 observeRpcEffect(
2129 WS_METHODS.serverGetConfig,
2130 loadServerConfig({ usageLimitsCommand: false }),
2131 {
2132 "rpc.aggregate": "server",
2133 },
2054 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2055 Effect.andThen(scheduledTasks.getWebhookDelivery(input)),
21342056 ),
2057 [WS_METHODS.serverProbe]: (_input) => Effect.succeed({}),
2058 [WS_METHODS.serverGetConfig]: (_input) => loadServerConfig({ usageLimitsCommand: false }),
21352059 [WS_METHODS.serverSearchAcpRegistry]: (input) =>
2136 observeRpcEffect(
2137 WS_METHODS.serverSearchAcpRegistry,
2138 acpRegistryCatalog
2139 .search(input)
2140 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2141 { "rpc.aggregate": "server" },
2142 ),
2060 acpRegistryCatalog
2061 .search(input)
2062 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
21432063 [WS_METHODS.serverPrepareAcpRegistryAgent]: (input) =>
2144 observeRpcEffect(
2145 WS_METHODS.serverPrepareAcpRegistryAgent,
2146 acpRegistryCatalog
2147 .prepare(input)
2148 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2149 {
2150 "rpc.aggregate": "server",
2151 "acp_registry.agent_id": input.agentId,
2152 },
2064 Effect.annotateCurrentSpan({ "acp_registry.agent_id": input.agentId }).pipe(
2065 Effect.andThen(
2066 acpRegistryCatalog
2067 .prepare(input)
2068 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2069 ),
21532070 ),
21542071 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: (input) =>
2155 observeRpcEffect(
2156 WS_METHODS.serverUninstallAcpRegistryManagedBinary,
2157 acpRegistryCatalog
2158 .uninstallManagedBinary(input)
2159 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2160 {
2161 "rpc.aggregate": "server",
2162 "acp_registry.agent_id": input.agentId,
2163 },
2072 Effect.annotateCurrentSpan({ "acp_registry.agent_id": input.agentId }).pipe(
2073 Effect.andThen(
2074 acpRegistryCatalog
2075 .uninstallManagedBinary(input)
2076 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2077 ),
21642078 ),
21652079 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: (input) =>
2166 observeRpcEffect(
2167 WS_METHODS.serverAcceptAcpRegistryUrlAuth,
2168 acpRegistryRuntimeCoordinator
2169 .acceptUrlAuthentication(input)
2170 .pipe(Effect.map((accepted) => ({ accepted }))),
2171 {
2172 "rpc.aggregate": "server",
2173 "provider.instance_id": input.instanceId,
2174 },
2080 Effect.annotateCurrentSpan({ "provider.instance_id": input.instanceId }).pipe(
2081 Effect.andThen(
2082 acpRegistryRuntimeCoordinator
2083 .acceptUrlAuthentication(input)
2084 .pipe(Effect.map((accepted) => ({ accepted }))),
2085 ),
21752086 ),
21762087 [WS_METHODS.serverListAcpRegistrySessions]: (input) =>
2177 observeRpcEffect(
2178 WS_METHODS.serverListAcpRegistrySessions,
2179 listAcpRegistrySessions(input),
2180 {
2181 "rpc.aggregate": "server",
2182 "provider.instance_id": input.instanceId,
2183 "project.id": input.projectId,
2184 },
2185 ),
2088 Effect.annotateCurrentSpan({
2089 "provider.instance_id": input.instanceId,
2090 "project.id": input.projectId,
2091 }).pipe(Effect.andThen(listAcpRegistrySessions(input))),
21862092 [WS_METHODS.serverImportAcpRegistrySession]: (input) =>
2187 observeRpcEffect(
2188 WS_METHODS.serverImportAcpRegistrySession,
2189 importAcpRegistrySession(input),
2190 {
2191 "rpc.aggregate": "server",
2192 "provider.instance_id": input.instanceId,
2193 "project.id": input.projectId,
2194 },
2195 ),
2093 Effect.annotateCurrentSpan({
2094 "provider.instance_id": input.instanceId,
2095 "project.id": input.projectId,
2096 }).pipe(Effect.andThen(importAcpRegistrySession(input))),
21962097 [WS_METHODS.serverDeleteAcpRegistrySession]: (input) =>
2197 observeRpcEffect(
2198 WS_METHODS.serverDeleteAcpRegistrySession,
2199 deleteAcpRegistrySession(input),
2200 {
2201 "rpc.aggregate": "server",
2202 "provider.instance_id": input.instanceId,
2203 "project.id": input.projectId,
2204 },
2205 ),
2098 Effect.annotateCurrentSpan({
2099 "provider.instance_id": input.instanceId,
2100 "project.id": input.projectId,
2101 }).pipe(Effect.andThen(deleteAcpRegistrySession(input))),
22062102 [WS_METHODS.serverListAcpRegistryProviders]: (input) =>
2207 observeRpcEffect(
2208 WS_METHODS.serverListAcpRegistryProviders,
2209 listAcpRegistryProviders(input),
2210 {
2211 "rpc.aggregate": "server",
2212 "provider.instance_id": input.instanceId,
2213 "project.id": input.projectId,
2214 },
2215 ),
2103 Effect.annotateCurrentSpan({
2104 "provider.instance_id": input.instanceId,
2105 "project.id": input.projectId,
2106 }).pipe(Effect.andThen(listAcpRegistryProviders(input))),
22162107 [WS_METHODS.serverSetAcpRegistryProvider]: (input) =>
2217 observeRpcEffect(WS_METHODS.serverSetAcpRegistryProvider, setAcpRegistryProvider(input), {
2218 "rpc.aggregate": "server",
2108 Effect.annotateCurrentSpan({
22192109 "provider.instance_id": input.instanceId,
22202110 "project.id": input.projectId,
2221 }),
2111 }).pipe(Effect.andThen(setAcpRegistryProvider(input))),
22222112 [WS_METHODS.serverDisableAcpRegistryProvider]: (input) =>
2223 observeRpcEffect(
2224 WS_METHODS.serverDisableAcpRegistryProvider,
2225 disableAcpRegistryProvider(input),
2226 {
2227 "rpc.aggregate": "server",
2228 "provider.instance_id": input.instanceId,
2229 "project.id": input.projectId,
2230 },
2231 ),
2113 Effect.annotateCurrentSpan({
2114 "provider.instance_id": input.instanceId,
2115 "project.id": input.projectId,
2116 }).pipe(Effect.andThen(disableAcpRegistryProvider(input))),
22322117 [WS_METHODS.serverLogoutAcpRegistry]: (input) =>
2233 observeRpcEffect(
2234 WS_METHODS.serverLogoutAcpRegistry,
2235 Effect.gen(function* () {
2236 const { instance, manager } = yield* acpSessionManager(input.instanceId);
2237 const snapshot = yield* instance.snapshot.getSnapshot;
2238 if (snapshot.auth.canLogout !== true) {
2239 return yield* new AcpRegistryOperationError({
2240 reason: "logout_unsupported",
2241 message: "The ACP agent does not advertise logout.",
2242 });
2243 }
2244 if (instance.auth) {
2245 yield* providerAuth.logout(input).pipe(
2246 Effect.mapError(
2247 (cause) =>
2248 new AcpRegistryOperationError({
2249 reason: "logout_failed",
2250 message: "Could not sign out of the ACP agent.",
2251 cause,
2252 }),
2253 ),
2254 );
2255 } else {
2256 yield* providerSessionManager.closeInstance(input.instanceId).pipe(
2257 Effect.mapError(
2258 (cause) =>
2259 new AcpRegistryOperationError({
2260 reason: "logout_failed",
2261 message: "Could not stop live sessions before ACP logout.",
2262 cause,
2263 }),
2264 ),
2265 );
2266 yield* manager.logout(config.cwd);
2267 }
2268 yield* providerRegistry.refreshInstance(input.instanceId);
2269 return { loggedOut: true } as const;
2270 }),
2271 {
2272 "rpc.aggregate": "server",
2273 "provider.instance_id": input.instanceId,
2274 },
2275 ),
2276 [WS_METHODS.serverRefreshProviders]: (input) =>
2277 observeRpcEffect(
2278 WS_METHODS.serverRefreshProviders,
2279 Effect.gen(function* () {
2280 // Only explicit catalog refreshes bypass T3's caches. Workspace
2281 // discovery and background status checks retain their timers.
2282 if (input.refreshModels) {
2283 yield* modelManifest.forceRefresh;
2284 const instances = yield* providerInstances.listInstances;
2285 yield* Effect.forEach(
2286 instances.filter(
2287 (instance) =>
2288 input.instanceId === undefined || input.instanceId === instance.instanceId,
2289 ),
2290 (instance) =>
2291 Effect.gen(function* () {
2292 yield* instance.invalidateCaches ?? Effect.void;
2293 const maintenance = yield* instance.snapshot.resolveMaintenance({
2294 fresh: true,
2295 });
2296 if (maintenance.packageName)
2297 providerVersionCache.delete(maintenance.packageName);
2298 }),
2299 { concurrency: "unbounded", discard: true },
2300 );
2301 }
2302 // An untargeted refresh is "re-read everything's status", which
2303 // includes quota from configured usage-limit sources. Awaited,
2304 // not forked: the RPC scope closes on return and would
2305 // interrupt a fork before the hub answered.
2306 if (input.instanceId === undefined) {
2307 yield* usageLimitSources.refresh;
2308 }
2309 let providers = yield* input.cwd !== undefined && input.instanceId !== undefined
2310 ? providerRegistry.refreshWorkspaceSnapshot({
2311 instanceId: input.instanceId,
2312 cwd: input.cwd,
2313 fresh: input.fresh === true,
2314 })
2315 : input.instanceId !== undefined
2316 ? providerRegistry.refreshInstance(input.instanceId)
2317 : providerRegistry.refresh();
2318 if (input.refreshModels) {
2319 const instances = yield* providerInstances.listInstances;
2320 for (const instance of instances) {
2321 if (
2322 !instance.refreshModels ||
2323 (input.instanceId !== undefined && input.instanceId !== instance.instanceId) ||
2324 !providers.some(
2325 (provider) =>
2326 provider.instanceId === instance.instanceId &&
2327 provider.enabled &&
2328 provider.installed,
2329 )
2330 )
2331 continue;
2332 yield* instance.refreshModels().pipe(
2118 Effect.annotateCurrentSpan({ "provider.instance_id": input.instanceId }).pipe(
2119 Effect.andThen(
2120 Effect.gen(function* () {
2121 const { instance, manager } = yield* acpSessionManager(input.instanceId);
2122 const snapshot = yield* instance.snapshot.getSnapshot;
2123 if (snapshot.auth.canLogout !== true) {
2124 return yield* new AcpRegistryOperationError({
2125 reason: "logout_unsupported",
2126 message: "The ACP agent does not advertise logout.",
2127 });
2128 }
2129 if (instance.auth) {
2130 yield* providerAuth.logout(input).pipe(
2131 Effect.mapError(
2132 (cause) =>
2133 new AcpRegistryOperationError({
2134 reason: "logout_failed",
2135 message: "Could not sign out of the ACP agent.",
2136 cause,
2137 }),
2138 ),
2139 );
2140 } else {
2141 yield* providerSessionManager.closeInstance(input.instanceId).pipe(
23332142 Effect.mapError(
2334 (error) =>
2335 new ProviderSetupError({
2336 instanceId: instance.instanceId,
2337 operation: "refresh-models",
2338 detail: error.detail,
2143 (cause) =>
2144 new AcpRegistryOperationError({
2145 reason: "logout_failed",
2146 message: "Could not stop live sessions before ACP logout.",
2147 cause,
23392148 }),
23402149 ),
23412150 );
2342 providers = yield* providerRegistry.refreshInstance(instance.instanceId);
2151 yield* manager.logout(config.cwd);
23432152 }
2344 }
2345 return { providers };
2346 }),
2347 { "rpc.aggregate": "server" },
2153 yield* providerRegistry.refreshInstance(input.instanceId);
2154 return { loggedOut: true } as const;
2155 }),
2156 ),
23482157 ),
2349 [WS_METHODS.providerUploadFeedback]: (input) =>
2350 observeRpcEffect(
2351 WS_METHODS.providerUploadFeedback,
2352 Effect.gen(function* () {
2353 const projection = yield* threadManagement.getThreadRecords(input.threadId, [
2354 "providerThreads",
2355 ]);
2356 const providerThread =
2357 projection.providerThreads.find(
2358 (candidate) => candidate.id === projection.thread.activeProviderThreadId,
2359 ) ?? projection.providerThreads.at(-1);
2360 const providerSessionId = providerThread?.providerSessionId ?? null;
2361 if (providerThread === undefined || providerSessionId === null) {
2362 return yield* Effect.fail(
2363 new ProviderUploadFeedbackError({
2364 threadId: input.threadId,
2365 cause: "No provider session has run in this thread yet.",
2158 [WS_METHODS.serverRefreshProviders]: (input) =>
2159 Effect.gen(function* () {
2160 // Only explicit catalog refreshes bypass T3's caches. Workspace
2161 // discovery and background status checks retain their timers.
2162 if (input.refreshModels) {
2163 yield* modelManifest.forceRefresh;
2164 const instances = yield* providerInstances.listInstances;
2165 yield* Effect.forEach(
2166 instances.filter(
2167 (instance) =>
2168 input.instanceId === undefined || input.instanceId === instance.instanceId,
2169 ),
2170 (instance) =>
2171 Effect.gen(function* () {
2172 yield* instance.invalidateCaches ?? Effect.void;
2173 const maintenance = yield* instance.snapshot.resolveMaintenance({
2174 fresh: true,
2175 });
2176 if (maintenance.packageName)
2177 providerVersionCache.delete(maintenance.packageName);
23662178 }),
2179 { concurrency: "unbounded", discard: true },
2180 );
2181 }
2182 // An untargeted refresh is "re-read everything's status", which
2183 // includes quota from configured usage-limit sources. Awaited,
2184 // not forked: the RPC scope closes on return and would
2185 // interrupt a fork before the hub answered.
2186 if (input.instanceId === undefined) {
2187 yield* usageLimitSources.refresh;
2188 }
2189 let providers = yield* input.cwd !== undefined && input.instanceId !== undefined
2190 ? providerRegistry.refreshWorkspaceSnapshot({
2191 instanceId: input.instanceId,
2192 cwd: input.cwd,
2193 fresh: input.fresh === true,
2194 })
2195 : input.instanceId !== undefined
2196 ? providerRegistry.refreshInstance(input.instanceId)
2197 : providerRegistry.refresh();
2198 if (input.refreshModels) {
2199 const instances = yield* providerInstances.listInstances;
2200 for (const instance of instances) {
2201 if (
2202 !instance.refreshModels ||
2203 (input.instanceId !== undefined && input.instanceId !== instance.instanceId) ||
2204 !providers.some(
2205 (provider) =>
2206 provider.instanceId === instance.instanceId &&
2207 provider.enabled &&
2208 provider.installed,
2209 )
2210 )
2211 continue;
2212 yield* instance.refreshModels().pipe(
2213 Effect.mapError(
2214 (error) =>
2215 new ProviderSetupError({
2216 instanceId: instance.instanceId,
2217 operation: "refresh-models",
2218 detail: error.detail,
2219 }),
2220 ),
23672221 );
2222 providers = yield* providerRegistry.refreshInstance(instance.instanceId);
23682223 }
2369 const runtime = Option.getOrNull(yield* providerSessionsV2.get(providerSessionId));
2370 if (runtime === null) {
2371 return yield* Effect.fail(
2372 new ProviderUploadFeedbackError({
2224 }
2225 return { providers };
2226 }),
2227 [WS_METHODS.providerUploadFeedback]: (input) =>
2228 Effect.gen(function* () {
2229 const projection = yield* threadManagement.getThreadRecords(input.threadId, [
2230 "providerThreads",
2231 ]);
2232 const providerThread =
2233 projection.providerThreads.find(
2234 (candidate) => candidate.id === projection.thread.activeProviderThreadId,
2235 ) ?? projection.providerThreads.at(-1);
2236 const providerSessionId = providerThread?.providerSessionId ?? null;
2237 if (providerThread === undefined || providerSessionId === null) {
2238 return yield* Effect.fail(
2239 new ProviderUploadFeedbackError({
2240 threadId: input.threadId,
2241 cause: "No provider session has run in this thread yet.",
2242 }),
2243 );
2244 }
2245 const runtime = Option.getOrNull(yield* providerSessionsV2.get(providerSessionId));
2246 if (runtime === null) {
2247 return yield* Effect.fail(
2248 new ProviderUploadFeedbackError({
2249 threadId: input.threadId,
2250 cause: "The provider session is no longer running. Send a message first.",
2251 }),
2252 );
2253 }
2254 if (runtime.uploadFeedback === undefined) {
2255 return yield* Effect.fail(
2256 new ProviderUploadFeedbackError({
2257 threadId: input.threadId,
2258 cause: `Provider '${runtime.driver}' does not support feedback uploads.`,
2259 }),
2260 );
2261 }
2262 return yield* runtime.uploadFeedback({
2263 providerThread,
2264 ...(input.reason === undefined ? {} : { reason: input.reason }),
2265 });
2266 }).pipe(
2267 Effect.mapError((cause) =>
2268 isProviderUploadFeedbackError(cause)
2269 ? cause
2270 : new ProviderUploadFeedbackError({
23732271 threadId: input.threadId,
2374 cause: "The provider session is no longer running. Send a message first.",
2272 cause,
23752273 }),
2376 );
2377 }
2378 if (runtime.uploadFeedback === undefined) {
2379 return yield* Effect.fail(
2380 new ProviderUploadFeedbackError({
2381 threadId: input.threadId,
2382 cause: `Provider '${runtime.driver}' does not support feedback uploads.`,
2383 }),
2384 );
2385 }
2386 return yield* runtime.uploadFeedback({
2387 providerThread,
2388 ...(input.reason === undefined ? {} : { reason: input.reason }),
2389 });
2390 }).pipe(
2391 Effect.mapError((cause) =>
2392 isProviderUploadFeedbackError(cause)
2393 ? cause
2394 : new ProviderUploadFeedbackError({
2395 threadId: input.threadId,
2396 cause,
2397 }),
2398 ),
23992274 ),
2400 { "rpc.aggregate": "provider" },
24012275 ),
24022276 [WS_METHODS.serverUpdateProvider]: (input) =>
2403 observeRpcEffect(
2404 WS_METHODS.serverUpdateProvider,
2405 providerMaintenanceRunner.updateProvider(input),
2406 {
2407 "rpc.aggregate": "server",
2408 },
2409 ),
2277 providerMaintenanceRunner.updateProvider(input),
24102278 [WS_METHODS.providerConsumeResetCredit]: (input) =>
2411 observeRpcEffect(
2412 WS_METHODS.providerConsumeResetCredit,
2413 Effect.gen(function* () {
2414 if ("sourceId" in input) return yield* usageLimitSources.consumeResetCredit(input);
2415 const instance = yield* providerInstances.getInstance(input.instanceId);
2416 // A disabled instance must not spend anything on its account.
2417 if (instance === undefined || !instance.enabled) {
2418 return yield* new ProviderSetupError({
2419 instanceId: input.instanceId,
2420 operation: "consume-reset-credit",
2421 detail: instance ? "This provider is disabled." : "Provider instance not found.",
2422 });
2423 }
2424 if (instance.consumeResetCredit === undefined) {
2425 return yield* new ProviderSetupError({
2426 instanceId: input.instanceId,
2427 operation: "consume-reset-credit",
2428 detail: "This provider does not bank reset credits.",
2429 });
2430 }
2431 const outcome = yield* instance.consumeResetCredit().pipe(
2432 Effect.mapError(
2433 (error) =>
2434 new ProviderSetupError({
2435 instanceId: input.instanceId,
2436 operation: "consume-reset-credit",
2437 detail: error.detail,
2438 cause: error,
2439 }),
2440 ),
2441 );
2442 return { outcome };
2443 }),
2444 { "rpc.aggregate": "provider" },
2445 ),
2446 [WS_METHODS.providerAuthStart]: (input) =>
2447 observeRpcEffect(
2448 WS_METHODS.providerAuthStart,
2449 providerAuth.start(input, currentSessionId),
2450 { "rpc.aggregate": "provider" },
2451 ),
2279 Effect.gen(function* () {
2280 if ("sourceId" in input) return yield* usageLimitSources.consumeResetCredit(input);
2281 const instance = yield* providerInstances.getInstance(input.instanceId);
2282 // A disabled instance must not spend anything on its account.
2283 if (instance === undefined || !instance.enabled) {
2284 return yield* new ProviderSetupError({
2285 instanceId: input.instanceId,
2286 operation: "consume-reset-credit",
2287 detail: instance ? "This provider is disabled." : "Provider instance not found.",
2288 });
2289 }
2290 if (instance.consumeResetCredit === undefined) {
2291 return yield* new ProviderSetupError({
2292 instanceId: input.instanceId,
2293 operation: "consume-reset-credit",
2294 detail: "This provider does not bank reset credits.",
2295 });
2296 }
2297 const outcome = yield* instance.consumeResetCredit().pipe(
2298 Effect.mapError(
2299 (error) =>
2300 new ProviderSetupError({
2301 instanceId: input.instanceId,
2302 operation: "consume-reset-credit",
2303 detail: error.detail,
2304 cause: error,
2305 }),
2306 ),
2307 );
2308 return { outcome };
2309 }),
2310 [WS_METHODS.providerAuthStart]: (input) => providerAuth.start(input, currentSessionId),
24522311 [WS_METHODS.providerAuthRespond]: (input) =>
2453 observeRpcEffect(
2454 WS_METHODS.providerAuthRespond,
2455 providerAuth.respond(input, currentSessionId),
2456 {
2457 "rpc.aggregate": "provider",
2458 instanceId: input.instanceId,
2459 },
2312 Effect.annotateCurrentSpan({ instanceId: input.instanceId }).pipe(
2313 Effect.andThen(providerAuth.respond(input, currentSessionId)),
24602314 ),
24612315 [WS_METHODS.providerAuthComplete]: (input) =>
2462 observeRpcEffect(
2463 WS_METHODS.providerAuthComplete,
2464 providerAuth.complete(input, currentSessionId),
2465 { "rpc.aggregate": "provider" },
2466 ),
2316 providerAuth.complete(input, currentSessionId),
24672317 [WS_METHODS.chatGptReconnectProfile]: (input) => providerAuth.reconnectProfile(input),
24682318 [WS_METHODS.chatGptImportProfile]: (input) => providerAuth.importProfile(input),
24692319 [WS_METHODS.chatGptHandoffSubscribe]: (input) =>
24702320 subscribeChatGptHandoff(input, currentSessionId),
2471 [WS_METHODS.codexAuthCallbackSubscribe]: (input) =>
2472 observeRpcStream(
2473 WS_METHODS.codexAuthCallbackSubscribe,
2474 subscribeCodexAuthCallback(input),
2475 {
2476 "rpc.aggregate": "provider",
2477 },
2478 ),
2479 [WS_METHODS.providerAuthCancel]: (input) =>
2480 observeRpcEffect(
2481 WS_METHODS.providerAuthCancel,
2482 providerAuth.cancel(input, currentSessionId),
2483 { "rpc.aggregate": "provider" },
2484 ),
2485 [WS_METHODS.providerAuthLogout]: (input) =>
2486 observeRpcEffect(WS_METHODS.providerAuthLogout, providerAuth.logout(input), {
2487 "rpc.aggregate": "provider",
2488 }),
2321 [WS_METHODS.codexAuthCallbackSubscribe]: (input) => subscribeCodexAuthCallback(input),
2322 [WS_METHODS.providerAuthCancel]: (input) => providerAuth.cancel(input, currentSessionId),
2323 [WS_METHODS.providerAuthLogout]: (input) => providerAuth.logout(input),
24892324 [WS_METHODS.providerAuthSubscribe]: (input) =>
2490 observeRpcStream(
2491 WS_METHODS.providerAuthSubscribe,
2492 providerAuth.subscribe(input, currentSessionId),
2493 { "rpc.aggregate": "provider" },
2494 ),
2495 [WS_METHODS.providerInstallStart]: (input) =>
2496 observeRpcEffect(WS_METHODS.providerInstallStart, providerInstallation.start(input), {
2497 "rpc.aggregate": "provider",
2498 }),
2499 [WS_METHODS.providerInstallCancel]: (input) =>
2500 observeRpcEffect(WS_METHODS.providerInstallCancel, providerInstallation.cancel(input), {
2501 "rpc.aggregate": "provider",
2502 }),
2503 [WS_METHODS.providerInstallSubscribe]: (input) =>
2504 observeRpcStream(
2505 WS_METHODS.providerInstallSubscribe,
2506 providerInstallation.subscribe(input),
2507 { "rpc.aggregate": "provider" },
2508 ),
2509 [WS_METHODS.providerInstallRemove]: (input) =>
2510 observeRpcEffect(WS_METHODS.providerInstallRemove, providerInstallation.remove(input), {
2511 "rpc.aggregate": "provider",
2512 }),
2513 [WS_METHODS.serverUpdateServer]: (input) =>
2514 observeRpcEffect(WS_METHODS.serverUpdateServer, serverSelfUpdate.update(input), {
2515 "rpc.aggregate": "server",
2516 }),
2325 providerAuth.subscribe(input, currentSessionId),
2326 [WS_METHODS.providerInstallStart]: (input) => providerInstallation.start(input),
2327 [WS_METHODS.providerInstallCancel]: (input) => providerInstallation.cancel(input),
2328 [WS_METHODS.providerInstallSubscribe]: (input) => providerInstallation.subscribe(input),
2329 [WS_METHODS.providerInstallRemove]: (input) => providerInstallation.remove(input),
2330 [WS_METHODS.serverUpdateServer]: (input) => serverSelfUpdate.update(input),
25172331 [WS_METHODS.serverUpdateServerWithProgress]: (input) =>
2518 observeRpcStream(
2519 WS_METHODS.serverUpdateServerWithProgress,
2520 Stream.callback<ServerSelfUpdateProgressEvent, ServerSelfUpdateError>((queue) =>
2521 serverSelfUpdate
2522 .update(input, (stage) =>
2332 Stream.callback<ServerSelfUpdateProgressEvent, ServerSelfUpdateError>((queue) =>
2333 serverSelfUpdate
2334 .update(input, (stage) =>
2335 Queue.offer(queue, {
2336 type: "progress",
2337 stage,
2338 }).pipe(Effect.asVoid),
2339 )
2340 .pipe(
2341 Effect.flatMap((result) =>
25232342 Queue.offer(queue, {
2524 type: "progress",
2525 stage,
2526 }).pipe(Effect.asVoid),
2527 )
2528 .pipe(
2529 Effect.flatMap((result) =>
2530 Queue.offer(queue, {
2531 type: "complete",
2532 result,
2533 }),
2534 ),
2535 Effect.catchTags({
2536 ServerSelfUpdateError: (error) => Queue.fail(queue, error),
2343 type: "complete",
2344 result,
25372345 }),
2538 Effect.andThen(Queue.end(queue)),
2539 Effect.forkScoped,
25402346 ),
2541 ),
2542 { "rpc.aggregate": "server" },
2347 Effect.catchTags({
2348 ServerSelfUpdateError: (error) => Queue.fail(queue, error),
2349 }),
2350 Effect.andThen(Queue.end(queue)),
2351 Effect.forkScoped,
2352 ),
25432353 ),
25442354 [WS_METHODS.serverCommitDesktopUpdate]: (input) =>
2545 observeRpcEffect(
2546 WS_METHODS.serverCommitDesktopUpdate,
2547 serverSelfUpdate.commitDesktopUpdate(input.requestId),
2548 { "rpc.aggregate": "server" },
2549 ),
2355 serverSelfUpdate.commitDesktopUpdate(input.requestId),
25502356 [WS_METHODS.serverUpsertKeybinding]: (rule) =>
2551 observeRpcEffect(
2552 WS_METHODS.serverUpsertKeybinding,
2553 Effect.gen(function* () {
2554 const keybindingsConfig = yield* keybindings.upsertKeybindingRule(rule);
2555 return { keybindings: keybindingsConfig, issues: [] };
2556 }),
2557 { "rpc.aggregate": "server" },
2558 ),
2357 Effect.gen(function* () {
2358 const keybindingsConfig = yield* keybindings.upsertKeybindingRule(rule);
2359 return { keybindings: keybindingsConfig, issues: [] };
2360 }),
25592361 [WS_METHODS.serverRemoveKeybinding]: (rule) =>
2560 observeRpcEffect(
2561 WS_METHODS.serverRemoveKeybinding,
2562 Effect.gen(function* () {
2563 const keybindingsConfig = yield* keybindings.removeKeybindingRule(rule);
2564 return { keybindings: keybindingsConfig, issues: [] };
2565 }),
2566 { "rpc.aggregate": "server" },
2567 ),
2362 Effect.gen(function* () {
2363 const keybindingsConfig = yield* keybindings.removeKeybindingRule(rule);
2364 return { keybindings: keybindingsConfig, issues: [] };
2365 }),
25682366 [WS_METHODS.serverGetSettings]: (_input) =>
2569 observeRpcEffect(
2570 WS_METHODS.serverGetSettings,
2571 serverSettings.getSettings.pipe(
2572 Effect.map(ServerSettings.redactServerSettingsForClient),
2573 ),
2574 {
2575 "rpc.aggregate": "server",
2576 },
2577 ),
2367 serverSettings.getSettings.pipe(Effect.map(ServerSettings.redactServerSettingsForClient)),
25782368 [WS_METHODS.serverUpdateSettings]: ({ patch, providerInstanceMutation }) =>
2579 observeRpcEffect(
2580 WS_METHODS.serverUpdateSettings,
2581 Effect.gen(function* () {
2582 const deviceHosts = patch.deviceHosts
2583 ? yield* remoteSshDeviceHosts(patch.deviceHosts).pipe(
2584 Effect.provide(deviceHostContext),
2585 )
2586 : undefined;
2587 const nextPatch = { ...patch, ...(deviceHosts ? { deviceHosts } : {}) };
2588 const settings = yield* providerInstanceMutation === undefined
2589 ? serverSettings.updateSettings(nextPatch)
2590 : serverSettings.updateProviderInstance(providerInstanceMutation, nextPatch);
2591 return ServerSettings.redactServerSettingsForClient(settings);
2592 }),
2593 {
2594 "rpc.aggregate": "server",
2595 },
2596 ),
2597 [WS_METHODS.serverDiscoverSourceControl]: (_input) =>
2598 observeRpcEffect(
2599 WS_METHODS.serverDiscoverSourceControl,
2600 sourceControlDiscovery.discover,
2601 {
2602 "rpc.aggregate": "server",
2603 },
2604 ),
2605 [WS_METHODS.serverGetTraceDiagnostics]: (_input) =>
2606 observeRpcEffect(
2607 WS_METHODS.serverGetTraceDiagnostics,
2608 TraceDiagnostics.readTraceDiagnostics({
2609 traceFilePath: config.serverTracePath,
2610 maxFiles: config.traceMaxFiles,
2611 }),
2612 {
2613 "rpc.aggregate": "server",
2614 },
2615 ),
2616 [WS_METHODS.serverGetProcessDiagnostics]: (_input) =>
2617 observeRpcEffect(WS_METHODS.serverGetProcessDiagnostics, processDiagnostics.read, {
2618 "rpc.aggregate": "server",
2369 Effect.gen(function* () {
2370 const deviceHosts = patch.deviceHosts
2371 ? yield* remoteSshDeviceHosts(patch.deviceHosts).pipe(
2372 Effect.provide(deviceHostContext),
2373 )
2374 : undefined;
2375 const nextPatch = { ...patch, ...(deviceHosts ? { deviceHosts } : {}) };
2376 const settings = yield* providerInstanceMutation === undefined
2377 ? serverSettings.updateSettings(nextPatch)
2378 : serverSettings.updateProviderInstance(providerInstanceMutation, nextPatch);
2379 return ServerSettings.redactServerSettingsForClient(settings);
26192380 }),
2620 [WS_METHODS.serverGetHostResources]: (_input) =>
2621 observeRpcEffect(WS_METHODS.serverGetHostResources, hostResources.read, {
2622 "rpc.aggregate": "server",
2381 [WS_METHODS.serverDiscoverSourceControl]: (_input) => sourceControlDiscovery.discover,
2382 [WS_METHODS.serverGetTraceDiagnostics]: (_input) =>
2383 TraceDiagnostics.readTraceDiagnostics({
2384 traceFilePath: config.serverTracePath,
2385 maxFiles: config.traceMaxFiles,
26232386 }),
2387 [WS_METHODS.serverGetProcessDiagnostics]: (_input) => processDiagnostics.read,
2388 [WS_METHODS.serverGetHostResources]: (_input) => hostResources.read,
26242389 [WS_METHODS.serverGetProcessResourceHistory]: (input) =>
2625 observeRpcEffect(
2626 WS_METHODS.serverGetProcessResourceHistory,
2627 processResourceMonitor.readHistory(input),
2628 {
2629 "rpc.aggregate": "server",
2630 },
2631 ),
2390 processResourceMonitor.readHistory(input),
26322391 [WS_METHODS.serverGetResourceTelemetryHistory]: (input) =>
2633 observeRpcEffect(
2634 WS_METHODS.serverGetResourceTelemetryHistory,
2635 resourceTelemetry.readHistory(input),
2636 {
2637 "rpc.aggregate": "server",
2638 },
2639 ),
2640 [WS_METHODS.serverGetUsageSummary]: (input) =>
2641 observeRpcEffect(WS_METHODS.serverGetUsageSummary, usage.readSummary(input), {
2642 "rpc.aggregate": "server",
2643 }),
2644 [WS_METHODS.serverRefreshUsageRates]: (_input) =>
2645 observeRpcEffect(WS_METHODS.serverRefreshUsageRates, usage.refreshRates, {
2646 "rpc.aggregate": "server",
2647 }),
2648 [WS_METHODS.serverRetryResourceTelemetry]: (_input) =>
2649 observeRpcEffect(WS_METHODS.serverRetryResourceTelemetry, resourceTelemetry.retry, {
2650 "rpc.aggregate": "server",
2651 }),
2652 [WS_METHODS.serverSignalProcess]: (input) =>
2653 observeRpcEffect(WS_METHODS.serverSignalProcess, processDiagnostics.signal(input), {
2654 "rpc.aggregate": "server",
2655 }),
2392 resourceTelemetry.readHistory(input),
2393 [WS_METHODS.serverGetUsageSummary]: (input) => usage.readSummary(input),
2394 [WS_METHODS.serverRefreshUsageRates]: (_input) => usage.refreshRates,
2395 [WS_METHODS.serverRetryResourceTelemetry]: (_input) => resourceTelemetry.retry,
2396 [WS_METHODS.serverSignalProcess]: (input) => processDiagnostics.signal(input),
26562397 [WS_METHODS.serverReportClientActivity]: (input, metadata) =>
26572398 Ref.update(rpcClientIds, (clientIds) => {
26582399 const next = new Set(clientIds);
......@@ -2660,583 +2401,303 @@ const layerWsRpc = (
26602401 return next;
26612402 }).pipe(
26622403 Effect.andThen(
2663 observeRpcEffect(
2664 WS_METHODS.serverReportClientActivity,
2665 backgroundPolicy.reportClientActivity(
2666 currentSessionId,
2667 RpcClientId.make(metadata.client.id),
2668 input,
2669 ),
2670 { "rpc.aggregate": "server" },
2404 backgroundPolicy.reportClientActivity(
2405 currentSessionId,
2406 RpcClientId.make(metadata.client.id),
2407 input,
26712408 ),
26722409 ),
26732410 ),
26742411 [WS_METHODS.serverReportHostPowerState]: (input) =>
2675 observeRpcEffect(
2676 WS_METHODS.serverReportHostPowerState,
2677 backgroundPolicy.reportHostPowerState(input),
2678 { "rpc.aggregate": "server" },
2679 ),
2680 [WS_METHODS.serverGetBackgroundPolicy]: (_input) =>
2681 observeRpcEffect(WS_METHODS.serverGetBackgroundPolicy, backgroundPolicy.snapshot, {
2682 "rpc.aggregate": "server",
2683 }),
2684 [WS_METHODS.cloudGetRelayClientStatus]: (_input) =>
2685 observeRpcEffect(WS_METHODS.cloudGetRelayClientStatus, relayClient.resolve, {
2686 "rpc.aggregate": "cloud",
2687 }),
2412 backgroundPolicy.reportHostPowerState(input),
2413 [WS_METHODS.serverGetBackgroundPolicy]: (_input) => backgroundPolicy.snapshot,
2414 [WS_METHODS.cloudGetRelayClientStatus]: (_input) => relayClient.resolve,
26882415 [WS_METHODS.cloudInstallRelayClient]: (_input) =>
2689 observeRpcStream(
2690 WS_METHODS.cloudInstallRelayClient,
2691 Stream.callback<RelayClientInstallProgressEvent, RelayClientInstallFailedError>(
2692 (queue) =>
2693 relayClient
2694 .installWithProgress((event) => Queue.offer(queue, event).pipe(Effect.asVoid))
2695 .pipe(
2696 Effect.flatMap((status) =>
2697 Queue.offer(queue, {
2698 type: "complete",
2699 status,
2416 Stream.callback<RelayClientInstallProgressEvent, RelayClientInstallFailedError>((queue) =>
2417 relayClient
2418 .installWithProgress((event) => Queue.offer(queue, event).pipe(Effect.asVoid))
2419 .pipe(
2420 Effect.flatMap((status) =>
2421 Queue.offer(queue, {
2422 type: "complete",
2423 status,
2424 }),
2425 ),
2426 Effect.catchTags({
2427 RelayClientInstallError: (error) =>
2428 Queue.fail(
2429 queue,
2430 new RelayClientInstallFailedError({
2431 reason: error.reason,
2432 message: error.message,
27002433 }),
27012434 ),
2702 Effect.catchTags({
2703 RelayClientInstallError: (error) =>
2704 Queue.fail(
2705 queue,
2706 new RelayClientInstallFailedError({
2707 reason: error.reason,
2708 message: error.message,
2709 }),
2710 ),
2711 }),
2712 Effect.andThen(Queue.end(queue)),
2713 Effect.forkScoped,
2714 ),
2715 ),
2716 { "rpc.aggregate": "cloud" },
2717 ),
2718 [WS_METHODS.pullRequestsList]: (input) =>
2719 observeRpcEffect(WS_METHODS.pullRequestsList, pullRequests.list(input), {
2720 "rpc.aggregate": "pull-requests",
2721 }),
2722 [WS_METHODS.pullRequestsListStats]: (input) =>
2723 observeRpcEffect(WS_METHODS.pullRequestsListStats, pullRequests.listStats(input), {
2724 "rpc.aggregate": "pull-requests",
2725 }),
2726 [WS_METHODS.pullRequestsRoutingIdentity]: (input) =>
2727 observeRpcEffect(
2728 WS_METHODS.pullRequestsRoutingIdentity,
2729 pullRequests.routingIdentity(input),
2730 {
2731 "rpc.aggregate": "pull-requests",
2732 },
2435 }),
2436 Effect.andThen(Queue.end(queue)),
2437 Effect.forkScoped,
2438 ),
27332439 ),
2734 [WS_METHODS.pullRequestsRouting]: (input) =>
2735 observeRpcEffect(WS_METHODS.pullRequestsRouting, pullRequests.routing(input), {
2736 "rpc.aggregate": "pull-requests",
2737 }),
2440 [WS_METHODS.pullRequestsList]: (input) => pullRequests.list(input),
2441 [WS_METHODS.pullRequestsListStats]: (input) => pullRequests.listStats(input),
2442 [WS_METHODS.pullRequestsRoutingIdentity]: (input) => pullRequests.routingIdentity(input),
2443 [WS_METHODS.pullRequestsRouting]: (input) => pullRequests.routing(input),
27382444 [WS_METHODS.pullRequestsSummary]: (input) =>
2739 observeRpcEffect(
2740 WS_METHODS.pullRequestsSummary,
2741 withPullRequestViewer(input, pullRequests.summary(input)),
2742 {
2743 "rpc.aggregate": "pull-requests",
2744 },
2745 ),
2445 withPullRequestViewer(input, pullRequests.summary(input)),
27462446 [WS_METHODS.pullRequestsStack]: (input) =>
2747 observeRpcEffect(
2748 WS_METHODS.pullRequestsStack,
2749 withPullRequestViewer(input, pullRequests.stack(input)),
2750 {
2751 "rpc.aggregate": "pull-requests",
2752 },
2753 ),
2447 withPullRequestViewer(input, pullRequests.stack(input)),
27542448 [WS_METHODS.pullRequestsLinkedThreads]: (input) =>
2755 observeRpcEffect(
2756 WS_METHODS.pullRequestsLinkedThreads,
2757 resolvePullRequestSyncKey(input).pipe(
2758 Effect.flatMap((key) =>
2759 key === null
2760 ? Effect.succeed({ threads: [] })
2761 : listLinkedPullRequestThreads(key).pipe(
2762 Effect.provideService(SqlClient.SqlClient, sql),
2763 ),
2764 ),
2449 resolvePullRequestSyncKey(input).pipe(
2450 Effect.flatMap((key) =>
2451 key === null
2452 ? Effect.succeed({ threads: [] })
2453 : listLinkedPullRequestThreads(key).pipe(
2454 Effect.provideService(SqlClient.SqlClient, sql),
2455 ),
27652456 ),
2766 { "rpc.aggregate": "pull-requests" },
27672457 ),
27682458 [WS_METHODS.pullRequestsDetail]: (input) =>
2769 observeRpcEffect(
2770 WS_METHODS.pullRequestsDetail,
2771 withPullRequestViewer(input, pullRequests.detail(input)),
2772 {
2773 "rpc.aggregate": "pull-requests",
2774 },
2775 ),
2459 withPullRequestViewer(input, pullRequests.detail(input)),
27762460 [WS_METHODS.pullRequestsPreview]: (input) =>
2777 observeRpcEffect(
2778 WS_METHODS.pullRequestsPreview,
2779 withPullRequestViewer(input, pullRequests.preview(input)),
2780 { "rpc.aggregate": "pull-requests" },
2781 ),
2461 withPullRequestViewer(input, pullRequests.preview(input)),
27822462 [WS_METHODS.pullRequestsChecks]: (input) =>
2783 observeRpcEffect(
2784 WS_METHODS.pullRequestsChecks,
2785 withPullRequestViewer(input, pullRequests.checks(input)),
2786 { "rpc.aggregate": "pull-requests" },
2787 ),
2463 withPullRequestViewer(input, pullRequests.checks(input)),
27882464 [WS_METHODS.pullRequestsActivity]: (input) =>
2789 observeRpcEffect(
2790 WS_METHODS.pullRequestsActivity,
2791 withPullRequestViewer(input, pullRequests.activity(input)),
2792 {
2793 "rpc.aggregate": "pull-requests",
2794 },
2795 ),
2465 withPullRequestViewer(input, pullRequests.activity(input)),
27962466 [WS_METHODS.pullRequestsThreadComments]: (input) =>
2797 observeRpcEffect(
2798 WS_METHODS.pullRequestsThreadComments,
2799 withPullRequestViewer(input, pullRequests.threadComments(input)),
2800 {
2801 "rpc.aggregate": "pull-requests",
2802 },
2803 ),
2467 withPullRequestViewer(input, pullRequests.threadComments(input)),
28042468 [WS_METHODS.pullRequestsDiffFileContents]: (input) =>
2805 observeRpcEffect(
2806 WS_METHODS.pullRequestsDiffFileContents,
2807 withPullRequestViewer(input, pullRequests.diffFileContents(input)),
2808 { "rpc.aggregate": "pull-requests" },
2809 ),
2469 withPullRequestViewer(input, pullRequests.diffFileContents(input)),
28102470 [WS_METHODS.pullRequestsFilesViewed]: (input) =>
2811 observeRpcEffect(
2812 WS_METHODS.pullRequestsFilesViewed,
2813 withPullRequestViewer(input, pullRequests.filesViewed(input)),
2814 { "rpc.aggregate": "pull-requests" },
2815 ),
2471 withPullRequestViewer(input, pullRequests.filesViewed(input)),
28162472 [WS_METHODS.pullRequestsSetFilesViewed]: (input) =>
2817 observeRpcEffect(
2818 WS_METHODS.pullRequestsSetFilesViewed,
2819 withPullRequestViewer(input, pullRequests.setFilesViewed(input)),
2820 { "rpc.aggregate": "pull-requests" },
2821 ),
2473 withPullRequestViewer(input, pullRequests.setFilesViewed(input)),
28222474 [WS_METHODS.pullRequestsRunAction]: (input) =>
2823 observeRpcEffect(
2824 WS_METHODS.pullRequestsRunAction,
2825 withPullRequestViewer(input, pullRequests.runAction(input)).pipe(
2826 Effect.tap(() =>
2827 resolvePullRequestSyncKey(input).pipe(
2828 Effect.flatMap((key) =>
2829 key === null ? Effect.void : pullRequestSync.requestSync(key),
2830 ),
2475 withPullRequestViewer(input, pullRequests.runAction(input)).pipe(
2476 Effect.tap(() =>
2477 resolvePullRequestSyncKey(input).pipe(
2478 Effect.flatMap((key) =>
2479 key === null ? Effect.void : pullRequestSync.requestSync(key),
28312480 ),
28322481 ),
28332482 ),
2834 { "rpc.aggregate": "pull-requests" },
28352483 ),
28362484 [WS_METHODS.pullRequestsUpdate]: (input) =>
2837 observeRpcEffect(
2838 WS_METHODS.pullRequestsUpdate,
2839 withPullRequestViewer(input, pullRequests.update(input)),
2840 {
2841 "rpc.aggregate": "pull-requests",
2842 },
2843 ),
2485 withPullRequestViewer(input, pullRequests.update(input)),
28442486 [WS_METHODS.pullRequestsComment]: (input) =>
2845 observeRpcEffect(
2846 WS_METHODS.pullRequestsComment,
2847 withPullRequestViewer(input, pullRequests.comment(input)),
2848 {
2849 "rpc.aggregate": "pull-requests",
2850 },
2851 ),
2487 withPullRequestViewer(input, pullRequests.comment(input)),
28522488 [WS_METHODS.pullRequestsUpdateComment]: (input) =>
2853 observeRpcEffect(
2854 WS_METHODS.pullRequestsUpdateComment,
2855 withPullRequestViewer(input, pullRequests.updateComment(input)),
2856 {
2857 "rpc.aggregate": "pull-requests",
2858 },
2859 ),
2489 withPullRequestViewer(input, pullRequests.updateComment(input)),
28602490 [WS_METHODS.pullRequestsSubmitReview]: (input) =>
2861 observeRpcEffect(
2862 WS_METHODS.pullRequestsSubmitReview,
2863 withPullRequestViewer(input, pullRequests.submitReview(input)),
2864 {
2865 "rpc.aggregate": "pull-requests",
2866 },
2867 ),
2491 withPullRequestViewer(input, pullRequests.submitReview(input)),
28682492 [WS_METHODS.pullRequestsReplyToThread]: (input) =>
2869 observeRpcEffect(
2870 WS_METHODS.pullRequestsReplyToThread,
2871 withPullRequestViewer(input, pullRequests.replyToThread(input)),
2872 { "rpc.aggregate": "pull-requests" },
2873 ),
2493 withPullRequestViewer(input, pullRequests.replyToThread(input)),
28742494 [WS_METHODS.pullRequestsSetThreadResolution]: (input) =>
2875 observeRpcEffect(
2876 WS_METHODS.pullRequestsSetThreadResolution,
2877 withPullRequestViewer(input, pullRequests.setThreadResolution(input)),
2878 { "rpc.aggregate": "pull-requests" },
2879 ),
2495 withPullRequestViewer(input, pullRequests.setThreadResolution(input)),
28802496 [WS_METHODS.pullRequestsSetReaction]: (input) =>
2881 observeRpcEffect(
2882 WS_METHODS.pullRequestsSetReaction,
2883 withPullRequestViewer(input, pullRequests.setReaction(input)),
2884 {
2885 "rpc.aggregate": "pull-requests",
2886 },
2887 ),
2497 withPullRequestViewer(input, pullRequests.setReaction(input)),
28882498 [WS_METHODS.pullRequestsInvalidate]: (input) =>
2889 observeRpcEffect(
2890 WS_METHODS.pullRequestsInvalidate,
2891 pullRequests.invalidate(input, { notifyReaders: true }).pipe(
2892 // A reader asking for fresh host state also wants the thread badges it feeds to
2893 // catch up, including a merged link the sweep would otherwise never revisit.
2894 Effect.andThen(
2895 input.reference === undefined || input.filesViewedOnly === true
2896 ? Effect.void
2897 : resolvePullRequestSyncKey(input.reference).pipe(
2898 Effect.flatMap((key) =>
2899 key === null ? Effect.void : pullRequestSync.requestSync(key),
2900 ),
2499 pullRequests.invalidate(input, { notifyReaders: true }).pipe(
2500 // A reader asking for fresh host state also wants the thread badges it feeds to
2501 // catch up, including a merged link the sweep would otherwise never revisit.
2502 Effect.andThen(
2503 input.reference === undefined || input.filesViewedOnly === true
2504 ? Effect.void
2505 : resolvePullRequestSyncKey(input.reference).pipe(
2506 Effect.flatMap((key) =>
2507 key === null ? Effect.void : pullRequestSync.requestSync(key),
29012508 ),
2902 ),
2509 ),
29032510 ),
2904 { "rpc.aggregate": "pull-requests" },
2905 ),
2906 [WS_METHODS.pullRequestsSubscribeRefreshes]: () =>
2907 observeRpcStream(
2908 WS_METHODS.pullRequestsSubscribeRefreshes,
2909 pullRequests.subscribeRefreshes,
2910 { "rpc.aggregate": "pull-requests" },
29112511 ),
2512 [WS_METHODS.pullRequestsSubscribeRefreshes]: () => pullRequests.subscribeRefreshes,
29122513 [WS_METHODS.pullRequestsReviewerCandidates]: (input) =>
2913 observeRpcEffect(
2914 WS_METHODS.pullRequestsReviewerCandidates,
2915 withPullRequestViewer(input, pullRequests.reviewerCandidates(input)),
2916 { "rpc.aggregate": "pull-requests" },
2917 ),
2514 withPullRequestViewer(input, pullRequests.reviewerCandidates(input)),
29182515 [WS_METHODS.pullRequestsRequestReviewers]: (input) =>
2919 observeRpcEffect(
2920 WS_METHODS.pullRequestsRequestReviewers,
2921 withPullRequestViewer(input, pullRequests.requestReviewers(input)),
2922 { "rpc.aggregate": "pull-requests" },
2923 ),
2516 withPullRequestViewer(input, pullRequests.requestReviewers(input)),
29242517 [WS_METHODS.pullRequestsLabelCandidates]: (input) =>
2925 observeRpcEffect(
2926 WS_METHODS.pullRequestsLabelCandidates,
2927 withPullRequestViewer(input, pullRequests.labelCandidates(input)),
2928 { "rpc.aggregate": "pull-requests" },
2929 ),
2518 withPullRequestViewer(input, pullRequests.labelCandidates(input)),
29302519 [WS_METHODS.pullRequestsSetLabels]: (input) =>
2931 observeRpcEffect(
2932 WS_METHODS.pullRequestsSetLabels,
2933 withPullRequestViewer(input, pullRequests.setLabels(input)),
2934 {
2935 "rpc.aggregate": "pull-requests",
2936 },
2937 ),
2520 withPullRequestViewer(input, pullRequests.setLabels(input)),
29382521 [WS_METHODS.sourceControlLookupRepository]: (input) =>
2939 observeRpcEffect(
2940 WS_METHODS.sourceControlLookupRepository,
2941 sourceControlRepositories.lookupRepository(input),
2942 {
2943 "rpc.aggregate": "source-control",
2944 },
2945 ),
2522 sourceControlRepositories.lookupRepository(input),
29462523 [WS_METHODS.sourceControlCloneRepository]: (input) =>
2947 observeRpcEffect(
2948 WS_METHODS.sourceControlCloneRepository,
2949 sourceControlRepositories.cloneRepository(input),
2950 {
2951 "rpc.aggregate": "source-control",
2952 },
2953 ),
2524 sourceControlRepositories.cloneRepository(input),
29542525 [WS_METHODS.projectCloneStart]: (input) =>
2955 observeRpcEffect(
2956 WS_METHODS.projectCloneStart,
2957 projectCloneTracker.start(input, {
2958 createProject: (project) =>
2959 projectService
2960 .create({
2961 commandId: CommandId.make(`project-clone-create:${project.projectId}`),
2962 projectId: project.projectId,
2963 title: project.title,
2964 workspaceRoot: project.workspaceRoot,
2965 createWorkspaceRootIfMissing: true,
2966 })
2967 .pipe(
2968 Effect.asVoid,
2969 Effect.mapError(
2970 (cause) =>
2971 new OrchestrationDispatchCommandError({
2972 message: "Failed to create clone project.",
2973 cause,
2974 }),
2975 ),
2976 ),
2977 onCloned: (project) =>
2978 repositoryIdentityResolver.resolve(project.workspaceRoot, { refresh: true }).pipe(
2979 Effect.andThen(
2980 projectService.update({
2981 commandId: CommandId.make(`project-clone-done:${project.projectId}`),
2982 projectId: project.projectId,
2983 }),
2526 projectCloneTracker.start(input, {
2527 createProject: (project) =>
2528 projectService
2529 .create({
2530 commandId: CommandId.make(`project-clone-create:${project.projectId}`),
2531 projectId: project.projectId,
2532 title: project.title,
2533 workspaceRoot: project.workspaceRoot,
2534 createWorkspaceRootIfMissing: true,
2535 })
2536 .pipe(
2537 Effect.asVoid,
2538 Effect.mapError(
2539 (cause) =>
2540 new OrchestrationDispatchCommandError({
2541 message: "Failed to create clone project.",
2542 cause,
2543 }),
29842544 ),
2985 Effect.andThen(refreshGitStatus(project.workspaceRoot)),
2986 Effect.ignoreCause({ log: true }),
29872545 ),
2988 }),
2989 { "rpc.aggregate": "source-control" },
2990 ),
2991 [WS_METHODS.projectsEnsureScratch]: () =>
2992 observeRpcEffect(
2993 WS_METHODS.projectsEnsureScratch,
2994 managedFolders.ensureScratchProject.pipe(
2995 Effect.mapError(
2996 (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }),
2546 onCloned: (project) =>
2547 repositoryIdentityResolver.resolve(project.workspaceRoot, { refresh: true }).pipe(
2548 Effect.andThen(
2549 projectService.update({
2550 commandId: CommandId.make(`project-clone-done:${project.projectId}`),
2551 projectId: project.projectId,
2552 }),
2553 ),
2554 Effect.andThen(refreshGitStatus(project.workspaceRoot)),
2555 Effect.ignoreCause({ log: true }),
29972556 ),
2557 }),
2558 [WS_METHODS.projectsEnsureScratch]: () =>
2559 managedFolders.ensureScratchProject.pipe(
2560 Effect.mapError(
2561 (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }),
29982562 ),
2999 { "rpc.aggregate": "orchestration" },
30002563 ),
30012564 [WS_METHODS.projectsCreateNew]: (input) =>
3002 observeRpcEffect(
3003 WS_METHODS.projectsCreateNew,
3004 managedFolders
3005 .createNamedProject(input)
3006 .pipe(
3007 Effect.mapError(
3008 (cause) =>
3009 new OrchestrationDispatchCommandError({ message: cause.message, cause }),
3010 ),
2565 managedFolders
2566 .createNamedProject(input)
2567 .pipe(
2568 Effect.mapError(
2569 (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }),
30112570 ),
3012 { "rpc.aggregate": "orchestration" },
3013 ),
2571 ),
30142572 [WS_METHODS.projectCloneCancel]: (input) =>
3015 observeRpcEffect(
3016 WS_METHODS.projectCloneCancel,
3017 projectCloneTracker
3018 .cancel(input.projectId)
3019 .pipe(Effect.map((applied) => ({ applied }))),
3020 { "rpc.aggregate": "source-control" },
3021 ),
2573 projectCloneTracker.cancel(input.projectId).pipe(Effect.map((applied) => ({ applied }))),
30222574 [WS_METHODS.projectCloneRetry]: (input) =>
3023 observeRpcEffect(
3024 WS_METHODS.projectCloneRetry,
3025 projectCloneTracker.retry(input.projectId).pipe(Effect.map((applied) => ({ applied }))),
3026 { "rpc.aggregate": "source-control" },
3027 ),
3028 [WS_METHODS.subscribeProjectClones]: () =>
3029 observeRpcStream(WS_METHODS.subscribeProjectClones, projectCloneTracker.stream, {
3030 "rpc.aggregate": "source-control",
3031 }),
2575 projectCloneTracker.retry(input.projectId).pipe(Effect.map((applied) => ({ applied }))),
2576 [WS_METHODS.subscribeProjectClones]: () => projectCloneTracker.stream,
30322577 [WS_METHODS.sourceControlPublishRepository]: (input) =>
3033 observeRpcEffect(
3034 WS_METHODS.sourceControlPublishRepository,
3035 sourceControlRepositories.publishRepository(input).pipe(
3036 // A new remote can change the cached identity. Only the `cwd` entry
3037 // refreshes, so after a publish from a linked worktree the project
3038 // root entry waits for its TTL.
3039 Effect.tap(() => repositoryIdentityResolver.resolve(input.cwd, { refresh: true })),
3040 Effect.tap(() => refreshGitStatus(input.cwd)),
3041 ),
3042 {
3043 "rpc.aggregate": "source-control",
3044 },
2578 sourceControlRepositories.publishRepository(input).pipe(
2579 // A new remote can change the cached identity. Only the `cwd` entry
2580 // refreshes, so after a publish from a linked worktree the project
2581 // root entry waits for its TTL.
2582 Effect.tap(() => repositoryIdentityResolver.resolve(input.cwd, { refresh: true })),
2583 Effect.tap(() => refreshGitStatus(input.cwd)),
30452584 ),
30462585 [WS_METHODS.projectsSearchEntries]: (input) =>
3047 observeRpcEffect(
3048 WS_METHODS.projectsSearchEntries,
3049 workspaceEntries.search(input).pipe(
3050 Effect.mapError(
3051 (cause) =>
3052 new ProjectSearchEntriesError({
3053 cwd: input.cwd,
3054 queryLength: input.query.length,
3055 limit: input.limit,
3056 ...projectEntriesFailureContext(cause),
3057 cause,
3058 }),
3059 ),
2586 workspaceEntries.search(input).pipe(
2587 Effect.mapError(
2588 (cause) =>
2589 new ProjectSearchEntriesError({
2590 cwd: input.cwd,
2591 queryLength: input.query.length,
2592 limit: input.limit,
2593 ...projectEntriesFailureContext(cause),
2594 cause,
2595 }),
30602596 ),
3061 { "rpc.aggregate": "workspace" },
30622597 ),
30632598 [WS_METHODS.projectsSearchContents]: (input) =>
3064 observeRpcEffect(
3065 WS_METHODS.projectsSearchContents,
3066 workspaceEntries.searchContents(input).pipe(
3067 Effect.mapError(
3068 (cause) =>
3069 new ProjectSearchContentsError({
3070 cwd: input.cwd,
3071 queryLength: input.query.length,
3072 limit: input.limit,
3073 ...projectEntriesFailureContext(cause),
3074 cause,
3075 }),
3076 ),
2599 workspaceEntries.searchContents(input).pipe(
2600 Effect.mapError(
2601 (cause) =>
2602 new ProjectSearchContentsError({
2603 cwd: input.cwd,
2604 queryLength: input.query.length,
2605 limit: input.limit,
2606 ...projectEntriesFailureContext(cause),
2607 cause,
2608 }),
30772609 ),
3078 { "rpc.aggregate": "workspace" },
30792610 ),
30802611 [WS_METHODS.projectsListEntries]: (input) =>
3081 observeRpcEffect(
3082 WS_METHODS.projectsListEntries,
3083 workspaceEntries.list(input).pipe(
3084 Effect.mapError(
3085 (cause) =>
3086 new ProjectListEntriesError({
3087 ...input,
3088 ...projectEntriesFailureContext(cause),
3089 cause,
3090 }),
3091 ),
2612 workspaceEntries.list(input).pipe(
2613 Effect.mapError(
2614 (cause) =>
2615 new ProjectListEntriesError({
2616 ...input,
2617 ...projectEntriesFailureContext(cause),
2618 cause,
2619 }),
30922620 ),
3093 { "rpc.aggregate": "workspace" },
30942621 ),
30952622 [WS_METHODS.projectsReadFile]: (input) =>
3096 observeRpcEffect(
3097 WS_METHODS.projectsReadFile,
3098 workspaceFileSystem.readFile(input).pipe(
3099 Effect.mapError(
3100 (cause) =>
3101 new ProjectReadFileError({
3102 ...input,
3103 ...projectFileFailureContext(cause),
3104 cause,
3105 }),
3106 ),
2623 workspaceFileSystem.readFile(input).pipe(
2624 Effect.mapError(
2625 (cause) =>
2626 new ProjectReadFileError({
2627 ...input,
2628 ...projectFileFailureContext(cause),
2629 cause,
2630 }),
31072631 ),
3108 { "rpc.aggregate": "workspace" },
31092632 ),
31102633 [WS_METHODS.projectsWriteFile]: (input) =>
3111 observeRpcEffect(
3112 WS_METHODS.projectsWriteFile,
3113 workspaceFileSystem.writeFile(input).pipe(
3114 Effect.mapError(
3115 (cause) =>
3116 new ProjectWriteFileError({
3117 cwd: input.cwd,
3118 relativePath: input.relativePath,
3119 ...projectFileFailureContext(cause),
3120 cause,
3121 }),
3122 ),
2634 workspaceFileSystem.writeFile(input).pipe(
2635 Effect.mapError(
2636 (cause) =>
2637 new ProjectWriteFileError({
2638 cwd: input.cwd,
2639 relativePath: input.relativePath,
2640 ...projectFileFailureContext(cause),
2641 cause,
2642 }),
31232643 ),
3124 { "rpc.aggregate": "workspace" },
31252644 ),
31262645 [WS_METHODS.projectsMutate]: (mutation) =>
3127 observeRpcEffect(
3128 WS_METHODS.projectsMutate,
3129 startup.enqueueCommand(mutateProject(mutation)).pipe(
3130 Effect.mapError(
3131 (cause) =>
3132 new ProjectMutationError({
3133 commandId: mutation.commandId,
3134 message:
3135 cause._tag === "ProjectNotEmptyError"
3136 ? cause.message
3137 : "Failed to mutate project.",
3138 cause,
3139 }),
3140 ),
2646 startup.enqueueCommand(mutateProject(mutation)).pipe(
2647 Effect.mapError(
2648 (cause) =>
2649 new ProjectMutationError({
2650 commandId: mutation.commandId,
2651 message:
2652 cause._tag === "ProjectNotEmptyError"
2653 ? cause.message
2654 : "Failed to mutate project.",
2655 cause,
2656 }),
31412657 ),
3142 { "rpc.aggregate": "orchestration" },
31432658 ),
3144 [WS_METHODS.shellOpenInEditor]: (input) =>
3145 observeRpcEffect(WS_METHODS.shellOpenInEditor, externalLauncher.launchEditor(input), {
3146 "rpc.aggregate": "workspace",
3147 }),
2659 [WS_METHODS.shellOpenInEditor]: (input) => externalLauncher.launchEditor(input),
31482660 [WS_METHODS.filesystemBrowse]: (input) =>
3149 observeRpcEffect(
3150 WS_METHODS.filesystemBrowse,
3151 workspaceEntries.browse(input).pipe(
3152 Effect.mapError(
3153 (cause) =>
3154 new FilesystemBrowseError({
3155 ...input,
3156 ...filesystemBrowseFailureContext(cause),
3157 cause,
3158 }),
3159 ),
2661 workspaceEntries.browse(input).pipe(
2662 Effect.mapError(
2663 (cause) =>
2664 new FilesystemBrowseError({
2665 ...input,
2666 ...filesystemBrowseFailureContext(cause),
2667 cause,
2668 }),
31602669 ),
3161 { "rpc.aggregate": "workspace" },
31622670 ),
3163 [WS_METHODS.attachmentsCreateUploadUrl]: (input) =>
3164 observeRpcEffect(WS_METHODS.attachmentsCreateUploadUrl, issueAttachmentUploadUrl(input), {
3165 "rpc.aggregate": "workspace",
3166 }),
3167 [WS_METHODS.attachmentsDelete]: (input) =>
3168 observeRpcEffect(
3169 WS_METHODS.attachmentsDelete,
3170 deletePendingAttachment(input.attachmentId),
3171 { "rpc.aggregate": "workspace" },
3172 ),
3173 [WS_METHODS.agentSessionsScan]: () =>
3174 observeRpcEffect(WS_METHODS.agentSessionsScan, agentSessionScanner.scan, {
3175 "rpc.aggregate": "workspace",
3176 }),
2671 [WS_METHODS.attachmentsCreateUploadUrl]: (input) => issueAttachmentUploadUrl(input),
2672 [WS_METHODS.attachmentsDelete]: (input) => deletePendingAttachment(input.attachmentId),
2673 [WS_METHODS.agentSessionsScan]: () => agentSessionScanner.scan,
31772674 [WS_METHODS.agentSessionsImport]: (input) =>
3178 observeRpcEffect(
3179 WS_METHODS.agentSessionsImport,
3180 agentSessionImporter.importRecentAgentThreads(input),
3181 { "rpc.aggregate": "workspace" },
3182 ),
2675 agentSessionImporter.importRecentAgentThreads(input),
31832676 [WS_METHODS.assetsCreateUrl]: (input) =>
3184 observeRpcEffect(
3185 WS_METHODS.assetsCreateUrl,
3186 Effect.gen(function* () {
3187 const path = yield* Path.Path;
3188 // An absolute media path can be linked from a thread on another environment.
3189 if (
3190 input.resource._tag === "attachment" ||
3191 input.resource._tag === "native-app-icon" ||
3192 input.resource._tag === "tool-output-image" ||
3193 // GitHub media names the repository it authenticates through itself.
3194 input.resource._tag === "github-media" ||
3195 (input.resource._tag === "media-file" && path.isAbsolute(input.resource.path))
3196 ) {
3197 return yield* issueAssetUrl({ resource: input.resource });
3198 }
3199 if (input.resource._tag === "draft-workspace-file") {
3200 // A project draft names its workspace directly; there is no
3201 // thread to resolve one from.
3202 return yield* issueAssetUrl({
3203 resource: input.resource,
3204 workspaceRoot: input.resource.cwd,
3205 });
3206 }
3207 if (input.resource._tag === "project-favicon") {
3208 const project = yield* projectStore
3209 .findActiveByWorkspaceRoot(input.resource.cwd)
3210 .pipe(
3211 Effect.mapError(
3212 (cause) =>
3213 new AssetWorkspaceContextResolutionError({
3214 resource: input.resource,
3215 cause,
3216 }),
3217 ),
3218 );
3219 if (Option.isNone(project)) {
3220 return yield* new AssetWorkspaceContextNotFoundError({
3221 resource: input.resource,
3222 });
3223 }
3224 // A cloned project exists before its files do. Clients ask again
3225 // when the clone lands (see createProjectFaviconUrlAtomFamily).
3226 const clone = yield* projectCloneTracker.get(project.value.projectId);
3227 return yield* issueAssetUrl({
3228 resource: input.resource,
3229 ...(project.value.faviconPath
3230 ? { projectFaviconPath: project.value.faviconPath }
3231 : {}),
3232 projectCheckoutPending:
3233 clone !== null &&
3234 clone.phase !== "done" &&
3235 clone.destinationPath === project.value.workspaceRoot,
3236 });
3237 }
3238 const thread = yield* threadManagement
3239 .getThreadRecords(input.resource.threadId, [])
2677 Effect.gen(function* () {
2678 const path = yield* Path.Path;
2679 // An absolute media path can be linked from a thread on another environment.
2680 if (
2681 input.resource._tag === "attachment" ||
2682 input.resource._tag === "native-app-icon" ||
2683 input.resource._tag === "tool-output-image" ||
2684 // GitHub media names the repository it authenticates through itself.
2685 input.resource._tag === "github-media" ||
2686 (input.resource._tag === "media-file" && path.isAbsolute(input.resource.path))
2687 ) {
2688 return yield* issueAssetUrl({ resource: input.resource });
2689 }
2690 if (input.resource._tag === "draft-workspace-file") {
2691 // A project draft names its workspace directly; there is no
2692 // thread to resolve one from.
2693 return yield* issueAssetUrl({
2694 resource: input.resource,
2695 workspaceRoot: input.resource.cwd,
2696 });
2697 }
2698 if (input.resource._tag === "project-favicon") {
2699 const project = yield* projectStore
2700 .findActiveByWorkspaceRoot(input.resource.cwd)
32402701 .pipe(
32412702 Effect.mapError(
32422703 (cause) =>
......@@ -3246,7 +2707,28 @@ const layerWsRpc = (
32462707 }),
32472708 ),
32482709 );
3249 const project = yield* projectService.getById(thread.thread.projectId).pipe(
2710 if (Option.isNone(project)) {
2711 return yield* new AssetWorkspaceContextNotFoundError({
2712 resource: input.resource,
2713 });
2714 }
2715 // A cloned project exists before its files do. Clients ask again
2716 // when the clone lands (see createProjectFaviconUrlAtomFamily).
2717 const clone = yield* projectCloneTracker.get(project.value.projectId);
2718 return yield* issueAssetUrl({
2719 resource: input.resource,
2720 ...(project.value.faviconPath
2721 ? { projectFaviconPath: project.value.faviconPath }
2722 : {}),
2723 projectCheckoutPending:
2724 clone !== null &&
2725 clone.phase !== "done" &&
2726 clone.destinationPath === project.value.workspaceRoot,
2727 });
2728 }
2729 const thread = yield* threadManagement
2730 .getThreadRecords(input.resource.threadId, [])
2731 .pipe(
32502732 Effect.mapError(
32512733 (cause) =>
32522734 new AssetWorkspaceContextResolutionError({
......@@ -3255,357 +2737,187 @@ const layerWsRpc = (
32552737 }),
32562738 ),
32572739 );
3258 if (Option.isNone(project)) {
3259 return yield* new AssetWorkspaceContextNotFoundError({
3260 resource: input.resource,
3261 });
3262 }
3263 return yield* issueAssetUrl({
2740 const project = yield* projectService.getById(thread.thread.projectId).pipe(
2741 Effect.mapError(
2742 (cause) =>
2743 new AssetWorkspaceContextResolutionError({
2744 resource: input.resource,
2745 cause,
2746 }),
2747 ),
2748 );
2749 if (Option.isNone(project)) {
2750 return yield* new AssetWorkspaceContextNotFoundError({
32642751 resource: input.resource,
3265 workspaceRoot: thread.thread.worktreePath ?? project.value.workspaceRoot,
32662752 });
3267 }),
3268 { "rpc.aggregate": "workspace" },
3269 ),
2753 }
2754 return yield* issueAssetUrl({
2755 resource: input.resource,
2756 workspaceRoot: thread.thread.worktreePath ?? project.value.workspaceRoot,
2757 });
2758 }),
32702759 [WS_METHODS.assetsPersistChatAttachments]: (input) =>
3271 observeRpcEffect(
3272 WS_METHODS.assetsPersistChatAttachments,
3273 persistChatAttachments(input).pipe(Effect.map((attachments) => ({ attachments }))),
3274 { "rpc.aggregate": "orchestration" },
3275 ),
2760 persistChatAttachments(input).pipe(Effect.map((attachments) => ({ attachments }))),
32762761 [WS_METHODS.subscribeVcsStatus]: (input) =>
3277 observeRpcStream(
3278 WS_METHODS.subscribeVcsStatus,
3279 vcsStatusBroadcaster.streamStatus(input, {
3280 automaticRemoteRefreshInterval: automaticGitFetchInterval,
3281 }),
3282 {
3283 "rpc.aggregate": "vcs",
3284 },
3285 ),
3286 [WS_METHODS.subscribeWorktreeSetup]: (input) =>
3287 observeRpcStream(
3288 WS_METHODS.subscribeWorktreeSetup,
3289 worktreeSetupTracker.stream(input.threadId),
3290 { "rpc.aggregate": "vcs" },
3291 ),
2762 vcsStatusBroadcaster.streamStatus(input, {
2763 automaticRemoteRefreshInterval: automaticGitFetchInterval,
2764 }),
2765 [WS_METHODS.subscribeWorktreeSetup]: (input) => worktreeSetupTracker.stream(input.threadId),
32922766 [WS_METHODS.worktreeSetupCancel]: (input) =>
3293 observeRpcEffect(
3294 WS_METHODS.worktreeSetupCancel,
3295 worktreeSetupTracker
3296 .cancel(input.threadId)
3297 .pipe(Effect.map((cancelled) => ({ cancelled }))),
3298 { "rpc.aggregate": "vcs" },
3299 ),
3300 [WS_METHODS.vcsRefreshStatus]: (input) =>
3301 observeRpcEffect(
3302 WS_METHODS.vcsRefreshStatus,
3303 vcsStatusBroadcaster.refreshStatus(input.cwd),
3304 {
3305 "rpc.aggregate": "vcs",
3306 },
3307 ),
2767 worktreeSetupTracker
2768 .cancel(input.threadId)
2769 .pipe(Effect.map((cancelled) => ({ cancelled }))),
2770 [WS_METHODS.vcsRefreshStatus]: (input) => vcsStatusBroadcaster.refreshStatus(input.cwd),
33082771 [WS_METHODS.vcsPull]: (input) =>
3309 observeRpcEffect(
3310 WS_METHODS.vcsPull,
3311 gitWorkflow.pullCurrentBranch(input.cwd).pipe(
3312 Effect.matchCauseEffect({
3313 onFailure: (cause) => Effect.failCause(cause),
3314 onSuccess: (result) =>
3315 refreshGitStatus(input.cwd).pipe(Effect.ignore({ log: true }), Effect.as(result)),
3316 }),
3317 ),
3318 { "rpc.aggregate": "git" },
2772 gitWorkflow.pullCurrentBranch(input.cwd).pipe(
2773 Effect.matchCauseEffect({
2774 onFailure: (cause) => Effect.failCause(cause),
2775 onSuccess: (result) =>
2776 refreshGitStatus(input.cwd).pipe(Effect.ignore({ log: true }), Effect.as(result)),
2777 }),
33192778 ),
33202779 [WS_METHODS.gitRunStackedAction]: (input) =>
3321 observeRpcStream(
3322 WS_METHODS.gitRunStackedAction,
3323 Stream.callback<GitActionProgressEvent, GitManagerServiceError>((queue) =>
3324 gitWorkflow
3325 .runStackedAction(input, {
3326 actionId: input.actionId,
3327 progressReporter: {
3328 publish: (event) => Queue.offer(queue, event).pipe(Effect.asVoid),
3329 },
3330 })
3331 .pipe(
3332 Effect.matchCauseEffect({
3333 onFailure: (cause) => Queue.failCause(queue, cause),
3334 onSuccess: (result) =>
3335 (input.threadId === undefined
3336 ? Effect.void
3337 : linkCreatedPullRequest({
3338 threadId: input.threadId,
3339 result,
3340 commandId: serverCommandId("pr-created-link"),
3341 }).pipe(
3342 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),
3343 Effect.provideService(ProjectService.ProjectService, projectService),
3344 )
3345 ).pipe(
3346 Effect.andThen(
3347 refreshPushedPullRequests(input, result).pipe(
3348 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),
3349 Effect.provideService(ProjectStore.ProjectStoreV2, projectStore),
3350 Effect.provideService(
3351 PullRequestService.PullRequestService,
3352 pullRequests,
3353 ),
2780 Stream.callback<GitActionProgressEvent, GitManagerServiceError>((queue) =>
2781 gitWorkflow
2782 .runStackedAction(input, {
2783 actionId: input.actionId,
2784 progressReporter: {
2785 publish: (event) => Queue.offer(queue, event).pipe(Effect.asVoid),
2786 },
2787 })
2788 .pipe(
2789 Effect.matchCauseEffect({
2790 onFailure: (cause) => Queue.failCause(queue, cause),
2791 onSuccess: (result) =>
2792 (input.threadId === undefined
2793 ? Effect.void
2794 : linkCreatedPullRequest({
2795 threadId: input.threadId,
2796 result,
2797 commandId: serverCommandId("pr-created-link"),
2798 }).pipe(
2799 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),
2800 Effect.provideService(ProjectService.ProjectService, projectService),
2801 )
2802 ).pipe(
2803 Effect.andThen(
2804 refreshPushedPullRequests(input, result).pipe(
2805 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),
2806 Effect.provideService(ProjectStore.ProjectStoreV2, projectStore),
2807 Effect.provideService(
2808 PullRequestService.PullRequestService,
2809 pullRequests,
33542810 ),
33552811 ),
3356 Effect.andThen(refreshGitStatus(input.cwd)),
3357 Effect.andThen(Queue.end(queue).pipe(Effect.asVoid)),
33582812 ),
3359 }),
3360 ),
3361 ),
3362 { "rpc.aggregate": "vcs" },
3363 ),
3364 [WS_METHODS.gitResolvePullRequest]: (input) =>
3365 observeRpcEffect(
3366 WS_METHODS.gitResolvePullRequest,
3367 gitWorkflow.resolvePullRequest(input),
3368 {
3369 "rpc.aggregate": "git",
3370 },
2813 Effect.andThen(refreshGitStatus(input.cwd)),
2814 Effect.andThen(Queue.end(queue).pipe(Effect.asVoid)),
2815 ),
2816 }),
2817 ),
33712818 ),
2819 [WS_METHODS.gitResolvePullRequest]: (input) => gitWorkflow.resolvePullRequest(input),
33722820 [WS_METHODS.gitPreparePullRequestThread]: (input) =>
3373 observeRpcEffect(
3374 WS_METHODS.gitPreparePullRequestThread,
3375 gitWorkflow
3376 .preparePullRequestThread(input)
3377 .pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3378 { "rpc.aggregate": "git" },
3379 ),
3380 [WS_METHODS.vcsListRefs]: (input) =>
3381 observeRpcEffect(WS_METHODS.vcsListRefs, gitWorkflow.listRefs(input), {
3382 "rpc.aggregate": "vcs",
3383 }),
2821 gitWorkflow
2822 .preparePullRequestThread(input)
2823 .pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
2824 [WS_METHODS.vcsListRefs]: (input) => gitWorkflow.listRefs(input),
33842825 [WS_METHODS.vcsCreateWorktree]: (input) =>
3385 observeRpcEffect(
3386 WS_METHODS.vcsCreateWorktree,
3387 gitWorkflow.createWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3388 { "rpc.aggregate": "vcs" },
3389 ),
2826 gitWorkflow.createWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
33902827 [WS_METHODS.vcsRemoveWorktree]: (input) =>
3391 observeRpcEffect(
3392 WS_METHODS.vcsRemoveWorktree,
3393 gitWorkflow.removeWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3394 { "rpc.aggregate": "vcs" },
3395 ),
2828 gitWorkflow.removeWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
33962829 [WS_METHODS.vcsCreateRef]: (input) =>
3397 observeRpcEffect(
3398 WS_METHODS.vcsCreateRef,
3399 gitWorkflow.createRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3400 { "rpc.aggregate": "vcs" },
3401 ),
2830 gitWorkflow.createRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
34022831 [WS_METHODS.vcsSwitchRef]: (input) =>
3403 observeRpcEffect(
3404 WS_METHODS.vcsSwitchRef,
3405 gitWorkflow.switchRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3406 { "rpc.aggregate": "vcs" },
3407 ),
2832 gitWorkflow.switchRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
34082833 [WS_METHODS.vcsInit]: (input) =>
3409 observeRpcEffect(
3410 WS_METHODS.vcsInit,
3411 vcsProvisioning
3412 .initRepository(input)
3413 .pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3414 { "rpc.aggregate": "vcs" },
3415 ),
3416 [WS_METHODS.reviewGetDiffPreview]: (input) =>
3417 observeRpcEffect(WS_METHODS.reviewGetDiffPreview, review.getDiffPreview(input), {
3418 "rpc.aggregate": "review",
3419 }),
3420 [WS_METHODS.reviewGetDiffFileContents]: (input) =>
3421 observeRpcEffect(
3422 WS_METHODS.reviewGetDiffFileContents,
3423 review.getDiffFileContents(input),
3424 { "rpc.aggregate": "review" },
3425 ),
3426 [WS_METHODS.terminalOpen]: (input) =>
3427 observeRpcEffect(WS_METHODS.terminalOpen, terminalManager.open(input), {
3428 "rpc.aggregate": "terminal",
3429 }),
2834 vcsProvisioning.initRepository(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
2835 [WS_METHODS.reviewGetDiffPreview]: (input) => review.getDiffPreview(input),
2836 [WS_METHODS.reviewGetDiffFileContents]: (input) => review.getDiffFileContents(input),
2837 [WS_METHODS.terminalOpen]: (input) => terminalManager.open(input),
34302838 [WS_METHODS.terminalAttach]: (input) =>
3431 observeRpcStream(
3432 WS_METHODS.terminalAttach,
3433 Stream.callback<TerminalAttachStreamEvent, TerminalError>((queue) =>
3434 Effect.acquireRelease(
3435 terminalManager.attachStream(input, (event) => Queue.offer(queue, event)),
3436 (unsubscribe) => Effect.sync(unsubscribe),
3437 ),
2839 Stream.callback<TerminalAttachStreamEvent, TerminalError>((queue) =>
2840 Effect.acquireRelease(
2841 terminalManager.attachStream(input, (event) => Queue.offer(queue, event)),
2842 (unsubscribe) => Effect.sync(unsubscribe),
34382843 ),
3439 { "rpc.aggregate": "terminal" },
34402844 ),
3441 [WS_METHODS.terminalWrite]: (input) =>
3442 observeRpcEffect(WS_METHODS.terminalWrite, terminalManager.write(input), {
3443 "rpc.aggregate": "terminal",
3444 }),
3445 [WS_METHODS.terminalResize]: (input) =>
3446 observeRpcEffect(WS_METHODS.terminalResize, terminalManager.resize(input), {
3447 "rpc.aggregate": "terminal",
3448 }),
3449 [WS_METHODS.terminalClear]: (input) =>
3450 observeRpcEffect(WS_METHODS.terminalClear, terminalManager.clear(input), {
3451 "rpc.aggregate": "terminal",
3452 }),
3453 [WS_METHODS.terminalRestart]: (input) =>
3454 observeRpcEffect(WS_METHODS.terminalRestart, terminalManager.restart(input), {
3455 "rpc.aggregate": "terminal",
3456 }),
3457 [WS_METHODS.terminalClose]: (input) =>
3458 observeRpcEffect(WS_METHODS.terminalClose, terminalManager.close(input), {
3459 "rpc.aggregate": "terminal",
3460 }),
2845 [WS_METHODS.terminalWrite]: (input) => terminalManager.write(input),
2846 [WS_METHODS.terminalResize]: (input) => terminalManager.resize(input),
2847 [WS_METHODS.terminalClear]: (input) => terminalManager.clear(input),
2848 [WS_METHODS.terminalRestart]: (input) => terminalManager.restart(input),
2849 [WS_METHODS.terminalClose]: (input) => terminalManager.close(input),
34612850 [WS_METHODS.subscribeTerminalEvents]: (_input) =>
3462 observeRpcStream(
3463 WS_METHODS.subscribeTerminalEvents,
3464 Stream.callback<TerminalEvent>((queue) =>
3465 Effect.acquireRelease(
3466 terminalManager.subscribe((event) => Queue.offer(queue, event)),
3467 (unsubscribe) => Effect.sync(unsubscribe),
3468 ),
2851 Stream.callback<TerminalEvent>((queue) =>
2852 Effect.acquireRelease(
2853 terminalManager.subscribe((event) => Queue.offer(queue, event)),
2854 (unsubscribe) => Effect.sync(unsubscribe),
34692855 ),
3470 { "rpc.aggregate": "terminal" },
34712856 ),
34722857 [WS_METHODS.subscribeTerminalMetadata]: (_input) =>
3473 observeRpcStream(
3474 WS_METHODS.subscribeTerminalMetadata,
3475 Stream.callback<TerminalMetadataStreamEvent>((queue) =>
3476 Effect.acquireRelease(
3477 terminalManager.subscribeMetadata((event) => Queue.offer(queue, event)),
3478 (unsubscribe) => Effect.sync(unsubscribe),
3479 ),
2858 Stream.callback<TerminalMetadataStreamEvent>((queue) =>
2859 Effect.acquireRelease(
2860 terminalManager.subscribeMetadata((event) => Queue.offer(queue, event)),
2861 (unsubscribe) => Effect.sync(unsubscribe),
34802862 ),
3481 { "rpc.aggregate": "terminal" },
34822863 ),
3483 [WS_METHODS.previewOpen]: (input) =>
3484 observeRpcEffect(WS_METHODS.previewOpen, previewManager.open(input), {
3485 "rpc.aggregate": "preview",
3486 }),
3487 [WS_METHODS.previewNavigate]: (input) =>
3488 observeRpcEffect(WS_METHODS.previewNavigate, previewManager.navigate(input), {
3489 "rpc.aggregate": "preview",
3490 }),
3491 [WS_METHODS.previewResize]: (input) =>
3492 observeRpcEffect(WS_METHODS.previewResize, previewManager.resize(input), {
3493 "rpc.aggregate": "preview",
3494 }),
3495 [WS_METHODS.previewAdjust]: (input) =>
3496 observeRpcEffect(WS_METHODS.previewAdjust, previewManager.adjust(input), {
3497 "rpc.aggregate": "preview",
3498 }),
3499 [WS_METHODS.previewRefresh]: (input) =>
3500 observeRpcEffect(WS_METHODS.previewRefresh, previewManager.refresh(input), {
3501 "rpc.aggregate": "preview",
3502 }),
3503 [WS_METHODS.previewClose]: (input) =>
3504 observeRpcEffect(WS_METHODS.previewClose, previewManager.close(input), {
3505 "rpc.aggregate": "preview",
3506 }),
3507 [WS_METHODS.previewList]: (input) =>
3508 observeRpcEffect(WS_METHODS.previewList, previewManager.list(input), {
3509 "rpc.aggregate": "preview",
3510 }),
3511 [WS_METHODS.previewClearProfile]: (input) =>
3512 observeRpcEffect(
3513 WS_METHODS.previewClearProfile,
3514 serverBrowser.clearProfile(input.profileId),
3515 { "rpc.aggregate": "preview" },
3516 ),
3517 [WS_METHODS.previewReportStatus]: (input) =>
3518 observeRpcEffect(WS_METHODS.previewReportStatus, previewManager.reportStatus(input), {
3519 "rpc.aggregate": "preview",
3520 }),
3521 [WS_METHODS.subscribePreviewEvents]: (_input) =>
3522 observeRpcStream(WS_METHODS.subscribePreviewEvents, previewManager.events, {
3523 "rpc.aggregate": "preview",
3524 }),
3525 [WS_METHODS.deviceConfigure]: (input) =>
3526 observeRpcEffect(WS_METHODS.deviceConfigure, deviceService.configure(input), {
3527 "rpc.aggregate": "device",
3528 }),
3529 [WS_METHODS.deviceTestHost]: (input) =>
3530 observeRpcEffect(WS_METHODS.deviceTestHost, deviceService.testHost(input), {
3531 "rpc.aggregate": "device",
3532 }),
2864 [WS_METHODS.previewOpen]: (input) => previewManager.open(input),
2865 [WS_METHODS.previewNavigate]: (input) => previewManager.navigate(input),
2866 [WS_METHODS.previewResize]: (input) => previewManager.resize(input),
2867 [WS_METHODS.previewAdjust]: (input) => previewManager.adjust(input),
2868 [WS_METHODS.previewRefresh]: (input) => previewManager.refresh(input),
2869 [WS_METHODS.previewClose]: (input) => previewManager.close(input),
2870 [WS_METHODS.previewList]: (input) => previewManager.list(input),
2871 [WS_METHODS.previewClearProfile]: (input) => serverBrowser.clearProfile(input.profileId),
2872 [WS_METHODS.previewReportStatus]: (input) => previewManager.reportStatus(input),
2873 [WS_METHODS.subscribePreviewEvents]: (_input) => previewManager.events,
2874 [WS_METHODS.deviceConfigure]: (input) => deviceService.configure(input),
2875 [WS_METHODS.deviceTestHost]: (input) => deviceService.testHost(input),
35332876 [WS_METHODS.deviceList]: (input) =>
3534 observeRpcEffect(
3535 WS_METHODS.deviceList,
3536 input.inspectOnly && !input.updateTool
3537 ? deviceService.inspect
3538 : authorizeEffect(
3539 requiredScopeForDeviceList(input),
3540 input.updateTool
3541 ? deviceService.updateTool(input.updateTool)
3542 : input.retryHostId
3543 ? deviceService.retryHost(input.retryHostId)
3544 : deviceService.list,
3545 ),
3546 {
3547 "rpc.aggregate": "device",
3548 },
3549 ),
3550 [WS_METHODS.deviceOpen]: (input) =>
3551 observeRpcEffect(WS_METHODS.deviceOpen, deviceService.open(input), {
3552 "rpc.aggregate": "device",
3553 }),
3554 [WS_METHODS.deviceClose]: (input) =>
3555 observeRpcEffect(WS_METHODS.deviceClose, deviceService.close(input), {
3556 "rpc.aggregate": "device",
3557 }),
3558 [WS_METHODS.deviceShutdown]: (input) =>
3559 observeRpcEffect(WS_METHODS.deviceShutdown, deviceService.shutdown(input), {
3560 "rpc.aggregate": "device",
3561 }),
3562 [WS_METHODS.deviceDetail]: (input) =>
3563 observeRpcEffect(WS_METHODS.deviceDetail, deviceService.detail(input), {
3564 "rpc.aggregate": "device",
3565 }),
3566 [WS_METHODS.deviceAction]: (input) =>
3567 observeRpcEffect(WS_METHODS.deviceAction, deviceService.action(input), {
3568 "rpc.aggregate": "device",
3569 }),
3570 [WS_METHODS.subscribeDeviceState]: (_input) =>
3571 observeRpcStream(
3572 WS_METHODS.subscribeDeviceState,
3573 DeviceService.stateStream(deviceService),
3574 { "rpc.aggregate": "device" },
3575 ),
2877 input.inspectOnly && !input.updateTool
2878 ? deviceService.inspect
2879 : authorizeEffect(
2880 requiredScopeForDeviceList(input),
2881 input.updateTool
2882 ? deviceService.updateTool(input.updateTool)
2883 : input.retryHostId
2884 ? deviceService.retryHost(input.retryHostId)
2885 : deviceService.list,
2886 ),
2887 [WS_METHODS.deviceOpen]: (input) => deviceService.open(input),
2888 [WS_METHODS.deviceClose]: (input) => deviceService.close(input),
2889 [WS_METHODS.deviceShutdown]: (input) => deviceService.shutdown(input),
2890 [WS_METHODS.deviceDetail]: (input) => deviceService.detail(input),
2891 [WS_METHODS.deviceAction]: (input) => deviceService.action(input),
2892 [WS_METHODS.subscribeDeviceState]: (_input) => DeviceService.stateStream(deviceService),
35762893 [WS_METHODS.subscribeDiscoveredLocalServers]: (input) =>
3577 observeRpcStream(
3578 WS_METHODS.subscribeDiscoveredLocalServers,
3579 Stream.callback<DiscoveredLocalServerList>((queue) =>
3580 Effect.gen(function* () {
3581 const configuredUrls = input.configuredUrls ?? [];
3582 yield* portDiscovery.retain;
3583 const initial = yield* portDiscovery.scan(configuredUrls);
3584 const initialScannedAt = DateTime.formatIso(yield* DateTime.now);
3585 yield* Queue.offer(queue, {
3586 servers: initial,
3587 scannedAt: initialScannedAt,
3588 configuredUrlProbing: true,
3589 });
3590 yield* portDiscovery.subscribe(
3591 { configuredUrls, initialSnapshot: initial },
3592 (servers) =>
3593 Effect.gen(function* () {
3594 const scannedAt = DateTime.formatIso(yield* DateTime.now);
3595 yield* Queue.offer(queue, {
3596 servers,
3597 scannedAt,
3598 configuredUrlProbing: true,
3599 });
3600 }),
3601 );
3602 }),
3603 ),
3604 { "rpc.aggregate": "preview" },
2894 Stream.callback<DiscoveredLocalServerList>((queue) =>
2895 Effect.gen(function* () {
2896 const configuredUrls = input.configuredUrls ?? [];
2897 yield* portDiscovery.retain;
2898 const initial = yield* portDiscovery.scan(configuredUrls);
2899 const initialScannedAt = DateTime.formatIso(yield* DateTime.now);
2900 yield* Queue.offer(queue, {
2901 servers: initial,
2902 scannedAt: initialScannedAt,
2903 configuredUrlProbing: true,
2904 });
2905 yield* portDiscovery.subscribe(
2906 { configuredUrls, initialSnapshot: initial },
2907 (servers) =>
2908 Effect.gen(function* () {
2909 const scannedAt = DateTime.formatIso(yield* DateTime.now);
2910 yield* Queue.offer(queue, {
2911 servers,
2912 scannedAt,
2913 configuredUrlProbing: true,
2914 });
2915 }),
2916 );
2917 }),
36052918 ),
36062919 [WS_METHODS.subscribeServerConfig]: (input) =>
3607 observeRpcStreamEffect(
3608 WS_METHODS.subscribeServerConfig,
2920 Stream.unwrap(
36092921 Effect.gen(function* () {
36102922 const usageLimitsCommand = input.usageLimitsCommand === true;
36112923 const config = yield* loadServerConfig({ usageLimitsCommand });
......@@ -3705,11 +3017,9 @@ const layerWsRpc = (
37053017 withLateEditorConfig(config, liveUpdates, externalLauncher),
37063018 );
37073019 }),
3708 { "rpc.aggregate": "server" },
37093020 ),
37103021 [WS_METHODS.subscribeServerLifecycle]: (_input) =>
3711 observeRpcStreamEffect(
3712 WS_METHODS.subscribeServerLifecycle,
3022 Stream.unwrap(
37133023 Effect.gen(function* () {
37143024 const liveBuffer = yield* Queue.unbounded<ServerLifecycleStreamEvent>();
37153025 yield* Effect.forkScoped(
......@@ -3727,11 +3037,9 @@ const layerWsRpc = (
37273037 );
37283038 return Stream.concat(rpcInitialItems(snapshotEvents), liveEvents);
37293039 }),
3730 { "rpc.aggregate": "server" },
37313040 ),
37323041 [WS_METHODS.subscribeAuthAccess]: (_input) =>
3733 observeRpcStreamEffect(
3734 WS_METHODS.subscribeAuthAccess,
3042 Stream.unwrap(
37353043 Effect.gen(function* () {
37363044 const initialSnapshot = yield* loadAuthAccessSnapshot();
37373045 const revisionRef = yield* Ref.make(1);
......@@ -3761,27 +3069,18 @@ const layerWsRpc = (
37613069 liveEvents,
37623070 );
37633071 }),
3764 { "rpc.aggregate": "auth" },
37653072 ),
37663073 [WS_METHODS.subscribeBackgroundPolicy]: (_input) =>
3767 observeRpcStream(
3768 WS_METHODS.subscribeBackgroundPolicy,
3769 Stream.unwrap(
3770 Effect.map(backgroundPolicy.subscribe, ({ latest, changes }) =>
3771 Stream.concat(Stream.make(latest), changes),
3772 ),
3074 Stream.unwrap(
3075 Effect.map(backgroundPolicy.subscribe, ({ latest, changes }) =>
3076 Stream.concat(Stream.make(latest), changes),
37733077 ),
3774 { "rpc.aggregate": "server" },
37753078 ),
37763079 [WS_METHODS.subscribeResourceTelemetry]: (_input) =>
3777 observeRpcStream(
3778 WS_METHODS.subscribeResourceTelemetry,
3779 Stream.unwrap(
3780 Effect.map(resourceTelemetry.subscribe, ({ latest, changes }) =>
3781 Stream.concat(Stream.make(latest), changes),
3782 ),
3080 Stream.unwrap(
3081 Effect.map(resourceTelemetry.subscribe, ({ latest, changes }) =>
3082 Stream.concat(Stream.make(latest), changes),
37833083 ),
3784 { "rpc.aggregate": "server" },
37853084 ),
37863085 });
37873086 return handlers;
......@@ -3841,7 +3140,9 @@ export const layer = Layer.unwrap(
38413140 const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket;
38423141 yield* RpcServer.make(ServerWsRpcGroup, WS_RPC_SERVER_OPTIONS).pipe(
38433142 Effect.provideService(RpcServer.Protocol, withTerminalOutputWindow(protocol)),
3844 Effect.provide(RpcAuthorization.layer(session.scopes)),
3143 Effect.provide(
3144 Layer.merge(RpcAuthorization.layer(session.scopes), rpcInstrumentationLayer),
3145 ),
38453146 Effect.forkScoped,
38463147 );
38473148 // @effect-diagnostics-next-line returnEffectInGen:off
docs/internals/effect-services.md+9-7
......@@ -24,15 +24,17 @@ message) are fine next to the service; the capability itself is the method.
2424```ts
2525// ws.ts: a thin handler
2626[WS_METHODS.projectsCreateNew]: (input) =>
27 observeRpcEffect(
28 WS_METHODS.projectsCreateNew,
29 projectFolders.createNamedProject(input).pipe(
30 Effect.mapError((cause) => new ProjectCreateNewError({ cause })),
31 ),
32 { "rpc.aggregate": "orchestration" },
33 ),
27 projectFolders
28 .createNamedProject(input)
29 .pipe(Effect.mapError((cause) => new ProjectCreateNewError({ cause }))),
3430```
3531
32Handlers don't add their own spans or request metrics. Group middleware authorizes every call
33([`RpcAuthorization.ts`](../../apps/server/src/auth/RpcAuthorization.ts)), and the server's group
34also instruments it
35([`RpcInstrumentation.ts`](../../apps/server/src/observability/RpcInstrumentation.ts)). A handler
36with per-call context, such as a thread id, adds it with `Effect.annotateCurrentSpan`.
37
3638## Shape of a service module
3739
3840One module per service, in this order: imports, errors and schemas, the `Context.Service` tag with
docs/operations/observability.md+2-2
......@@ -635,8 +635,8 @@ wins for its signal. `otlp` is the default, and any other exporter name, such as
635635
636636Current high-value span and metric boundaries include:
637637
638- Effect RPC websocket request spans from `effect/rpc`
639- RPC request metrics in `apps/server/src/observability/RpcInstrumentation.ts`
638- WebSocket RPC request spans (`ws.rpc.<method>`) and metrics in
639 `apps/server/src/observability/RpcInstrumentation.ts`
640640- startup phases
641641- orchestration command processing
642642- provider session and turn operations