@tea-agent/loop-agent 0.11.0 → 0.13.0-alpha.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/CHANGELOG.md +74 -1
- package/README.md +33 -4
- package/dist/application/dag/generate-task-dag.js +45 -0
- package/dist/application/dag/run-dag.js +10 -0
- package/dist/application/dag/validate-dag.js +11 -0
- package/dist/cli/command-definitions.js +10 -3
- package/dist/commands/init.js +74 -7
- package/dist/commands/knowledge.js +129 -31
- package/dist/governance/manifest-types.js +3 -0
- package/dist/shared/package-metadata.js +135 -0
- package/dist/task/config-types.js +6 -1
- package/dist/worker/cli.js +99 -2
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +552 -118
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +310 -23
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +46 -0
- package/dist/worker/observe/static/app.js +120 -2598
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +133 -0
- package/dist/worker/observe/static/router.js +93 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1731 -495
- package/dist/worker/observe/static/views/batch.js +227 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +596 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +445 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +492 -0
- package/dist/worker/observe/static/views/pool.js +350 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +314 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +1233 -11
- package/dist/workflows/dag/node-execution.js +123 -29
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +32 -0
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/skill-snapshot.js +2 -0
- package/dist/workflows/dag/types.js +45 -1
- package/dist/workflows/dag/validate.js +68 -4
- package/docs/README.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +9 -0
- package/docs/agent-dag-runner.md +26 -1
- package/docs/architecture/dag-execution.md +6 -0
- package/docs/architecture/evolution.md +7 -5
- package/docs/architecture/facts-and-state.md +15 -2
- package/docs/architecture/worker-and-feature.md +6 -2
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +12 -3
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +108 -2
- package/docs/loop-agent-harness.md +15 -4
- package/docs/progress/README.md +22 -0
- package/docs/reports/README.md +14 -2
- package/docs/templates/agent-dag-report.schema.json +17 -0
- package/docs/templates/agent-dag.schema.json +69 -1
- package/docs/templates/agent-dag.supervised-implementation.json +8 -2
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +288 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +177 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/verification-matrix.md +2 -1
- package/package.json +8 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
- package/scripts/kb-graph-incremental-prepare.mjs +372 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +153 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +3 -1
- package/skills/agent-worker/references/agent-worker-operator.md +18 -1
- package/skills/frontend-design-review/SKILL.md +26 -24
- package/skills/frontend-implementation/SKILL.md +29 -26
- package/skills/frontend-implementation/references/node-contracts.md +50 -19
- package/skills/frontend-review/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -0
- package/skills/loop-agent/references/hybrid-dag.md +22 -3
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
|
@@ -3,10 +3,44 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { truncateUtf8Preview } from "../../shared/preview.js";
|
|
5
5
|
import { parseWorkerEventLine } from "./events.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readBoundedJsonlTail } from "./event-history.js";
|
|
7
|
+
import { getTaskPoolRoot, listLegacyStateFiles, listTaskPoolStates, } from "../pool/run-store.js";
|
|
7
8
|
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, deriveDagRunEffectiveStatus, } from "../../workflows/dag/lifecycle.js";
|
|
8
9
|
import { parseDagSpec, resolveModelForTask, } from "../../workflows/dag/types.js";
|
|
9
10
|
import { loadFeatureDecisionModels } from "../feature/decision-loader.js";
|
|
11
|
+
/** Internal map key: featureId + NUL + taskId (stable, non-printable delimiter). */
|
|
12
|
+
export function taskIdentityKey(featureId, taskId) {
|
|
13
|
+
return `${featureId}\0${taskId}`;
|
|
14
|
+
}
|
|
15
|
+
export function compareTaskIdentity(a, b) {
|
|
16
|
+
const fa = a.featureId ?? "";
|
|
17
|
+
const fb = b.featureId ?? "";
|
|
18
|
+
const byFeature = fa.localeCompare(fb);
|
|
19
|
+
if (byFeature !== 0)
|
|
20
|
+
return byFeature;
|
|
21
|
+
return a.taskId.localeCompare(b.taskId);
|
|
22
|
+
}
|
|
23
|
+
export function findTasksByTaskId(tasks, taskId) {
|
|
24
|
+
return tasks.filter((task) => task.taskId === taskId);
|
|
25
|
+
}
|
|
26
|
+
export function resolveLegacyTask(tasks, taskId) {
|
|
27
|
+
const matches = findTasksByTaskId(tasks, taskId);
|
|
28
|
+
const candidates = matches.map((task) => ({
|
|
29
|
+
featureId: task.featureId,
|
|
30
|
+
taskId: task.taskId,
|
|
31
|
+
status: task.status,
|
|
32
|
+
workerRunId: task.workerRunId,
|
|
33
|
+
}));
|
|
34
|
+
if (matches.length === 0) {
|
|
35
|
+
return { kind: "missing", candidates: [] };
|
|
36
|
+
}
|
|
37
|
+
if (matches.length === 1) {
|
|
38
|
+
return { kind: "unique", task: matches[0], candidates };
|
|
39
|
+
}
|
|
40
|
+
return { kind: "ambiguous", candidates };
|
|
41
|
+
}
|
|
42
|
+
export const TASK_RUN_HISTORY_DEFAULT_LIMIT = 20;
|
|
43
|
+
export const TASK_RUN_HISTORY_MAX_LIMIT = 100;
|
|
10
44
|
const ACTIVE_TASK_STATUSES = new Set(["running", "pending"]);
|
|
11
45
|
const ACTIVE_BATCH_STATUSES = new Set(["running", "pending"]);
|
|
12
46
|
export async function buildGlobalSnapshot(options) {
|
|
@@ -15,7 +49,8 @@ export async function buildGlobalSnapshot(options) {
|
|
|
15
49
|
const now = options.now ?? (() => new Date());
|
|
16
50
|
const staleThresholdMs = options.staleThresholdMs ?? 90_000;
|
|
17
51
|
const quietThresholdMs = options.quietThresholdMs ?? 60_000;
|
|
18
|
-
const
|
|
52
|
+
const taskPoolPresent = existsSync(getTaskPoolRoot(repoRoot));
|
|
53
|
+
const [eventsLoad, ledgerLoad, ledgerBatches, statesLoad, handoffs, dagRuns,] = await Promise.all([
|
|
19
54
|
loadObservabilityEvents(repoRoot),
|
|
20
55
|
loadLedgerRuns(repoRoot),
|
|
21
56
|
loadLedgerBatches(repoRoot),
|
|
@@ -23,29 +58,45 @@ export async function buildGlobalSnapshot(options) {
|
|
|
23
58
|
loadFailureHandoffs(repoRoot),
|
|
24
59
|
loadDagRuns(repoRoot, now()),
|
|
25
60
|
]);
|
|
61
|
+
const events = eventsLoad.events;
|
|
62
|
+
const ledgerRuns = ledgerLoad.runs;
|
|
63
|
+
const identityWarnings = [];
|
|
26
64
|
const taskMap = new Map();
|
|
27
|
-
|
|
28
|
-
mergeStateRecord(taskMap, state);
|
|
29
|
-
}
|
|
65
|
+
// Ledger first so featureId-bearing rows seed composite identity before states/events.
|
|
30
66
|
for (const run of ledgerRuns) {
|
|
31
|
-
mergeLedgerRun(taskMap, run);
|
|
67
|
+
mergeLedgerRun(taskMap, run, identityWarnings);
|
|
68
|
+
}
|
|
69
|
+
for (const state of statesLoad.states) {
|
|
70
|
+
mergeStateRecord(taskMap, state, identityWarnings);
|
|
71
|
+
}
|
|
72
|
+
// Attach uniquely-resolvable legacy flat states after owned rows exist.
|
|
73
|
+
for (const state of statesLoad.legacyCandidates) {
|
|
74
|
+
mergeLegacyStateRecord(taskMap, state, identityWarnings);
|
|
32
75
|
}
|
|
33
|
-
|
|
76
|
+
const workerRunFeatureIndex = buildWorkerRunFeatureIndex(ledgerRuns, taskMap);
|
|
77
|
+
mergeEvents(taskMap, events, workerRunFeatureIndex, identityWarnings);
|
|
34
78
|
mergeHandoffs(taskMap, handoffs);
|
|
35
79
|
applyStaleDetection(taskMap, now(), staleThresholdMs, quietThresholdMs);
|
|
36
|
-
const historicalRunMap = buildHistoricalRunMap(ledgerRuns, events);
|
|
80
|
+
const historicalRunMap = buildHistoricalRunMap(ledgerRuns, events, workerRunFeatureIndex, identityWarnings);
|
|
37
81
|
mergeHandoffs(historicalRunMap, handoffs);
|
|
38
82
|
applyStaleDetection(historicalRunMap, now(), staleThresholdMs, quietThresholdMs);
|
|
39
83
|
const tasks = [...taskMap.values()]
|
|
40
84
|
.map(toPublicTaskSummary)
|
|
41
85
|
.map((task) => normalizeTaskArtifactRefs(task, repoRoot))
|
|
42
|
-
.sort(
|
|
86
|
+
.sort(compareTaskIdentity);
|
|
43
87
|
const batches = buildBatches(ledgerBatches, events, historicalRunMap).map((batch) => ({
|
|
44
88
|
...batch,
|
|
45
89
|
tasks: batch.tasks.map((task) => normalizeTaskArtifactRefs(task, repoRoot)),
|
|
46
90
|
}));
|
|
47
91
|
const health = computeHealth(batches, tasks, dagRuns);
|
|
48
|
-
const
|
|
92
|
+
const featureLoad = await loadFeatureDecisionModels(repoRoot);
|
|
93
|
+
const projectionWarnings = [
|
|
94
|
+
...(eventsLoad.warnings ?? []),
|
|
95
|
+
...(ledgerLoad.warnings ?? []),
|
|
96
|
+
...(statesLoad.warnings ?? []),
|
|
97
|
+
...identityWarnings,
|
|
98
|
+
...(featureLoad.projectionWarnings ?? []),
|
|
99
|
+
];
|
|
49
100
|
return {
|
|
50
101
|
schemaVersion: 1,
|
|
51
102
|
generatedAt: now().toISOString(),
|
|
@@ -53,7 +104,8 @@ export async function buildGlobalSnapshot(options) {
|
|
|
53
104
|
batches,
|
|
54
105
|
tasks,
|
|
55
106
|
dagRuns,
|
|
56
|
-
features,
|
|
107
|
+
features: featureLoad.features,
|
|
108
|
+
taskPool: { present: taskPoolPresent },
|
|
57
109
|
projectionWarnings,
|
|
58
110
|
health,
|
|
59
111
|
};
|
|
@@ -80,7 +132,105 @@ export async function buildGlobalSnapshot(options) {
|
|
|
80
132
|
return snapshot;
|
|
81
133
|
}
|
|
82
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Bounded Task run history from Task Pool ledger facts (newest first).
|
|
137
|
+
* Filtered by composite `{featureId, taskId}`.
|
|
138
|
+
* `before` is an exclusive upper bound on the sort key (`recordedAt` or empty).
|
|
139
|
+
*/
|
|
140
|
+
export async function listTaskRunHistory(repoRoot, featureId, taskId, options = {}) {
|
|
141
|
+
const limit = clampTaskRunHistoryLimit(options.limit);
|
|
142
|
+
const before = decodeTaskRunHistoryCursor(options.before);
|
|
143
|
+
const [ledgerLoad, eventsLoad] = await Promise.all([
|
|
144
|
+
loadLedgerRuns(path.resolve(repoRoot)),
|
|
145
|
+
loadObservabilityEvents(path.resolve(repoRoot)),
|
|
146
|
+
]);
|
|
147
|
+
const ledgerRuns = ledgerLoad.runs;
|
|
148
|
+
const events = eventsLoad.events;
|
|
149
|
+
const workerRunFeatureIndex = buildWorkerRunFeatureIndex(ledgerRuns);
|
|
150
|
+
const historical = buildHistoricalRunMap(ledgerRuns, events, workerRunFeatureIndex);
|
|
151
|
+
const items = [];
|
|
152
|
+
for (const run of ledgerRuns) {
|
|
153
|
+
if (run.taskId !== taskId)
|
|
154
|
+
continue;
|
|
155
|
+
if ((run.featureId ?? "") !== featureId)
|
|
156
|
+
continue;
|
|
157
|
+
const enriched = historical.get(run.workerRunId);
|
|
158
|
+
items.push({
|
|
159
|
+
workerRunId: run.workerRunId,
|
|
160
|
+
taskId: run.taskId,
|
|
161
|
+
featureId: run.featureId ?? featureId,
|
|
162
|
+
status: enriched?.status ?? mapRunRecordStatus(run.status),
|
|
163
|
+
startedAt: enriched?.startedAt,
|
|
164
|
+
finishedAt: enriched?.finishedAt ?? run.recordedAt,
|
|
165
|
+
recordedAt: run.recordedAt,
|
|
166
|
+
batchRunId: run.batchRunId,
|
|
167
|
+
dagRunId: enriched?.dagRunId,
|
|
168
|
+
...(run.retryOfWorkerRunId
|
|
169
|
+
? { retryOfWorkerRunId: run.retryOfWorkerRunId }
|
|
170
|
+
: {}),
|
|
171
|
+
failureCategory: run.failureCategory ?? enriched?.failureCategory,
|
|
172
|
+
recommendedFollowUp: run.recommendedFollowUp ?? enriched?.recommendedFollowUp,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
items.sort((a, b) => {
|
|
176
|
+
const ta = a.recordedAt ?? a.finishedAt ?? "";
|
|
177
|
+
const tb = b.recordedAt ?? b.finishedAt ?? "";
|
|
178
|
+
if (ta !== tb)
|
|
179
|
+
return tb.localeCompare(ta);
|
|
180
|
+
return b.workerRunId.localeCompare(a.workerRunId);
|
|
181
|
+
});
|
|
182
|
+
const eligible = before
|
|
183
|
+
? items.filter((item) => taskRunSortKey(item).localeCompare(before) < 0)
|
|
184
|
+
: items;
|
|
185
|
+
const page = eligible.slice(0, limit);
|
|
186
|
+
const nextBefore = eligible.length > limit
|
|
187
|
+
? encodeTaskRunHistoryCursor(page[page.length - 1])
|
|
188
|
+
: null;
|
|
189
|
+
return {
|
|
190
|
+
featureId,
|
|
191
|
+
taskId,
|
|
192
|
+
runs: page,
|
|
193
|
+
limit,
|
|
194
|
+
nextBefore,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function taskRunSortKey(item) {
|
|
198
|
+
return `${item.recordedAt ?? item.finishedAt ?? ""}\u0000${item.workerRunId}`;
|
|
199
|
+
}
|
|
200
|
+
function encodeTaskRunHistoryCursor(item) {
|
|
201
|
+
if (!item)
|
|
202
|
+
return null;
|
|
203
|
+
return `v1.${Buffer.from(taskRunSortKey(item), "utf-8").toString("base64url")}`;
|
|
204
|
+
}
|
|
205
|
+
function decodeTaskRunHistoryCursor(cursor) {
|
|
206
|
+
const value = cursor?.trim();
|
|
207
|
+
if (!value)
|
|
208
|
+
return null;
|
|
209
|
+
if (!value.startsWith("v1.")) {
|
|
210
|
+
// Compatibility with the initial timestamp-only cursor shape.
|
|
211
|
+
return `${value}\u0000`;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return Buffer.from(value.slice(3), "base64url").toString("utf-8");
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return `${value}\u0000`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export function clampTaskRunHistoryLimit(limit) {
|
|
221
|
+
if (limit === undefined || !Number.isFinite(limit) || limit <= 0) {
|
|
222
|
+
return TASK_RUN_HISTORY_DEFAULT_LIMIT;
|
|
223
|
+
}
|
|
224
|
+
return Math.min(Math.floor(limit), TASK_RUN_HISTORY_MAX_LIMIT);
|
|
225
|
+
}
|
|
83
226
|
function emptySnapshot(repoRoot, now) {
|
|
227
|
+
let taskPoolPresent = false;
|
|
228
|
+
try {
|
|
229
|
+
taskPoolPresent = existsSync(getTaskPoolRoot(repoRoot));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
taskPoolPresent = false;
|
|
233
|
+
}
|
|
84
234
|
return {
|
|
85
235
|
schemaVersion: 1,
|
|
86
236
|
generatedAt: now().toISOString(),
|
|
@@ -88,6 +238,7 @@ function emptySnapshot(repoRoot, now) {
|
|
|
88
238
|
batches: [],
|
|
89
239
|
tasks: [],
|
|
90
240
|
dagRuns: [],
|
|
241
|
+
taskPool: { present: taskPoolPresent },
|
|
91
242
|
health: {
|
|
92
243
|
activeBatches: 0,
|
|
93
244
|
activeTasks: 0,
|
|
@@ -118,15 +269,29 @@ function emptySnapshot(repoRoot, now) {
|
|
|
118
269
|
};
|
|
119
270
|
}
|
|
120
271
|
function toPublicTaskSummary(task) {
|
|
121
|
-
const {
|
|
272
|
+
const { timeoutMs: _to, commandStartedAt: _cs, ...rest } = task;
|
|
122
273
|
return rest;
|
|
123
274
|
}
|
|
124
|
-
function mergeStateRecord(taskMap, state) {
|
|
125
|
-
const
|
|
126
|
-
|
|
275
|
+
function mergeStateRecord(taskMap, state, warnings) {
|
|
276
|
+
const featureId = state.featureId?.trim();
|
|
277
|
+
if (!featureId) {
|
|
278
|
+
warnings?.push(`state for task ${state.taskId} missing featureId; excluded from composite projection`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const key = taskIdentityKey(featureId, state.taskId);
|
|
282
|
+
const existing = taskMap.get(key) ?? {
|
|
283
|
+
taskId: state.taskId,
|
|
284
|
+
featureId,
|
|
285
|
+
status: "unknown",
|
|
286
|
+
};
|
|
287
|
+
taskMap.set(key, {
|
|
127
288
|
...existing,
|
|
128
|
-
|
|
289
|
+
featureId,
|
|
290
|
+
status: existing.status !== "unknown"
|
|
291
|
+
? existing.status
|
|
292
|
+
: mapStateStatus(state.status),
|
|
129
293
|
workerRunId: existing.workerRunId ?? state.workerRunId,
|
|
294
|
+
updatedAt: state.updatedAt ?? existing.updatedAt,
|
|
130
295
|
artifactRefs: mergeArtifactRefs(existing.artifactRefs, {
|
|
131
296
|
runRecordPath: state.lastRunRecordPath,
|
|
132
297
|
}),
|
|
@@ -134,46 +299,140 @@ function mergeStateRecord(taskMap, state) {
|
|
|
134
299
|
recommendedFollowUp: existing.recommendedFollowUp ?? state.recommendedFollowUp,
|
|
135
300
|
});
|
|
136
301
|
}
|
|
137
|
-
|
|
138
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Legacy flat states/<taskId>.json: only merge when the taskId already maps to
|
|
304
|
+
* exactly one composite row in the snapshot (unique ownership). Otherwise warn+exclude.
|
|
305
|
+
*/
|
|
306
|
+
function mergeLegacyStateRecord(taskMap, state, warnings) {
|
|
307
|
+
const owners = [...taskMap.values()].filter((task) => task.taskId === state.taskId && Boolean(task.featureId));
|
|
308
|
+
if (owners.length === 1) {
|
|
309
|
+
const owner = owners[0];
|
|
310
|
+
mergeStateRecord(taskMap, { ...state, featureId: owner.featureId }, warnings);
|
|
311
|
+
warnings.push(`legacy flat state for task ${state.taskId} attached to unique feature ${owner.featureId}`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (owners.length === 0) {
|
|
315
|
+
// No owned composite row yet: still surface as a warning and do not invent featureId.
|
|
316
|
+
warnings.push(`legacy flat state for task ${state.taskId} has no unique feature ownership; excluded from composite projection`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
warnings.push(`legacy flat state for task ${state.taskId} is ambiguous across features; excluded from composite projection`);
|
|
320
|
+
}
|
|
321
|
+
function mergeLedgerRun(taskMap, run, warnings) {
|
|
322
|
+
const featureId = run.featureId?.trim();
|
|
323
|
+
if (!featureId) {
|
|
324
|
+
warnings?.push(`ledger run ${run.workerRunId} for task ${run.taskId} missing featureId; excluded from composite projection`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const key = taskIdentityKey(featureId, run.taskId);
|
|
328
|
+
const existing = taskMap.get(key) ?? {
|
|
329
|
+
taskId: run.taskId,
|
|
330
|
+
featureId,
|
|
331
|
+
status: "unknown",
|
|
332
|
+
};
|
|
139
333
|
const sameRun = existing.workerRunId === run.workerRunId;
|
|
140
334
|
const runArtifactRefs = {
|
|
141
335
|
runRecordPath: run.runRecordPath,
|
|
142
336
|
dagPath: run.dagPath,
|
|
143
337
|
...run.failureArtifacts,
|
|
144
338
|
};
|
|
145
|
-
taskMap.set(
|
|
339
|
+
taskMap.set(key, {
|
|
146
340
|
...(sameRun
|
|
147
341
|
? existing
|
|
148
342
|
: {
|
|
149
343
|
taskId: run.taskId,
|
|
344
|
+
featureId,
|
|
150
345
|
status: "unknown",
|
|
151
|
-
featureId: existing.featureId,
|
|
152
346
|
harnessTaskId: existing.harnessTaskId,
|
|
153
347
|
}),
|
|
154
348
|
batchRunId: run.batchRunId,
|
|
155
|
-
featureId
|
|
349
|
+
featureId,
|
|
156
350
|
status: mapRunRecordStatus(run.status),
|
|
157
351
|
workerRunId: run.workerRunId,
|
|
158
352
|
harnessTaskId: run.harnessTaskId ?? existing.harnessTaskId,
|
|
159
353
|
finishedAt: run.recordedAt ?? (sameRun ? existing.finishedAt : undefined),
|
|
160
354
|
failureCategory: run.failureCategory ?? (sameRun ? existing.failureCategory : undefined),
|
|
161
|
-
recommendedFollowUp: run.recommendedFollowUp ??
|
|
355
|
+
recommendedFollowUp: run.recommendedFollowUp ??
|
|
356
|
+
(sameRun ? existing.recommendedFollowUp : undefined),
|
|
162
357
|
artifactRefs: mergeArtifactRefs(sameRun ? existing.artifactRefs : undefined, runArtifactRefs),
|
|
163
358
|
});
|
|
164
359
|
}
|
|
165
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Build workerRunId → featureId unique index from ledger + already-owned task rows.
|
|
362
|
+
* Ambiguous (0 or N) mappings are omitted so callers treat them as unowned.
|
|
363
|
+
*/
|
|
364
|
+
function buildWorkerRunFeatureIndex(ledgerRuns, taskMap) {
|
|
365
|
+
const candidates = new Map();
|
|
366
|
+
const add = (workerRunId, featureId) => {
|
|
367
|
+
const wr = workerRunId?.trim();
|
|
368
|
+
const fid = featureId?.trim();
|
|
369
|
+
if (!wr || !fid)
|
|
370
|
+
return;
|
|
371
|
+
let set = candidates.get(wr);
|
|
372
|
+
if (!set) {
|
|
373
|
+
set = new Set();
|
|
374
|
+
candidates.set(wr, set);
|
|
375
|
+
}
|
|
376
|
+
set.add(fid);
|
|
377
|
+
};
|
|
378
|
+
for (const run of ledgerRuns) {
|
|
379
|
+
add(run.workerRunId, run.featureId);
|
|
380
|
+
}
|
|
381
|
+
if (taskMap) {
|
|
382
|
+
for (const task of taskMap.values()) {
|
|
383
|
+
add(task.workerRunId, task.featureId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const unique = new Map();
|
|
387
|
+
for (const [workerRunId, features] of candidates) {
|
|
388
|
+
if (features.size === 1) {
|
|
389
|
+
unique.set(workerRunId, [...features][0]);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return unique;
|
|
393
|
+
}
|
|
394
|
+
function mergeEvents(taskMap, events, workerRunFeatureIndex, warnings) {
|
|
166
395
|
const sorted = [...events].sort((a, b) => a.at.localeCompare(b.at));
|
|
167
396
|
for (const event of sorted) {
|
|
168
397
|
if (event.taskId) {
|
|
169
|
-
applyTaskEvent(taskMap, event);
|
|
398
|
+
applyTaskEvent(taskMap, event, workerRunFeatureIndex, warnings);
|
|
170
399
|
}
|
|
171
400
|
}
|
|
172
401
|
}
|
|
173
|
-
function applyTaskEvent(taskMap, event) {
|
|
402
|
+
function applyTaskEvent(taskMap, event, workerRunFeatureIndex, warnings) {
|
|
403
|
+
const taskId = event.taskId;
|
|
404
|
+
const featureId = resolveEventFeatureId(event, taskMap, workerRunFeatureIndex, warnings);
|
|
405
|
+
if (!featureId)
|
|
406
|
+
return;
|
|
407
|
+
const key = taskIdentityKey(featureId, taskId);
|
|
408
|
+
const existing = taskMap.get(key) ?? {
|
|
409
|
+
taskId,
|
|
410
|
+
featureId,
|
|
411
|
+
status: "unknown",
|
|
412
|
+
};
|
|
413
|
+
taskMap.set(key, applyEventToSummary(existing, event));
|
|
414
|
+
}
|
|
415
|
+
function resolveEventFeatureId(event, taskMap, workerRunFeatureIndex, warnings) {
|
|
174
416
|
const taskId = event.taskId;
|
|
175
|
-
|
|
176
|
-
|
|
417
|
+
if (!taskId)
|
|
418
|
+
return undefined;
|
|
419
|
+
if (event.workerRunId) {
|
|
420
|
+
const fromIndex = workerRunFeatureIndex.get(event.workerRunId);
|
|
421
|
+
if (fromIndex)
|
|
422
|
+
return fromIndex;
|
|
423
|
+
// Fall through: maybe a single existing composite row already owns this workerRunId.
|
|
424
|
+
const byWorkerRun = [...taskMap.values()].filter((task) => task.workerRunId === event.workerRunId && Boolean(task.featureId));
|
|
425
|
+
if (byWorkerRun.length === 1)
|
|
426
|
+
return byWorkerRun[0].featureId;
|
|
427
|
+
}
|
|
428
|
+
// Unique taskId ownership in current map (single Feature).
|
|
429
|
+
const byTaskId = [...taskMap.values()].filter((task) => task.taskId === taskId && Boolean(task.featureId));
|
|
430
|
+
if (byTaskId.length === 1)
|
|
431
|
+
return byTaskId[0].featureId;
|
|
432
|
+
warnings?.push(`unowned event ${event.type} for task ${taskId}` +
|
|
433
|
+
(event.workerRunId ? ` workerRunId ${event.workerRunId}` : "") +
|
|
434
|
+
` cannot uniquely resolve featureId; excluded from task projection`);
|
|
435
|
+
return undefined;
|
|
177
436
|
}
|
|
178
437
|
function applyEventToSummary(existing, event) {
|
|
179
438
|
const updated = {
|
|
@@ -224,9 +483,10 @@ function applyEventToSummary(existing, event) {
|
|
|
224
483
|
}
|
|
225
484
|
return updated;
|
|
226
485
|
}
|
|
227
|
-
function buildHistoricalRunMap(ledgerRuns, events) {
|
|
486
|
+
function buildHistoricalRunMap(ledgerRuns, events, workerRunFeatureIndex = new Map(), warnings) {
|
|
228
487
|
const runMap = new Map();
|
|
229
488
|
for (const run of ledgerRuns) {
|
|
489
|
+
// History map is keyed by workerRunId (run-scoped); featureId still required when present.
|
|
230
490
|
runMap.set(run.workerRunId, {
|
|
231
491
|
taskId: run.taskId,
|
|
232
492
|
batchRunId: run.batchRunId,
|
|
@@ -247,12 +507,23 @@ function buildHistoricalRunMap(ledgerRuns, events) {
|
|
|
247
507
|
for (const event of [...events].sort((a, b) => a.at.localeCompare(b.at))) {
|
|
248
508
|
if (!event.workerRunId || !event.taskId)
|
|
249
509
|
continue;
|
|
250
|
-
const existing = runMap.get(event.workerRunId)
|
|
510
|
+
const existing = runMap.get(event.workerRunId);
|
|
511
|
+
if (existing) {
|
|
512
|
+
runMap.set(event.workerRunId, applyEventToSummary(existing, event));
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
// Only invent a historical row when workerRunId uniquely maps to a feature.
|
|
516
|
+
const featureId = workerRunFeatureIndex.get(event.workerRunId);
|
|
517
|
+
if (!featureId) {
|
|
518
|
+
warnings?.push(`unowned historical event ${event.type} for task ${event.taskId} workerRunId ${event.workerRunId} cannot uniquely resolve featureId; excluded from historical run map`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
runMap.set(event.workerRunId, applyEventToSummary({
|
|
251
522
|
taskId: event.taskId,
|
|
523
|
+
featureId,
|
|
252
524
|
workerRunId: event.workerRunId,
|
|
253
525
|
status: "unknown",
|
|
254
|
-
};
|
|
255
|
-
runMap.set(event.workerRunId, applyEventToSummary(existing, event));
|
|
526
|
+
}, event));
|
|
256
527
|
}
|
|
257
528
|
return runMap;
|
|
258
529
|
}
|
|
@@ -320,7 +591,9 @@ function computeLiveness(task, nowMs, staleThresholdMs, quietThresholdMs) {
|
|
|
320
591
|
task.timeoutAt = new Date(commandStartedMs + timeoutMs).toISOString();
|
|
321
592
|
}
|
|
322
593
|
}
|
|
323
|
-
const heartbeatMs = task.lastHeartbeatAt
|
|
594
|
+
const heartbeatMs = task.lastHeartbeatAt
|
|
595
|
+
? Date.parse(task.lastHeartbeatAt)
|
|
596
|
+
: NaN;
|
|
324
597
|
const outputMs = task.lastOutputAt ? Date.parse(task.lastOutputAt) : NaN;
|
|
325
598
|
// Priority 1: timeout-risk (elapsed > 0.8 * timeoutMs), only if timeoutMs known.
|
|
326
599
|
if (timeoutMs !== undefined &&
|
|
@@ -333,7 +606,9 @@ function computeLiveness(task, nowMs, staleThresholdMs, quietThresholdMs) {
|
|
|
333
606
|
// (a) Heartbeat present but older than staleThresholdMs.
|
|
334
607
|
// (b) No heartbeat but running longer than staleThresholdMs (startedAt known).
|
|
335
608
|
const heartbeatStale = !Number.isNaN(heartbeatMs) && nowMs - heartbeatMs > staleThresholdMs;
|
|
336
|
-
const noHeartbeatStale = Number.isNaN(heartbeatMs) &&
|
|
609
|
+
const noHeartbeatStale = Number.isNaN(heartbeatMs) &&
|
|
610
|
+
elapsedMs !== undefined &&
|
|
611
|
+
elapsedMs > staleThresholdMs;
|
|
337
612
|
if (heartbeatStale || noHeartbeatStale) {
|
|
338
613
|
task.liveness = "stale";
|
|
339
614
|
task.status = "stale";
|
|
@@ -390,7 +665,7 @@ function buildBatches(ledgerBatches, events, taskMap) {
|
|
|
390
665
|
batch.tasks.push(toPublicTaskSummary(task));
|
|
391
666
|
}
|
|
392
667
|
for (const batch of batchMap.values()) {
|
|
393
|
-
batch.tasks.sort(
|
|
668
|
+
batch.tasks.sort(compareTaskIdentity);
|
|
394
669
|
if (batch.summary === undefined && batch.tasks.length > 0) {
|
|
395
670
|
batch.summary = summarizeTasks(batch.tasks);
|
|
396
671
|
}
|
|
@@ -414,7 +689,8 @@ function summarizeTasks(tasks) {
|
|
|
414
689
|
function computeHealth(batches, tasks, dagRuns) {
|
|
415
690
|
const worker = {
|
|
416
691
|
activeTasks: tasks.filter((t) => ACTIVE_TASK_STATUSES.has(t.status)).length,
|
|
417
|
-
activeBatches: batches.filter((b) => ACTIVE_BATCH_STATUSES.has(b.status))
|
|
692
|
+
activeBatches: batches.filter((b) => ACTIVE_BATCH_STATUSES.has(b.status))
|
|
693
|
+
.length,
|
|
418
694
|
quietCount: tasks.filter((t) => t.liveness === "quiet").length,
|
|
419
695
|
staleCount: tasks.filter((t) => t.status === "stale").length,
|
|
420
696
|
timeoutRiskCount: tasks.filter((t) => t.liveness === "timeout-risk").length,
|
|
@@ -451,7 +727,8 @@ const TERMINAL_RUN_STATUSES = new Set([
|
|
|
451
727
|
* already-loaded DagRunSummary facts without touching app.js or new files.
|
|
452
728
|
*/
|
|
453
729
|
function isDagRunActiveForHealth(dag) {
|
|
454
|
-
if (dag.effectiveStatus &&
|
|
730
|
+
if (dag.effectiveStatus &&
|
|
731
|
+
ACTIVE_EFFECTIVE_STATUSES.has(dag.effectiveStatus)) {
|
|
455
732
|
return true;
|
|
456
733
|
}
|
|
457
734
|
// Only "unknown" (and absent) means liveness evidence is insufficient;
|
|
@@ -472,7 +749,16 @@ function isDagRunActiveForHealth(dag) {
|
|
|
472
749
|
}
|
|
473
750
|
function isNodeStatusActive(status) {
|
|
474
751
|
const normalized = (status ?? "").toLowerCase();
|
|
475
|
-
if ([
|
|
752
|
+
if ([
|
|
753
|
+
"finished",
|
|
754
|
+
"completed",
|
|
755
|
+
"succeeded",
|
|
756
|
+
"done",
|
|
757
|
+
"error",
|
|
758
|
+
"failed",
|
|
759
|
+
"skipped",
|
|
760
|
+
"partial_failed",
|
|
761
|
+
].includes(normalized)) {
|
|
476
762
|
return false;
|
|
477
763
|
}
|
|
478
764
|
return true;
|
|
@@ -511,13 +797,13 @@ function computeDagHealth(dagRuns) {
|
|
|
511
797
|
interruptedRuns++;
|
|
512
798
|
if (dag.stateConsistent === false)
|
|
513
799
|
inconsistentRuns++;
|
|
514
|
-
if (lifecycle !== "completed"
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
800
|
+
if (lifecycle !== "completed" &&
|
|
801
|
+
(lifecycle === "paused" ||
|
|
802
|
+
liveness === "stale" ||
|
|
803
|
+
liveness === "orphaned" ||
|
|
804
|
+
dag.effectiveStatus === "interrupted" ||
|
|
805
|
+
dag.effectiveStatus === "remote-unknown" ||
|
|
806
|
+
dag.stateConsistent === false)) {
|
|
521
807
|
attentionRuns++;
|
|
522
808
|
}
|
|
523
809
|
}
|
|
@@ -547,12 +833,14 @@ function sanitizeProjectionError(error) {
|
|
|
547
833
|
let name;
|
|
548
834
|
let message;
|
|
549
835
|
try {
|
|
550
|
-
name =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
836
|
+
name =
|
|
837
|
+
typeof error.name === "string"
|
|
838
|
+
? error.name
|
|
839
|
+
: undefined;
|
|
840
|
+
message =
|
|
841
|
+
typeof error.message === "string"
|
|
842
|
+
? error.message
|
|
843
|
+
: undefined;
|
|
556
844
|
}
|
|
557
845
|
catch {
|
|
558
846
|
return { message: fallback, at };
|
|
@@ -563,12 +851,12 @@ function sanitizeProjectionError(error) {
|
|
|
563
851
|
}
|
|
564
852
|
// Reject anything that looks like a stack frame, an absolute path, or a secret.
|
|
565
853
|
const raw = String(candidate);
|
|
566
|
-
if (raw.includes("\n")
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
854
|
+
if (raw.includes("\n") ||
|
|
855
|
+
/\bat \S+/i.test(raw) ||
|
|
856
|
+
/[A-Za-z]:\\/.test(raw) ||
|
|
857
|
+
raw.includes("/Users/") ||
|
|
858
|
+
raw.includes("/home/") ||
|
|
859
|
+
/SECRET|TOKEN|PASSWORD|API_KEY/i.test(raw)) {
|
|
572
860
|
return { message: name ? `${name}: 投影失败` : fallback, at };
|
|
573
861
|
}
|
|
574
862
|
const trimmed = raw.trim().slice(0, 200);
|
|
@@ -581,7 +869,11 @@ function mergeArtifactRefs(existing, incoming) {
|
|
|
581
869
|
for (const [key, value] of Object.entries(incoming)) {
|
|
582
870
|
if (!value)
|
|
583
871
|
continue;
|
|
584
|
-
if (key === "runRecordPath" ||
|
|
872
|
+
if (key === "runRecordPath" ||
|
|
873
|
+
key === "dagPath" ||
|
|
874
|
+
key === "reportMarkdown" ||
|
|
875
|
+
key === "doctorMarkdown" ||
|
|
876
|
+
key === "closeoutDraft") {
|
|
585
877
|
merged[key] = value;
|
|
586
878
|
}
|
|
587
879
|
}
|
|
@@ -645,6 +937,7 @@ function mapStateStatus(status) {
|
|
|
645
937
|
case "Running":
|
|
646
938
|
return "running";
|
|
647
939
|
case "Queued":
|
|
940
|
+
case "Ready":
|
|
648
941
|
return "pending";
|
|
649
942
|
case "Done":
|
|
650
943
|
return "succeeded";
|
|
@@ -668,49 +961,86 @@ function mapBatchStatus(status) {
|
|
|
668
961
|
return "unknown";
|
|
669
962
|
}
|
|
670
963
|
}
|
|
964
|
+
/** Per-file hard cap for projection merge (snapshot must not embed unbounded ledger). */
|
|
965
|
+
const SNAPSHOT_EVENTS_MAX_LINES_PER_FILE = 2_000;
|
|
966
|
+
const SNAPSHOT_EVENTS_MAX_BYTES_PER_FILE = 4 * 1024 * 1024;
|
|
967
|
+
/** Global hard cap after dedupe for snapshot projection merge. */
|
|
968
|
+
const SNAPSHOT_EVENTS_MAX_TOTAL = 8_000;
|
|
969
|
+
const SNAPSHOT_LEDGER_MAX_LINES = 10_000;
|
|
970
|
+
const SNAPSHOT_LEDGER_MAX_BYTES = 8 * 1024 * 1024;
|
|
671
971
|
async function loadObservabilityEvents(repoRoot) {
|
|
672
972
|
const events = [];
|
|
973
|
+
const warnings = [];
|
|
673
974
|
const obsRoot = path.join(getTaskPoolRoot(repoRoot), "observability");
|
|
674
|
-
await
|
|
975
|
+
await appendJsonlEventsBounded(path.join(obsRoot, "events.jsonl"), events, warnings);
|
|
675
976
|
const batchesDir = path.join(obsRoot, "batches");
|
|
676
|
-
await forEachSubdirJsonl(batchesDir, "events.jsonl", events);
|
|
977
|
+
await forEachSubdirJsonl(batchesDir, "events.jsonl", events, warnings);
|
|
677
978
|
const runsDir = path.join(obsRoot, "runs");
|
|
678
|
-
await forEachSubdirJsonl(runsDir, "events.jsonl", events);
|
|
679
|
-
|
|
979
|
+
await forEachSubdirJsonl(runsDir, "events.jsonl", events, warnings);
|
|
980
|
+
const deduped = [
|
|
981
|
+
...new Map(events.map((event) => [event.id, event])).values(),
|
|
982
|
+
].sort(compareWorkerEvents);
|
|
983
|
+
if (deduped.length > SNAPSHOT_EVENTS_MAX_TOTAL) {
|
|
984
|
+
warnings.push(`observability event projection truncated to ${SNAPSHOT_EVENTS_MAX_TOTAL} events (capacity bound)`);
|
|
985
|
+
return {
|
|
986
|
+
events: deduped.slice(-SNAPSHOT_EVENTS_MAX_TOTAL),
|
|
987
|
+
warnings,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return { events: deduped, warnings };
|
|
991
|
+
}
|
|
992
|
+
function compareWorkerEvents(a, b) {
|
|
993
|
+
const byTime = a.at.localeCompare(b.at);
|
|
994
|
+
if (byTime !== 0)
|
|
995
|
+
return byTime;
|
|
996
|
+
return a.id.localeCompare(b.id);
|
|
680
997
|
}
|
|
681
|
-
async function forEachSubdirJsonl(parentDir, filename, events) {
|
|
998
|
+
async function forEachSubdirJsonl(parentDir, filename, events, warnings) {
|
|
682
999
|
try {
|
|
683
1000
|
const entries = await readdir(parentDir, { withFileTypes: true });
|
|
684
1001
|
for (const entry of entries) {
|
|
685
1002
|
if (!entry.isDirectory())
|
|
686
1003
|
continue;
|
|
687
|
-
await
|
|
1004
|
+
await appendJsonlEventsBounded(path.join(parentDir, entry.name, filename), events, warnings);
|
|
688
1005
|
}
|
|
689
1006
|
}
|
|
690
1007
|
catch {
|
|
691
1008
|
// skip missing dir
|
|
692
1009
|
}
|
|
693
1010
|
}
|
|
694
|
-
async function
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1011
|
+
async function appendJsonlEventsBounded(filePath, events, warnings) {
|
|
1012
|
+
const tail = await readBoundedJsonlTail(filePath, {
|
|
1013
|
+
maxBytes: SNAPSHOT_EVENTS_MAX_BYTES_PER_FILE,
|
|
1014
|
+
maxLines: SNAPSHOT_EVENTS_MAX_LINES_PER_FILE,
|
|
1015
|
+
});
|
|
1016
|
+
let accepted = 0;
|
|
1017
|
+
for (const line of tail.lines) {
|
|
1018
|
+
const trimmed = line.text.trim();
|
|
700
1019
|
if (!trimmed)
|
|
701
1020
|
continue;
|
|
702
1021
|
const event = parseWorkerEventLine(trimmed);
|
|
703
|
-
if (event)
|
|
1022
|
+
if (event) {
|
|
704
1023
|
events.push(event);
|
|
1024
|
+
accepted += 1;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (tail.truncated) {
|
|
1028
|
+
warnings.push("truncated event tail for " + path.basename(path.dirname(filePath)) + "/" + path.basename(filePath) + " to newest " + accepted + " valid events");
|
|
705
1029
|
}
|
|
706
1030
|
}
|
|
707
1031
|
async function loadLedgerRuns(repoRoot) {
|
|
708
1032
|
const runs = [];
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1033
|
+
const warnings = [];
|
|
1034
|
+
const tail = await readBoundedJsonlTail(path.join(getTaskPoolRoot(repoRoot), "runs.jsonl"), { maxBytes: SNAPSHOT_LEDGER_MAX_BYTES, maxLines: SNAPSHOT_LEDGER_MAX_LINES });
|
|
1035
|
+
if (tail.truncated) {
|
|
1036
|
+
warnings.push("runs.jsonl projection truncated to newest " +
|
|
1037
|
+
SNAPSHOT_LEDGER_MAX_LINES +
|
|
1038
|
+
" lines / " +
|
|
1039
|
+
SNAPSHOT_LEDGER_MAX_BYTES +
|
|
1040
|
+
" bytes");
|
|
1041
|
+
}
|
|
1042
|
+
for (const line of tail.lines) {
|
|
1043
|
+
const trimmed = line.text.trim();
|
|
714
1044
|
if (!trimmed)
|
|
715
1045
|
continue;
|
|
716
1046
|
const parsed = safeParseJson(trimmed);
|
|
@@ -724,6 +1054,7 @@ async function loadLedgerRuns(repoRoot) {
|
|
|
724
1054
|
continue;
|
|
725
1055
|
const failure = readObject(parsed, "failure");
|
|
726
1056
|
const failureArtifacts = readObject(parsed, "failureArtifacts");
|
|
1057
|
+
const retryOfWorkerRunId = readString(parsed, "retryOfWorkerRunId");
|
|
727
1058
|
runs.push({
|
|
728
1059
|
batchRunId,
|
|
729
1060
|
workerRunId,
|
|
@@ -734,8 +1065,11 @@ async function loadLedgerRuns(repoRoot) {
|
|
|
734
1065
|
runRecordPath: readString(parsed, "runRecordPath"),
|
|
735
1066
|
dagPath: readString(parsed, "dagPath"),
|
|
736
1067
|
recordedAt: readString(parsed, "recordedAt"),
|
|
1068
|
+
...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
|
|
737
1069
|
failureCategory: failure ? readString(failure, "category") : undefined,
|
|
738
|
-
recommendedFollowUp: failure
|
|
1070
|
+
recommendedFollowUp: failure
|
|
1071
|
+
? readString(failure, "recommendedFollowUpKind")
|
|
1072
|
+
: undefined,
|
|
739
1073
|
failureArtifacts: failureArtifacts
|
|
740
1074
|
? {
|
|
741
1075
|
reportMarkdown: readString(failureArtifacts, "reportMarkdownArtifactPath"),
|
|
@@ -745,7 +1079,7 @@ async function loadLedgerRuns(repoRoot) {
|
|
|
745
1079
|
: undefined,
|
|
746
1080
|
});
|
|
747
1081
|
}
|
|
748
|
-
return runs;
|
|
1082
|
+
return { runs, warnings };
|
|
749
1083
|
}
|
|
750
1084
|
async function loadLedgerBatches(repoRoot) {
|
|
751
1085
|
const batches = [];
|
|
@@ -761,20 +1095,26 @@ async function loadLedgerBatches(repoRoot) {
|
|
|
761
1095
|
if (!parsed)
|
|
762
1096
|
continue;
|
|
763
1097
|
const summaryObj = readObject(parsed, "summary");
|
|
1098
|
+
let summary;
|
|
1099
|
+
if (summaryObj) {
|
|
1100
|
+
const recordErrors = readNumber(summaryObj, "recordErrors");
|
|
1101
|
+
const runErrors = readNumber(summaryObj, "runErrors");
|
|
1102
|
+
summary = {
|
|
1103
|
+
total: readNumber(summaryObj, "total") ?? 0,
|
|
1104
|
+
succeeded: readNumber(summaryObj, "succeeded") ?? 0,
|
|
1105
|
+
failed: readNumber(summaryObj, "failed") ?? 0,
|
|
1106
|
+
reused: readNumber(summaryObj, "reused") ?? 0,
|
|
1107
|
+
...(recordErrors !== undefined ? { recordErrors } : {}),
|
|
1108
|
+
...(runErrors !== undefined ? { runErrors } : {}),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
764
1111
|
batches.push({
|
|
765
1112
|
batchRunId: readString(parsed, "batchRunId") ?? batchRunId,
|
|
766
1113
|
featureId: readString(parsed, "featureId"),
|
|
767
1114
|
startedAt: readString(parsed, "startedAt"),
|
|
768
1115
|
finishedAt: readString(parsed, "finishedAt"),
|
|
769
1116
|
status: mapBatchStatus(readString(parsed, "status") ?? "unknown"),
|
|
770
|
-
summary
|
|
771
|
-
? {
|
|
772
|
-
total: readNumber(summaryObj, "total") ?? 0,
|
|
773
|
-
succeeded: readNumber(summaryObj, "succeeded") ?? 0,
|
|
774
|
-
failed: readNumber(summaryObj, "failed") ?? 0,
|
|
775
|
-
reused: readNumber(summaryObj, "reused") ?? 0,
|
|
776
|
-
}
|
|
777
|
-
: undefined,
|
|
1117
|
+
summary,
|
|
778
1118
|
tasks: [],
|
|
779
1119
|
batchRunPath,
|
|
780
1120
|
});
|
|
@@ -787,34 +1127,88 @@ async function loadLedgerBatches(repoRoot) {
|
|
|
787
1127
|
}
|
|
788
1128
|
async function loadStateRecords(repoRoot) {
|
|
789
1129
|
const states = [];
|
|
790
|
-
const
|
|
1130
|
+
const legacyCandidates = [];
|
|
1131
|
+
const warnings = [];
|
|
1132
|
+
// Primary path: v2 nested states via pool list API (read-only import).
|
|
791
1133
|
try {
|
|
792
|
-
const
|
|
793
|
-
for (const
|
|
794
|
-
if (!
|
|
1134
|
+
const v2 = await listTaskPoolStates(repoRoot);
|
|
1135
|
+
for (const record of v2) {
|
|
1136
|
+
if (!record.taskId || !record.status)
|
|
795
1137
|
continue;
|
|
796
|
-
|
|
797
|
-
|
|
1138
|
+
if (!record.featureId) {
|
|
1139
|
+
warnings.push(`v2 state for task ${record.taskId} missing featureId; excluded from composite projection`);
|
|
798
1140
|
continue;
|
|
799
|
-
|
|
1141
|
+
}
|
|
1142
|
+
states.push({
|
|
1143
|
+
taskId: record.taskId,
|
|
1144
|
+
featureId: record.featureId,
|
|
1145
|
+
status: record.status,
|
|
1146
|
+
workerRunId: record.workerRunId,
|
|
1147
|
+
lastRunRecordPath: record.lastRunRecordPath,
|
|
1148
|
+
updatedAt: record.updatedAt,
|
|
1149
|
+
failureCategory: record.failure?.category,
|
|
1150
|
+
recommendedFollowUp: record.failure?.recommendedFollowUpKind,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
warnings.push(`failed to load feature-scoped task pool states: ${error instanceof Error ? error.message : String(error)}`);
|
|
1156
|
+
}
|
|
1157
|
+
// Compatibility: legacy flat states/<taskId>.json (warning + unique-only merge later).
|
|
1158
|
+
try {
|
|
1159
|
+
const legacyFiles = await listLegacyStateFiles(repoRoot);
|
|
1160
|
+
for (const file of legacyFiles) {
|
|
1161
|
+
const parsed = await safeReadJson(file.path);
|
|
1162
|
+
if (!parsed) {
|
|
1163
|
+
warnings.push(`legacy flat state file unreadable or corrupt: ${file.path}`);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const taskId = readString(parsed, "taskId") ?? file.taskId;
|
|
800
1167
|
const status = readString(parsed, "status");
|
|
801
|
-
if (!status)
|
|
1168
|
+
if (!taskId || !status) {
|
|
1169
|
+
warnings.push(`legacy flat state missing taskId/status: ${file.path}`);
|
|
802
1170
|
continue;
|
|
1171
|
+
}
|
|
803
1172
|
const failure = readObject(parsed, "failure");
|
|
804
|
-
|
|
1173
|
+
const embeddedFeatureId = readString(parsed, "featureId");
|
|
1174
|
+
if (embeddedFeatureId) {
|
|
1175
|
+
// Rare: flat file already carries featureId — treat as owned state.
|
|
1176
|
+
states.push({
|
|
1177
|
+
taskId,
|
|
1178
|
+
featureId: embeddedFeatureId,
|
|
1179
|
+
status,
|
|
1180
|
+
workerRunId: readString(parsed, "workerRunId"),
|
|
1181
|
+
lastRunRecordPath: readString(parsed, "lastRunRecordPath"),
|
|
1182
|
+
updatedAt: readString(parsed, "updatedAt"),
|
|
1183
|
+
failureCategory: failure
|
|
1184
|
+
? readString(failure, "category")
|
|
1185
|
+
: undefined,
|
|
1186
|
+
recommendedFollowUp: failure
|
|
1187
|
+
? readString(failure, "recommendedFollowUpKind")
|
|
1188
|
+
: undefined,
|
|
1189
|
+
legacyFlat: true,
|
|
1190
|
+
});
|
|
1191
|
+
warnings.push(`legacy flat state for task ${taskId} carries featureId ${embeddedFeatureId}`);
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
legacyCandidates.push({
|
|
805
1195
|
taskId,
|
|
806
1196
|
status,
|
|
807
1197
|
workerRunId: readString(parsed, "workerRunId"),
|
|
808
1198
|
lastRunRecordPath: readString(parsed, "lastRunRecordPath"),
|
|
1199
|
+
updatedAt: readString(parsed, "updatedAt"),
|
|
809
1200
|
failureCategory: failure ? readString(failure, "category") : undefined,
|
|
810
|
-
recommendedFollowUp: failure
|
|
1201
|
+
recommendedFollowUp: failure
|
|
1202
|
+
? readString(failure, "recommendedFollowUpKind")
|
|
1203
|
+
: undefined,
|
|
1204
|
+
legacyFlat: true,
|
|
811
1205
|
});
|
|
812
1206
|
}
|
|
813
1207
|
}
|
|
814
|
-
catch {
|
|
815
|
-
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
warnings.push(`failed to scan legacy flat states: ${error instanceof Error ? error.message : String(error)}`);
|
|
816
1210
|
}
|
|
817
|
-
return states;
|
|
1211
|
+
return { states, legacyCandidates, warnings };
|
|
818
1212
|
}
|
|
819
1213
|
async function loadFailureHandoffs(repoRoot) {
|
|
820
1214
|
const handoffs = [];
|
|
@@ -858,7 +1252,9 @@ function mergeDagRun(existing, incoming) {
|
|
|
858
1252
|
return incoming;
|
|
859
1253
|
const startedAt = incoming.startedAt ?? existing.startedAt;
|
|
860
1254
|
const finishedAt = incoming.finishedAt ?? existing.finishedAt;
|
|
861
|
-
const ranks = incoming.ranks && incoming.ranks.length > 0
|
|
1255
|
+
const ranks = incoming.ranks && incoming.ranks.length > 0
|
|
1256
|
+
? incoming.ranks
|
|
1257
|
+
: existing.ranks;
|
|
862
1258
|
const durationMs = incoming.durationMs ??
|
|
863
1259
|
existing.durationMs ??
|
|
864
1260
|
computeDurationMs(startedAt, finishedAt);
|
|
@@ -877,7 +1273,9 @@ function mergeDagRun(existing, incoming) {
|
|
|
877
1273
|
...(ranks && ranks.length > 0 ? { ranks } : {}),
|
|
878
1274
|
nodes: mergeDagNodes(existing.nodes, incoming.nodes),
|
|
879
1275
|
edges: incoming.edges.length > 0 ? incoming.edges : existing.edges,
|
|
880
|
-
...(incoming.dagPath ?? existing.dagPath
|
|
1276
|
+
...((incoming.dagPath ?? existing.dagPath)
|
|
1277
|
+
? { dagPath: incoming.dagPath ?? existing.dagPath }
|
|
1278
|
+
: {}),
|
|
881
1279
|
};
|
|
882
1280
|
}
|
|
883
1281
|
function mergeDagNodes(existing, incoming) {
|
|
@@ -921,7 +1319,9 @@ function isFailedNodeStatus(status) {
|
|
|
921
1319
|
if (!status)
|
|
922
1320
|
return false;
|
|
923
1321
|
const normalized = status.toLowerCase();
|
|
924
|
-
return normalized === "error" ||
|
|
1322
|
+
return (normalized === "error" ||
|
|
1323
|
+
normalized === "failed" ||
|
|
1324
|
+
normalized === "partial_failed");
|
|
925
1325
|
}
|
|
926
1326
|
function nodeOutputPreview(node) {
|
|
927
1327
|
const stdout = readString(node, "stdout") ?? readString(node, "assistantText");
|
|
@@ -997,13 +1397,19 @@ async function loadDagEventFiles(repoRoot) {
|
|
|
997
1397
|
const outputPreview = readString(parsed, "outputPreview");
|
|
998
1398
|
nodeMap.set(nodeId, {
|
|
999
1399
|
nodeId,
|
|
1000
|
-
...(rank ?? prev.rank ? { rank: rank ?? prev.rank } : {}),
|
|
1001
|
-
...(executor ?? prev.executor
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
...(
|
|
1005
|
-
...(
|
|
1006
|
-
...(
|
|
1400
|
+
...((rank ?? prev.rank) ? { rank: rank ?? prev.rank } : {}),
|
|
1401
|
+
...((executor ?? prev.executor)
|
|
1402
|
+
? { executor: executor ?? prev.executor }
|
|
1403
|
+
: {}),
|
|
1404
|
+
...((model ?? prev.model) ? { model: model ?? prev.model } : {}),
|
|
1405
|
+
...((label ?? prev.label) ? { label: label ?? prev.label } : {}),
|
|
1406
|
+
...((status ?? prev.status) ? { status: status ?? prev.status } : {}),
|
|
1407
|
+
...((durationMs ?? prev.durationMs)
|
|
1408
|
+
? { durationMs: durationMs ?? prev.durationMs }
|
|
1409
|
+
: {}),
|
|
1410
|
+
...((outputPreview ?? prev.outputPreview)
|
|
1411
|
+
? { outputPreview: outputPreview ?? prev.outputPreview }
|
|
1412
|
+
: {}),
|
|
1007
1413
|
});
|
|
1008
1414
|
byId.set(dagRunId, nodeMap);
|
|
1009
1415
|
}
|
|
@@ -1059,7 +1465,9 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1059
1465
|
return undefined;
|
|
1060
1466
|
const runDir = path.dirname(statePath);
|
|
1061
1467
|
const lifecycleName = path.basename(path.dirname(runDir));
|
|
1062
|
-
const lifecycle = lifecycleName === "active" ||
|
|
1468
|
+
const lifecycle = lifecycleName === "active" ||
|
|
1469
|
+
lifecycleName === "paused" ||
|
|
1470
|
+
lifecycleName === "completed"
|
|
1063
1471
|
? lifecycleName
|
|
1064
1472
|
: undefined;
|
|
1065
1473
|
const dagRunId = readString(parsed, "runId") ?? path.basename(runDir);
|
|
@@ -1077,14 +1485,29 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1077
1485
|
const state = parsed;
|
|
1078
1486
|
const liveness = assessDagRunLiveness({ state, now });
|
|
1079
1487
|
const effectiveStatus = lifecycle
|
|
1080
|
-
? deriveDagRunEffectiveStatus({
|
|
1488
|
+
? deriveDagRunEffectiveStatus({
|
|
1489
|
+
lifecycle,
|
|
1490
|
+
state,
|
|
1491
|
+
liveness: liveness.status,
|
|
1492
|
+
})
|
|
1081
1493
|
: "unknown";
|
|
1082
1494
|
const stateConsistent = lifecycle === undefined
|
|
1083
1495
|
? false
|
|
1084
|
-
: !((lifecycle === "paused" && state.status !== "paused")
|
|
1085
|
-
|
|
1496
|
+
: !((lifecycle === "paused" && state.status !== "paused") ||
|
|
1497
|
+
(lifecycle === "completed" &&
|
|
1498
|
+
![
|
|
1499
|
+
"finished",
|
|
1500
|
+
"failed",
|
|
1501
|
+
"partial_failed",
|
|
1502
|
+
"superseded",
|
|
1503
|
+
"abandoned",
|
|
1504
|
+
].includes(state.status)));
|
|
1086
1505
|
const recoveryEligibility = lifecycle
|
|
1087
|
-
? assessDagRunRecoveryEligibility({
|
|
1506
|
+
? assessDagRunRecoveryEligibility({
|
|
1507
|
+
lifecycle,
|
|
1508
|
+
state,
|
|
1509
|
+
liveness: liveness.status,
|
|
1510
|
+
})
|
|
1088
1511
|
: undefined;
|
|
1089
1512
|
return {
|
|
1090
1513
|
dagRunId,
|
|
@@ -1122,7 +1545,9 @@ async function parseDagEdges(runPath, nodes) {
|
|
|
1122
1545
|
if (!taskId || !knownNodeIds.has(taskId))
|
|
1123
1546
|
continue;
|
|
1124
1547
|
const dependencies = readStringArray(task, "depends_on");
|
|
1125
|
-
const compatibleDependencies = dependencies.length > 0
|
|
1548
|
+
const compatibleDependencies = dependencies.length > 0
|
|
1549
|
+
? dependencies
|
|
1550
|
+
: readStringArray(task, "dependsOn");
|
|
1126
1551
|
for (const dependencyId of compatibleDependencies) {
|
|
1127
1552
|
if (dependencyId === taskId || !knownNodeIds.has(dependencyId))
|
|
1128
1553
|
continue;
|
|
@@ -1169,7 +1594,10 @@ async function loadDagNodeModels(runPath) {
|
|
|
1169
1594
|
const legacy = raw;
|
|
1170
1595
|
const tasks = legacy.tasks;
|
|
1171
1596
|
const executorModels = legacy.executorModels;
|
|
1172
|
-
if (!Array.isArray(tasks) ||
|
|
1597
|
+
if (!Array.isArray(tasks) ||
|
|
1598
|
+
typeof executorModels !== "object" ||
|
|
1599
|
+
executorModels === null ||
|
|
1600
|
+
Array.isArray(executorModels)) {
|
|
1173
1601
|
return models;
|
|
1174
1602
|
}
|
|
1175
1603
|
for (const task of tasks) {
|
|
@@ -1194,7 +1622,9 @@ async function loadDagNodeModels(runPath) {
|
|
|
1194
1622
|
function parseDagNodes(parsed, rankByNode, modelByNode) {
|
|
1195
1623
|
const nodes = [];
|
|
1196
1624
|
const nodesValue = parsed.nodes;
|
|
1197
|
-
if (nodesValue &&
|
|
1625
|
+
if (nodesValue &&
|
|
1626
|
+
typeof nodesValue === "object" &&
|
|
1627
|
+
!Array.isArray(nodesValue)) {
|
|
1198
1628
|
for (const [nodeId, nodeVal] of Object.entries(nodesValue)) {
|
|
1199
1629
|
if (!nodeVal || typeof nodeVal !== "object" || Array.isArray(nodeVal))
|
|
1200
1630
|
continue;
|
|
@@ -1261,7 +1691,9 @@ async function safeReadJson(filePath) {
|
|
|
1261
1691
|
function safeParseJson(raw) {
|
|
1262
1692
|
try {
|
|
1263
1693
|
const parsed = JSON.parse(raw);
|
|
1264
|
-
if (parsed === null ||
|
|
1694
|
+
if (parsed === null ||
|
|
1695
|
+
typeof parsed !== "object" ||
|
|
1696
|
+
Array.isArray(parsed)) {
|
|
1265
1697
|
return undefined;
|
|
1266
1698
|
}
|
|
1267
1699
|
return parsed;
|
|
@@ -1304,7 +1736,9 @@ function readNumber(value, key) {
|
|
|
1304
1736
|
if (!value || typeof value !== "object")
|
|
1305
1737
|
return undefined;
|
|
1306
1738
|
const child = value[key];
|
|
1307
|
-
return typeof child === "number" && Number.isFinite(child)
|
|
1739
|
+
return typeof child === "number" && Number.isFinite(child)
|
|
1740
|
+
? child
|
|
1741
|
+
: undefined;
|
|
1308
1742
|
}
|
|
1309
1743
|
function readStringMatrix(value, key) {
|
|
1310
1744
|
if (!value || typeof value !== "object")
|