@zq-silk/yui 0.15.12 → 0.16.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.
- package/ARCHITECTURE.md +3 -2
- package/ARCHITECTURE.zh-CN.md +3 -2
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +57 -41
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1280 -1243
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/globalRoleCommands.js +53 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +1 -10
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +271 -710
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +17 -57
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +3 -3
- package/dist/commands/taskPublicationCommands.js +1 -1
- package/dist/commands/taskPublicationVerifyCommand.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +2 -3
- package/dist/context/taskCatalog.js +3 -5
- package/dist/context/taskContext.js +139 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +9 -22
- package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryDelivery.js +7 -7
- package/dist/controller/runtime.js +51 -44
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +60 -84
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +1 -9
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/projectMaintenanceLock.js +23 -64
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +13 -22
- package/dist/repository/taskWorkspacePreparer.js +29 -25
- package/dist/repository/workspaceCleanupInspection.js +5 -5
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +15 -15
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +4 -41
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +7 -7
- package/dist/scheduler/leaderWakeupProcessor.js +19 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +25 -33
- package/dist/scheduler/taskObservabilityProjection.js +0 -1
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +142 -1
- package/dist/storage/sqliteStore.js +156 -162
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +15 -7
- package/dist/storage/taskStore.js +3 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/taskSurface.js +4 -4
- package/dist/web/webServer.js +4 -6
- package/dist/web/webSnapshot.js +3 -83
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/docs/architecture/capabilities-and-resources.md +14 -2
- package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
- package/docs/managed-turn-and-session-runtime.md +83 -10
- package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
- package/docs/plugin-sdk.md +4 -2
- package/docs/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +91 -5
- package/docs/task-delivery.zh-CN.md +72 -4
- package/docs/task-discovery.md +10 -11
- package/docs/task-discovery.zh-CN.md +8 -9
- package/docs/testing/verification-levels.md +145 -9
- package/docs/testing/verification-levels.zh-CN.md +103 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +15 -10
- package/skills/yui-runtime/SKILL.md +6 -6
- package/skills/yui-runtime/references/recovery.md +47 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -377
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner.js";
|
|
5
6
|
/**
|
|
6
7
|
* Per-Project maintenance fence.
|
|
7
8
|
*
|
|
@@ -87,6 +88,7 @@ export function acquireProjectMaintenanceLock(home, projectId, options = {}) {
|
|
|
87
88
|
async function acquireWithBudget(home, projectId, budget, firstProject = true) {
|
|
88
89
|
const lock = projectMaintenanceLockPath(home, projectId);
|
|
89
90
|
assertNotCancelled(projectId, budget.signal);
|
|
91
|
+
const ownerIdentity = currentFileLockOwner();
|
|
90
92
|
mkdirSync(join(home, "locks", "projects"), { recursive: true, mode: 0o700 });
|
|
91
93
|
let firstAttempt = firstProject;
|
|
92
94
|
while (true) {
|
|
@@ -99,7 +101,7 @@ async function acquireWithBudget(home, projectId, budget, firstProject = true) {
|
|
|
99
101
|
firstAttempt = false;
|
|
100
102
|
try {
|
|
101
103
|
mkdirSync(lock, { mode: 0o700 });
|
|
102
|
-
|
|
104
|
+
writeOwnerIdentity(lock, ownerIdentity);
|
|
103
105
|
let released = false;
|
|
104
106
|
return () => {
|
|
105
107
|
if (released)
|
|
@@ -134,8 +136,7 @@ async function acquireWithBudget(home, projectId, budget, firstProject = true) {
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
/** Write this process's owner identity and return the exact bytes recorded. */
|
|
137
|
-
function writeOwnerIdentity(lock) {
|
|
138
|
-
const identity = `${process.pid}:${processStartIdentity() ?? ""}`;
|
|
139
|
+
function writeOwnerIdentity(lock, identity = currentFileLockOwner()) {
|
|
139
140
|
writeFileSync(join(lock, "owner"), `${identity}\n`, { mode: 0o600 });
|
|
140
141
|
return identity;
|
|
141
142
|
}
|
|
@@ -188,14 +189,17 @@ export async function acquireProjectMaintenanceLocks(home, projectIds, options =
|
|
|
188
189
|
}
|
|
189
190
|
/**
|
|
190
191
|
* Non-blocking fence check for the Controller: a Project is fenced only
|
|
191
|
-
* while a live process holds its lock.
|
|
192
|
-
*
|
|
192
|
+
* while a live or unverified process holds its lock. Only proven stale owners
|
|
193
|
+
* read as unfenced; incomplete evidence never authorizes preparation.
|
|
193
194
|
*/
|
|
194
195
|
export function isProjectMaintenanceFenced(home, projectId) {
|
|
195
196
|
const lock = projectMaintenanceLockPath(home, projectId);
|
|
196
|
-
|
|
197
|
-
return
|
|
198
|
-
|
|
197
|
+
try {
|
|
198
|
+
return fileLockOwnerIsLive(lock);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
199
203
|
}
|
|
200
204
|
/**
|
|
201
205
|
* Reclaim a stale (dead-owner) lock through a crash-safe compare-and-delete
|
|
@@ -213,9 +217,8 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
213
217
|
catch (error) {
|
|
214
218
|
if (!isEnoent(error))
|
|
215
219
|
throw error;
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// initializing; a fresh lock may have its owner file written imminently.
|
|
220
|
+
// Allow publication its initial grace period; an older missing owner is
|
|
221
|
+
// diagnosed as unverified below, never treated as a dead creator.
|
|
219
222
|
try {
|
|
220
223
|
if (Date.now() - statSync(lock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
|
|
221
224
|
return;
|
|
@@ -236,7 +239,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
236
239
|
return;
|
|
237
240
|
throw error;
|
|
238
241
|
}
|
|
239
|
-
if (
|
|
242
|
+
if (fileLockOwnerIsLive(lock))
|
|
240
243
|
return;
|
|
241
244
|
const reclaimLock = `${lock}.reclaim`;
|
|
242
245
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -285,7 +288,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
285
288
|
}
|
|
286
289
|
if (currentOwner !== expectedOwner)
|
|
287
290
|
return;
|
|
288
|
-
if (
|
|
291
|
+
if (fileLockOwnerIsLive(lock))
|
|
289
292
|
return;
|
|
290
293
|
rmSync(lock, { recursive: true, force: true });
|
|
291
294
|
}
|
|
@@ -297,70 +300,26 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
297
300
|
}
|
|
298
301
|
/**
|
|
299
302
|
* Reclaim a reclaim-lock directory only when it is provably orphaned: its
|
|
300
|
-
* owner is dead
|
|
303
|
+
* owner is provably dead AND it is older than the age bound, so a
|
|
301
304
|
* lock whose owner just mkdir'ed but has not written its owner is not stolen.
|
|
302
305
|
* Uses the same PID + start-time identity check as the main fence, so a
|
|
303
306
|
* recycled PID with a different start identity does not keep an orphaned
|
|
304
|
-
* reclaim lock alive. An ownerless
|
|
305
|
-
* an unreadable owner fails closed (treated as live).
|
|
307
|
+
* reclaim lock alive. An ownerless or unreadable lock remains unverified.
|
|
306
308
|
* Returns true when it removed the lock (caller retries), false otherwise.
|
|
307
309
|
*/
|
|
308
310
|
function reclaimOrphanedReclaimLock(reclaimLock) {
|
|
309
311
|
try {
|
|
310
312
|
if (Date.now() - statSync(reclaimLock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
|
|
311
313
|
return false;
|
|
312
|
-
if (
|
|
314
|
+
if (fileLockOwnerIsLive(reclaimLock))
|
|
313
315
|
return false;
|
|
314
316
|
rmSync(reclaimLock, { recursive: true, force: true });
|
|
315
317
|
return true;
|
|
316
318
|
}
|
|
317
319
|
catch (error) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
function lockOwnerIsAlive(lock) {
|
|
322
|
-
let owner;
|
|
323
|
-
try {
|
|
324
|
-
owner = readFileSync(join(lock, "owner"), "utf8").trim();
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
328
|
-
return false;
|
|
329
|
-
// An unreadable owner file fails closed: treat the fence as held.
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
const separator = owner.indexOf(":");
|
|
333
|
-
const pid = Number.parseInt(separator < 0 ? owner : owner.slice(0, separator), 10);
|
|
334
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
335
|
-
return false;
|
|
336
|
-
const recordedIdentity = separator < 0 ? "" : owner.slice(separator + 1);
|
|
337
|
-
// PID + start-time identity: a recycled PID can never match the holder.
|
|
338
|
-
const currentIdentity = processStartIdentity(pid);
|
|
339
|
-
if (currentIdentity !== undefined) {
|
|
340
|
-
return recordedIdentity !== "" && currentIdentity === recordedIdentity;
|
|
341
|
-
}
|
|
342
|
-
// Off /proc (non-Linux): fall back to liveness.
|
|
343
|
-
return processIsAlive(pid);
|
|
344
|
-
}
|
|
345
|
-
/** Linux process start time (clock ticks since boot) for a PID; undefined off /proc. */
|
|
346
|
-
function processStartIdentity(pid = process.pid) {
|
|
347
|
-
try {
|
|
348
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
349
|
-
const close = stat.lastIndexOf(")");
|
|
350
|
-
const fields = stat.slice(close + 2).split(" ");
|
|
351
|
-
return fields[19] ?? "0"; // field 22: starttime
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
function processIsAlive(pid) {
|
|
358
|
-
try {
|
|
359
|
-
process.kill(pid, 0);
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
320
|
+
if (isEnoent(error))
|
|
321
|
+
return true;
|
|
322
|
+
throw error;
|
|
364
323
|
}
|
|
365
324
|
}
|
|
366
325
|
function isEnoent(error) {
|
|
@@ -95,7 +95,7 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
|
|
|
95
95
|
baseRef: binding.baseRef,
|
|
96
96
|
baseCommit,
|
|
97
97
|
status,
|
|
98
|
-
source: tracked?.source ?? (project.remoteUrl === undefined ? "not-applicable" : "
|
|
98
|
+
source: tracked?.source ?? (project.remoteUrl === undefined ? "not-applicable" : "unknown"),
|
|
99
99
|
workspacePath,
|
|
100
100
|
workspaceClean,
|
|
101
101
|
...(physicalHead === undefined ? {} : { physicalHead }),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
1
|
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { isArchivePersistenceFailure, recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { archiveSettlementChecks, archiveExecutionChecks } from "../task/archivePreflight.js";
|
|
4
|
+
import { archiveExecutionChecks, archiveSettlementChecks } from "../task/archivePreflight.js";
|
|
5
|
+
import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
|
|
7
6
|
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
7
|
+
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
8
8
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
9
9
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
10
10
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
@@ -235,7 +235,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
235
235
|
throw new CleanupInspectionError(checks);
|
|
236
236
|
const roleNames = this.store.listRoles(taskId).map(({ name }) => name);
|
|
237
237
|
await this.#stopLiveRoles(taskId, roleNames);
|
|
238
|
-
await this.runtime.
|
|
238
|
+
await this.runtime.releaseTaskTerminals(task.id);
|
|
239
239
|
this.#assertTaskArchiveSnapshot(snapshot);
|
|
240
240
|
for (const workspace of laneWorkspaces) {
|
|
241
241
|
this.#assertTaskArchiveLifecycle(task);
|
|
@@ -336,10 +336,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
336
336
|
throw new CleanupInspectionError(checks);
|
|
337
337
|
if (!runtimeReleased)
|
|
338
338
|
throw new Error("One or more Role runtimes could not be safely released.");
|
|
339
|
-
|
|
340
|
-
throw new Error("Physical resource release inspection is unavailable; workspace references retained.");
|
|
341
|
-
}
|
|
342
|
-
await this.runtime.assertTaskPhysicalResourcesReleased(taskId);
|
|
339
|
+
await this.runtime.releaseTaskTerminals(taskId);
|
|
343
340
|
return "released";
|
|
344
341
|
});
|
|
345
342
|
if (physicalReleased) {
|
|
@@ -447,9 +444,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
447
444
|
* human/Leader has taken responsibility for the outcome.
|
|
448
445
|
*/
|
|
449
446
|
#assertNoActiveWorkItemDurableJobs(item) {
|
|
450
|
-
const jobs = this.store.listDurableJobs
|
|
451
|
-
if (jobs === undefined)
|
|
452
|
-
return;
|
|
447
|
+
const jobs = this.store.listDurableJobs(item.taskId);
|
|
453
448
|
const blocking = jobs.filter((job) => (job.owner.kind === "work-item"
|
|
454
449
|
&& job.owner.workItemId === item.id
|
|
455
450
|
&& (job.status === "queued"
|
|
@@ -470,18 +465,16 @@ export class TaskWorkspaceCoordinator {
|
|
|
470
465
|
#reviewRoundRoleNames(round) {
|
|
471
466
|
return [
|
|
472
467
|
round.reviewerRoleName,
|
|
473
|
-
...(round.executionGroup?.lanes.map(({ roleName }) => roleName) ?? []),
|
|
474
468
|
...(round.executionGroup?.lanes.map(({ roleName }) => roleName) ?? [])
|
|
475
469
|
];
|
|
476
470
|
}
|
|
477
471
|
async #stopLiveRoles(taskId, roleNames) {
|
|
478
472
|
const targets = [...new Set(roleNames)];
|
|
479
|
-
const getActiveRun = this.store.getActiveRun?.bind(this.store);
|
|
480
473
|
for (const roleName of targets) {
|
|
481
|
-
if (getActiveRun
|
|
474
|
+
if (this.store.getActiveRun(taskId, roleName) !== null) {
|
|
482
475
|
throw new Error(`Role has an active AgentRun: ${taskId}/${roleName}.`);
|
|
483
476
|
}
|
|
484
|
-
if (
|
|
477
|
+
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
485
478
|
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
486
479
|
}
|
|
487
480
|
}
|
|
@@ -498,11 +491,9 @@ export class TaskWorkspaceCoordinator {
|
|
|
498
491
|
});
|
|
499
492
|
if (live.length > 0)
|
|
500
493
|
await this.runtime.stopTaskRoleSessions(taskId, live);
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
// cannot reserve a AgentRun/Session between absence proof and terminalization.
|
|
505
|
-
if (inspect === undefined || this.store.transaction === undefined)
|
|
494
|
+
// Recheck exact runtime quiescence under the Task transaction when this
|
|
495
|
+
// runtime exposes physical panes. Store reads are always authoritative.
|
|
496
|
+
if (inspect === undefined)
|
|
506
497
|
return;
|
|
507
498
|
this.store.transaction((tx) => {
|
|
508
499
|
const panes = inspect(taskId);
|
|
@@ -513,7 +504,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
513
504
|
if (tx.getActiveRun(taskId, roleName) !== null) {
|
|
514
505
|
throw new Error(`Role has an active AgentRun: ${taskId}/${roleName}.`);
|
|
515
506
|
}
|
|
516
|
-
if (
|
|
507
|
+
if (hasRuntimeLifecycleWork(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
517
508
|
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
518
509
|
}
|
|
519
510
|
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { lstat, mkdir,
|
|
1
|
+
import { lstat, mkdir, readdir, readlink, rm, rmdir, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
5
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
6
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
7
|
import { retireTaskRoleSessionsForWorkspace, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
6
8
|
import { formatWorkspacePreflightError } from "../executor/workspacePreflightClassification.js";
|
|
9
|
+
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
10
|
+
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
11
|
+
import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
|
|
7
12
|
import { updateRole } from "../role/role.js";
|
|
13
|
+
import { hasRuntimeCleanupObligation, RUNTIME_HOST_DETACH_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
8
14
|
import { taskRoleRuntimeIdentity } from "../runtime/managedCaller.js";
|
|
9
|
-
import {
|
|
10
|
-
import { taskLocalActor } from "../commands/taskActor.js";
|
|
11
|
-
import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
|
|
15
|
+
import { managedTaskRoot } from "../storage/homeLayout.js";
|
|
12
16
|
import { StorageConflictError } from "../storage/taskStore.js";
|
|
13
|
-
import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
|
|
14
|
-
import { admitStoredTaskActivation, adoptTaskActivationResources, recordFailedTaskActivation, recordAdoptedTaskActivation } from "../task/taskActivationService.js";
|
|
15
17
|
import { validateDraftTaskForActivation } from "../task/draftPlan.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot,
|
|
18
|
+
import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
|
|
19
|
+
import { admitStoredTaskActivation, adoptTaskActivationResources, recordAdoptedTaskActivation, recordFailedTaskActivation } from "../task/taskActivationService.js";
|
|
20
|
+
import { taskLocalActor } from "../task/taskAuthority.js";
|
|
21
|
+
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, recordWorkItemWorkspaceDisposition, workItemExecutionGroupById } from "../workItem/workItem.js";
|
|
22
|
+
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
20
23
|
import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
21
24
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
22
|
-
import { acquireProjectMaintenanceLocks,
|
|
23
|
-
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
24
|
-
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
25
|
+
import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockCancelledError, ProjectMaintenanceLockedError } from "./projectMaintenanceLock.js";
|
|
25
26
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
26
|
-
import {
|
|
27
|
+
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
27
28
|
import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
|
|
28
|
-
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
29
29
|
const MAIN_WORKTREE = "main";
|
|
30
30
|
const LEADER_ROLE = "leader";
|
|
31
31
|
/**
|
|
@@ -154,12 +154,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
154
154
|
changed: false
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
const admission = admitStoredTaskActivation(this.store, task.id);
|
|
158
|
+
if (admission.disposition === "absent") {
|
|
159
|
+
throw new Error(`Task activation request is required: ${task.id}. `
|
|
160
|
+
+ `Use yui task activation request ${task.id} --request-id <id> --environment <plan> first.`);
|
|
161
|
+
}
|
|
157
162
|
const provider = this.store.getTaskRoleSessionSet(task.id, LEADER_ROLE)?.providerBinding;
|
|
158
163
|
if (provider?.run != null
|
|
159
164
|
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
|
|
160
165
|
throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
|
|
161
166
|
}
|
|
162
|
-
const admission = admitStoredTaskActivation(this.store, task.id);
|
|
163
167
|
if (admission.disposition === "ready")
|
|
164
168
|
attemptedRequest = admission.request;
|
|
165
169
|
validateDraftTaskForActivation(this.store, task);
|
|
@@ -218,9 +222,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
218
222
|
* Prepares and adopts the resource configuration an explicit activation
|
|
219
223
|
* request named, before any status change.
|
|
220
224
|
*
|
|
221
|
-
* A
|
|
222
|
-
*
|
|
223
|
-
* binds no Project owns nothing physical, and is reported as workspace-free
|
|
225
|
+
* A request whose plan is empty and whose Task binds no Project owns nothing
|
|
226
|
+
* physical, and is reported as workspace-free
|
|
224
227
|
* so activation creates no directory at all.
|
|
225
228
|
*
|
|
226
229
|
* The exact request that was adopted travels with the result, so the later
|
|
@@ -229,8 +232,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
229
232
|
*/
|
|
230
233
|
async #adoptActivationEnvironment(task) {
|
|
231
234
|
const admission = admitStoredTaskActivation(this.store, task.id);
|
|
232
|
-
if (admission.disposition === "absent")
|
|
233
|
-
return { workspaceFree: false };
|
|
234
235
|
if (admission.disposition !== "ready") {
|
|
235
236
|
throw new Error(`Task activation is not adoptable: ${task.id}/${admission.disposition}`
|
|
236
237
|
+ (admission.disposition === "unavailable" ? ` (${admission.reason})` : "")
|
|
@@ -1467,7 +1468,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1467
1468
|
if (round.status !== "pending") {
|
|
1468
1469
|
throw new Error(`ReviewRound workspace can only prepare while pending: ${round.id}.`);
|
|
1469
1470
|
}
|
|
1470
|
-
const taskScope =
|
|
1471
|
+
const taskScope = round.scope === "task";
|
|
1471
1472
|
const item = taskScope || round.workItemId === undefined
|
|
1472
1473
|
? undefined
|
|
1473
1474
|
: requireWorkItem(this.store, task.id, round.workItemId);
|
|
@@ -1770,7 +1771,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1770
1771
|
async #reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries) {
|
|
1771
1772
|
const previous = this.store.listReviewRounds(task.id)
|
|
1772
1773
|
.filter((candidate) => (candidate.id !== round.id
|
|
1773
|
-
&&
|
|
1774
|
+
&& candidate.scope === "task"
|
|
1774
1775
|
&& candidate.reviewerRoleName === round.reviewerRoleName
|
|
1775
1776
|
&& (candidate.status === "completed" || candidate.status === "failed")
|
|
1776
1777
|
&& candidate.workspaceDisposition?.kind !== "removed"
|
|
@@ -2128,6 +2129,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
2128
2129
|
if (main.owner.type !== "task") {
|
|
2129
2130
|
throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
|
|
2130
2131
|
}
|
|
2132
|
+
if (main.entries.length === 0)
|
|
2133
|
+
return "missing";
|
|
2131
2134
|
return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
|
|
2132
2135
|
}
|
|
2133
2136
|
async cleanupTaskForArchive(taskId, disposition = "abandoned") {
|
|
@@ -2147,7 +2150,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
2147
2150
|
if (main !== null) {
|
|
2148
2151
|
await this.#assertWorkspaceCleanup(main, disposition);
|
|
2149
2152
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2150
|
-
|
|
2153
|
+
// Scratch-only workspaces have no Git entries or Git workspace identity.
|
|
2154
|
+
if (main.entries.length > 0 && await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
|
|
2151
2155
|
return {
|
|
2152
2156
|
taskId,
|
|
2153
2157
|
status: "retained-dirty",
|
|
@@ -2228,7 +2232,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2228
2232
|
return integrationWorkspaceRoot(this.home, taskId, integrationId);
|
|
2229
2233
|
}
|
|
2230
2234
|
#reviewWorktreeName(round) {
|
|
2231
|
-
return
|
|
2235
|
+
return round.scope === "task"
|
|
2232
2236
|
? `reviewer-${round.reviewerRoleName}`
|
|
2233
2237
|
: round.id;
|
|
2234
2238
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { dirname, join, relative
|
|
1
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
3
|
+
import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
|
|
5
4
|
import { cleanupCheckFromError } from "../workspace/cleanupInspection.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
6
|
+
import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
|
|
8
7
|
import { integrationWorktreeIdentity, worktreeIdentity } from "./gitWorkspace.js";
|
|
8
|
+
import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
|
|
9
9
|
/** Current owner facts + the same Git inspection used immediately before removal.
|
|
10
10
|
* No preparation, locks, Git refresh, runtime stop, DB writes or repair.
|
|
11
11
|
*/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* so every safety guarantee (live-ref re-scan, cleanliness proof, fail-closed
|
|
11
11
|
* sources) applies identically.
|
|
12
12
|
*/
|
|
13
|
-
import { applyResourceGc, planResourceGc } from "./resourceGc.js";
|
|
13
|
+
import { applyResourceGc, planResourceGc, readResourceGcState, } from "./resourceGc.js";
|
|
14
14
|
import { resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours } from "../config/yuiConfig.js";
|
|
15
15
|
/**
|
|
16
16
|
* Create the Controller's automatic Resource GC hook. The hook self-skips
|
|
@@ -25,22 +25,16 @@ export function createResourceAutoGc(options) {
|
|
|
25
25
|
return { skipped: true, applied: 0, failed: 0, restored: 0 };
|
|
26
26
|
}
|
|
27
27
|
const now = new Date();
|
|
28
|
-
const projects = store.listProjects();
|
|
29
|
-
const managedWorkspaces = collectManagedWorkspaces(store);
|
|
30
|
-
const taskStatusById = collectTaskStatuses(store);
|
|
31
28
|
const input = {
|
|
32
29
|
home,
|
|
33
|
-
|
|
34
|
-
managedWorkspaces,
|
|
35
|
-
taskStatusById,
|
|
30
|
+
...readResourceGcState(store),
|
|
36
31
|
mode: "quarantine",
|
|
37
32
|
now,
|
|
38
33
|
quarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
|
|
39
|
-
environment
|
|
40
|
-
activeWorkspaceOwnerPaths: collectActiveWorkspaceOwnerPaths(store)
|
|
34
|
+
environment
|
|
41
35
|
};
|
|
42
36
|
const plan = await planResourceGc(input);
|
|
43
|
-
const result = await applyResourceGc(input, plan);
|
|
37
|
+
const result = await applyResourceGc(input, plan, store);
|
|
44
38
|
return {
|
|
45
39
|
skipped: false,
|
|
46
40
|
applied: result.applied.length,
|
|
@@ -49,70 +43,3 @@ export function createResourceAutoGc(options) {
|
|
|
49
43
|
};
|
|
50
44
|
};
|
|
51
45
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Run one automatic GC pass. Returns `ran: false` when auto-GC is disabled or
|
|
54
|
-
* the GC mode is not `quarantine`. The pass never purges: permanent deletion
|
|
55
|
-
* is always manual.
|
|
56
|
-
*/
|
|
57
|
-
export async function runAutoResourceGc(store, options = {}) {
|
|
58
|
-
const config = store.getConfig();
|
|
59
|
-
const autoQuarantine = resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine);
|
|
60
|
-
if (!autoQuarantine) {
|
|
61
|
-
return { ran: false, reason: "auto-quarantine is disabled" };
|
|
62
|
-
}
|
|
63
|
-
const gcMode = resolveResourcesGcMode(config.resourcesGcMode);
|
|
64
|
-
if (gcMode !== "quarantine") {
|
|
65
|
-
return { ran: false, reason: "resources.gcMode is not quarantine" };
|
|
66
|
-
}
|
|
67
|
-
const now = options.now ?? new Date();
|
|
68
|
-
const home = store.rootDirectory();
|
|
69
|
-
const projects = store.listProjects();
|
|
70
|
-
const managedWorkspaces = collectManagedWorkspaces(store);
|
|
71
|
-
const taskStatusById = collectTaskStatuses(store);
|
|
72
|
-
const input = {
|
|
73
|
-
home,
|
|
74
|
-
projects,
|
|
75
|
-
managedWorkspaces,
|
|
76
|
-
taskStatusById,
|
|
77
|
-
mode: "quarantine",
|
|
78
|
-
now,
|
|
79
|
-
quarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
|
|
80
|
-
activeWorkspaceOwnerPaths: collectActiveWorkspaceOwnerPaths(store)
|
|
81
|
-
};
|
|
82
|
-
const plan = await planResourceGc(input);
|
|
83
|
-
const result = await applyResourceGc(input, plan);
|
|
84
|
-
return { ran: true, result };
|
|
85
|
-
}
|
|
86
|
-
function collectManagedWorkspaces(store) {
|
|
87
|
-
const workspaces = [];
|
|
88
|
-
for (const task of store.listTasks()) {
|
|
89
|
-
workspaces.push(...store.listManagedWorkspaces(task.id));
|
|
90
|
-
}
|
|
91
|
-
return workspaces;
|
|
92
|
-
}
|
|
93
|
-
function collectTaskStatuses(store) {
|
|
94
|
-
const statuses = new Map();
|
|
95
|
-
for (const task of store.listTasks()) {
|
|
96
|
-
statuses.set(task.id, task.status);
|
|
97
|
-
}
|
|
98
|
-
return statuses;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Workspace paths claimed by active durable Jobs. An active AgentRun still
|
|
102
|
-
* holds its workspace even after the managed workspace record is gone, so
|
|
103
|
-
* those paths stay protected until the AgentRun finishes.
|
|
104
|
-
*/
|
|
105
|
-
function collectActiveWorkspaceOwnerPaths(store) {
|
|
106
|
-
const paths = [];
|
|
107
|
-
for (const task of store.listTasks()) {
|
|
108
|
-
for (const run of store.listRuns(task.id)) {
|
|
109
|
-
if (run.status !== "active")
|
|
110
|
-
continue;
|
|
111
|
-
const workspace = run.workspace;
|
|
112
|
-
if (workspace === undefined)
|
|
113
|
-
continue;
|
|
114
|
-
paths.push(workspace.root, ...workspace.entries.map((entry) => entry.path));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return paths;
|
|
118
|
-
}
|
|
@@ -112,7 +112,7 @@ export async function scanLiveReferences(input) {
|
|
|
112
112
|
diagnostics.push(...runtimeClaims.diagnostics);
|
|
113
113
|
const activeReleaseClaims = readActiveReleaseClaims(home);
|
|
114
114
|
diagnostics.push(...activeReleaseClaims.diagnostics);
|
|
115
|
-
const sessionOwnerClaims = readSessionOwnerClaims(
|
|
115
|
+
const sessionOwnerClaims = readSessionOwnerClaims(input.sessionOwners);
|
|
116
116
|
diagnostics.push(...sessionOwnerClaims.diagnostics);
|
|
117
117
|
for (const claim of [
|
|
118
118
|
...runtimeClaims.claims,
|
|
@@ -231,62 +231,15 @@ function readActiveReleaseClaims(home) {
|
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
235
|
-
*
|
|
236
|
-
|
|
237
|
-
* identity protects its runtime root. A dead PID is stale (reconciliation
|
|
238
|
-
* removes those records); an identity conflict or unreadable record fails
|
|
239
|
-
* closed because liveness can no longer be proven.
|
|
240
|
-
*/
|
|
241
|
-
function readSessionOwnerClaims(home) {
|
|
242
|
-
const directory = join(home, "runtime", "session-owners");
|
|
243
|
-
let entries;
|
|
244
|
-
try {
|
|
245
|
-
entries = readdirSync(directory, { withFileTypes: true });
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
if (error.code === "ENOENT") {
|
|
249
|
-
return Object.freeze({ claims: Object.freeze([]), diagnostics: Object.freeze([]) });
|
|
250
|
-
}
|
|
251
|
-
return Object.freeze({
|
|
252
|
-
claims: Object.freeze([]),
|
|
253
|
-
diagnostics: Object.freeze([{
|
|
254
|
-
source: "controller",
|
|
255
|
-
severity: "error",
|
|
256
|
-
message: `session owner records are unreadable: ${error instanceof Error ? error.message : "unknown error"}`
|
|
257
|
-
}])
|
|
258
|
-
});
|
|
259
|
-
}
|
|
234
|
+
/** SQLite supplies durable custody; exact OS generation proves current life.
|
|
235
|
+
* The same check is used during scanning and under the GC writer fence. */
|
|
236
|
+
export function readSessionOwnerClaims(records) {
|
|
260
237
|
const claims = [];
|
|
261
238
|
const diagnostics = [];
|
|
262
|
-
for (const
|
|
263
|
-
|
|
264
|
-
continue;
|
|
265
|
-
const path = join(directory, entry.name);
|
|
266
|
-
let record;
|
|
267
|
-
try {
|
|
268
|
-
record = JSON.parse(readFileSync(path, "utf8"));
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
diagnostics.push({
|
|
272
|
-
source: "controller",
|
|
273
|
-
severity: "error",
|
|
274
|
-
message: `session owner record is unreadable at ${path}: ${error instanceof Error ? error.message : "unknown error"}`
|
|
275
|
-
});
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
const pid = record.providerRoot?.pid;
|
|
279
|
-
const startIdentity = record.providerRoot?.startIdentity;
|
|
280
|
-
if (typeof pid !== "number" || pid <= 0 || typeof startIdentity !== "string") {
|
|
281
|
-
diagnostics.push({
|
|
282
|
-
source: "controller",
|
|
283
|
-
severity: "error",
|
|
284
|
-
message: `session owner record at ${path} has no valid provider root identity`
|
|
285
|
-
});
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
239
|
+
for (const record of records) {
|
|
240
|
+
const { pid, startIdentity } = record.providerRoot;
|
|
288
241
|
if (!isProcessAlive(pid)) {
|
|
289
|
-
//
|
|
242
|
+
// The normal reconciliation owner releases dead custody records.
|
|
290
243
|
continue;
|
|
291
244
|
}
|
|
292
245
|
const currentIdentity = readLinuxProcessStartIdentity(pid);
|