@zq-silk/yui 0.11.2 → 0.12.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 +23 -4
- package/README.md +80 -13
- package/dist/cli/commandCatalog.js +30 -5
- package/dist/cli.js +105 -43
- package/dist/commands/configCommands.js +12 -45
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/projectCommands.js +581 -2
- package/dist/commands/taskCommands.js +159 -172
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +55 -11
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskIntegrationCommands.js +2 -1
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +24 -0
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +2 -2
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +1 -1
- package/dist/controller/controller.js +8 -12
- package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
- package/dist/controller/runtime.js +3 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +36 -16
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +1 -2
- package/dist/lifecycle/taskRoleSessionReset.js +5 -13
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/repository/checkoutSwap.js +61 -0
- package/dist/repository/gitWorkspace.js +36 -0
- package/dist/repository/project.js +53 -3
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtimeObservation.js +13 -0
- package/dist/runtime/runtimeProjection.js +16 -25
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/scheduler/activeRoleRunDelivery.js +4 -4
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +3 -5
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +0 -14
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +467 -0
- package/dist/storage/sqliteSchema.js +18 -8
- package/dist/storage/sqliteStore.js +61 -21
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +47 -31
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -24
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +4 -0
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +53 -16
- package/skills/yui-operator/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +28 -7
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -55,7 +55,7 @@ import { assertHomeWritable } from "./upgradeFence.js";
|
|
|
55
55
|
import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, validateYuiConfig } from "./taskStore.js";
|
|
56
56
|
import { publicationExternalKey } from "../task/publicationReference.js";
|
|
57
57
|
import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
|
|
58
|
-
import { migrateSqliteSchema, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
|
|
58
|
+
import { migrateSqliteSchema, SqliteSchemaMigrationError, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
|
|
59
59
|
import { inspectStorageSchema } from "./storageSchema.js";
|
|
60
60
|
/** Read the immutable Home identity without opening a writable Store connection. */
|
|
61
61
|
export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
|
|
@@ -143,16 +143,29 @@ export class SqliteTaskStore {
|
|
|
143
143
|
this.#migration = _options.migration ?? false;
|
|
144
144
|
mkdirSync(rootDir, { recursive: true, mode: 0o700 });
|
|
145
145
|
const filename = _options.databaseFilename ?? "yui.db";
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.#db
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
const databasePath = join(rootDir, filename);
|
|
147
|
+
const databaseExisted = existsSync(databasePath);
|
|
148
|
+
if (this.#migration && filename === "yui.db") {
|
|
149
|
+
throw new SqliteSchemaMigrationError("offline migration mode cannot open the authoritative yui.db", "admission");
|
|
150
|
+
}
|
|
151
|
+
this.#db = new Database(databasePath);
|
|
152
|
+
try {
|
|
153
|
+
// §4.1 / §9: WAL, no fsync weakening, FKs on, busy timeout for CLI contention.
|
|
154
|
+
this.#db.pragma("journal_mode = WAL");
|
|
155
|
+
this.#db.pragma("synchronous = FULL");
|
|
156
|
+
this.#db.pragma("foreign_keys = ON");
|
|
157
|
+
this.#db.pragma("busy_timeout = 5000");
|
|
158
|
+
this.#db.pragma("wal_autocheckpoint = 1000");
|
|
159
|
+
migrateSqliteSchema(this.#db, {
|
|
160
|
+
mode: this.#migration || !databaseExisted ? "apply" : "validate"
|
|
161
|
+
});
|
|
162
|
+
this.#seedHomeMeta();
|
|
163
|
+
this.#seedConfig();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.#db.close();
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
156
169
|
}
|
|
157
170
|
rootDirectory() { return this.#rootDir; }
|
|
158
171
|
/** Close the underlying database connection. */
|
|
@@ -596,7 +609,43 @@ export class SqliteTaskStore {
|
|
|
596
609
|
return this.#getPayload("projects", "id = ?", [id]);
|
|
597
610
|
}
|
|
598
611
|
removeProject(id) {
|
|
599
|
-
return this.#mutate(() =>
|
|
612
|
+
return this.#mutate(() => {
|
|
613
|
+
if (this.listTasks().some((task) => task.projectBindings.some((binding) => binding.projectId === id))) {
|
|
614
|
+
throw new StorageRecordError(`Project is still used by a Task: ${id}`);
|
|
615
|
+
}
|
|
616
|
+
return this.#db.prepare("DELETE FROM projects WHERE id = ?").run(id).changes > 0;
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
summarizeProjectReferences(projectId) {
|
|
620
|
+
const boundTasks = this.listTasks().filter((task) => task.projectBindings.some((binding) => binding.projectId === projectId));
|
|
621
|
+
const activeTasks = boundTasks.filter((task) => task.status === "active");
|
|
622
|
+
const unresolvedWorkItemRefs = [];
|
|
623
|
+
const activeRunRefs = [];
|
|
624
|
+
const unresolvedIntegrationRefs = [];
|
|
625
|
+
for (const task of activeTasks) {
|
|
626
|
+
for (const workItem of this.listWorkItems(task.id)) {
|
|
627
|
+
if (workItem.status !== "completed" && workItem.status !== "retired") {
|
|
628
|
+
unresolvedWorkItemRefs.push(`${task.id}/${workItem.id}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
for (const run of this.listAgentRuns(task.id)) {
|
|
632
|
+
if (run.status === "active")
|
|
633
|
+
activeRunRefs.push(`${task.id}/${run.id}`);
|
|
634
|
+
}
|
|
635
|
+
for (const attempt of this.listIntegrationAttempts(task.id)) {
|
|
636
|
+
if (attempt.status === "running" || attempt.status === "blocked") {
|
|
637
|
+
unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
projectId,
|
|
643
|
+
boundTaskIds: boundTasks.map(({ id }) => id),
|
|
644
|
+
activeTaskIds: activeTasks.map(({ id }) => id),
|
|
645
|
+
unresolvedWorkItemRefs,
|
|
646
|
+
activeRunRefs,
|
|
647
|
+
unresolvedIntegrationRefs
|
|
648
|
+
};
|
|
600
649
|
}
|
|
601
650
|
// -- agent profiles ---------------------------------------------------------
|
|
602
651
|
saveAgentProfile(profile) {
|
|
@@ -2012,15 +2061,6 @@ export class SqliteTaskStore {
|
|
|
2012
2061
|
clearLeaderFailure(taskId) {
|
|
2013
2062
|
this.#clearProjection(taskId, "leader-failure");
|
|
2014
2063
|
}
|
|
2015
|
-
getOperatorNotification(taskId) {
|
|
2016
|
-
return this.#getProjection(taskId, "operator-notification");
|
|
2017
|
-
}
|
|
2018
|
-
saveOperatorNotification(notification) {
|
|
2019
|
-
this.#saveProjection(notification.taskId, "operator-notification", notification);
|
|
2020
|
-
}
|
|
2021
|
-
clearOperatorNotification(taskId) {
|
|
2022
|
-
this.#clearProjection(taskId, "operator-notification");
|
|
2023
|
-
}
|
|
2024
2064
|
// -- pending wakeups (leader-role work-mailbox projection, mirrors taskStore.ts) --
|
|
2025
2065
|
getPendingWakeup(taskId) {
|
|
2026
2066
|
return pendingWakeupProjection(this.getWorkMailbox({ kind: "role", taskId, roleName: "leader" }));
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -31,7 +31,6 @@ import { validateIntegrationQueueEntry } from "../integration/integrationQueueEn
|
|
|
31
31
|
import { CURRENT_DURABLE_JOB_SCHEMA_VERSION, validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
|
|
32
32
|
import { validateGlobalRole, validateTaskRole } from "../role/role.js";
|
|
33
33
|
import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailure.js";
|
|
34
|
-
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
|
|
35
34
|
import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
|
|
36
35
|
import { validateTask } from "../task/task.js";
|
|
37
36
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
@@ -46,7 +45,7 @@ import { CURRENT_AGGREGATE_SCHEMA_VERSION, requireCompatibleStorageSchema, requi
|
|
|
46
45
|
export const STORAGE_STATE_FILE = "state.json";
|
|
47
46
|
/** The root StorageState schema is the persisted aggregate document version. */
|
|
48
47
|
export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
|
|
49
|
-
export const CURRENT_CONFIG_SCHEMA_VERSION =
|
|
48
|
+
export const CURRENT_CONFIG_SCHEMA_VERSION = 3;
|
|
50
49
|
export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
|
|
51
50
|
export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
|
|
52
51
|
/**
|
|
@@ -58,7 +57,7 @@ export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
|
|
|
58
57
|
* below, but are not independent record-axis families.
|
|
59
58
|
*/
|
|
60
59
|
export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
|
|
61
|
-
export const CURRENT_PROJECT_SCHEMA_VERSION =
|
|
60
|
+
export const CURRENT_PROJECT_SCHEMA_VERSION = 5;
|
|
62
61
|
export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
|
|
63
62
|
export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
|
|
64
63
|
export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
|
|
@@ -113,7 +112,7 @@ export function executionLaneActiveRunKeyParts(key) {
|
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
/** The schema version of each persisted `state.json#/tasks/*` aggregate. */
|
|
116
|
-
export const CURRENT_STORED_TASK_SCHEMA_VERSION =
|
|
115
|
+
export const CURRENT_STORED_TASK_SCHEMA_VERSION = 18;
|
|
117
116
|
/**
|
|
118
117
|
* Persisted nested-record versions consumed by this store's strict parser.
|
|
119
118
|
* Keep these named at the storage boundary so the upgrade record-axis map can
|
|
@@ -279,6 +278,37 @@ export class FileTaskStore {
|
|
|
279
278
|
return this.#remove((state) => state.projects, id);
|
|
280
279
|
});
|
|
281
280
|
}
|
|
281
|
+
summarizeProjectReferences(projectId) {
|
|
282
|
+
const boundTasks = this.listTasks().filter((task) => task.projectBindings.some((binding) => binding.projectId === projectId));
|
|
283
|
+
const activeTasks = boundTasks.filter((task) => task.status === "active");
|
|
284
|
+
const unresolvedWorkItemRefs = [];
|
|
285
|
+
const activeRunRefs = [];
|
|
286
|
+
const unresolvedIntegrationRefs = [];
|
|
287
|
+
for (const task of activeTasks) {
|
|
288
|
+
for (const workItem of this.listWorkItems(task.id)) {
|
|
289
|
+
if (workItem.status !== "completed" && workItem.status !== "retired") {
|
|
290
|
+
unresolvedWorkItemRefs.push(`${task.id}/${workItem.id}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const run of this.listAgentRuns(task.id)) {
|
|
294
|
+
if (run.status === "active")
|
|
295
|
+
activeRunRefs.push(`${task.id}/${run.id}`);
|
|
296
|
+
}
|
|
297
|
+
for (const attempt of this.listIntegrationAttempts(task.id)) {
|
|
298
|
+
if (attempt.status === "running" || attempt.status === "blocked") {
|
|
299
|
+
unresolvedIntegrationRefs.push(`${task.id}/${attempt.id}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
projectId,
|
|
305
|
+
boundTaskIds: boundTasks.map(({ id }) => id),
|
|
306
|
+
activeTaskIds: activeTasks.map(({ id }) => id),
|
|
307
|
+
unresolvedWorkItemRefs,
|
|
308
|
+
activeRunRefs,
|
|
309
|
+
unresolvedIntegrationRefs
|
|
310
|
+
};
|
|
311
|
+
}
|
|
282
312
|
saveAgentProfile(profile) {
|
|
283
313
|
const stored = identified(profile, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, "id", profile.id, "Agent Profile");
|
|
284
314
|
validateAgentProfile(stored);
|
|
@@ -1699,22 +1729,16 @@ export class FileTaskStore {
|
|
|
1699
1729
|
this.saveWorkMailbox(consumePendingBatch(mailbox, "normal"));
|
|
1700
1730
|
}
|
|
1701
1731
|
getLeaderFailure(taskId) { return optional(this.#state().tasks[taskId]?.leaderFailure ?? undefined); }
|
|
1702
|
-
saveLeaderFailure(value) {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
clearOperatorNotification(taskId) { this.#clearSingleton(taskId, "operatorNotification"); }
|
|
1707
|
-
#saveSingleton(taskId, key, value, label) {
|
|
1708
|
-
const schemaVersion = key === "leaderFailure"
|
|
1709
|
-
? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
|
|
1710
|
-
: CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
|
|
1711
|
-
const stored = identified(value, schemaVersion, "taskId", taskId, label);
|
|
1712
|
-
this.#requireTaskForWrite(taskId);
|
|
1713
|
-
this.#mutate((state) => { state.tasks[taskId][key] = stored; });
|
|
1732
|
+
saveLeaderFailure(value) {
|
|
1733
|
+
const stored = identified(value, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", value.taskId, "Leader failure");
|
|
1734
|
+
this.#requireTaskForWrite(value.taskId);
|
|
1735
|
+
this.#mutate((state) => { state.tasks[value.taskId].leaderFailure = stored; });
|
|
1714
1736
|
}
|
|
1715
|
-
|
|
1716
|
-
this.#mutate((state) => {
|
|
1717
|
-
state.tasks[
|
|
1737
|
+
clearLeaderFailure(taskId) {
|
|
1738
|
+
this.#mutate((state) => {
|
|
1739
|
+
if (state.tasks[taskId] !== undefined)
|
|
1740
|
+
state.tasks[taskId].leaderFailure = null;
|
|
1741
|
+
});
|
|
1718
1742
|
}
|
|
1719
1743
|
#saveTaskRecord(taskId, key, value, label) {
|
|
1720
1744
|
const record = versioned(value, CURRENT_MESSAGE_SCHEMA_VERSION, label);
|
|
@@ -1983,8 +2007,7 @@ function emptyStoredTask(task) {
|
|
|
1983
2007
|
capabilityGrants: {},
|
|
1984
2008
|
releaseWorkflows: {},
|
|
1985
2009
|
publicationReferences: {},
|
|
1986
|
-
leaderFailure: null
|
|
1987
|
-
operatorNotification: null
|
|
2010
|
+
leaderFailure: null
|
|
1988
2011
|
};
|
|
1989
2012
|
}
|
|
1990
2013
|
function emptyTaskIdHighWaterMarks() {
|
|
@@ -2202,8 +2225,7 @@ function parseStoredTask(value, taskId) {
|
|
|
2202
2225
|
"capabilityGrants",
|
|
2203
2226
|
"releaseWorkflows",
|
|
2204
2227
|
"publicationReferences",
|
|
2205
|
-
"leaderFailure"
|
|
2206
|
-
"operatorNotification"
|
|
2228
|
+
"leaderFailure"
|
|
2207
2229
|
], `Task aggregate ${taskId}`);
|
|
2208
2230
|
parseMap(aggregate.changeSets, (record, key) => {
|
|
2209
2231
|
const changeSet = identifiedChangeSet(record, key);
|
|
@@ -2403,14 +2425,8 @@ function parseStoredTask(value, taskId) {
|
|
|
2403
2425
|
}
|
|
2404
2426
|
return reference;
|
|
2405
2427
|
}, "publicationReferences");
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
if (record !== null) {
|
|
2409
|
-
const schemaVersion = key === "leaderFailure"
|
|
2410
|
-
? CURRENT_LEADER_FAILURE_SCHEMA_VERSION
|
|
2411
|
-
: CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION;
|
|
2412
|
-
identified(record, schemaVersion, "taskId", taskId, label);
|
|
2413
|
-
}
|
|
2428
|
+
if (aggregate.leaderFailure !== null) {
|
|
2429
|
+
identified(aggregate.leaderFailure, CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "taskId", taskId, "Leader failure");
|
|
2414
2430
|
}
|
|
2415
2431
|
validateTaskIdHighWaterCoverage(aggregate, taskId);
|
|
2416
2432
|
return aggregate;
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
|
|
19
19
|
import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
|
|
20
20
|
import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
|
|
21
|
-
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
|
|
22
21
|
import { CURRENT_TASK_WAKE_SCHEMA_VERSION } from "../../scheduler/taskWake.js";
|
|
23
22
|
import { CURRENT_DURABLE_JOB_SCHEMA_VERSION } from "../../job/durableJob.js";
|
|
24
23
|
function descriptor(version, path) {
|
|
@@ -118,7 +117,9 @@ function getCurrentRecordDescriptors() {
|
|
|
118
117
|
releaseWorkflow: descriptor(CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, "state.json#/tasks/*/releaseWorkflows"),
|
|
119
118
|
publicationReference: descriptor(CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, "state.json#/tasks/*/publicationReferences"),
|
|
120
119
|
leaderFailure: descriptor(CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "state.json#/tasks/*/leaderFailure"),
|
|
121
|
-
|
|
120
|
+
// Historical family retained in the version graph so StoredTask v17
|
|
121
|
+
// Homes can migrate; current v18 aggregates never store this field.
|
|
122
|
+
operatorNotification: descriptor(1, "state.json#/tasks/*/operatorNotification"),
|
|
122
123
|
workMailbox: descriptor(CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "state.json#/mailboxes")
|
|
123
124
|
});
|
|
124
125
|
}
|
|
@@ -149,6 +149,12 @@ export function createSqliteMigrationTarget(options) {
|
|
|
149
149
|
}
|
|
150
150
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
151
151
|
let backupPath;
|
|
152
|
+
const schemaPath = join(home, STORAGE_SCHEMA_FILE);
|
|
153
|
+
const sourceSchemaText = readFileSync(schemaPath, "utf8");
|
|
154
|
+
const receiptPath = migrationReceiptPath(home);
|
|
155
|
+
const sourceReceiptText = existsSync(receiptPath)
|
|
156
|
+
? readFileSync(receiptPath, "utf8")
|
|
157
|
+
: null;
|
|
152
158
|
// Phase 1: back up any existing yui.db, then promote the sidecar.
|
|
153
159
|
try {
|
|
154
160
|
if (existsSync(committedDbPath)) {
|
|
@@ -185,7 +191,7 @@ export function createSqliteMigrationTarget(options) {
|
|
|
185
191
|
// Phase 2: advance schema.json to layout 7 in the same critical section.
|
|
186
192
|
try {
|
|
187
193
|
if (stagedSchemaManifest !== null) {
|
|
188
|
-
writeTextFileAtomically(
|
|
194
|
+
writeTextFileAtomically(schemaPath, `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
|
|
189
195
|
}
|
|
190
196
|
// Certify the dual-copy state (Issue 01): the 6→7 migration retains
|
|
191
197
|
// state.json read-only, so the Home legitimately holds both copies.
|
|
@@ -204,9 +210,22 @@ export function createSqliteMigrationTarget(options) {
|
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
catch (error) {
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
213
|
+
// A sidecar or receipt writer can fail after an atomic rename. Restore
|
|
214
|
+
// the exact source metadata as well as the database before claiming the
|
|
215
|
+
// layout switch did not commit.
|
|
216
|
+
let rollbackError;
|
|
217
|
+
try {
|
|
218
|
+
writeTextFileAtomically(schemaPath, sourceSchemaText);
|
|
219
|
+
if (sourceReceiptText === null) {
|
|
220
|
+
rmSync(receiptPath, { force: true });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
writeTextFileAtomically(receiptPath, sourceReceiptText);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (restoreError) {
|
|
227
|
+
rollbackError = restoreError;
|
|
228
|
+
}
|
|
210
229
|
try {
|
|
211
230
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
212
231
|
removeSqliteFileSet(committedDbPath);
|
|
@@ -216,14 +235,17 @@ export function createSqliteMigrationTarget(options) {
|
|
|
216
235
|
removeSqliteFileSet(committedDbPath);
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
|
-
catch {
|
|
238
|
+
catch (restoreError) {
|
|
239
|
+
rollbackError ??= restoreError;
|
|
240
|
+
}
|
|
241
|
+
if (rollbackError !== undefined) {
|
|
220
242
|
throw new AmbiguousSwitchError({
|
|
221
243
|
homePath: home,
|
|
222
244
|
backupPath: backupPath ?? committedDbPath,
|
|
223
245
|
stagingPath: stagedDbPath,
|
|
224
|
-
detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
|
|
225
|
-
`and the
|
|
226
|
-
`
|
|
246
|
+
detail: `SQLite database was promoted but schema.json or its receipt could not be durably advanced (${messageOf(error)}), ` +
|
|
247
|
+
`and the database/metadata rollback also failed (${messageOf(rollbackError)}). ` +
|
|
248
|
+
`Inspect ${committedDbPath}, ${schemaPath}, ${receiptPath}, and the backup before recovery.`
|
|
227
249
|
});
|
|
228
250
|
}
|
|
229
251
|
throw error;
|
|
@@ -38,7 +38,7 @@ import { AmbiguousSwitchError } from "../migration/index.js";
|
|
|
38
38
|
import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
|
|
39
39
|
import { writeSwitchProgress } from "./switchProgress.js";
|
|
40
40
|
import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
|
|
41
|
-
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState,
|
|
41
|
+
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readMigrationSourceStateFromSqlite } from "./sqliteStateMigration.js";
|
|
42
42
|
/** Build the SQLite-backed record-migration target. */
|
|
43
43
|
export function createSqliteRecordMigrationTarget(options) {
|
|
44
44
|
const home = options.home;
|
|
@@ -70,7 +70,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
70
70
|
readSource() {
|
|
71
71
|
const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
|
|
72
72
|
const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
|
|
73
|
-
const state =
|
|
73
|
+
const state = readMigrationSourceStateFromSqlite(home);
|
|
74
74
|
return Object.freeze({ schemaManifest, state });
|
|
75
75
|
},
|
|
76
76
|
writeFreshOutput(snapshot) {
|
|
@@ -129,6 +129,8 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
129
129
|
}
|
|
130
130
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
131
131
|
let backupPath;
|
|
132
|
+
const schemaPath = join(home, STORAGE_SCHEMA_FILE);
|
|
133
|
+
const sourceSchemaText = readFileSync(schemaPath, "utf8");
|
|
132
134
|
// Phase 1: back up the existing yui.db, then promote the sidecar.
|
|
133
135
|
try {
|
|
134
136
|
if (existsSync(committedDbPath)) {
|
|
@@ -167,13 +169,20 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
167
169
|
// critical section. The layout version is unchanged.
|
|
168
170
|
try {
|
|
169
171
|
if (stagedSchemaManifest !== null) {
|
|
170
|
-
writeTextFileAtomically(
|
|
172
|
+
writeTextFileAtomically(schemaPath, `${JSON.stringify(stagedSchemaManifest, null, 2)}\n`);
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
catch (error) {
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
176
|
+
// A durable writer can report an fsync failure after its atomic rename,
|
|
177
|
+
// so "write threw" does not prove schema.json stayed old. Restore both
|
|
178
|
+
// the exact source sidecar and database before claiming no switch.
|
|
179
|
+
let rollbackError;
|
|
180
|
+
try {
|
|
181
|
+
writeTextFileAtomically(schemaPath, sourceSchemaText);
|
|
182
|
+
}
|
|
183
|
+
catch (restoreError) {
|
|
184
|
+
rollbackError = restoreError;
|
|
185
|
+
}
|
|
177
186
|
try {
|
|
178
187
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
179
188
|
removeSqliteFileSet(committedDbPath);
|
|
@@ -183,15 +192,18 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
183
192
|
removeSqliteFileSet(committedDbPath);
|
|
184
193
|
}
|
|
185
194
|
}
|
|
186
|
-
catch {
|
|
195
|
+
catch (restoreError) {
|
|
196
|
+
rollbackError ??= restoreError;
|
|
197
|
+
}
|
|
198
|
+
if (rollbackError !== undefined) {
|
|
187
199
|
writeInterruptedMarker(home, backupPath ?? committedDbPath, stagedDbPath, now);
|
|
188
200
|
throw new AmbiguousSwitchError({
|
|
189
201
|
homePath: home,
|
|
190
202
|
backupPath: backupPath ?? committedDbPath,
|
|
191
203
|
stagingPath: stagedDbPath,
|
|
192
|
-
detail: `SQLite database was promoted but schema.json could not be advanced (${messageOf(error)}), ` +
|
|
193
|
-
`and the
|
|
194
|
-
`
|
|
204
|
+
detail: `SQLite database was promoted but schema.json could not be durably advanced (${messageOf(error)}), ` +
|
|
205
|
+
`and the database/schema rollback also failed (${messageOf(rollbackError)}). ` +
|
|
206
|
+
`Inspect ${committedDbPath}, ${schemaPath}, and the backup before recovery.`
|
|
195
207
|
});
|
|
196
208
|
}
|
|
197
209
|
throw error;
|
|
@@ -214,7 +226,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
214
226
|
function readSourceFresh() {
|
|
215
227
|
const manifestRaw = readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8");
|
|
216
228
|
const schemaManifest = parseJsonObject(manifestRaw, STORAGE_SCHEMA_FILE);
|
|
217
|
-
const state =
|
|
229
|
+
const state = readMigrationSourceStateFromSqlite(home);
|
|
218
230
|
return Object.freeze({ schemaManifest, state });
|
|
219
231
|
}
|
|
220
232
|
/**
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* dropped, duplicated, or mutated record.
|
|
23
23
|
*/
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
|
+
import { existsSync, rmSync } from "node:fs";
|
|
25
26
|
import { join } from "node:path";
|
|
26
27
|
import Database from "better-sqlite3";
|
|
27
28
|
import { mailboxTargetKey } from "../../coordination/workMailbox.js";
|
|
@@ -33,6 +34,50 @@ import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
|
|
|
33
34
|
export const STAGED_DATABASE_FILENAME = "yui.db.staged";
|
|
34
35
|
/** The committed database filename. */
|
|
35
36
|
export const COMMITTED_DATABASE_FILENAME = "yui.db";
|
|
37
|
+
/** Disposable, schema-current snapshot used to reconstruct an older source. */
|
|
38
|
+
export const MIGRATION_SOURCE_DATABASE_FILENAME = "yui.db.migration-source";
|
|
39
|
+
/**
|
|
40
|
+
* Reconstruct a migration snapshot without advancing the authoritative DB.
|
|
41
|
+
*
|
|
42
|
+
* `VACUUM INTO` takes a consistent SQLite snapshot, including committed WAL
|
|
43
|
+
* state, without writing the source. Pending physical migrations are then
|
|
44
|
+
* applied only to that disposable copy before the current reader enumerates
|
|
45
|
+
* record families that may not exist in the older source schema.
|
|
46
|
+
*/
|
|
47
|
+
export function readMigrationSourceStateFromSqlite(home) {
|
|
48
|
+
const sourcePath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
49
|
+
const snapshotPath = join(home, MIGRATION_SOURCE_DATABASE_FILENAME);
|
|
50
|
+
if (existsSync(snapshotPath)) {
|
|
51
|
+
throw new Error(`Refusing to overwrite an existing SQLite migration-source snapshot: ${snapshotPath}. `
|
|
52
|
+
+ "Discard it and retry.");
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const source = new Database(sourcePath, { readonly: true });
|
|
56
|
+
try {
|
|
57
|
+
source.prepare("VACUUM INTO ?").run(snapshotPath);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
source.close();
|
|
61
|
+
}
|
|
62
|
+
// Physical migration 14 rewrites the WorkMailbox family while adding its
|
|
63
|
+
// current columns. Preserve the source rows before applying it so the
|
|
64
|
+
// logical record migration still sees and transforms v1 exactly once.
|
|
65
|
+
const sourceMailboxes = readWorkMailboxesFromSqlite(snapshotPath);
|
|
66
|
+
const stagedStore = new SqliteTaskStore(home, {
|
|
67
|
+
databaseFilename: MIGRATION_SOURCE_DATABASE_FILENAME,
|
|
68
|
+
migration: true
|
|
69
|
+
});
|
|
70
|
+
stagedStore.close();
|
|
71
|
+
const state = readStateFromSqlite(home, MIGRATION_SOURCE_DATABASE_FILENAME);
|
|
72
|
+
state.mailboxes = sourceMailboxes;
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rmSync(snapshotPath, { force: true });
|
|
77
|
+
rmSync(`${snapshotPath}-wal`, { force: true });
|
|
78
|
+
rmSync(`${snapshotPath}-shm`, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
36
81
|
/**
|
|
37
82
|
* Preserve SQLite-owned durable state that intentionally sits outside the
|
|
38
83
|
* state.json-shaped Task aggregate snapshot. Volatile coordination locks and
|
|
@@ -351,9 +396,6 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
351
396
|
if (stored.leaderFailure !== null) {
|
|
352
397
|
store.saveLeaderFailure(stored.leaderFailure);
|
|
353
398
|
}
|
|
354
|
-
if (stored.operatorNotification !== null) {
|
|
355
|
-
store.saveOperatorNotification(stored.operatorNotification);
|
|
356
|
-
}
|
|
357
399
|
// Per-task ID high-water marks.
|
|
358
400
|
for (const [kind, highWater] of Object.entries(stored.idHighWaterMarks)) {
|
|
359
401
|
if (typeof highWater === "number" && Number.isFinite(highWater)) {
|
|
@@ -595,6 +637,20 @@ function readWorkMailboxRows(db) {
|
|
|
595
637
|
const hasInputDelivery = db.prepare("PRAGMA table_info(mailboxes)").all().some(({ name }) => name === "input_delivery");
|
|
596
638
|
return db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending${hasInputDelivery ? ", input_delivery" : ""} FROM mailboxes ORDER BY target_key`).all();
|
|
597
639
|
}
|
|
640
|
+
function readWorkMailboxesFromSqlite(dbPath) {
|
|
641
|
+
const db = new Database(dbPath, { readonly: true });
|
|
642
|
+
try {
|
|
643
|
+
const mailboxes = {};
|
|
644
|
+
for (const row of readWorkMailboxRows(db)) {
|
|
645
|
+
const mailbox = rowToMailbox(row);
|
|
646
|
+
mailboxes[mailboxTargetKey(mailbox.target)] = mailbox;
|
|
647
|
+
}
|
|
648
|
+
return mailboxes;
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
db.close();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
598
654
|
/**
|
|
599
655
|
* Compute per-family checksums from the SQLite database. The database is
|
|
600
656
|
* opened read-only; this is the independent verification read.
|
|
@@ -692,8 +748,8 @@ export function verifySqliteChecksums(state, home, databaseFilename) {
|
|
|
692
748
|
*
|
|
693
749
|
* The database is opened read-only; this function never writes.
|
|
694
750
|
*/
|
|
695
|
-
export function readStateFromSqlite(home) {
|
|
696
|
-
const dbPath = join(home,
|
|
751
|
+
export function readStateFromSqlite(home, databaseFilename = COMMITTED_DATABASE_FILENAME) {
|
|
752
|
+
const dbPath = join(home, databaseFilename);
|
|
697
753
|
const db = new Database(dbPath, { readonly: true });
|
|
698
754
|
try {
|
|
699
755
|
const manifest = readStorageSchemaManifest(home);
|
|
@@ -737,7 +793,7 @@ export function readStateFromSqlite(home) {
|
|
|
737
793
|
const tasks = state.tasks;
|
|
738
794
|
const taskRows = db.prepare("SELECT task_id, payload, brief FROM task_records").all();
|
|
739
795
|
for (const row of taskRows) {
|
|
740
|
-
|
|
796
|
+
const aggregate = {
|
|
741
797
|
schemaVersion: storedTaskVersion,
|
|
742
798
|
task: JSON.parse(row.payload),
|
|
743
799
|
brief: row.brief === null ? null : JSON.parse(row.brief),
|
|
@@ -761,12 +817,14 @@ export function readStateFromSqlite(home) {
|
|
|
761
817
|
events: {},
|
|
762
818
|
wakes: {},
|
|
763
819
|
leaderFailure: null,
|
|
764
|
-
operatorNotification: null,
|
|
765
820
|
idHighWaterMarks: {},
|
|
766
821
|
capabilityGrants: {},
|
|
767
822
|
releaseWorkflows: {},
|
|
768
823
|
publicationReferences: {}
|
|
769
824
|
};
|
|
825
|
+
if (storedTaskVersion <= 17)
|
|
826
|
+
aggregate.operatorNotification = null;
|
|
827
|
+
tasks[row.task_id] = aggregate;
|
|
770
828
|
}
|
|
771
829
|
// Per-task record families, keyed by their state.json map keys.
|
|
772
830
|
loadTaskPayloadMap(db, "task_roles", tasks, "roles", (record) => record.name);
|
|
@@ -808,7 +866,7 @@ export function readStateFromSqlite(home) {
|
|
|
808
866
|
if (row.kind === "leader-failure") {
|
|
809
867
|
stored.leaderFailure = JSON.parse(row.payload);
|
|
810
868
|
}
|
|
811
|
-
else {
|
|
869
|
+
else if (storedTaskVersion <= 17) {
|
|
812
870
|
stored.operatorNotification = JSON.parse(row.payload);
|
|
813
871
|
}
|
|
814
872
|
}
|
|
@@ -4,6 +4,7 @@ import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
|
4
4
|
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
5
5
|
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
6
6
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
7
|
+
import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
7
8
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
8
9
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
9
10
|
"queued",
|
|
@@ -22,7 +23,8 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
22
23
|
const advisories = [];
|
|
23
24
|
const { task } = facts;
|
|
24
25
|
const findingsGate = options.findingsGate ?? true;
|
|
25
|
-
const
|
|
26
|
+
const taskFinalReviewContract = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents)?.effective;
|
|
27
|
+
const taskFinalReviewRequired = taskFinalReviewContract !== undefined;
|
|
26
28
|
// A pending/running Task-final Review must be resumed or blocked first.
|
|
27
29
|
for (const round of facts.reviewRounds) {
|
|
28
30
|
if ((round.scope ?? "work-item") !== "task")
|
|
@@ -43,11 +45,13 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
43
45
|
// escalations remain audit evidence, but a later completed full Review (or
|
|
44
46
|
// accepted delta) supersedes them instead of blocking completion forever.
|
|
45
47
|
const latestCompletedTaskReview = facts.reviewRounds
|
|
46
|
-
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
49
|
+
&& (taskFinalReviewContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalReviewContract))
|
|
50
|
+
&& isSemanticReviewRound(round, {
|
|
51
|
+
listAgentRuns: () => facts.agentRuns,
|
|
52
|
+
listReviewFindings: () => facts.reviewFindings,
|
|
53
|
+
listEvents: () => facts.events
|
|
54
|
+
})))
|
|
51
55
|
.slice()
|
|
52
56
|
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
53
57
|
|| left.id.localeCompare(right.id, undefined, { numeric: true })))
|