@zq-silk/yui 0.5.3 → 0.6.1

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 (157) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/agentConfigurationPicker.js +1 -1
  4. package/dist/cli/commandCatalog.js +251 -13
  5. package/dist/cli/updateOrchestrator.js +8 -0
  6. package/dist/cli/updatePorts.js +76 -22
  7. package/dist/cli.js +264 -20
  8. package/dist/commands/configCommands.js +83 -9
  9. package/dist/commands/controllerCommands.js +103 -0
  10. package/dist/commands/deliveryGuardPreflight.js +35 -0
  11. package/dist/commands/durableJobCommands.js +231 -0
  12. package/dist/commands/executionAuditCommands.js +193 -0
  13. package/dist/commands/grantCommands.js +374 -0
  14. package/dist/commands/projectCommands.js +119 -81
  15. package/dist/commands/releaseCommands.js +444 -0
  16. package/dist/commands/resourcesCommands.js +274 -0
  17. package/dist/commands/sessionCommands.js +104 -0
  18. package/dist/commands/taskActor.js +117 -0
  19. package/dist/commands/taskChangeSetCommands.js +60 -0
  20. package/dist/commands/taskCommands.js +610 -201
  21. package/dist/commands/taskCompletionGate.js +78 -1
  22. package/dist/commands/taskContextCommand.js +24 -6
  23. package/dist/commands/taskInputCommands.js +1 -1
  24. package/dist/commands/taskIntegrationCommands.js +136 -33
  25. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  26. package/dist/commands/taskNextActionCommand.js +85 -0
  27. package/dist/commands/taskOverlapCommands.js +120 -0
  28. package/dist/commands/taskOverviewCommand.js +36 -8
  29. package/dist/commands/telemetryCommands.js +330 -0
  30. package/dist/commands/workflowCommands.js +415 -0
  31. package/dist/config/yuiConfig.js +60 -0
  32. package/dist/controller/clientRuntime.js +42 -1
  33. package/dist/controller/controller.js +413 -61
  34. package/dist/controller/controllerMain.js +25 -2
  35. package/dist/controller/domainIdentity.js +16 -8
  36. package/dist/controller/ephemeralResourceReaper.js +2 -1
  37. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  38. package/dist/controller/handoverCandidate.js +168 -0
  39. package/dist/controller/jobClient.js +102 -0
  40. package/dist/controller/jobControl.js +613 -0
  41. package/dist/controller/jobSupervisor.js +498 -0
  42. package/dist/controller/providerHookRunFence.js +34 -5
  43. package/dist/controller/resourceCleanupLinux.js +18 -9
  44. package/dist/controller/resourceInventoryLinux.js +90 -39
  45. package/dist/controller/resourceInventoryRpc.js +85 -0
  46. package/dist/controller/resourceInventoryWorker.js +50 -0
  47. package/dist/controller/runtime.js +238 -22
  48. package/dist/controller/runtimeEventInbox.js +234 -57
  49. package/dist/controller/runtimeEventProcessor.js +549 -42
  50. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  51. package/dist/core/boundedRpc.js +475 -0
  52. package/dist/core/controllerServer.js +416 -27
  53. package/dist/core/controllerTelemetry.js +167 -0
  54. package/dist/doctor/doctor.js +113 -16
  55. package/dist/domain/validation.js +9 -0
  56. package/dist/execution/executionGroup.js +40 -3
  57. package/dist/executor/agentExecutor.js +6 -3
  58. package/dist/executor/effectiveLaunch.js +52 -0
  59. package/dist/executor/executorRegistry.js +50 -0
  60. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  61. package/dist/grant/capabilityGrant.js +282 -0
  62. package/dist/integration/changeSet.js +16 -3
  63. package/dist/integration/changeSetManifest.js +46 -0
  64. package/dist/integration/gitIntegrationService.js +528 -147
  65. package/dist/integration/integrationAttempt.js +54 -5
  66. package/dist/integration/integrationQueueEntry.js +221 -0
  67. package/dist/integration/integrationQueueService.js +955 -0
  68. package/dist/integration/manifestTags.js +99 -0
  69. package/dist/integration/overlapDiagnostics.js +211 -0
  70. package/dist/job/durableJob.js +449 -0
  71. package/dist/job/jobRunner.js +350 -0
  72. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  73. package/dist/lifecycle/providerErrorClass.js +126 -0
  74. package/dist/message/message.js +16 -3
  75. package/dist/observability/executionAudit.js +545 -0
  76. package/dist/observability/faultClassification.js +160 -0
  77. package/dist/observability/runtimeIdentity.js +367 -0
  78. package/dist/release/fakeReleasePorts.js +55 -0
  79. package/dist/release/releaseHandover.js +475 -0
  80. package/dist/release/releaseIdempotencyStore.js +165 -0
  81. package/dist/release/releaseWorkflow.js +459 -0
  82. package/dist/release/releaseWorkflowEngine.js +688 -0
  83. package/dist/release/releaseWorkflowPorts.js +1720 -0
  84. package/dist/release/runtimeRelease.js +495 -0
  85. package/dist/release/workflowFileLock.js +218 -0
  86. package/dist/repository/gitWorkspace.js +177 -1
  87. package/dist/repository/projectMaintenanceLock.js +315 -0
  88. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  89. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  90. package/dist/resources/autoResourceGc.js +116 -0
  91. package/dist/resources/liveReferences.js +574 -0
  92. package/dist/resources/resourceDiscovery.js +477 -0
  93. package/dist/resources/resourceGc.js +645 -0
  94. package/dist/resources/resourceRegistrar.js +256 -0
  95. package/dist/resources/resourceRegistry.js +150 -0
  96. package/dist/resources/resourceRegistryStore.js +41 -0
  97. package/dist/resources/resourceTypes.js +42 -0
  98. package/dist/resources/sqliteResourceRegistry.js +111 -0
  99. package/dist/review/reviewConfig.js +10 -0
  100. package/dist/review/reviewFinding.js +240 -0
  101. package/dist/review/reviewFindingLedger.js +545 -0
  102. package/dist/review/reviewOutcomeClassifier.js +61 -0
  103. package/dist/review/reviewRound.js +56 -4
  104. package/dist/run/agentRun.js +80 -4
  105. package/dist/run/providerRetry.js +84 -0
  106. package/dist/run/providerRetryConfig.js +63 -0
  107. package/dist/run/yieldReceipt.js +65 -0
  108. package/dist/runtime/exactControlPlane.js +79 -2
  109. package/dist/runtime/index.js +4 -0
  110. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  111. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  112. package/dist/runtime/sessionReconciliation.js +93 -0
  113. package/dist/runtime/sessionTerminationGuard.js +211 -0
  114. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  115. package/dist/runtime/tmuxAdapters.js +34 -1
  116. package/dist/scheduler/actionability.js +155 -0
  117. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  118. package/dist/scheduler/activeTaskProgress.js +60 -0
  119. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  120. package/dist/scheduler/roleRunStall.js +135 -29
  121. package/dist/scheduler/taskExecutionProjection.js +11 -0
  122. package/dist/setup/setupCommand.js +27 -4
  123. package/dist/storage/compatibleTaskStore.js +112 -5
  124. package/dist/storage/migration/productionRegistry.js +769 -1
  125. package/dist/storage/persistenceWorker.js +194 -0
  126. package/dist/storage/sqliteSchema.js +705 -0
  127. package/dist/storage/sqliteStore.js +1695 -0
  128. package/dist/storage/storageVersions.js +9 -2
  129. package/dist/storage/storeRpc.js +298 -0
  130. package/dist/storage/taskStore.js +982 -21
  131. package/dist/storage/upgrade/homeClassification.js +157 -12
  132. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  133. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  134. package/dist/storage/upgrade/recordVersions.js +10 -1
  135. package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
  136. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  137. package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
  138. package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
  139. package/dist/task/deliveryGuard.js +226 -0
  140. package/dist/task/nextAction.js +343 -0
  141. package/dist/task/repairWave.js +137 -0
  142. package/dist/task/taskRecordReference.js +6 -1
  143. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  144. package/dist/telemetry/telemetryCompaction.js +251 -0
  145. package/dist/telemetry/telemetryConfig.js +64 -0
  146. package/dist/telemetry/telemetryRouter.js +32 -0
  147. package/dist/telemetry/telemetryStore.js +19 -0
  148. package/dist/telemetry/telemetryWiring.js +33 -0
  149. package/dist/tmux/tmuxManager.js +20 -1
  150. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  151. package/dist/verification/gateArtifact.js +216 -0
  152. package/dist/verification/gateArtifactStore.js +87 -0
  153. package/dist/verification/verificationGateService.js +414 -0
  154. package/dist/verification/verificationPlan.js +308 -0
  155. package/dist/workspace/gitChangeSetCapture.js +12 -2
  156. package/dist/workspace/workItemChangeSetManager.js +60 -3
  157. package/package.json +2 -1
@@ -1,24 +1,26 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
3
  import { basename, join, resolve } from "node:path";
5
4
  import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
6
5
  import { inspectStorageSchema } from "../storage/storageSchema.js";
7
6
  import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
8
7
  import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
9
8
  import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
9
+ import { tmuxSocketDirectory, tmuxSocketEnvironment } from "../tmux/tmuxSocketEndpoint.js";
10
10
  import { readControllerDiscovery } from "../core/controllerClient.js";
11
11
  import { CONTROLLER_DISCOVERY_PATH } from "../core/protocol.js";
12
12
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
13
13
  import { buildControllerResourceInventory } from "./resourceInventory.js";
14
14
  import { CONTROLLER_DOMAIN_PATH, EPHEMERAL_DOMAIN_GRACE_MS, ephemeralDomainFingerprint, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "./domainIdentity.js";
15
+ const LINUX_PROCESS_SCAN_BATCH_SIZE = 64;
16
+ export const INVENTORY_EVENT_LOOP_TURN_BUDGET_MS = 25;
15
17
  export async function scanControllerResourceInventory(options) {
16
18
  const currentHome = resolve(options.currentHome);
17
19
  const environment = options.environment ?? process.env;
18
20
  const observedAt = (options.now ?? (() => new Date()))();
19
21
  const warnings = [];
20
22
  const activeSockets = readActiveUnixSocketPaths(warnings);
21
- const processes = listLinuxProcesses(warnings)
23
+ const processes = (await listLinuxProcesses(warnings))
22
24
  .filter(({ pid }) => pid !== process.pid);
23
25
  const homes = new Set([currentHome]);
24
26
  if (options.scope === "all") {
@@ -27,22 +29,31 @@ export async function scanControllerResourceInventory(options) {
27
29
  homes.add(process.yuiHome);
28
30
  }
29
31
  }
30
- const uid = typeof process.getuid === "function" ? process.getuid() : 0;
31
- const rawTmuxArtifacts = listSocketArtifacts(join(tmpdir(), `tmux-${uid}`), /^yui-[a-f0-9]{24}$/u, "tmux-socket", activeSockets);
32
+ const tmuxDirectory = tmuxSocketDirectory(environment);
33
+ const listTmuxSocketArtifacts = options.listTmuxSocketArtifacts
34
+ ?? listSharedTmuxSocketArtifacts;
35
+ // scope=current owns exactly one YUI_HOME, so only its exact tmux server
36
+ // socket is observable. Enumerating the whole shared directory (readdir +
37
+ // lstat per entry) blocked the event loop for seconds under a large
38
+ // unrelated yui-* population and starved control commands. scope=all keeps
39
+ // the full cross-domain enumeration for global cleanup reporting.
40
+ const rawTmuxArtifacts = options.scope === "all"
41
+ ? listTmuxSocketArtifacts(tmuxDirectory, activeSockets)
42
+ : inspectExactTmuxSocket(currentHome, tmuxDirectory, activeSockets);
32
43
  const homeFacts = [];
33
44
  const associatedArtifacts = new Set();
34
45
  for (const home of [...homes].sort()) {
35
46
  const matchingProcesses = processes.filter(({ yuiHome }) => yuiHome === home);
36
47
  const state = loadHomeState(home, warnings, options);
37
48
  const domain = inspectRuntimeDomain(home, matchingProcesses, state.roles, state.storageStatus, observedAt);
38
- const tmuxSocketPath = join(tmpdir(), `tmux-${uid}`, yuiTmuxServerName(home));
49
+ const tmuxSocketPath = join(tmuxDirectory, yuiTmuxServerName(home));
39
50
  const tmuxArtifact = rawTmuxArtifacts.find(({ path }) => path === tmuxSocketPath);
40
51
  if (tmuxArtifact !== undefined)
41
52
  associatedArtifacts.add(tmuxArtifact.path);
42
53
  const panes = options.panes !== undefined
43
54
  ? options.panes
44
55
  : tmuxArtifact?.active === true
45
- ? inspectHomePanes(home, environment.YUI_TMUX_BIN ?? "tmux", warnings)
56
+ ? inspectHomePanes(home, environment, warnings)
46
57
  : [];
47
58
  const discovery = await inspectDiscovery(home, matchingProcesses, activeSockets);
48
59
  if (discovery.status === "valid"
@@ -164,7 +175,7 @@ export function classifyRuntimeProcess(args, command) {
164
175
  return "agent";
165
176
  return "other";
166
177
  }
167
- function listLinuxProcesses(warnings) {
178
+ async function listLinuxProcesses(warnings) {
168
179
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
169
180
  const clockTicks = readClockTicks();
170
181
  const uptimeMs = readSystemUptimeMs();
@@ -179,21 +190,21 @@ function listLinuxProcesses(warnings) {
179
190
  warnings.push(`Cannot enumerate Linux processes: ${message(error)}`);
180
191
  return [];
181
192
  }
182
- return entries.flatMap((entryName) => {
193
+ const result = [];
194
+ await forEachInEventLoopBatches(entries, LINUX_PROCESS_SCAN_BATCH_SIZE, (entryName) => {
195
+ const pid = linuxProcessEntryPid(entryName);
196
+ if (pid === undefined)
197
+ return;
183
198
  try {
184
- const pid = linuxProcessEntryPid(entryName);
185
- if (pid === undefined)
186
- return [];
187
199
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
188
200
  const processUid = parseStatusNumber(status, "Uid");
189
- if (processUid !== uid)
190
- return [];
191
- const parsed = parseLinuxProcessStat(readFileSync(`/proc/${pid}/stat`, "utf8"), clockTicks, uptimeMs);
192
- const args = splitNullDelimited(readFileSync(`/proc/${pid}/cmdline`));
193
- const command = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
194
- const yuiHome = readYuiHome(pid);
195
- const io = readProcessIo(pid);
196
- return [{
201
+ if (processUid === uid) {
202
+ const parsed = parseLinuxProcessStat(readFileSync(`/proc/${pid}/stat`, "utf8"), clockTicks, uptimeMs);
203
+ const args = splitNullDelimited(readFileSync(`/proc/${pid}/cmdline`));
204
+ const command = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
205
+ const yuiHome = readYuiHome(pid);
206
+ const io = readProcessIo(pid);
207
+ result.push({
197
208
  pid,
198
209
  ppid: parsed.ppid,
199
210
  uid: processUid,
@@ -206,14 +217,35 @@ function listLinuxProcesses(warnings) {
206
217
  cpuTimeMs: parsed.cpuTimeMs,
207
218
  ...(io === undefined ? {} : io),
208
219
  ageMs: parsed.ageMs
209
- }];
220
+ });
221
+ }
210
222
  }
211
223
  catch {
212
224
  // Processes commonly exit during a /proc scan. A point-in-time inventory
213
225
  // omits those races instead of turning them into persistent warnings.
214
- return [];
215
226
  }
216
227
  });
228
+ return result;
229
+ }
230
+ /** Cooperatively visits synchronous inventory entries in bounded turns. */
231
+ export async function forEachInEventLoopBatches(entries, batchSize, visit, timing = {}) {
232
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1) {
233
+ throw new Error("Event-loop batch size must be a positive integer.");
234
+ }
235
+ const now = timing.now ?? (() => performance.now());
236
+ let turnStartedAt = now();
237
+ let entriesInTurn = 0;
238
+ for (let index = 0; index < entries.length; index += 1) {
239
+ visit(entries[index], index);
240
+ entriesInTurn += 1;
241
+ if (index + 1 < entries.length
242
+ && (entriesInTurn >= batchSize
243
+ || now() - turnStartedAt >= INVENTORY_EVENT_LOOP_TURN_BUDGET_MS)) {
244
+ await new Promise((resolve) => setImmediate(resolve));
245
+ turnStartedAt = now();
246
+ entriesInTurn = 0;
247
+ }
248
+ }
217
249
  }
218
250
  /**
219
251
  * Resolve one numeric /proc entry name without asking the filesystem for
@@ -304,28 +336,36 @@ function listSocketArtifacts(directory, pattern, artifactKind, activeSockets) {
304
336
  return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
305
337
  if (!pattern.test(entry.name))
306
338
  return [];
307
- const path = join(directory, entry.name);
308
- let metadata;
309
- try {
310
- metadata = lstatSync(path);
311
- }
312
- catch {
313
- return [];
314
- }
315
- if (!metadata.isSocket())
316
- return [];
317
- return [{
318
- artifactKind,
319
- path,
320
- active: activeSockets.has(path),
321
- fingerprint: statFingerprint(metadata)
322
- }];
339
+ return inspectSocketArtifact(join(directory, entry.name), artifactKind, activeSockets);
323
340
  });
324
341
  }
325
342
  catch {
326
343
  return [];
327
344
  }
328
345
  }
346
+ function listSharedTmuxSocketArtifacts(directory, activeSockets) {
347
+ return listSocketArtifacts(directory, /^yui-[a-f0-9]{24}$/u, "tmux-socket", activeSockets);
348
+ }
349
+ function inspectExactTmuxSocket(currentHome, tmuxDirectory, activeSockets) {
350
+ return inspectSocketArtifact(join(tmuxDirectory, yuiTmuxServerName(currentHome)), "tmux-socket", activeSockets);
351
+ }
352
+ function inspectSocketArtifact(path, artifactKind, activeSockets) {
353
+ let metadata;
354
+ try {
355
+ metadata = lstatSync(path);
356
+ }
357
+ catch {
358
+ return [];
359
+ }
360
+ if (!metadata.isSocket())
361
+ return [];
362
+ return [{
363
+ artifactKind,
364
+ path,
365
+ active: activeSockets.has(path),
366
+ fingerprint: statFingerprint(metadata)
367
+ }];
368
+ }
329
369
  async function inspectDiscovery(home, processes, activeSockets) {
330
370
  const path = join(home, CONTROLLER_DISCOVERY_PATH);
331
371
  try {
@@ -576,9 +616,20 @@ function loadHomeState(home, warnings, options) {
576
616
  return { storageStatus: "invalid", roles: [] };
577
617
  }
578
618
  }
579
- function inspectHomePanes(home, tmuxBin, warnings) {
619
+ function inspectHomePanes(home, environment, warnings) {
580
620
  try {
581
- return new TmuxManager(tmuxBin, new NodeCommandExecutor(), {
621
+ const executor = new NodeCommandExecutor();
622
+ const commandEnvironment = tmuxSocketEnvironment(environment);
623
+ return new TmuxManager(environment.YUI_TMUX_BIN ?? "tmux", {
624
+ run: (command, args, options) => executor.run(command, args, {
625
+ ...options,
626
+ environment: {
627
+ ...commandEnvironment,
628
+ ...options?.environment,
629
+ TMUX_TMPDIR: commandEnvironment.TMUX_TMPDIR
630
+ }
631
+ })
632
+ }, {
582
633
  yuiHome: home
583
634
  }).inspectRolePaneInventory();
584
635
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Bounded RPC client for the resource inventory Worker Thread
3
+ * (task-21, work-item-6).
4
+ *
5
+ * The `/proc` scanning in `resourceInventoryLinux` is blocking IO (hundreds of
6
+ * `readFileSync` calls per pass, `spawnSync`, tmux inspection). When the worker
7
+ * backend is active (`YUI_STORE_BACKEND=sqlite` + `YUI_STORE_WORKER=1`, the same
8
+ * flag as the persistence worker, §6), the Controller runtime and the ephemeral
9
+ * reaper scan through this client instead: the scan runs in the inventory
10
+ * worker (its own thread), the main event loop stays free, and the result is
11
+ * posted back at the same cadence (design §3.3).
12
+ *
13
+ * The backpressure, cancellation, and fault-boundary machinery is the shared
14
+ * `core/boundedRpc` client; this module adds the inventory dialect:
15
+ *
16
+ * main -> worker: init | scan | cancel | shutdown
17
+ * worker -> main: ready | result | error
18
+ *
19
+ * Only structured-cloneable scan options cross the port (the file-backed
20
+ * `inspectStorage` / `openCompatibleStore` / `now` seams stay on the direct
21
+ * path, which the file backend still uses). The inventory worker never touches
22
+ * the persistence worker's database connection (§3.3): it is a separate worker
23
+ * with a separate concern.
24
+ */
25
+ import { BoundedRpcClient, deserializeError, nextRequestId } from "../core/boundedRpc.js";
26
+ // -- Protocol adapter ---------------------------------------------------------
27
+ const inventoryProtocol = {
28
+ initRequest: () => ({ kind: "init" }),
29
+ cancelRequest: (requestId) => ({ kind: "cancel", requestId }),
30
+ shutdownRequest: () => ({ kind: "shutdown" }),
31
+ isReady: (response) => response.kind === "ready",
32
+ responseRequestId: (response) => {
33
+ if (response.kind === "ready") {
34
+ throw new Error("ready response has no requestId.");
35
+ }
36
+ return response.requestId;
37
+ },
38
+ settle: (response, settlement) => {
39
+ if (response.kind === "result") {
40
+ settlement.resolve(response.inventory);
41
+ return;
42
+ }
43
+ if (response.kind === "error") {
44
+ settlement.reject(deserializeError(response.error));
45
+ }
46
+ }
47
+ };
48
+ // -- Client ------------------------------------------------------------------
49
+ /**
50
+ * The main-thread client for the inventory worker. `scan` runs the full
51
+ * `scanControllerResourceInventory` in the worker and resolves with the same
52
+ * inventory shape the direct call returns; the scheduler and the ephemeral
53
+ * reaper consume it through the same ports as today (§3.3, behavior unchanged).
54
+ */
55
+ export class ResourceInventoryClient {
56
+ #rpc;
57
+ constructor(options = {}) {
58
+ this.#rpc = new BoundedRpcClient(inventoryProtocol, {
59
+ maxInFlight: options.maxInFlight ?? 4,
60
+ maxQueue: options.maxQueue ?? 16,
61
+ workerScript: options.workerScript ?? new URL("./resourceInventoryWorker.js", import.meta.url)
62
+ });
63
+ }
64
+ /** Run one resource inventory scan in the worker. */
65
+ scan(options, rpcOptions) {
66
+ const requestId = nextRequestId();
67
+ return this.#rpc.send(requestId, { kind: "scan", requestId, options }, rpcOptions);
68
+ }
69
+ /** Close the worker. */
70
+ close() {
71
+ return this.#rpc.close();
72
+ }
73
+ /** Currently in-flight scans (metrics/tests). */
74
+ get inFlight() {
75
+ return this.#rpc.inFlight;
76
+ }
77
+ /** Currently queued scans waiting for a slot (metrics/tests). */
78
+ get queueDepth() {
79
+ return this.#rpc.queueDepth;
80
+ }
81
+ /** Test-only fault injection: terminate the worker (the client restarts it). */
82
+ crashForTest() {
83
+ return this.#rpc.crashForTest();
84
+ }
85
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Resource inventory Worker Thread (task-21, work-item-6).
3
+ *
4
+ * Runs the blocking `/proc` scanning of `resourceInventoryLinux` off the main
5
+ * thread. The main thread asks for a scan over a `MessageChannel` port; the
6
+ * worker runs the exact same `scanControllerResourceInventory` the file backend
7
+ * calls directly and posts the inventory back. Because worker threads share the
8
+ * process, the scanner's self-exclusion (`process.pid`, its start identity)
9
+ * keeps excluding the Controller process exactly as on the main thread.
10
+ *
11
+ * The port handshake, dispatch, cancellation observation, and error
12
+ * serialization are the shared `core/boundedRpc` worker host; this module only
13
+ * supplies the inventory dialect. The worker is read-only: it never touches the
14
+ * persistence worker's database connection (§3.3) — it opens the same file
15
+ * store reads the direct scan uses, in its own thread.
16
+ *
17
+ * Protocol (see resourceInventoryRpc.ts):
18
+ * main -> worker: init | scan | cancel | shutdown
19
+ * worker -> main: ready | result | error
20
+ */
21
+ import { runRpcWorker } from "../core/boundedRpc.js";
22
+ import { scanControllerResourceInventory } from "./resourceInventoryLinux.js";
23
+ runRpcWorker({
24
+ kindOf: (request) => (request.kind === "scan" ? "request" : request.kind),
25
+ requestIdOf: (request) => (request.kind === "scan" || request.kind === "cancel" ? request.requestId : undefined),
26
+ init: async () => {
27
+ // No resources to initialize: each scan opens what it needs and closes it.
28
+ },
29
+ handle: async (request) => {
30
+ if (request.kind !== "scan") {
31
+ throw new Error(`Unexpected inventory request: ${request.kind}`);
32
+ }
33
+ return scanControllerResourceInventory(request.options);
34
+ },
35
+ cancel: () => {
36
+ // The /proc scan has no cancellation points. The main thread rejects the
37
+ // aborted call immediately and suppresses the late result; the worker
38
+ // thread finishes the scan on its own time without blocking the main loop.
39
+ },
40
+ shutdown: async () => {
41
+ // Nothing to close: scans own no long-lived resources.
42
+ },
43
+ ready: () => ({ kind: "ready" }),
44
+ result: (requestId, value) => ({
45
+ kind: "result",
46
+ requestId,
47
+ inventory: value
48
+ }),
49
+ error: (requestId, error) => ({ kind: "error", requestId, error })
50
+ });