@zq-silk/yui 0.15.11 → 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 +11 -6
- package/ARCHITECTURE.zh-CN.md +8 -4
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- 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 +92 -48
- 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 +11 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +2 -2
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1303 -1235
- 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/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +78 -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 +45 -25
- 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 +355 -732
- 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 +20 -58
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +12 -3
- package/dist/commands/taskPublicationVerifyCommand.js +24 -40
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
- 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 +5 -3
- package/dist/context/taskCatalog.js +185 -0
- package/dist/context/taskContext.js +152 -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 +20 -24
- package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +85 -43
- 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 +79 -90
- 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 +5 -10
- package/dist/message/globalProviderRetry.js +15 -0
- 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/executionAudit.js +19 -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/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +98 -82
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +82 -144
- package/dist/repository/taskWorkspacePreparer.js +113 -47
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- 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/agentError.js +5 -3
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +42 -25
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +10 -43
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +19 -7
- package/dist/scheduler/leaderWakeupProcessor.js +24 -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 +49 -36
- package/dist/scheduler/taskObservabilityProjection.js +6 -45
- 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 +173 -1
- package/dist/storage/sqliteStore.js +172 -161
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +16 -7
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +5 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- 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 +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +6 -5
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +12 -3
- package/dist/web/webSnapshot.js +14 -85
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +42 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
- package/docs/managed-turn-and-session-runtime.md +121 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/plugin-sdk.md +4 -2
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- 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 +223 -17
- package/docs/task-delivery.zh-CN.md +171 -14
- package/docs/task-discovery.md +101 -0
- package/docs/task-discovery.zh-CN.md +85 -0
- package/docs/testing/verification-levels.md +161 -9
- package/docs/testing/verification-levels.zh-CN.md +116 -9
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +24 -8
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +71 -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 -376
- 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,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner.js";
|
|
3
6
|
/**
|
|
4
7
|
* Per-Project maintenance fence.
|
|
5
8
|
*
|
|
@@ -12,35 +15,66 @@ import { join } from "node:path";
|
|
|
12
15
|
* gone; the owner file records PID + process start time, so a recycled PID
|
|
13
16
|
* can never pass for the original holder.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* Contenders wait asynchronously within one monotonic budget. Only lock
|
|
19
|
+
* acquisition is retried; the caller's protected operation is never replayed.
|
|
17
20
|
*/
|
|
18
|
-
const PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS =
|
|
19
|
-
const
|
|
21
|
+
const PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS = 60_000;
|
|
22
|
+
const PROJECT_MAINTENANCE_LOCK_RETRY_MIN_MS = 200;
|
|
23
|
+
const PROJECT_MAINTENANCE_LOCK_RETRY_MAX_MS = 500;
|
|
20
24
|
/** A lock older than this with a dead owner is reclaimed. */
|
|
21
25
|
const STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS = 1_000;
|
|
22
26
|
/**
|
|
23
|
-
* Raised when
|
|
24
|
-
*
|
|
27
|
+
* Raised when acquisition exhausted its budget. This acquisition has not
|
|
28
|
+
* started the protected operation; callers retain responsibility for any
|
|
29
|
+
* earlier effects and must not replay the whole operation implicitly.
|
|
25
30
|
*/
|
|
26
31
|
export class ProjectMaintenanceLockedError extends Error {
|
|
32
|
+
timeoutMs;
|
|
27
33
|
projectId;
|
|
28
34
|
retryable = true;
|
|
29
|
-
constructor(projectId) {
|
|
30
|
-
super(`
|
|
35
|
+
constructor(projectId, timeoutMs = PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS) {
|
|
36
|
+
super(`Timed out after ${timeoutMs}ms waiting for Project maintenance on ${projectId}; `
|
|
37
|
+
+ "the lock was not acquired. Read current state before retrying.");
|
|
38
|
+
this.timeoutMs = timeoutMs;
|
|
31
39
|
this.name = "ProjectMaintenanceLockedError";
|
|
32
40
|
this.projectId = projectId;
|
|
33
41
|
}
|
|
34
42
|
}
|
|
43
|
+
export class ProjectMaintenanceLockCancelledError extends Error {
|
|
44
|
+
projectId;
|
|
45
|
+
constructor(projectId, cause) {
|
|
46
|
+
super(`Cancelled waiting for Project maintenance on ${projectId}; the lock was not acquired.`, { cause });
|
|
47
|
+
this.projectId = projectId;
|
|
48
|
+
this.name = "ProjectMaintenanceLockCancelledError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function acquisitionBudget(options) {
|
|
52
|
+
const timeoutMs = options.timeoutMs ?? PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS;
|
|
53
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
54
|
+
throw new RangeError("Project maintenance timeout must be a finite nonnegative number.");
|
|
55
|
+
}
|
|
56
|
+
const now = options.now ?? (() => performance.now());
|
|
57
|
+
return {
|
|
58
|
+
timeoutMs,
|
|
59
|
+
deadline: now() + timeoutMs,
|
|
60
|
+
now,
|
|
61
|
+
random: options.random ?? Math.random,
|
|
62
|
+
wait: options.wait ?? ((milliseconds, signal) => delay(milliseconds, undefined, { signal })),
|
|
63
|
+
signal: options.signal
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function assertNotCancelled(projectId, signal) {
|
|
67
|
+
if (signal?.aborted)
|
|
68
|
+
throw new ProjectMaintenanceLockCancelledError(projectId, signal.reason);
|
|
69
|
+
}
|
|
35
70
|
/** Directory of one Project's maintenance fence, below the Home's locks area. */
|
|
36
71
|
export function projectMaintenanceLockPath(home, projectId) {
|
|
37
72
|
return join(home, "locks", "projects", `${projectId}.lock`);
|
|
38
73
|
}
|
|
39
74
|
/**
|
|
40
75
|
* Acquire one Project's maintenance fence. Returns the release function;
|
|
41
|
-
* callers MUST release on every exit path (try/finally). A live holder
|
|
42
|
-
*
|
|
43
|
-
* holder is reclaimed.
|
|
76
|
+
* callers MUST release on every exit path (try/finally). A live holder is
|
|
77
|
+
* never stolen, including at timeout; a stale (dead) holder is reclaimed.
|
|
44
78
|
*
|
|
45
79
|
* There is no in-process reentrancy: every acquisition contends, so two
|
|
46
80
|
* independent operations in the same process are mutually exclusive just
|
|
@@ -48,14 +82,26 @@ export function projectMaintenanceLockPath(home, projectId) {
|
|
|
48
82
|
* fence while its caller already holds it) goes through private
|
|
49
83
|
* already-locked methods, never through a second acquisition.
|
|
50
84
|
*/
|
|
51
|
-
export function acquireProjectMaintenanceLock(home, projectId) {
|
|
85
|
+
export function acquireProjectMaintenanceLock(home, projectId, options = {}) {
|
|
86
|
+
return acquireWithBudget(home, projectId, acquisitionBudget(options));
|
|
87
|
+
}
|
|
88
|
+
async function acquireWithBudget(home, projectId, budget, firstProject = true) {
|
|
52
89
|
const lock = projectMaintenanceLockPath(home, projectId);
|
|
90
|
+
assertNotCancelled(projectId, budget.signal);
|
|
91
|
+
const ownerIdentity = currentFileLockOwner();
|
|
53
92
|
mkdirSync(join(home, "locks", "projects"), { recursive: true, mode: 0o700 });
|
|
54
|
-
|
|
93
|
+
let firstAttempt = firstProject;
|
|
55
94
|
while (true) {
|
|
95
|
+
assertNotCancelled(projectId, budget.signal);
|
|
96
|
+
// Always try the first Project immediately, even for a zero-budget probe.
|
|
97
|
+
// Every retry and subsequent Project shares the original deadline.
|
|
98
|
+
if (!firstAttempt && budget.now() >= budget.deadline) {
|
|
99
|
+
throw new ProjectMaintenanceLockedError(projectId, budget.timeoutMs);
|
|
100
|
+
}
|
|
101
|
+
firstAttempt = false;
|
|
56
102
|
try {
|
|
57
103
|
mkdirSync(lock, { mode: 0o700 });
|
|
58
|
-
|
|
104
|
+
writeOwnerIdentity(lock, ownerIdentity);
|
|
59
105
|
let released = false;
|
|
60
106
|
return () => {
|
|
61
107
|
if (released)
|
|
@@ -70,16 +116,27 @@ export function acquireProjectMaintenanceLock(home, projectId) {
|
|
|
70
116
|
catch (error) {
|
|
71
117
|
if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
|
|
72
118
|
throw error;
|
|
119
|
+
if (budget.now() >= budget.deadline) {
|
|
120
|
+
throw new ProjectMaintenanceLockedError(projectId, budget.timeoutMs);
|
|
121
|
+
}
|
|
73
122
|
reclaimStaleProjectMaintenanceLock(lock);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
const remaining = budget.deadline - budget.now();
|
|
124
|
+
if (remaining <= 0)
|
|
125
|
+
throw new ProjectMaintenanceLockedError(projectId, budget.timeoutMs);
|
|
126
|
+
const jitter = PROJECT_MAINTENANCE_LOCK_RETRY_MIN_MS
|
|
127
|
+
+ budget.random() * (PROJECT_MAINTENANCE_LOCK_RETRY_MAX_MS - PROJECT_MAINTENANCE_LOCK_RETRY_MIN_MS);
|
|
128
|
+
try {
|
|
129
|
+
await budget.wait(Math.min(jitter, remaining), budget.signal);
|
|
130
|
+
}
|
|
131
|
+
catch (waitError) {
|
|
132
|
+
assertNotCancelled(projectId, budget.signal);
|
|
133
|
+
throw waitError;
|
|
134
|
+
}
|
|
77
135
|
}
|
|
78
136
|
}
|
|
79
137
|
}
|
|
80
138
|
/** Write this process's owner identity and return the exact bytes recorded. */
|
|
81
|
-
function writeOwnerIdentity(lock) {
|
|
82
|
-
const identity = `${process.pid}:${processStartIdentity() ?? ""}`;
|
|
139
|
+
function writeOwnerIdentity(lock, identity = currentFileLockOwner()) {
|
|
83
140
|
writeFileSync(join(lock, "owner"), `${identity}\n`, { mode: 0o600 });
|
|
84
141
|
return identity;
|
|
85
142
|
}
|
|
@@ -108,11 +165,12 @@ function releaseOwnedProjectMaintenanceLock(lock, ownerIdentity) {
|
|
|
108
165
|
* including ones this process already holds: callers that need nesting must
|
|
109
166
|
* use an already-locked private path instead of acquiring twice.
|
|
110
167
|
*/
|
|
111
|
-
export function acquireProjectMaintenanceLocks(home, projectIds) {
|
|
168
|
+
export async function acquireProjectMaintenanceLocks(home, projectIds, options = {}) {
|
|
169
|
+
const budget = acquisitionBudget(options);
|
|
112
170
|
const releases = [];
|
|
113
171
|
try {
|
|
114
172
|
for (const projectId of [...new Set(projectIds)].sort()) {
|
|
115
|
-
releases.push(
|
|
173
|
+
releases.push(await acquireWithBudget(home, projectId, budget, releases.length === 0));
|
|
116
174
|
}
|
|
117
175
|
}
|
|
118
176
|
catch (error) {
|
|
@@ -131,14 +189,17 @@ export function acquireProjectMaintenanceLocks(home, projectIds) {
|
|
|
131
189
|
}
|
|
132
190
|
/**
|
|
133
191
|
* Non-blocking fence check for the Controller: a Project is fenced only
|
|
134
|
-
* while a live process holds its lock.
|
|
135
|
-
*
|
|
192
|
+
* while a live or unverified process holds its lock. Only proven stale owners
|
|
193
|
+
* read as unfenced; incomplete evidence never authorizes preparation.
|
|
136
194
|
*/
|
|
137
195
|
export function isProjectMaintenanceFenced(home, projectId) {
|
|
138
196
|
const lock = projectMaintenanceLockPath(home, projectId);
|
|
139
|
-
|
|
140
|
-
return
|
|
141
|
-
|
|
197
|
+
try {
|
|
198
|
+
return fileLockOwnerIsLive(lock);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
142
203
|
}
|
|
143
204
|
/**
|
|
144
205
|
* Reclaim a stale (dead-owner) lock through a crash-safe compare-and-delete
|
|
@@ -156,9 +217,8 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
156
217
|
catch (error) {
|
|
157
218
|
if (!isEnoent(error))
|
|
158
219
|
throw error;
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// 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.
|
|
162
222
|
try {
|
|
163
223
|
if (Date.now() - statSync(lock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
|
|
164
224
|
return;
|
|
@@ -179,7 +239,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
179
239
|
return;
|
|
180
240
|
throw error;
|
|
181
241
|
}
|
|
182
|
-
if (
|
|
242
|
+
if (fileLockOwnerIsLive(lock))
|
|
183
243
|
return;
|
|
184
244
|
const reclaimLock = `${lock}.reclaim`;
|
|
185
245
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -228,7 +288,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
228
288
|
}
|
|
229
289
|
if (currentOwner !== expectedOwner)
|
|
230
290
|
return;
|
|
231
|
-
if (
|
|
291
|
+
if (fileLockOwnerIsLive(lock))
|
|
232
292
|
return;
|
|
233
293
|
rmSync(lock, { recursive: true, force: true });
|
|
234
294
|
}
|
|
@@ -240,70 +300,26 @@ function reclaimStaleProjectMaintenanceLock(lock) {
|
|
|
240
300
|
}
|
|
241
301
|
/**
|
|
242
302
|
* Reclaim a reclaim-lock directory only when it is provably orphaned: its
|
|
243
|
-
* owner is dead
|
|
303
|
+
* owner is provably dead AND it is older than the age bound, so a
|
|
244
304
|
* lock whose owner just mkdir'ed but has not written its owner is not stolen.
|
|
245
305
|
* Uses the same PID + start-time identity check as the main fence, so a
|
|
246
306
|
* recycled PID with a different start identity does not keep an orphaned
|
|
247
|
-
* reclaim lock alive. An ownerless
|
|
248
|
-
* an unreadable owner fails closed (treated as live).
|
|
307
|
+
* reclaim lock alive. An ownerless or unreadable lock remains unverified.
|
|
249
308
|
* Returns true when it removed the lock (caller retries), false otherwise.
|
|
250
309
|
*/
|
|
251
310
|
function reclaimOrphanedReclaimLock(reclaimLock) {
|
|
252
311
|
try {
|
|
253
312
|
if (Date.now() - statSync(reclaimLock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
|
|
254
313
|
return false;
|
|
255
|
-
if (
|
|
314
|
+
if (fileLockOwnerIsLive(reclaimLock))
|
|
256
315
|
return false;
|
|
257
316
|
rmSync(reclaimLock, { recursive: true, force: true });
|
|
258
317
|
return true;
|
|
259
318
|
}
|
|
260
319
|
catch (error) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
function lockOwnerIsAlive(lock) {
|
|
265
|
-
let owner;
|
|
266
|
-
try {
|
|
267
|
-
owner = readFileSync(join(lock, "owner"), "utf8").trim();
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
271
|
-
return false;
|
|
272
|
-
// An unreadable owner file fails closed: treat the fence as held.
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
const separator = owner.indexOf(":");
|
|
276
|
-
const pid = Number.parseInt(separator < 0 ? owner : owner.slice(0, separator), 10);
|
|
277
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
278
|
-
return false;
|
|
279
|
-
const recordedIdentity = separator < 0 ? "" : owner.slice(separator + 1);
|
|
280
|
-
// PID + start-time identity: a recycled PID can never match the holder.
|
|
281
|
-
const currentIdentity = processStartIdentity(pid);
|
|
282
|
-
if (currentIdentity !== undefined) {
|
|
283
|
-
return recordedIdentity !== "" && currentIdentity === recordedIdentity;
|
|
284
|
-
}
|
|
285
|
-
// Off /proc (non-Linux): fall back to liveness.
|
|
286
|
-
return processIsAlive(pid);
|
|
287
|
-
}
|
|
288
|
-
/** Linux process start time (clock ticks since boot) for a PID; undefined off /proc. */
|
|
289
|
-
function processStartIdentity(pid = process.pid) {
|
|
290
|
-
try {
|
|
291
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
292
|
-
const close = stat.lastIndexOf(")");
|
|
293
|
-
const fields = stat.slice(close + 2).split(" ");
|
|
294
|
-
return fields[19] ?? "0"; // field 22: starttime
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
return undefined;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
function processIsAlive(pid) {
|
|
301
|
-
try {
|
|
302
|
-
process.kill(pid, 0);
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
320
|
+
if (isEnoent(error))
|
|
321
|
+
return true;
|
|
322
|
+
throw error;
|
|
307
323
|
}
|
|
308
324
|
}
|
|
309
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 }),
|