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