@zq-silk/yui 0.6.0 → 0.6.2
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/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 +30 -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 +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -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 +100 -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 +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- 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/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -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/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- 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 +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -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 +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -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
|
}
|
|
@@ -7,18 +7,21 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
|
|
|
7
7
|
import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
8
8
|
import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
|
|
9
9
|
import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
10
|
-
import { effectiveLaunchSnapshotsCompatible, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
10
|
+
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
11
11
|
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
12
12
|
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
13
13
|
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
14
14
|
import { SqliteTaskStore } from "../storage/sqliteStore.js";
|
|
15
|
-
import { AsyncTaskStoreClient,
|
|
15
|
+
import { AsyncTaskStoreClient, resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
16
16
|
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
17
17
|
import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
|
|
18
18
|
import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
19
19
|
import { FileTaskRuntimeIsolation, TmuxPromptPushAdapter, TmuxSessionHost } from "../runtime/index.js";
|
|
20
20
|
import { startFileTaskController } from "./controller.js";
|
|
21
21
|
import { FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
22
|
+
import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
|
|
23
|
+
import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
|
|
24
|
+
import { createDurableJobControl } from "./jobControl.js";
|
|
22
25
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
23
26
|
import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver } from "./runtimeEventProcessor.js";
|
|
24
27
|
import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
|
|
@@ -26,13 +29,52 @@ import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./dom
|
|
|
26
29
|
import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
|
|
27
30
|
import { scanControllerResourceInventory } from "./resourceInventoryLinux.js";
|
|
28
31
|
import { ResourceInventoryClient } from "./resourceInventoryRpc.js";
|
|
32
|
+
import { createResourceAutoGc } from "../resources/autoResourceGc.js";
|
|
29
33
|
import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
34
|
+
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
35
|
+
/** Refreshes only the exact Task runtime generation folded by the event transaction. */
|
|
36
|
+
export function refreshAppliedTaskRuntimeDescriptor(store, planner, input) {
|
|
37
|
+
if (input.launchId === undefined)
|
|
38
|
+
return;
|
|
39
|
+
const run = input.runId === undefined
|
|
40
|
+
? null
|
|
41
|
+
: store.getAgentRun(input.taskId, input.runId);
|
|
42
|
+
if (input.runId !== undefined && run === null) {
|
|
43
|
+
throw new Error("Prepared Task runtime generation is not current.");
|
|
44
|
+
}
|
|
45
|
+
// A terminal completion has already settled this exact Run and no later
|
|
46
|
+
// prompt can use its descriptor. Acknowledge the applied provider fact
|
|
47
|
+
// without republishing a dead generation.
|
|
48
|
+
if (run !== null && run.status !== "active")
|
|
49
|
+
return;
|
|
50
|
+
const session = store.getTaskRoleSessionSet(input.taskId, input.roleName)
|
|
51
|
+
?.sessions[input.agentId];
|
|
52
|
+
const effective = run?.effective ?? session?.effective;
|
|
53
|
+
if (effective === undefined
|
|
54
|
+
|| session === undefined
|
|
55
|
+
|| session.agentId !== input.agentId
|
|
56
|
+
|| session.adapterId !== input.adapterId
|
|
57
|
+
|| session.launchId !== input.launchId
|
|
58
|
+
|| session.nativeSessionId !== input.nativeSessionId
|
|
59
|
+
|| (run !== null && (run.roleName !== input.roleName
|
|
60
|
+
|| run.effective.agentId !== input.agentId
|
|
61
|
+
|| run.effective.adapterId !== input.adapterId))) {
|
|
62
|
+
throw new Error("Prepared Task runtime generation is not current.");
|
|
63
|
+
}
|
|
64
|
+
planner.refreshTaskRuntimeDescriptor({
|
|
65
|
+
...input,
|
|
66
|
+
launchId: input.launchId,
|
|
67
|
+
workspace: effective.workspace.root
|
|
68
|
+
});
|
|
69
|
+
}
|
|
30
70
|
/** Production composition root for the lean FileTaskStore + tmux Controller. */
|
|
31
71
|
export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
32
|
-
// The
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
72
|
+
// The Home decides the backend (Issue 01): a layout-7 Home runs SQLite with
|
|
73
|
+
// the persistence worker on by default; YUI_STORE_WORKER=0/false forces the
|
|
74
|
+
// in-process SQLite connection. The non-worker path opens the Home-decided
|
|
75
|
+
// backend through the compatibility opener (SQLite for layout 7, file store
|
|
76
|
+
// with normalization for older layouts).
|
|
77
|
+
const useWorker = resolveStoreWorkerEnabledForHome(home, options.environment ?? process.env);
|
|
36
78
|
const store = options.store
|
|
37
79
|
?? (useWorker
|
|
38
80
|
// Transitional: the scheduler/planner still use a sync store. The worker
|
|
@@ -55,7 +97,8 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
55
97
|
const inventoryClient = useWorker
|
|
56
98
|
? new ResourceInventoryClient()
|
|
57
99
|
: undefined;
|
|
58
|
-
const schedulerStore = options.schedulerStore
|
|
100
|
+
const schedulerStore = options.schedulerStore
|
|
101
|
+
?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, options.environment ?? process.env));
|
|
59
102
|
const domainIdentity = options.domainIdentity
|
|
60
103
|
?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
|
|
61
104
|
const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
|
|
@@ -73,7 +116,27 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
73
116
|
}
|
|
74
117
|
})
|
|
75
118
|
});
|
|
76
|
-
const
|
|
119
|
+
const sessionOwners = new SessionOwnerReconciliation({
|
|
120
|
+
home,
|
|
121
|
+
store,
|
|
122
|
+
environment: options.environment,
|
|
123
|
+
tmux,
|
|
124
|
+
onWarning: options.onError
|
|
125
|
+
});
|
|
126
|
+
const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
|
|
127
|
+
onHostCreated: ({ binding, pane }) => {
|
|
128
|
+
sessionOwners.recordHostOwner({
|
|
129
|
+
owner: binding.owner,
|
|
130
|
+
agentId: binding.agentId,
|
|
131
|
+
adapterId: binding.adapterId,
|
|
132
|
+
launchId: binding.launchId,
|
|
133
|
+
...(binding.nativeSessionId === undefined
|
|
134
|
+
? {}
|
|
135
|
+
: { nativeSessionId: binding.nativeSessionId }),
|
|
136
|
+
...(pane.pid === undefined ? {} : { panePid: pane.pid })
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
77
140
|
const promptPush = options.promptPush
|
|
78
141
|
?? new TmuxPromptPushAdapter(tmux, agentProcessReadinessProbe);
|
|
79
142
|
const runtimeIsolation = options.runtimeIsolation
|
|
@@ -95,7 +158,19 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
95
158
|
: {
|
|
96
159
|
inspectOwners: (owners) => sessionHost.inspectOwners(owners)
|
|
97
160
|
}),
|
|
98
|
-
stopOwner: (owner) =>
|
|
161
|
+
stopOwner: (owner) => {
|
|
162
|
+
// Issue 03: the durable `stopped` transition is gated on physical
|
|
163
|
+
// exit proof. A blocked result keeps the Session non-terminal and
|
|
164
|
+
// preserves owner records for Operator recovery.
|
|
165
|
+
return sessionOwners.terminateOwner(owner).then((result) => {
|
|
166
|
+
if (result.outcome === "stop-blocked") {
|
|
167
|
+
(options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
|
|
168
|
+
.map(({ record, detail }) => `${record.launchId}: ${detail}`)
|
|
169
|
+
.join("; ")}`));
|
|
170
|
+
}
|
|
171
|
+
return result.outcome === "stop-confirmed";
|
|
172
|
+
});
|
|
173
|
+
},
|
|
99
174
|
...(runtimeIsolation.cleanupTaskLaunch === undefined
|
|
100
175
|
? {}
|
|
101
176
|
: {
|
|
@@ -238,7 +313,61 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
238
313
|
})
|
|
239
314
|
})
|
|
240
315
|
}));
|
|
316
|
+
// Issue 10: automatic Resource GC. The runner self-skips unless
|
|
317
|
+
// resourcesGcMode=quarantine and resourcesGcAutoQuarantine=true, so wiring
|
|
318
|
+
// it unconditionally costs one config read per full pass when disabled.
|
|
319
|
+
const resourceAutoGc = options.resourceAutoGc
|
|
320
|
+
?? createResourceAutoGc({
|
|
321
|
+
home,
|
|
322
|
+
store,
|
|
323
|
+
environment: options.environment
|
|
324
|
+
});
|
|
241
325
|
const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
|
|
326
|
+
// f7/rr5: Share one inbox between the supervisor's terminal channel and
|
|
327
|
+
// the runtime event processor. When a Job reaches a terminal state, the
|
|
328
|
+
// supervisor enqueues a durable-job-terminal event; the processor drains
|
|
329
|
+
// it on the next pass, waking the Controller immediately instead of
|
|
330
|
+
// waiting for the poll interval.
|
|
331
|
+
const runtimeEventInbox = new FileRuntimeEventInbox(home);
|
|
332
|
+
const jobSupervisor = new DurableJobSupervisor({
|
|
333
|
+
store: schedulerStore,
|
|
334
|
+
process: createLinuxProcessPort(),
|
|
335
|
+
artifacts: createFileArtifactPort(home),
|
|
336
|
+
// rr6/f1: Bounded supervision wake. The supervisor signals the Controller
|
|
337
|
+
// after spawning a runner (queued→running adoption) and when a runner
|
|
338
|
+
// exits (terminal harvest), so a quick job converges without waiting for
|
|
339
|
+
// the recovery interval. Closes over runningRuntime, which is assigned
|
|
340
|
+
// once startFileTaskController resolves; a wake during shutdown is a
|
|
341
|
+
// no-op. The recovery interval stays the cross-restart fallback.
|
|
342
|
+
wake: (taskId) => {
|
|
343
|
+
try {
|
|
344
|
+
runningRuntime?.signal(`task:${taskId}`);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Controller stopped; the recovery interval remains the fallback.
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
terminalEvents: {
|
|
351
|
+
deliverTerminalEvent(notice) {
|
|
352
|
+
try {
|
|
353
|
+
runtimeEventInbox.enqueueDurableJobTerminal({
|
|
354
|
+
scope: "task",
|
|
355
|
+
taskId: notice.taskId,
|
|
356
|
+
jobId: notice.jobId,
|
|
357
|
+
status: notice.status,
|
|
358
|
+
outcome: notice.outcome
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
// Best-effort terminal channel: the terminal transition already
|
|
363
|
+
// committed. A delivery failure must not fail the reconcile pass.
|
|
364
|
+
(options.onError ?? (() => undefined))(error);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
onError: options.onError
|
|
369
|
+
});
|
|
370
|
+
const jobControl = createDurableJobControl(store);
|
|
242
371
|
const running = await startFileTaskController(home, schedulerStore, delivery, lifecycleDispatcher, {
|
|
243
372
|
intervalMs: options.intervalMs
|
|
244
373
|
?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
|
|
@@ -248,7 +377,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
248
377
|
now: options.now,
|
|
249
378
|
onError: options.onError,
|
|
250
379
|
lifecycleHost,
|
|
380
|
+
jobSupervisor,
|
|
381
|
+
jobControl,
|
|
251
382
|
...(resourceReaper === undefined ? {} : { resourceReaper }),
|
|
383
|
+
resourceAutoGc,
|
|
252
384
|
onExpiredEphemeralDomain: (domain) => {
|
|
253
385
|
if (domain.yuiHome !== home)
|
|
254
386
|
return;
|
|
@@ -257,14 +389,14 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
257
389
|
workspacePreparer,
|
|
258
390
|
runtimeEventProcessor: options.runtimeEventProcessor
|
|
259
391
|
?? (useWorker && asyncStoreClient !== undefined
|
|
260
|
-
? new AsyncRuntimeEventProcessor(
|
|
392
|
+
? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)), {
|
|
261
393
|
onTaskRuntimeApplied: (input) => {
|
|
262
|
-
planner
|
|
394
|
+
refreshAppliedTaskRuntimeDescriptor(store, planner, input);
|
|
263
395
|
}
|
|
264
396
|
})
|
|
265
|
-
: new FileRuntimeEventProcessor(
|
|
397
|
+
: new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore, {
|
|
266
398
|
onTaskRuntimeApplied: (input) => {
|
|
267
|
-
planner
|
|
399
|
+
refreshAppliedTaskRuntimeDescriptor(store, planner, input);
|
|
268
400
|
}
|
|
269
401
|
})),
|
|
270
402
|
domainIdentity,
|
|
@@ -280,6 +412,21 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
280
412
|
});
|
|
281
413
|
runningController = running;
|
|
282
414
|
runningRuntime = running.runtime;
|
|
415
|
+
// Issue 03: read-only startup reconciliation. Surfaces durable/physical
|
|
416
|
+
// Session mismatches (including generations whose durable map was cleared)
|
|
417
|
+
// without changing stop or archive behavior. Cleanup stays an explicit
|
|
418
|
+
// Operator action in exact-owner-cleanup mode.
|
|
419
|
+
try {
|
|
420
|
+
const startupReport = sessionOwners.report();
|
|
421
|
+
if (startupReport.summary.livePhysicalRoots > 0) {
|
|
422
|
+
(options.onError ?? (() => undefined))(new Error(`Session reconciliation: ${startupReport.summary.livePhysicalRoots} `
|
|
423
|
+
+ `live physical root(s) across ${startupReport.summary.owners} owner record(s); `
|
|
424
|
+
+ "run `yui session reconcile --report` for details."));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
(options.onError ?? (() => undefined))(error);
|
|
429
|
+
}
|
|
283
430
|
return {
|
|
284
431
|
...running,
|
|
285
432
|
close: async () => {
|
|
@@ -391,7 +538,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
391
538
|
throw applicationError("INVALID_PARAMS", `Configured Agent does not match Role: ${effective.agentId}.`);
|
|
392
539
|
}
|
|
393
540
|
validateLifecycleEnvironment(request.environment, agent);
|
|
394
|
-
const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective);
|
|
541
|
+
const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective, managedWorkspace);
|
|
395
542
|
const session = sessions?.sessions[effective.agentId];
|
|
396
543
|
const owner = request.scope === "task"
|
|
397
544
|
? { scope: "task", taskId: request.taskId, roleName: request.roleName }
|
|
@@ -513,7 +660,10 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
513
660
|
|| session.nativeSessionId !== request.nativeSessionId) {
|
|
514
661
|
throw new Error(`Native session changed: ${request.owner.roleName}.`);
|
|
515
662
|
}
|
|
516
|
-
|
|
663
|
+
const sessionEffectiveCompatible = request.owner.scope === "task"
|
|
664
|
+
? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, request.effective, request.managedWorkspace)
|
|
665
|
+
: effectiveLaunchSnapshotsCompatible(session.effective, request.effective);
|
|
666
|
+
if (!sessionEffectiveCompatible) {
|
|
517
667
|
throw new Error(`Native session effective launch changed: ${request.owner.roleName}.`);
|
|
518
668
|
}
|
|
519
669
|
}
|