@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.
- package/README.md +1 -0
- package/dist/relay-registry/src/client.d.ts +43 -0
- package/dist/relay-registry/src/schema.d.ts +317 -0
- package/dist/src/discovery.d.ts +33 -0
- package/dist/src/discovery.js +308 -0
- package/dist/src/dispatch.d.ts +47 -0
- package/dist/src/dispatch.js +915 -0
- package/dist/src/identity-env.d.ts +13 -0
- package/dist/src/identity-env.js +45 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +943 -0
- package/dist/src/plugin.d.ts +12 -0
- package/dist/src/plugin.js +923 -0
- package/dist/src/relay-registry.d.ts +3 -0
- package/dist/src/relay-registry.js +284 -0
- package/dist/src/run-discovery-bus.d.ts +32 -0
- package/dist/src/run-discovery-bus.js +24 -0
- package/dist/src/transport-provider.d.ts +9 -0
- package/dist/src/transport-provider.js +915 -0
- package/package.json +57 -0
|
@@ -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;
|