@zq-silk/yui 0.11.3 → 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 +36 -13
- package/dist/cli/commandCatalog.js +2 -4
- 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/taskCommands.js +154 -171
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +55 -11
- package/dist/commands/taskInputCommands.js +8 -5
- 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/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 +379 -0
- package/dist/storage/sqliteSchema.js +18 -8
- package/dist/storage/sqliteStore.js +24 -20
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- 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
|
@@ -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 })))
|
package/dist/task/nextAction.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { changeSetDeliverySettled, governingChangeSets } from "../integration/deliveryObligation.js";
|
|
3
|
-
import {
|
|
4
|
-
import { classifyReviewRoundOutcome
|
|
4
|
+
import { isAcceptedTaskReviewBaselineFromEvidence } from "../review/reviewAcceptance.js";
|
|
5
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
5
6
|
import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
|
|
6
7
|
import { candidateConvergenceDisagreement, candidateConvergenceEvidenceSufficient, candidateConvergenceStageResultsValid } from "../execution/candidateConvergence.js";
|
|
7
8
|
import { executionStageSpendClosed, routeExecutionStage } from "../execution/resourceBroker.js";
|
|
9
|
+
import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
8
10
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
9
11
|
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
10
12
|
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
@@ -232,13 +234,15 @@ export function projectNextAction(facts) {
|
|
|
232
234
|
judgmentRequired: `Leader must judge Candidate ${candidateReady.id}/${candidate?.id ?? "unknown"} against the Task objective, acceptance criteria, and delivery risk.`
|
|
233
235
|
});
|
|
234
236
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
// AgentRun purpose owns routing. Review Runs remain attached to their exact
|
|
238
|
+
// ReviewRound branches below instead of being mistaken for Worker delivery.
|
|
239
|
+
const activeDelegatedExecutions = facts.activeRuns.filter((run) => (run.purpose === "execution" && run.roleName !== "leader"));
|
|
240
|
+
if (activeDelegatedExecutions.length > 0) {
|
|
237
241
|
return buildAction(facts, {
|
|
238
242
|
kind: "wait-for-owned-execution",
|
|
239
|
-
reason: `${
|
|
240
|
-
refs:
|
|
241
|
-
preconditions:
|
|
243
|
+
reason: `${activeDelegatedExecutions.length} delegated execution Run(s) are active; wait for their delivery.`,
|
|
244
|
+
refs: activeDelegatedExecutions.map((run) => ref("agent-run", run.id)),
|
|
245
|
+
preconditions: activeDelegatedExecutions.map((run) => ({ fact: `Execution Run ${run.id} is active`, satisfied: true, ref: ref("agent-run", run.id) }))
|
|
242
246
|
});
|
|
243
247
|
}
|
|
244
248
|
const failedWork = facts.workItems.find((item) => item.status === "failed");
|
|
@@ -386,7 +390,8 @@ export function projectNextAction(facts) {
|
|
|
386
390
|
});
|
|
387
391
|
}
|
|
388
392
|
const finalReviewRequired = taskFinalReviewRequired(facts);
|
|
389
|
-
const
|
|
393
|
+
const finalReviewContract = taskFinalReviewContract(facts);
|
|
394
|
+
const failedFinal = latestTaskFinalReview(facts.reviewRounds, finalReviewContract);
|
|
390
395
|
const failedFinalOutcome = failedFinal === undefined
|
|
391
396
|
? null
|
|
392
397
|
: classifyReviewRoundOutcome(failedFinal, nextActionReviewOutcomeEvidence(facts));
|
|
@@ -414,11 +419,38 @@ export function projectNextAction(facts) {
|
|
|
414
419
|
]
|
|
415
420
|
});
|
|
416
421
|
}
|
|
422
|
+
if (finalReviewRequired
|
|
423
|
+
&& failedFinal !== undefined
|
|
424
|
+
&& failedFinalOutcome?.kind === "semantic"
|
|
425
|
+
&& failedFinal.deltaRecheck?.disposition === "requires-full-review") {
|
|
426
|
+
return buildAction(facts, {
|
|
427
|
+
kind: "request-final-review",
|
|
428
|
+
reason: `Delta Recheck ${failedFinal.id} could not establish equivalence; Yui recorded the result and left the next Review action to the Leader.`,
|
|
429
|
+
refs: [ref("review-round", failedFinal.id)],
|
|
430
|
+
preconditions: [
|
|
431
|
+
{ fact: "Current frozen head has accepting final Review evidence", satisfied: false }
|
|
432
|
+
],
|
|
433
|
+
alternatives: [
|
|
434
|
+
{
|
|
435
|
+
kind: "request-full-review",
|
|
436
|
+
reason: "Request a full Review when independent evidence is still required.",
|
|
437
|
+
recommendedCommand: `yui task review request ${task.id} --role ${failedFinal.reviewerRoleName}`,
|
|
438
|
+
refs: [ref("review-round", failedFinal.id)]
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
kind: "continue-leader-work",
|
|
442
|
+
reason: "Inspect directly, change the candidate, or choose another Reviewer as Task risk requires.",
|
|
443
|
+
refs: [ref("review-round", failedFinal.id)]
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
judgmentRequired: "Leader must choose full Review, another Reviewer, direct inspection, or more development; Core will not auto-escalate."
|
|
447
|
+
});
|
|
448
|
+
}
|
|
417
449
|
if (finalReviewRequired
|
|
418
450
|
&& failedFinal !== undefined
|
|
419
451
|
&& failedFinalOutcome?.kind === "semantic"
|
|
420
452
|
&& ((failedFinal.checks ?? []).some(({ outcome }) => outcome === "failed")
|
|
421
|
-
||
|
|
453
|
+
|| failedFinal.deltaRecheck?.disposition === "finding")) {
|
|
422
454
|
return buildAction(facts, {
|
|
423
455
|
kind: "route-review-findings",
|
|
424
456
|
reason: `Task-final Review ${failedFinal.id} delivered semantic negative evidence; route its open findings into a repair wave on one frozen head.`,
|
|
@@ -429,7 +461,7 @@ export function projectNextAction(facts) {
|
|
|
429
461
|
recommendedCommand: `yui task review finding repair-wave ${task.id} --create`
|
|
430
462
|
});
|
|
431
463
|
}
|
|
432
|
-
const activeFinal = latestTaskFinalReview(facts.reviewRounds);
|
|
464
|
+
const activeFinal = latestTaskFinalReview(facts.reviewRounds, finalReviewContract);
|
|
433
465
|
if (activeFinal !== undefined
|
|
434
466
|
&& (activeFinal.status === "pending" || activeFinal.status === "running")) {
|
|
435
467
|
const reviewRef = ref("review-round", activeFinal.id);
|
|
@@ -475,12 +507,26 @@ export function projectNextAction(facts) {
|
|
|
475
507
|
}
|
|
476
508
|
return buildAction(facts, {
|
|
477
509
|
kind: "wait-for-owned-execution",
|
|
478
|
-
reason: `Reviewer Run ${reviewRun.id} is executing Task-final Review ${activeFinal.id}.`,
|
|
510
|
+
reason: `Reviewer Run ${reviewRun.id} is executing frozen Task-final Review ${activeFinal.id}; this Review does not globally pause Leader decisions on newer facts.`,
|
|
479
511
|
refs: [reviewRef, ref("agent-run", reviewRun.id)],
|
|
480
512
|
preconditions: [
|
|
481
513
|
{ fact: "Task-final ReviewRound is running", satisfied: true, ref: reviewRef },
|
|
482
514
|
{ fact: "Reviewer Run is active", satisfied: true, ref: ref("agent-run", reviewRun.id) }
|
|
483
|
-
]
|
|
515
|
+
],
|
|
516
|
+
alternatives: [
|
|
517
|
+
{
|
|
518
|
+
kind: "continue-leader-work",
|
|
519
|
+
reason: "Process new user input or advance a later candidate while preserving this frozen Review.",
|
|
520
|
+
refs: [reviewRef]
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
kind: "request-another-reviewer",
|
|
524
|
+
reason: "Use another available Reviewer slot when an independent view adds value.",
|
|
525
|
+
recommendedCommand: `yui task review request ${task.id} --role <other-reviewer-role>`,
|
|
526
|
+
refs: [reviewRef]
|
|
527
|
+
}
|
|
528
|
+
],
|
|
529
|
+
judgmentRequired: "Leader decides whether the current facts justify waiting, continuing development, direct review, or another Reviewer."
|
|
484
530
|
});
|
|
485
531
|
}
|
|
486
532
|
if (activeFinal.reviewerRunId !== undefined) {
|
|
@@ -530,11 +576,16 @@ export function projectNextAction(facts) {
|
|
|
530
576
|
}
|
|
531
577
|
const finalReviewOptional = !finalReviewRequired
|
|
532
578
|
&& !hasValidFinalReview(facts);
|
|
533
|
-
const
|
|
579
|
+
const unresolvedDelta = failedFinal?.deltaRecheck?.disposition === "requires-full-review";
|
|
580
|
+
const optionalReviewer = facts.reviewConfig?.roleName
|
|
581
|
+
?? (unresolvedDelta ? failedFinal?.reviewerRoleName : undefined);
|
|
582
|
+
const finalReviewAlternative = finalReviewOptional && optionalReviewer !== undefined
|
|
534
583
|
? [{
|
|
535
584
|
kind: "request-final-review",
|
|
536
|
-
reason:
|
|
537
|
-
|
|
585
|
+
reason: unresolvedDelta
|
|
586
|
+
? `Delta Recheck ${failedFinal.id} returned requires-full-review; request full independent evidence when the Leader judges it necessary.`
|
|
587
|
+
: "Request an independent Task-final Review when the Leader wants extra assurance before completion.",
|
|
588
|
+
recommendedCommand: `yui task review request ${task.id} --role ${optionalReviewer}`,
|
|
538
589
|
refs: [ref("task", task.id)]
|
|
539
590
|
}]
|
|
540
591
|
: [];
|
|
@@ -564,7 +615,9 @@ export function projectNextAction(facts) {
|
|
|
564
615
|
...(!finalReviewOptional
|
|
565
616
|
? {}
|
|
566
617
|
: {
|
|
567
|
-
judgmentRequired:
|
|
618
|
+
judgmentRequired: unresolvedDelta
|
|
619
|
+
? "Leader must route the non-accepting Delta result: full Review, another Reviewer, direct inspection, more development, or completion when policy permits."
|
|
620
|
+
: "Leader must decide whether the frozen Task result is safe to complete or needs one optional Task-final Review."
|
|
568
621
|
}),
|
|
569
622
|
recommendedCommand: `yui task complete ${task.id} --summary-file -`
|
|
570
623
|
});
|
|
@@ -1013,10 +1066,11 @@ function latestFailedReviewFor(rounds, workItemId) {
|
|
|
1013
1066
|
.reverse()
|
|
1014
1067
|
.find((round) => round.workItemId === workItemId && round.status === "failed");
|
|
1015
1068
|
}
|
|
1016
|
-
function latestTaskFinalReview(rounds) {
|
|
1069
|
+
function latestTaskFinalReview(rounds, contract) {
|
|
1017
1070
|
return [...rounds]
|
|
1018
1071
|
.reverse()
|
|
1019
|
-
.find((round) => (round.scope ?? "work-item") === "task"
|
|
1072
|
+
.find((round) => ((round.scope ?? "work-item") === "task"
|
|
1073
|
+
&& (contract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, contract))));
|
|
1020
1074
|
}
|
|
1021
1075
|
function needsChangeSetCapture(facts, item) {
|
|
1022
1076
|
if (item.status !== "completed")
|
|
@@ -1036,13 +1090,17 @@ function needsChangeSetCapture(facts, item) {
|
|
|
1036
1090
|
return candidate.workspace !== undefined || candidate.gitSnapshot !== undefined;
|
|
1037
1091
|
}
|
|
1038
1092
|
function hasValidFinalReview(facts) {
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1093
|
+
const contract = taskFinalReviewContract(facts);
|
|
1094
|
+
const final = [...facts.reviewRounds]
|
|
1095
|
+
.reverse()
|
|
1096
|
+
.find((round) => ((round.scope ?? "work-item") === "task"
|
|
1097
|
+
&& (contract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, contract))));
|
|
1098
|
+
if (final === undefined || !isAcceptedTaskReviewBaselineFromEvidence(final, nextActionReviewOutcomeEvidence(facts)))
|
|
1045
1099
|
return false;
|
|
1100
|
+
if (facts.currentTaskReviewCandidate !== undefined) {
|
|
1101
|
+
return facts.currentTaskReviewCandidate !== null
|
|
1102
|
+
&& isDeepStrictEqual(final.taskCandidate, facts.currentTaskReviewCandidate);
|
|
1103
|
+
}
|
|
1046
1104
|
const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
|
|
1047
1105
|
if (reviewedCommits.size === 0)
|
|
1048
1106
|
return false;
|
|
@@ -246,7 +246,7 @@ export class SqliteTelemetryStore {
|
|
|
246
246
|
db.pragma("foreign_keys = ON");
|
|
247
247
|
db.pragma("busy_timeout = 5000");
|
|
248
248
|
db.pragma("wal_autocheckpoint = 1000");
|
|
249
|
-
migrateSqliteSchema(db);
|
|
249
|
+
migrateSqliteSchema(db, { mode: "validate" });
|
|
250
250
|
this.#db = db;
|
|
251
251
|
return db;
|
|
252
252
|
}
|
|
@@ -114,9 +114,9 @@ export function executionBand(projection, t, locale) {
|
|
|
114
114
|
head.append(pill(t, "exec.status", projection.status));
|
|
115
115
|
head.append(node("span", "exec-band-owner",
|
|
116
116
|
t("exec.owner." + projection.owner) + " · " + t("exec.action." + projection.action)));
|
|
117
|
-
if (projection.
|
|
117
|
+
if (projection.activeRuns && projection.activeRuns.length > 0) {
|
|
118
118
|
head.append(node("span", "exec-band-executors",
|
|
119
|
-
projection.
|
|
119
|
+
projection.activeRuns.length + " " + t("exec.executors")));
|
|
120
120
|
}
|
|
121
121
|
if (projection.monitoring === "stopped") {
|
|
122
122
|
head.append(node("span", "exec-band-stopped", t("exec.monitoring.stopped")));
|
|
@@ -242,7 +242,6 @@ export function observabilityMetricCard(observability, t) {
|
|
|
242
242
|
card.append(metricTile(t("detail.wallClock"), cost.wallClockSeconds + "s"));
|
|
243
243
|
card.append(metricTile(t("detail.ready"), (observability.dag?.readyIds || []).length, { hot: true }));
|
|
244
244
|
card.append(metricTile(t("detail.contextSnapshots"), context.snapshotCount));
|
|
245
|
-
card.append(metricTile(t("detail.contextPeak"), context.observedInputPeakTokens));
|
|
246
245
|
const contextMeta = node("div", "record-meta observability-context-meta");
|
|
247
246
|
contextMeta.append(node("span", "", t("detail.contextBytes") + " · "
|
|
248
247
|
+ (context.totalBytes === null ? t("detail.partial") : context.totalBytes + " B")));
|
|
@@ -775,6 +774,17 @@ export function roleCard(role, task, t, locale, actions) {
|
|
|
775
774
|
card.append(eff);
|
|
776
775
|
}
|
|
777
776
|
|
|
777
|
+
if (role.sessionTokens) {
|
|
778
|
+
const tokenMeta = node("div", "record-meta");
|
|
779
|
+
const cumulative = role.sessionTokens.cumulativeTotal || {};
|
|
780
|
+
const maximum = role.sessionTokens.maximumRequestInput || {};
|
|
781
|
+
tokenMeta.append(node("span", "", t("detail.sessionTotalTokens") + " · "
|
|
782
|
+
+ (cumulative.status === "observed" ? cumulative.totalTokens : t("detail.unobserved"))));
|
|
783
|
+
tokenMeta.append(node("span", "", t("detail.maximumRequestInputTokens") + " · "
|
|
784
|
+
+ (maximum.status === "observed" ? maximum.inputTokens : t("detail.unobserved"))));
|
|
785
|
+
card.append(tokenMeta);
|
|
786
|
+
}
|
|
787
|
+
|
|
778
788
|
if (role.description) card.append(richText(null, role.description, t, { muted: true }));
|
|
779
789
|
|
|
780
790
|
const cols = node("div", "record-cols");
|
|
@@ -61,17 +61,19 @@ const messages = {
|
|
|
61
61
|
"detail.stageAttempt": "Attempt",
|
|
62
62
|
"detail.cost": "Cost",
|
|
63
63
|
"detail.tokens": "Tokens",
|
|
64
|
+
"detail.sessionTotalTokens": "Session total tokens",
|
|
65
|
+
"detail.maximumRequestInputTokens": "Maximum request input",
|
|
64
66
|
"detail.toolCalls": "Tool calls",
|
|
65
67
|
"detail.wallClock": "Wall clock",
|
|
66
68
|
"detail.ready": "Ready",
|
|
67
69
|
"detail.quorum": "Quorum",
|
|
68
70
|
"detail.contextSnapshots": "Context snapshots",
|
|
69
|
-
"detail.contextPeak": "Context peak",
|
|
70
71
|
"detail.contextBytes": "Context bytes",
|
|
71
72
|
"detail.compression": "Compression",
|
|
72
73
|
"detail.marginalValue": "Marginal value",
|
|
73
74
|
"detail.partial": "partial",
|
|
74
75
|
"detail.unavailable": "unavailable",
|
|
76
|
+
"detail.unobserved": "unobserved",
|
|
75
77
|
"detail.openFindings": "Open findings",
|
|
76
78
|
"detail.desired": "Desired launch",
|
|
77
79
|
"detail.desiredAgent": "Desired Agent",
|
|
@@ -397,17 +399,19 @@ const messages = {
|
|
|
397
399
|
"detail.stageAttempt": "阶段尝试",
|
|
398
400
|
"detail.cost": "成本",
|
|
399
401
|
"detail.tokens": "Token",
|
|
402
|
+
"detail.sessionTotalTokens": "Session 累计 Token",
|
|
403
|
+
"detail.maximumRequestInputTokens": "单次请求最大输入",
|
|
400
404
|
"detail.toolCalls": "工具调用",
|
|
401
405
|
"detail.wallClock": "墙钟时间",
|
|
402
406
|
"detail.ready": "可就绪",
|
|
403
407
|
"detail.quorum": "法定数量",
|
|
404
408
|
"detail.contextSnapshots": "上下文快照",
|
|
405
|
-
"detail.contextPeak": "上下文峰值",
|
|
406
409
|
"detail.contextBytes": "上下文字节",
|
|
407
410
|
"detail.compression": "压缩",
|
|
408
411
|
"detail.marginalValue": "边际价值",
|
|
409
412
|
"detail.partial": "部分可见",
|
|
410
413
|
"detail.unavailable": "不可用",
|
|
414
|
+
"detail.unobserved": "未观测",
|
|
411
415
|
"detail.openFindings": "未解决发现",
|
|
412
416
|
"detail.desired": "期望启动配置",
|
|
413
417
|
"detail.desiredAgent": "期望 Agent",
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -7,6 +7,7 @@ import { classifyRuntimeHealth, projectRuntimeTaskEvents } from "../runtime/runt
|
|
|
7
7
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
8
8
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
9
9
|
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
10
|
+
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
10
11
|
export function buildWebDashboardSnapshot(store, now = new Date()) {
|
|
11
12
|
return store.transaction((reader) => {
|
|
12
13
|
const statusCounts = {
|
|
@@ -97,6 +98,9 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
|
|
|
97
98
|
const effectiveLaunch = activeRun?.effective ?? activeSession?.effective ?? null;
|
|
98
99
|
return {
|
|
99
100
|
...role,
|
|
101
|
+
sessionTokens: projectSessionTokenMetrics(events, resolveSessionTokenIdentity(activeSession === undefined
|
|
102
|
+
? null
|
|
103
|
+
: { taskId, roleName: role.name, ...activeSession })),
|
|
100
104
|
effectiveLaunch,
|
|
101
105
|
effectiveLaunchSource: activeRun === undefined
|
|
102
106
|
? activeSession === undefined ? null : "session"
|