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