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 @@...@@ -1,313 +1,336 @@
1import { assert, describe, it } from "@effect/vitest";1import { 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";
3import * as Duration from "effect/Duration";12import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";14import * as Exit from "effect/Exit";
6import * as Fiber from "effect/Fiber";15import * as Fiber from "effect/Fiber";
16import * as Layer from "effect/Layer";
7import * as Metric from "effect/Metric";17import * as Metric from "effect/Metric";
18import * as Option from "effect/Option";
8import * as Stream from "effect/Stream";19import * as Stream from "effect/Stream";
9import * as Tracer from "effect/Tracer";20import * as Tracer from "effect/Tracer";
21import * as RpcTest from "effect/rpc/RpcTest";
10import * as TestClock from "effect/testing/TestClock";22import * as TestClock from "effect/testing/TestClock";
1123
12import {24import { RPC_REQUIRED_SCOPES } from "../auth/RpcAuthorization.ts";
13 observeRpcEffect,25import * as RpcAuthorization from "../auth/RpcAuthorization.ts";
14 observeRpcStream,26import { RpcInstrumentation, rpcInstrumentationLayer } from "./RpcInstrumentation.ts";
15 observeRpcStreamEffect,27
16} from "./RpcInstrumentation.ts";28type WsRpcMethod = keyof typeof RPC_REQUIRED_SCOPES;
1729
18const hasMetricSnapshot = (30/** The server group narrowed to `tags`, so a test only implements the handlers it calls. */
19 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,31const groupOf = <const Tags extends ReadonlyArray<WsRpcMethod>>(...tags: Tags) =>
20 id: string,32 WsRpcGroup.omit(
21 attributes: Readonly<Record<string, string>>,33 ...[...WsRpcGroup.requests.keys()].filter(
22) =>34 (tag): tag is Exclude<WsRpcMethod, Tags[number]> =>
23 snapshots.some(35 !(tags as ReadonlyArray<string>).includes(tag),
24 (snapshot) =>36 ),
25 snapshot.id === id &&37 ).middleware(RpcInstrumentation);
26 Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value),38
27 );39/** The middleware ws.ts installs for a connection with `scopes`. */
2840const connectionMiddleware = (scopes: ReadonlyArray<AuthEnvironmentScope>) =>
29const findHistogramSnapshot = (41 Layer.merge(RpcAuthorization.layer(scopes), rpcInstrumentationLayer);
30 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,42const readOnlyConnection = connectionMiddleware([AuthOrchestrationReadScope]);
31 id: string,43const taskId = ScheduledTaskId.make("scheduled-task:instrumented");
32 attributes: Readonly<Record<string, string>>,44const rpcSpanDefaults = { "rpc.transport": "websocket", "rpc.system": "effect-rpc" };
33) =>45
34 snapshots.find(46/** Runs a test with a fresh metric registry and a tracer that keeps every span it ends. */
35 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Histogram" }> =>47const withTelemetry = <A, E, R>(
36 snapshot.type === "Histogram" &&48 test: (ended: ReadonlyArray<Tracer.NativeSpan>) => Effect.Effect<A, E, R>,
37 snapshot.id === id &&49) => {
38 Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value),50 const ended: Array<Tracer.NativeSpan> = [];
39 );51 const tracer = Tracer.make({
4052 span: (options) => {
41const collectSpanNames = <A, E, R>(53 const span = new Tracer.NativeSpan(options);
42 effect: Effect.Effect<A, E, R>,54 const end = span.end.bind(span);
43): Effect.Effect<ReadonlyArray<string>, E, R> =>55 span.end = (endTime, exit) => {
44 Effect.gen(function* () {56 end(endTime, exit);
45 const spanNames: Array<string> = [];57 ended.push(span);
46 const tracer = Tracer.make({58 };
47 span: (options) => {59 return span;
48 const span = new Tracer.NativeSpan(options);60 },
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;
65 });61 });
6662 return test(ended).pipe(
67describe("RpcInstrumentation", () => {63 Effect.scoped,
68 it.effect("records success metrics for unary RPC handlers", () =>64 Effect.withTracer(tracer),
69 Effect.gen(function* () {65 Effect.provideService(Metric.MetricRegistry, new Map()),
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 }),
90 );66 );
67};
9168
92 it.effect("records failure outcomes for unary RPC handlers", () =>69const rpcSpans = (ended: ReadonlyArray<Tracer.NativeSpan>) =>
93 Effect.gen(function* () {70 ended.filter((span) => span.name.startsWith("ws.rpc."));
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 );
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(77const exitTag = (span: Tracer.NativeSpan | undefined) =>
103 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {78 span?.status._tag === "Ended" ? span.status.exit._tag : undefined;
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 );
11779
118 it.effect("records subscription activation metrics for stream RPC handlers", () =>80const parentSpanId = (span: Tracer.NativeSpan | undefined) =>
119 Effect.gen(function* () {81 span?.parent._tag === "Some" ? span.parent.value.spanId : undefined;
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);
19182
192 yield* Effect.yieldNow;83const requestCount = (
193 yield* TestClock.adjust(duration);84 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
194 return yield* Fiber.join(fiber);85 method: string,
195 }).pipe(Effect.provide(TestClock.layer()));86 outcome: string,
19687) =>
197 assert.deepStrictEqual(Array.from(events), ["ok"]);88 snapshots.find(
19889 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Counter" }> =>
199 const snapshots = yield* Metric.snapshot;90 snapshot.type === "Counter" &&
200 const snapshot = findHistogramSnapshot(snapshots, "t3_rpc_request_duration", {91 snapshot.id === "t3_rpc_requests_total" &&
201 method: WS_METHODS.serverGetProcessDiagnostics,92 snapshot.attributes?.["method"] === method &&
202 });93 snapshot.attributes?.["outcome"] === outcome,
20394 )?.state;
204 assert.equal(snapshot?.state.count, 1);95
205 assert.equal(snapshot?.state.sum, 1.5);96const requestDuration = (snapshots: ReadonlyArray<Metric.Metric.Snapshot>, method: string) =>
206 }),97 snapshots.find(
207 );98 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Histogram" }> =>
20899 snapshot.type === "Histogram" &&
209 it.effect("records failure outcomes when a stream RPC effect produces a failing stream", () =>100 snapshot.id === "t3_rpc_request_duration" &&
210 Effect.gen(function* () {101 snapshot.attributes?.["method"] === method,
211 const exit = yield* Stream.runCollect(102 )?.state;
212 observeRpcStreamEffect(103
213 "rpc.instrumentation.stream.effect.failure",104describe("WS RPC instrumentation middleware", () => {
214 Effect.succeed(Stream.fail("boom")),105 it.effect("records one span and request metric per call, including rejected calls", () =>
215 { "rpc.aggregate": "test" },106 withTelemetry((ended) =>
216 ).pipe(Stream.withSpan("rpc.instrumentation.stream.effect.failure.span")),107 Effect.gen(function* () {
217 ).pipe(Effect.exit);108 const group = groupOf(
218109 WS_METHODS.serverProbe,
219 assert.equal(Exit.isFailure(exit), true);110 WS_METHODS.scheduledTasksList,
220111 WS_METHODS.serverRetryResourceTelemetry,
221 const snapshots = yield* Metric.snapshot;112 WS_METHODS.pullRequestsSubscribeRefreshes,
222113 WS_METHODS.scheduledTasksSubscribe,
223 assert.equal(114 WS_METHODS.serverGetSettings,
224 hasMetricSnapshot(snapshots, "t3_rpc_requests_total", {115 );
225 method: "rpc.instrumentation.stream.effect.failure",116 const client = yield* RpcTest.makeClient(group).pipe(
226 outcome: "failure",117 Effect.provide(
227 }),118 Layer.mergeAll(
228 true,119 group.toLayerHandler(WS_METHODS.serverProbe, () =>
229 );120 Effect.succeed({}).pipe(Effect.withSpan("serverProbe.child")),
230 assert.equal(121 ),
231 hasMetricSnapshot(snapshots, "t3_rpc_request_duration", {122 group.toLayerHandler(WS_METHODS.scheduledTasksList, () =>
232 method: "rpc.instrumentation.stream.effect.failure",123 Effect.annotateCurrentSpan({ "scheduled_task.id": taskId }).pipe(
233 }),124 Effect.andThen(new ScheduledTaskError({ message: "List failed." })),
234 true,125 ),
235 );126 ),
236 }),127 group.toLayerHandler(WS_METHODS.serverRetryResourceTelemetry, () =>
237 );128 Effect.die("authorization let a rejected call through"),
238129 ),
239 it.effect("records spans for traced stream RPC handlers", () =>130 group.toLayerHandler(WS_METHODS.pullRequestsSubscribeRefreshes, () =>
240 Effect.gen(function* () {131 Stream.make(1, 2),
241 const spanNames = yield* collectSpanNames(132 ),
242 Stream.runCollect(133 group.toLayerHandler(WS_METHODS.scheduledTasksSubscribe, () =>
243 observeRpcStream(134 Stream.concat(
244 "rpc.instrumentation.traced.stream",135 Stream.make({ tasks: [] }),
245 Stream.fromEffect(136 Stream.fail(new ScheduledTaskError({ message: "Subscription failed." })),
246 Effect.succeed("ok").pipe(Effect.withSpan("rpc.instrumentation.traced.stream.child")),137 ),
138 ),
139 group.toLayerHandler(WS_METHODS.serverGetSettings, () => Effect.die("broken")),
140 readOnlyConnection,
247 ),141 ),
248 { "rpc.aggregate": "test" },
249 ),142 ),
250 ),143 );
251 );144
252145 assert.deepStrictEqual(yield* client[WS_METHODS.serverProbe]({}), {});
253 assert.equal(spanNames.includes("ws.rpc.rpc.instrumentation.traced.stream"), true);146 const listError = yield* client[WS_METHODS.scheduledTasksList]({}).pipe(Effect.flip);
254 assert.equal(spanNames.includes("rpc.instrumentation.traced.stream.child"), true);147 assert.equal(listError._tag, "ScheduledTaskError");
255 }),148 // Retrying telemetry needs operate scope, so the handler must not run.
256 );149 const rejection = yield* client[WS_METHODS.serverRetryResourceTelemetry]({}).pipe(
257150 Effect.flip,
258 it.effect("does not create spans for disabled unary RPC handlers", () =>151 );
259 Effect.gen(function* () {152 assert.equal(rejection._tag, "EnvironmentAuthorizationError");
260 const spanNames = yield* collectSpanNames(153 const refreshes = yield* Stream.runCollect(
261 observeRpcEffect(154 client[WS_METHODS.pullRequestsSubscribeRefreshes]({}),
262 WS_METHODS.serverGetTraceDiagnostics,155 );
263 Effect.succeed("ok").pipe(Effect.withSpan("rpc.instrumentation.disabled.unary.child")),156 assert.deepStrictEqual(Array.from(refreshes), [1, 2]);
264 { "rpc.aggregate": "test" },157 const subscribeError = yield* Stream.runDrain(
265 ),158 client[WS_METHODS.scheduledTasksSubscribe]({}),
266 );159 ).pipe(Effect.flip);
267160 assert.equal(subscribeError._tag, "ScheduledTaskError");
268 assert.deepStrictEqual(spanNames, []);161 const settingsExit = yield* Effect.exit(client[WS_METHODS.serverGetSettings]({}));
269 }),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 ),
270 );247 );
271248
272 it.effect("does not create spans for disabled direct stream RPC handlers", () =>249 it.effect("keeps a stream's span and duration open until the subscription is interrupted", () =>
273 Effect.gen(function* () {250 withTelemetry((ended) =>
274 const spanNames = yield* collectSpanNames(251 Effect.gen(function* () {
275 Stream.runCollect(252 const waiting = yield* Deferred.make<void>();
276 observeRpcStream(253 const group = groupOf(WS_METHODS.pullRequestsSubscribeRefreshes);
277 WS_METHODS.serverGetTraceDiagnostics,254 const client = yield* RpcTest.makeClient(group).pipe(
278 Stream.fromEffect(255 Effect.provide(
279 Effect.succeed("ok").pipe(256 Layer.mergeAll(
280 Effect.withSpan("rpc.instrumentation.disabled.stream.child"),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 ),
281 ),267 ),
268 readOnlyConnection,
282 ),269 ),
283 { "rpc.aggregate": "test" },
284 ),270 ),
285 ),271 );
286 );272
287273 // Wall and monotonic time disagree before the call starts, so a duration that mixes
288 assert.deepStrictEqual(spanNames, []);274 // the two clocks is caught.
289 }),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 ),
290 );305 );
291306
292 it.effect("does not create spans for disabled stream effect RPC handlers", () =>307 it.effect("records metrics but no spans for methods with tracing disabled", () =>
293 Effect.gen(function* () {308 withTelemetry((ended) =>
294 const spanNames = yield* collectSpanNames(309 Effect.gen(function* () {
295 Stream.runCollect(310 const group = groupOf(WS_METHODS.serverSignalProcess);
296 observeRpcStreamEffect(311 const client = yield* RpcTest.makeClient(group).pipe(
297 WS_METHODS.serverGetTraceDiagnostics,312 Effect.provide(
298 Effect.succeed(313 Layer.mergeAll(
299 Stream.fromEffect(314 group.toLayerHandler(WS_METHODS.serverSignalProcess, (input) =>
300 Effect.succeed("ok").pipe(315 Effect.succeed({
301 Effect.withSpan("rpc.instrumentation.disabled.stream.effect.consume"),316 pid: input.pid,
302 ),317 signal: input.signal,
318 signaled: true,
319 message: Option.none(),
320 }).pipe(Effect.withSpan("signalProcess.child")),
303 ),321 ),
304 ).pipe(Effect.withSpan("rpc.instrumentation.disabled.stream.effect.create")),322 connectionMiddleware([AuthOrchestrationReadScope, AuthOrchestrationOperateScope]),
305 { "rpc.aggregate": "test" },323 ),
306 ),324 ),
307 ),325 );
308 );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, []);330 assert.deepStrictEqual(appSpans(ended), []);
311 }),331 const snapshots = yield* Metric.snapshot;
332 assert.equal(requestCount(snapshots, WS_METHODS.serverSignalProcess, "success")?.count, 1);
333 }),
334 ),
312 );335 );
313});336});
apps/server/src/observability/RpcInstrumentation.ts+219-116
...@@ -1,14 +1,198 @@...@@ -1,14 +1,198 @@
1import { WS_METHODS } from "@t3tools/contracts";1import { ORCHESTRATION_V2_WS_METHODS, WS_METHODS, type WsRpcGroup } from "@t3tools/contracts";
2import * as Clock from "effect/Clock";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";3import * as Layer from "effect/Layer";
6import * as Metric from "effect/Metric";
7import * as References from "effect/References";4import * 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";8import { rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";
11import { metricAttributes, 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
13const RPC_SPAN_PREFIX = "ws.rpc";197const RPC_SPAN_PREFIX = "ws.rpc";
14const DEFAULT_RPC_SPAN_ATTRIBUTES = {198const DEFAULT_RPC_SPAN_ATTRIBUTES = {
...@@ -22,117 +206,36 @@ const RPC_METHODS_WITH_TRACING_DISABLED: ReadonlySet<string> = new Set([...@@ -22,117 +206,36 @@ const RPC_METHODS_WITH_TRACING_DISABLED: ReadonlySet<string> = new Set([
22 WS_METHODS.serverSignalProcess,206 WS_METHODS.serverSignalProcess,
23]);207]);
24208
25function shouldTraceRpc(method: string): boolean {209/**
26 return !RPC_METHODS_WITH_TRACING_DISABLED.has(method);210 * Records each WebSocket RPC's span and request metrics. `ws.ts` adds it to the server's group
27}211 * after `RpcScopeAuthorization`, so it wraps authorization and also records rejected calls.
28212 */
29const rpcSpanAttributes = (213export class RpcInstrumentation extends RpcMiddleware.Service<RpcInstrumentation>()(
30 method: string,214 "t3/server/RpcInstrumentation",
31 traceAttributes?: Readonly<Record<string, unknown>>,215) {}
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));
50216
51const withRpcStreamTracing = <A, E, R>(217/**
52 method: string,218 * Wraps each WebSocket RPC call in its `ws.rpc.<method>` span and records its request counter and
53 stream: Stream.Stream<A, E, R>,219 * duration. For a stream RPC, the middleware receives the whole stream run, so the span and the
54 traceAttributes?: Readonly<Record<string, unknown>>,220 * metrics cover the subscription until it ends, fails, or is interrupted. Methods in
55): Stream.Stream<A, E, R> =>221 * `RPC_METHODS_WITH_TRACING_DISABLED` record metrics but no spans, for the call or anything it runs.
56 shouldTraceRpc(method)222 */
57 ? stream.pipe(223export const rpcInstrumentationLayer = Layer.succeed(RpcInstrumentation)((effect, { rpc }) => {
58 Stream.withSpan(`${RPC_SPAN_PREFIX}.${method}`, {224 const method = rpc._tag;
59 attributes: rpcSpanAttributes(method, traceAttributes),225 const measured = effect.pipe(
60 }),226 withMetrics({ counter: rpcRequestsTotal, timer: rpcRequestDuration, attributes: { method } }),
61 )227 );
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 });
85228
86export const observeRpcEffect = <A, E, R>(229 if (RPC_METHODS_WITH_TRACING_DISABLED.has(method)) {
87 method: string,230 return measured.pipe(Effect.provideService(References.TracerEnabled, false));
88 effect: Effect.Effect<A, E, R>,231 }
89 traceAttributes?: Readonly<Record<string, unknown>>,232 return measured.pipe(
90): Effect.Effect<A, E, R> => {233 Effect.withSpan(`${RPC_SPAN_PREFIX}.${method}`, {
91 const instrumented = effect.pipe(
92 withMetrics({
93 counter: rpcRequestsTotal,
94 timer: rpcRequestDuration,
95 attributes: {234 attributes: {
96 method,235 ...DEFAULT_RPC_SPAN_ATTRIBUTES,
236 "rpc.method": method,
237 "rpc.aggregate": RPC_AGGREGATES[method as WsRpcMethod],
97 },238 },
98 }),239 }),
99 );240 );
100241});
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};
apps/server/src/ws.ts+902-1601
...@@ -159,11 +159,6 @@ import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";...@@ -159,11 +159,6 @@ import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
159import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts";159import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts";
160import * as OrchestrationEventStore from "./persistence/OrchestrationEventStore.ts";160import * as OrchestrationEventStore from "./persistence/OrchestrationEventStore.ts";
161import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts";161import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts";
162import {
163 observeRpcEffect,
164 observeRpcStream,
165 observeRpcStreamEffect,
166} from "./observability/RpcInstrumentation.ts";
167import * as ProviderRegistry from "./provider/ProviderRegistry.ts";162import * as ProviderRegistry from "./provider/ProviderRegistry.ts";
168import * as ProviderInstanceRegistry from "./provider/ProviderInstanceRegistry.ts";163import * as ProviderInstanceRegistry from "./provider/ProviderInstanceRegistry.ts";
169import * as AcpRegistrySupport from "./provider/acp/AcpRegistrySupport.ts";164import * as AcpRegistrySupport from "./provider/acp/AcpRegistrySupport.ts";
...@@ -215,6 +210,7 @@ import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";...@@ -215,6 +210,7 @@ import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
215import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";210import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
216import { requiredScopeForDeviceList, rpcAuthorizationError } from "./auth/RpcAuthorization.ts";211import { requiredScopeForDeviceList, rpcAuthorizationError } from "./auth/RpcAuthorization.ts";
217import * as RpcAuthorization from "./auth/RpcAuthorization.ts";212import * as RpcAuthorization from "./auth/RpcAuthorization.ts";
213import { RpcInstrumentation, rpcInstrumentationLayer } from "./observability/RpcInstrumentation.ts";
218import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";214import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
219import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";215import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts";
220import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";216import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts";
...@@ -541,7 +537,8 @@ function projectFileFailureContext(...@@ -541,7 +537,8 @@ function projectFileFailureContext(
541537
542const PROVIDER_STATUS_DEBOUNCE_MS = 200;538const 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);
545// When a resuming client's cursor is more than this many events behind the542// When a resuming client's cursor is more than this many events behind the
546// current head, skip the per-event catch-up replay and send a fresh shell543// current head, skip the per-event catch-up replay and send a fresh shell
547// snapshot instead. Replaying each intervening event costs a shell refetch;544// snapshot instead. Replaying each intervening event costs a shell refetch;
...@@ -1808,851 +1805,595 @@ const layerWsRpc = (...@@ -1808,851 +1805,595 @@ const layerWsRpc = (
18081805
1809 const handlers = ServerWsRpcGroup.of({1806 const handlers = ServerWsRpcGroup.of({
1810 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: (command) =>1807 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: (command) =>
1811 observeRpcEffect(1808 Effect.annotateCurrentSpan({
1812 ORCHESTRATION_V2_WS_METHODS.dispatchCommand,1809 "orchestration_v2.command_id": command.commandId,
1813 startup1810 "orchestration_v2.command_type": command.type,
1814 .enqueueCommand(1811 "orchestration_v2.thread_id":
1815 // A retry also restarts the preparation work the launch owns.1812 command.type === "thread.fork" || command.type === "thread.merge_back"
1816 (command.type === "prepared-run.retry"1813 ? command.targetThreadId
1817 ? threadLaunch.retryPreparation(command)1814 : command.type === "delegated_task.request" ||
1818 : ThreadMessageIntake.dispatchCommand(1815 command.type === "delegated_task.wake-policy" ||
1819 ThreadManagementService.withCreationProvenance(command, {1816 command.type === "delegated_task.completion-delivery.acknowledge" ||
1820 createdBy: "user",1817 command.type === "delegated_task.completion-delivery.dispose" ||
1821 creationSource:1818 command.type === "thread.created.record"
1822 "creationSource" in command ? command.creationSource : "web",1819 ? command.parentThreadId
1823 }),1820 : command.threadId,
1824 )1821 ...(command.type === "thread.fork" || command.type === "thread.merge_back"
1825 ).pipe(Effect.provide(intakeContext)),1822 ? { "orchestration_v2.source_thread_id": command.sourceThreadId }
1826 )1823 : {}),
1827 .pipe(1824 }).pipe(
1828 Effect.tap(() => recordClientCommandAnalytics(command)),1825 Effect.andThen(
1829 Effect.map((result) => ({ sequence: result.sequence })),1826 startup
1830 Effect.mapError((cause) => {1827 .enqueueCommand(
1831 const detail = userFacingDispatchErrorMessage(cause);1828 // A retry also restarts the preparation work the launch owns.
1832 return new OrchestrationV2DispatchCommandError({1829 (command.type === "prepared-run.retry"
1833 commandId: command.commandId,1830 ? threadLaunch.retryPreparation(command)
1834 commandType: command.type,1831 : ThreadMessageIntake.dispatchCommand(
1835 message: detail ?? "Failed to dispatch orchestration V2 command",1832 ThreadManagementService.withCreationProvenance(command, {
1836 ...(detail === undefined ? {} : { detail }),1833 createdBy: "user",
1837 cause,1834 creationSource:
1838 });1835 "creationSource" in command ? command.creationSource : "web",
1839 }),1836 }),
1840 ),1837 )
1841 {1838 ).pipe(Effect.provide(intakeContext)),
1842 "rpc.aggregate": "orchestrationV2",1839 )
1843 "orchestration_v2.command_id": command.commandId,1840 .pipe(
1844 "orchestration_v2.command_type": command.type,1841 Effect.tap(() => recordClientCommandAnalytics(command)),
1845 "orchestration_v2.thread_id":1842 Effect.map((result) => ({ sequence: result.sequence })),
1846 command.type === "thread.fork" || command.type === "thread.merge_back"1843 Effect.mapError((cause) => {
1847 ? command.targetThreadId1844 const detail = userFacingDispatchErrorMessage(cause);
1848 : command.type === "delegated_task.request" ||1845 return new OrchestrationV2DispatchCommandError({
1849 command.type === "delegated_task.wake-policy" ||1846 commandId: command.commandId,
1850 command.type === "delegated_task.completion-delivery.acknowledge" ||1847 commandType: command.type,
1851 command.type === "delegated_task.completion-delivery.dispose" ||1848 message: detail ?? "Failed to dispatch orchestration V2 command",
1852 command.type === "thread.created.record"1849 ...(detail === undefined ? {} : { detail }),
1853 ? command.parentThreadId1850 cause,
1854 : command.threadId,1851 });
1855 ...(command.type === "thread.fork" || command.type === "thread.merge_back"1852 }),
1856 ? { "orchestration_v2.source_thread_id": command.sourceThreadId }1853 ),
1857 : {}),1854 ),
1858 },
1859 ),1855 ),
1860 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: (input) =>1856 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: (input) =>
1861 observeRpcEffect(1857 readWorkflowScript({ scriptPath: input.scriptPath }),
1862 ORCHESTRATION_V2_WS_METHODS.getWorkflowScript,
1863 readWorkflowScript({ scriptPath: input.scriptPath }),
1864 { "rpc.aggregate": "orchestration" },
1865 ),
1866 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: (input) =>1858 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: (input) =>
1867 observeRpcEffect(1859 threadManagement.getTurnItem(input).pipe(
1868 ORCHESTRATION_V2_WS_METHODS.getTurnItem,1860 Effect.mapError(
1869 threadManagement.getTurnItem(input).pipe(1861 (cause) =>
1870 Effect.mapError(1862 new OrchestrationV2GetThreadProjectionError({
1871 (cause) =>1863 threadId: input.threadId,
1872 new OrchestrationV2GetThreadProjectionError({1864 message: "Failed to load turn item",
1873 threadId: input.threadId,1865 cause,
1874 message: "Failed to load turn item",1866 }),
1875 cause,
1876 }),
1877 ),
1878 ),1867 ),
1879 { "rpc.aggregate": "orchestration" },
1880 ),1868 ),
1881 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: (input) =>1869 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: (input) =>
1882 observeRpcEffect(1870 checkpointDiffQuery.getTurnDiff(input).pipe(
1883 ORCHESTRATION_V2_WS_METHODS.getTurnDiff,1871 Effect.mapError(
1884 checkpointDiffQuery.getTurnDiff(input).pipe(1872 (cause) =>
1885 Effect.mapError(1873 new OrchestrationGetTurnDiffError({
1886 (cause) =>1874 message: "Failed to load turn diff",
1887 new OrchestrationGetTurnDiffError({1875 cause,
1888 message: "Failed to load turn diff",1876 }),
1889 cause,
1890 }),
1891 ),
1892 ),1877 ),
1893 { "rpc.aggregate": "orchestration" },
1894 ),1878 ),
1895 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: (input) =>1879 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: (input) =>
1896 observeRpcEffect(1880 checkpointDiffQuery.getFullThreadDiff(input).pipe(
1897 ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff,1881 Effect.mapError(
1898 checkpointDiffQuery.getFullThreadDiff(input).pipe(1882 (cause) =>
1899 Effect.mapError(1883 new OrchestrationGetFullThreadDiffError({
1900 (cause) =>1884 message: "Failed to load full thread diff",
1901 new OrchestrationGetFullThreadDiffError({1885 cause,
1902 message: "Failed to load full thread diff",1886 }),
1903 cause,
1904 }),
1905 ),
1906 ),1887 ),
1907 { "rpc.aggregate": "orchestration" },
1908 ),1888 ),
1909 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: (input) =>1889 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: (input) =>
1910 observeRpcEffect(1890 threadSearch.search(input).pipe(
1911 ORCHESTRATION_V2_WS_METHODS.searchThreads,1891 Effect.mapError(
1912 threadSearch.search(input).pipe(1892 (cause) =>
1913 Effect.mapError(1893 new OrchestrationSearchThreadsError({
1914 (cause) =>1894 message: "Failed to search threads",
1915 new OrchestrationSearchThreadsError({1895 cause,
1916 message: "Failed to search threads",1896 }),
1917 cause,
1918 }),
1919 ),
1920 ),1897 ),
1921 { "rpc.aggregate": "orchestration" },
1922 ),1898 ),
1923 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: (_input) =>1899 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: (_input) =>
1924 observeRpcEffect(1900 getOrchestrationV2ArchivedShellSnapshot,
1925 ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot,
1926 getOrchestrationV2ArchivedShellSnapshot,
1927 { "rpc.aggregate": "orchestration" },
1928 ),
1929 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: (input) =>1901 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: (input) =>
1930 observeRpcEffect(1902 Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe(
1931 ORCHESTRATION_V2_WS_METHODS.getThreadProjection,1903 Effect.andThen(
1932 // Pre-pagination clients still call this compatibility endpoint.1904 // Pre-pagination clients still call this compatibility endpoint.
1933 // Keep stale clients from materializing an unbounded transcript.1905 // Keep stale clients from materializing an unbounded transcript.
1934 threadManagement1906 threadManagement
1935 .getThreadSnapshotWindow(input.threadId, {1907 .getThreadSnapshotWindow(input.threadId, {
1936 rowLimit: THREAD_HISTORY_SNAPSHOT_ROW_LIMIT,1908 rowLimit: THREAD_HISTORY_SNAPSHOT_ROW_LIMIT,
1937 })1909 })
1938 .pipe(1910 .pipe(
1939 Effect.map((snapshot) =>1911 Effect.map((snapshot) =>
1940 projectThreadProjectionForWire(1912 projectThreadProjectionForWire(
1941 buildBoundedThreadProjection({1913 buildBoundedThreadProjection({
1942 projection: snapshot.projection,1914 projection: snapshot.projection,
1943 snapshotSequence: snapshot.snapshotSequence,1915 snapshotSequence: snapshot.snapshotSequence,
1944 }).projection,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 }),
1945 ),1926 ),
1946 ),1927 ),
1947 Effect.mapError(1928 ),
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 },
1960 ),1929 ),
1961 [ORCHESTRATION_V2_WS_METHODS.launchThread]: (input) =>1930 [ORCHESTRATION_V2_WS_METHODS.launchThread]: (input) =>
1962 observeRpcEffect(1931 Effect.annotateCurrentSpan({
1963 ORCHESTRATION_V2_WS_METHODS.launchThread,1932 "orchestration_v2.command_id": input.commandId,
1964 startup1933 "orchestration_v2.project_id": input.projectId,
1965 .enqueueCommand(1934 }).pipe(
1966 ThreadMessageIntake.launchThread({1935 Effect.andThen(
1967 commandId: input.commandId,1936 startup
1968 ...(input.threadId === undefined ? {} : { threadId: input.threadId }),1937 .enqueueCommand(
1969 ...(input.reuseExistingThread === undefined1938 ThreadMessageIntake.launchThread({
1970 ? {}1939 commandId: input.commandId,
1971 : { reuseExistingThread: input.reuseExistingThread }),1940 ...(input.threadId === undefined ? {} : { threadId: input.threadId }),
1972 projectId: input.projectId,1941 ...(input.reuseExistingThread === undefined
1973 title: input.title,1942 ? {}
1974 ...(input.generateTitle === undefined1943 : { reuseExistingThread: input.reuseExistingThread }),
1975 ? {}1944 projectId: input.projectId,
1976 : { generateTitle: input.generateTitle }),1945 title: input.title,
1977 modelSelection: input.modelSelection,1946 ...(input.generateTitle === undefined
1978 runtimeMode: input.runtimeMode,1947 ? {}
1979 interactionMode: input.interactionMode,1948 : { generateTitle: input.generateTitle }),
1980 workspaceStrategy: input.workspaceStrategy,1949 modelSelection: input.modelSelection,
1981 ...(input.initialMessage === undefined1950 runtimeMode: input.runtimeMode,
1982 ? {}1951 interactionMode: input.interactionMode,
1983 : {1952 workspaceStrategy: input.workspaceStrategy,
1984 initialMessage: {1953 ...(input.initialMessage === undefined
1985 ...(input.initialMessage.messageId === undefined1954 ? {}
1986 ? {}1955 : {
1987 : { messageId: input.initialMessage.messageId }),1956 initialMessage: {
1988 text: input.initialMessage.text,1957 ...(input.initialMessage.messageId === undefined
1989 attachments: input.initialMessage.attachments,1958 ? {}
1990 ...(input.initialMessage.context === undefined1959 : { messageId: input.initialMessage.messageId }),
1991 ? {}1960 text: input.initialMessage.text,
1992 : { context: input.initialMessage.context }),1961 attachments: input.initialMessage.attachments,
1993 },1962 ...(input.initialMessage.context === undefined
1994 }),1963 ? {}
1995 createdBy: "user",1964 : { context: input.initialMessage.context }),
1996 creationSource: input.creationSource ?? "web",1965 },
1997 }).pipe(Effect.provide(intakeContext)),1966 }),
1998 )1967 createdBy: "user",
1999 .pipe(1968 creationSource: input.creationSource ?? "web",
2000 Effect.tap(() =>1969 }).pipe(Effect.provide(intakeContext)),
2001 analytics1970 )
2002 .record("client.thread.started", originProps)1971 .pipe(
2003 .pipe(1972 Effect.tap(() =>
2004 Effect.andThen(1973 analytics
2005 input.initialMessage === undefined1974 .record("client.thread.started", originProps)
2006 ? Effect.void1975 .pipe(
2007 : analytics.record("client.turn.requested", originProps),1976 Effect.andThen(
1977 input.initialMessage === undefined
1978 ? Effect.void
1979 : analytics.record("client.turn.requested", originProps),
1980 ),
1981 Effect.ignore,
2008 ),1982 ),
2009 Effect.ignore,1983 ),
2010 ),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 }),
2011 ),2011 ),
2012 Effect.map((result) => ({2012 ),
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 },
2045 ),2013 ),
2046 [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: (_input) =>2014 [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: (_input) =>
2047 observeRpcStreamEffect(2015 Stream.unwrap(subscribeOrchestrationV2ArchivedShell()),
2048 ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell,
2049 subscribeOrchestrationV2ArchivedShell(),
2050 { "rpc.aggregate": "orchestration" },
2051 ),
2052 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: (input) =>2016 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: (input) =>
2053 observeRpcStreamEffect(2017 Stream.unwrap(subscribeOrchestrationV2Shell(input)),
2054 ORCHESTRATION_V2_WS_METHODS.subscribeShell,
2055 subscribeOrchestrationV2Shell(input),
2056 {
2057 "rpc.aggregate": "orchestrationV2",
2058 },
2059 ),
2060 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: (input) =>2018 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: (input) =>
2061 observeRpcStreamEffect(2019 Stream.unwrap(
2062 ORCHESTRATION_V2_WS_METHODS.subscribeThread,2020 Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe(
2063 subscribeOrchestrationV2Thread(input),2021 Effect.andThen(subscribeOrchestrationV2Thread(input)),
2064 {2022 ),
2065 "rpc.aggregate": "orchestrationV2",
2066 "orchestration_v2.thread_id": input.threadId,
2067 },
2068 ),2023 ),
2069 [WS_METHODS.scheduledTasksList]: (_input) =>2024 [WS_METHODS.scheduledTasksList]: (_input) =>
2070 observeRpcEffect(2025 scheduledTasks.list().pipe(Effect.map(withVisibleWebhookUrls)),
2071 WS_METHODS.scheduledTasksList,
2072 scheduledTasks.list().pipe(Effect.map(withVisibleWebhookUrls)),
2073 { "rpc.aggregate": "scheduledTasks" },
2074 ),
2075 [WS_METHODS.scheduledTasksSubscribe]: (_input) =>2026 [WS_METHODS.scheduledTasksSubscribe]: (_input) =>
2076 observeRpcStream(2027 scheduledTasks.subscribeList().pipe(Stream.map(withVisibleWebhookUrls)),
2077 WS_METHODS.scheduledTasksSubscribe,2028 [WS_METHODS.scheduledTasksUpsert]: (input) => scheduledTasks.upsert(input),
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 }),
2085 [WS_METHODS.scheduledTasksSetEnabled]: (input) =>2029 [WS_METHODS.scheduledTasksSetEnabled]: (input) =>
2086 observeRpcEffect(WS_METHODS.scheduledTasksSetEnabled, scheduledTasks.setEnabled(input), {2030 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2087 "rpc.aggregate": "scheduledTasks",2031 Effect.andThen(scheduledTasks.setEnabled(input)),
2088 "scheduled_task.id": input.id,2032 ),
2089 }),
2090 [WS_METHODS.scheduledTasksDelete]: (input) =>2033 [WS_METHODS.scheduledTasksDelete]: (input) =>
2091 observeRpcEffect(WS_METHODS.scheduledTasksDelete, scheduledTasks.delete(input), {2034 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2092 "rpc.aggregate": "scheduledTasks",2035 Effect.andThen(scheduledTasks.delete(input)),
2093 "scheduled_task.id": input.id,2036 ),
2094 }),
2095 [WS_METHODS.scheduledTasksRunNow]: (input) =>2037 [WS_METHODS.scheduledTasksRunNow]: (input) =>
2096 observeRpcEffect(WS_METHODS.scheduledTasksRunNow, scheduledTasks.runNow(input), {2038 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2097 "rpc.aggregate": "scheduledTasks",2039 Effect.andThen(scheduledTasks.runNow(input)),
2098 "scheduled_task.id": input.id,2040 ),
2099 }),
2100 [WS_METHODS.scheduledTasksRotateWebhookToken]: (input) =>2041 [WS_METHODS.scheduledTasksRotateWebhookToken]: (input) =>
2101 observeRpcEffect(2042 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2102 WS_METHODS.scheduledTasksRotateWebhookToken,2043 Effect.andThen(scheduledTasks.rotateWebhookToken(input)),
2103 scheduledTasks.rotateWebhookToken(input),
2104 { "rpc.aggregate": "scheduledTasks", "scheduled_task.id": input.id },
2105 ),2044 ),
2106 [WS_METHODS.secretsAnswerRequest]: (input) =>2045 [WS_METHODS.secretsAnswerRequest]: (input) =>
2107 observeRpcEffect(WS_METHODS.secretsAnswerRequest, secretRequests.answer(input), {2046 Effect.annotateCurrentSpan({ "orchestration_v2.thread_id": input.threadId }).pipe(
2108 "rpc.aggregate": "secrets",2047 Effect.andThen(secretRequests.answer(input)),
2109 "orchestration_v2.thread_id": input.threadId,2048 ),
2110 }),
2111 [WS_METHODS.scheduledTasksListWebhookDeliveries]: (input) =>2049 [WS_METHODS.scheduledTasksListWebhookDeliveries]: (input) =>
2112 observeRpcEffect(2050 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2113 WS_METHODS.scheduledTasksListWebhookDeliveries,2051 Effect.andThen(scheduledTasks.listWebhookDeliveries(input)),
2114 scheduledTasks.listWebhookDeliveries(input),
2115 { "rpc.aggregate": "scheduledTasks", "scheduled_task.id": input.id },
2116 ),2052 ),
2117 [WS_METHODS.scheduledTasksGetWebhookDelivery]: (input) =>2053 [WS_METHODS.scheduledTasksGetWebhookDelivery]: (input) =>
2118 observeRpcEffect(2054 Effect.annotateCurrentSpan({ "scheduled_task.id": input.id }).pipe(
2119 WS_METHODS.scheduledTasksGetWebhookDelivery,2055 Effect.andThen(scheduledTasks.getWebhookDelivery(input)),
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 },
2134 ),2056 ),
2057 [WS_METHODS.serverProbe]: (_input) => Effect.succeed({}),
2058 [WS_METHODS.serverGetConfig]: (_input) => loadServerConfig({ usageLimitsCommand: false }),
2135 [WS_METHODS.serverSearchAcpRegistry]: (input) =>2059 [WS_METHODS.serverSearchAcpRegistry]: (input) =>
2136 observeRpcEffect(2060 acpRegistryCatalog
2137 WS_METHODS.serverSearchAcpRegistry,2061 .search(input)
2138 acpRegistryCatalog2062 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2139 .search(input)
2140 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2141 { "rpc.aggregate": "server" },
2142 ),
2143 [WS_METHODS.serverPrepareAcpRegistryAgent]: (input) =>2063 [WS_METHODS.serverPrepareAcpRegistryAgent]: (input) =>
2144 observeRpcEffect(2064 Effect.annotateCurrentSpan({ "acp_registry.agent_id": input.agentId }).pipe(
2145 WS_METHODS.serverPrepareAcpRegistryAgent,2065 Effect.andThen(
2146 acpRegistryCatalog2066 acpRegistryCatalog
2147 .prepare(input)2067 .prepare(input)
2148 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),2068 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2149 {2069 ),
2150 "rpc.aggregate": "server",
2151 "acp_registry.agent_id": input.agentId,
2152 },
2153 ),2070 ),
2154 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: (input) =>2071 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: (input) =>
2155 observeRpcEffect(2072 Effect.annotateCurrentSpan({ "acp_registry.agent_id": input.agentId }).pipe(
2156 WS_METHODS.serverUninstallAcpRegistryManagedBinary,2073 Effect.andThen(
2157 acpRegistryCatalog2074 acpRegistryCatalog
2158 .uninstallManagedBinary(input)2075 .uninstallManagedBinary(input)
2159 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),2076 .pipe(Effect.mapError(AcpRegistrySupport.toAcpRegistryOperationError)),
2160 {2077 ),
2161 "rpc.aggregate": "server",
2162 "acp_registry.agent_id": input.agentId,
2163 },
2164 ),2078 ),
2165 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: (input) =>2079 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: (input) =>
2166 observeRpcEffect(2080 Effect.annotateCurrentSpan({ "provider.instance_id": input.instanceId }).pipe(
2167 WS_METHODS.serverAcceptAcpRegistryUrlAuth,2081 Effect.andThen(
2168 acpRegistryRuntimeCoordinator2082 acpRegistryRuntimeCoordinator
2169 .acceptUrlAuthentication(input)2083 .acceptUrlAuthentication(input)
2170 .pipe(Effect.map((accepted) => ({ accepted }))),2084 .pipe(Effect.map((accepted) => ({ accepted }))),
2171 {2085 ),
2172 "rpc.aggregate": "server",
2173 "provider.instance_id": input.instanceId,
2174 },
2175 ),2086 ),
2176 [WS_METHODS.serverListAcpRegistrySessions]: (input) =>2087 [WS_METHODS.serverListAcpRegistrySessions]: (input) =>
2177 observeRpcEffect(2088 Effect.annotateCurrentSpan({
2178 WS_METHODS.serverListAcpRegistrySessions,2089 "provider.instance_id": input.instanceId,
2179 listAcpRegistrySessions(input),2090 "project.id": input.projectId,
2180 {2091 }).pipe(Effect.andThen(listAcpRegistrySessions(input))),
2181 "rpc.aggregate": "server",
2182 "provider.instance_id": input.instanceId,
2183 "project.id": input.projectId,
2184 },
2185 ),
2186 [WS_METHODS.serverImportAcpRegistrySession]: (input) =>2092 [WS_METHODS.serverImportAcpRegistrySession]: (input) =>
2187 observeRpcEffect(2093 Effect.annotateCurrentSpan({
2188 WS_METHODS.serverImportAcpRegistrySession,2094 "provider.instance_id": input.instanceId,
2189 importAcpRegistrySession(input),2095 "project.id": input.projectId,
2190 {2096 }).pipe(Effect.andThen(importAcpRegistrySession(input))),
2191 "rpc.aggregate": "server",
2192 "provider.instance_id": input.instanceId,
2193 "project.id": input.projectId,
2194 },
2195 ),
2196 [WS_METHODS.serverDeleteAcpRegistrySession]: (input) =>2097 [WS_METHODS.serverDeleteAcpRegistrySession]: (input) =>
2197 observeRpcEffect(2098 Effect.annotateCurrentSpan({
2198 WS_METHODS.serverDeleteAcpRegistrySession,2099 "provider.instance_id": input.instanceId,
2199 deleteAcpRegistrySession(input),2100 "project.id": input.projectId,
2200 {2101 }).pipe(Effect.andThen(deleteAcpRegistrySession(input))),
2201 "rpc.aggregate": "server",
2202 "provider.instance_id": input.instanceId,
2203 "project.id": input.projectId,
2204 },
2205 ),
2206 [WS_METHODS.serverListAcpRegistryProviders]: (input) =>2102 [WS_METHODS.serverListAcpRegistryProviders]: (input) =>
2207 observeRpcEffect(2103 Effect.annotateCurrentSpan({
2208 WS_METHODS.serverListAcpRegistryProviders,2104 "provider.instance_id": input.instanceId,
2209 listAcpRegistryProviders(input),2105 "project.id": input.projectId,
2210 {2106 }).pipe(Effect.andThen(listAcpRegistryProviders(input))),
2211 "rpc.aggregate": "server",
2212 "provider.instance_id": input.instanceId,
2213 "project.id": input.projectId,
2214 },
2215 ),
2216 [WS_METHODS.serverSetAcpRegistryProvider]: (input) =>2107 [WS_METHODS.serverSetAcpRegistryProvider]: (input) =>
2217 observeRpcEffect(WS_METHODS.serverSetAcpRegistryProvider, setAcpRegistryProvider(input), {2108 Effect.annotateCurrentSpan({
2218 "rpc.aggregate": "server",
2219 "provider.instance_id": input.instanceId,2109 "provider.instance_id": input.instanceId,
2220 "project.id": input.projectId,2110 "project.id": input.projectId,
2221 }),2111 }).pipe(Effect.andThen(setAcpRegistryProvider(input))),
2222 [WS_METHODS.serverDisableAcpRegistryProvider]: (input) =>2112 [WS_METHODS.serverDisableAcpRegistryProvider]: (input) =>
2223 observeRpcEffect(2113 Effect.annotateCurrentSpan({
2224 WS_METHODS.serverDisableAcpRegistryProvider,2114 "provider.instance_id": input.instanceId,
2225 disableAcpRegistryProvider(input),2115 "project.id": input.projectId,
2226 {2116 }).pipe(Effect.andThen(disableAcpRegistryProvider(input))),
2227 "rpc.aggregate": "server",
2228 "provider.instance_id": input.instanceId,
2229 "project.id": input.projectId,
2230 },
2231 ),
2232 [WS_METHODS.serverLogoutAcpRegistry]: (input) =>2117 [WS_METHODS.serverLogoutAcpRegistry]: (input) =>
2233 observeRpcEffect(2118 Effect.annotateCurrentSpan({ "provider.instance_id": input.instanceId }).pipe(
2234 WS_METHODS.serverLogoutAcpRegistry,2119 Effect.andThen(
2235 Effect.gen(function* () {2120 Effect.gen(function* () {
2236 const { instance, manager } = yield* acpSessionManager(input.instanceId);2121 const { instance, manager } = yield* acpSessionManager(input.instanceId);
2237 const snapshot = yield* instance.snapshot.getSnapshot;2122 const snapshot = yield* instance.snapshot.getSnapshot;
2238 if (snapshot.auth.canLogout !== true) {2123 if (snapshot.auth.canLogout !== true) {
2239 return yield* new AcpRegistryOperationError({2124 return yield* new AcpRegistryOperationError({
2240 reason: "logout_unsupported",2125 reason: "logout_unsupported",
2241 message: "The ACP agent does not advertise logout.",2126 message: "The ACP agent does not advertise logout.",
2242 });2127 });
2243 }2128 }
2244 if (instance.auth) {2129 if (instance.auth) {
2245 yield* providerAuth.logout(input).pipe(2130 yield* providerAuth.logout(input).pipe(
2246 Effect.mapError(2131 Effect.mapError(
2247 (cause) =>2132 (cause) =>
2248 new AcpRegistryOperationError({2133 new AcpRegistryOperationError({
2249 reason: "logout_failed",2134 reason: "logout_failed",
2250 message: "Could not sign out of the ACP agent.",2135 message: "Could not sign out of the ACP agent.",
2251 cause,2136 cause,
2252 }),2137 }),
2253 ),2138 ),
2254 );2139 );
2255 } else {2140 } else {
2256 yield* providerSessionManager.closeInstance(input.instanceId).pipe(2141 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(
2333 Effect.mapError(2142 Effect.mapError(
2334 (error) =>2143 (cause) =>
2335 new ProviderSetupError({2144 new AcpRegistryOperationError({
2336 instanceId: instance.instanceId,2145 reason: "logout_failed",
2337 operation: "refresh-models",2146 message: "Could not stop live sessions before ACP logout.",
2338 detail: error.detail,2147 cause,
2339 }),2148 }),
2340 ),2149 ),
2341 );2150 );
2342 providers = yield* providerRegistry.refreshInstance(instance.instanceId);2151 yield* manager.logout(config.cwd);
2343 }2152 }
2344 }2153 yield* providerRegistry.refreshInstance(input.instanceId);
2345 return { providers };2154 return { loggedOut: true } as const;
2346 }),2155 }),
2347 { "rpc.aggregate": "server" },2156 ),
2348 ),2157 ),
2349 [WS_METHODS.providerUploadFeedback]: (input) =>2158 [WS_METHODS.serverRefreshProviders]: (input) =>
2350 observeRpcEffect(2159 Effect.gen(function* () {
2351 WS_METHODS.providerUploadFeedback,2160 // Only explicit catalog refreshes bypass T3's caches. Workspace
2352 Effect.gen(function* () {2161 // discovery and background status checks retain their timers.
2353 const projection = yield* threadManagement.getThreadRecords(input.threadId, [2162 if (input.refreshModels) {
2354 "providerThreads",2163 yield* modelManifest.forceRefresh;
2355 ]);2164 const instances = yield* providerInstances.listInstances;
2356 const providerThread =2165 yield* Effect.forEach(
2357 projection.providerThreads.find(2166 instances.filter(
2358 (candidate) => candidate.id === projection.thread.activeProviderThreadId,2167 (instance) =>
2359 ) ?? projection.providerThreads.at(-1);2168 input.instanceId === undefined || input.instanceId === instance.instanceId,
2360 const providerSessionId = providerThread?.providerSessionId ?? null;2169 ),
2361 if (providerThread === undefined || providerSessionId === null) {2170 (instance) =>
2362 return yield* Effect.fail(2171 Effect.gen(function* () {
2363 new ProviderUploadFeedbackError({2172 yield* instance.invalidateCaches ?? Effect.void;
2364 threadId: input.threadId,2173 const maintenance = yield* instance.snapshot.resolveMaintenance({
2365 cause: "No provider session has run in this thread yet.",2174 fresh: true,
2175 });
2176 if (maintenance.packageName)
2177 providerVersionCache.delete(maintenance.packageName);
2366 }),2178 }),
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 ),
2367 );2221 );
2222 providers = yield* providerRegistry.refreshInstance(instance.instanceId);
2368 }2223 }
2369 const runtime = Option.getOrNull(yield* providerSessionsV2.get(providerSessionId));2224 }
2370 if (runtime === null) {2225 return { providers };
2371 return yield* Effect.fail(2226 }),
2372 new ProviderUploadFeedbackError({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({
2373 threadId: input.threadId,2271 threadId: input.threadId,
2374 cause: "The provider session is no longer running. Send a message first.",2272 cause,
2375 }),2273 }),
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 ),
2399 ),2274 ),
2400 { "rpc.aggregate": "provider" },
2401 ),2275 ),
2402 [WS_METHODS.serverUpdateProvider]: (input) =>2276 [WS_METHODS.serverUpdateProvider]: (input) =>
2403 observeRpcEffect(2277 providerMaintenanceRunner.updateProvider(input),
2404 WS_METHODS.serverUpdateProvider,
2405 providerMaintenanceRunner.updateProvider(input),
2406 {
2407 "rpc.aggregate": "server",
2408 },
2409 ),
2410 [WS_METHODS.providerConsumeResetCredit]: (input) =>2278 [WS_METHODS.providerConsumeResetCredit]: (input) =>
2411 observeRpcEffect(2279 Effect.gen(function* () {
2412 WS_METHODS.providerConsumeResetCredit,2280 if ("sourceId" in input) return yield* usageLimitSources.consumeResetCredit(input);
2413 Effect.gen(function* () {2281 const instance = yield* providerInstances.getInstance(input.instanceId);
2414 if ("sourceId" in input) return yield* usageLimitSources.consumeResetCredit(input);2282 // A disabled instance must not spend anything on its account.
2415 const instance = yield* providerInstances.getInstance(input.instanceId);2283 if (instance === undefined || !instance.enabled) {
2416 // A disabled instance must not spend anything on its account.2284 return yield* new ProviderSetupError({
2417 if (instance === undefined || !instance.enabled) {2285 instanceId: input.instanceId,
2418 return yield* new ProviderSetupError({2286 operation: "consume-reset-credit",
2419 instanceId: input.instanceId,2287 detail: instance ? "This provider is disabled." : "Provider instance not found.",
2420 operation: "consume-reset-credit",2288 });
2421 detail: instance ? "This provider is disabled." : "Provider instance not found.",2289 }
2422 });2290 if (instance.consumeResetCredit === undefined) {
2423 }2291 return yield* new ProviderSetupError({
2424 if (instance.consumeResetCredit === undefined) {2292 instanceId: input.instanceId,
2425 return yield* new ProviderSetupError({2293 operation: "consume-reset-credit",
2426 instanceId: input.instanceId,2294 detail: "This provider does not bank reset credits.",
2427 operation: "consume-reset-credit",2295 });
2428 detail: "This provider does not bank reset credits.",2296 }
2429 });2297 const outcome = yield* instance.consumeResetCredit().pipe(
2430 }2298 Effect.mapError(
2431 const outcome = yield* instance.consumeResetCredit().pipe(2299 (error) =>
2432 Effect.mapError(2300 new ProviderSetupError({
2433 (error) =>2301 instanceId: input.instanceId,
2434 new ProviderSetupError({2302 operation: "consume-reset-credit",
2435 instanceId: input.instanceId,2303 detail: error.detail,
2436 operation: "consume-reset-credit",2304 cause: error,
2437 detail: error.detail,2305 }),
2438 cause: error,2306 ),
2439 }),2307 );
2440 ),2308 return { outcome };
2441 );2309 }),
2442 return { outcome };2310 [WS_METHODS.providerAuthStart]: (input) => providerAuth.start(input, currentSessionId),
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 ),
2452 [WS_METHODS.providerAuthRespond]: (input) =>2311 [WS_METHODS.providerAuthRespond]: (input) =>
2453 observeRpcEffect(2312 Effect.annotateCurrentSpan({ instanceId: input.instanceId }).pipe(
2454 WS_METHODS.providerAuthRespond,2313 Effect.andThen(providerAuth.respond(input, currentSessionId)),
2455 providerAuth.respond(input, currentSessionId),
2456 {
2457 "rpc.aggregate": "provider",
2458 instanceId: input.instanceId,
2459 },
2460 ),2314 ),
2461 [WS_METHODS.providerAuthComplete]: (input) =>2315 [WS_METHODS.providerAuthComplete]: (input) =>
2462 observeRpcEffect(2316 providerAuth.complete(input, currentSessionId),
2463 WS_METHODS.providerAuthComplete,
2464 providerAuth.complete(input, currentSessionId),
2465 { "rpc.aggregate": "provider" },
2466 ),
2467 [WS_METHODS.chatGptReconnectProfile]: (input) => providerAuth.reconnectProfile(input),2317 [WS_METHODS.chatGptReconnectProfile]: (input) => providerAuth.reconnectProfile(input),
2468 [WS_METHODS.chatGptImportProfile]: (input) => providerAuth.importProfile(input),2318 [WS_METHODS.chatGptImportProfile]: (input) => providerAuth.importProfile(input),
2469 [WS_METHODS.chatGptHandoffSubscribe]: (input) =>2319 [WS_METHODS.chatGptHandoffSubscribe]: (input) =>
2470 subscribeChatGptHandoff(input, currentSessionId),2320 subscribeChatGptHandoff(input, currentSessionId),
2471 [WS_METHODS.codexAuthCallbackSubscribe]: (input) =>2321 [WS_METHODS.codexAuthCallbackSubscribe]: (input) => subscribeCodexAuthCallback(input),
2472 observeRpcStream(2322 [WS_METHODS.providerAuthCancel]: (input) => providerAuth.cancel(input, currentSessionId),
2473 WS_METHODS.codexAuthCallbackSubscribe,2323 [WS_METHODS.providerAuthLogout]: (input) => providerAuth.logout(input),
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 }),
2489 [WS_METHODS.providerAuthSubscribe]: (input) =>2324 [WS_METHODS.providerAuthSubscribe]: (input) =>
2490 observeRpcStream(2325 providerAuth.subscribe(input, currentSessionId),
2491 WS_METHODS.providerAuthSubscribe,2326 [WS_METHODS.providerInstallStart]: (input) => providerInstallation.start(input),
2492 providerAuth.subscribe(input, currentSessionId),2327 [WS_METHODS.providerInstallCancel]: (input) => providerInstallation.cancel(input),
2493 { "rpc.aggregate": "provider" },2328 [WS_METHODS.providerInstallSubscribe]: (input) => providerInstallation.subscribe(input),
2494 ),2329 [WS_METHODS.providerInstallRemove]: (input) => providerInstallation.remove(input),
2495 [WS_METHODS.providerInstallStart]: (input) =>2330 [WS_METHODS.serverUpdateServer]: (input) => serverSelfUpdate.update(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 }),
2517 [WS_METHODS.serverUpdateServerWithProgress]: (input) =>2331 [WS_METHODS.serverUpdateServerWithProgress]: (input) =>
2518 observeRpcStream(2332 Stream.callback<ServerSelfUpdateProgressEvent, ServerSelfUpdateError>((queue) =>
2519 WS_METHODS.serverUpdateServerWithProgress,2333 serverSelfUpdate
2520 Stream.callback<ServerSelfUpdateProgressEvent, ServerSelfUpdateError>((queue) =>2334 .update(input, (stage) =>
2521 serverSelfUpdate2335 Queue.offer(queue, {
2522 .update(input, (stage) =>2336 type: "progress",
2337 stage,
2338 }).pipe(Effect.asVoid),
2339 )
2340 .pipe(
2341 Effect.flatMap((result) =>
2523 Queue.offer(queue, {2342 Queue.offer(queue, {
2524 type: "progress",2343 type: "complete",
2525 stage,2344 result,
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),
2537 }),2345 }),
2538 Effect.andThen(Queue.end(queue)),
2539 Effect.forkScoped,
2540 ),2346 ),
2541 ),2347 Effect.catchTags({
2542 { "rpc.aggregate": "server" },2348 ServerSelfUpdateError: (error) => Queue.fail(queue, error),
2349 }),
2350 Effect.andThen(Queue.end(queue)),
2351 Effect.forkScoped,
2352 ),
2543 ),2353 ),
2544 [WS_METHODS.serverCommitDesktopUpdate]: (input) =>2354 [WS_METHODS.serverCommitDesktopUpdate]: (input) =>
2545 observeRpcEffect(2355 serverSelfUpdate.commitDesktopUpdate(input.requestId),
2546 WS_METHODS.serverCommitDesktopUpdate,
2547 serverSelfUpdate.commitDesktopUpdate(input.requestId),
2548 { "rpc.aggregate": "server" },
2549 ),
2550 [WS_METHODS.serverUpsertKeybinding]: (rule) =>2356 [WS_METHODS.serverUpsertKeybinding]: (rule) =>
2551 observeRpcEffect(2357 Effect.gen(function* () {
2552 WS_METHODS.serverUpsertKeybinding,2358 const keybindingsConfig = yield* keybindings.upsertKeybindingRule(rule);
2553 Effect.gen(function* () {2359 return { keybindings: keybindingsConfig, issues: [] };
2554 const keybindingsConfig = yield* keybindings.upsertKeybindingRule(rule);2360 }),
2555 return { keybindings: keybindingsConfig, issues: [] };
2556 }),
2557 { "rpc.aggregate": "server" },
2558 ),
2559 [WS_METHODS.serverRemoveKeybinding]: (rule) =>2361 [WS_METHODS.serverRemoveKeybinding]: (rule) =>
2560 observeRpcEffect(2362 Effect.gen(function* () {
2561 WS_METHODS.serverRemoveKeybinding,2363 const keybindingsConfig = yield* keybindings.removeKeybindingRule(rule);
2562 Effect.gen(function* () {2364 return { keybindings: keybindingsConfig, issues: [] };
2563 const keybindingsConfig = yield* keybindings.removeKeybindingRule(rule);2365 }),
2564 return { keybindings: keybindingsConfig, issues: [] };
2565 }),
2566 { "rpc.aggregate": "server" },
2567 ),
2568 [WS_METHODS.serverGetSettings]: (_input) =>2366 [WS_METHODS.serverGetSettings]: (_input) =>
2569 observeRpcEffect(2367 serverSettings.getSettings.pipe(Effect.map(ServerSettings.redactServerSettingsForClient)),
2570 WS_METHODS.serverGetSettings,
2571 serverSettings.getSettings.pipe(
2572 Effect.map(ServerSettings.redactServerSettingsForClient),
2573 ),
2574 {
2575 "rpc.aggregate": "server",
2576 },
2577 ),
2578 [WS_METHODS.serverUpdateSettings]: ({ patch, providerInstanceMutation }) =>2368 [WS_METHODS.serverUpdateSettings]: ({ patch, providerInstanceMutation }) =>
2579 observeRpcEffect(2369 Effect.gen(function* () {
2580 WS_METHODS.serverUpdateSettings,2370 const deviceHosts = patch.deviceHosts
2581 Effect.gen(function* () {2371 ? yield* remoteSshDeviceHosts(patch.deviceHosts).pipe(
2582 const deviceHosts = patch.deviceHosts2372 Effect.provide(deviceHostContext),
2583 ? yield* remoteSshDeviceHosts(patch.deviceHosts).pipe(2373 )
2584 Effect.provide(deviceHostContext),2374 : undefined;
2585 )2375 const nextPatch = { ...patch, ...(deviceHosts ? { deviceHosts } : {}) };
2586 : undefined;2376 const settings = yield* providerInstanceMutation === undefined
2587 const nextPatch = { ...patch, ...(deviceHosts ? { deviceHosts } : {}) };2377 ? serverSettings.updateSettings(nextPatch)
2588 const settings = yield* providerInstanceMutation === undefined2378 : serverSettings.updateProviderInstance(providerInstanceMutation, nextPatch);
2589 ? serverSettings.updateSettings(nextPatch)2379 return ServerSettings.redactServerSettingsForClient(settings);
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",
2619 }),2380 }),
2620 [WS_METHODS.serverGetHostResources]: (_input) =>2381 [WS_METHODS.serverDiscoverSourceControl]: (_input) => sourceControlDiscovery.discover,
2621 observeRpcEffect(WS_METHODS.serverGetHostResources, hostResources.read, {2382 [WS_METHODS.serverGetTraceDiagnostics]: (_input) =>
2622 "rpc.aggregate": "server",2383 TraceDiagnostics.readTraceDiagnostics({
2384 traceFilePath: config.serverTracePath,
2385 maxFiles: config.traceMaxFiles,
2623 }),2386 }),
2387 [WS_METHODS.serverGetProcessDiagnostics]: (_input) => processDiagnostics.read,
2388 [WS_METHODS.serverGetHostResources]: (_input) => hostResources.read,
2624 [WS_METHODS.serverGetProcessResourceHistory]: (input) =>2389 [WS_METHODS.serverGetProcessResourceHistory]: (input) =>
2625 observeRpcEffect(2390 processResourceMonitor.readHistory(input),
2626 WS_METHODS.serverGetProcessResourceHistory,
2627 processResourceMonitor.readHistory(input),
2628 {
2629 "rpc.aggregate": "server",
2630 },
2631 ),
2632 [WS_METHODS.serverGetResourceTelemetryHistory]: (input) =>2391 [WS_METHODS.serverGetResourceTelemetryHistory]: (input) =>
2633 observeRpcEffect(2392 resourceTelemetry.readHistory(input),
2634 WS_METHODS.serverGetResourceTelemetryHistory,2393 [WS_METHODS.serverGetUsageSummary]: (input) => usage.readSummary(input),
2635 resourceTelemetry.readHistory(input),2394 [WS_METHODS.serverRefreshUsageRates]: (_input) => usage.refreshRates,
2636 {2395 [WS_METHODS.serverRetryResourceTelemetry]: (_input) => resourceTelemetry.retry,
2637 "rpc.aggregate": "server",2396 [WS_METHODS.serverSignalProcess]: (input) => processDiagnostics.signal(input),
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 }),
2656 [WS_METHODS.serverReportClientActivity]: (input, metadata) =>2397 [WS_METHODS.serverReportClientActivity]: (input, metadata) =>
2657 Ref.update(rpcClientIds, (clientIds) => {2398 Ref.update(rpcClientIds, (clientIds) => {
2658 const next = new Set(clientIds);2399 const next = new Set(clientIds);
...@@ -2660,583 +2401,303 @@ const layerWsRpc = (...@@ -2660,583 +2401,303 @@ const layerWsRpc = (
2660 return next;2401 return next;
2661 }).pipe(2402 }).pipe(
2662 Effect.andThen(2403 Effect.andThen(
2663 observeRpcEffect(2404 backgroundPolicy.reportClientActivity(
2664 WS_METHODS.serverReportClientActivity,2405 currentSessionId,
2665 backgroundPolicy.reportClientActivity(2406 RpcClientId.make(metadata.client.id),
2666 currentSessionId,2407 input,
2667 RpcClientId.make(metadata.client.id),
2668 input,
2669 ),
2670 { "rpc.aggregate": "server" },
2671 ),2408 ),
2672 ),2409 ),
2673 ),2410 ),
2674 [WS_METHODS.serverReportHostPowerState]: (input) =>2411 [WS_METHODS.serverReportHostPowerState]: (input) =>
2675 observeRpcEffect(2412 backgroundPolicy.reportHostPowerState(input),
2676 WS_METHODS.serverReportHostPowerState,2413 [WS_METHODS.serverGetBackgroundPolicy]: (_input) => backgroundPolicy.snapshot,
2677 backgroundPolicy.reportHostPowerState(input),2414 [WS_METHODS.cloudGetRelayClientStatus]: (_input) => relayClient.resolve,
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 }),
2688 [WS_METHODS.cloudInstallRelayClient]: (_input) =>2415 [WS_METHODS.cloudInstallRelayClient]: (_input) =>
2689 observeRpcStream(2416 Stream.callback<RelayClientInstallProgressEvent, RelayClientInstallFailedError>((queue) =>
2690 WS_METHODS.cloudInstallRelayClient,2417 relayClient
2691 Stream.callback<RelayClientInstallProgressEvent, RelayClientInstallFailedError>(2418 .installWithProgress((event) => Queue.offer(queue, event).pipe(Effect.asVoid))
2692 (queue) =>2419 .pipe(
2693 relayClient2420 Effect.flatMap((status) =>
2694 .installWithProgress((event) => Queue.offer(queue, event).pipe(Effect.asVoid))2421 Queue.offer(queue, {
2695 .pipe(2422 type: "complete",
2696 Effect.flatMap((status) =>2423 status,
2697 Queue.offer(queue, {2424 }),
2698 type: "complete",2425 ),
2699 status,2426 Effect.catchTags({
2427 RelayClientInstallError: (error) =>
2428 Queue.fail(
2429 queue,
2430 new RelayClientInstallFailedError({
2431 reason: error.reason,
2432 message: error.message,
2700 }),2433 }),
2701 ),2434 ),
2702 Effect.catchTags({2435 }),
2703 RelayClientInstallError: (error) =>2436 Effect.andThen(Queue.end(queue)),
2704 Queue.fail(2437 Effect.forkScoped,
2705 queue,2438 ),
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 },
2733 ),2439 ),
2734 [WS_METHODS.pullRequestsRouting]: (input) =>2440 [WS_METHODS.pullRequestsList]: (input) => pullRequests.list(input),
2735 observeRpcEffect(WS_METHODS.pullRequestsRouting, pullRequests.routing(input), {2441 [WS_METHODS.pullRequestsListStats]: (input) => pullRequests.listStats(input),
2736 "rpc.aggregate": "pull-requests",2442 [WS_METHODS.pullRequestsRoutingIdentity]: (input) => pullRequests.routingIdentity(input),
2737 }),2443 [WS_METHODS.pullRequestsRouting]: (input) => pullRequests.routing(input),
2738 [WS_METHODS.pullRequestsSummary]: (input) =>2444 [WS_METHODS.pullRequestsSummary]: (input) =>
2739 observeRpcEffect(2445 withPullRequestViewer(input, pullRequests.summary(input)),
2740 WS_METHODS.pullRequestsSummary,
2741 withPullRequestViewer(input, pullRequests.summary(input)),
2742 {
2743 "rpc.aggregate": "pull-requests",
2744 },
2745 ),
2746 [WS_METHODS.pullRequestsStack]: (input) =>2446 [WS_METHODS.pullRequestsStack]: (input) =>
2747 observeRpcEffect(2447 withPullRequestViewer(input, pullRequests.stack(input)),
2748 WS_METHODS.pullRequestsStack,
2749 withPullRequestViewer(input, pullRequests.stack(input)),
2750 {
2751 "rpc.aggregate": "pull-requests",
2752 },
2753 ),
2754 [WS_METHODS.pullRequestsLinkedThreads]: (input) =>2448 [WS_METHODS.pullRequestsLinkedThreads]: (input) =>
2755 observeRpcEffect(2449 resolvePullRequestSyncKey(input).pipe(
2756 WS_METHODS.pullRequestsLinkedThreads,2450 Effect.flatMap((key) =>
2757 resolvePullRequestSyncKey(input).pipe(2451 key === null
2758 Effect.flatMap((key) =>2452 ? Effect.succeed({ threads: [] })
2759 key === null2453 : listLinkedPullRequestThreads(key).pipe(
2760 ? Effect.succeed({ threads: [] })2454 Effect.provideService(SqlClient.SqlClient, sql),
2761 : listLinkedPullRequestThreads(key).pipe(2455 ),
2762 Effect.provideService(SqlClient.SqlClient, sql),
2763 ),
2764 ),
2765 ),2456 ),
2766 { "rpc.aggregate": "pull-requests" },
2767 ),2457 ),
2768 [WS_METHODS.pullRequestsDetail]: (input) =>2458 [WS_METHODS.pullRequestsDetail]: (input) =>
2769 observeRpcEffect(2459 withPullRequestViewer(input, pullRequests.detail(input)),
2770 WS_METHODS.pullRequestsDetail,
2771 withPullRequestViewer(input, pullRequests.detail(input)),
2772 {
2773 "rpc.aggregate": "pull-requests",
2774 },
2775 ),
2776 [WS_METHODS.pullRequestsPreview]: (input) =>2460 [WS_METHODS.pullRequestsPreview]: (input) =>
2777 observeRpcEffect(2461 withPullRequestViewer(input, pullRequests.preview(input)),
2778 WS_METHODS.pullRequestsPreview,
2779 withPullRequestViewer(input, pullRequests.preview(input)),
2780 { "rpc.aggregate": "pull-requests" },
2781 ),
2782 [WS_METHODS.pullRequestsChecks]: (input) =>2462 [WS_METHODS.pullRequestsChecks]: (input) =>
2783 observeRpcEffect(2463 withPullRequestViewer(input, pullRequests.checks(input)),
2784 WS_METHODS.pullRequestsChecks,
2785 withPullRequestViewer(input, pullRequests.checks(input)),
2786 { "rpc.aggregate": "pull-requests" },
2787 ),
2788 [WS_METHODS.pullRequestsActivity]: (input) =>2464 [WS_METHODS.pullRequestsActivity]: (input) =>
2789 observeRpcEffect(2465 withPullRequestViewer(input, pullRequests.activity(input)),
2790 WS_METHODS.pullRequestsActivity,
2791 withPullRequestViewer(input, pullRequests.activity(input)),
2792 {
2793 "rpc.aggregate": "pull-requests",
2794 },
2795 ),
2796 [WS_METHODS.pullRequestsThreadComments]: (input) =>2466 [WS_METHODS.pullRequestsThreadComments]: (input) =>
2797 observeRpcEffect(2467 withPullRequestViewer(input, pullRequests.threadComments(input)),
2798 WS_METHODS.pullRequestsThreadComments,
2799 withPullRequestViewer(input, pullRequests.threadComments(input)),
2800 {
2801 "rpc.aggregate": "pull-requests",
2802 },
2803 ),
2804 [WS_METHODS.pullRequestsDiffFileContents]: (input) =>2468 [WS_METHODS.pullRequestsDiffFileContents]: (input) =>
2805 observeRpcEffect(2469 withPullRequestViewer(input, pullRequests.diffFileContents(input)),
2806 WS_METHODS.pullRequestsDiffFileContents,
2807 withPullRequestViewer(input, pullRequests.diffFileContents(input)),
2808 { "rpc.aggregate": "pull-requests" },
2809 ),
2810 [WS_METHODS.pullRequestsFilesViewed]: (input) =>2470 [WS_METHODS.pullRequestsFilesViewed]: (input) =>
2811 observeRpcEffect(2471 withPullRequestViewer(input, pullRequests.filesViewed(input)),
2812 WS_METHODS.pullRequestsFilesViewed,
2813 withPullRequestViewer(input, pullRequests.filesViewed(input)),
2814 { "rpc.aggregate": "pull-requests" },
2815 ),
2816 [WS_METHODS.pullRequestsSetFilesViewed]: (input) =>2472 [WS_METHODS.pullRequestsSetFilesViewed]: (input) =>
2817 observeRpcEffect(2473 withPullRequestViewer(input, pullRequests.setFilesViewed(input)),
2818 WS_METHODS.pullRequestsSetFilesViewed,
2819 withPullRequestViewer(input, pullRequests.setFilesViewed(input)),
2820 { "rpc.aggregate": "pull-requests" },
2821 ),
2822 [WS_METHODS.pullRequestsRunAction]: (input) =>2474 [WS_METHODS.pullRequestsRunAction]: (input) =>
2823 observeRpcEffect(2475 withPullRequestViewer(input, pullRequests.runAction(input)).pipe(
2824 WS_METHODS.pullRequestsRunAction,2476 Effect.tap(() =>
2825 withPullRequestViewer(input, pullRequests.runAction(input)).pipe(2477 resolvePullRequestSyncKey(input).pipe(
2826 Effect.tap(() =>2478 Effect.flatMap((key) =>
2827 resolvePullRequestSyncKey(input).pipe(2479 key === null ? Effect.void : pullRequestSync.requestSync(key),
2828 Effect.flatMap((key) =>
2829 key === null ? Effect.void : pullRequestSync.requestSync(key),
2830 ),
2831 ),2480 ),
2832 ),2481 ),
2833 ),2482 ),
2834 { "rpc.aggregate": "pull-requests" },
2835 ),2483 ),
2836 [WS_METHODS.pullRequestsUpdate]: (input) =>2484 [WS_METHODS.pullRequestsUpdate]: (input) =>
2837 observeRpcEffect(2485 withPullRequestViewer(input, pullRequests.update(input)),
2838 WS_METHODS.pullRequestsUpdate,
2839 withPullRequestViewer(input, pullRequests.update(input)),
2840 {
2841 "rpc.aggregate": "pull-requests",
2842 },
2843 ),
2844 [WS_METHODS.pullRequestsComment]: (input) =>2486 [WS_METHODS.pullRequestsComment]: (input) =>
2845 observeRpcEffect(2487 withPullRequestViewer(input, pullRequests.comment(input)),
2846 WS_METHODS.pullRequestsComment,
2847 withPullRequestViewer(input, pullRequests.comment(input)),
2848 {
2849 "rpc.aggregate": "pull-requests",
2850 },
2851 ),
2852 [WS_METHODS.pullRequestsUpdateComment]: (input) =>2488 [WS_METHODS.pullRequestsUpdateComment]: (input) =>
2853 observeRpcEffect(2489 withPullRequestViewer(input, pullRequests.updateComment(input)),
2854 WS_METHODS.pullRequestsUpdateComment,
2855 withPullRequestViewer(input, pullRequests.updateComment(input)),
2856 {
2857 "rpc.aggregate": "pull-requests",
2858 },
2859 ),
2860 [WS_METHODS.pullRequestsSubmitReview]: (input) =>2490 [WS_METHODS.pullRequestsSubmitReview]: (input) =>
2861 observeRpcEffect(2491 withPullRequestViewer(input, pullRequests.submitReview(input)),
2862 WS_METHODS.pullRequestsSubmitReview,
2863 withPullRequestViewer(input, pullRequests.submitReview(input)),
2864 {
2865 "rpc.aggregate": "pull-requests",
2866 },
2867 ),
2868 [WS_METHODS.pullRequestsReplyToThread]: (input) =>2492 [WS_METHODS.pullRequestsReplyToThread]: (input) =>
2869 observeRpcEffect(2493 withPullRequestViewer(input, pullRequests.replyToThread(input)),
2870 WS_METHODS.pullRequestsReplyToThread,
2871 withPullRequestViewer(input, pullRequests.replyToThread(input)),
2872 { "rpc.aggregate": "pull-requests" },
2873 ),
2874 [WS_METHODS.pullRequestsSetThreadResolution]: (input) =>2494 [WS_METHODS.pullRequestsSetThreadResolution]: (input) =>
2875 observeRpcEffect(2495 withPullRequestViewer(input, pullRequests.setThreadResolution(input)),
2876 WS_METHODS.pullRequestsSetThreadResolution,
2877 withPullRequestViewer(input, pullRequests.setThreadResolution(input)),
2878 { "rpc.aggregate": "pull-requests" },
2879 ),
2880 [WS_METHODS.pullRequestsSetReaction]: (input) =>2496 [WS_METHODS.pullRequestsSetReaction]: (input) =>
2881 observeRpcEffect(2497 withPullRequestViewer(input, pullRequests.setReaction(input)),
2882 WS_METHODS.pullRequestsSetReaction,
2883 withPullRequestViewer(input, pullRequests.setReaction(input)),
2884 {
2885 "rpc.aggregate": "pull-requests",
2886 },
2887 ),
2888 [WS_METHODS.pullRequestsInvalidate]: (input) =>2498 [WS_METHODS.pullRequestsInvalidate]: (input) =>
2889 observeRpcEffect(2499 pullRequests.invalidate(input, { notifyReaders: true }).pipe(
2890 WS_METHODS.pullRequestsInvalidate,2500 // A reader asking for fresh host state also wants the thread badges it feeds to
2891 pullRequests.invalidate(input, { notifyReaders: true }).pipe(2501 // catch up, including a merged link the sweep would otherwise never revisit.
2892 // A reader asking for fresh host state also wants the thread badges it feeds to2502 Effect.andThen(
2893 // catch up, including a merged link the sweep would otherwise never revisit.2503 input.reference === undefined || input.filesViewedOnly === true
2894 Effect.andThen(2504 ? Effect.void
2895 input.reference === undefined || input.filesViewedOnly === true2505 : resolvePullRequestSyncKey(input.reference).pipe(
2896 ? Effect.void2506 Effect.flatMap((key) =>
2897 : resolvePullRequestSyncKey(input.reference).pipe(2507 key === null ? Effect.void : pullRequestSync.requestSync(key),
2898 Effect.flatMap((key) =>
2899 key === null ? Effect.void : pullRequestSync.requestSync(key),
2900 ),
2901 ),2508 ),
2902 ),2509 ),
2903 ),2510 ),
2904 { "rpc.aggregate": "pull-requests" },
2905 ),
2906 [WS_METHODS.pullRequestsSubscribeRefreshes]: () =>
2907 observeRpcStream(
2908 WS_METHODS.pullRequestsSubscribeRefreshes,
2909 pullRequests.subscribeRefreshes,
2910 { "rpc.aggregate": "pull-requests" },
2911 ),2511 ),
2512 [WS_METHODS.pullRequestsSubscribeRefreshes]: () => pullRequests.subscribeRefreshes,
2912 [WS_METHODS.pullRequestsReviewerCandidates]: (input) =>2513 [WS_METHODS.pullRequestsReviewerCandidates]: (input) =>
2913 observeRpcEffect(2514 withPullRequestViewer(input, pullRequests.reviewerCandidates(input)),
2914 WS_METHODS.pullRequestsReviewerCandidates,
2915 withPullRequestViewer(input, pullRequests.reviewerCandidates(input)),
2916 { "rpc.aggregate": "pull-requests" },
2917 ),
2918 [WS_METHODS.pullRequestsRequestReviewers]: (input) =>2515 [WS_METHODS.pullRequestsRequestReviewers]: (input) =>
2919 observeRpcEffect(2516 withPullRequestViewer(input, pullRequests.requestReviewers(input)),
2920 WS_METHODS.pullRequestsRequestReviewers,
2921 withPullRequestViewer(input, pullRequests.requestReviewers(input)),
2922 { "rpc.aggregate": "pull-requests" },
2923 ),
2924 [WS_METHODS.pullRequestsLabelCandidates]: (input) =>2517 [WS_METHODS.pullRequestsLabelCandidates]: (input) =>
2925 observeRpcEffect(2518 withPullRequestViewer(input, pullRequests.labelCandidates(input)),
2926 WS_METHODS.pullRequestsLabelCandidates,
2927 withPullRequestViewer(input, pullRequests.labelCandidates(input)),
2928 { "rpc.aggregate": "pull-requests" },
2929 ),
2930 [WS_METHODS.pullRequestsSetLabels]: (input) =>2519 [WS_METHODS.pullRequestsSetLabels]: (input) =>
2931 observeRpcEffect(2520 withPullRequestViewer(input, pullRequests.setLabels(input)),
2932 WS_METHODS.pullRequestsSetLabels,
2933 withPullRequestViewer(input, pullRequests.setLabels(input)),
2934 {
2935 "rpc.aggregate": "pull-requests",
2936 },
2937 ),
2938 [WS_METHODS.sourceControlLookupRepository]: (input) =>2521 [WS_METHODS.sourceControlLookupRepository]: (input) =>
2939 observeRpcEffect(2522 sourceControlRepositories.lookupRepository(input),
2940 WS_METHODS.sourceControlLookupRepository,
2941 sourceControlRepositories.lookupRepository(input),
2942 {
2943 "rpc.aggregate": "source-control",
2944 },
2945 ),
2946 [WS_METHODS.sourceControlCloneRepository]: (input) =>2523 [WS_METHODS.sourceControlCloneRepository]: (input) =>
2947 observeRpcEffect(2524 sourceControlRepositories.cloneRepository(input),
2948 WS_METHODS.sourceControlCloneRepository,
2949 sourceControlRepositories.cloneRepository(input),
2950 {
2951 "rpc.aggregate": "source-control",
2952 },
2953 ),
2954 [WS_METHODS.projectCloneStart]: (input) =>2525 [WS_METHODS.projectCloneStart]: (input) =>
2955 observeRpcEffect(2526 projectCloneTracker.start(input, {
2956 WS_METHODS.projectCloneStart,2527 createProject: (project) =>
2957 projectCloneTracker.start(input, {2528 projectService
2958 createProject: (project) =>2529 .create({
2959 projectService2530 commandId: CommandId.make(`project-clone-create:${project.projectId}`),
2960 .create({2531 projectId: project.projectId,
2961 commandId: CommandId.make(`project-clone-create:${project.projectId}`),2532 title: project.title,
2962 projectId: project.projectId,2533 workspaceRoot: project.workspaceRoot,
2963 title: project.title,2534 createWorkspaceRootIfMissing: true,
2964 workspaceRoot: project.workspaceRoot,2535 })
2965 createWorkspaceRootIfMissing: true,2536 .pipe(
2966 })2537 Effect.asVoid,
2967 .pipe(2538 Effect.mapError(
2968 Effect.asVoid,2539 (cause) =>
2969 Effect.mapError(2540 new OrchestrationDispatchCommandError({
2970 (cause) =>2541 message: "Failed to create clone project.",
2971 new OrchestrationDispatchCommandError({2542 cause,
2972 message: "Failed to create clone project.",2543 }),
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 }),
2984 ),2544 ),
2985 Effect.andThen(refreshGitStatus(project.workspaceRoot)),
2986 Effect.ignoreCause({ log: true }),
2987 ),2545 ),
2988 }),2546 onCloned: (project) =>
2989 { "rpc.aggregate": "source-control" },2547 repositoryIdentityResolver.resolve(project.workspaceRoot, { refresh: true }).pipe(
2990 ),2548 Effect.andThen(
2991 [WS_METHODS.projectsEnsureScratch]: () =>2549 projectService.update({
2992 observeRpcEffect(2550 commandId: CommandId.make(`project-clone-done:${project.projectId}`),
2993 WS_METHODS.projectsEnsureScratch,2551 projectId: project.projectId,
2994 managedFolders.ensureScratchProject.pipe(2552 }),
2995 Effect.mapError(2553 ),
2996 (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }),2554 Effect.andThen(refreshGitStatus(project.workspaceRoot)),
2555 Effect.ignoreCause({ log: true }),
2997 ),2556 ),
2557 }),
2558 [WS_METHODS.projectsEnsureScratch]: () =>
2559 managedFolders.ensureScratchProject.pipe(
2560 Effect.mapError(
2561 (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }),
2998 ),2562 ),
2999 { "rpc.aggregate": "orchestration" },
3000 ),2563 ),
3001 [WS_METHODS.projectsCreateNew]: (input) =>2564 [WS_METHODS.projectsCreateNew]: (input) =>
3002 observeRpcEffect(2565 managedFolders
3003 WS_METHODS.projectsCreateNew,2566 .createNamedProject(input)
3004 managedFolders2567 .pipe(
3005 .createNamedProject(input)2568 Effect.mapError(
3006 .pipe(2569 (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }),
3007 Effect.mapError(
3008 (cause) =>
3009 new OrchestrationDispatchCommandError({ message: cause.message, cause }),
3010 ),
3011 ),2570 ),
3012 { "rpc.aggregate": "orchestration" },2571 ),
3013 ),
3014 [WS_METHODS.projectCloneCancel]: (input) =>2572 [WS_METHODS.projectCloneCancel]: (input) =>
3015 observeRpcEffect(2573 projectCloneTracker.cancel(input.projectId).pipe(Effect.map((applied) => ({ applied }))),
3016 WS_METHODS.projectCloneCancel,
3017 projectCloneTracker
3018 .cancel(input.projectId)
3019 .pipe(Effect.map((applied) => ({ applied }))),
3020 { "rpc.aggregate": "source-control" },
3021 ),
3022 [WS_METHODS.projectCloneRetry]: (input) =>2574 [WS_METHODS.projectCloneRetry]: (input) =>
3023 observeRpcEffect(2575 projectCloneTracker.retry(input.projectId).pipe(Effect.map((applied) => ({ applied }))),
3024 WS_METHODS.projectCloneRetry,2576 [WS_METHODS.subscribeProjectClones]: () => projectCloneTracker.stream,
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 }),
3032 [WS_METHODS.sourceControlPublishRepository]: (input) =>2577 [WS_METHODS.sourceControlPublishRepository]: (input) =>
3033 observeRpcEffect(2578 sourceControlRepositories.publishRepository(input).pipe(
3034 WS_METHODS.sourceControlPublishRepository,2579 // A new remote can change the cached identity. Only the `cwd` entry
3035 sourceControlRepositories.publishRepository(input).pipe(2580 // refreshes, so after a publish from a linked worktree the project
3036 // A new remote can change the cached identity. Only the `cwd` entry2581 // root entry waits for its TTL.
3037 // refreshes, so after a publish from a linked worktree the project2582 Effect.tap(() => repositoryIdentityResolver.resolve(input.cwd, { refresh: true })),
3038 // root entry waits for its TTL.2583 Effect.tap(() => refreshGitStatus(input.cwd)),
3039 Effect.tap(() => repositoryIdentityResolver.resolve(input.cwd, { refresh: true })),
3040 Effect.tap(() => refreshGitStatus(input.cwd)),
3041 ),
3042 {
3043 "rpc.aggregate": "source-control",
3044 },
3045 ),2584 ),
3046 [WS_METHODS.projectsSearchEntries]: (input) =>2585 [WS_METHODS.projectsSearchEntries]: (input) =>
3047 observeRpcEffect(2586 workspaceEntries.search(input).pipe(
3048 WS_METHODS.projectsSearchEntries,2587 Effect.mapError(
3049 workspaceEntries.search(input).pipe(2588 (cause) =>
3050 Effect.mapError(2589 new ProjectSearchEntriesError({
3051 (cause) =>2590 cwd: input.cwd,
3052 new ProjectSearchEntriesError({2591 queryLength: input.query.length,
3053 cwd: input.cwd,2592 limit: input.limit,
3054 queryLength: input.query.length,2593 ...projectEntriesFailureContext(cause),
3055 limit: input.limit,2594 cause,
3056 ...projectEntriesFailureContext(cause),2595 }),
3057 cause,
3058 }),
3059 ),
3060 ),2596 ),
3061 { "rpc.aggregate": "workspace" },
3062 ),2597 ),
3063 [WS_METHODS.projectsSearchContents]: (input) =>2598 [WS_METHODS.projectsSearchContents]: (input) =>
3064 observeRpcEffect(2599 workspaceEntries.searchContents(input).pipe(
3065 WS_METHODS.projectsSearchContents,2600 Effect.mapError(
3066 workspaceEntries.searchContents(input).pipe(2601 (cause) =>
3067 Effect.mapError(2602 new ProjectSearchContentsError({
3068 (cause) =>2603 cwd: input.cwd,
3069 new ProjectSearchContentsError({2604 queryLength: input.query.length,
3070 cwd: input.cwd,2605 limit: input.limit,
3071 queryLength: input.query.length,2606 ...projectEntriesFailureContext(cause),
3072 limit: input.limit,2607 cause,
3073 ...projectEntriesFailureContext(cause),2608 }),
3074 cause,
3075 }),
3076 ),
3077 ),2609 ),
3078 { "rpc.aggregate": "workspace" },
3079 ),2610 ),
3080 [WS_METHODS.projectsListEntries]: (input) =>2611 [WS_METHODS.projectsListEntries]: (input) =>
3081 observeRpcEffect(2612 workspaceEntries.list(input).pipe(
3082 WS_METHODS.projectsListEntries,2613 Effect.mapError(
3083 workspaceEntries.list(input).pipe(2614 (cause) =>
3084 Effect.mapError(2615 new ProjectListEntriesError({
3085 (cause) =>2616 ...input,
3086 new ProjectListEntriesError({2617 ...projectEntriesFailureContext(cause),
3087 ...input,2618 cause,
3088 ...projectEntriesFailureContext(cause),2619 }),
3089 cause,
3090 }),
3091 ),
3092 ),2620 ),
3093 { "rpc.aggregate": "workspace" },
3094 ),2621 ),
3095 [WS_METHODS.projectsReadFile]: (input) =>2622 [WS_METHODS.projectsReadFile]: (input) =>
3096 observeRpcEffect(2623 workspaceFileSystem.readFile(input).pipe(
3097 WS_METHODS.projectsReadFile,2624 Effect.mapError(
3098 workspaceFileSystem.readFile(input).pipe(2625 (cause) =>
3099 Effect.mapError(2626 new ProjectReadFileError({
3100 (cause) =>2627 ...input,
3101 new ProjectReadFileError({2628 ...projectFileFailureContext(cause),
3102 ...input,2629 cause,
3103 ...projectFileFailureContext(cause),2630 }),
3104 cause,
3105 }),
3106 ),
3107 ),2631 ),
3108 { "rpc.aggregate": "workspace" },
3109 ),2632 ),
3110 [WS_METHODS.projectsWriteFile]: (input) =>2633 [WS_METHODS.projectsWriteFile]: (input) =>
3111 observeRpcEffect(2634 workspaceFileSystem.writeFile(input).pipe(
3112 WS_METHODS.projectsWriteFile,2635 Effect.mapError(
3113 workspaceFileSystem.writeFile(input).pipe(2636 (cause) =>
3114 Effect.mapError(2637 new ProjectWriteFileError({
3115 (cause) =>2638 cwd: input.cwd,
3116 new ProjectWriteFileError({2639 relativePath: input.relativePath,
3117 cwd: input.cwd,2640 ...projectFileFailureContext(cause),
3118 relativePath: input.relativePath,2641 cause,
3119 ...projectFileFailureContext(cause),2642 }),
3120 cause,
3121 }),
3122 ),
3123 ),2643 ),
3124 { "rpc.aggregate": "workspace" },
3125 ),2644 ),
3126 [WS_METHODS.projectsMutate]: (mutation) =>2645 [WS_METHODS.projectsMutate]: (mutation) =>
3127 observeRpcEffect(2646 startup.enqueueCommand(mutateProject(mutation)).pipe(
3128 WS_METHODS.projectsMutate,2647 Effect.mapError(
3129 startup.enqueueCommand(mutateProject(mutation)).pipe(2648 (cause) =>
3130 Effect.mapError(2649 new ProjectMutationError({
3131 (cause) =>2650 commandId: mutation.commandId,
3132 new ProjectMutationError({2651 message:
3133 commandId: mutation.commandId,2652 cause._tag === "ProjectNotEmptyError"
3134 message:2653 ? cause.message
3135 cause._tag === "ProjectNotEmptyError"2654 : "Failed to mutate project.",
3136 ? cause.message2655 cause,
3137 : "Failed to mutate project.",2656 }),
3138 cause,
3139 }),
3140 ),
3141 ),2657 ),
3142 { "rpc.aggregate": "orchestration" },
3143 ),2658 ),
3144 [WS_METHODS.shellOpenInEditor]: (input) =>2659 [WS_METHODS.shellOpenInEditor]: (input) => externalLauncher.launchEditor(input),
3145 observeRpcEffect(WS_METHODS.shellOpenInEditor, externalLauncher.launchEditor(input), {
3146 "rpc.aggregate": "workspace",
3147 }),
3148 [WS_METHODS.filesystemBrowse]: (input) =>2660 [WS_METHODS.filesystemBrowse]: (input) =>
3149 observeRpcEffect(2661 workspaceEntries.browse(input).pipe(
3150 WS_METHODS.filesystemBrowse,2662 Effect.mapError(
3151 workspaceEntries.browse(input).pipe(2663 (cause) =>
3152 Effect.mapError(2664 new FilesystemBrowseError({
3153 (cause) =>2665 ...input,
3154 new FilesystemBrowseError({2666 ...filesystemBrowseFailureContext(cause),
3155 ...input,2667 cause,
3156 ...filesystemBrowseFailureContext(cause),2668 }),
3157 cause,
3158 }),
3159 ),
3160 ),2669 ),
3161 { "rpc.aggregate": "workspace" },
3162 ),2670 ),
3163 [WS_METHODS.attachmentsCreateUploadUrl]: (input) =>2671 [WS_METHODS.attachmentsCreateUploadUrl]: (input) => issueAttachmentUploadUrl(input),
3164 observeRpcEffect(WS_METHODS.attachmentsCreateUploadUrl, issueAttachmentUploadUrl(input), {2672 [WS_METHODS.attachmentsDelete]: (input) => deletePendingAttachment(input.attachmentId),
3165 "rpc.aggregate": "workspace",2673 [WS_METHODS.agentSessionsScan]: () => agentSessionScanner.scan,
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 }),
3177 [WS_METHODS.agentSessionsImport]: (input) =>2674 [WS_METHODS.agentSessionsImport]: (input) =>
3178 observeRpcEffect(2675 agentSessionImporter.importRecentAgentThreads(input),
3179 WS_METHODS.agentSessionsImport,
3180 agentSessionImporter.importRecentAgentThreads(input),
3181 { "rpc.aggregate": "workspace" },
3182 ),
3183 [WS_METHODS.assetsCreateUrl]: (input) =>2676 [WS_METHODS.assetsCreateUrl]: (input) =>
3184 observeRpcEffect(2677 Effect.gen(function* () {
3185 WS_METHODS.assetsCreateUrl,2678 const path = yield* Path.Path;
3186 Effect.gen(function* () {2679 // An absolute media path can be linked from a thread on another environment.
3187 const path = yield* Path.Path;2680 if (
3188 // An absolute media path can be linked from a thread on another environment.2681 input.resource._tag === "attachment" ||
3189 if (2682 input.resource._tag === "native-app-icon" ||
3190 input.resource._tag === "attachment" ||2683 input.resource._tag === "tool-output-image" ||
3191 input.resource._tag === "native-app-icon" ||2684 // GitHub media names the repository it authenticates through itself.
3192 input.resource._tag === "tool-output-image" ||2685 input.resource._tag === "github-media" ||
3193 // GitHub media names the repository it authenticates through itself.2686 (input.resource._tag === "media-file" && path.isAbsolute(input.resource.path))
3194 input.resource._tag === "github-media" ||2687 ) {
3195 (input.resource._tag === "media-file" && path.isAbsolute(input.resource.path))2688 return yield* issueAssetUrl({ resource: input.resource });
3196 ) {2689 }
3197 return yield* issueAssetUrl({ resource: input.resource });2690 if (input.resource._tag === "draft-workspace-file") {
3198 }2691 // A project draft names its workspace directly; there is no
3199 if (input.resource._tag === "draft-workspace-file") {2692 // thread to resolve one from.
3200 // A project draft names its workspace directly; there is no2693 return yield* issueAssetUrl({
3201 // thread to resolve one from.2694 resource: input.resource,
3202 return yield* issueAssetUrl({2695 workspaceRoot: input.resource.cwd,
3203 resource: input.resource,2696 });
3204 workspaceRoot: input.resource.cwd,2697 }
3205 });2698 if (input.resource._tag === "project-favicon") {
3206 }2699 const project = yield* projectStore
3207 if (input.resource._tag === "project-favicon") {2700 .findActiveByWorkspaceRoot(input.resource.cwd)
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, [])
3240 .pipe(2701 .pipe(
3241 Effect.mapError(2702 Effect.mapError(
3242 (cause) =>2703 (cause) =>
...@@ -3246,7 +2707,28 @@ const layerWsRpc = (...@@ -3246,7 +2707,28 @@ const layerWsRpc = (
3246 }),2707 }),
3247 ),2708 ),
3248 );2709 );
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(
3250 Effect.mapError(2732 Effect.mapError(
3251 (cause) =>2733 (cause) =>
3252 new AssetWorkspaceContextResolutionError({2734 new AssetWorkspaceContextResolutionError({
...@@ -3255,357 +2737,187 @@ const layerWsRpc = (...@@ -3255,357 +2737,187 @@ const layerWsRpc = (
3255 }),2737 }),
3256 ),2738 ),
3257 );2739 );
3258 if (Option.isNone(project)) {2740 const project = yield* projectService.getById(thread.thread.projectId).pipe(
3259 return yield* new AssetWorkspaceContextNotFoundError({2741 Effect.mapError(
3260 resource: input.resource,2742 (cause) =>
3261 });2743 new AssetWorkspaceContextResolutionError({
3262 }2744 resource: input.resource,
3263 return yield* issueAssetUrl({2745 cause,
2746 }),
2747 ),
2748 );
2749 if (Option.isNone(project)) {
2750 return yield* new AssetWorkspaceContextNotFoundError({
3264 resource: input.resource,2751 resource: input.resource,
3265 workspaceRoot: thread.thread.worktreePath ?? project.value.workspaceRoot,
3266 });2752 });
3267 }),2753 }
3268 { "rpc.aggregate": "workspace" },2754 return yield* issueAssetUrl({
3269 ),2755 resource: input.resource,
2756 workspaceRoot: thread.thread.worktreePath ?? project.value.workspaceRoot,
2757 });
2758 }),
3270 [WS_METHODS.assetsPersistChatAttachments]: (input) =>2759 [WS_METHODS.assetsPersistChatAttachments]: (input) =>
3271 observeRpcEffect(2760 persistChatAttachments(input).pipe(Effect.map((attachments) => ({ attachments }))),
3272 WS_METHODS.assetsPersistChatAttachments,
3273 persistChatAttachments(input).pipe(Effect.map((attachments) => ({ attachments }))),
3274 { "rpc.aggregate": "orchestration" },
3275 ),
3276 [WS_METHODS.subscribeVcsStatus]: (input) =>2761 [WS_METHODS.subscribeVcsStatus]: (input) =>
3277 observeRpcStream(2762 vcsStatusBroadcaster.streamStatus(input, {
3278 WS_METHODS.subscribeVcsStatus,2763 automaticRemoteRefreshInterval: automaticGitFetchInterval,
3279 vcsStatusBroadcaster.streamStatus(input, {2764 }),
3280 automaticRemoteRefreshInterval: automaticGitFetchInterval,2765 [WS_METHODS.subscribeWorktreeSetup]: (input) => worktreeSetupTracker.stream(input.threadId),
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 ),
3292 [WS_METHODS.worktreeSetupCancel]: (input) =>2766 [WS_METHODS.worktreeSetupCancel]: (input) =>
3293 observeRpcEffect(2767 worktreeSetupTracker
3294 WS_METHODS.worktreeSetupCancel,2768 .cancel(input.threadId)
3295 worktreeSetupTracker2769 .pipe(Effect.map((cancelled) => ({ cancelled }))),
3296 .cancel(input.threadId)2770 [WS_METHODS.vcsRefreshStatus]: (input) => vcsStatusBroadcaster.refreshStatus(input.cwd),
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 ),
3308 [WS_METHODS.vcsPull]: (input) =>2771 [WS_METHODS.vcsPull]: (input) =>
3309 observeRpcEffect(2772 gitWorkflow.pullCurrentBranch(input.cwd).pipe(
3310 WS_METHODS.vcsPull,2773 Effect.matchCauseEffect({
3311 gitWorkflow.pullCurrentBranch(input.cwd).pipe(2774 onFailure: (cause) => Effect.failCause(cause),
3312 Effect.matchCauseEffect({2775 onSuccess: (result) =>
3313 onFailure: (cause) => Effect.failCause(cause),2776 refreshGitStatus(input.cwd).pipe(Effect.ignore({ log: true }), Effect.as(result)),
3314 onSuccess: (result) =>2777 }),
3315 refreshGitStatus(input.cwd).pipe(Effect.ignore({ log: true }), Effect.as(result)),
3316 }),
3317 ),
3318 { "rpc.aggregate": "git" },
3319 ),2778 ),
3320 [WS_METHODS.gitRunStackedAction]: (input) =>2779 [WS_METHODS.gitRunStackedAction]: (input) =>
3321 observeRpcStream(2780 Stream.callback<GitActionProgressEvent, GitManagerServiceError>((queue) =>
3322 WS_METHODS.gitRunStackedAction,2781 gitWorkflow
3323 Stream.callback<GitActionProgressEvent, GitManagerServiceError>((queue) =>2782 .runStackedAction(input, {
3324 gitWorkflow2783 actionId: input.actionId,
3325 .runStackedAction(input, {2784 progressReporter: {
3326 actionId: input.actionId,2785 publish: (event) => Queue.offer(queue, event).pipe(Effect.asVoid),
3327 progressReporter: {2786 },
3328 publish: (event) => Queue.offer(queue, event).pipe(Effect.asVoid),2787 })
3329 },2788 .pipe(
3330 })2789 Effect.matchCauseEffect({
3331 .pipe(2790 onFailure: (cause) => Queue.failCause(queue, cause),
3332 Effect.matchCauseEffect({2791 onSuccess: (result) =>
3333 onFailure: (cause) => Queue.failCause(queue, cause),2792 (input.threadId === undefined
3334 onSuccess: (result) =>2793 ? Effect.void
3335 (input.threadId === undefined2794 : linkCreatedPullRequest({
3336 ? Effect.void2795 threadId: input.threadId,
3337 : linkCreatedPullRequest({2796 result,
3338 threadId: input.threadId,2797 commandId: serverCommandId("pr-created-link"),
3339 result,2798 }).pipe(
3340 commandId: serverCommandId("pr-created-link"),2799 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),
3341 }).pipe(2800 Effect.provideService(ProjectService.ProjectService, projectService),
3342 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),2801 )
3343 Effect.provideService(ProjectService.ProjectService, projectService),2802 ).pipe(
3344 )2803 Effect.andThen(
3345 ).pipe(2804 refreshPushedPullRequests(input, result).pipe(
3346 Effect.andThen(2805 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),
3347 refreshPushedPullRequests(input, result).pipe(2806 Effect.provideService(ProjectStore.ProjectStoreV2, projectStore),
3348 Effect.provideService(Orchestrator.OrchestratorV2, orchestrationEngine),2807 Effect.provideService(
3349 Effect.provideService(ProjectStore.ProjectStoreV2, projectStore),2808 PullRequestService.PullRequestService,
3350 Effect.provideService(2809 pullRequests,
3351 PullRequestService.PullRequestService,
3352 pullRequests,
3353 ),
3354 ),2810 ),
3355 ),2811 ),
3356 Effect.andThen(refreshGitStatus(input.cwd)),
3357 Effect.andThen(Queue.end(queue).pipe(Effect.asVoid)),
3358 ),2812 ),
3359 }),2813 Effect.andThen(refreshGitStatus(input.cwd)),
3360 ),2814 Effect.andThen(Queue.end(queue).pipe(Effect.asVoid)),
3361 ),2815 ),
3362 { "rpc.aggregate": "vcs" },2816 }),
3363 ),2817 ),
3364 [WS_METHODS.gitResolvePullRequest]: (input) =>
3365 observeRpcEffect(
3366 WS_METHODS.gitResolvePullRequest,
3367 gitWorkflow.resolvePullRequest(input),
3368 {
3369 "rpc.aggregate": "git",
3370 },
3371 ),2818 ),
2819 [WS_METHODS.gitResolvePullRequest]: (input) => gitWorkflow.resolvePullRequest(input),
3372 [WS_METHODS.gitPreparePullRequestThread]: (input) =>2820 [WS_METHODS.gitPreparePullRequestThread]: (input) =>
3373 observeRpcEffect(2821 gitWorkflow
3374 WS_METHODS.gitPreparePullRequestThread,2822 .preparePullRequestThread(input)
3375 gitWorkflow2823 .pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3376 .preparePullRequestThread(input)2824 [WS_METHODS.vcsListRefs]: (input) => gitWorkflow.listRefs(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 }),
3384 [WS_METHODS.vcsCreateWorktree]: (input) =>2825 [WS_METHODS.vcsCreateWorktree]: (input) =>
3385 observeRpcEffect(2826 gitWorkflow.createWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3386 WS_METHODS.vcsCreateWorktree,
3387 gitWorkflow.createWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3388 { "rpc.aggregate": "vcs" },
3389 ),
3390 [WS_METHODS.vcsRemoveWorktree]: (input) =>2827 [WS_METHODS.vcsRemoveWorktree]: (input) =>
3391 observeRpcEffect(2828 gitWorkflow.removeWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3392 WS_METHODS.vcsRemoveWorktree,
3393 gitWorkflow.removeWorktree(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3394 { "rpc.aggregate": "vcs" },
3395 ),
3396 [WS_METHODS.vcsCreateRef]: (input) =>2829 [WS_METHODS.vcsCreateRef]: (input) =>
3397 observeRpcEffect(2830 gitWorkflow.createRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3398 WS_METHODS.vcsCreateRef,
3399 gitWorkflow.createRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3400 { "rpc.aggregate": "vcs" },
3401 ),
3402 [WS_METHODS.vcsSwitchRef]: (input) =>2831 [WS_METHODS.vcsSwitchRef]: (input) =>
3403 observeRpcEffect(2832 gitWorkflow.switchRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3404 WS_METHODS.vcsSwitchRef,
3405 gitWorkflow.switchRef(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3406 { "rpc.aggregate": "vcs" },
3407 ),
3408 [WS_METHODS.vcsInit]: (input) =>2833 [WS_METHODS.vcsInit]: (input) =>
3409 observeRpcEffect(2834 vcsProvisioning.initRepository(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))),
3410 WS_METHODS.vcsInit,2835 [WS_METHODS.reviewGetDiffPreview]: (input) => review.getDiffPreview(input),
3411 vcsProvisioning2836 [WS_METHODS.reviewGetDiffFileContents]: (input) => review.getDiffFileContents(input),
3412 .initRepository(input)2837 [WS_METHODS.terminalOpen]: (input) => terminalManager.open(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 }),
3430 [WS_METHODS.terminalAttach]: (input) =>2838 [WS_METHODS.terminalAttach]: (input) =>
3431 observeRpcStream(2839 Stream.callback<TerminalAttachStreamEvent, TerminalError>((queue) =>
3432 WS_METHODS.terminalAttach,2840 Effect.acquireRelease(
3433 Stream.callback<TerminalAttachStreamEvent, TerminalError>((queue) =>2841 terminalManager.attachStream(input, (event) => Queue.offer(queue, event)),
3434 Effect.acquireRelease(2842 (unsubscribe) => Effect.sync(unsubscribe),
3435 terminalManager.attachStream(input, (event) => Queue.offer(queue, event)),
3436 (unsubscribe) => Effect.sync(unsubscribe),
3437 ),
3438 ),2843 ),
3439 { "rpc.aggregate": "terminal" },
3440 ),2844 ),
3441 [WS_METHODS.terminalWrite]: (input) =>2845 [WS_METHODS.terminalWrite]: (input) => terminalManager.write(input),
3442 observeRpcEffect(WS_METHODS.terminalWrite, terminalManager.write(input), {2846 [WS_METHODS.terminalResize]: (input) => terminalManager.resize(input),
3443 "rpc.aggregate": "terminal",2847 [WS_METHODS.terminalClear]: (input) => terminalManager.clear(input),
3444 }),2848 [WS_METHODS.terminalRestart]: (input) => terminalManager.restart(input),
3445 [WS_METHODS.terminalResize]: (input) =>2849 [WS_METHODS.terminalClose]: (input) => terminalManager.close(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 }),
3461 [WS_METHODS.subscribeTerminalEvents]: (_input) =>2850 [WS_METHODS.subscribeTerminalEvents]: (_input) =>
3462 observeRpcStream(2851 Stream.callback<TerminalEvent>((queue) =>
3463 WS_METHODS.subscribeTerminalEvents,2852 Effect.acquireRelease(
3464 Stream.callback<TerminalEvent>((queue) =>2853 terminalManager.subscribe((event) => Queue.offer(queue, event)),
3465 Effect.acquireRelease(2854 (unsubscribe) => Effect.sync(unsubscribe),
3466 terminalManager.subscribe((event) => Queue.offer(queue, event)),
3467 (unsubscribe) => Effect.sync(unsubscribe),
3468 ),
3469 ),2855 ),
3470 { "rpc.aggregate": "terminal" },
3471 ),2856 ),
3472 [WS_METHODS.subscribeTerminalMetadata]: (_input) =>2857 [WS_METHODS.subscribeTerminalMetadata]: (_input) =>
3473 observeRpcStream(2858 Stream.callback<TerminalMetadataStreamEvent>((queue) =>
3474 WS_METHODS.subscribeTerminalMetadata,2859 Effect.acquireRelease(
3475 Stream.callback<TerminalMetadataStreamEvent>((queue) =>2860 terminalManager.subscribeMetadata((event) => Queue.offer(queue, event)),
3476 Effect.acquireRelease(2861 (unsubscribe) => Effect.sync(unsubscribe),
3477 terminalManager.subscribeMetadata((event) => Queue.offer(queue, event)),
3478 (unsubscribe) => Effect.sync(unsubscribe),
3479 ),
3480 ),2862 ),
3481 { "rpc.aggregate": "terminal" },
3482 ),2863 ),
3483 [WS_METHODS.previewOpen]: (input) =>2864 [WS_METHODS.previewOpen]: (input) => previewManager.open(input),
3484 observeRpcEffect(WS_METHODS.previewOpen, previewManager.open(input), {2865 [WS_METHODS.previewNavigate]: (input) => previewManager.navigate(input),
3485 "rpc.aggregate": "preview",2866 [WS_METHODS.previewResize]: (input) => previewManager.resize(input),
3486 }),2867 [WS_METHODS.previewAdjust]: (input) => previewManager.adjust(input),
3487 [WS_METHODS.previewNavigate]: (input) =>2868 [WS_METHODS.previewRefresh]: (input) => previewManager.refresh(input),
3488 observeRpcEffect(WS_METHODS.previewNavigate, previewManager.navigate(input), {2869 [WS_METHODS.previewClose]: (input) => previewManager.close(input),
3489 "rpc.aggregate": "preview",2870 [WS_METHODS.previewList]: (input) => previewManager.list(input),
3490 }),2871 [WS_METHODS.previewClearProfile]: (input) => serverBrowser.clearProfile(input.profileId),
3491 [WS_METHODS.previewResize]: (input) =>2872 [WS_METHODS.previewReportStatus]: (input) => previewManager.reportStatus(input),
3492 observeRpcEffect(WS_METHODS.previewResize, previewManager.resize(input), {2873 [WS_METHODS.subscribePreviewEvents]: (_input) => previewManager.events,
3493 "rpc.aggregate": "preview",2874 [WS_METHODS.deviceConfigure]: (input) => deviceService.configure(input),
3494 }),2875 [WS_METHODS.deviceTestHost]: (input) => deviceService.testHost(input),
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 }),
3533 [WS_METHODS.deviceList]: (input) =>2876 [WS_METHODS.deviceList]: (input) =>
3534 observeRpcEffect(2877 input.inspectOnly && !input.updateTool
3535 WS_METHODS.deviceList,2878 ? deviceService.inspect
3536 input.inspectOnly && !input.updateTool2879 : authorizeEffect(
3537 ? deviceService.inspect2880 requiredScopeForDeviceList(input),
3538 : authorizeEffect(2881 input.updateTool
3539 requiredScopeForDeviceList(input),2882 ? deviceService.updateTool(input.updateTool)
3540 input.updateTool2883 : input.retryHostId
3541 ? deviceService.updateTool(input.updateTool)2884 ? deviceService.retryHost(input.retryHostId)
3542 : input.retryHostId2885 : deviceService.list,
3543 ? deviceService.retryHost(input.retryHostId)2886 ),
3544 : deviceService.list,2887 [WS_METHODS.deviceOpen]: (input) => deviceService.open(input),
3545 ),2888 [WS_METHODS.deviceClose]: (input) => deviceService.close(input),
3546 {2889 [WS_METHODS.deviceShutdown]: (input) => deviceService.shutdown(input),
3547 "rpc.aggregate": "device",2890 [WS_METHODS.deviceDetail]: (input) => deviceService.detail(input),
3548 },2891 [WS_METHODS.deviceAction]: (input) => deviceService.action(input),
3549 ),2892 [WS_METHODS.subscribeDeviceState]: (_input) => DeviceService.stateStream(deviceService),
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 ),
3576 [WS_METHODS.subscribeDiscoveredLocalServers]: (input) =>2893 [WS_METHODS.subscribeDiscoveredLocalServers]: (input) =>
3577 observeRpcStream(2894 Stream.callback<DiscoveredLocalServerList>((queue) =>
3578 WS_METHODS.subscribeDiscoveredLocalServers,2895 Effect.gen(function* () {
3579 Stream.callback<DiscoveredLocalServerList>((queue) =>2896 const configuredUrls = input.configuredUrls ?? [];
3580 Effect.gen(function* () {2897 yield* portDiscovery.retain;
3581 const configuredUrls = input.configuredUrls ?? [];2898 const initial = yield* portDiscovery.scan(configuredUrls);
3582 yield* portDiscovery.retain;2899 const initialScannedAt = DateTime.formatIso(yield* DateTime.now);
3583 const initial = yield* portDiscovery.scan(configuredUrls);2900 yield* Queue.offer(queue, {
3584 const initialScannedAt = DateTime.formatIso(yield* DateTime.now);2901 servers: initial,
3585 yield* Queue.offer(queue, {2902 scannedAt: initialScannedAt,
3586 servers: initial,2903 configuredUrlProbing: true,
3587 scannedAt: initialScannedAt,2904 });
3588 configuredUrlProbing: true,2905 yield* portDiscovery.subscribe(
3589 });2906 { configuredUrls, initialSnapshot: initial },
3590 yield* portDiscovery.subscribe(2907 (servers) =>
3591 { configuredUrls, initialSnapshot: initial },2908 Effect.gen(function* () {
3592 (servers) =>2909 const scannedAt = DateTime.formatIso(yield* DateTime.now);
3593 Effect.gen(function* () {2910 yield* Queue.offer(queue, {
3594 const scannedAt = DateTime.formatIso(yield* DateTime.now);2911 servers,
3595 yield* Queue.offer(queue, {2912 scannedAt,
3596 servers,2913 configuredUrlProbing: true,
3597 scannedAt,2914 });
3598 configuredUrlProbing: true,2915 }),
3599 });2916 );
3600 }),2917 }),
3601 );
3602 }),
3603 ),
3604 { "rpc.aggregate": "preview" },
3605 ),2918 ),
3606 [WS_METHODS.subscribeServerConfig]: (input) =>2919 [WS_METHODS.subscribeServerConfig]: (input) =>
3607 observeRpcStreamEffect(2920 Stream.unwrap(
3608 WS_METHODS.subscribeServerConfig,
3609 Effect.gen(function* () {2921 Effect.gen(function* () {
3610 const usageLimitsCommand = input.usageLimitsCommand === true;2922 const usageLimitsCommand = input.usageLimitsCommand === true;
3611 const config = yield* loadServerConfig({ usageLimitsCommand });2923 const config = yield* loadServerConfig({ usageLimitsCommand });
...@@ -3705,11 +3017,9 @@ const layerWsRpc = (...@@ -3705,11 +3017,9 @@ const layerWsRpc = (
3705 withLateEditorConfig(config, liveUpdates, externalLauncher),3017 withLateEditorConfig(config, liveUpdates, externalLauncher),
3706 );3018 );
3707 }),3019 }),
3708 { "rpc.aggregate": "server" },
3709 ),3020 ),
3710 [WS_METHODS.subscribeServerLifecycle]: (_input) =>3021 [WS_METHODS.subscribeServerLifecycle]: (_input) =>
3711 observeRpcStreamEffect(3022 Stream.unwrap(
3712 WS_METHODS.subscribeServerLifecycle,
3713 Effect.gen(function* () {3023 Effect.gen(function* () {
3714 const liveBuffer = yield* Queue.unbounded<ServerLifecycleStreamEvent>();3024 const liveBuffer = yield* Queue.unbounded<ServerLifecycleStreamEvent>();
3715 yield* Effect.forkScoped(3025 yield* Effect.forkScoped(
...@@ -3727,11 +3037,9 @@ const layerWsRpc = (...@@ -3727,11 +3037,9 @@ const layerWsRpc = (
3727 );3037 );
3728 return Stream.concat(rpcInitialItems(snapshotEvents), liveEvents);3038 return Stream.concat(rpcInitialItems(snapshotEvents), liveEvents);
3729 }),3039 }),
3730 { "rpc.aggregate": "server" },
3731 ),3040 ),
3732 [WS_METHODS.subscribeAuthAccess]: (_input) =>3041 [WS_METHODS.subscribeAuthAccess]: (_input) =>
3733 observeRpcStreamEffect(3042 Stream.unwrap(
3734 WS_METHODS.subscribeAuthAccess,
3735 Effect.gen(function* () {3043 Effect.gen(function* () {
3736 const initialSnapshot = yield* loadAuthAccessSnapshot();3044 const initialSnapshot = yield* loadAuthAccessSnapshot();
3737 const revisionRef = yield* Ref.make(1);3045 const revisionRef = yield* Ref.make(1);
...@@ -3761,27 +3069,18 @@ const layerWsRpc = (...@@ -3761,27 +3069,18 @@ const layerWsRpc = (
3761 liveEvents,3069 liveEvents,
3762 );3070 );
3763 }),3071 }),
3764 { "rpc.aggregate": "auth" },
3765 ),3072 ),
3766 [WS_METHODS.subscribeBackgroundPolicy]: (_input) =>3073 [WS_METHODS.subscribeBackgroundPolicy]: (_input) =>
3767 observeRpcStream(3074 Stream.unwrap(
3768 WS_METHODS.subscribeBackgroundPolicy,3075 Effect.map(backgroundPolicy.subscribe, ({ latest, changes }) =>
3769 Stream.unwrap(3076 Stream.concat(Stream.make(latest), changes),
3770 Effect.map(backgroundPolicy.subscribe, ({ latest, changes }) =>
3771 Stream.concat(Stream.make(latest), changes),
3772 ),
3773 ),3077 ),
3774 { "rpc.aggregate": "server" },
3775 ),3078 ),
3776 [WS_METHODS.subscribeResourceTelemetry]: (_input) =>3079 [WS_METHODS.subscribeResourceTelemetry]: (_input) =>
3777 observeRpcStream(3080 Stream.unwrap(
3778 WS_METHODS.subscribeResourceTelemetry,3081 Effect.map(resourceTelemetry.subscribe, ({ latest, changes }) =>
3779 Stream.unwrap(3082 Stream.concat(Stream.make(latest), changes),
3780 Effect.map(resourceTelemetry.subscribe, ({ latest, changes }) =>
3781 Stream.concat(Stream.make(latest), changes),
3782 ),
3783 ),3083 ),
3784 { "rpc.aggregate": "server" },
3785 ),3084 ),
3786 });3085 });
3787 return handlers;3086 return handlers;
...@@ -3841,7 +3140,9 @@ export const layer = Layer.unwrap(...@@ -3841,7 +3140,9 @@ export const layer = Layer.unwrap(
3841 const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket;3140 const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket;
3842 yield* RpcServer.make(ServerWsRpcGroup, WS_RPC_SERVER_OPTIONS).pipe(3141 yield* RpcServer.make(ServerWsRpcGroup, WS_RPC_SERVER_OPTIONS).pipe(
3843 Effect.provideService(RpcServer.Protocol, withTerminalOutputWindow(protocol)),3142 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 ),
3845 Effect.forkScoped,3146 Effect.forkScoped,
3846 );3147 );
3847 // @effect-diagnostics-next-line returnEffectInGen:off3148 // @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....@@ -24,15 +24,17 @@ message) are fine next to the service; the capability itself is the method.
24```ts24```ts
25// ws.ts: a thin handler25// ws.ts: a thin handler
26[WS_METHODS.projectsCreateNew]: (input) =>26[WS_METHODS.projectsCreateNew]: (input) =>
27 observeRpcEffect(27 projectFolders
28 WS_METHODS.projectsCreateNew,28 .createNamedProject(input)
29 projectFolders.createNamedProject(input).pipe(29 .pipe(Effect.mapError((cause) => new ProjectCreateNewError({ cause }))),
30 Effect.mapError((cause) => new ProjectCreateNewError({ cause })),
31 ),
32 { "rpc.aggregate": "orchestration" },
33 ),
34```30```
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
36## Shape of a service module38## Shape of a service module
3739
38One module per service, in this order: imports, errors and schemas, the `Context.Service` tag with40One 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...@@ -635,8 +635,8 @@ wins for its signal. `otlp` is the default, and any other exporter name, such as
635635
636Current high-value span and metric boundaries include:636Current high-value span and metric boundaries include:
637637
638- Effect RPC websocket request spans from `effect/rpc`638- WebSocket RPC request spans (`ws.rpc.<method>`) and metrics in
639- RPC request metrics in `apps/server/src/observability/RpcInstrumentation.ts`639 `apps/server/src/observability/RpcInstrumentation.ts`
640- startup phases640- startup phases
641- orchestration command processing641- orchestration command processing
642- provider session and turn operations642- provider session and turn operations