@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.
Files changed (133) hide show
  1. package/AGENTS.md +145 -142
  2. package/CHANGELOG.md +161 -158
  3. package/README.md +206 -204
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +9 -2
  7. package/dist/commands/init.js +518 -488
  8. package/dist/commands/loop-benchmark.js +11 -11
  9. package/dist/commands/pi-reuse-benchmark.js +16 -16
  10. package/dist/executors/cursor-executor.js +1 -1
  11. package/dist/executors/shell-executor.js +74 -8
  12. package/dist/governance/manifest-types.js +1 -1
  13. package/dist/shared/reference-context.js +48 -22
  14. package/dist/task/config-types.js +1 -1
  15. package/dist/task/runtime.js +28 -28
  16. package/dist/worker/cli.js +3 -3
  17. package/dist/worker/loop-agent/loop-agent-client.js +51 -10
  18. package/dist/worker/observability/event-store.js +2 -1
  19. package/dist/worker/observability/read-model.js +13 -11
  20. package/dist/worker/observe/paths.js +2 -2
  21. package/dist/worker/observe/routes.js +82 -23
  22. package/dist/worker/observe/server.js +8 -6
  23. package/dist/worker/observe/static/app.js +1965 -1480
  24. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  25. package/dist/worker/observe/static/dag-layout.js +83 -83
  26. package/dist/worker/observe/static/index.html +88 -63
  27. package/dist/worker/observe/static/styles.css +598 -722
  28. package/dist/worker/pool/run-store.js +7 -8
  29. package/dist/worker/run-task/run-task.js +11 -2
  30. package/dist/worker/runner/run-ready.js +1 -1
  31. package/dist/workflows/dag/canvas-observer.js +275 -275
  32. package/docs/README.md +84 -79
  33. package/docs/agent-dag-recovery-playbook.md +184 -184
  34. package/docs/agent-dag-runner.md +42 -42
  35. package/docs/architecture/runtime-boundaries.md +162 -162
  36. package/docs/cursor-executor-usage.md +25 -25
  37. package/docs/decisions/README.md +3 -3
  38. package/docs/design/README.md +49 -49
  39. package/docs/development-principles.md +73 -73
  40. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  41. package/docs/exec-plans/README.md +6 -6
  42. package/docs/exec-plans/active/README.md +11 -11
  43. package/docs/exec-plans/completed/README.md +43 -34
  44. package/docs/feature-workflow.md +187 -187
  45. package/docs/harness-methodology-debugging.md +153 -153
  46. package/docs/harness-methodology-tdd.md +130 -130
  47. package/docs/harness-methodology-verification.md +27 -27
  48. package/docs/init-surface.manifest.json +245 -241
  49. package/docs/loop-agent-harness.md +63 -55
  50. package/docs/production-readiness.md +96 -96
  51. package/docs/progress/README.md +3 -3
  52. package/docs/reports/README.md +12 -9
  53. package/docs/skills/README.md +6 -6
  54. package/docs/skills/vetted-skill-registry.md +26 -26
  55. package/docs/templates/adr.md +60 -60
  56. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  57. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  58. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  59. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  60. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  61. package/docs/templates/agent-dag-report.schema.json +454 -454
  62. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  63. package/docs/templates/agent-dag.base.json +195 -195
  64. package/docs/templates/agent-dag.final-verification.json +190 -190
  65. package/docs/templates/agent-dag.schema.json +316 -316
  66. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  67. package/docs/templates/exec-plan.md +64 -64
  68. package/docs/templates/feature-spec.md +53 -53
  69. package/docs/templates/harness.schema.json +218 -0
  70. package/docs/templates/hybrid-dag.json +193 -193
  71. package/docs/templates/init-evolution-review.md +33 -33
  72. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  73. package/docs/templates/product-line/AGENTS.md +8 -8
  74. package/docs/templates/product-line/README.md +9 -9
  75. package/docs/templates/product-line/acceptance.yaml +14 -14
  76. package/docs/templates/product-line/closeout.yaml +9 -9
  77. package/docs/templates/product-line/design.md +13 -13
  78. package/docs/templates/product-line/links.md +10 -10
  79. package/docs/templates/product-line/requirement.md +17 -17
  80. package/docs/templates/product-line/task-graph.yaml +15 -15
  81. package/docs/templates/product-line/task.yaml +65 -65
  82. package/docs/templates/product-line/test-plan.md +7 -7
  83. package/docs/templates/production-readiness-checklist.md +57 -57
  84. package/docs/templates/progress-log.md +17 -17
  85. package/docs/templates/project-start-checklist.md +9 -9
  86. package/docs/templates/qa-report.md +48 -48
  87. package/docs/templates/sprint-contract.md +29 -29
  88. package/docs/templates/worker-dogfood-evidence.md +52 -52
  89. package/docs/templates/worker-dogfood-setup.md +48 -48
  90. package/docs/verification-matrix.md +49 -49
  91. package/examples/decision-gate-agent-dag.json +123 -123
  92. package/examples/example-dag.json +51 -51
  93. package/examples/hybrid-loop-agent-dag.json +194 -194
  94. package/harness.json +73 -71
  95. package/package.json +68 -67
  96. package/scripts/check-product-line-docs.sh +22 -22
  97. package/scripts/check-task-pool-root.sh +32 -0
  98. package/skills/ai-engineering-context/SKILL.md +48 -48
  99. package/skills/code-review-core/SKILL.md +20 -20
  100. package/skills/codebase-scout/SKILL.md +19 -19
  101. package/skills/init-capability-evolution/SKILL.md +69 -69
  102. package/skills/loop-agent/SKILL.md +149 -149
  103. package/skills/loop-agent/references/README.md +67 -67
  104. package/skills/loop-agent/references/command-reference.md +432 -412
  105. package/skills/loop-agent/references/harness-policy.md +263 -263
  106. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  107. package/skills/loop-agent/references/learned/README.md +21 -21
  108. package/skills/loop-agent/references/long-running-loop.md +59 -59
  109. package/skills/loop-agent/references/model-routing.md +36 -36
  110. package/skills/loop-agent/references/multi-worktree.md +54 -54
  111. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  112. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  113. package/skills/loop-agent/references/pi-prompt.md +23 -23
  114. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  115. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  116. package/skills/loop-agent/references/task-workflow.md +89 -89
  117. package/skills/loop-agent/references/verification-and-failure-handling.md +133 -128
  118. package/skills/requesting-code-review/SKILL.md +101 -101
  119. package/skills/requesting-code-review/code-reviewer.md +168 -168
  120. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  121. package/skills/systematic-debugging/SKILL.md +296 -296
  122. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  123. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  124. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  125. package/skills/systematic-debugging/find-polluter.sh +63 -63
  126. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  127. package/skills/systematic-debugging/test-academic.md +14 -14
  128. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  129. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  130. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  131. package/skills/test-driven-development/SKILL.md +20 -20
  132. package/skills/verification-before-completion/SKILL.md +154 -154
  133. 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, ".task-pool", "observability");
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, ".task-pool", "runs", "runs.jsonl"));
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 runsDir = path.join(repoRoot, ".task-pool", "runs");
557
+ const artifactsDir = path.join(getTaskPoolRoot(repoRoot), "artifacts");
557
558
  try {
558
- const entries = await readdir(runsDir, { withFileTypes: true });
559
+ const entries = await readdir(artifactsDir, { withFileTypes: true });
559
560
  for (const entry of entries) {
560
- if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name === "runs.jsonl")
561
+ if (!entry.isDirectory())
561
562
  continue;
562
- const batchRunId = entry.name.replace(/\.json$/, "");
563
- const parsed = await safeReadJson(path.join(runsDir, entry.name));
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: path.join(runsDir, entry.name),
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, ".task-pool", "state");
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, ".task-pool", "failure-handoffs");
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, ".task-pool", "observability", "runs");
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[1] ?? "");
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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, ".task-pool", "observability", "runs", workerRunId, "events.jsonl");
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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, ".task-pool", "observability", "runs", workerRunId, "events.jsonl");
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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 buildGlobalSnapshot({ repoRoot: ctx.repoRoot });
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, ".task-pool", "observability");
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 timer = setInterval(() => {
271
- void sendMatchingEvents(false).catch(() => {
272
- clearInterval(timer);
273
- if (!res.writableEnded)
274
- res.end();
275
- });
276
- }, ctx.pollIntervalMs);
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
- clearInterval(timer);
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
  }