@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.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +35 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +610 -201
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +24 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +85 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +60 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +413 -61
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/resourceInventoryRpc.js +85 -0
- package/dist/controller/resourceInventoryWorker.js +50 -0
- package/dist/controller/runtime.js +238 -22
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +549 -42
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/boundedRpc.js +475 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/setup/setupCommand.js +27 -4
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +769 -1
- package/dist/storage/persistenceWorker.js +194 -0
- package/dist/storage/sqliteSchema.js +705 -0
- package/dist/storage/sqliteStore.js +1695 -0
- package/dist/storage/storageVersions.js +9 -2
- package/dist/storage/storeRpc.js +298 -0
- package/dist/storage/taskStore.js +982 -21
- package/dist/storage/upgrade/homeClassification.js +157 -12
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +343 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- 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
|
|
31
|
-
const
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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,
|
|
619
|
+
function inspectHomePanes(home, environment, warnings) {
|
|
580
620
|
try {
|
|
581
|
-
|
|
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
|
+
});
|