@h-rig/server 0.0.6-alpha.0

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.
Files changed (60) hide show
  1. package/README.md +14 -0
  2. package/dist/src/bootstrap.js +161 -0
  3. package/dist/src/index.js +13153 -0
  4. package/dist/src/inspector/agent-runtime.js +1077 -0
  5. package/dist/src/inspector/analysis.js +41 -0
  6. package/dist/src/inspector/discovery.js +137 -0
  7. package/dist/src/inspector/journal.js +518 -0
  8. package/dist/src/inspector/mission.js +562 -0
  9. package/dist/src/inspector/prompt.js +97 -0
  10. package/dist/src/inspector/provider-session.js +65 -0
  11. package/dist/src/inspector/reconcile.js +118 -0
  12. package/dist/src/inspector/review.js +13 -0
  13. package/dist/src/inspector/service.js +1759 -0
  14. package/dist/src/inspector/skills.js +155 -0
  15. package/dist/src/inspector/tools.js +1592 -0
  16. package/dist/src/inspector/types.js +1 -0
  17. package/dist/src/inspector/upstream-sync.js +479 -0
  18. package/dist/src/orchestration.js +402 -0
  19. package/dist/src/remote.js +123 -0
  20. package/dist/src/scheduler.js +84 -0
  21. package/dist/src/server-helpers/broadcasters.js +161 -0
  22. package/dist/src/server-helpers/conversation-snapshot.js +382 -0
  23. package/dist/src/server-helpers/event-emitter.js +41 -0
  24. package/dist/src/server-helpers/github-auth-store.js +155 -0
  25. package/dist/src/server-helpers/github-credentials.js +38 -0
  26. package/dist/src/server-helpers/github-project-status-sync.js +196 -0
  27. package/dist/src/server-helpers/github-projects.js +147 -0
  28. package/dist/src/server-helpers/github-reconciler.js +89 -0
  29. package/dist/src/server-helpers/http-router.js +3781 -0
  30. package/dist/src/server-helpers/http-utils.js +135 -0
  31. package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
  32. package/dist/src/server-helpers/inspector-jobs.js +4145 -0
  33. package/dist/src/server-helpers/issue-analysis.js +362 -0
  34. package/dist/src/server-helpers/normalizers.js +31 -0
  35. package/dist/src/server-helpers/notifications.js +96 -0
  36. package/dist/src/server-helpers/orchestration-ops.js +287 -0
  37. package/dist/src/server-helpers/orchestration.js +39 -0
  38. package/dist/src/server-helpers/plugin-host-cache.js +86 -0
  39. package/dist/src/server-helpers/project-fs-ops.js +194 -0
  40. package/dist/src/server-helpers/project-registry.js +124 -0
  41. package/dist/src/server-helpers/queue-state.js +78 -0
  42. package/dist/src/server-helpers/remote-checkout.js +140 -0
  43. package/dist/src/server-helpers/remote-snapshots.js +119 -0
  44. package/dist/src/server-helpers/run-io.js +262 -0
  45. package/dist/src/server-helpers/run-mutations.js +1784 -0
  46. package/dist/src/server-helpers/run-steering.js +176 -0
  47. package/dist/src/server-helpers/run-writers.js +75 -0
  48. package/dist/src/server-helpers/server-paths.js +27 -0
  49. package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
  50. package/dist/src/server-helpers/snapshot-service.js +1143 -0
  51. package/dist/src/server-helpers/summaries.js +126 -0
  52. package/dist/src/server-helpers/task-config.js +50 -0
  53. package/dist/src/server-helpers/task-projection.js +98 -0
  54. package/dist/src/server-helpers/terminal-runtime.js +156 -0
  55. package/dist/src/server-helpers/terminal-sessions.js +22 -0
  56. package/dist/src/server-helpers/validation-failure.js +31 -0
  57. package/dist/src/server-helpers/ws-router.js +1308 -0
  58. package/dist/src/server.js +12628 -0
  59. package/dist/src/websocket.js +63 -0
  60. package/package.json +33 -0
@@ -0,0 +1,402 @@
1
+ // @bun
2
+ // packages/server/src/orchestration.ts
3
+ function nowIso() {
4
+ return new Date().toISOString();
5
+ }
6
+ function findMutableProject(readModel, projectId) {
7
+ return readModel.projects.find((project) => project.id === projectId) ?? null;
8
+ }
9
+ function findMutableThread(readModel, threadId) {
10
+ return readModel.threads.find((thread) => thread.id === threadId) ?? null;
11
+ }
12
+ function createEmptyOrchestrationReadModel(updatedAt = nowIso()) {
13
+ return {
14
+ snapshotSequence: 0,
15
+ projects: [],
16
+ threads: [],
17
+ updatedAt
18
+ };
19
+ }
20
+ function createEmptyOrchestrationProject(input) {
21
+ const createdAt = input.createdAt ?? nowIso();
22
+ return {
23
+ id: input.id,
24
+ title: input.title,
25
+ workspaceRoot: input.workspaceRoot,
26
+ defaultModel: input.defaultModel ?? null,
27
+ scripts: [],
28
+ createdAt,
29
+ updatedAt: createdAt,
30
+ deletedAt: null
31
+ };
32
+ }
33
+ function createEmptyOrchestrationThread(input) {
34
+ const createdAt = input.createdAt ?? nowIso();
35
+ return {
36
+ id: input.id,
37
+ projectId: input.projectId,
38
+ title: input.title,
39
+ model: input.model,
40
+ runtimeMode: input.runtimeMode,
41
+ interactionMode: input.interactionMode,
42
+ branch: input.branch ?? null,
43
+ worktreePath: input.worktreePath ?? null,
44
+ latestTurn: null,
45
+ createdAt,
46
+ updatedAt: createdAt,
47
+ deletedAt: null,
48
+ messages: [],
49
+ proposedPlans: [],
50
+ activities: [],
51
+ checkpoints: [],
52
+ session: null
53
+ };
54
+ }
55
+ function appendActivity(thread, input) {
56
+ thread.activities = [
57
+ ...thread.activities,
58
+ {
59
+ id: `activity:${thread.id}:${thread.activities.length + 1}`,
60
+ tone: input.tone ?? "info",
61
+ kind: input.kind,
62
+ summary: input.summary,
63
+ payload: input.payload,
64
+ turnId: input.turnId ?? null,
65
+ sequence: thread.activities.length,
66
+ createdAt: input.createdAt ?? nowIso()
67
+ }
68
+ ];
69
+ }
70
+ function upsertAssistantMessage(thread, messageId, createdAt) {
71
+ const existing = thread.messages.find((message) => message.id === messageId);
72
+ if (existing) {
73
+ return existing;
74
+ }
75
+ const next = {
76
+ id: messageId,
77
+ role: "assistant",
78
+ text: "",
79
+ attachments: [],
80
+ turnId: thread.latestTurn?.turnId ?? null,
81
+ streaming: true,
82
+ createdAt,
83
+ updatedAt: createdAt
84
+ };
85
+ thread.messages = [...thread.messages, next];
86
+ return next;
87
+ }
88
+ function upsertProposedPlan(thread, proposedPlan) {
89
+ const existingIndex = thread.proposedPlans.findIndex((item) => item.id === proposedPlan.id);
90
+ if (existingIndex >= 0) {
91
+ const next = [...thread.proposedPlans];
92
+ next[existingIndex] = proposedPlan;
93
+ thread.proposedPlans = next;
94
+ return;
95
+ }
96
+ thread.proposedPlans = [...thread.proposedPlans, proposedPlan];
97
+ }
98
+ function applyOrchestrationCommand(readModel, command, updatedAt = nowIso()) {
99
+ const next = {
100
+ snapshotSequence: readModel.snapshotSequence + 1,
101
+ updatedAt,
102
+ projects: readModel.projects.map((project) => ({ ...project, scripts: [...project.scripts] })),
103
+ threads: readModel.threads.map((thread) => ({
104
+ ...thread,
105
+ messages: [...thread.messages],
106
+ proposedPlans: [...thread.proposedPlans],
107
+ activities: [...thread.activities],
108
+ checkpoints: [...thread.checkpoints],
109
+ latestTurn: thread.latestTurn ? { ...thread.latestTurn } : null,
110
+ session: thread.session ? { ...thread.session } : null
111
+ }))
112
+ };
113
+ switch (command.type) {
114
+ case "project.create": {
115
+ const project = findMutableProject(next, command.projectId);
116
+ if (project) {
117
+ project.title = command.title;
118
+ project.workspaceRoot = command.workspaceRoot;
119
+ project.defaultModel = command.defaultModel ?? null;
120
+ project.updatedAt = command.createdAt;
121
+ project.deletedAt = null;
122
+ } else {
123
+ next.projects.push(createEmptyOrchestrationProject({
124
+ id: command.projectId,
125
+ title: command.title,
126
+ workspaceRoot: command.workspaceRoot,
127
+ defaultModel: command.defaultModel ?? null,
128
+ createdAt: command.createdAt
129
+ }));
130
+ }
131
+ return next;
132
+ }
133
+ case "project.meta.update": {
134
+ const project = findMutableProject(next, command.projectId);
135
+ if (!project)
136
+ return next;
137
+ if ("title" in command && command.title)
138
+ project.title = command.title;
139
+ if ("workspaceRoot" in command && command.workspaceRoot)
140
+ project.workspaceRoot = command.workspaceRoot;
141
+ if ("defaultModel" in command)
142
+ project.defaultModel = command.defaultModel ?? null;
143
+ if (Array.isArray(command.scripts))
144
+ project.scripts = [...command.scripts];
145
+ project.updatedAt = updatedAt;
146
+ return next;
147
+ }
148
+ case "project.delete": {
149
+ next.projects = next.projects.filter((project) => project.id !== command.projectId);
150
+ next.threads = next.threads.filter((thread) => thread.projectId !== command.projectId);
151
+ return next;
152
+ }
153
+ case "thread.create": {
154
+ const thread = findMutableThread(next, command.threadId);
155
+ if (thread) {
156
+ thread.projectId = command.projectId;
157
+ thread.title = command.title;
158
+ thread.model = command.model;
159
+ thread.runtimeMode = command.runtimeMode;
160
+ thread.interactionMode = command.interactionMode;
161
+ thread.branch = command.branch ?? null;
162
+ thread.worktreePath = command.worktreePath ?? null;
163
+ thread.updatedAt = command.createdAt;
164
+ thread.deletedAt = null;
165
+ } else {
166
+ next.threads.push(createEmptyOrchestrationThread({
167
+ id: command.threadId,
168
+ projectId: command.projectId,
169
+ title: command.title,
170
+ model: command.model,
171
+ runtimeMode: command.runtimeMode,
172
+ interactionMode: command.interactionMode,
173
+ branch: command.branch ?? null,
174
+ worktreePath: command.worktreePath ?? null,
175
+ createdAt: command.createdAt
176
+ }));
177
+ }
178
+ return next;
179
+ }
180
+ case "thread.meta.update": {
181
+ const thread = findMutableThread(next, command.threadId);
182
+ if (!thread)
183
+ return next;
184
+ if ("title" in command && command.title)
185
+ thread.title = command.title;
186
+ if ("model" in command && command.model)
187
+ thread.model = command.model;
188
+ if ("branch" in command)
189
+ thread.branch = command.branch ?? null;
190
+ if ("worktreePath" in command)
191
+ thread.worktreePath = command.worktreePath ?? null;
192
+ thread.updatedAt = updatedAt;
193
+ return next;
194
+ }
195
+ case "thread.runtime-mode.set":
196
+ case "thread.interaction-mode.set":
197
+ case "thread.session.stop":
198
+ case "thread.turn.start":
199
+ case "thread.turn.interrupt":
200
+ case "thread.approval.respond":
201
+ case "thread.user-input.respond":
202
+ case "thread.checkpoint.revert": {
203
+ const thread = findMutableThread(next, command.threadId);
204
+ if (!thread)
205
+ return next;
206
+ const createdAt = "createdAt" in command ? command.createdAt : updatedAt;
207
+ if (command.type === "thread.runtime-mode.set") {
208
+ thread.runtimeMode = command.runtimeMode;
209
+ }
210
+ if (command.type === "thread.interaction-mode.set") {
211
+ thread.interactionMode = command.interactionMode;
212
+ }
213
+ if (command.type === "thread.turn.start") {
214
+ const turnId = `turn-${thread.id}-${thread.messages.length + 1}`;
215
+ thread.messages = [
216
+ ...thread.messages,
217
+ {
218
+ id: command.message.messageId,
219
+ role: "user",
220
+ text: command.message.text,
221
+ attachments: [...command.message.attachments ?? []],
222
+ turnId,
223
+ streaming: false,
224
+ createdAt,
225
+ updatedAt: createdAt
226
+ }
227
+ ];
228
+ thread.latestTurn = {
229
+ turnId,
230
+ state: "running",
231
+ requestedAt: createdAt,
232
+ startedAt: createdAt,
233
+ completedAt: null,
234
+ assistantMessageId: null
235
+ };
236
+ thread.session = {
237
+ threadId: thread.id,
238
+ status: "running",
239
+ providerName: command.provider ?? null,
240
+ runtimeMode: command.runtimeMode,
241
+ activeTurnId: thread.latestTurn.turnId,
242
+ lastError: null,
243
+ updatedAt: createdAt
244
+ };
245
+ }
246
+ if (command.type === "thread.turn.interrupt") {
247
+ if (thread.latestTurn) {
248
+ thread.latestTurn = {
249
+ ...thread.latestTurn,
250
+ state: "interrupted",
251
+ completedAt: createdAt
252
+ };
253
+ }
254
+ if (thread.session) {
255
+ thread.session = {
256
+ ...thread.session,
257
+ status: "interrupted",
258
+ updatedAt: createdAt
259
+ };
260
+ }
261
+ }
262
+ if (command.type === "thread.session.stop" && thread.session) {
263
+ thread.session = {
264
+ ...thread.session,
265
+ status: "stopped",
266
+ activeTurnId: null,
267
+ updatedAt: createdAt
268
+ };
269
+ }
270
+ appendActivity(thread, {
271
+ kind: command.type,
272
+ summary: command.type.replace(/^thread\./, "").replaceAll(".", " "),
273
+ payload: command,
274
+ turnId: thread.latestTurn?.turnId ?? null,
275
+ createdAt
276
+ });
277
+ thread.updatedAt = createdAt;
278
+ return next;
279
+ }
280
+ case "thread.session.set": {
281
+ const thread = findMutableThread(next, command.threadId);
282
+ if (!thread)
283
+ return next;
284
+ thread.session = command.session;
285
+ thread.updatedAt = command.createdAt;
286
+ return next;
287
+ }
288
+ case "thread.message.assistant.delta": {
289
+ const thread = findMutableThread(next, command.threadId);
290
+ if (!thread)
291
+ return next;
292
+ upsertAssistantMessage(thread, command.messageId, command.createdAt);
293
+ thread.messages = thread.messages.map((message) => message.id === command.messageId ? {
294
+ ...message,
295
+ text: message.text + command.delta,
296
+ turnId: command.turnId ?? message.turnId,
297
+ streaming: true,
298
+ updatedAt: command.createdAt
299
+ } : message);
300
+ thread.updatedAt = command.createdAt;
301
+ return next;
302
+ }
303
+ case "thread.message.assistant.complete": {
304
+ const thread = findMutableThread(next, command.threadId);
305
+ if (!thread)
306
+ return next;
307
+ upsertAssistantMessage(thread, command.messageId, command.createdAt);
308
+ thread.messages = thread.messages.map((message) => message.id === command.messageId ? {
309
+ ...message,
310
+ turnId: command.turnId ?? message.turnId,
311
+ streaming: false,
312
+ updatedAt: command.createdAt
313
+ } : message);
314
+ if (thread.latestTurn) {
315
+ thread.latestTurn = {
316
+ ...thread.latestTurn,
317
+ state: "completed",
318
+ completedAt: command.createdAt,
319
+ assistantMessageId: command.messageId
320
+ };
321
+ }
322
+ if (thread.session) {
323
+ thread.session = {
324
+ ...thread.session,
325
+ status: "ready",
326
+ activeTurnId: null,
327
+ updatedAt: command.createdAt
328
+ };
329
+ }
330
+ thread.updatedAt = command.createdAt;
331
+ return next;
332
+ }
333
+ case "thread.proposed-plan.upsert": {
334
+ const thread = findMutableThread(next, command.threadId);
335
+ if (!thread)
336
+ return next;
337
+ upsertProposedPlan(thread, command.proposedPlan);
338
+ thread.updatedAt = command.createdAt;
339
+ return next;
340
+ }
341
+ case "thread.turn.diff.complete": {
342
+ const thread = findMutableThread(next, command.threadId);
343
+ if (!thread)
344
+ return next;
345
+ thread.checkpoints = [
346
+ ...thread.checkpoints.filter((checkpoint) => checkpoint.turnId !== command.turnId),
347
+ {
348
+ turnId: command.turnId,
349
+ checkpointTurnCount: command.checkpointTurnCount,
350
+ checkpointRef: command.checkpointRef,
351
+ status: command.status,
352
+ files: [...command.files],
353
+ assistantMessageId: command.assistantMessageId ?? null,
354
+ completedAt: command.completedAt
355
+ }
356
+ ];
357
+ if (thread.latestTurn?.turnId === command.turnId) {
358
+ thread.latestTurn = {
359
+ ...thread.latestTurn,
360
+ state: "completed",
361
+ completedAt: command.completedAt,
362
+ assistantMessageId: command.assistantMessageId ?? thread.latestTurn.assistantMessageId
363
+ };
364
+ }
365
+ thread.updatedAt = command.createdAt;
366
+ return next;
367
+ }
368
+ case "thread.activity.append": {
369
+ const thread = findMutableThread(next, command.threadId);
370
+ if (!thread)
371
+ return next;
372
+ thread.activities = [...thread.activities, command.activity];
373
+ thread.updatedAt = command.createdAt;
374
+ return next;
375
+ }
376
+ case "thread.revert.complete": {
377
+ const thread = findMutableThread(next, command.threadId);
378
+ if (!thread)
379
+ return next;
380
+ appendActivity(thread, {
381
+ kind: command.type,
382
+ summary: "revert complete",
383
+ payload: { turnCount: command.turnCount },
384
+ createdAt: command.createdAt
385
+ });
386
+ thread.updatedAt = command.createdAt;
387
+ return next;
388
+ }
389
+ case "thread.delete": {
390
+ next.threads = next.threads.filter((thread) => thread.id !== command.threadId);
391
+ return next;
392
+ }
393
+ default:
394
+ return next;
395
+ }
396
+ }
397
+ export {
398
+ createEmptyOrchestrationThread,
399
+ createEmptyOrchestrationReadModel,
400
+ createEmptyOrchestrationProject,
401
+ applyOrchestrationCommand
402
+ };
@@ -0,0 +1,123 @@
1
+ // @bun
2
+ // packages/server/src/remote.ts
3
+ function createConnectedRemoteConnection(endpointId, connectedAt = new Date().toISOString()) {
4
+ return {
5
+ endpointId,
6
+ status: "connected",
7
+ error: null,
8
+ connectedAt,
9
+ tokenExpiresAt: null,
10
+ latencyMs: null,
11
+ subscribedEvents: []
12
+ };
13
+ }
14
+ function createDisconnectedRemoteConnection(endpointId) {
15
+ return {
16
+ endpointId,
17
+ status: "disconnected",
18
+ error: null,
19
+ connectedAt: null,
20
+ tokenExpiresAt: null,
21
+ latencyMs: null,
22
+ subscribedEvents: []
23
+ };
24
+ }
25
+ function registerRemoteHost(input, acceptedAt = new Date().toISOString()) {
26
+ return {
27
+ workspaceId: input.workspaceId,
28
+ hostId: input.hostId,
29
+ name: input.name,
30
+ baseUrl: input.baseUrl,
31
+ workspacePath: input.workspacePath ?? null,
32
+ transport: input.transport ?? "websocket",
33
+ hostname: input.hostname ?? null,
34
+ region: input.region ?? null,
35
+ labels: [...input.labels ?? []],
36
+ capabilities: [...input.capabilities ?? []],
37
+ runtimeAdapters: [...input.runtimeAdapters ?? []],
38
+ status: input.status ?? "ready",
39
+ currentLeaseCount: input.currentLeaseCount ?? 0,
40
+ registeredAt: acceptedAt,
41
+ lastHeartbeatAt: acceptedAt
42
+ };
43
+ }
44
+ function heartbeatRemoteHost(host, input, acceptedAt = new Date().toISOString()) {
45
+ return {
46
+ ...host,
47
+ status: input.status ?? host.status,
48
+ currentLeaseCount: input.currentLeaseCount ?? host.currentLeaseCount,
49
+ lastHeartbeatAt: input.observedAt ?? acceptedAt
50
+ };
51
+ }
52
+ function createWorkspaceRemoteHostSummary(host, manifestPath) {
53
+ return {
54
+ id: host.hostId,
55
+ name: host.name,
56
+ baseUrl: host.baseUrl,
57
+ workspacePath: host.workspacePath,
58
+ transport: host.transport,
59
+ hostname: host.hostname,
60
+ region: host.region,
61
+ labels: [...host.labels],
62
+ capabilities: [...host.capabilities],
63
+ runtimeAdapters: [...host.runtimeAdapters],
64
+ status: host.status,
65
+ currentLeaseCount: host.currentLeaseCount,
66
+ lastHeartbeatAt: host.lastHeartbeatAt,
67
+ registeredAt: host.registeredAt,
68
+ manifestPath
69
+ };
70
+ }
71
+ function createRemoteLeaseAssignment(args) {
72
+ const claimedAt = args.claimedAt ?? new Date().toISOString();
73
+ const leaseId = args.leaseId ?? `lease-${args.run.runId}`;
74
+ return {
75
+ lease: {
76
+ leaseId,
77
+ runId: args.run.runId,
78
+ workspaceId: args.run.workspaceId,
79
+ title: args.run.title,
80
+ runtimeAdapter: args.run.runtimeAdapter,
81
+ model: args.run.model,
82
+ runtimeMode: args.run.runtimeMode,
83
+ interactionMode: args.run.interactionMode,
84
+ executionTarget: "remote",
85
+ remoteHostId: args.hostId,
86
+ claimedAt
87
+ },
88
+ bundle: {
89
+ workspaceId: args.run.workspaceId,
90
+ runId: args.run.runId,
91
+ leaseId,
92
+ workspacePath: args.workspacePath ?? null,
93
+ runtimeAdapter: args.run.runtimeAdapter,
94
+ model: args.run.model,
95
+ runtimeMode: args.run.runtimeMode,
96
+ interactionMode: args.run.interactionMode,
97
+ prompt: args.prompt,
98
+ conversation: [...args.conversation ?? []],
99
+ taskId: args.run.taskId,
100
+ taskTitle: args.run.title
101
+ }
102
+ };
103
+ }
104
+ function createRemoteOrchestrationSummary(input) {
105
+ return {
106
+ orchestrationId: input.orchestrationId,
107
+ endpointId: input.endpointId,
108
+ status: input.status,
109
+ totalTasks: input.totalTasks,
110
+ totalGroups: input.totalGroups,
111
+ maxParallelism: input.maxParallelism,
112
+ startedAt: input.startedAt ?? new Date().toISOString()
113
+ };
114
+ }
115
+ export {
116
+ registerRemoteHost,
117
+ heartbeatRemoteHost,
118
+ createWorkspaceRemoteHostSummary,
119
+ createRemoteOrchestrationSummary,
120
+ createRemoteLeaseAssignment,
121
+ createDisconnectedRemoteConnection,
122
+ createConnectedRemoteConnection
123
+ };
@@ -0,0 +1,84 @@
1
+ // @bun
2
+ // packages/server/src/scheduler.ts
3
+ import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
4
+ var TERMINAL_RUN_STATUSES = new Set(["done", "completed", "error", "failed", "stopped", "cancelled"]);
5
+ var RUNNABLE_TASK_STATUSES = new Set(["draft", "open", "ready", "queued"]);
6
+ var REMOTE_READY_STATUSES = new Set(["ready", "idle", "connected"]);
7
+ function resolveLocalSchedulerWorkerCount(env = process.env) {
8
+ const raw = env.RIG_SCHEDULER_LOCAL_WORKERS?.trim();
9
+ if (!raw) {
10
+ return Number.MAX_SAFE_INTEGER;
11
+ }
12
+ const parsed = Number.parseInt(raw, 10);
13
+ if (!Number.isFinite(parsed)) {
14
+ return Number.MAX_SAFE_INTEGER;
15
+ }
16
+ return Math.max(0, parsed);
17
+ }
18
+ function planSchedulerWork(input) {
19
+ const taskById = new Map(input.tasks.map((task) => [task.id, task]));
20
+ const activeTaskIds = new Set(input.runs.filter((run) => isActiveRun(run) && typeof run.taskId === "string" && run.taskId.length > 0).map((run) => run.taskId));
21
+ const activeLocalRuns = input.runs.filter((run) => isActiveRun(run) && typeof run.taskId === "string" && run.taskId.length > 0 && !isRemoteRun(run));
22
+ const occupiedRemoteHosts = new Set(input.runs.filter((run) => isActiveRun(run) && isRemoteRun(run) && typeof run.hostId === "string" && run.hostId.length > 0).map((run) => run.hostId));
23
+ const pruneTaskIds = [];
24
+ const eligibleQueue = input.queue.filter((entry) => {
25
+ const task = taskById.get(entry.taskId);
26
+ const normalizedStatus = task ? normalizeTaskLifecycleStatus(task.status) : null;
27
+ if (!task || !normalizedStatus || !RUNNABLE_TASK_STATUSES.has(normalizedStatus)) {
28
+ pruneTaskIds.push(entry.taskId);
29
+ return false;
30
+ }
31
+ if (activeTaskIds.has(entry.taskId)) {
32
+ pruneTaskIds.push(entry.taskId);
33
+ return false;
34
+ }
35
+ return true;
36
+ });
37
+ let freeLocalWorkers = Math.max(0, input.localWorkerCount - activeLocalRuns.length);
38
+ const dispatches = [];
39
+ const deferredForRemote = [];
40
+ for (const entry of eligibleQueue) {
41
+ if (freeLocalWorkers > 0) {
42
+ dispatches.push({ taskId: entry.taskId, workerKind: "local" });
43
+ freeLocalWorkers -= 1;
44
+ continue;
45
+ }
46
+ deferredForRemote.push(entry);
47
+ }
48
+ const availableRemoteWorkers = input.remoteWorkers.filter((worker) => REMOTE_READY_STATUSES.has(worker.status)).filter((worker) => !occupiedRemoteHosts.has(worker.hostId)).toSorted((left, right) => {
49
+ if (left.currentLeaseCount !== right.currentLeaseCount) {
50
+ return left.currentLeaseCount - right.currentLeaseCount;
51
+ }
52
+ return left.registeredAt.localeCompare(right.registeredAt) || left.hostId.localeCompare(right.hostId);
53
+ });
54
+ for (let index = 0;index < Math.min(deferredForRemote.length, availableRemoteWorkers.length); index += 1) {
55
+ const entry = deferredForRemote[index];
56
+ const worker = availableRemoteWorkers[index];
57
+ if (!entry || !worker) {
58
+ continue;
59
+ }
60
+ dispatches.push({
61
+ taskId: entry.taskId,
62
+ workerKind: "remote",
63
+ hostId: worker.hostId
64
+ });
65
+ }
66
+ return {
67
+ pruneTaskIds: Array.from(new Set(pruneTaskIds)),
68
+ dispatches
69
+ };
70
+ }
71
+ function isActiveRun(run) {
72
+ const status = normalizeStatus(run.status);
73
+ return !TERMINAL_RUN_STATUSES.has(status);
74
+ }
75
+ function isRemoteRun(run) {
76
+ return normalizeStatus(run.mode) === "remote";
77
+ }
78
+ function normalizeStatus(value) {
79
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
80
+ }
81
+ export {
82
+ resolveLocalSchedulerWorkerCount,
83
+ planSchedulerWork
84
+ };