@zq-silk/yui 0.10.0 → 0.11.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/README.md +53 -0
- package/dist/cli/commandCatalog.js +28 -7
- package/dist/cli.js +50 -47
- package/dist/commands/projectCommands.js +69 -6
- package/dist/commands/taskCommands.js +639 -89
- package/dist/commands/taskContextCommand.js +78 -27
- package/dist/commands/taskNextActionCommand.js +13 -2
- package/dist/commands/taskOverviewCommand.js +21 -5
- package/dist/commands/taskUpstreamCommands.js +136 -0
- package/dist/context/runContextPack.js +184 -17
- package/dist/controller/agentRuntimeObserver.js +31 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
- package/dist/execution/candidateConvergence.js +623 -0
- package/dist/execution/executionGroup.js +255 -13
- package/dist/execution/executionHealth.js +324 -0
- package/dist/execution/resourceBroker.js +425 -0
- package/dist/executor/fileRoleLaunchPlanner.js +6 -9
- package/dist/executor/workspacePreflightClassification.js +117 -0
- package/dist/lifecycle/exactRunTerminalization.js +13 -2
- package/dist/lifecycle/taskRoleSessionReset.js +4 -2
- package/dist/repository/taskBaseFreshness.js +26 -1
- package/dist/repository/taskWorkspacePreparer.js +17 -1
- package/dist/review/reviewRound.js +27 -6
- package/dist/run/agentRun.js +2 -2
- package/dist/run/recoveryProjection.js +15 -0
- package/dist/runtime/runtimeContinuationProjection.js +7 -0
- package/dist/runtime/tmuxAdapters.js +8 -2
- package/dist/scheduler/actionability.js +169 -3
- package/dist/scheduler/activeTaskProgress.js +15 -10
- package/dist/scheduler/leaderWakeupProcessor.js +17 -1
- package/dist/scheduler/taskExecutionProjection.js +105 -8
- package/dist/scheduler/taskObservabilityProjection.js +282 -0
- package/dist/storage/migration/productionRegistry.js +14 -0
- package/dist/storage/sqliteStore.js +12 -0
- package/dist/storage/taskStore.js +1 -1
- package/dist/task/completionReadiness.js +1 -1
- package/dist/task/nextAction.js +314 -2
- package/dist/web/assets/client/components.js +116 -0
- package/dist/web/assets/client/i18n.js +66 -0
- package/dist/web/assets/client/view.js +15 -0
- package/dist/web/assets/styles/cards.js +23 -0
- package/dist/web/assets/styles/responsive.js +2 -0
- package/dist/web/webSnapshot.js +8 -2
- package/dist/workItem/workItem.js +262 -5
- package/i18n/README.zh-CN.md +42 -0
- package/package.json +1 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
2
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
3
3
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
4
|
-
import {
|
|
4
|
+
import { actionableExecutionLaneRecoveries, summarizeExecutionGroupHealth } from "../execution/executionHealth.js";
|
|
5
|
+
import { buildTaskObservabilityProjection } from "./taskObservabilityProjection.js";
|
|
5
6
|
import { isRoleRunStalled, latestStallProgressAt } from "./roleRunStall.js";
|
|
7
|
+
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
6
8
|
/**
|
|
7
9
|
* Build one consistent Task-first projection from the existing durable
|
|
8
10
|
* aggregate. This function only reads; it never starts a Controller, queues a
|
|
9
11
|
* wake, writes a Message, or mutates any record.
|
|
10
12
|
*/
|
|
11
|
-
export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
13
|
+
export function buildTaskExecutionProjection(store, taskId, taskOverride, now = new Date()) {
|
|
12
14
|
const task = store.getTask?.(taskId) ?? taskOverride ?? null;
|
|
13
15
|
if (task === null)
|
|
14
16
|
return null;
|
|
@@ -34,6 +36,9 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
34
36
|
? []
|
|
35
37
|
: collectExecutionGroups(store.listWorkItems?.(taskId) ?? [], store.listReviewRounds?.(taskId) ?? []),
|
|
36
38
|
workItems: store.listWorkItems?.(taskId) ?? [],
|
|
39
|
+
...(store.listContextSnapshots === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { contextSnapshots: store.listContextSnapshots(taskId) }),
|
|
37
42
|
inputRequests: store.listInputRequests?.(taskId) ?? [],
|
|
38
43
|
...(store.listReviewRounds === undefined
|
|
39
44
|
? {}
|
|
@@ -50,7 +55,9 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
50
55
|
leaderMailbox,
|
|
51
56
|
leaderFailure: store.getLeaderFailure?.(taskId) ?? null,
|
|
52
57
|
operatorNotification: store.getOperatorNotification?.(taskId) ?? null,
|
|
53
|
-
roleSessions
|
|
58
|
+
roleSessions,
|
|
59
|
+
now,
|
|
60
|
+
runtimeHealthPolicy: resolveRuntimeHealth(store.getConfig?.().runtimeHealth)
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
@@ -72,10 +79,37 @@ export function projectTaskExecutionFromFacts(facts) {
|
|
|
72
79
|
export const deriveTaskExecutionProjection = projectTaskExecution;
|
|
73
80
|
export function projectTaskExecution(facts) {
|
|
74
81
|
const { task, roles, runs, workItems = [], inputRequests = [], reviewRounds = [], integrations = [], events = [], pendingWakeup = null, leaderMailbox = null, leaderFailure = null, operatorNotification = null, roleSessions = [], executionGroups = [] } = facts;
|
|
75
|
-
const
|
|
82
|
+
const now = facts.now ?? new Date();
|
|
83
|
+
const groupSummaries = executionGroups.map((group) => {
|
|
84
|
+
const stageGroups = workItems.find((item) => (item.executionGroups.some(({ id }) => id === group.id)))?.executionGroups;
|
|
85
|
+
return summarizeExecutionGroupHealth({
|
|
86
|
+
group,
|
|
87
|
+
...(stageGroups === undefined ? {} : { stageGroups }),
|
|
88
|
+
runs,
|
|
89
|
+
sessions: roleSessions,
|
|
90
|
+
events,
|
|
91
|
+
now,
|
|
92
|
+
policy: facts.runtimeHealthPolicy
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
const observabilityGroups = uniqueExecutionGroups([
|
|
96
|
+
...executionGroups,
|
|
97
|
+
...workItems.flatMap((item) => item.executionGroups)
|
|
98
|
+
]);
|
|
99
|
+
const observability = buildTaskObservabilityProjection({
|
|
100
|
+
workItems,
|
|
101
|
+
executionGroups: observabilityGroups,
|
|
102
|
+
groupSummaries,
|
|
103
|
+
runs,
|
|
104
|
+
events,
|
|
105
|
+
contextSnapshots: facts.contextSnapshots,
|
|
106
|
+
now
|
|
107
|
+
});
|
|
108
|
+
const laneRecovery = actionableExecutionLaneRecoveries(groupSummaries)[0];
|
|
76
109
|
const render = (input) => projection({
|
|
77
110
|
...input,
|
|
78
|
-
executionGroups: groupSummaries
|
|
111
|
+
executionGroups: groupSummaries,
|
|
112
|
+
observability
|
|
79
113
|
});
|
|
80
114
|
const activeRuns = runs.filter((run) => run.status === "active");
|
|
81
115
|
const activeRunViews = activeRuns.map((run) => ({
|
|
@@ -138,8 +172,10 @@ export function projectTaskExecution(facts) {
|
|
|
138
172
|
|| attempt.status === "validating"
|
|
139
173
|
|| attempt.status === "blocked"));
|
|
140
174
|
const hasLeaderMismatch = attention.some((item) => item.kind === "identity-mismatch");
|
|
141
|
-
|
|
175
|
+
const renderAttention = () => {
|
|
142
176
|
const first = attention[0];
|
|
177
|
+
if (first === undefined)
|
|
178
|
+
throw new Error("Task execution attention disappeared.");
|
|
143
179
|
const progressingWithAttention = first.kind === "checkpoint-overdue"
|
|
144
180
|
&& healthyExecutionCarriers.length > 0;
|
|
145
181
|
return render({
|
|
@@ -159,6 +195,9 @@ export function projectTaskExecution(facts) {
|
|
|
159
195
|
blockers,
|
|
160
196
|
pendingWakeup
|
|
161
197
|
});
|
|
198
|
+
};
|
|
199
|
+
if (attention.length > 0 && hasLeaderMismatch) {
|
|
200
|
+
return renderAttention();
|
|
162
201
|
}
|
|
163
202
|
if (openInputs.length > 0) {
|
|
164
203
|
return render({
|
|
@@ -177,6 +216,30 @@ export function projectTaskExecution(facts) {
|
|
|
177
216
|
pendingWakeup
|
|
178
217
|
});
|
|
179
218
|
}
|
|
219
|
+
if (laneRecovery !== undefined) {
|
|
220
|
+
return render({
|
|
221
|
+
task,
|
|
222
|
+
status: laneRecovery.runtimeHealth === "confirmed-dead"
|
|
223
|
+
? "attention"
|
|
224
|
+
: "needs-leader-action",
|
|
225
|
+
owner: "leader",
|
|
226
|
+
action: "recover-execution",
|
|
227
|
+
summary: `Execution Lane ${laneRecovery.laneId} in ${laneRecovery.groupId}`
|
|
228
|
+
+ ` requires ${laneRecovery.recovery}`
|
|
229
|
+
+ (laneRecovery.runId === undefined ? "." : ` for exact Run ${laneRecovery.runId}.`),
|
|
230
|
+
reason: `execution-lane-${laneRecovery.recovery}`,
|
|
231
|
+
monitoring,
|
|
232
|
+
failClosed: false,
|
|
233
|
+
activeRuns: activeRunViews,
|
|
234
|
+
activeExecutorCount: activeExecutors.length,
|
|
235
|
+
attention,
|
|
236
|
+
blockers,
|
|
237
|
+
pendingWakeup
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (attention.length > 0) {
|
|
241
|
+
return renderAttention();
|
|
242
|
+
}
|
|
180
243
|
if (blockedIntegration || failedWork || hasLeaderMismatch) {
|
|
181
244
|
return render({
|
|
182
245
|
task,
|
|
@@ -335,19 +398,53 @@ function projection(input) {
|
|
|
335
398
|
activeExecutorCount: input.activeExecutorCount,
|
|
336
399
|
activeRuns: input.activeRuns,
|
|
337
400
|
executionGroups: input.executionGroups ?? [],
|
|
401
|
+
observability: input.observability,
|
|
338
402
|
attention: input.attention,
|
|
339
403
|
blockers: input.blockers,
|
|
340
404
|
pendingWakeup: input.pendingWakeup,
|
|
341
405
|
next: { owner: input.owner, action: input.action }
|
|
342
406
|
};
|
|
343
407
|
}
|
|
408
|
+
function uniqueExecutionGroups(groups) {
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
return groups.filter((group) => {
|
|
411
|
+
if (seen.has(group.id))
|
|
412
|
+
return false;
|
|
413
|
+
seen.add(group.id);
|
|
414
|
+
return true;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
344
417
|
function collectExecutionGroups(workItems, reviewRounds) {
|
|
418
|
+
const workItemsById = new Map(workItems.map((item) => [item.id, item]));
|
|
419
|
+
const orderedRounds = [...reviewRounds].sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })));
|
|
420
|
+
const latestTaskRound = orderedRounds
|
|
421
|
+
.filter((round) => (round.scope ?? "work-item") === "task")
|
|
422
|
+
.at(-1);
|
|
423
|
+
const latestWorkItemRounds = new Map();
|
|
424
|
+
for (const round of orderedRounds) {
|
|
425
|
+
if ((round.scope ?? "work-item") === "task"
|
|
426
|
+
|| round.workItemId === undefined
|
|
427
|
+
|| round.candidateId === undefined)
|
|
428
|
+
continue;
|
|
429
|
+
const item = workItemsById.get(round.workItemId);
|
|
430
|
+
if (item === undefined
|
|
431
|
+
|| item.status === "retired"
|
|
432
|
+
|| item.candidates.at(-1)?.id !== round.candidateId)
|
|
433
|
+
continue;
|
|
434
|
+
latestWorkItemRounds.set(`${round.workItemId}\0${round.candidateId}`, round);
|
|
435
|
+
}
|
|
436
|
+
const operationalReviewRoundIds = new Set([
|
|
437
|
+
...(latestTaskRound === undefined ? [] : [latestTaskRound.id]),
|
|
438
|
+
...[...latestWorkItemRounds.values()].map(({ id }) => id)
|
|
439
|
+
]);
|
|
345
440
|
const groups = [
|
|
346
|
-
...workItems.flatMap((item) => {
|
|
441
|
+
...workItems.filter(({ status }) => status !== "retired").flatMap((item) => {
|
|
347
442
|
const group = currentWorkItemExecutionGroup(item);
|
|
348
443
|
return group === undefined ? [] : [group];
|
|
349
444
|
}),
|
|
350
|
-
...reviewRounds.flatMap((round) => round.executionGroup === undefined
|
|
445
|
+
...reviewRounds.flatMap((round) => (!operationalReviewRoundIds.has(round.id) || round.executionGroup === undefined
|
|
446
|
+
? []
|
|
447
|
+
: [round.executionGroup]))
|
|
351
448
|
];
|
|
352
449
|
const seen = new Set();
|
|
353
450
|
return groups.filter((group) => {
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { observedExecutionResourceUsage } from "../execution/resourceBroker.js";
|
|
3
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build the read-only DAG, execution, cost, and context view consumed by CLI
|
|
6
|
+
* and Web. It deliberately derives every value from existing Task records and
|
|
7
|
+
* never persists or repairs a second graph/status authority.
|
|
8
|
+
*/
|
|
9
|
+
export function buildTaskObservabilityProjection(input) {
|
|
10
|
+
const now = input.now ?? new Date();
|
|
11
|
+
const summariesById = new Map((input.groupSummaries ?? []).map((summary) => [summary.groupId, summary]));
|
|
12
|
+
const dag = projectDag(input.workItems);
|
|
13
|
+
const workItems = input.workItems.map((item) => {
|
|
14
|
+
const groups = item.executionGroups;
|
|
15
|
+
const executionGroups = groups.flatMap((group) => {
|
|
16
|
+
const summary = summariesById.get(group.id);
|
|
17
|
+
return summary === undefined ? [] : [summary];
|
|
18
|
+
});
|
|
19
|
+
const stages = groups.map((group) => {
|
|
20
|
+
const summary = summariesById.get(group.id);
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
groupId: group.id,
|
|
23
|
+
...(group.stage === undefined ? {} : {
|
|
24
|
+
mode: group.stage.mode,
|
|
25
|
+
stage: group.stage.stage,
|
|
26
|
+
round: group.stage.round,
|
|
27
|
+
stageAttempt: group.stage.stageAttempt
|
|
28
|
+
}),
|
|
29
|
+
laneCount: group.lanes.length,
|
|
30
|
+
activeLaneCount: group.lanes.filter(({ status }) => status === "pending" || status === "running").length,
|
|
31
|
+
terminalLaneCount: group.lanes.filter(({ status }) => isTerminalLane(status)).length,
|
|
32
|
+
...(group.resolution === undefined ? {} : { resolution: group.resolution.decision }),
|
|
33
|
+
...(summary?.resources === undefined ? {} : { resources: summary.resources })
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
const itemCost = projectCost(groups, input.runs, input.events, now);
|
|
37
|
+
const itemContext = projectContext(groups, input.runs, input.events, input.contextSnapshots);
|
|
38
|
+
const evidence = groups.flatMap((group) => group.lanes.flatMap((lane) => (lane.result?.evidence ?? [])));
|
|
39
|
+
const openFindingCount = groups.reduce((count, group) => count + group.lanes.reduce((laneCount, lane) => laneCount + (lane.result?.findings ?? [])
|
|
40
|
+
.filter(({ status }) => status === "open").length, 0), 0);
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
workItemId: item.id,
|
|
43
|
+
title: item.title,
|
|
44
|
+
status: item.status,
|
|
45
|
+
...(item.currentExecutionGroupId === undefined ? {} : { currentGroupId: item.currentExecutionGroupId }),
|
|
46
|
+
groupIds: groups.map(({ id }) => id),
|
|
47
|
+
executionGroups,
|
|
48
|
+
stages,
|
|
49
|
+
cost: itemCost,
|
|
50
|
+
context: itemContext,
|
|
51
|
+
evidenceCount: evidence.length,
|
|
52
|
+
openFindingCount
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
const cost = projectCost(input.executionGroups, input.runs, input.events, now);
|
|
56
|
+
const context = projectContext(input.executionGroups, input.runs, input.events, input.contextSnapshots);
|
|
57
|
+
return Object.freeze({ dag, workItems, cost, context });
|
|
58
|
+
}
|
|
59
|
+
function projectDag(workItems) {
|
|
60
|
+
const byId = new Map(workItems.map((item) => [item.id, item]));
|
|
61
|
+
const dependents = new Map();
|
|
62
|
+
for (const item of workItems)
|
|
63
|
+
dependents.set(item.id, []);
|
|
64
|
+
const edges = [];
|
|
65
|
+
for (const item of workItems) {
|
|
66
|
+
for (const dependency of item.dependsOn) {
|
|
67
|
+
const target = byId.get(dependency);
|
|
68
|
+
const status = dependencyEdgeStatus(dependency, byId);
|
|
69
|
+
edges.push({ from: dependency, to: item.id, status });
|
|
70
|
+
dependents.get(dependency)?.push(item.id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const nodes = workItems.map((item) => {
|
|
74
|
+
const unresolved = item.dependsOn.filter((dependency) => {
|
|
75
|
+
return !dependencySatisfied(dependency, byId);
|
|
76
|
+
});
|
|
77
|
+
const projectedStatus = item.status === "pending"
|
|
78
|
+
? unresolved.length === 0 ? "ready" : "blocked"
|
|
79
|
+
: item.status;
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
id: item.id,
|
|
82
|
+
title: item.title,
|
|
83
|
+
status: item.status,
|
|
84
|
+
projectedStatus,
|
|
85
|
+
dependsOn: item.dependsOn,
|
|
86
|
+
dependentIds: Object.freeze([...(dependents.get(item.id) ?? [])]),
|
|
87
|
+
rootCauseIds: Object.freeze(rootCauses(item.id, byId)),
|
|
88
|
+
...(item.disposition?.replacementWorkItemId === undefined
|
|
89
|
+
? {}
|
|
90
|
+
: { replacementWorkItemId: item.disposition.replacementWorkItemId })
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
nodes,
|
|
95
|
+
edges: Object.freeze(edges),
|
|
96
|
+
readyIds: Object.freeze(nodes.filter(({ projectedStatus }) => projectedStatus === "ready").map(({ id }) => id)),
|
|
97
|
+
blockedIds: Object.freeze(nodes.filter(({ projectedStatus }) => projectedStatus === "blocked").map(({ id }) => id))
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function rootCauses(id, byId) {
|
|
101
|
+
const result = [];
|
|
102
|
+
const visited = new Set();
|
|
103
|
+
const visit = (currentId) => {
|
|
104
|
+
if (visited.has(currentId))
|
|
105
|
+
return;
|
|
106
|
+
visited.add(currentId);
|
|
107
|
+
const item = byId.get(currentId);
|
|
108
|
+
if (item === undefined) {
|
|
109
|
+
result.push(currentId);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const unresolved = item.dependsOn.filter((dependency) => !dependencySatisfied(dependency, byId));
|
|
113
|
+
if (unresolved.length === 0) {
|
|
114
|
+
if (item.status === "failed" || item.status === "awaiting_acceptance")
|
|
115
|
+
result.push(item.id);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const dependency of unresolved) {
|
|
119
|
+
const target = resolveDependency(dependency, byId);
|
|
120
|
+
if (target?.status === "failed" || target?.status === "awaiting_acceptance")
|
|
121
|
+
result.push(target.id);
|
|
122
|
+
else
|
|
123
|
+
visit(dependency);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
visit(id);
|
|
127
|
+
return [...new Set(result)];
|
|
128
|
+
}
|
|
129
|
+
function dependencySatisfied(id, byId) {
|
|
130
|
+
const target = resolveDependency(id, byId);
|
|
131
|
+
return target !== undefined && (target.status === "completed" || (target.status === "retired" && target.disposition?.replacementWorkItemId === undefined));
|
|
132
|
+
}
|
|
133
|
+
function dependencyEdgeStatus(id, byId) {
|
|
134
|
+
const target = byId.get(id);
|
|
135
|
+
if (target === undefined)
|
|
136
|
+
return "dead";
|
|
137
|
+
if (dependencySatisfied(id, byId))
|
|
138
|
+
return "satisfied";
|
|
139
|
+
const resolved = resolveDependency(id, byId);
|
|
140
|
+
if (resolved === undefined)
|
|
141
|
+
return "dead";
|
|
142
|
+
if (resolved.status === "failed" || resolved.status === "awaiting_acceptance")
|
|
143
|
+
return "failed-open";
|
|
144
|
+
return "active";
|
|
145
|
+
}
|
|
146
|
+
function resolveDependency(id, byId) {
|
|
147
|
+
const visited = new Set();
|
|
148
|
+
let current = byId.get(id);
|
|
149
|
+
while (current?.status === "retired" && current.disposition?.replacementWorkItemId !== undefined) {
|
|
150
|
+
if (visited.has(current.id))
|
|
151
|
+
return undefined;
|
|
152
|
+
visited.add(current.id);
|
|
153
|
+
current = byId.get(current.disposition.replacementWorkItemId);
|
|
154
|
+
}
|
|
155
|
+
return current;
|
|
156
|
+
}
|
|
157
|
+
function projectCost(groups, runs, events, now) {
|
|
158
|
+
const stageGroups = latestStageGroups(groups);
|
|
159
|
+
let tokens = 0;
|
|
160
|
+
let toolCalls = 0;
|
|
161
|
+
let wallClockSeconds = 0;
|
|
162
|
+
let tokensObservable = true;
|
|
163
|
+
let toolCallsObservable = true;
|
|
164
|
+
let laneCount = 0;
|
|
165
|
+
let retryCount = 0;
|
|
166
|
+
for (const group of stageGroups) {
|
|
167
|
+
const lineage = groups.filter((candidate) => sameStage(candidate, group));
|
|
168
|
+
const usage = observedExecutionResourceUsage({ group, stageGroups: lineage, runs, events });
|
|
169
|
+
tokens += usage.tokens;
|
|
170
|
+
toolCalls += usage.toolCalls;
|
|
171
|
+
tokensObservable = tokensObservable && (usage.tokensObservable ?? true);
|
|
172
|
+
toolCallsObservable = toolCallsObservable && (usage.toolCallsObservable ?? true);
|
|
173
|
+
wallClockSeconds += stageDurationSeconds(lineage, now);
|
|
174
|
+
laneCount += group.lanes.length;
|
|
175
|
+
retryCount += Math.max(0, lineage.length - 1);
|
|
176
|
+
}
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
tokens,
|
|
179
|
+
toolCalls,
|
|
180
|
+
wallClockSeconds,
|
|
181
|
+
tokensObservable,
|
|
182
|
+
toolCallsObservable,
|
|
183
|
+
laneCount,
|
|
184
|
+
groupCount: stageGroups.length,
|
|
185
|
+
retryCount,
|
|
186
|
+
marginalValuePercent: null,
|
|
187
|
+
marginalValueStatus: "unavailable"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function projectContext(groups, runs, events, snapshots) {
|
|
191
|
+
const refs = new Map();
|
|
192
|
+
for (const group of groups) {
|
|
193
|
+
const ref = group.stage?.contextSnapshotRef;
|
|
194
|
+
if (ref !== undefined)
|
|
195
|
+
refs.set(ref.id, ref);
|
|
196
|
+
}
|
|
197
|
+
for (const run of runs) {
|
|
198
|
+
const ref = run.assignment.contextSnapshotRef;
|
|
199
|
+
if (ref !== undefined)
|
|
200
|
+
refs.set(ref.id, ref);
|
|
201
|
+
}
|
|
202
|
+
const snapshotById = new Map((snapshots ?? []).map((snapshot) => [snapshot.id, snapshot]));
|
|
203
|
+
const metrics = [...refs.values()].sort((left, right) => left.sequence - right.sequence || left.id.localeCompare(right.id))
|
|
204
|
+
.map((ref) => {
|
|
205
|
+
const snapshot = snapshotById.get(ref.id);
|
|
206
|
+
const byteSize = snapshot === undefined ? null : Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
id: ref.id,
|
|
209
|
+
scope: ref.scope,
|
|
210
|
+
sequence: ref.sequence,
|
|
211
|
+
digest: ref.digest,
|
|
212
|
+
refCount: snapshot?.refs.length ?? null,
|
|
213
|
+
resourceCount: snapshot?.resources.length ?? null,
|
|
214
|
+
byteSize,
|
|
215
|
+
...(snapshot?.parentRef === undefined ? {} : { parentId: snapshot.parentRef.id })
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
const sizes = metrics.flatMap(({ byteSize }) => byteSize === null ? [] : [byteSize]);
|
|
219
|
+
const observedInputPeakTokens = events.flatMap((event) => {
|
|
220
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
221
|
+
const inputTokens = observation?.payload.usage?.inputTokens;
|
|
222
|
+
return inputTokens === undefined ? [] : [inputTokens];
|
|
223
|
+
}).reduce((peak, value) => Math.max(peak, value), 0);
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
snapshotCount: metrics.length,
|
|
226
|
+
snapshots: Object.freeze(metrics),
|
|
227
|
+
totalBytes: sizes.length === metrics.length ? sizes.reduce((sum, size) => sum + size, 0) : null,
|
|
228
|
+
largestBytes: sizes.length === 0 ? null : Math.max(...sizes),
|
|
229
|
+
observedInputPeakTokens,
|
|
230
|
+
compressionEvents: null,
|
|
231
|
+
compressionRatio: null,
|
|
232
|
+
compressionStatus: "unavailable"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function latestStageGroups(groups) {
|
|
236
|
+
const latest = new Map();
|
|
237
|
+
for (const group of groups) {
|
|
238
|
+
const key = group.stage === undefined ? `group:${group.id}` : stageKey(group);
|
|
239
|
+
const existing = latest.get(key);
|
|
240
|
+
if (existing === undefined || compareStageGroup(existing, group) < 0)
|
|
241
|
+
latest.set(key, group);
|
|
242
|
+
}
|
|
243
|
+
return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
244
|
+
}
|
|
245
|
+
function sameStage(left, right) {
|
|
246
|
+
if (left.stage === undefined || right.stage === undefined)
|
|
247
|
+
return left.id === right.id;
|
|
248
|
+
return left.taskId === right.taskId
|
|
249
|
+
&& executionTargetKey(left) === executionTargetKey(right)
|
|
250
|
+
&& left.stage.mode === right.stage.mode
|
|
251
|
+
&& left.stage.stage === right.stage.stage
|
|
252
|
+
&& left.stage.round === right.stage.round
|
|
253
|
+
&& isDeepStrictEqual(left.stage.budget, right.stage.budget)
|
|
254
|
+
&& isDeepStrictEqual(left.stage.resources, right.stage.resources);
|
|
255
|
+
}
|
|
256
|
+
function stageKey(group) {
|
|
257
|
+
const stage = group.stage;
|
|
258
|
+
return `${executionTargetKey(group)}\0${stage.mode}\0${stage.stage}\0${stage.round}`;
|
|
259
|
+
}
|
|
260
|
+
function executionTargetKey(group) {
|
|
261
|
+
const target = group.target;
|
|
262
|
+
return `${target.kind}\0${target.workItemId ?? ""}\0${target.candidateId ?? ""}`;
|
|
263
|
+
}
|
|
264
|
+
function compareStageGroup(left, right) {
|
|
265
|
+
return (left.stage?.stageAttempt ?? 1) - (right.stage?.stageAttempt ?? 1)
|
|
266
|
+
|| left.updatedAt.localeCompare(right.updatedAt)
|
|
267
|
+
|| left.id.localeCompare(right.id);
|
|
268
|
+
}
|
|
269
|
+
function stageDurationSeconds(groups, now) {
|
|
270
|
+
const started = groups.map(({ createdAt }) => Date.parse(createdAt)).filter(Number.isFinite);
|
|
271
|
+
const ended = groups.map((group) => Date.parse(group.updatedAt)).filter(Number.isFinite);
|
|
272
|
+
if (started.length === 0)
|
|
273
|
+
return 0;
|
|
274
|
+
const hasOpenWork = groups.some((group) => group.lanes.some(({ status }) => (status === "pending" || status === "running")));
|
|
275
|
+
const end = hasOpenWork || ended.length === 0
|
|
276
|
+
? now.getTime()
|
|
277
|
+
: Math.max(...ended);
|
|
278
|
+
return Math.max(0, Math.floor((end - Math.min(...started)) / 1_000));
|
|
279
|
+
}
|
|
280
|
+
function isTerminalLane(status) {
|
|
281
|
+
return status === "yielded" || status === "completed" || status === "failed" || status === "skipped";
|
|
282
|
+
}
|
|
@@ -28,6 +28,12 @@ const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
|
|
|
28
28
|
const WORK_ITEM_GIT_SNAPSHOT_TO_VERSION = 8;
|
|
29
29
|
const WORK_ITEM_GROUP_HISTORY_FROM_VERSION = 8;
|
|
30
30
|
const WORK_ITEM_GROUP_HISTORY_TO_VERSION = 9;
|
|
31
|
+
const WORK_ITEM_EXPLORATION_STAGE_FROM_VERSION = 9;
|
|
32
|
+
const WORK_ITEM_EXPLORATION_STAGE_TO_VERSION = 10;
|
|
33
|
+
const WORK_ITEM_CANDIDATE_CONVERGENCE_FROM_VERSION = 10;
|
|
34
|
+
const WORK_ITEM_CANDIDATE_CONVERGENCE_TO_VERSION = 11;
|
|
35
|
+
const WORK_ITEM_RESOURCE_SCHEDULING_FROM_VERSION = 11;
|
|
36
|
+
const WORK_ITEM_RESOURCE_SCHEDULING_TO_VERSION = 12;
|
|
31
37
|
const AGENT_RUN_FROM_VERSION = 5;
|
|
32
38
|
const AGENT_RUN_TO_VERSION = 6;
|
|
33
39
|
/**
|
|
@@ -144,6 +150,14 @@ export function createProductionStorageRegistry() {
|
|
|
144
150
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
|
|
145
151
|
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
|
|
146
152
|
.registerOfflineMigration(workItemExecutionGroupHistoryStep())
|
|
153
|
+
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_EXPLORATION_STAGE_FROM_VERSION, WORK_ITEM_EXPLORATION_STAGE_TO_VERSION, "workItems"))
|
|
154
|
+
// Candidate convergence is frozen only on newly created exploration
|
|
155
|
+
// histories. Existing valid T4 histories remain byte-for-byte evidence,
|
|
156
|
+
// so this adjacent persistent transition advances only the family version.
|
|
157
|
+
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_CANDIDATE_CONVERGENCE_FROM_VERSION, WORK_ITEM_CANDIDATE_CONVERGENCE_TO_VERSION, "workItems"))
|
|
158
|
+
// Existing v11 histories remain valid immutable evidence without a T6
|
|
159
|
+
// resource policy. Newly planned stages always freeze the full policy.
|
|
160
|
+
.registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_RESOURCE_SCHEDULING_FROM_VERSION, WORK_ITEM_RESOURCE_SCHEDULING_TO_VERSION, "workItems"))
|
|
147
161
|
.registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_FROM_VERSION, AGENT_RUN_TO_VERSION, "agentRuns"))
|
|
148
162
|
.registerOfflineMigration(recordFamilyStep("agentRun", AGENT_RUN_OPTIONAL_FIELDS_FROM_VERSION, AGENT_RUN_OPTIONAL_FIELDS_TO_VERSION, "agentRuns"))
|
|
149
163
|
.registerOfflineMigration(agentRunContextProtocolStep())
|
|
@@ -1480,6 +1480,18 @@ export class SqliteTaskStore {
|
|
|
1480
1480
|
saveAgentRun(run) {
|
|
1481
1481
|
if (run.taskId !== undefined)
|
|
1482
1482
|
this.#requireTask(run.taskId);
|
|
1483
|
+
if (run.reviewRoundId !== undefined) {
|
|
1484
|
+
const round = this.getReviewRound(run.taskId, run.reviewRoundId);
|
|
1485
|
+
if (round === null) {
|
|
1486
|
+
throw new StorageRecordError(`Agent run ReviewRound not found: ${run.reviewRoundId}.`);
|
|
1487
|
+
}
|
|
1488
|
+
const laneRole = round.executionGroup?.lanes
|
|
1489
|
+
.find(({ id }) => id === run.executionLaneId)?.roleName;
|
|
1490
|
+
if (round.workItemId !== run.workItemId
|
|
1491
|
+
|| (round.reviewerRoleName !== run.roleName && laneRole !== run.roleName)) {
|
|
1492
|
+
throw new StorageRecordError(`Agent run does not match ReviewRound: ${run.id}.`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1483
1495
|
this.#mutate(() => {
|
|
1484
1496
|
this.#db.prepare(`INSERT INTO agent_runs (task_id, run_id, role_name, status, payload, updated_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
1485
1497
|
ON CONFLICT(task_id, run_id) DO UPDATE SET role_name = excluded.role_name, status = excluded.status,
|
|
@@ -67,7 +67,7 @@ export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
|
|
|
67
67
|
export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
68
68
|
export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
|
|
69
69
|
export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
|
|
70
|
-
export const CURRENT_WORK_ITEM_SCHEMA_VERSION =
|
|
70
|
+
export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 12;
|
|
71
71
|
export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 5;
|
|
72
72
|
export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 3;
|
|
73
73
|
export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION = 4;
|
|
@@ -16,7 +16,7 @@ const UNRESOLVED_INTEGRATION_STATUSES = new Set([
|
|
|
16
16
|
"validating"
|
|
17
17
|
]);
|
|
18
18
|
const TERMINAL_REVIEW_STATUSES = new Set(["completed", "failed"]);
|
|
19
|
-
const TERMINAL_LANE_STATUSES = new Set(["completed", "failed", "yielded"]);
|
|
19
|
+
const TERMINAL_LANE_STATUSES = new Set(["completed", "failed", "yielded", "skipped"]);
|
|
20
20
|
export function projectCompletionReadiness(facts, options = {}) {
|
|
21
21
|
const blockers = [];
|
|
22
22
|
const advisories = [];
|