@tea-agent/loop-agent 0.7.5 → 0.9.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/AGENTS.md +145 -142
- package/CHANGELOG.md +161 -158
- package/README.md +206 -204
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/args.js +9 -2
- package/dist/commands/init.js +518 -488
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/cursor-executor.js +1 -1
- package/dist/executors/shell-executor.js +74 -8
- package/dist/governance/manifest-types.js +1 -1
- package/dist/shared/reference-context.js +48 -22
- package/dist/task/config-types.js +1 -1
- package/dist/task/runtime.js +28 -28
- package/dist/worker/cli.js +3 -3
- package/dist/worker/loop-agent/loop-agent-client.js +51 -10
- package/dist/worker/observability/event-store.js +2 -1
- package/dist/worker/observability/read-model.js +13 -11
- package/dist/worker/observe/paths.js +2 -2
- package/dist/worker/observe/routes.js +82 -23
- package/dist/worker/observe/server.js +8 -6
- package/dist/worker/observe/static/app.js +1965 -1480
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/index.html +88 -63
- package/dist/worker/observe/static/styles.css +598 -722
- package/dist/worker/pool/run-store.js +7 -8
- package/dist/worker/run-task/run-task.js +11 -2
- package/dist/worker/runner/run-ready.js +1 -1
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/docs/README.md +84 -79
- package/docs/agent-dag-recovery-playbook.md +184 -184
- package/docs/agent-dag-runner.md +42 -42
- package/docs/architecture/runtime-boundaries.md +162 -162
- package/docs/cursor-executor-usage.md +25 -25
- package/docs/decisions/README.md +3 -3
- package/docs/design/README.md +49 -49
- package/docs/development-principles.md +73 -73
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +11 -11
- package/docs/exec-plans/completed/README.md +43 -34
- package/docs/feature-workflow.md +187 -187
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +245 -241
- package/docs/loop-agent-harness.md +63 -55
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +3 -3
- package/docs/reports/README.md +12 -9
- package/docs/skills/README.md +6 -6
- package/docs/skills/vetted-skill-registry.md +26 -26
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +454 -454
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +316 -316
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/harness.schema.json +218 -0
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/init-evolution-review.md +33 -33
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/product-line/AGENTS.md +8 -8
- package/docs/templates/product-line/README.md +9 -9
- package/docs/templates/product-line/acceptance.yaml +14 -14
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/task-graph.yaml +15 -15
- package/docs/templates/product-line/task.yaml +65 -65
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +52 -52
- package/docs/templates/worker-dogfood-setup.md +48 -48
- package/docs/verification-matrix.md +49 -49
- package/examples/decision-gate-agent-dag.json +123 -123
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +73 -71
- package/package.json +68 -67
- package/scripts/check-product-line-docs.sh +22 -22
- package/scripts/check-task-pool-root.sh +32 -0
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/init-capability-evolution/SKILL.md +69 -69
- package/skills/loop-agent/SKILL.md +149 -149
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +432 -412
- package/skills/loop-agent/references/harness-policy.md +263 -263
- package/skills/loop-agent/references/hybrid-dag.md +216 -216
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +59 -59
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -89
- package/skills/loop-agent/references/verification-and-failure-handling.md +133 -128
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -3,6 +3,7 @@ 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 { getTaskPoolRoot } from "../pool/run-store.js";
|
|
6
7
|
const ACTIVE_TASK_STATUSES = new Set(["running", "pending"]);
|
|
7
8
|
const ACTIVE_BATCH_STATUSES = new Set(["running", "pending"]);
|
|
8
9
|
export async function buildGlobalSnapshot(options) {
|
|
@@ -474,7 +475,7 @@ function mapBatchStatus(status) {
|
|
|
474
475
|
}
|
|
475
476
|
async function loadObservabilityEvents(repoRoot) {
|
|
476
477
|
const events = [];
|
|
477
|
-
const obsRoot = path.join(repoRoot, "
|
|
478
|
+
const obsRoot = path.join(getTaskPoolRoot(repoRoot), "observability");
|
|
478
479
|
await appendJsonlEvents(path.join(obsRoot, "events.jsonl"), events);
|
|
479
480
|
const batchesDir = path.join(obsRoot, "batches");
|
|
480
481
|
await forEachSubdirJsonl(batchesDir, "events.jsonl", events);
|
|
@@ -510,7 +511,7 @@ async function appendJsonlEvents(filePath, events) {
|
|
|
510
511
|
}
|
|
511
512
|
async function loadLedgerRuns(repoRoot) {
|
|
512
513
|
const runs = [];
|
|
513
|
-
const raw = await safeReadText(path.join(repoRoot, "
|
|
514
|
+
const raw = await safeReadText(path.join(getTaskPoolRoot(repoRoot), "runs.jsonl"));
|
|
514
515
|
if (!raw)
|
|
515
516
|
return runs;
|
|
516
517
|
for (const line of raw.split("\n")) {
|
|
@@ -553,14 +554,15 @@ async function loadLedgerRuns(repoRoot) {
|
|
|
553
554
|
}
|
|
554
555
|
async function loadLedgerBatches(repoRoot) {
|
|
555
556
|
const batches = [];
|
|
556
|
-
const
|
|
557
|
+
const artifactsDir = path.join(getTaskPoolRoot(repoRoot), "artifacts");
|
|
557
558
|
try {
|
|
558
|
-
const entries = await readdir(
|
|
559
|
+
const entries = await readdir(artifactsDir, { withFileTypes: true });
|
|
559
560
|
for (const entry of entries) {
|
|
560
|
-
if (!entry.
|
|
561
|
+
if (!entry.isDirectory())
|
|
561
562
|
continue;
|
|
562
|
-
const batchRunId = entry.name
|
|
563
|
-
const
|
|
563
|
+
const batchRunId = entry.name;
|
|
564
|
+
const batchRunPath = path.join(artifactsDir, batchRunId, "batch-run.json");
|
|
565
|
+
const parsed = await safeReadJson(batchRunPath);
|
|
564
566
|
if (!parsed)
|
|
565
567
|
continue;
|
|
566
568
|
const summaryObj = readObject(parsed, "summary");
|
|
@@ -579,7 +581,7 @@ async function loadLedgerBatches(repoRoot) {
|
|
|
579
581
|
}
|
|
580
582
|
: undefined,
|
|
581
583
|
tasks: [],
|
|
582
|
-
batchRunPath
|
|
584
|
+
batchRunPath,
|
|
583
585
|
});
|
|
584
586
|
}
|
|
585
587
|
}
|
|
@@ -590,7 +592,7 @@ async function loadLedgerBatches(repoRoot) {
|
|
|
590
592
|
}
|
|
591
593
|
async function loadStateRecords(repoRoot) {
|
|
592
594
|
const states = [];
|
|
593
|
-
const stateDir = path.join(repoRoot, "
|
|
595
|
+
const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
|
|
594
596
|
try {
|
|
595
597
|
const entries = await readdir(stateDir, { withFileTypes: true });
|
|
596
598
|
for (const entry of entries) {
|
|
@@ -621,7 +623,7 @@ async function loadStateRecords(repoRoot) {
|
|
|
621
623
|
}
|
|
622
624
|
async function loadFailureHandoffs(repoRoot) {
|
|
623
625
|
const handoffs = [];
|
|
624
|
-
const handoffDir = path.join(repoRoot, "
|
|
626
|
+
const handoffDir = path.join(getTaskPoolRoot(repoRoot), "failure-handoffs");
|
|
625
627
|
try {
|
|
626
628
|
const entries = await readdir(handoffDir, { withFileTypes: true });
|
|
627
629
|
for (const entry of entries) {
|
|
@@ -736,7 +738,7 @@ function nodeErrorPreview(node, status) {
|
|
|
736
738
|
return truncateUtf8Preview(stderr);
|
|
737
739
|
}
|
|
738
740
|
async function loadDagEventFiles(repoRoot) {
|
|
739
|
-
const runsDir = path.join(repoRoot, "
|
|
741
|
+
const runsDir = path.join(getTaskPoolRoot(repoRoot), "observability", "runs");
|
|
740
742
|
let entries;
|
|
741
743
|
try {
|
|
742
744
|
entries = await readdir(runsDir, { withFileTypes: true });
|
|
@@ -33,8 +33,8 @@ export function resolveArtifactPath(repoRoot, relative) {
|
|
|
33
33
|
function isAllowedArtifactRelativePath(relative) {
|
|
34
34
|
const normalized = path.normalize(relative);
|
|
35
35
|
const segments = normalized.split(path.sep).filter(Boolean);
|
|
36
|
-
if (segments[0] === ".task-pool") {
|
|
37
|
-
return ["artifacts", "failure-handoffs", "reports"].includes(segments[
|
|
36
|
+
if (segments[0] === ".harness" && segments[1] === "task-pool") {
|
|
37
|
+
return ["artifacts", "failure-handoffs", "reports"].includes(segments[2] ?? "");
|
|
38
38
|
}
|
|
39
39
|
if (segments[0] === ".harness" && segments[1] === "tasks") {
|
|
40
40
|
return segments.includes("artifacts");
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseWorkerEventLine } from "../observability/events.js";
|
|
5
5
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
6
6
|
import { buildGlobalSnapshot } from "../observability/read-model.js";
|
|
7
|
+
import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
7
8
|
import { resolveArtifactPath, toRepoRelativeArtifactPath, } from "./paths.js";
|
|
9
|
+
export function createObserveSnapshotCache() {
|
|
10
|
+
return { expiresAt: 0 };
|
|
11
|
+
}
|
|
8
12
|
const ROUTES = [
|
|
9
13
|
{ method: "GET", pattern: /^\/api\/health$/, handler: handleHealth },
|
|
10
14
|
{ method: "GET", pattern: /^\/api\/snapshot$/, handler: handleSnapshot },
|
|
@@ -98,16 +102,38 @@ async function handleHealth(_req, res, _match, ctx) {
|
|
|
98
102
|
generatedAt: new Date().toISOString(),
|
|
99
103
|
});
|
|
100
104
|
}
|
|
105
|
+
async function getSnapshot(ctx) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (ctx.snapshotCache.snapshot && now < ctx.snapshotCache.expiresAt) {
|
|
108
|
+
return ctx.snapshotCache.snapshot;
|
|
109
|
+
}
|
|
110
|
+
if (ctx.snapshotCache.inFlight) {
|
|
111
|
+
return ctx.snapshotCache.inFlight;
|
|
112
|
+
}
|
|
113
|
+
const inFlight = buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
|
|
114
|
+
ctx.snapshotCache.inFlight = inFlight;
|
|
115
|
+
try {
|
|
116
|
+
const snapshot = await inFlight;
|
|
117
|
+
ctx.snapshotCache.snapshot = snapshot;
|
|
118
|
+
ctx.snapshotCache.expiresAt = Date.now() + ctx.pollIntervalMs;
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (ctx.snapshotCache.inFlight === inFlight) {
|
|
123
|
+
ctx.snapshotCache.inFlight = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
101
127
|
async function handleSnapshot(_req, res, _match, ctx) {
|
|
102
|
-
const snapshot = await
|
|
128
|
+
const snapshot = await getSnapshot(ctx);
|
|
103
129
|
sendJson(res, 200, snapshot);
|
|
104
130
|
}
|
|
105
131
|
async function handleBatches(_req, res, _match, ctx) {
|
|
106
|
-
const snapshot = await
|
|
132
|
+
const snapshot = await getSnapshot(ctx);
|
|
107
133
|
sendJson(res, 200, snapshot.batches);
|
|
108
134
|
}
|
|
109
135
|
async function handleBatchById(_req, res, match, ctx) {
|
|
110
|
-
const snapshot = await
|
|
136
|
+
const snapshot = await getSnapshot(ctx);
|
|
111
137
|
const batch = snapshot.batches.find((b) => b.batchRunId === match.params.id);
|
|
112
138
|
if (!batch) {
|
|
113
139
|
sendJson(res, 404, { error: "Batch not found" });
|
|
@@ -116,11 +142,11 @@ async function handleBatchById(_req, res, match, ctx) {
|
|
|
116
142
|
sendJson(res, 200, batch);
|
|
117
143
|
}
|
|
118
144
|
async function handleTasks(_req, res, _match, ctx) {
|
|
119
|
-
const snapshot = await
|
|
145
|
+
const snapshot = await getSnapshot(ctx);
|
|
120
146
|
sendJson(res, 200, snapshot.tasks);
|
|
121
147
|
}
|
|
122
148
|
async function handleTaskById(_req, res, match, ctx) {
|
|
123
|
-
const snapshot = await
|
|
149
|
+
const snapshot = await getSnapshot(ctx);
|
|
124
150
|
const task = snapshot.tasks.find((t) => t.taskId === match.params.id);
|
|
125
151
|
if (!task) {
|
|
126
152
|
sendJson(res, 404, { error: "Task not found" });
|
|
@@ -133,7 +159,7 @@ async function handleRunById(_req, res, match, ctx) {
|
|
|
133
159
|
sendJson(res, 400, { error: "Invalid run identifier" });
|
|
134
160
|
return;
|
|
135
161
|
}
|
|
136
|
-
const snapshot = await
|
|
162
|
+
const snapshot = await getSnapshot(ctx);
|
|
137
163
|
const task = findRunByWorkerRunId(snapshot.tasks, snapshot.batches, match.params.id);
|
|
138
164
|
if (!task) {
|
|
139
165
|
sendJson(res, 404, { error: "Run not found" });
|
|
@@ -148,7 +174,7 @@ async function handleRunEvents(_req, res, match, ctx) {
|
|
|
148
174
|
return;
|
|
149
175
|
}
|
|
150
176
|
const after = parseNonNegativeInt(match.query.get("after"), 0);
|
|
151
|
-
const eventsPath = path.join(ctx.repoRoot, "
|
|
177
|
+
const eventsPath = path.join(getTaskPoolRoot(ctx.repoRoot), "observability", "runs", workerRunId, "events.jsonl");
|
|
152
178
|
const events = (await readJsonlLines(eventsPath, after)).map((event) => normalizeEventArtifactRefs(ctx.repoRoot, event));
|
|
153
179
|
sendJson(res, 200, { events });
|
|
154
180
|
}
|
|
@@ -158,7 +184,7 @@ async function handleRunArtifacts(_req, res, match, ctx) {
|
|
|
158
184
|
sendJson(res, 400, { error: "Invalid run identifier" });
|
|
159
185
|
return;
|
|
160
186
|
}
|
|
161
|
-
const snapshot = await
|
|
187
|
+
const snapshot = await getSnapshot(ctx);
|
|
162
188
|
const task = findRunByWorkerRunId(snapshot.tasks, snapshot.batches, workerRunId);
|
|
163
189
|
const artifacts = [];
|
|
164
190
|
const seen = new Set();
|
|
@@ -170,7 +196,7 @@ async function handleRunArtifacts(_req, res, match, ctx) {
|
|
|
170
196
|
artifacts.push({ path: artifactPath, label });
|
|
171
197
|
}
|
|
172
198
|
}
|
|
173
|
-
const eventsPath = path.join(ctx.repoRoot, "
|
|
199
|
+
const eventsPath = path.join(getTaskPoolRoot(ctx.repoRoot), "observability", "runs", workerRunId, "events.jsonl");
|
|
174
200
|
const runEvents = await readJsonlLines(eventsPath, 0);
|
|
175
201
|
for (const event of runEvents) {
|
|
176
202
|
const parsed = event;
|
|
@@ -189,7 +215,7 @@ async function handleRunArtifacts(_req, res, match, ctx) {
|
|
|
189
215
|
sendJson(res, 200, { artifacts });
|
|
190
216
|
}
|
|
191
217
|
async function handleDagRunById(_req, res, match, ctx) {
|
|
192
|
-
const snapshot = await
|
|
218
|
+
const snapshot = await getSnapshot(ctx);
|
|
193
219
|
const dagRun = snapshot.dagRuns.find((d) => d.dagRunId === match.params.id);
|
|
194
220
|
if (!dagRun) {
|
|
195
221
|
sendJson(res, 404, { error: "DAG run not found" });
|
|
@@ -205,7 +231,7 @@ async function handleDagNodeSessionEvents(_req, res, match, ctx) {
|
|
|
205
231
|
return;
|
|
206
232
|
}
|
|
207
233
|
const after = parseNonNegativeInt(match.query.get("after"), 0);
|
|
208
|
-
const snapshot = await
|
|
234
|
+
const snapshot = await getSnapshot(ctx);
|
|
209
235
|
const dagRun = snapshot.dagRuns.find((d) => d.dagRunId === dagRunId);
|
|
210
236
|
const eventsPath = resolveSessionEventsPath(ctx.repoRoot, dagRunId, nodeId, dagRun?.dagPath);
|
|
211
237
|
if (!eventsPath) {
|
|
@@ -254,12 +280,32 @@ async function handleEventStream(req, res, match, ctx) {
|
|
|
254
280
|
"Cache-Control": "no-cache",
|
|
255
281
|
Connection: "keep-alive",
|
|
256
282
|
});
|
|
257
|
-
const observabilityRoot = path.join(ctx.repoRoot, "
|
|
283
|
+
const observabilityRoot = path.join(getTaskPoolRoot(ctx.repoRoot), "observability");
|
|
258
284
|
const eventsPath = path.join(observabilityRoot, "events.jsonl");
|
|
259
285
|
let offset = 0;
|
|
286
|
+
let knownFileSize;
|
|
287
|
+
let timer;
|
|
288
|
+
let closed = false;
|
|
260
289
|
const sendMatchingEvents = async (initial) => {
|
|
290
|
+
let currentFileSize;
|
|
291
|
+
try {
|
|
292
|
+
currentFileSize = (await stat(eventsPath)).size;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
knownFileSize = undefined;
|
|
296
|
+
offset = 0;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!shouldReadSseEventFile(knownFileSize, currentFileSize))
|
|
300
|
+
return;
|
|
301
|
+
if (knownFileSize !== undefined && currentFileSize < knownFileSize) {
|
|
302
|
+
offset = 0;
|
|
303
|
+
}
|
|
261
304
|
const page = await readFilteredEvents(eventsPath, offset, batchRunId, ctx.repoRoot);
|
|
305
|
+
if (!page.ok)
|
|
306
|
+
return;
|
|
262
307
|
offset = page.nextOffset;
|
|
308
|
+
knownFileSize = currentFileSize;
|
|
263
309
|
for (const event of page.events) {
|
|
264
310
|
if (initial || !res.writableEnded) {
|
|
265
311
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
@@ -267,15 +313,25 @@ async function handleEventStream(req, res, match, ctx) {
|
|
|
267
313
|
}
|
|
268
314
|
};
|
|
269
315
|
await sendMatchingEvents(true);
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
316
|
+
const scheduleNextPoll = () => {
|
|
317
|
+
if (closed)
|
|
318
|
+
return;
|
|
319
|
+
timer = setTimeout(() => {
|
|
320
|
+
timer = undefined;
|
|
321
|
+
void sendMatchingEvents(false)
|
|
322
|
+
.then(scheduleNextPoll)
|
|
323
|
+
.catch(() => {
|
|
324
|
+
closed = true;
|
|
325
|
+
if (!res.writableEnded)
|
|
326
|
+
res.end();
|
|
327
|
+
});
|
|
328
|
+
}, ctx.pollIntervalMs);
|
|
329
|
+
};
|
|
330
|
+
scheduleNextPoll();
|
|
277
331
|
req.on("close", () => {
|
|
278
|
-
|
|
332
|
+
closed = true;
|
|
333
|
+
if (timer)
|
|
334
|
+
clearTimeout(timer);
|
|
279
335
|
if (!res.writableEnded)
|
|
280
336
|
res.end();
|
|
281
337
|
});
|
|
@@ -367,12 +423,15 @@ async function readFilteredEvents(filePath, offset, batchRunId, repoRoot) {
|
|
|
367
423
|
events.push(normalizeEventArtifactRefs(repoRoot, event));
|
|
368
424
|
}
|
|
369
425
|
}
|
|
370
|
-
return { events, nextOffset: lines.length };
|
|
426
|
+
return { events, nextOffset: lines.length, ok: true };
|
|
371
427
|
}
|
|
372
428
|
catch {
|
|
373
|
-
return { events: [], nextOffset: offset };
|
|
429
|
+
return { events: [], nextOffset: offset, ok: false };
|
|
374
430
|
}
|
|
375
431
|
}
|
|
432
|
+
export function shouldReadSseEventFile(previousSize, currentSize) {
|
|
433
|
+
return previousSize !== currentSize;
|
|
434
|
+
}
|
|
376
435
|
function normalizeEventArtifactRefs(repoRoot, value) {
|
|
377
436
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
378
437
|
return value;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { handleRequest, serveStatic } from "./routes.js";
|
|
4
|
+
import { createObserveSnapshotCache, handleRequest, serveStatic, } from "./routes.js";
|
|
5
5
|
const STATIC_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
6
6
|
export function createObserveServer(options) {
|
|
7
7
|
const repoRoot = path.resolve(options.repoRoot);
|
|
@@ -11,6 +11,12 @@ export function createObserveServer(options) {
|
|
|
11
11
|
}
|
|
12
12
|
const port = options.port ?? 0;
|
|
13
13
|
const pollIntervalMs = options.pollIntervalMs ?? 1000;
|
|
14
|
+
const routeContext = {
|
|
15
|
+
repoRoot,
|
|
16
|
+
pollIntervalMs,
|
|
17
|
+
staticDir: STATIC_DIR,
|
|
18
|
+
snapshotCache: createObserveSnapshotCache(),
|
|
19
|
+
};
|
|
14
20
|
return new Promise((resolve, reject) => {
|
|
15
21
|
const server = createServer((req, res) => {
|
|
16
22
|
void dispatch(req, res).catch((error) => {
|
|
@@ -27,11 +33,7 @@ export function createObserveServer(options) {
|
|
|
27
33
|
});
|
|
28
34
|
});
|
|
29
35
|
async function dispatch(req, res) {
|
|
30
|
-
const handled = await handleRequest(req, res,
|
|
31
|
-
repoRoot,
|
|
32
|
-
pollIntervalMs,
|
|
33
|
-
staticDir: STATIC_DIR,
|
|
34
|
-
});
|
|
36
|
+
const handled = await handleRequest(req, res, routeContext);
|
|
35
37
|
if (!handled) {
|
|
36
38
|
await serveStatic(req, res, STATIC_DIR);
|
|
37
39
|
}
|