@zq-silk/yui 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -2,7 +2,7 @@ import { createAgentRun } from "../run/agentRun.js";
|
|
|
2
2
|
import { markYuiRunInput } from "../run/runIdentity.js";
|
|
3
3
|
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
4
4
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
5
|
-
import { effectiveLaunchSnapshotsCompatible } from "../executor/effectiveLaunch.js";
|
|
5
|
+
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
6
6
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
7
7
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
8
8
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
@@ -93,7 +93,9 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
const compatibleSession = existingSession !== null
|
|
96
|
-
&&
|
|
96
|
+
&& (reopening
|
|
97
|
+
? effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective)
|
|
98
|
+
: effectiveLaunchSnapshotsCompatibleForTaskMain(existingSession.effective, role.effective, role.managedWorkspace));
|
|
97
99
|
const reopenIdentityDrift = reopening
|
|
98
100
|
&& hasNativeSession(existingSession)
|
|
99
101
|
&& !compatibleSession;
|
|
@@ -146,7 +148,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
146
148
|
...(mode === "resume" ? { nativeSessionId: existingSession.nativeSessionId } : {}),
|
|
147
149
|
beforeHostStart: (preflight) => {
|
|
148
150
|
persistPreStartFence(store, task, role, run, existingSession, mode, preflight, now);
|
|
149
|
-
effectiveSession = preflightSession(run.effective, existingSession, mode, preflight);
|
|
151
|
+
effectiveSession = preflightSession(role, run.effective, existingSession, mode, preflight);
|
|
150
152
|
preStartFencePersisted = true;
|
|
151
153
|
}
|
|
152
154
|
});
|
|
@@ -164,7 +166,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
164
166
|
if (!preStartFencePersisted) {
|
|
165
167
|
const preparedFenceSession = prepared.session === undefined
|
|
166
168
|
? existingSession
|
|
167
|
-
: validateReadySession(run.effective, existingSession, mode, prepared.session);
|
|
169
|
+
: validateReadySession(role, run.effective, existingSession, mode, prepared.session);
|
|
168
170
|
effectiveSession = preparedFenceSession;
|
|
169
171
|
store.saveRoleRunPrepared({
|
|
170
172
|
task,
|
|
@@ -191,7 +193,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
191
193
|
results.push({ taskId: task.id, status: "skipped", reason: "unavailable" });
|
|
192
194
|
continue;
|
|
193
195
|
}
|
|
194
|
-
effectiveSession = validateReadySession(run.effective, existingSession, mode, ready.session);
|
|
196
|
+
effectiveSession = validateReadySession(role, run.effective, existingSession, mode, ready.session);
|
|
195
197
|
store.saveRoleRunPrepared({
|
|
196
198
|
task,
|
|
197
199
|
role,
|
|
@@ -309,7 +311,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
309
311
|
}
|
|
310
312
|
return results;
|
|
311
313
|
}
|
|
312
|
-
function validateReadySession(effective, existing, mode, session) {
|
|
314
|
+
function validateReadySession(role, effective, existing, mode, session) {
|
|
313
315
|
if (mode === "new" && session === null)
|
|
314
316
|
return null;
|
|
315
317
|
if (session === null)
|
|
@@ -317,7 +319,10 @@ function validateReadySession(effective, existing, mode, session) {
|
|
|
317
319
|
if (session.agentId !== effective.agentId || session.adapterId !== effective.adapterId) {
|
|
318
320
|
throw new Error(`Ready session belongs to another Agent: ${session.agentId}.`);
|
|
319
321
|
}
|
|
320
|
-
|
|
322
|
+
const compatible = mode === "resume"
|
|
323
|
+
? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, effective, role.managedWorkspace)
|
|
324
|
+
: effectiveLaunchSnapshotsCompatible(session.effective, effective);
|
|
325
|
+
if (!compatible) {
|
|
321
326
|
throw new Error("Ready Leader session effective snapshot changed.");
|
|
322
327
|
}
|
|
323
328
|
if (!hasNativeSession(session)) {
|
|
@@ -326,7 +331,13 @@ function validateReadySession(effective, existing, mode, session) {
|
|
|
326
331
|
if (mode === "resume" && session.nativeSessionId !== existing?.nativeSessionId) {
|
|
327
332
|
throw new Error("Leader resume changed the fixed native session id.");
|
|
328
333
|
}
|
|
329
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
...session,
|
|
336
|
+
status: "running",
|
|
337
|
+
...(mode === "resume" && existing !== null
|
|
338
|
+
? { effective: existing.effective }
|
|
339
|
+
: {})
|
|
340
|
+
};
|
|
330
341
|
}
|
|
331
342
|
function persistPreStartFence(store, task, role, run, existingSession, mode, preflight, now) {
|
|
332
343
|
if (preflight.owner.scope !== "task"
|
|
@@ -336,7 +347,7 @@ function persistPreStartFence(store, task, role, run, existingSession, mode, pre
|
|
|
336
347
|
|| preflight.launchId.trim().length === 0) {
|
|
337
348
|
throw new Error(`Pre-start launch fence changed the Leader Run: ${task.id}/${role.name}.`);
|
|
338
349
|
}
|
|
339
|
-
const session = preflightSession(run.effective, existingSession, mode, preflight);
|
|
350
|
+
const session = preflightSession(role, run.effective, existingSession, mode, preflight);
|
|
340
351
|
store.saveRoleRunPrepared({
|
|
341
352
|
task,
|
|
342
353
|
role,
|
|
@@ -346,7 +357,7 @@ function persistPreStartFence(store, task, role, run, existingSession, mode, pre
|
|
|
346
357
|
now
|
|
347
358
|
});
|
|
348
359
|
}
|
|
349
|
-
function preflightSession(effective, existing, mode, preflight) {
|
|
360
|
+
function preflightSession(role, effective, existing, mode, preflight) {
|
|
350
361
|
const session = preflight.nativeSessionId === undefined
|
|
351
362
|
? null
|
|
352
363
|
: {
|
|
@@ -357,7 +368,7 @@ function preflightSession(effective, existing, mode, preflight) {
|
|
|
357
368
|
status: "ready",
|
|
358
369
|
effective: preflight.effective
|
|
359
370
|
};
|
|
360
|
-
return validateReadySession(effective, existing, mode, session);
|
|
371
|
+
return validateReadySession(role, effective, existing, mode, session);
|
|
361
372
|
}
|
|
362
373
|
function hasNativeSession(session) {
|
|
363
374
|
return session !== null &&
|
|
@@ -17,6 +17,22 @@ export const RUN_RESOURCE_SUPPRESSED_EVENT = "run.resource-suppressed";
|
|
|
17
17
|
/** Structured, non-Message recovery evidence written by an explicit Leader. */
|
|
18
18
|
export const RUN_RECOVERY_REQUESTED_EVENT = "run.recovery-requested";
|
|
19
19
|
export const RUN_RECOVERY_APPLIED_EVENT = "run.recovery-applied";
|
|
20
|
+
/** Event types that count as Run activity for the durable progress clock. */
|
|
21
|
+
const ACTIVITY_EVENT_TYPES = new Set([
|
|
22
|
+
RUN_PROGRESS_EVENT,
|
|
23
|
+
"runtime.provider-turn-progress",
|
|
24
|
+
"message.sent",
|
|
25
|
+
"input.answered",
|
|
26
|
+
"input.auto-answered",
|
|
27
|
+
"input.cancelled",
|
|
28
|
+
"work.accepted",
|
|
29
|
+
"work.updated",
|
|
30
|
+
"review.completed",
|
|
31
|
+
"review.failed",
|
|
32
|
+
"integration.updated",
|
|
33
|
+
"integration.completed",
|
|
34
|
+
"integration.failed"
|
|
35
|
+
]);
|
|
20
36
|
/**
|
|
21
37
|
* Clear only the Operator projection raised for this exact Leader Run. A
|
|
22
38
|
* later Run (or a recovery notification of another kind) must remain intact;
|
|
@@ -195,16 +211,23 @@ export function currentRoleRunProgressAt(store, taskId, roleName, run, events =
|
|
|
195
211
|
}
|
|
196
212
|
if (run.deliveredAt === undefined)
|
|
197
213
|
return { progressAt: run.createdAt };
|
|
214
|
+
// The richer fold is authoritative when present; skip the per-candidate
|
|
215
|
+
// full-history scans that every stall candidate otherwise pays.
|
|
216
|
+
if (richerProgress !== undefined && richerProgress !== null) {
|
|
217
|
+
return {
|
|
218
|
+
progressAt: richerProgress.progressAt,
|
|
219
|
+
...(richerProgress.evidence === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { evidence: richerProgress.evidence })
|
|
222
|
+
};
|
|
223
|
+
}
|
|
198
224
|
const fallbackProgressAt = latestDurableProgressAt({
|
|
199
225
|
deliveredAt: run.deliveredAt,
|
|
200
226
|
latestCheckpointAt: latestRunProgressAt(events, run.id),
|
|
201
227
|
latestActivityAt: latestRunActivityAt(events, run.id)
|
|
202
228
|
});
|
|
203
229
|
return {
|
|
204
|
-
progressAt:
|
|
205
|
-
...(richerProgress?.evidence === undefined
|
|
206
|
-
? {}
|
|
207
|
-
: { evidence: richerProgress.evidence })
|
|
230
|
+
progressAt: fallbackProgressAt
|
|
208
231
|
};
|
|
209
232
|
}
|
|
210
233
|
/**
|
|
@@ -212,13 +235,23 @@ export function currentRoleRunProgressAt(store, taskId, roleName, run, events =
|
|
|
212
235
|
* related-record readers mirror the adapter's Work/Review/Integration fold;
|
|
213
236
|
* narrow scheduler ports can omit them and still retain Run/Event evidence.
|
|
214
237
|
*/
|
|
215
|
-
export function latestRunDurableProgressAt(store, taskId, roleName, runId) {
|
|
238
|
+
export function latestRunDurableProgressAt(store, taskId, roleName, runId, precomputed) {
|
|
216
239
|
const run = store.getAgentRun(taskId, runId);
|
|
217
240
|
if (run === null || run.taskId !== taskId || run.roleName !== roleName)
|
|
218
241
|
return null;
|
|
219
242
|
const events = store.listEvents(taskId);
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
// The adapter folds checkpoint/activity once per revision. When the fold
|
|
244
|
+
// port exists, a missing per-Run entry is an authoritative empty fold: use
|
|
245
|
+
// its (possibly undefined) values directly and never re-scan the whole
|
|
246
|
+
// history per candidate. Legacy callers without the port omit precomputed
|
|
247
|
+
// and keep the per-candidate full-history scans.
|
|
248
|
+
const folded = precomputed !== undefined;
|
|
249
|
+
const latestCheckpointAt = folded
|
|
250
|
+
? precomputed.latestCheckpointAt
|
|
251
|
+
: latestRunProgressAt(events, run.id);
|
|
252
|
+
const latestActivityAt = folded
|
|
253
|
+
? precomputed.latestActivityAt
|
|
254
|
+
: latestRunActivityAt(events, run.id);
|
|
222
255
|
const baseline = run.deliveredAt === undefined
|
|
223
256
|
? run.createdAt
|
|
224
257
|
: latestDurableProgressAt({
|
|
@@ -280,24 +313,9 @@ export function latestRunEventTime(events, type, runId) {
|
|
|
280
313
|
}
|
|
281
314
|
/** Most recent non-control event carrying a Run identity. */
|
|
282
315
|
export function latestRunActivityAt(events, runId) {
|
|
283
|
-
const semanticTypes = new Set([
|
|
284
|
-
RUN_PROGRESS_EVENT,
|
|
285
|
-
"runtime.provider-turn-progress",
|
|
286
|
-
"message.sent",
|
|
287
|
-
"input.answered",
|
|
288
|
-
"input.auto-answered",
|
|
289
|
-
"input.cancelled",
|
|
290
|
-
"work.accepted",
|
|
291
|
-
"work.updated",
|
|
292
|
-
"review.completed",
|
|
293
|
-
"review.failed",
|
|
294
|
-
"integration.updated",
|
|
295
|
-
"integration.completed",
|
|
296
|
-
"integration.failed"
|
|
297
|
-
]);
|
|
298
316
|
let latest;
|
|
299
317
|
for (const event of events) {
|
|
300
|
-
if (!
|
|
318
|
+
if (!ACTIVITY_EVENT_TYPES.has(event.type)
|
|
301
319
|
|| event.payload.runId !== runId
|
|
302
320
|
|| event.type === RUN_STALLED_EVENT
|
|
303
321
|
|| event.type === RUN_RECOVERED_EVENT)
|
|
@@ -347,6 +365,79 @@ export function latestStallEvidenceKey(events, runId) {
|
|
|
347
365
|
evidenceKey: latest.payload.evidenceKey ?? "live-pane-no-progress"
|
|
348
366
|
};
|
|
349
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Folds a Task's event history into per-Run progress facts in one O(events)
|
|
370
|
+
* pass. The adapter builds this once per durable revision and serves the
|
|
371
|
+
* stall reconciliation from it, replacing the per-candidate full-history
|
|
372
|
+
* clones and scans that turned one pass into an O(candidates x events) hot
|
|
373
|
+
* read. The fold mirrors latestRunProgressAt/latestRunActivityAt/
|
|
374
|
+
* latestStallProgressAt/latestStallEvidenceKey exactly.
|
|
375
|
+
*/
|
|
376
|
+
export function foldRunProgressFacts(events) {
|
|
377
|
+
const byRun = new Map();
|
|
378
|
+
const latestStallCreatedAt = new Map();
|
|
379
|
+
for (const event of events) {
|
|
380
|
+
const runId = event.payload.runId;
|
|
381
|
+
if (typeof runId !== "string")
|
|
382
|
+
continue;
|
|
383
|
+
let facts = byRun.get(runId);
|
|
384
|
+
if (facts === undefined) {
|
|
385
|
+
facts = {};
|
|
386
|
+
byRun.set(runId, facts);
|
|
387
|
+
}
|
|
388
|
+
const type = event.type;
|
|
389
|
+
const isProgress = type === RUN_PROGRESS_EVENT
|
|
390
|
+
|| type === "runtime.provider-turn-progress";
|
|
391
|
+
if (isProgress) {
|
|
392
|
+
const validProgress = typeof event.payload.progressAt === "string"
|
|
393
|
+
&& Number.isFinite(Date.parse(event.payload.progressAt));
|
|
394
|
+
if (type === "runtime.provider-turn-progress" && !validProgress)
|
|
395
|
+
continue;
|
|
396
|
+
const progressAt = validProgress
|
|
397
|
+
? event.payload.progressAt
|
|
398
|
+
: event.createdAt;
|
|
399
|
+
if (facts.latestCheckpointAt === undefined
|
|
400
|
+
|| Date.parse(progressAt) > Date.parse(facts.latestCheckpointAt)) {
|
|
401
|
+
facts.latestCheckpointAt = progressAt;
|
|
402
|
+
}
|
|
403
|
+
// Both progress event types are activity events; the stall/recovered
|
|
404
|
+
// exclusion in latestRunActivityAt is structural here because a progress
|
|
405
|
+
// event can never be either type.
|
|
406
|
+
if (facts.latestActivityAt === undefined
|
|
407
|
+
|| Date.parse(progressAt) > Date.parse(facts.latestActivityAt)) {
|
|
408
|
+
facts.latestActivityAt = progressAt;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (type === RUN_STALLED_EVENT) {
|
|
412
|
+
const previousCreatedAt = latestStallCreatedAt.get(runId);
|
|
413
|
+
if (previousCreatedAt === undefined
|
|
414
|
+
|| Date.parse(event.createdAt) > Date.parse(previousCreatedAt)) {
|
|
415
|
+
latestStallCreatedAt.set(runId, event.createdAt);
|
|
416
|
+
facts.latestStall = typeof event.payload.progressAt === "string"
|
|
417
|
+
? {
|
|
418
|
+
progressAt: event.payload.progressAt,
|
|
419
|
+
evidenceKey: typeof event.payload.evidenceKey === "string"
|
|
420
|
+
? event.payload.evidenceKey
|
|
421
|
+
: "live-pane-no-progress"
|
|
422
|
+
}
|
|
423
|
+
: undefined;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else if (ACTIVITY_EVENT_TYPES.has(type)
|
|
427
|
+
&& type !== RUN_STALLED_EVENT
|
|
428
|
+
&& type !== RUN_RECOVERED_EVENT) {
|
|
429
|
+
if (facts.latestActivityAt === undefined
|
|
430
|
+
|| Date.parse(event.createdAt) > Date.parse(facts.latestActivityAt)) {
|
|
431
|
+
facts.latestActivityAt = event.createdAt;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return byRun;
|
|
436
|
+
}
|
|
437
|
+
/** Yield the event loop so already-written control requests stay responsive. */
|
|
438
|
+
function yieldEventLoop() {
|
|
439
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
440
|
+
}
|
|
350
441
|
/**
|
|
351
442
|
* True while a Run remains in an unresolved stall episode: a stall was raised
|
|
352
443
|
* and no later checkpoint has advanced its durable progress since. This is the
|
|
@@ -388,6 +479,11 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
388
479
|
return [];
|
|
389
480
|
if (store.listEvents === undefined || store.recordRoleRunStall === undefined)
|
|
390
481
|
return [];
|
|
482
|
+
// When the fold port exists, a missing per-Run entry is an authoritative
|
|
483
|
+
// empty fold: the stall reconciliation must not re-scan the whole history
|
|
484
|
+
// per candidate. Legacy stores without the port keep the per-candidate
|
|
485
|
+
// scans.
|
|
486
|
+
const foldPortPresent = store.getRunProgressFacts !== undefined;
|
|
391
487
|
const candidates = selectedSchedulerTasks(store, selection).flatMap((task) => (task.status !== "active"
|
|
392
488
|
? []
|
|
393
489
|
: selectedSchedulerRoles(store, task.id, selection).flatMap((role) => {
|
|
@@ -473,7 +569,12 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
473
569
|
}
|
|
474
570
|
}
|
|
475
571
|
const raised = [];
|
|
476
|
-
for (const candidate of stallCandidates) {
|
|
572
|
+
for (const [candidateIndex, candidate] of stallCandidates.entries()) {
|
|
573
|
+
// A large Task's per-candidate reads are now bounded, but many candidates
|
|
574
|
+
// in one pass still add up. Yield periodically so already-written control
|
|
575
|
+
// requests are not starved by the whole reconciliation.
|
|
576
|
+
if (candidateIndex > 0 && candidateIndex % 16 === 0)
|
|
577
|
+
await yieldEventLoop();
|
|
477
578
|
const key = `${candidate.task.id}\0${candidate.role.name}`;
|
|
478
579
|
let live;
|
|
479
580
|
try {
|
|
@@ -514,6 +615,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
514
615
|
continue;
|
|
515
616
|
}
|
|
516
617
|
const events = store.listEvents(candidate.task.id);
|
|
618
|
+
const progressFacts = store.getRunProgressFacts?.(candidate.task.id, candidate.run.id);
|
|
517
619
|
// Before exact acceptance there is no execution progress clock. Keep the
|
|
518
620
|
// delivery watch anchored to the Run creation/transport boundary even if
|
|
519
621
|
// checkpoints, output, or related WorkItem/Review/Integration records are
|
|
@@ -526,7 +628,9 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
526
628
|
progressAt,
|
|
527
629
|
now,
|
|
528
630
|
windowMs,
|
|
529
|
-
lastAttentionProgressAt:
|
|
631
|
+
lastAttentionProgressAt: foldPortPresent
|
|
632
|
+
? progressFacts?.latestStall?.progressAt
|
|
633
|
+
: latestStallProgressAt(events, candidate.run.id)
|
|
530
634
|
});
|
|
531
635
|
const runAgentId = candidate.run.effective.agentId;
|
|
532
636
|
const runAdapterId = candidate.run.effective.adapterId;
|
|
@@ -605,8 +709,9 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
605
709
|
if (current.live !== "present" || !current.stalled || !current.stallCandidate)
|
|
606
710
|
continue;
|
|
607
711
|
const { candidate, progressAt } = current;
|
|
608
|
-
const
|
|
609
|
-
|
|
712
|
+
const previous = foldPortPresent
|
|
713
|
+
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)?.latestStall
|
|
714
|
+
: latestStallEvidenceKey(store.listEvents(candidate.task.id), candidate.run.id);
|
|
610
715
|
if (previous !== undefined
|
|
611
716
|
&& Date.parse(progressAt) > Date.parse(previous.progressAt)) {
|
|
612
717
|
// A new semantic progress point closes the previous episode first. It
|
|
@@ -678,8 +783,9 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
678
783
|
if (current.live !== "present" || current.stalled || !current.stallCandidate)
|
|
679
784
|
continue;
|
|
680
785
|
const { candidate, progressAt } = current;
|
|
681
|
-
const
|
|
682
|
-
|
|
786
|
+
const previous = foldPortPresent
|
|
787
|
+
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)?.latestStall
|
|
788
|
+
: latestStallEvidenceKey(store.listEvents(candidate.task.id), candidate.run.id);
|
|
683
789
|
if (previous !== undefined
|
|
684
790
|
&& Date.parse(progressAt) > Date.parse(previous.progressAt)) {
|
|
685
791
|
store.recordRoleRunProgress?.({
|
|
@@ -50,6 +50,17 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
50
50
|
roleSessions
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Fold a Task projection from already-read facts. The Task overview reads each
|
|
55
|
+
* per-Task fact once and reuses it here, instead of letting
|
|
56
|
+
* buildTaskExecutionProjection read the store a second time for the same
|
|
57
|
+
* unchanged revision.
|
|
58
|
+
*/
|
|
59
|
+
export function projectTaskExecutionFromFacts(facts) {
|
|
60
|
+
const executionGroups = facts.executionGroups
|
|
61
|
+
?? collectExecutionGroups(facts.workItems ?? [], facts.reviewRounds ?? []);
|
|
62
|
+
return projectTaskExecution({ ...facts, executionGroups });
|
|
63
|
+
}
|
|
53
64
|
/** Alias kept intentionally small for scheduler callers and external read models. */
|
|
54
65
|
export const deriveTaskExecutionProjection = projectTaskExecution;
|
|
55
66
|
export function projectTaskExecution(facts) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { StorageCompatibilityError, loadCompatibleSnapshot } from "./migration/index.js";
|
|
4
4
|
import { createProductionStorageRegistry } from "./migration/productionRegistry.js";
|
|
5
|
-
import { FileTaskStore, validateCurrentStorageStateSnapshot } from "./taskStore.js";
|
|
6
|
-
import {
|
|
5
|
+
import { FileTaskStore, STORAGE_STATE_FILE, stateFileFingerprint, StorageRecordError, validateCurrentStorageStateSnapshot } from "./taskStore.js";
|
|
6
|
+
import { SqliteTaskStore } from "./sqliteStore.js";
|
|
7
|
+
import { COMMITTED_DATABASE_FILENAME } from "./upgrade/sqliteStateMigration.js";
|
|
8
|
+
import { ensureStorageSchema, inspectStorageSchema, readStorageSchemaManifest, STORAGE_SCHEMA_FILE } from "./storageSchema.js";
|
|
7
9
|
import { classifyHome } from "./upgrade/homeClassification.js";
|
|
8
10
|
import { inspectSnapshotVersionState } from "./upgrade/homeMigrationTarget.js";
|
|
9
11
|
import { latestStorageVersionState } from "./upgrade/recordVersions.js";
|
|
@@ -23,14 +25,52 @@ export function initializeCompatibleFileTaskStore(home, options = {}) {
|
|
|
23
25
|
* Open current or explicitly compatible-old storage. Compatible records are
|
|
24
26
|
* normalized in memory through strict old-shape validators; FileTaskStore then
|
|
25
27
|
* runs its one current parser and its existing writer emits only current bytes.
|
|
28
|
+
*
|
|
29
|
+
* A current Home is opened from a single fingerprint-fenced `state.json`
|
|
30
|
+
* snapshot: the same bytes drive the record-version inspection, the strict
|
|
31
|
+
* shape/reference validation, and the returned store's initial cache seed, so
|
|
32
|
+
* the open never re-reads the (large) state merely to construct or first use
|
|
33
|
+
* the store.
|
|
26
34
|
*/
|
|
27
35
|
export function openCompatibleFileTaskStore(home, options = {}) {
|
|
28
36
|
const registry = options.registry ?? createProductionStorageRegistry();
|
|
29
37
|
const latest = options.latest ?? latestStorageVersionState();
|
|
38
|
+
const schema = inspectStorageSchema(home);
|
|
39
|
+
// Issue 01: a layout-7 Home's authoritative backend is SQLite WAL. When
|
|
40
|
+
// `yui.db` exists, open it directly — never fall back to a FileTaskStore
|
|
41
|
+
// over a `state.json` that the repair may already have archived.
|
|
42
|
+
if (!options.forceStateSource
|
|
43
|
+
&&
|
|
44
|
+
(schema.status === "current" || schema.status === "unsupported")
|
|
45
|
+
&& schema.currentLayoutVersion >= 7
|
|
46
|
+
&& existsSync(join(home, COMMITTED_DATABASE_FILENAME))) {
|
|
47
|
+
return new SqliteTaskStore(home);
|
|
48
|
+
}
|
|
49
|
+
if (schema.status === "current") {
|
|
50
|
+
return openCurrentFileTaskStore(home, latest);
|
|
51
|
+
}
|
|
30
52
|
const classification = classifyHome({ home, registry, latest });
|
|
31
53
|
switch (classification.classification.status) {
|
|
32
54
|
case "current":
|
|
33
|
-
|
|
55
|
+
case "needs-storage-repair":
|
|
56
|
+
// A pseudo-layout-7 Home whose aggregate axis is also older cannot be
|
|
57
|
+
// normalized in memory: the compatible loader (requireCompatibleStorageSchema)
|
|
58
|
+
// only bypasses record-axis mismatches, not aggregate/layout ones. Such a
|
|
59
|
+
// Home needs the offline migration path (repair + aggregate migration in
|
|
60
|
+
// one upgrade), so surface the same migration-required error the pure
|
|
61
|
+
// version classifier would have produced, rather than a lazy strict-schema
|
|
62
|
+
// error from the first store read.
|
|
63
|
+
if (classification.classification.status === "needs-storage-repair"
|
|
64
|
+
&& classification.aggregateVersion !== undefined
|
|
65
|
+
&& classification.aggregateVersion < latest.aggregate) {
|
|
66
|
+
throw new StorageCompatibilityError("Storage requires an offline migration. Re-run `yui update` when active Sessions are clear.");
|
|
67
|
+
}
|
|
68
|
+
// A pseudo-layout-7 Home (manifest 7, no yui.db) may also carry
|
|
69
|
+
// compatible-old record versions, so open with the same normalization
|
|
70
|
+
// path as `compatible-old` rather than the strict current gate.
|
|
71
|
+
return new FileTaskStore(home, {
|
|
72
|
+
normalizeState: (raw) => normalizeState(home, raw, registry, latest)
|
|
73
|
+
});
|
|
34
74
|
case "compatible-old":
|
|
35
75
|
return new FileTaskStore(home, {
|
|
36
76
|
normalizeState: (raw) => normalizeState(home, raw, registry, latest)
|
|
@@ -41,13 +81,80 @@ export function openCompatibleFileTaskStore(home, options = {}) {
|
|
|
41
81
|
throw new StorageCompatibilityError(describeUnsupported(classification.classification));
|
|
42
82
|
}
|
|
43
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The one-snapshot open for a Home whose manifest is already current. A single
|
|
86
|
+
* fingerprint-fenced read of `state.json` supplies:
|
|
87
|
+
* - the record-version inspection (structural scan cross-checked against the
|
|
88
|
+
* durable manifest, never the strict loader),
|
|
89
|
+
* - the strict shape/reference validation (the store's own parser, run while
|
|
90
|
+
* seeding its cache), and
|
|
91
|
+
* - the returned store's initial cache seed under the same fingerprint.
|
|
92
|
+
*
|
|
93
|
+
* A fingerprint that drifts across the read retries once and then fails
|
|
94
|
+
* closed; a later external writer still invalidates the seeded cache on the
|
|
95
|
+
* store's next read. Corruption surfaces as the same `StorageCompatibilityError`
|
|
96
|
+
* diagnosis the classifier produced before this fast path existed.
|
|
97
|
+
*/
|
|
98
|
+
function openCurrentFileTaskStore(home, latest) {
|
|
99
|
+
const statePath = join(home, STORAGE_STATE_FILE);
|
|
100
|
+
if (!existsSync(statePath)) {
|
|
101
|
+
// No state.json yet: the store's own missing-file path seeds an empty
|
|
102
|
+
// state without reading anything.
|
|
103
|
+
return new FileTaskStore(home);
|
|
104
|
+
}
|
|
105
|
+
const manifest = readStorageSchemaManifest(home);
|
|
106
|
+
const snapshot = readFingerprintFencedState(statePath);
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(snapshot.raw);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new StorageCompatibilityError(`Invalid state.json: state.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
if (!isRecord(parsed)) {
|
|
115
|
+
throw new StorageCompatibilityError("Invalid state.json: state.json is not a JSON object.");
|
|
116
|
+
}
|
|
117
|
+
const inspection = inspectSnapshotVersionState(Object.freeze({ schemaManifest: manifest, state: parsed }), latest);
|
|
118
|
+
if ("corruption" in inspection) {
|
|
119
|
+
throw new StorageCompatibilityError(`Invalid state.json: ${inspection.corruption.detail}`);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return new FileTaskStore(home, {
|
|
123
|
+
initialStateSnapshot: { fingerprint: snapshot.fingerprint, raw: snapshot.raw }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof StorageRecordError) {
|
|
128
|
+
throw new StorageCompatibilityError(`Invalid state.json: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read `state.json` once, fenced by the same fingerprint the store's read cache
|
|
135
|
+
* is keyed on. A fingerprint that changes between the before and after stat
|
|
136
|
+
* means a concurrent writer is mid-update: retry once (bounded), then fail
|
|
137
|
+
* closed rather than seed a store from bytes that may be torn.
|
|
138
|
+
*/
|
|
139
|
+
export function readFingerprintFencedState(statePath, io = {}) {
|
|
140
|
+
const fingerprint = io.fingerprint ?? stateFileFingerprint;
|
|
141
|
+
const read = io.read ?? ((path) => readFileSync(path, "utf8"));
|
|
142
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
143
|
+
const before = fingerprint(statePath);
|
|
144
|
+
const raw = read(statePath);
|
|
145
|
+
const after = fingerprint(statePath);
|
|
146
|
+
if (before === after)
|
|
147
|
+
return { raw, fingerprint: after };
|
|
148
|
+
}
|
|
149
|
+
throw new StorageCompatibilityError("state.json changed while opening; a concurrent writer is updating the Home. Retry the command.");
|
|
150
|
+
}
|
|
44
151
|
/**
|
|
45
152
|
* Eagerly prove that a compatible-old Home reaches the strict current parser.
|
|
46
153
|
* Staged update/upgrade preflight uses this before any Controller or binary
|
|
47
154
|
* action; ordinary commands may keep the store's normal lazy-read behavior.
|
|
48
155
|
*/
|
|
49
156
|
export function validateCompatibleFileTaskStore(home, options = {}) {
|
|
50
|
-
openCompatibleFileTaskStore(home, options).getConfig();
|
|
157
|
+
openCompatibleFileTaskStore(home, { ...options, forceStateSource: true }).getConfig();
|
|
51
158
|
}
|
|
52
159
|
function normalizeState(home, raw, registry, latest) {
|
|
53
160
|
let state;
|