@vitest-agent/engine 0.1.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/index.d.ts +2679 -0
  4. package/index.js +63 -0
  5. package/layers/ConfigLive.js +46 -0
  6. package/layers/DataReaderLive.js +1923 -0
  7. package/layers/DataStoreLive.js +1283 -0
  8. package/layers/DetailResolverLive.js +9 -0
  9. package/layers/DiscoveryRegistryLive.js +70 -0
  10. package/layers/EnvironmentDetectorLive.js +36 -0
  11. package/layers/EnvironmentDetectorTest.js +13 -0
  12. package/layers/ExecutorResolverLive.js +9 -0
  13. package/layers/FormatSelectorLive.js +20 -0
  14. package/layers/HistoryTrackerLive.js +49 -0
  15. package/layers/HistoryTrackerTest.js +25 -0
  16. package/layers/LoggerLive.js +70 -0
  17. package/layers/OutputPipelineLive.js +20 -0
  18. package/layers/OutputRendererLive.js +23 -0
  19. package/layers/PathResolutionLive.js +46 -0
  20. package/layers/PerClientSessionMapLive.js +145 -0
  21. package/layers/ProjectDiscoveryLive.js +67 -0
  22. package/layers/ProjectDiscoveryTest.js +12 -0
  23. package/layers/ProjectIdentityLive.js +95 -0
  24. package/layers/RunContextLive.js +82 -0
  25. package/lib/format-triage.js +103 -0
  26. package/lib/format-wrapup.js +67 -0
  27. package/migrations/0001_initial.js +824 -0
  28. package/migrations/0002_test_artifacts.js +30 -0
  29. package/migrations/index.js +27 -0
  30. package/migrations/registry_0001_initial.js +39 -0
  31. package/migrations/session_map_0001_initial.js +38 -0
  32. package/package.json +59 -0
  33. package/platform.js +58 -0
  34. package/programs/end-agent.js +19 -0
  35. package/programs/hook-paths.js +115 -0
  36. package/programs/platform-sidecar.js +37 -0
  37. package/programs/record-session.js +49 -0
  38. package/programs/record-tdd-artifact.js +119 -0
  39. package/programs/record-turn.js +61 -0
  40. package/programs/record-workspace-changes.js +57 -0
  41. package/programs/register-agent.js +85 -0
  42. package/programs/resolve-session-for-recording.js +44 -0
  43. package/programs/session-env.js +85 -0
  44. package/project-dir.js +32 -0
  45. package/services/Config.js +15 -0
  46. package/services/DataReader.js +8 -0
  47. package/services/DataStore.js +15 -0
  48. package/services/DetailResolver.js +8 -0
  49. package/services/DiscoveryRegistry.js +8 -0
  50. package/services/EnvironmentDetector.js +8 -0
  51. package/services/ExecutorResolver.js +8 -0
  52. package/services/FormatSelector.js +8 -0
  53. package/services/HistoryTracker.js +20 -0
  54. package/services/OutputRenderer.js +8 -0
  55. package/services/PerClientSessionMap.js +22 -0
  56. package/services/ProjectDiscovery.js +8 -0
  57. package/services/ProjectIdentity.js +62 -0
  58. package/services/RunContext.js +53 -0
  59. package/services/idempotency.js +58 -0
  60. package/sql/assemblers.js +69 -0
  61. package/testing/layers.js +16 -0
  62. package/testing.d.ts +1101 -0
  63. package/testing.js +243 -0
  64. package/tsdoc-metadata.json +11 -0
  65. package/utils/ensure-migrated.js +47 -0
  66. package/utils/failure-signature.js +34 -0
  67. package/utils/resolve-data-path.js +64 -0
  68. package/utils/resolve-project-key-from-cwd.js +52 -0
  69. package/utils/resolve-workspace-key.js +41 -0
  70. package/version.js +6 -0
@@ -0,0 +1,1283 @@
1
+ import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "../services/DataStore.js";
2
+ import { Agent, AgentNotFoundError, BehaviorNotFoundError, DataStoreError, GoalNotFoundError, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, coerceErrorText, extractSqlReason } from "@vitest-agent/sdk";
3
+ import { Effect, Layer, Option } from "effect";
4
+ import { SqlClient } from "effect/unstable/sql/SqlClient";
5
+ import { Buffer } from "node:buffer";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ //#region src/layers/DataStoreLive.ts
9
+ const isLegalLifecycleTransition = (from, to) => {
10
+ if (from === to) return true;
11
+ if (from === "done" || from === "abandoned") return false;
12
+ if (from === "pending") return to === "in_progress" || to === "done" || to === "abandoned";
13
+ if (from === "in_progress") return to === "done" || to === "abandoned";
14
+ return false;
15
+ };
16
+ const goalRowFromDb = (row) => ({
17
+ id: row.id,
18
+ sessionId: row.session_id,
19
+ ordinal: row.ordinal,
20
+ goal: row.goal,
21
+ status: row.status,
22
+ createdAt: row.created_at
23
+ });
24
+ const behaviorRowFromDb = (row) => ({
25
+ id: row.id,
26
+ goalId: row.goal_id,
27
+ ordinal: row.ordinal,
28
+ behavior: row.behavior,
29
+ suggestedTestName: row.suggested_test_name,
30
+ status: row.status,
31
+ createdAt: row.created_at
32
+ });
33
+ const boolToInt = (v) => v === void 0 ? null : v ? 1 : 0;
34
+ /** @public */
35
+ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
36
+ const sql = yield* SqlClient;
37
+ yield* sql`PRAGMA foreign_keys=ON`.pipe(Effect.catch(() => Effect.void));
38
+ const ensureFile = (filePath) => Effect.gen(function* () {
39
+ yield* Effect.logDebug("ensureFile").pipe(Effect.annotateLogs({ filePath }));
40
+ yield* sql`INSERT OR IGNORE INTO files (path) VALUES (${filePath})`;
41
+ return (yield* sql`SELECT id FROM files WHERE path = ${filePath}`)[0].id;
42
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
43
+ operation: "write",
44
+ table: "files",
45
+ reason: extractSqlReason(e)
46
+ })));
47
+ const writeSettings = (hash, settings, envVars) => Effect.gen(function* () {
48
+ yield* Effect.logDebug("writeSettings").pipe(Effect.annotateLogs({ hash }));
49
+ yield* sql`INSERT OR IGNORE INTO settings (hash, vitest_version, pool, environment, test_timeout, hook_timeout, slow_test_threshold, max_concurrency, max_workers, isolate, bail, globals, file_parallelism, sequence_seed, coverage_provider) VALUES (${hash}, ${settings.vitestVersion}, ${settings.pool ?? null}, ${settings.environment ?? null}, ${settings.testTimeout ?? null}, ${settings.hookTimeout ?? null}, ${settings.slowTestThreshold ?? null}, ${settings.maxConcurrency ?? null}, ${settings.maxWorkers ?? null}, ${boolToInt(settings.isolate)}, ${settings.bail ?? null}, ${boolToInt(settings.globals)}, ${boolToInt(settings.fileParallelism)}, ${settings.sequenceSeed ?? null}, ${settings.coverageProvider ?? null})`;
50
+ for (const [key, value] of Object.entries(envVars)) yield* sql`INSERT OR IGNORE INTO settings_env_vars (settings_hash, key, value) VALUES (${hash}, ${key}, ${value})`;
51
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
52
+ operation: "write",
53
+ table: "settings",
54
+ reason: extractSqlReason(e)
55
+ })));
56
+ const writeRun = (input) => Effect.gen(function* () {
57
+ yield* Effect.logDebug("writeRun").pipe(Effect.annotateLogs({
58
+ project: input.project,
59
+ invocationId: input.invocationId
60
+ }));
61
+ const actorType = input.actorType ?? "system";
62
+ const agentId = input.agentId ?? null;
63
+ const conversationId = input.conversationId ?? null;
64
+ const gitDirtyInt = input.gitDirty === void 0 || input.gitDirty === null ? null : input.gitDirty ? 1 : 0;
65
+ const hostMetadataJson = input.hostMetadata == null ? null : JSON.stringify(input.hostMetadata);
66
+ yield* sql`INSERT INTO test_runs (invocation_id, project, settings_hash, timestamp, commit_sha, branch, reason, duration, total, passed, failed, skipped, scoped, snapshot_added, snapshot_matched, snapshot_unmatched, snapshot_updated, snapshot_unchecked, snapshot_total, snapshot_failure, snapshot_did_update, snapshot_files_added, snapshot_files_removed, snapshot_files_unmatched, snapshot_files_updated, actor_type, agent_id, conversation_id, git_branch, git_commit_sha, git_dirty, git_upstream, git_worktree_dir, host_source, host_value, host_metadata) VALUES (${input.invocationId}, ${input.project}, ${input.settingsHash}, ${input.timestamp}, ${input.commitSha ?? null}, ${input.branch ?? null}, ${input.reason}, ${input.duration}, ${input.total}, ${input.passed}, ${input.failed}, ${input.skipped}, ${input.scoped ? 1 : 0}, ${input.snapshotAdded ?? 0}, ${input.snapshotMatched ?? 0}, ${input.snapshotUnmatched ?? 0}, ${input.snapshotUpdated ?? 0}, ${input.snapshotUnchecked ?? 0}, ${input.snapshotTotal ?? 0}, ${boolToInt(input.snapshotFailure) ?? 0}, ${boolToInt(input.snapshotDidUpdate) ?? 0}, ${input.snapshotFilesAdded ?? 0}, ${input.snapshotFilesRemoved ?? 0}, ${input.snapshotFilesUnmatched ?? 0}, ${input.snapshotFilesUpdated ?? 0}, ${actorType}, ${agentId}, ${conversationId}, ${input.gitBranch ?? null}, ${input.gitCommitSha ?? null}, ${gitDirtyInt}, ${input.gitUpstream ?? null}, ${input.gitWorktreeDir ?? null}, ${input.hostSource ?? null}, ${input.hostValue ?? null}, ${hostMetadataJson})`;
67
+ return (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
68
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
69
+ operation: "write",
70
+ table: "test_runs",
71
+ reason: extractSqlReason(e)
72
+ })));
73
+ const writeModules = (runId, modules) => Effect.gen(function* () {
74
+ yield* Effect.logDebug("writeModules").pipe(Effect.annotateLogs({
75
+ runId,
76
+ count: modules.length
77
+ }));
78
+ const ids = [];
79
+ for (const mod of modules) {
80
+ yield* sql`INSERT INTO test_modules (run_id, file_id, relative_module_id, state, duration, environment_setup_duration, prepare_duration, collect_duration, setup_duration, heap) VALUES (${runId}, ${mod.fileId}, ${mod.relativeModuleId}, ${mod.state}, ${mod.duration ?? null}, ${mod.environmentSetupDuration ?? null}, ${mod.prepareDuration ?? null}, ${mod.collectDuration ?? null}, ${mod.setupDuration ?? null}, ${mod.heap ?? null})`;
81
+ const rows = yield* sql`SELECT last_insert_rowid() as id`;
82
+ ids.push(rows[0].id);
83
+ }
84
+ return ids;
85
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
86
+ operation: "write",
87
+ table: "test_modules",
88
+ reason: extractSqlReason(e)
89
+ })));
90
+ const writeSuites = (moduleId, suites) => Effect.gen(function* () {
91
+ yield* Effect.logDebug("writeSuites").pipe(Effect.annotateLogs({
92
+ moduleId,
93
+ count: suites.length
94
+ }));
95
+ const ids = [];
96
+ for (const suite of suites) {
97
+ yield* sql`INSERT INTO test_suites (module_id, parent_suite_id, name, full_name, state, mode, concurrent, shuffle, retry, repeats, location_line, location_column) VALUES (${moduleId}, ${suite.parentSuiteId ?? null}, ${suite.name}, ${suite.fullName}, ${suite.state}, ${suite.mode ?? null}, ${boolToInt(suite.concurrent)}, ${boolToInt(suite.shuffle)}, ${suite.retry ?? null}, ${suite.repeats ?? null}, ${suite.locationLine ?? null}, ${suite.locationColumn ?? null})`;
98
+ const rows = yield* sql`SELECT last_insert_rowid() as id`;
99
+ ids.push(rows[0].id);
100
+ }
101
+ return ids;
102
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
103
+ operation: "write",
104
+ table: "test_suites",
105
+ reason: extractSqlReason(e)
106
+ })));
107
+ const writeTestCases = (moduleId, tests) => Effect.gen(function* () {
108
+ yield* Effect.logDebug("writeTestCases").pipe(Effect.annotateLogs({
109
+ moduleId,
110
+ count: tests.length
111
+ }));
112
+ const ids = [];
113
+ for (const tc of tests) {
114
+ yield* sql`INSERT INTO test_cases (module_id, suite_id, vitest_id, name, full_name, state, classification, duration, start_time, flaky, slow, retry_count, repeat_count, heap, mode, each, fails, concurrent, shuffle, timeout, skip_note, location_line, location_column, created_turn_id) VALUES (${moduleId}, ${tc.suiteId ?? null}, ${tc.vitestId ?? null}, ${tc.name}, ${tc.fullName}, ${tc.state}, ${tc.classification ?? null}, ${tc.duration ?? null}, ${tc.startTime ?? null}, ${boolToInt(tc.flaky)}, ${boolToInt(tc.slow)}, ${tc.retryCount ?? 0}, ${tc.repeatCount ?? 0}, ${tc.heap ?? null}, ${tc.mode ?? null}, ${boolToInt(tc.each)}, ${boolToInt(tc.fails)}, ${boolToInt(tc.concurrent)}, ${boolToInt(tc.shuffle)}, ${tc.timeout ?? null}, ${tc.skipNote ?? null}, ${tc.locationLine ?? null}, ${tc.locationColumn ?? null}, ${tc.createdTurnId ?? null})`;
115
+ const testCaseId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
116
+ ids.push(testCaseId);
117
+ if (tc.tags && tc.tags.length > 0) for (const tag of tc.tags) {
118
+ yield* sql`INSERT OR IGNORE INTO tags (name) VALUES (${tag})`;
119
+ const tagRows = yield* sql`SELECT id FROM tags WHERE name = ${tag}`;
120
+ if (tagRows.length > 0) yield* sql`INSERT OR IGNORE INTO test_case_tags (test_case_id, tag_id) VALUES (${testCaseId}, ${tagRows[0].id})`;
121
+ }
122
+ }
123
+ return ids;
124
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
125
+ operation: "write",
126
+ table: "test_cases",
127
+ reason: extractSqlReason(e)
128
+ })));
129
+ const writeErrors = (runId, errors) => Effect.gen(function* () {
130
+ yield* Effect.logDebug("writeErrors").pipe(Effect.annotateLogs({
131
+ runId,
132
+ count: errors.length
133
+ }));
134
+ for (const err of errors) {
135
+ const message = coerceErrorText(err.message) ?? "<missing message>";
136
+ yield* sql`INSERT INTO test_errors (run_id, test_case_id, test_suite_id, module_id, scope, name, message, diff, actual, expected, stack, cause_error_id, signature_hash, ordinal) VALUES (${runId}, ${err.testCaseId ?? null}, ${err.testSuiteId ?? null}, ${err.moduleId ?? null}, ${err.scope}, ${coerceErrorText(err.name) ?? null}, ${message}, ${coerceErrorText(err.diff) ?? null}, ${coerceErrorText(err.actual) ?? null}, ${coerceErrorText(err.expected) ?? null}, ${coerceErrorText(err.stack) ?? null}, ${err.causeErrorId ?? null}, ${err.signatureHash ?? null}, ${err.ordinal ?? 0})`;
137
+ const stackText = coerceErrorText(err.stack);
138
+ if (err.frames && err.frames.length > 0) {
139
+ const errorId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
140
+ for (const frame of err.frames) {
141
+ const fileId = yield* ensureFile(frame.filePath);
142
+ yield* sql`INSERT INTO stack_frames (error_id, ordinal, method, file_id, line, col, source_mapped_line, function_boundary_line) VALUES (${errorId}, ${frame.ordinal}, ${frame.method}, ${fileId}, ${frame.line}, ${frame.col}, ${frame.sourceMappedLine ?? null}, ${frame.functionBoundaryLine ?? null})`;
143
+ }
144
+ } else if (stackText) {
145
+ const errorId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
146
+ const frames = [...stackText.matchAll(/at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?/g)];
147
+ for (let frameOrdinal = 0; frameOrdinal < frames.length; frameOrdinal++) {
148
+ const m = frames[frameOrdinal];
149
+ const method = m[1] ?? null;
150
+ const filePath = m[2];
151
+ const line = Number.parseInt(m[3], 10);
152
+ const col = Number.parseInt(m[4], 10);
153
+ const fileId = yield* ensureFile(filePath);
154
+ yield* sql`INSERT INTO stack_frames (error_id, ordinal, method, file_id, line, col, source_mapped_line, function_boundary_line) VALUES (${errorId}, ${frameOrdinal}, ${method}, ${fileId}, ${line}, ${col}, NULL, NULL)`;
155
+ }
156
+ }
157
+ }
158
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
159
+ operation: "write",
160
+ table: "test_errors",
161
+ reason: extractSqlReason(e)
162
+ })));
163
+ const writeAttachments = (owner, attachments) => Effect.gen(function* () {
164
+ for (const att of attachments) {
165
+ const artifactId = "artifactId" in owner ? owner.artifactId : null;
166
+ const annotationId = "annotationId" in owner ? owner.annotationId : null;
167
+ const storedBytes = att.body === void 0 ? 0 : Buffer.byteLength(att.body, "utf8");
168
+ const inline = att.body !== void 0 && Math.max(storedBytes, att.byteSize) <= 65536;
169
+ const body = inline ? att.body : null;
170
+ const bodyEncoding = inline ? att.bodyEncoding ?? null : null;
171
+ yield* sql`INSERT INTO attachments (artifact_id, annotation_id, content_type, path, body, body_encoding, byte_size) VALUES (${artifactId}, ${annotationId}, ${att.contentType ?? null}, ${att.path ?? null}, ${body}, ${bodyEncoding}, ${att.byteSize})`;
172
+ }
173
+ });
174
+ const writeAnnotations = (runId, annotations) => Effect.gen(function* () {
175
+ yield* Effect.logDebug("writeAnnotations").pipe(Effect.annotateLogs({
176
+ runId,
177
+ count: annotations.length
178
+ }));
179
+ for (const anno of annotations) {
180
+ const locationFileId = anno.locationFile !== void 0 ? yield* ensureFile(anno.locationFile) : null;
181
+ yield* sql`INSERT INTO test_annotations (test_case_id, type, message, location_file_id, location_line, location_column) VALUES (${anno.testCaseId}, ${anno.type}, ${anno.message}, ${locationFileId}, ${anno.locationLine ?? null}, ${anno.locationColumn ?? null})`;
182
+ const idRows = yield* sql`SELECT last_insert_rowid() as id`;
183
+ yield* writeAttachments({ annotationId: idRows[0].id }, anno.attachments ?? []);
184
+ }
185
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
186
+ operation: "write",
187
+ table: "test_annotations",
188
+ reason: extractSqlReason(e)
189
+ })));
190
+ const writeArtifacts = (runId, artifacts) => Effect.gen(function* () {
191
+ yield* Effect.logDebug("writeArtifacts").pipe(Effect.annotateLogs({
192
+ runId,
193
+ count: artifacts.length
194
+ }));
195
+ for (const art of artifacts) {
196
+ const locationFileId = art.locationFile !== void 0 ? yield* ensureFile(art.locationFile) : null;
197
+ yield* sql`INSERT INTO test_artifacts (test_case_id, type, message, data, location_file_id, location_line, location_column) VALUES (${art.testCaseId}, ${art.type}, ${art.message ?? null}, ${art.data ?? null}, ${locationFileId}, ${art.locationLine ?? null}, ${art.locationColumn ?? null})`;
198
+ const idRows = yield* sql`SELECT last_insert_rowid() as id`;
199
+ yield* writeAttachments({ artifactId: idRows[0].id }, art.attachments ?? []);
200
+ }
201
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
202
+ operation: "write",
203
+ table: "test_artifacts",
204
+ reason: extractSqlReason(e)
205
+ })));
206
+ const writeCoverage = (runId, coverage) => Effect.gen(function* () {
207
+ yield* Effect.logDebug("writeCoverage").pipe(Effect.annotateLogs({
208
+ runId,
209
+ count: coverage.length
210
+ }));
211
+ for (const cov of coverage) {
212
+ const tier = cov.tier ?? "below_threshold";
213
+ yield* sql`INSERT INTO file_coverage (run_id, file_id, statements, branches, functions, lines, uncovered_lines, tier) VALUES (${runId}, ${cov.fileId}, ${cov.statements}, ${cov.branches}, ${cov.functions}, ${cov.lines}, ${cov.uncoveredLines ?? null}, ${tier})`;
214
+ }
215
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
216
+ operation: "write",
217
+ table: "file_coverage",
218
+ reason: extractSqlReason(e)
219
+ })));
220
+ const writeHistory = (project, fullName, modulePath, runId, timestamp, state, duration, flaky, retryCount, errorMessage) => Effect.gen(function* () {
221
+ yield* Effect.logDebug("writeHistory").pipe(Effect.annotateLogs({
222
+ project,
223
+ modulePath,
224
+ runId
225
+ }));
226
+ yield* sql`INSERT INTO test_history (run_id, project, module_path, full_name, timestamp, state, duration, flaky, retry_count, error_message) VALUES (${runId}, ${project}, ${modulePath}, ${fullName}, ${timestamp}, ${state}, ${duration}, ${flaky ? 1 : 0}, ${retryCount}, ${errorMessage}) ON CONFLICT(project, module_path, full_name, timestamp) DO UPDATE SET run_id = excluded.run_id, state = excluded.state, duration = excluded.duration, flaky = excluded.flaky, retry_count = excluded.retry_count, error_message = excluded.error_message`;
227
+ yield* sql`DELETE FROM test_history WHERE id NOT IN (SELECT id FROM test_history WHERE project = ${project} AND module_path = ${modulePath} AND full_name = ${fullName} ORDER BY timestamp DESC LIMIT 10) AND project = ${project} AND module_path = ${modulePath} AND full_name = ${fullName}`;
228
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
229
+ operation: "write",
230
+ table: "test_history",
231
+ reason: extractSqlReason(e)
232
+ })));
233
+ const writeBaselines = (baselines) => Effect.gen(function* () {
234
+ yield* Effect.logDebug("writeBaselines").pipe(Effect.annotateLogs({ updatedAt: baselines.updatedAt }));
235
+ const { updatedAt, global: g, patterns = [] } = baselines;
236
+ const metrics = [
237
+ "lines",
238
+ "functions",
239
+ "branches",
240
+ "statements"
241
+ ];
242
+ for (const metric of metrics) {
243
+ const value = g[metric];
244
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', 'baseline', ${metric}, ${value}, '', ${updatedAt}) ON CONFLICT (project, kind, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
245
+ }
246
+ for (const [pattern, thresholds] of patterns) for (const metric of metrics) {
247
+ const value = thresholds[metric];
248
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', 'baseline', ${metric}, ${value}, ${pattern}, ${updatedAt}) ON CONFLICT (project, kind, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
249
+ }
250
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
251
+ operation: "write",
252
+ table: "coverage_baselines",
253
+ reason: extractSqlReason(e)
254
+ })));
255
+ /**
256
+ * Shared upsert for the `coverage_baselines` table's non-baseline
257
+ * `kind` rows (`threshold` / `target`). Distinct from `writeBaselines`
258
+ * so the ratcheted high-water mark never collides with the resolved,
259
+ * enforced Vitest thresholds or the aspirational coverageTargets
260
+ * (issue #237).
261
+ */
262
+ const writeCoveragePolicy = (kind, policy) => sql.withTransaction(Effect.gen(function* () {
263
+ yield* Effect.logDebug("writeCoveragePolicy").pipe(Effect.annotateLogs({ kind }));
264
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
265
+ const { global: g, patterns = [] } = policy;
266
+ const metrics = [
267
+ "lines",
268
+ "functions",
269
+ "branches",
270
+ "statements"
271
+ ];
272
+ yield* sql`DELETE FROM coverage_baselines WHERE project = '__global__' AND kind = ${kind}`;
273
+ for (const metric of metrics) {
274
+ const value = g[metric];
275
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', ${kind}, ${metric}, ${value}, '', ${updatedAt})`;
276
+ }
277
+ for (const [pattern, thresholds] of patterns) for (const metric of metrics) {
278
+ const value = thresholds[metric];
279
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', ${kind}, ${metric}, ${value}, ${pattern}, ${updatedAt})`;
280
+ }
281
+ })).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
282
+ operation: "write",
283
+ table: "coverage_baselines",
284
+ reason: extractSqlReason(e)
285
+ })));
286
+ const writeThresholds = (thresholds) => writeCoveragePolicy("threshold", thresholds);
287
+ const writeTargets = (targets) => writeCoveragePolicy("target", targets);
288
+ const writeTrends = (project, runId, entry) => Effect.gen(function* () {
289
+ yield* Effect.logDebug("writeTrends").pipe(Effect.annotateLogs({
290
+ project,
291
+ runId
292
+ }));
293
+ yield* sql`INSERT OR REPLACE INTO coverage_trends (run_id, project, timestamp, lines, functions, branches, statements, direction, targets_hash) VALUES (${runId}, ${project}, ${entry.timestamp}, ${entry.coverage.lines}, ${entry.coverage.functions}, ${entry.coverage.branches}, ${entry.coverage.statements}, ${entry.direction}, ${entry.targetsHash ?? null})`;
294
+ yield* sql`DELETE FROM coverage_trends WHERE id NOT IN (SELECT id FROM coverage_trends WHERE project = ${project} ORDER BY timestamp DESC LIMIT 50) AND project = ${project}`;
295
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
296
+ operation: "write",
297
+ table: "coverage_trends",
298
+ reason: extractSqlReason(e)
299
+ })));
300
+ const writeSourceMap = (sourceFilePath, testModuleId, mappingType) => Effect.gen(function* () {
301
+ yield* Effect.logDebug("writeSourceMap").pipe(Effect.annotateLogs({
302
+ sourceFilePath,
303
+ testModuleId,
304
+ mappingType
305
+ }));
306
+ const sourceFileId = yield* ensureFile(sourceFilePath);
307
+ yield* sql`INSERT OR IGNORE INTO source_test_map (source_file_id, test_module_id, mapping_type) VALUES (${sourceFileId}, ${testModuleId}, ${mappingType})`;
308
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
309
+ operation: "write",
310
+ table: "source_test_map",
311
+ reason: extractSqlReason(e)
312
+ })));
313
+ const writeNote = (note) => Effect.gen(function* () {
314
+ yield* Effect.logDebug("writeNote").pipe(Effect.annotateLogs({
315
+ scope: note.scope,
316
+ project: note.project ?? null
317
+ }));
318
+ yield* sql`INSERT INTO notes (title, content, scope, project, test_full_name, module_path, parent_note_id, created_by, expires_at, pinned) VALUES (${note.title}, ${note.content}, ${note.scope}, ${note.project ?? null}, ${note.testFullName ?? null}, ${note.modulePath ?? null}, ${note.parentNoteId ?? null}, ${note.createdBy ?? null}, ${note.expiresAt ?? null}, ${note.pinned ? 1 : 0})`;
319
+ return (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
320
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
321
+ operation: "write",
322
+ table: "notes",
323
+ reason: extractSqlReason(e)
324
+ })));
325
+ const updateNote = (id, fields) => Effect.gen(function* () {
326
+ yield* Effect.logDebug("updateNote").pipe(Effect.annotateLogs({ id }));
327
+ const setClauses = [];
328
+ const values = [];
329
+ if (fields.title !== void 0) {
330
+ setClauses.push("title = ?");
331
+ values.push(fields.title);
332
+ }
333
+ if (fields.content !== void 0) {
334
+ setClauses.push("content = ?");
335
+ values.push(fields.content);
336
+ }
337
+ if (fields.scope !== void 0) {
338
+ setClauses.push("scope = ?");
339
+ values.push(fields.scope);
340
+ }
341
+ if (fields.project !== void 0) {
342
+ setClauses.push("project = ?");
343
+ values.push(fields.project);
344
+ }
345
+ if (fields.testFullName !== void 0) {
346
+ setClauses.push("test_full_name = ?");
347
+ values.push(fields.testFullName);
348
+ }
349
+ if (fields.modulePath !== void 0) {
350
+ setClauses.push("module_path = ?");
351
+ values.push(fields.modulePath);
352
+ }
353
+ if (fields.parentNoteId !== void 0) {
354
+ setClauses.push("parent_note_id = ?");
355
+ values.push(fields.parentNoteId);
356
+ }
357
+ if (fields.createdBy !== void 0) {
358
+ setClauses.push("created_by = ?");
359
+ values.push(fields.createdBy);
360
+ }
361
+ if (fields.expiresAt !== void 0) {
362
+ setClauses.push("expires_at = ?");
363
+ values.push(fields.expiresAt);
364
+ }
365
+ if (fields.pinned !== void 0) {
366
+ setClauses.push("pinned = ?");
367
+ values.push(fields.pinned ? 1 : 0);
368
+ }
369
+ if (setClauses.length === 0) return;
370
+ setClauses.push("updated_at = datetime('now')");
371
+ yield* sql.unsafe(`UPDATE notes SET ${setClauses.join(", ")} WHERE id = ?`, [...values, id]);
372
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
373
+ operation: "write",
374
+ table: "notes",
375
+ reason: extractSqlReason(e)
376
+ })));
377
+ const deleteNote = (id) => Effect.gen(function* () {
378
+ yield* Effect.logDebug("deleteNote").pipe(Effect.annotateLogs({ id }));
379
+ yield* sql`DELETE FROM notes WHERE id = ${id}`;
380
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
381
+ operation: "write",
382
+ table: "notes",
383
+ reason: extractSqlReason(e)
384
+ })));
385
+ const writeSession = (input) => Effect.gen(function* () {
386
+ yield* Effect.logDebug("writeSession").pipe(Effect.annotateLogs({ chatId: input.chatId }));
387
+ yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, conversation_id) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}, ${input.conversationId ?? null})`;
388
+ return (yield* sql`SELECT id FROM sessions WHERE chat_id = ${input.chatId}`)[0].id;
389
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
390
+ operation: "write",
391
+ table: "sessions",
392
+ reason: extractSqlReason(e)
393
+ })));
394
+ const upsertSession = (input) => Effect.gen(function* () {
395
+ yield* Effect.logDebug("upsertSession").pipe(Effect.annotateLogs({ chatId: input.chatId }));
396
+ yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, conversation_id) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}, ${input.conversationId ?? null}) ON CONFLICT(chat_id) DO NOTHING`;
397
+ return (yield* sql`SELECT id FROM sessions WHERE chat_id = ${input.chatId}`)[0].id;
398
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
399
+ operation: "write",
400
+ table: "sessions",
401
+ reason: extractSqlReason(e)
402
+ })));
403
+ const setSessionConversationIdIfNull = (input) => Effect.gen(function* () {
404
+ yield* Effect.logDebug("setSessionConversationIdIfNull").pipe(Effect.annotateLogs(input));
405
+ yield* sql`
406
+ UPDATE sessions SET conversation_id = ${input.conversationId}
407
+ WHERE id = ${input.sessionId} AND conversation_id IS NULL
408
+ `;
409
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
410
+ operation: "write",
411
+ table: "sessions",
412
+ reason: extractSqlReason(e)
413
+ })));
414
+ const writeTurn = (input) => sql.withTransaction(Effect.gen(function* () {
415
+ yield* Effect.logDebug("writeTurn").pipe(Effect.annotateLogs({
416
+ sessionId: input.sessionId,
417
+ type: input.type
418
+ }));
419
+ if (input.turnNo !== void 0) yield* sql`INSERT INTO turns (session_id, turn_no, type, payload, occurred_at) VALUES (${input.sessionId}, ${input.turnNo}, ${input.type}, ${input.payload}, ${input.occurredAt})`;
420
+ else yield* sql`INSERT INTO turns (session_id, turn_no, type, payload, occurred_at) SELECT ${input.sessionId}, COALESCE(MAX(turn_no), 0) + 1, ${input.type}, ${input.payload}, ${input.occurredAt} FROM turns WHERE session_id = ${input.sessionId}`;
421
+ const turnId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
422
+ if (input.type === "file_edit" || input.type === "tool_result") {
423
+ const payload = yield* Effect.try({
424
+ try: () => JSON.parse(input.payload),
425
+ catch: (e) => new DataStoreError({
426
+ operation: "write",
427
+ table: input.type === "file_edit" ? "file_edits" : "tool_invocations",
428
+ reason: `invalid turn payload JSON: ${e.message}`
429
+ })
430
+ });
431
+ if (input.type === "file_edit") {
432
+ const filePath = payload.file_path;
433
+ yield* sql`INSERT OR IGNORE INTO files (path) VALUES (${filePath})`;
434
+ const fileId = (yield* sql`SELECT id FROM files WHERE path = ${filePath}`)[0].id;
435
+ yield* sql`
436
+ INSERT INTO file_edits (turn_id, file_id, edit_kind, lines_added, lines_removed, diff)
437
+ VALUES (
438
+ ${turnId},
439
+ ${fileId},
440
+ ${payload.edit_kind},
441
+ ${payload.lines_added ?? null},
442
+ ${payload.lines_removed ?? null},
443
+ ${payload.diff ?? null}
444
+ )
445
+ `;
446
+ } else {
447
+ const rawToolName = payload.tool_name;
448
+ const toolName = typeof rawToolName === "string" && rawToolName.startsWith("mcp__") ? rawToolName.split("__").at(-1) ?? rawToolName : rawToolName;
449
+ yield* sql`
450
+ INSERT INTO tool_invocations (turn_id, tool_name, params_hash, result_summary, duration_ms, success)
451
+ VALUES (
452
+ ${turnId},
453
+ ${toolName},
454
+ ${null},
455
+ ${payload.result_summary ?? null},
456
+ ${payload.duration_ms ?? null},
457
+ ${boolToInt(payload.success) ?? 0}
458
+ )
459
+ `;
460
+ }
461
+ }
462
+ return turnId;
463
+ })).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => e instanceof DataStoreError ? e : new DataStoreError({
464
+ operation: "write",
465
+ table: "turns",
466
+ reason: extractSqlReason(e)
467
+ })));
468
+ const endSession = (chatId, endedAt, endReason) => Effect.gen(function* () {
469
+ yield* Effect.logDebug("endSession").pipe(Effect.annotateLogs({
470
+ chatId,
471
+ endReason
472
+ }));
473
+ yield* sql`UPDATE sessions SET ended_at = ${endedAt}, end_reason = ${endReason} WHERE chat_id = ${chatId}`;
474
+ if ((yield* sql`SELECT changes() as changes`)[0].changes === 0) return yield* Effect.fail(new DataStoreError({
475
+ operation: "write",
476
+ table: "sessions",
477
+ reason: `unknown chat_id: ${chatId}`
478
+ }));
479
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => e instanceof DataStoreError ? e : new DataStoreError({
480
+ operation: "write",
481
+ table: "sessions",
482
+ reason: extractSqlReason(e)
483
+ })));
484
+ const writeFailureSignature = (input) => Effect.gen(function* () {
485
+ yield* Effect.logDebug("writeFailureSignature").pipe(Effect.annotateLogs({
486
+ signatureHash: input.signatureHash,
487
+ runId: input.runId
488
+ }));
489
+ yield* sql`INSERT INTO failure_signatures (signature_hash, first_seen_run_id, first_seen_at, last_seen_at, occurrence_count) VALUES (${input.signatureHash}, ${input.runId}, ${input.seenAt}, ${input.seenAt}, 1) ON CONFLICT(signature_hash) DO UPDATE SET occurrence_count = occurrence_count + 1, last_seen_at = excluded.last_seen_at`;
490
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
491
+ operation: "write",
492
+ table: "failure_signatures",
493
+ reason: extractSqlReason(e)
494
+ })));
495
+ const writeHypothesis = (input) => Effect.gen(function* () {
496
+ yield* Effect.logDebug("writeHypothesis").pipe(Effect.annotateLogs({ sessionId: input.sessionId }));
497
+ yield* sql`INSERT INTO hypotheses (session_id, content, created_turn_id, cited_test_error_id, cited_stack_frame_id) VALUES (${input.sessionId}, ${input.content}, ${input.createdTurnId ?? null}, ${input.citedTestErrorId ?? null}, ${input.citedStackFrameId ?? null})`;
498
+ return (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
499
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
500
+ operation: "write",
501
+ table: "hypotheses",
502
+ reason: extractSqlReason(e)
503
+ })));
504
+ const validateHypothesis = (input) => Effect.gen(function* () {
505
+ yield* Effect.logDebug("validateHypothesis").pipe(Effect.annotateLogs({
506
+ id: input.id,
507
+ outcome: input.outcome
508
+ }));
509
+ yield* sql`UPDATE hypotheses SET validation_outcome = ${input.outcome}, validated_at = ${input.validatedAt}, validated_turn_id = ${input.validatedTurnId ?? null} WHERE id = ${input.id}`;
510
+ if ((yield* sql`SELECT changes() as changes`)[0].changes === 0) return yield* Effect.fail(new DataStoreError({
511
+ operation: "write",
512
+ table: "hypotheses",
513
+ reason: `unknown hypothesis id: ${input.id}`
514
+ }));
515
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => e instanceof DataStoreError ? e : new DataStoreError({
516
+ operation: "write",
517
+ table: "hypotheses",
518
+ reason: extractSqlReason(e)
519
+ })));
520
+ const writeTddTask = (input) => sql.withTransaction(Effect.gen(function* () {
521
+ yield* Effect.logDebug("writeTddTask").pipe(Effect.annotateLogs({
522
+ sessionId: input.sessionId,
523
+ goal: input.goal
524
+ }));
525
+ const rows = yield* sql`
526
+ INSERT OR IGNORE INTO tdd_tasks (session_id, goal, started_at, parent_tdd_task_id, run_id)
527
+ VALUES (
528
+ ${input.sessionId},
529
+ ${input.goal},
530
+ ${input.startedAt},
531
+ ${input.parentTddTaskId ?? null},
532
+ ${input.runId ?? null}
533
+ )
534
+ RETURNING id
535
+ `;
536
+ let tddTaskId;
537
+ let isNewTask;
538
+ if (rows.length > 0) {
539
+ tddTaskId = rows[0].id;
540
+ isNewTask = true;
541
+ } else {
542
+ const existing = yield* sql`
543
+ SELECT id, goal FROM tdd_tasks
544
+ WHERE session_id = ${input.sessionId} AND run_id = ${input.runId}
545
+ LIMIT 1
546
+ `;
547
+ if (existing.length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("writeTddTask: INSERT was ignored but no existing row found"));
548
+ if (existing[0].goal !== input.goal) return yield* Effect.fail(/* @__PURE__ */ new Error(`writeTddTask: runId conflict — existing task has goal "${existing[0].goal}", caller provided "${input.goal}"`));
549
+ tddTaskId = existing[0].id;
550
+ isNewTask = false;
551
+ }
552
+ if (isNewTask) yield* sql`
553
+ INSERT INTO tdd_phases
554
+ (tdd_task_id, behavior_id, phase, started_at, transition_reason, parent_phase_id)
555
+ VALUES
556
+ (
557
+ ${tddTaskId},
558
+ ${null},
559
+ ${"spike"},
560
+ ${input.startedAt},
561
+ ${"opened by tdd_task_start"},
562
+ ${null}
563
+ )
564
+ `;
565
+ return tddTaskId;
566
+ })).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
567
+ operation: "write",
568
+ table: "tdd_tasks",
569
+ reason: extractSqlReason(e)
570
+ })));
571
+ const endTddTask = (input) => Effect.gen(function* () {
572
+ yield* Effect.logDebug("endTddTask").pipe(Effect.annotateLogs({
573
+ id: input.id,
574
+ outcome: input.outcome
575
+ }));
576
+ yield* sql`
577
+ UPDATE tdd_tasks
578
+ SET ended_at = ${input.endedAt},
579
+ outcome = ${input.outcome},
580
+ summary_note_id = ${input.summaryNoteId ?? null}
581
+ WHERE id = ${input.id}
582
+ `;
583
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
584
+ operation: "write",
585
+ table: "tdd_tasks",
586
+ reason: extractSqlReason(e)
587
+ })));
588
+ const ensureTddTaskOpen = (tddTaskId) => Effect.gen(function* () {
589
+ const rows = yield* sql`
590
+ SELECT ended_at, outcome FROM tdd_tasks WHERE id = ${tddTaskId}
591
+ `.pipe(Effect.mapError((e) => new DataStoreError({
592
+ operation: "read",
593
+ table: "tdd_tasks",
594
+ reason: extractSqlReason(e)
595
+ })));
596
+ if (rows.length === 0) return yield* Effect.fail(new TddTaskNotFoundError({
597
+ id: tddTaskId,
598
+ reason: "no tdd_tasks row for that id"
599
+ }));
600
+ const row = rows[0];
601
+ if (row.ended_at !== null) return yield* Effect.fail(new TddTaskAlreadyEndedError({
602
+ id: tddTaskId,
603
+ endedAt: row.ended_at,
604
+ outcome: row.outcome ?? "abandoned"
605
+ }));
606
+ });
607
+ const ensureTddTaskExists = (tddTaskId) => Effect.gen(function* () {
608
+ if ((yield* sql`SELECT id FROM tdd_tasks WHERE id = ${tddTaskId}`.pipe(Effect.mapError((e) => new DataStoreError({
609
+ operation: "read",
610
+ table: "tdd_tasks",
611
+ reason: extractSqlReason(e)
612
+ })))).length === 0) return yield* Effect.fail(new TddTaskNotFoundError({
613
+ id: tddTaskId,
614
+ reason: "no tdd_tasks row for that id"
615
+ }));
616
+ });
617
+ const createGoal = (input) => Effect.gen(function* () {
618
+ yield* Effect.logDebug("createGoal").pipe(Effect.annotateLogs({
619
+ tddTaskId: input.tddTaskId,
620
+ goal: input.goal
621
+ }));
622
+ yield* ensureTddTaskOpen(input.tddTaskId);
623
+ const rows = yield* sql`
624
+ INSERT INTO tdd_session_goals (session_id, ordinal, goal)
625
+ SELECT ${input.tddTaskId},
626
+ COALESCE(MAX(ordinal), -1) + 1,
627
+ ${input.goal}
628
+ FROM tdd_session_goals
629
+ WHERE session_id = ${input.tddTaskId}
630
+ RETURNING id, session_id, ordinal, goal, status, created_at
631
+ `.pipe(Effect.mapError((e) => new DataStoreError({
632
+ operation: "write",
633
+ table: "tdd_session_goals",
634
+ reason: extractSqlReason(e)
635
+ })));
636
+ return goalRowFromDb(rows[0]);
637
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
638
+ const getGoal = (id) => Effect.gen(function* () {
639
+ const rows = yield* sql`
640
+ SELECT id, session_id, ordinal, goal, status, created_at
641
+ FROM tdd_session_goals
642
+ WHERE id = ${id}
643
+ `;
644
+ return rows.length === 0 ? Option.none() : Option.some(goalRowFromDb(rows[0]));
645
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
646
+ operation: "read",
647
+ table: "tdd_session_goals",
648
+ reason: extractSqlReason(e)
649
+ })));
650
+ const updateGoal = (input) => Effect.gen(function* () {
651
+ yield* Effect.logDebug("updateGoal").pipe(Effect.annotateLogs({ id: input.id }));
652
+ const existing = yield* sql`
653
+ SELECT id, session_id, ordinal, goal, status, created_at
654
+ FROM tdd_session_goals
655
+ WHERE id = ${input.id}
656
+ `.pipe(Effect.mapError((e) => new DataStoreError({
657
+ operation: "read",
658
+ table: "tdd_session_goals",
659
+ reason: extractSqlReason(e)
660
+ })));
661
+ if (existing.length === 0) return yield* Effect.fail(new GoalNotFoundError({
662
+ id: input.id,
663
+ reason: "no tdd_session_goals row for that id"
664
+ }));
665
+ const current = existing[0];
666
+ yield* ensureTddTaskOpen(current.session_id).pipe(Effect.catchTag("TddTaskNotFoundError", (e) => Effect.fail(new DataStoreError({
667
+ operation: "read",
668
+ table: "tdd_tasks",
669
+ reason: `FK integrity violation: goal ${input.id} references missing tdd_tasks row ${e.id}`
670
+ }))));
671
+ const fromStatus = current.status;
672
+ const toStatus = input.status ?? fromStatus;
673
+ if (input.status !== void 0 && !isLegalLifecycleTransition(fromStatus, input.status)) return yield* Effect.fail(new IllegalStatusTransitionError({
674
+ entity: "goal",
675
+ id: input.id,
676
+ from: fromStatus,
677
+ to: input.status,
678
+ reason: "transition forbidden by goal lifecycle rules"
679
+ }));
680
+ const newGoalText = input.goal ?? current.goal;
681
+ yield* sql`
682
+ UPDATE tdd_session_goals
683
+ SET goal = ${newGoalText},
684
+ status = ${toStatus}
685
+ WHERE id = ${input.id}
686
+ `.pipe(Effect.mapError((e) => new DataStoreError({
687
+ operation: "write",
688
+ table: "tdd_session_goals",
689
+ reason: extractSqlReason(e)
690
+ })));
691
+ return {
692
+ id: current.id,
693
+ sessionId: current.session_id,
694
+ ordinal: current.ordinal,
695
+ goal: newGoalText,
696
+ status: toStatus,
697
+ createdAt: current.created_at
698
+ };
699
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
700
+ const deleteGoal = (id) => Effect.gen(function* () {
701
+ yield* Effect.logDebug("deleteGoal").pipe(Effect.annotateLogs({ id }));
702
+ if ((yield* sql`SELECT id FROM tdd_session_goals WHERE id = ${id}`.pipe(Effect.mapError((e) => new DataStoreError({
703
+ operation: "read",
704
+ table: "tdd_session_goals",
705
+ reason: extractSqlReason(e)
706
+ })))).length === 0) return yield* Effect.fail(new GoalNotFoundError({
707
+ id,
708
+ reason: "no tdd_session_goals row for that id"
709
+ }));
710
+ yield* sql`DELETE FROM tdd_session_goals WHERE id = ${id}`.pipe(Effect.mapError((e) => new DataStoreError({
711
+ operation: "write",
712
+ table: "tdd_session_goals",
713
+ reason: extractSqlReason(e)
714
+ })));
715
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
716
+ const listGoalsByTddTask = (tddTaskId) => Effect.gen(function* () {
717
+ yield* ensureTddTaskExists(tddTaskId);
718
+ return (yield* sql`
719
+ SELECT id, session_id, ordinal, goal, status, created_at
720
+ FROM tdd_session_goals
721
+ WHERE session_id = ${tddTaskId}
722
+ ORDER BY ordinal
723
+ `.pipe(Effect.mapError((e) => new DataStoreError({
724
+ operation: "read",
725
+ table: "tdd_session_goals",
726
+ reason: extractSqlReason(e)
727
+ })))).map(goalRowFromDb);
728
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
729
+ const ensureGoalOpenAndTaskOpen = (goalId) => Effect.gen(function* () {
730
+ const goals = yield* sql`
731
+ SELECT id, session_id, status FROM tdd_session_goals WHERE id = ${goalId}
732
+ `.pipe(Effect.mapError((e) => new DataStoreError({
733
+ operation: "read",
734
+ table: "tdd_session_goals",
735
+ reason: extractSqlReason(e)
736
+ })));
737
+ if (goals.length === 0) return yield* Effect.fail(new GoalNotFoundError({
738
+ id: goalId,
739
+ reason: "no tdd_session_goals row"
740
+ }));
741
+ const goal = goals[0];
742
+ yield* ensureTddTaskOpen(goal.session_id).pipe(Effect.catchTag("TddTaskNotFoundError", (e) => Effect.fail(new DataStoreError({
743
+ operation: "read",
744
+ table: "tdd_tasks",
745
+ reason: `FK integrity violation: goal ${goalId} references missing tdd_tasks row ${e.id}`
746
+ }))));
747
+ if (goal.status === "done" || goal.status === "abandoned") return yield* Effect.fail(new IllegalStatusTransitionError({
748
+ entity: "goal",
749
+ id: goalId,
750
+ from: goal.status,
751
+ to: "in_progress",
752
+ reason: "cannot create a behavior under a closed goal"
753
+ }));
754
+ return {
755
+ goalTddTaskId: goal.session_id,
756
+ goalStatus: goal.status
757
+ };
758
+ });
759
+ const writeBehaviorDependencies = (behaviorId, goalId, depIds) => Effect.gen(function* () {
760
+ const uniqueDepIds = Array.from(new Set(depIds));
761
+ if (uniqueDepIds.length === 0) return;
762
+ const verified = yield* sql`
763
+ SELECT id FROM tdd_session_behaviors
764
+ WHERE goal_id = ${goalId} AND id IN ${sql.in(uniqueDepIds)}
765
+ `.pipe(Effect.mapError((e) => new DataStoreError({
766
+ operation: "read",
767
+ table: "tdd_session_behaviors",
768
+ reason: extractSqlReason(e)
769
+ })));
770
+ const verifiedIds = new Set(verified.map((r) => r.id));
771
+ for (const depId of uniqueDepIds) if (!verifiedIds.has(depId)) return yield* Effect.fail(new BehaviorNotFoundError({
772
+ id: depId,
773
+ reason: `dependency id ${depId} does not belong to goal ${goalId}`
774
+ }));
775
+ for (const depId of uniqueDepIds) yield* sql`
776
+ INSERT INTO tdd_behavior_dependencies (behavior_id, depends_on_id)
777
+ VALUES (${behaviorId}, ${depId})
778
+ `.pipe(Effect.mapError((e) => new DataStoreError({
779
+ operation: "write",
780
+ table: "tdd_behavior_dependencies",
781
+ reason: extractSqlReason(e)
782
+ })));
783
+ });
784
+ const createBehavior = (input) => sql.withTransaction(Effect.gen(function* () {
785
+ yield* Effect.logDebug("createBehavior").pipe(Effect.annotateLogs({
786
+ goalId: input.goalId,
787
+ behavior: input.behavior
788
+ }));
789
+ yield* ensureGoalOpenAndTaskOpen(input.goalId);
790
+ const beh = (yield* sql`
791
+ INSERT INTO tdd_session_behaviors (goal_id, ordinal, behavior, suggested_test_name)
792
+ SELECT ${input.goalId},
793
+ COALESCE(MAX(ordinal), -1) + 1,
794
+ ${input.behavior},
795
+ ${input.suggestedTestName ?? null}
796
+ FROM tdd_session_behaviors
797
+ WHERE goal_id = ${input.goalId}
798
+ RETURNING id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
799
+ `.pipe(Effect.mapError((e) => new DataStoreError({
800
+ operation: "write",
801
+ table: "tdd_session_behaviors",
802
+ reason: extractSqlReason(e)
803
+ }))))[0];
804
+ if (input.dependsOnBehaviorIds && input.dependsOnBehaviorIds.length > 0) yield* writeBehaviorDependencies(beh.id, input.goalId, input.dependsOnBehaviorIds);
805
+ return behaviorRowFromDb(beh);
806
+ })).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => e instanceof DataStoreError || e instanceof GoalNotFoundError || e instanceof BehaviorNotFoundError || e instanceof TddTaskAlreadyEndedError || e instanceof IllegalStatusTransitionError ? e : new DataStoreError({
807
+ operation: "write",
808
+ table: "tdd_session_behaviors",
809
+ reason: extractSqlReason(e)
810
+ })));
811
+ const getBehavior = (id) => Effect.gen(function* () {
812
+ const rows = yield* sql`
813
+ SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
814
+ FROM tdd_session_behaviors
815
+ WHERE id = ${id}
816
+ `;
817
+ return rows.length === 0 ? Option.none() : Option.some(behaviorRowFromDb(rows[0]));
818
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
819
+ operation: "read",
820
+ table: "tdd_session_behaviors",
821
+ reason: extractSqlReason(e)
822
+ })));
823
+ const updateBehavior = (input) => sql.withTransaction(Effect.gen(function* () {
824
+ yield* Effect.logDebug("updateBehavior").pipe(Effect.annotateLogs({ id: input.id }));
825
+ const existing = yield* sql`
826
+ SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
827
+ FROM tdd_session_behaviors
828
+ WHERE id = ${input.id}
829
+ `.pipe(Effect.mapError((e) => new DataStoreError({
830
+ operation: "read",
831
+ table: "tdd_session_behaviors",
832
+ reason: extractSqlReason(e)
833
+ })));
834
+ if (existing.length === 0) return yield* Effect.fail(new BehaviorNotFoundError({
835
+ id: input.id,
836
+ reason: "no tdd_session_behaviors row for that id"
837
+ }));
838
+ const current = existing[0];
839
+ const goalRows = yield* sql`
840
+ SELECT session_id FROM tdd_session_goals WHERE id = ${current.goal_id}
841
+ `.pipe(Effect.mapError((e) => new DataStoreError({
842
+ operation: "read",
843
+ table: "tdd_session_goals",
844
+ reason: extractSqlReason(e)
845
+ })));
846
+ if (goalRows.length === 0) return yield* Effect.fail(new DataStoreError({
847
+ operation: "read",
848
+ table: "tdd_session_goals",
849
+ reason: `FK integrity violation: behavior ${input.id} references missing tdd_session_goals row ${current.goal_id}`
850
+ }));
851
+ yield* ensureTddTaskOpen(goalRows[0].session_id).pipe(Effect.catchTag("TddTaskNotFoundError", (e) => Effect.fail(new DataStoreError({
852
+ operation: "read",
853
+ table: "tdd_tasks",
854
+ reason: `FK integrity violation: goal ${current.goal_id} references missing tdd_tasks row ${e.id}`
855
+ }))));
856
+ const fromStatus = current.status;
857
+ if (input.status !== void 0 && !isLegalLifecycleTransition(fromStatus, input.status)) return yield* Effect.fail(new IllegalStatusTransitionError({
858
+ entity: "behavior",
859
+ id: input.id,
860
+ from: fromStatus,
861
+ to: input.status,
862
+ reason: "transition forbidden by behavior lifecycle rules"
863
+ }));
864
+ const newBehaviorText = input.behavior ?? current.behavior;
865
+ const newStatus = input.status ?? fromStatus;
866
+ const newSuggested = input.suggestedTestName === void 0 ? current.suggested_test_name : input.suggestedTestName;
867
+ yield* sql`
868
+ UPDATE tdd_session_behaviors
869
+ SET behavior = ${newBehaviorText},
870
+ suggested_test_name = ${newSuggested},
871
+ status = ${newStatus}
872
+ WHERE id = ${input.id}
873
+ `.pipe(Effect.mapError((e) => new DataStoreError({
874
+ operation: "write",
875
+ table: "tdd_session_behaviors",
876
+ reason: extractSqlReason(e)
877
+ })));
878
+ if (input.dependsOnBehaviorIds !== void 0) {
879
+ yield* sql`DELETE FROM tdd_behavior_dependencies WHERE behavior_id = ${input.id}`.pipe(Effect.mapError((e) => new DataStoreError({
880
+ operation: "write",
881
+ table: "tdd_behavior_dependencies",
882
+ reason: extractSqlReason(e)
883
+ })));
884
+ if (input.dependsOnBehaviorIds.length > 0) yield* writeBehaviorDependencies(input.id, current.goal_id, input.dependsOnBehaviorIds);
885
+ }
886
+ return {
887
+ id: current.id,
888
+ goalId: current.goal_id,
889
+ ordinal: current.ordinal,
890
+ behavior: newBehaviorText,
891
+ suggestedTestName: newSuggested,
892
+ status: newStatus,
893
+ createdAt: current.created_at
894
+ };
895
+ })).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => e instanceof DataStoreError || e instanceof BehaviorNotFoundError || e instanceof TddTaskAlreadyEndedError || e instanceof IllegalStatusTransitionError ? e : new DataStoreError({
896
+ operation: "write",
897
+ table: "tdd_session_behaviors",
898
+ reason: extractSqlReason(e)
899
+ })));
900
+ const deleteBehavior = (id) => Effect.gen(function* () {
901
+ yield* Effect.logDebug("deleteBehavior").pipe(Effect.annotateLogs({ id }));
902
+ if ((yield* sql`SELECT id FROM tdd_session_behaviors WHERE id = ${id}`.pipe(Effect.mapError((e) => new DataStoreError({
903
+ operation: "read",
904
+ table: "tdd_session_behaviors",
905
+ reason: extractSqlReason(e)
906
+ })))).length === 0) return yield* Effect.fail(new BehaviorNotFoundError({
907
+ id,
908
+ reason: "no tdd_session_behaviors row for that id"
909
+ }));
910
+ yield* sql`DELETE FROM tdd_session_behaviors WHERE id = ${id}`.pipe(Effect.mapError((e) => new DataStoreError({
911
+ operation: "write",
912
+ table: "tdd_session_behaviors",
913
+ reason: extractSqlReason(e)
914
+ })));
915
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
916
+ const ensureGoalExists = (goalId) => Effect.gen(function* () {
917
+ if ((yield* sql`SELECT id FROM tdd_session_goals WHERE id = ${goalId}`.pipe(Effect.mapError((e) => new DataStoreError({
918
+ operation: "read",
919
+ table: "tdd_session_goals",
920
+ reason: extractSqlReason(e)
921
+ })))).length === 0) return yield* Effect.fail(new GoalNotFoundError({
922
+ id: goalId,
923
+ reason: "no tdd_session_goals row"
924
+ }));
925
+ });
926
+ const listBehaviorsByGoal = (goalId) => Effect.gen(function* () {
927
+ yield* ensureGoalExists(goalId);
928
+ return (yield* sql`
929
+ SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
930
+ FROM tdd_session_behaviors
931
+ WHERE goal_id = ${goalId}
932
+ ORDER BY ordinal
933
+ `.pipe(Effect.mapError((e) => new DataStoreError({
934
+ operation: "read",
935
+ table: "tdd_session_behaviors",
936
+ reason: extractSqlReason(e)
937
+ })))).map(behaviorRowFromDb);
938
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
939
+ const listBehaviorsByTddTask = (tddTaskId) => Effect.gen(function* () {
940
+ yield* ensureTddTaskExists(tddTaskId);
941
+ return (yield* sql`
942
+ SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
943
+ FROM tdd_session_behaviors b
944
+ JOIN tdd_session_goals g ON g.id = b.goal_id
945
+ WHERE g.session_id = ${tddTaskId}
946
+ ORDER BY g.ordinal, b.ordinal
947
+ `.pipe(Effect.mapError((e) => new DataStoreError({
948
+ operation: "read",
949
+ table: "tdd_session_behaviors",
950
+ reason: extractSqlReason(e)
951
+ })))).map(behaviorRowFromDb);
952
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
953
+ const writeTddArtifact = (input) => Effect.gen(function* () {
954
+ yield* Effect.logDebug("writeTddArtifact").pipe(Effect.annotateLogs({
955
+ phaseId: input.phaseId,
956
+ artifactKind: input.artifactKind
957
+ }));
958
+ const truncatedDiff = input.diffExcerpt !== void 0 && input.diffExcerpt.length > 4096 ? input.diffExcerpt.slice(0, 4096) : input.diffExcerpt ?? null;
959
+ return (yield* sql`
960
+ INSERT INTO tdd_artifacts
961
+ (phase_id, artifact_kind, file_id, test_case_id, test_run_id,
962
+ test_first_failure_run_id, diff_excerpt, recorded_at, suite)
963
+ VALUES
964
+ (
965
+ ${input.phaseId},
966
+ ${input.artifactKind},
967
+ ${input.fileId ?? null},
968
+ ${input.testCaseId ?? null},
969
+ ${input.testRunId ?? null},
970
+ ${input.testFirstFailureRunId ?? null},
971
+ ${truncatedDiff},
972
+ ${input.recordedAt},
973
+ ${input.suite ?? "vitest"}
974
+ )
975
+ RETURNING id
976
+ `)[0].id;
977
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
978
+ operation: "write",
979
+ table: "tdd_artifacts",
980
+ reason: extractSqlReason(e)
981
+ })));
982
+ const writeTddPhase = (input) => Effect.gen(function* () {
983
+ yield* Effect.logDebug("writeTddPhase").pipe(Effect.annotateLogs({
984
+ tddTaskId: input.tddTaskId,
985
+ phase: input.phase
986
+ }));
987
+ const open = yield* sql`
988
+ SELECT id FROM tdd_phases
989
+ WHERE tdd_task_id = ${input.tddTaskId} AND ended_at IS NULL
990
+ ORDER BY started_at DESC LIMIT 1
991
+ `;
992
+ const previousPhaseId = open.length === 0 ? null : open[0].id;
993
+ if (previousPhaseId !== null) yield* sql`
994
+ UPDATE tdd_phases SET ended_at = ${input.startedAt}
995
+ WHERE id = ${previousPhaseId}
996
+ `;
997
+ return {
998
+ id: (yield* sql`
999
+ INSERT INTO tdd_phases
1000
+ (tdd_task_id, behavior_id, phase, started_at, transition_reason, parent_phase_id)
1001
+ VALUES
1002
+ (
1003
+ ${input.tddTaskId},
1004
+ ${input.behaviorId ?? null},
1005
+ ${input.phase},
1006
+ ${input.startedAt},
1007
+ ${input.transitionReason ?? null},
1008
+ ${input.parentPhaseId ?? null}
1009
+ )
1010
+ RETURNING id
1011
+ `)[0].id,
1012
+ previousPhaseId
1013
+ };
1014
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1015
+ operation: "write",
1016
+ table: "tdd_phases",
1017
+ reason: extractSqlReason(e)
1018
+ })));
1019
+ const writeCommit = (input) => Effect.gen(function* () {
1020
+ yield* Effect.logDebug("writeCommit").pipe(Effect.annotateLogs({ sha: input.sha }));
1021
+ yield* sql`
1022
+ INSERT INTO commits (sha, parent_sha, message, author, committed_at, branch)
1023
+ VALUES (
1024
+ ${input.sha},
1025
+ ${input.parentSha ?? null},
1026
+ ${input.message ?? null},
1027
+ ${input.author ?? null},
1028
+ ${input.committedAt ?? null},
1029
+ ${input.branch ?? null}
1030
+ )
1031
+ ON CONFLICT(sha) DO NOTHING
1032
+ `;
1033
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1034
+ operation: "write",
1035
+ table: "commits",
1036
+ reason: extractSqlReason(e)
1037
+ })));
1038
+ const writeRunChangedFiles = (input) => Effect.gen(function* () {
1039
+ yield* Effect.logDebug("writeRunChangedFiles").pipe(Effect.annotateLogs({
1040
+ runId: input.runId,
1041
+ count: input.files.length
1042
+ }));
1043
+ for (const file of input.files) {
1044
+ const fileId = yield* ensureFile(file.filePath);
1045
+ yield* sql`
1046
+ INSERT INTO run_changed_files (run_id, file_id, change_kind, commit_sha)
1047
+ VALUES (${input.runId}, ${fileId}, ${file.changeKind}, ${file.commitSha ?? null})
1048
+ ON CONFLICT(run_id, file_id) DO UPDATE SET
1049
+ change_kind = excluded.change_kind,
1050
+ commit_sha = excluded.commit_sha
1051
+ `;
1052
+ }
1053
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1054
+ operation: "write",
1055
+ table: "run_changed_files",
1056
+ reason: extractSqlReason(e)
1057
+ })));
1058
+ const recordIdempotentResponse = (input) => Effect.gen(function* () {
1059
+ yield* Effect.logDebug("recordIdempotentResponse").pipe(Effect.annotateLogs({
1060
+ procedurePath: input.procedurePath,
1061
+ key: input.key
1062
+ }));
1063
+ yield* sql`INSERT INTO mcp_idempotent_responses (procedure_path, key, result_json, created_at) VALUES (${input.procedurePath}, ${input.key}, ${input.resultJson}, ${input.createdAt}) ON CONFLICT(procedure_path, key) DO NOTHING`;
1064
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1065
+ operation: "write",
1066
+ table: "mcp_idempotent_responses",
1067
+ reason: extractSqlReason(e)
1068
+ })));
1069
+ const pruneSessions = (keepRecent) => Effect.gen(function* () {
1070
+ yield* Effect.logDebug("pruneSessions").pipe(Effect.annotateLogs({ keepRecent }));
1071
+ const cutoffRows = yield* sql`
1072
+ SELECT started_at FROM sessions ORDER BY started_at DESC LIMIT 1 OFFSET ${keepRecent}
1073
+ `;
1074
+ if (cutoffRows.length === 0) return {
1075
+ affectedSessions: 0,
1076
+ prunedTurns: 0
1077
+ };
1078
+ const cutoff = cutoffRows[0].started_at;
1079
+ const prunedTurns = (yield* sql`
1080
+ SELECT COUNT(*) AS count FROM turns
1081
+ WHERE session_id IN (SELECT id FROM sessions WHERE started_at <= ${cutoff})
1082
+ `)[0]?.count ?? 0;
1083
+ const affectedSessions = (yield* sql`
1084
+ SELECT COUNT(*) AS count FROM sessions WHERE started_at <= ${cutoff}
1085
+ `)[0]?.count ?? 0;
1086
+ yield* sql`
1087
+ DELETE FROM turns WHERE session_id IN (
1088
+ SELECT id FROM sessions WHERE started_at <= ${cutoff}
1089
+ )
1090
+ `;
1091
+ return {
1092
+ affectedSessions,
1093
+ prunedTurns
1094
+ };
1095
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1096
+ operation: "write",
1097
+ table: "turns",
1098
+ reason: extractSqlReason(e)
1099
+ })));
1100
+ const associateLatestRunWithSession = (input) => Effect.gen(function* () {
1101
+ yield* Effect.logDebug("associateLatestRunWithSession").pipe(Effect.annotateLogs({
1102
+ chatId: input.chatId,
1103
+ invocationMethod: input.invocationMethod
1104
+ }));
1105
+ yield* sql`
1106
+ INSERT OR IGNORE INTO run_triggers (run_id, trigger, invocation_method, agent_session_id)
1107
+ SELECT r.id, 'agent', ${input.invocationMethod}, s.id
1108
+ FROM (SELECT id FROM test_runs ORDER BY id DESC LIMIT 1) r
1109
+ CROSS JOIN (SELECT id FROM sessions WHERE chat_id = ${input.chatId} LIMIT 1) s
1110
+ `;
1111
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1112
+ operation: "write",
1113
+ table: "run_triggers",
1114
+ reason: extractSqlReason(e)
1115
+ })));
1116
+ const backfillTestCaseTurns = (chatId) => Effect.gen(function* () {
1117
+ yield* Effect.logDebug("backfillTestCaseTurns").pipe(Effect.annotateLogs({ chatId }));
1118
+ yield* sql`
1119
+ UPDATE test_cases
1120
+ SET created_turn_id = (
1121
+ SELECT t.id
1122
+ FROM turns t
1123
+ JOIN file_edits fe ON fe.turn_id = t.id
1124
+ JOIN files f_edit ON fe.file_id = f_edit.id
1125
+ JOIN sessions s ON t.session_id = s.id
1126
+ WHERE s.chat_id = ${chatId}
1127
+ AND EXISTS (
1128
+ SELECT 1
1129
+ FROM test_modules tm
1130
+ JOIN files f_mod ON f_mod.id = tm.file_id
1131
+ WHERE tm.id = test_cases.module_id
1132
+ AND (
1133
+ f_edit.path = f_mod.path
1134
+ OR f_edit.path LIKE '%/' || f_mod.path
1135
+ OR f_mod.path LIKE '%/' || f_edit.path
1136
+ )
1137
+ )
1138
+ ORDER BY t.occurred_at DESC
1139
+ LIMIT 1
1140
+ )
1141
+ WHERE test_cases.created_turn_id IS NULL
1142
+ AND test_cases.module_id IN (
1143
+ SELECT id FROM test_modules
1144
+ WHERE run_id = (SELECT id FROM test_runs ORDER BY id DESC LIMIT 1)
1145
+ )
1146
+ `;
1147
+ return (yield* sql`SELECT changes() AS n`)[0]?.n ?? 0;
1148
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
1149
+ operation: "write",
1150
+ table: "test_cases",
1151
+ reason: extractSqlReason(e)
1152
+ })));
1153
+ const registerAgent = (input) => Effect.gen(function* () {
1154
+ yield* Effect.logDebug("registerAgent").pipe(Effect.annotateLogs({
1155
+ sessionId: input.sessionId,
1156
+ agentType: input.agentType
1157
+ }));
1158
+ const existing = yield* sql`
1159
+ SELECT agent_id FROM agents WHERE session_id = ${input.sessionId} AND idempotency_key = ${input.idempotencyKey}
1160
+ `.pipe(Effect.mapError((e) => new DataStoreError({
1161
+ operation: "read",
1162
+ table: "agents",
1163
+ reason: extractSqlReason(e)
1164
+ })));
1165
+ if (existing.length > 0) return new IdempotencyHit({ existingAgentId: existing[0].agent_id });
1166
+ if (input.parentAgentId !== null) {
1167
+ const parentRows = yield* sql`
1168
+ SELECT session_id FROM agents WHERE agent_id = ${input.parentAgentId}
1169
+ `.pipe(Effect.mapError((e) => new DataStoreError({
1170
+ operation: "read",
1171
+ table: "agents",
1172
+ reason: extractSqlReason(e)
1173
+ })));
1174
+ if (parentRows.length === 0) return yield* Effect.fail(new RegistrationConflictError({ reason: `parent agent ${input.parentAgentId} does not exist` }));
1175
+ const registeringParentSessionId = (yield* sql`
1176
+ SELECT parent_session_id FROM sessions WHERE id = ${input.sessionId} LIMIT 1
1177
+ `.pipe(Effect.mapError((e) => new DataStoreError({
1178
+ operation: "read",
1179
+ table: "sessions",
1180
+ reason: extractSqlReason(e)
1181
+ }))))[0]?.parent_session_id ?? null;
1182
+ const parentSession = parentRows[0].session_id;
1183
+ if (parentSession !== input.sessionId && parentSession !== registeringParentSessionId) return yield* Effect.fail(new RegistrationConflictError({ reason: `parent agent ${input.parentAgentId} belongs to session ${parentSession}, which is neither the registering session ${input.sessionId} nor its parent session ${registeringParentSessionId ?? "(none)"}` }));
1184
+ }
1185
+ const agentId = input.agentId ?? randomUUID();
1186
+ yield* sql`
1187
+ INSERT INTO agents (
1188
+ agent_id, session_id, parent_agent_id, conversation_id, agent_type,
1189
+ started_at, ended_at, start_git_branch, start_git_commit_sha, start_worktree_dir,
1190
+ idempotency_key
1191
+ ) VALUES (
1192
+ ${agentId}, ${input.sessionId}, ${input.parentAgentId}, ${input.conversationId},
1193
+ ${input.agentType}, ${input.startedAt}, ${null}, ${input.startGitBranch ?? null},
1194
+ ${input.startGitCommitSha ?? null}, ${input.startWorktreeDir ?? null}, ${input.idempotencyKey}
1195
+ )
1196
+ `.pipe(Effect.mapError((e) => new DataStoreError({
1197
+ operation: "write",
1198
+ table: "agents",
1199
+ reason: extractSqlReason(e)
1200
+ })));
1201
+ return new Agent({
1202
+ agentId,
1203
+ sessionId: input.sessionId,
1204
+ parentAgentId: input.parentAgentId,
1205
+ conversationId: input.conversationId,
1206
+ agentType: input.agentType,
1207
+ startedAt: input.startedAt,
1208
+ endedAt: null,
1209
+ startGitBranch: input.startGitBranch ?? null,
1210
+ startGitCommitSha: input.startGitCommitSha ?? null,
1211
+ startWorktreeDir: input.startWorktreeDir ?? null,
1212
+ idempotencyKey: input.idempotencyKey
1213
+ });
1214
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
1215
+ const endAgent = (agentId, endedAt) => Effect.gen(function* () {
1216
+ yield* Effect.logDebug("endAgent").pipe(Effect.annotateLogs({ agentId }));
1217
+ yield* sql`UPDATE agents SET ended_at = ${endedAt} WHERE agent_id = ${agentId}`.pipe(Effect.mapError((e) => new DataStoreError({
1218
+ operation: "write",
1219
+ table: "agents",
1220
+ reason: extractSqlReason(e)
1221
+ })));
1222
+ if (((yield* sql`SELECT changes() AS n`.pipe(Effect.mapError((e) => new DataStoreError({
1223
+ operation: "read",
1224
+ table: "agents",
1225
+ reason: extractSqlReason(e)
1226
+ }))))[0]?.n ?? 0) === 0) return yield* Effect.fail(new AgentNotFoundError({ agentId }));
1227
+ }).pipe(Effect.annotateLogs("service", "DataStore"));
1228
+ return {
1229
+ ensureFile,
1230
+ writeSettings,
1231
+ writeRun,
1232
+ writeModules,
1233
+ writeSuites,
1234
+ writeTestCases,
1235
+ writeErrors,
1236
+ writeAnnotations,
1237
+ writeArtifacts,
1238
+ writeCoverage,
1239
+ writeHistory,
1240
+ writeBaselines,
1241
+ writeThresholds,
1242
+ writeTargets,
1243
+ writeTrends,
1244
+ writeSourceMap,
1245
+ writeNote,
1246
+ updateNote,
1247
+ deleteNote,
1248
+ writeSession,
1249
+ upsertSession,
1250
+ setSessionConversationIdIfNull,
1251
+ writeTurn,
1252
+ writeFailureSignature,
1253
+ endSession,
1254
+ writeHypothesis,
1255
+ validateHypothesis,
1256
+ writeTddTask,
1257
+ endTddTask,
1258
+ createGoal,
1259
+ getGoal,
1260
+ updateGoal,
1261
+ deleteGoal,
1262
+ listGoalsByTddTask,
1263
+ createBehavior,
1264
+ getBehavior,
1265
+ updateBehavior,
1266
+ deleteBehavior,
1267
+ listBehaviorsByGoal,
1268
+ listBehaviorsByTddTask,
1269
+ writeTddPhase,
1270
+ writeTddArtifact,
1271
+ writeCommit,
1272
+ writeRunChangedFiles,
1273
+ recordIdempotentResponse,
1274
+ pruneSessions,
1275
+ associateLatestRunWithSession,
1276
+ backfillTestCaseTurns,
1277
+ registerAgent,
1278
+ endAgent
1279
+ };
1280
+ }));
1281
+
1282
+ //#endregion
1283
+ export { DataStoreLive };