@zq-silk/yui 0.13.9 → 0.14.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 +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The upgrade admission fence.
|
|
3
|
-
*
|
|
4
|
-
* A fence is a small marker file inside a Yui Home. While it exists and is owned
|
|
5
|
-
* by a live process other than the caller, every authoritative writer — a
|
|
6
|
-
* baseline `yui` CLI invocation *and* the per-home Controller alike — refuses to
|
|
7
|
-
* begin a new write. This is the mechanism by which `yui upgrade`/`yui update`
|
|
8
|
-
* stop new writes before quiescing and switching the store, without a broad
|
|
9
|
-
* process kill or a TTL/idle heuristic.
|
|
10
|
-
*
|
|
11
|
-
* The fence is honored at the single storage write choke point
|
|
12
|
-
* (`FileTaskStore` commit), so both the CLI and the Controller — which mutate
|
|
13
|
-
* through the same store — observe it. Because the check lives in that shared
|
|
14
|
-
* path, every writer built from this release forward enforces it; it cannot
|
|
15
|
-
* retroactively bind an already-installed older binary, so the upgrade
|
|
16
|
-
* orchestrator additionally drains the Controller and fails closed on any live
|
|
17
|
-
* writer rather than relying on the fence alone. It never blocks reads, and it
|
|
18
|
-
* never blocks the process that placed it (so the upgrade orchestrator can
|
|
19
|
-
* re-pin the revision under the write lock). A fence whose owner process is gone
|
|
20
|
-
* is stale and is reclaimed, mirroring the storage lock's dead-owner reclaim.
|
|
21
|
-
*/
|
|
22
|
-
import { closeSync, constants, fsyncSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeSync } from "node:fs";
|
|
23
|
-
import { dirname, join } from "node:path";
|
|
24
|
-
/** Fence marker location, beside the Controller discovery file under `runtime/`. */
|
|
25
|
-
export const UPGRADE_FENCE_FILE = "runtime/upgrade.fence";
|
|
26
|
-
/** Thrown when a writer is refused because an upgrade fence is in place. */
|
|
27
|
-
export class UpgradeFenceError extends Error {
|
|
28
|
-
reason;
|
|
29
|
-
constructor(reason) {
|
|
30
|
-
super("Yui storage is fenced for an in-progress upgrade; new writes are refused "
|
|
31
|
-
+ `(${reason}). Wait for the upgrade to finish, or if no upgrade is `
|
|
32
|
-
+ "running remove the stale fence and retry.");
|
|
33
|
-
this.name = "UpgradeFenceError";
|
|
34
|
-
this.reason = reason;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function fencePath(home) {
|
|
38
|
-
return join(home, UPGRADE_FENCE_FILE);
|
|
39
|
-
}
|
|
40
|
-
/** Read the fence file's raw bytes at an exact path, or `null` when absent. */
|
|
41
|
-
function readFenceRaw(path) {
|
|
42
|
-
try {
|
|
43
|
-
return readFileSync(path, "utf8");
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
if (isEnoent(error))
|
|
47
|
-
return null;
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Parse raw fence bytes into a record. A well-formed fence yields its fields; a
|
|
53
|
-
* malformed/unparseable fence yields a sentinel `ownerPid: -1` "malformed fence"
|
|
54
|
-
* so callers treat it as an undeterminable (fail-closed) live fence, never as
|
|
55
|
-
* absent or reclaimable.
|
|
56
|
-
*/
|
|
57
|
-
function parseFence(raw) {
|
|
58
|
-
try {
|
|
59
|
-
const value = JSON.parse(raw);
|
|
60
|
-
if (value.schemaVersion === 1
|
|
61
|
-
&& Number.isInteger(value.ownerPid)
|
|
62
|
-
&& typeof value.reason === "string"
|
|
63
|
-
&& typeof value.createdAt === "string") {
|
|
64
|
-
return {
|
|
65
|
-
schemaVersion: 1,
|
|
66
|
-
ownerPid: value.ownerPid,
|
|
67
|
-
reason: value.reason,
|
|
68
|
-
createdAt: value.createdAt
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// fall through to the malformed sentinel.
|
|
74
|
-
}
|
|
75
|
-
return { schemaVersion: 1, ownerPid: -1, reason: "malformed fence", createdAt: "" };
|
|
76
|
-
}
|
|
77
|
-
/** Read the current fence record, or `null` when none is present or it is unreadable. */
|
|
78
|
-
export function readUpgradeFence(home) {
|
|
79
|
-
const raw = readFenceRaw(fencePath(home));
|
|
80
|
-
if (raw === null)
|
|
81
|
-
return null;
|
|
82
|
-
return parseFence(raw);
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Refuse a write when a live upgrade fence owned by another process is present.
|
|
86
|
-
* A fence owned by the current process, or by a process that is gone, never
|
|
87
|
-
* blocks: the placer may re-pin under the lock, and a stale (dead-owner) fence is
|
|
88
|
-
* reclaimed. A stale fence is only cleared for the caller once the reclaim is
|
|
89
|
-
* PROVEN complete — if it cannot be cleared, this fails closed rather than
|
|
90
|
-
* falsely reporting the Home writable (R2-F4).
|
|
91
|
-
*/
|
|
92
|
-
export function assertHomeWritable(home, callerPid = process.pid) {
|
|
93
|
-
const path = fencePath(home);
|
|
94
|
-
const raw = readFenceRaw(path);
|
|
95
|
-
if (raw === null)
|
|
96
|
-
return;
|
|
97
|
-
const fence = parseFence(raw);
|
|
98
|
-
if (fence.ownerPid === callerPid)
|
|
99
|
-
return;
|
|
100
|
-
if (fence.ownerPid > 0 && !processIsAlive(fence.ownerPid)) {
|
|
101
|
-
// Stale fence from a crashed upgrade: reclaim it with the crash-safe atomic
|
|
102
|
-
// rename-aside (F4/R2-F4), then RE-VERIFY. We only permit the write if the
|
|
103
|
-
// admission path is now clear of a blocking fence:
|
|
104
|
-
// - absent / owned by us / owned by a still-dead pid -> writable, and
|
|
105
|
-
// - a LIVE foreign owner that took the freed slot -> refuse.
|
|
106
|
-
// If the stale fence is somehow still present (reclaim could not move it), we
|
|
107
|
-
// do NOT return writable — fail closed.
|
|
108
|
-
reclaimStaleFence(path, raw);
|
|
109
|
-
const afterRaw = readFenceRaw(path);
|
|
110
|
-
if (afterRaw === null)
|
|
111
|
-
return; // reclaimed: path is free.
|
|
112
|
-
const after = parseFence(afterRaw);
|
|
113
|
-
if (after.ownerPid === callerPid)
|
|
114
|
-
return;
|
|
115
|
-
if (after.ownerPid > 0 && !processIsAlive(after.ownerPid) && afterRaw !== raw) {
|
|
116
|
-
// A different dead-owner fence appeared (a racer's crashed attempt); it is
|
|
117
|
-
// itself reclaimable, so the Home is not live-fenced. Permit.
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
// Either the SAME stale fence is still there (reclaim did not complete) or a
|
|
121
|
-
// live/undeterminable owner now holds it: fail closed, never falsely writable.
|
|
122
|
-
throw new UpgradeFenceError(after.reason);
|
|
123
|
-
}
|
|
124
|
-
throw new UpgradeFenceError(fence.reason);
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Place an upgrade fence owned by the current process and return a release
|
|
128
|
-
* handle. The handle is idempotent and only removes a fence this process owns.
|
|
129
|
-
*
|
|
130
|
-
* Acquisition is a SINGLE atomic `O_CREAT | O_EXCL` create (P2-5): the kernel
|
|
131
|
-
* guarantees exactly one of any number of concurrent entrants wins that create,
|
|
132
|
-
* so there is no check-then-write window in which two upgraders both believe
|
|
133
|
-
* they acquired. A loser reads the existing fence and either:
|
|
134
|
-
* - re-enters (it already owns the fence — idempotent), or
|
|
135
|
-
* - reclaims a PROVABLY-DEAD owner's stale fence and retries the atomic create
|
|
136
|
-
* once (a dead owner cannot be a live competitor), or
|
|
137
|
-
* - fails closed with {@link UpgradeFenceError} for a live foreign owner or an
|
|
138
|
-
* undeterminable/malformed fence.
|
|
139
|
-
* There is no lease, heartbeat, or multi-round negotiation — just the one atomic
|
|
140
|
-
* create plus a bounded dead-owner reclaim.
|
|
141
|
-
*/
|
|
142
|
-
export function placeUpgradeFence(home, options) {
|
|
143
|
-
const ownerPid = options.ownerPid ?? process.pid;
|
|
144
|
-
const fence = {
|
|
145
|
-
schemaVersion: 1,
|
|
146
|
-
ownerPid,
|
|
147
|
-
reason: options.reason,
|
|
148
|
-
createdAt: options.createdAt
|
|
149
|
-
};
|
|
150
|
-
const content = `${JSON.stringify(fence, null, 2)}\n`;
|
|
151
|
-
const path = fencePath(home);
|
|
152
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
153
|
-
// At most a bounded number of attempts: each reclaim of a dead owner's stale
|
|
154
|
-
// fence is followed by one more atomic-create try. A fresh race is settled by
|
|
155
|
-
// the first atomic create; a live foreign owner fails immediately.
|
|
156
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
157
|
-
if (tryCreateFenceExclusive(path, content)) {
|
|
158
|
-
fsyncDirectory(dirname(path));
|
|
159
|
-
return makeFenceRelease(home, ownerPid);
|
|
160
|
-
}
|
|
161
|
-
// The atomic create lost: a fence already exists. Decide purely from its
|
|
162
|
-
// owner — never by overwriting, which would reintroduce the race.
|
|
163
|
-
const rawExisting = readFenceRaw(path);
|
|
164
|
-
if (rawExisting === null)
|
|
165
|
-
continue; // vanished between create and read; retry.
|
|
166
|
-
const existing = parseFence(rawExisting);
|
|
167
|
-
if (existing.ownerPid === ownerPid) {
|
|
168
|
-
// This process already owns the fence: idempotent re-entry.
|
|
169
|
-
return makeFenceRelease(home, ownerPid);
|
|
170
|
-
}
|
|
171
|
-
if (existing.ownerPid > 0 && !processIsAlive(existing.ownerPid)) {
|
|
172
|
-
// Provably-dead owner: reclaim the stale fence with a crash-safe atomic
|
|
173
|
-
// rename-aside (F4). It moves ONLY the exact stale inode out of the
|
|
174
|
-
// admission path — a racer that replaced it with a live fence is preserved,
|
|
175
|
-
// and no persistent coordination lock is left to orphan and block future
|
|
176
|
-
// reclaims. Then retry the atomic create against the freed path.
|
|
177
|
-
reclaimStaleFence(path, rawExisting);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
// A live foreign owner, or an undeterminable/malformed fence (ownerPid <= 0):
|
|
181
|
-
// another upgrade holds this Home. Fail closed with no coordination protocol.
|
|
182
|
-
throw new UpgradeFenceError(existing.reason);
|
|
183
|
-
}
|
|
184
|
-
// Still contended after the bounded reclaim retries: fail closed rather than spin.
|
|
185
|
-
throw new UpgradeFenceError(readUpgradeFence(home)?.reason ?? "upgrade fence contended");
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Reclaim a stale (dead-owner) fence: delete the fence file at `path` only if its
|
|
189
|
-
* bytes still exactly equal `expectedRaw`, under a short critical section that is
|
|
190
|
-
* itself CRASH-SAFE (R2-F4).
|
|
191
|
-
*
|
|
192
|
-
* The exclusion is a `mkdir` lock — but, mirroring the storage lock's
|
|
193
|
-
* `reclaimDeadLock`, the lock records its owner pid and is itself reclaimable, so
|
|
194
|
-
* a holder that crashes mid-critical-section cannot permanently orphan it and
|
|
195
|
-
* strand admission. On contention we first try to reclaim an orphaned lock (dead
|
|
196
|
-
* owner, or older than a small age bound), then retry; if the lock is genuinely
|
|
197
|
-
* held by a live process we simply return (that live reclaimer will finish, and
|
|
198
|
-
* the caller's next O_EXCL create settles the race).
|
|
199
|
-
*
|
|
200
|
-
* Correctness of the delete: inside the lock we RE-READ the fence and delete only
|
|
201
|
-
* if the bytes are still the exact stale ones, so a racer that O_EXCL-created a
|
|
202
|
-
* fresh live fence at `path` between our observe and the delete is never
|
|
203
|
-
* clobbered. The single O_EXCL create in `placeUpgradeFence` remains the sole
|
|
204
|
-
* winner-decider, so a reclaim never grants ownership by itself.
|
|
205
|
-
*/
|
|
206
|
-
function reclaimStaleFence(path, expectedRaw) {
|
|
207
|
-
const reclaimLock = `${path}.reclaim.lock`;
|
|
208
|
-
const ownerFile = join(reclaimLock, "owner");
|
|
209
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
210
|
-
try {
|
|
211
|
-
mkdirSync(reclaimLock); // atomic; EEXIST if another reclaimer holds it.
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
if (!isEexist(error))
|
|
215
|
-
throw error;
|
|
216
|
-
// Contended: reclaim the lock only if it is orphaned (dead owner or too
|
|
217
|
-
// old), then retry once. A live-held lock means another reclaimer is
|
|
218
|
-
// actively working — leave it and let the O_EXCL create settle the race.
|
|
219
|
-
if (reclaimOrphanedLock(reclaimLock, ownerFile))
|
|
220
|
-
continue;
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
writeFenceLockOwner(ownerFile);
|
|
225
|
-
// Re-read UNDER the lock; delete only if still the exact stale bytes.
|
|
226
|
-
const current = readFenceRaw(path);
|
|
227
|
-
if (current !== null && current === expectedRaw) {
|
|
228
|
-
rmSync(path, { force: true });
|
|
229
|
-
fsyncDirectory(dirname(path));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
rmSync(reclaimLock, { recursive: true, force: true });
|
|
234
|
-
}
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/** Small age bound below which an orphaned-looking lock is given the benefit of the doubt. */
|
|
239
|
-
const RECLAIM_LOCK_MIN_AGE_MS = 1_000;
|
|
240
|
-
/**
|
|
241
|
-
* Reclaim a reclaim-lock directory ONLY when it is provably orphaned: its owner
|
|
242
|
-
* pid is dead (or unreadable) AND it is older than a small age bound (so a lock
|
|
243
|
-
* whose owner just `mkdir`ed but has not yet written its pid is not stolen).
|
|
244
|
-
* Returns true when it removed the lock (caller should retry), false otherwise.
|
|
245
|
-
*/
|
|
246
|
-
function reclaimOrphanedLock(reclaimLock, ownerFile) {
|
|
247
|
-
try {
|
|
248
|
-
const age = Date.now() - statSync(reclaimLock).mtimeMs;
|
|
249
|
-
if (age < RECLAIM_LOCK_MIN_AGE_MS)
|
|
250
|
-
return false; // too fresh; owner may be mid-acquire.
|
|
251
|
-
let pid;
|
|
252
|
-
try {
|
|
253
|
-
pid = Number.parseInt(readFileSync(ownerFile, "utf8"), 10);
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
pid = null; // no/unreadable owner on an old lock: treat as orphaned.
|
|
257
|
-
}
|
|
258
|
-
if (pid !== null && Number.isInteger(pid) && processIsAlive(pid))
|
|
259
|
-
return false; // live holder.
|
|
260
|
-
rmSync(reclaimLock, { recursive: true, force: true });
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
// ENOENT: the lock vanished (holder released it) — retry the mkdir.
|
|
265
|
-
return isEnoent(error);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
function writeFenceLockOwner(ownerFile) {
|
|
269
|
-
let descriptor;
|
|
270
|
-
try {
|
|
271
|
-
descriptor = openSync(ownerFile, constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC, 0o600);
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// Best effort: without the owner pid the lock is still age-reclaimable, so a
|
|
275
|
-
// failure to record it degrades to the age bound rather than orphaning.
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
// Keep the descriptor so it can be closed in `finally` — never leak an fd per
|
|
279
|
-
// reclaim (R3-F5).
|
|
280
|
-
try {
|
|
281
|
-
writeSync(descriptor, `${process.pid}\n`);
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
// Recording the pid is best-effort; the age bound still bounds reclaim.
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
closeSync(descriptor);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Atomically create the fence file and write its content, failing (returning
|
|
292
|
-
* `false`) iff the file already exists. The `O_EXCL` create is the single point
|
|
293
|
-
* that decides a concurrent race; the owner bytes are written into the same
|
|
294
|
-
* descriptor and fsynced before it is closed.
|
|
295
|
-
*/
|
|
296
|
-
function tryCreateFenceExclusive(path, content) {
|
|
297
|
-
let descriptor;
|
|
298
|
-
try {
|
|
299
|
-
descriptor = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
300
|
-
}
|
|
301
|
-
catch (error) {
|
|
302
|
-
if (isEexist(error))
|
|
303
|
-
return false;
|
|
304
|
-
throw error;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
writeSync(descriptor, content);
|
|
308
|
-
fsyncSync(descriptor);
|
|
309
|
-
}
|
|
310
|
-
finally {
|
|
311
|
-
closeSync(descriptor);
|
|
312
|
-
}
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
/** Build the idempotent release handle that only clears a fence this owner holds. */
|
|
316
|
-
function makeFenceRelease(home, ownerPid) {
|
|
317
|
-
return () => {
|
|
318
|
-
const current = readUpgradeFence(home);
|
|
319
|
-
if (current !== null && current.ownerPid === ownerPid)
|
|
320
|
-
clearUpgradeFence(home);
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
/** Unconditionally remove any fence marker (used by release and stale reclaim). */
|
|
324
|
-
export function clearUpgradeFence(home) {
|
|
325
|
-
rmSync(fencePath(home), { force: true });
|
|
326
|
-
}
|
|
327
|
-
function processIsAlive(pid) {
|
|
328
|
-
try {
|
|
329
|
-
process.kill(pid, 0);
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function isEnoent(error) {
|
|
337
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
338
|
-
}
|
|
339
|
-
function isEexist(error) {
|
|
340
|
-
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* fsync the directory holding the fence so a crash right after acquisition still
|
|
344
|
-
* observes the fence on the next boot (the marker's whole purpose is durability
|
|
345
|
-
* across an interrupted upgrade). Best-effort: a platform that cannot open a
|
|
346
|
-
* directory for fsync must not fail the acquisition.
|
|
347
|
-
*/
|
|
348
|
-
function fsyncDirectory(directory) {
|
|
349
|
-
let descriptor;
|
|
350
|
-
try {
|
|
351
|
-
descriptor = openSync(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
try {
|
|
357
|
-
fsyncSync(descriptor);
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
// Some filesystems reject fsync on a directory fd; the O_EXCL create already
|
|
361
|
-
// committed the inode, so this is not fatal to correctness.
|
|
362
|
-
}
|
|
363
|
-
finally {
|
|
364
|
-
closeSync(descriptor);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
|
|
2
|
-
/**
|
|
3
|
-
* Issue 09 — historical compaction.
|
|
4
|
-
*
|
|
5
|
-
* Progress events that were appended to semantic Task event history (legacy
|
|
6
|
-
* behavior) are folded into the telemetry tables inside the Home's `yui.db`:
|
|
7
|
-
* the newest `keep` rows per Run/generation become the retained window, and
|
|
8
|
-
* every Run/generation gets an accurate aggregate
|
|
9
|
-
* (count/first/last/maxSequence/errorCount). The semantic progress events are
|
|
10
|
-
* then removed through the store's public transaction adapter. Semantic
|
|
11
|
-
* evidence (Run dispatch/delivery/terminal receipts, Session lifecycle,
|
|
12
|
-
* Review, Decision, Integration, ...) is never touched.
|
|
13
|
-
*
|
|
14
|
-
* Compaction runs on a staged copy of a Home. The caller promotes the staged
|
|
15
|
-
* copy only after this module validates the result; a validation failure
|
|
16
|
-
* throws and leaves the original history unchanged.
|
|
17
|
-
*/
|
|
18
|
-
/** Historical pre-Driver event type accepted only by the offline migration. */
|
|
19
|
-
export const PROGRESS_EVENT_TYPE = "runtime.provider-turn-progress";
|
|
20
|
-
/**
|
|
21
|
-
* Scan a store and build the compaction plan. Read-only: nothing is modified.
|
|
22
|
-
*/
|
|
23
|
-
export function planTelemetryCompaction(store, options = {}) {
|
|
24
|
-
const keep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
|
|
25
|
-
const tasks = [];
|
|
26
|
-
let progressEvents = 0;
|
|
27
|
-
let generations = 0;
|
|
28
|
-
let telemetryRows = 0;
|
|
29
|
-
for (const task of store.listTasks()) {
|
|
30
|
-
const taskPlan = planTaskCompaction(task.id, store.listEvents(task.id), keep);
|
|
31
|
-
if (taskPlan === null)
|
|
32
|
-
continue;
|
|
33
|
-
tasks.push(taskPlan);
|
|
34
|
-
progressEvents += taskPlan.progressEventIds.length;
|
|
35
|
-
generations += taskPlan.generations.length;
|
|
36
|
-
for (const generation of taskPlan.generations)
|
|
37
|
-
telemetryRows += generation.window.length;
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
tasks,
|
|
41
|
-
totals: { tasks: tasks.length, progressEvents, generations, telemetryRows }
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function planTaskCompaction(taskId, events, keep) {
|
|
45
|
-
const byGeneration = new Map();
|
|
46
|
-
const progressEventIds = [];
|
|
47
|
-
let malformedKept = 0;
|
|
48
|
-
for (const event of events) {
|
|
49
|
-
if (event.type !== PROGRESS_EVENT_TYPE)
|
|
50
|
-
continue;
|
|
51
|
-
const runId = event.payload.runId;
|
|
52
|
-
const progressId = event.payload.progressId;
|
|
53
|
-
if (typeof runId !== "string" || typeof progressId !== "string") {
|
|
54
|
-
// Fail closed: an anomalous progress row stays semantic and is reported.
|
|
55
|
-
malformedKept++;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const roleName = typeof event.payload.roleName === "string" ? event.payload.roleName : "unknown";
|
|
59
|
-
const generation = typeof event.payload.launchId === "string" && event.payload.launchId !== ""
|
|
60
|
-
? event.payload.launchId
|
|
61
|
-
: typeof event.payload.nativeSessionId === "string" && event.payload.nativeSessionId !== ""
|
|
62
|
-
? event.payload.nativeSessionId
|
|
63
|
-
: "unbound";
|
|
64
|
-
const key = `${roleName}\u0000${runId}\u0000${generation}`;
|
|
65
|
-
const group = byGeneration.get(key);
|
|
66
|
-
if (group === undefined)
|
|
67
|
-
byGeneration.set(key, [event]);
|
|
68
|
-
else
|
|
69
|
-
group.push(event);
|
|
70
|
-
progressEventIds.push(event.id);
|
|
71
|
-
}
|
|
72
|
-
if (progressEventIds.length === 0 && malformedKept === 0)
|
|
73
|
-
return null;
|
|
74
|
-
const generations = [];
|
|
75
|
-
for (const group of byGeneration.values()) {
|
|
76
|
-
generations.push(planGenerationCompaction(taskId, group, keep));
|
|
77
|
-
}
|
|
78
|
-
return { taskId, progressEventIds, malformedKept, generations };
|
|
79
|
-
}
|
|
80
|
-
function planGenerationCompaction(taskId, events, keep) {
|
|
81
|
-
const first = events[0];
|
|
82
|
-
const roleName = typeof first.payload.roleName === "string" ? first.payload.roleName : "unknown";
|
|
83
|
-
const runId = first.payload.runId;
|
|
84
|
-
const generation = typeof first.payload.launchId === "string" && first.payload.launchId !== ""
|
|
85
|
-
? first.payload.launchId
|
|
86
|
-
: typeof first.payload.nativeSessionId === "string" && first.payload.nativeSessionId !== ""
|
|
87
|
-
? first.payload.nativeSessionId
|
|
88
|
-
: "unbound";
|
|
89
|
-
const ordered = [...events].sort(compareProgressEvents);
|
|
90
|
-
let firstAt = progressAt(ordered[0]);
|
|
91
|
-
let lastAt = firstAt;
|
|
92
|
-
let maxSequence = null;
|
|
93
|
-
let errorCount = 0;
|
|
94
|
-
for (const event of ordered) {
|
|
95
|
-
const at = progressAt(event);
|
|
96
|
-
if (at < firstAt)
|
|
97
|
-
firstAt = at;
|
|
98
|
-
if (at > lastAt)
|
|
99
|
-
lastAt = at;
|
|
100
|
-
const sequence = event.payload.sequence;
|
|
101
|
-
if (typeof sequence === "string") {
|
|
102
|
-
const value = Number(sequence);
|
|
103
|
-
if (Number.isSafeInteger(value) && (maxSequence === null || value > maxSequence)) {
|
|
104
|
-
maxSequence = value;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if ((typeof event.payload.error === "string" && event.payload.error !== "")
|
|
108
|
-
|| (typeof event.payload.errorKind === "string" && event.payload.errorKind !== "")) {
|
|
109
|
-
errorCount++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const window = ordered.slice(-keep).map((event) => ({
|
|
113
|
-
taskId,
|
|
114
|
-
roleName,
|
|
115
|
-
runId,
|
|
116
|
-
generation,
|
|
117
|
-
progressId: event.payload.progressId,
|
|
118
|
-
...(typeof event.payload.sequence === "string"
|
|
119
|
-
&& Number.isSafeInteger(Number(event.payload.sequence))
|
|
120
|
-
? { sequence: Number(event.payload.sequence) }
|
|
121
|
-
: {}),
|
|
122
|
-
payload: { ...event.payload },
|
|
123
|
-
receivedAt: progressAt(event)
|
|
124
|
-
}));
|
|
125
|
-
return {
|
|
126
|
-
roleName,
|
|
127
|
-
runId,
|
|
128
|
-
generation,
|
|
129
|
-
aggregate: {
|
|
130
|
-
taskId,
|
|
131
|
-
roleName,
|
|
132
|
-
runId,
|
|
133
|
-
generation,
|
|
134
|
-
firstAt,
|
|
135
|
-
lastAt,
|
|
136
|
-
count: ordered.length,
|
|
137
|
-
maxSequence,
|
|
138
|
-
errorCount
|
|
139
|
-
},
|
|
140
|
-
window
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
function compareProgressEvents(a, b) {
|
|
144
|
-
const seqA = numericSequence(a);
|
|
145
|
-
const seqB = numericSequence(b);
|
|
146
|
-
if (seqA !== null && seqB !== null && seqA !== seqB)
|
|
147
|
-
return seqA - seqB;
|
|
148
|
-
const atA = Date.parse(progressAt(a));
|
|
149
|
-
const atB = Date.parse(progressAt(b));
|
|
150
|
-
if (atA !== atB)
|
|
151
|
-
return atA - atB;
|
|
152
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
153
|
-
}
|
|
154
|
-
function numericSequence(event) {
|
|
155
|
-
const raw = event.payload.sequence;
|
|
156
|
-
if (typeof raw !== "string")
|
|
157
|
-
return null;
|
|
158
|
-
const value = Number(raw);
|
|
159
|
-
return Number.isSafeInteger(value) ? value : null;
|
|
160
|
-
}
|
|
161
|
-
function progressAt(event) {
|
|
162
|
-
const raw = event.payload.progressAt;
|
|
163
|
-
if (typeof raw === "string" && Number.isFinite(Date.parse(raw)))
|
|
164
|
-
return raw;
|
|
165
|
-
return event.createdAt;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Apply a plan to a staged Home. The telemetry windows/aggregates are imported
|
|
169
|
-
* first and validated, then the semantic progress events are removed inside
|
|
170
|
-
* one store transaction and validated again. Throws on any validation
|
|
171
|
-
* failure; the caller must discard the staged Home in that case. A crash
|
|
172
|
-
* between the two phases leaves the staged Home in the equivalent of
|
|
173
|
-
* dual-write state (telemetry imported, semantic progress still present),
|
|
174
|
-
* which is harmless and re-runnable.
|
|
175
|
-
*/
|
|
176
|
-
export function applyTelemetryCompaction(store, telemetry, plan, options) {
|
|
177
|
-
const now = options.now ?? new Date();
|
|
178
|
-
const keep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
|
|
179
|
-
if (!options.dryRun) {
|
|
180
|
-
// Import first and validate the sidecar before touching semantic history:
|
|
181
|
-
// a validation failure leaves the staged Home's semantic events intact.
|
|
182
|
-
for (const task of plan.tasks) {
|
|
183
|
-
for (const generation of task.generations) {
|
|
184
|
-
telemetry.importGeneration(generation.window, generation.aggregate);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
validateAggregates(telemetry, plan);
|
|
188
|
-
store.transaction(() => {
|
|
189
|
-
for (const task of plan.tasks) {
|
|
190
|
-
if (task.progressEventIds.length === 0)
|
|
191
|
-
continue;
|
|
192
|
-
store.removeEvents(task.taskId, task.progressEventIds);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
validateSemanticClean(store, plan);
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
schemaVersion: 1,
|
|
199
|
-
createdAt: now.toISOString(),
|
|
200
|
-
dryRun: options.dryRun,
|
|
201
|
-
source: options.source,
|
|
202
|
-
terminalKeep: keep,
|
|
203
|
-
totals: plan.totals,
|
|
204
|
-
tasks: plan.tasks.map((task) => ({
|
|
205
|
-
taskId: task.taskId,
|
|
206
|
-
removedProgressEvents: options.dryRun ? 0 : task.progressEventIds.length,
|
|
207
|
-
generations: task.generations.map((generation) => generation.aggregate)
|
|
208
|
-
})),
|
|
209
|
-
validation: "passed"
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Verify every planned Run/generation aggregate in the sidecar matches the
|
|
214
|
-
* plan (count, first/last, max sequence) before semantic history is touched.
|
|
215
|
-
*/
|
|
216
|
-
function validateAggregates(telemetry, plan) {
|
|
217
|
-
for (const task of plan.tasks) {
|
|
218
|
-
for (const generation of task.generations) {
|
|
219
|
-
const stored = telemetry.aggregateGeneration(generation.aggregate.taskId, generation.aggregate.roleName, generation.aggregate.runId, generation.aggregate.generation);
|
|
220
|
-
if (stored === null) {
|
|
221
|
-
throw new Error(`Compaction validation failed: missing aggregate for Run ${generation.aggregate.runId}.`);
|
|
222
|
-
}
|
|
223
|
-
const expected = generation.aggregate;
|
|
224
|
-
if (stored.count !== expected.count
|
|
225
|
-
|| stored.firstAt !== expected.firstAt
|
|
226
|
-
|| stored.lastAt !== expected.lastAt
|
|
227
|
-
|| stored.maxSequence !== expected.maxSequence
|
|
228
|
-
|| stored.errorCount !== expected.errorCount) {
|
|
229
|
-
throw new Error(`Compaction validation failed: aggregate mismatch for Run ${expected.runId} `
|
|
230
|
-
+ `(expected count=${expected.count} first=${expected.firstAt} last=${expected.lastAt} `
|
|
231
|
-
+ `maxSequence=${String(expected.maxSequence)} errors=${expected.errorCount}; `
|
|
232
|
-
+ `got count=${stored.count} first=${stored.firstAt} last=${stored.lastAt} `
|
|
233
|
-
+ `maxSequence=${String(stored.maxSequence)} errors=${stored.errorCount}).`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/** Verify no planned progress event remains in semantic history. */
|
|
239
|
-
function validateSemanticClean(store, plan) {
|
|
240
|
-
for (const task of plan.tasks) {
|
|
241
|
-
if (task.progressEventIds.length === 0)
|
|
242
|
-
continue;
|
|
243
|
-
const remaining = new Set(store.listEvents(task.taskId)
|
|
244
|
-
.filter((event) => event.type === PROGRESS_EVENT_TYPE)
|
|
245
|
-
.map((event) => event.id));
|
|
246
|
-
for (const id of task.progressEventIds) {
|
|
247
|
-
if (remaining.has(id)) {
|
|
248
|
-
throw new Error(`Compaction validation failed: progress event ${task.taskId}/${id} still present.`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|