@h-rig/transport-plugin 0.0.6-alpha.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,308 @@
1
+ // @bun
2
+ // packages/transport-plugin/src/discovery.ts
3
+ import { Effect as Effect2, Option, Stream as Stream2, Duration } from "effect";
4
+
5
+ // packages/transport-plugin/relay-registry/src/client.ts
6
+ import { Effect, Queue, Stream } from "effect";
7
+ function registryUrl(baseUrl, pathname) {
8
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
9
+ return new URL(pathname.replace(/^\/+/, ""), base);
10
+ }
11
+ function registryWsUrl(baseUrl, pathname) {
12
+ const url = registryUrl(baseUrl, pathname);
13
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
14
+ return url;
15
+ }
16
+ async function parseResponseJson(response) {
17
+ const text = await response.text();
18
+ const data = text ? JSON.parse(text) : null;
19
+ if (!response.ok) {
20
+ const message = data && typeof data === "object" && "error" in data ? String(data.error) : response.statusText;
21
+ throw new Error(`registry request failed (${response.status}): ${message}`);
22
+ }
23
+ return data;
24
+ }
25
+ function subscribeDiscovery(namespaceKey, repo, options) {
26
+ return Stream.callback((queue) => Effect.sync(() => {
27
+ const WebSocketCtor = options.WebSocket ?? WebSocket;
28
+ const url = registryWsUrl(options.baseUrl, "ws");
29
+ url.searchParams.set("namespaceKey", namespaceKey);
30
+ if (repo)
31
+ url.searchParams.set("repo", repo);
32
+ const ws = new WebSocketCtor(url);
33
+ const entries = new Map;
34
+ const snapshot = (sentAt) => ({
35
+ type: "registry.snapshot",
36
+ namespaceKey,
37
+ sentAt,
38
+ entries: Array.from(entries.values())
39
+ });
40
+ ws.addEventListener("message", (event) => {
41
+ try {
42
+ const text = typeof event.data === "string" ? event.data : String(event.data);
43
+ const frame = JSON.parse(text);
44
+ if (frame?.type === "registry.snapshot") {
45
+ entries.clear();
46
+ for (const entry of frame.entries)
47
+ entries.set(entry.roomId, entry);
48
+ Queue.offerUnsafe(queue, frame);
49
+ return;
50
+ }
51
+ if (frame?.type !== "registry.delta")
52
+ return;
53
+ if (frame.action === "remove") {
54
+ entries.delete(frame.roomId);
55
+ } else if (frame.entry) {
56
+ entries.set(frame.entry.roomId, frame.entry);
57
+ }
58
+ Queue.offerUnsafe(queue, snapshot(frame.sentAt));
59
+ } catch {}
60
+ });
61
+ ws.addEventListener("error", () => {});
62
+ return ws;
63
+ }).pipe(Effect.flatMap((ws) => Effect.addFinalizer(() => Effect.sync(() => ws.close())))), { bufferSize: 1, strategy: "sliding" });
64
+ }
65
+ function createRegistryClient(options) {
66
+ const requestFetch = options.fetch ?? fetch;
67
+ const headers = { "content-type": "application/json" };
68
+ async function post(pathname, body) {
69
+ const response = await requestFetch(registryUrl(options.baseUrl, pathname), {
70
+ method: "POST",
71
+ headers,
72
+ body: JSON.stringify(body)
73
+ });
74
+ return parseResponseJson(response);
75
+ }
76
+ return {
77
+ async registerRoom(room) {
78
+ const response = await post("register", room);
79
+ return response.entry;
80
+ },
81
+ async heartbeatRoom(roomId, status, projection) {
82
+ const body = projection === undefined ? { roomId, namespaceKey: options.namespaceKey, status } : { roomId, namespaceKey: options.namespaceKey, status, projection };
83
+ const response = await post("heartbeat", body);
84
+ return response.entry;
85
+ },
86
+ async ingestProjection(input) {
87
+ const response = await post("projection", {
88
+ ...input,
89
+ namespaceKey: input.namespaceKey ?? options.namespaceKey
90
+ });
91
+ return response.entry;
92
+ },
93
+ async listRoomsByOwner(filter) {
94
+ const namespaceKey = filter.namespaceKey ?? options.namespaceKey;
95
+ const url = registryUrl(options.baseUrl, "list");
96
+ url.searchParams.set("namespaceKey", namespaceKey);
97
+ if (filter.selectedRepo)
98
+ url.searchParams.set("repo", filter.selectedRepo);
99
+ const response = await requestFetch(url);
100
+ return (await parseResponseJson(response)).entries;
101
+ },
102
+ async removeRoom(roomId) {
103
+ const response = await post("remove", { roomId, namespaceKey: options.namespaceKey });
104
+ return response.removed;
105
+ }
106
+ };
107
+ }
108
+ // packages/transport-plugin/relay-registry/src/schema.ts
109
+ import { RunStatus as ContractRunStatus, Schema } from "@rig/contracts";
110
+ var RegistryOwner = Schema.Struct({
111
+ githubUserId: Schema.String,
112
+ login: Schema.String,
113
+ namespaceKey: Schema.String
114
+ });
115
+ var REGISTRY_STATUS_ALIASES = ["starting", "waiting-input"];
116
+ var REGISTRY_STATUS_VALUES = [
117
+ ...ContractRunStatus.members.map((member) => member.literal),
118
+ ...REGISTRY_STATUS_ALIASES
119
+ ];
120
+ var RegistryStatus = Schema.Literals(REGISTRY_STATUS_VALUES);
121
+ var REGISTRY_STATUS_SET = new Set(REGISTRY_STATUS_VALUES);
122
+ var RegistryEntry = Schema.Struct({
123
+ roomId: Schema.String,
124
+ owner: RegistryOwner,
125
+ repo: Schema.String,
126
+ title: Schema.String,
127
+ status: RegistryStatus,
128
+ joinLink: Schema.String,
129
+ webLink: Schema.String,
130
+ relayUrl: Schema.String,
131
+ startedAt: Schema.String,
132
+ cwd: Schema.optional(Schema.String),
133
+ sessionPath: Schema.optional(Schema.String),
134
+ heartbeatAt: Schema.String,
135
+ pid: Schema.optional(Schema.Number),
136
+ projection: Schema.optional(Schema.Unknown)
137
+ });
138
+ var RegisterRequest = Schema.Struct({
139
+ roomId: Schema.String,
140
+ owner: RegistryOwner,
141
+ repo: Schema.String,
142
+ title: Schema.String,
143
+ status: RegistryStatus,
144
+ joinLink: Schema.String,
145
+ webLink: Schema.String,
146
+ relayUrl: Schema.String,
147
+ startedAt: Schema.String,
148
+ cwd: Schema.optional(Schema.String),
149
+ sessionPath: Schema.optional(Schema.String),
150
+ pid: Schema.optional(Schema.Number),
151
+ projection: Schema.optional(Schema.Unknown)
152
+ });
153
+ var HeartbeatRequest = Schema.Struct({
154
+ roomId: Schema.String,
155
+ namespaceKey: Schema.String,
156
+ status: RegistryStatus,
157
+ projection: Schema.optional(Schema.Unknown)
158
+ });
159
+ var ProjectionIngestRequest = Schema.Struct({
160
+ namespaceKey: Schema.String,
161
+ roomId: Schema.optional(Schema.String),
162
+ owner: Schema.optional(RegistryOwner),
163
+ repo: Schema.optional(Schema.String),
164
+ projection: Schema.Unknown
165
+ });
166
+ var RemoveRequest = Schema.Struct({
167
+ namespaceKey: Schema.String,
168
+ roomId: Schema.String
169
+ });
170
+ var ListedRegistryEntry = Schema.Struct({
171
+ roomId: Schema.String,
172
+ owner: RegistryOwner,
173
+ repo: Schema.String,
174
+ title: Schema.String,
175
+ status: RegistryStatus,
176
+ joinLink: Schema.String,
177
+ webLink: Schema.String,
178
+ relayUrl: Schema.String,
179
+ startedAt: Schema.String,
180
+ heartbeatAt: Schema.String,
181
+ cwd: Schema.optional(Schema.String),
182
+ sessionPath: Schema.optional(Schema.String),
183
+ pid: Schema.optional(Schema.Number),
184
+ projection: Schema.optional(Schema.Unknown),
185
+ stale: Schema.Boolean
186
+ });
187
+ var ListResponse = Schema.Struct({
188
+ entries: Schema.Array(ListedRegistryEntry)
189
+ });
190
+ var RegisterResponse = Schema.Struct({
191
+ entry: RegistryEntry
192
+ });
193
+ var HeartbeatResponse = Schema.Struct({
194
+ entry: RegistryEntry
195
+ });
196
+ var ProjectionIngestResponse = Schema.Struct({
197
+ entry: RegistryEntry
198
+ });
199
+ var RemoveResponse = Schema.Struct({
200
+ removed: Schema.Boolean
201
+ });
202
+ var RegistrySnapshotFrame = Schema.Struct({
203
+ type: Schema.Literal("registry.snapshot"),
204
+ namespaceKey: Schema.String,
205
+ sentAt: Schema.String,
206
+ entries: Schema.Array(ListedRegistryEntry)
207
+ });
208
+ var RegistryDeltaFrame = Schema.Struct({
209
+ type: Schema.Literal("registry.delta"),
210
+ namespaceKey: Schema.String,
211
+ sentAt: Schema.String,
212
+ action: Schema.Literals(["upsert", "remove"]),
213
+ roomId: Schema.String,
214
+ entry: Schema.optional(ListedRegistryEntry)
215
+ });
216
+ var WorkerProjectionFrame = Schema.Struct({
217
+ type: Schema.Literal("projection"),
218
+ projection: Schema.Unknown
219
+ });
220
+ // packages/transport-plugin/src/discovery.ts
221
+ import { resolveOwnerNamespaceKey, resolveRegistryBaseUrl } from "@rig/core/remote-config";
222
+ function registryBaseUrl(projectRoot = process.cwd(), env = process.env) {
223
+ return resolveRegistryBaseUrl(projectRoot, env);
224
+ }
225
+ function collabFromRegistryEntry(entry) {
226
+ return {
227
+ sessionId: entry.roomId,
228
+ title: entry.title,
229
+ sessionPath: entry.sessionPath ?? (entry.projection && typeof entry.projection === "object" && "sessionPath" in entry.projection && typeof entry.projection.sessionPath === "string" ? entry.projection.sessionPath : ""),
230
+ cwd: entry.cwd ?? (entry.projection && typeof entry.projection === "object" && "worktreePath" in entry.projection && typeof entry.projection.worktreePath === "string" ? entry.projection.worktreePath : ""),
231
+ joinLink: entry.joinLink ?? "",
232
+ webLink: entry.webLink ?? "",
233
+ relayUrl: entry.relayUrl ?? "",
234
+ owner: entry.owner,
235
+ selectedRepo: entry.repo,
236
+ startedAt: entry.startedAt,
237
+ updatedAt: entry.heartbeatAt,
238
+ ...entry.pid === undefined ? {} : { pid: entry.pid },
239
+ stale: entry.stale,
240
+ registryStatus: entry.status,
241
+ registryProjection: entry.projection
242
+ };
243
+ }
244
+ function namespaceKeyFor(projectRoot, filter = {}) {
245
+ return filter.namespaceKey ?? resolveOwnerNamespaceKey(projectRoot) ?? "anonymous";
246
+ }
247
+ async function listActiveRunCollab(projectRoot, filter) {
248
+ return defaultListActiveCollabSessions(projectRoot, filter);
249
+ }
250
+ async function defaultListActiveCollabSessions(projectRoot, filter) {
251
+ const namespaceKey = namespaceKeyFor(projectRoot, filter);
252
+ if (!namespaceKey)
253
+ return [];
254
+ try {
255
+ const client = createRegistryClient({ baseUrl: registryBaseUrl(projectRoot), namespaceKey });
256
+ return (await client.listRoomsByOwner({ ...filter, namespaceKey })).map(collabFromRegistryEntry);
257
+ } catch {
258
+ return [];
259
+ }
260
+ }
261
+ function matchesRun(collab, runId, taskId) {
262
+ if (collab.sessionId === runId)
263
+ return true;
264
+ return Boolean(taskId && collab.title?.includes(taskId));
265
+ }
266
+ function runDiscoveryStream(projectRoot) {
267
+ const namespaceKey = namespaceKeyFor(projectRoot);
268
+ if (!namespaceKey)
269
+ return Stream2.never;
270
+ return subscribeDiscovery(namespaceKey, null, { baseUrl: registryBaseUrl(projectRoot) }).pipe(Stream2.map(() => {
271
+ return;
272
+ }));
273
+ }
274
+ function registrySnapshotStream(projectRoot, filter = {}) {
275
+ const namespaceKey = namespaceKeyFor(projectRoot, filter);
276
+ if (!namespaceKey)
277
+ return Stream2.never;
278
+ return subscribeDiscovery(namespaceKey, filter.selectedRepo ?? null, { baseUrl: registryBaseUrl(projectRoot) });
279
+ }
280
+ var DEFAULT_ATTACH_TIMEOUT_MS = 15000;
281
+ async function attachViaRelay(input, deps = {}) {
282
+ const timeoutMs = input.timeoutMs ?? DEFAULT_ATTACH_TIMEOUT_MS;
283
+ const root = input.projectRoot ?? process.cwd();
284
+ const listActive = deps.listActiveCollabSessions ?? ((filter) => defaultListActiveCollabSessions(root, filter));
285
+ const checkOnce = async () => {
286
+ const live = await listActive(input.identityFilter);
287
+ const exact = live.find((collab) => collab.sessionId === input.runId && !collab.stale);
288
+ const match = exact ?? live.find((collab) => matchesRun(collab, input.runId, input.taskId) && !collab.stale);
289
+ if (!match)
290
+ return null;
291
+ return { sessionPath: match.sessionPath ?? null, joinLink: match.joinLink ?? null, collabSessionId: match.sessionId ?? null };
292
+ };
293
+ const immediate = await checkOnce();
294
+ if (immediate)
295
+ return immediate;
296
+ const events = deps.discoveryStream ? deps.discoveryStream(root) : runDiscoveryStream(root);
297
+ return await Effect2.runPromise(events.pipe(Stream2.mapEffect(() => Effect2.promise(checkOnce)), Stream2.filter((m) => m !== null), Stream2.runHead, Effect2.timeout(Duration.millis(timeoutMs)), Effect2.map(Option.getOrNull), Effect2.catch(() => Effect2.succeed(null))));
298
+ }
299
+ export {
300
+ runDiscoveryStream,
301
+ registrySnapshotStream,
302
+ registryBaseUrl,
303
+ matchesRun,
304
+ listActiveRunCollab,
305
+ defaultListActiveCollabSessions,
306
+ collabFromRegistryEntry,
307
+ attachViaRelay
308
+ };
@@ -0,0 +1,47 @@
1
+ import type { ChildProcess, SpawnOptions } from "node:child_process";
2
+ import { type TransportCapability } from "@rig/contracts";
3
+ import { type DispatchTransportPlacement } from "@rig/core/remote-config";
4
+ import type { RegistrySnapshotFrame } from "./relay-registry";
5
+ import { Stream } from "effect";
6
+ export { resolveDispatchTransportPlacement, type DispatchTransportPlacement } from "@rig/core/remote-config";
7
+ export type SpawnFunction = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
8
+ export type DispatchPlacementResolver = (projectRoot: string, env: NodeJS.ProcessEnv) => DispatchTransportPlacement;
9
+ export type DispatchRunOptions = {
10
+ readonly placement?: DispatchTransportPlacement | DispatchPlacementResolver;
11
+ };
12
+ export type SpawnRunProcessDeps = {
13
+ readonly spawn?: SpawnFunction;
14
+ readonly uuid?: () => string;
15
+ readonly resolveRigRunBin?: () => string;
16
+ readonly env?: NodeJS.ProcessEnv;
17
+ readonly resolvePlacement?: DispatchPlacementResolver;
18
+ };
19
+ export type DispatchRunDeps = SpawnRunProcessDeps & {
20
+ readonly spawnRunProcess?: (input: SpawnRunProcessInput, deps?: SpawnRunProcessDeps) => Promise<{
21
+ readonly runId: string;
22
+ }>;
23
+ readonly discoverySnapshots?: (projectRoot: string) => Stream.Stream<RegistrySnapshotFrame>;
24
+ readonly localSessionTimeoutMs?: number;
25
+ readonly remoteSessionTimeoutMs?: number;
26
+ };
27
+ export type SpawnRunProcessInput = {
28
+ readonly projectRoot: string;
29
+ readonly taskId: string;
30
+ readonly title?: string | null;
31
+ readonly model?: string | null;
32
+ readonly prompt?: string | null;
33
+ readonly force?: boolean;
34
+ };
35
+ export declare function resolveRigRunBin(): string;
36
+ /**
37
+ * Inner process spawn. Its runId is the transient worktree handle/correlation key.
38
+ * Operator-facing dispatch uses dispatchRun(), which waits for and returns the OMP session id.
39
+ */
40
+ export declare function spawnRunProcess(input: SpawnRunProcessInput, deps?: SpawnRunProcessDeps): Promise<{
41
+ readonly runId: string;
42
+ }>;
43
+ export declare function sessionIdFromDispatchSnapshot(snapshot: RegistrySnapshotFrame, dispatchHandle: string): string | null;
44
+ export declare function dispatchRun(input: SpawnRunProcessInput, deps?: DispatchRunDeps): Promise<{
45
+ readonly runId: string;
46
+ }>;
47
+ export declare function createPlacementRunTransport(deps?: DispatchRunDeps): TransportCapability;