astrocode-workflow 0.4.0 → 0.4.1

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 (147) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/shared/metrics.d.ts +66 -0
  3. package/dist/shared/metrics.js +112 -0
  4. package/dist/src/agents/commands.d.ts +9 -0
  5. package/dist/src/agents/commands.js +121 -0
  6. package/dist/src/agents/prompts.d.ts +3 -0
  7. package/dist/src/agents/prompts.js +232 -0
  8. package/dist/src/agents/registry.d.ts +6 -0
  9. package/dist/src/agents/registry.js +242 -0
  10. package/dist/src/agents/types.d.ts +14 -0
  11. package/dist/src/agents/types.js +8 -0
  12. package/dist/src/astro/workflow-runner.d.ts +15 -0
  13. package/dist/src/astro/workflow-runner.js +25 -0
  14. package/dist/src/config/config-handler.d.ts +4 -0
  15. package/dist/src/config/config-handler.js +46 -0
  16. package/dist/src/config/defaults.d.ts +3 -0
  17. package/dist/src/config/defaults.js +3 -0
  18. package/dist/src/config/loader.d.ts +11 -0
  19. package/dist/src/config/loader.js +82 -0
  20. package/dist/src/config/schema.d.ts +195 -0
  21. package/dist/src/config/schema.js +224 -0
  22. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  23. package/dist/src/hooks/continuation-enforcer.js +190 -0
  24. package/dist/src/hooks/inject-provider.d.ts +27 -0
  25. package/dist/src/hooks/inject-provider.js +189 -0
  26. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  27. package/dist/src/hooks/tool-output-truncator.js +57 -0
  28. package/dist/src/index.d.ts +3 -0
  29. package/dist/src/index.js +313 -0
  30. package/dist/src/shared/deep-merge.d.ts +8 -0
  31. package/dist/src/shared/deep-merge.js +25 -0
  32. package/dist/src/shared/hash.d.ts +1 -0
  33. package/dist/src/shared/hash.js +4 -0
  34. package/dist/src/shared/log.d.ts +7 -0
  35. package/dist/src/shared/log.js +24 -0
  36. package/dist/src/shared/metrics.d.ts +66 -0
  37. package/dist/src/shared/metrics.js +112 -0
  38. package/dist/src/shared/model-tuning.d.ts +9 -0
  39. package/dist/src/shared/model-tuning.js +28 -0
  40. package/dist/src/shared/paths.d.ts +19 -0
  41. package/dist/src/shared/paths.js +64 -0
  42. package/dist/src/shared/text.d.ts +4 -0
  43. package/dist/src/shared/text.js +19 -0
  44. package/dist/src/shared/time.d.ts +1 -0
  45. package/dist/src/shared/time.js +3 -0
  46. package/dist/src/state/adapters/index.d.ts +41 -0
  47. package/dist/src/state/adapters/index.js +115 -0
  48. package/dist/src/state/db.d.ts +16 -0
  49. package/dist/src/state/db.js +225 -0
  50. package/dist/src/state/ids.d.ts +8 -0
  51. package/dist/src/state/ids.js +25 -0
  52. package/dist/src/state/repo-lock.d.ts +67 -0
  53. package/dist/src/state/repo-lock.js +580 -0
  54. package/dist/src/state/schema.d.ts +2 -0
  55. package/dist/src/state/schema.js +258 -0
  56. package/dist/src/state/types.d.ts +71 -0
  57. package/dist/src/state/types.js +1 -0
  58. package/dist/src/state/workflow-repo-lock.d.ts +23 -0
  59. package/dist/src/state/workflow-repo-lock.js +83 -0
  60. package/dist/src/tools/artifacts.d.ts +18 -0
  61. package/dist/src/tools/artifacts.js +71 -0
  62. package/dist/src/tools/health.d.ts +8 -0
  63. package/dist/src/tools/health.js +119 -0
  64. package/dist/src/tools/index.d.ts +20 -0
  65. package/dist/src/tools/index.js +97 -0
  66. package/dist/src/tools/init.d.ts +17 -0
  67. package/dist/src/tools/init.js +96 -0
  68. package/dist/src/tools/injects.d.ts +53 -0
  69. package/dist/src/tools/injects.js +325 -0
  70. package/dist/src/tools/lock.d.ts +4 -0
  71. package/dist/src/tools/lock.js +78 -0
  72. package/dist/src/tools/metrics.d.ts +7 -0
  73. package/dist/src/tools/metrics.js +61 -0
  74. package/dist/src/tools/repair.d.ts +8 -0
  75. package/dist/src/tools/repair.js +59 -0
  76. package/dist/src/tools/reset.d.ts +8 -0
  77. package/dist/src/tools/reset.js +92 -0
  78. package/dist/src/tools/run.d.ts +13 -0
  79. package/dist/src/tools/run.js +54 -0
  80. package/dist/src/tools/spec.d.ts +12 -0
  81. package/dist/src/tools/spec.js +44 -0
  82. package/dist/src/tools/stage.d.ts +23 -0
  83. package/dist/src/tools/stage.js +371 -0
  84. package/dist/src/tools/status.d.ts +8 -0
  85. package/dist/src/tools/status.js +125 -0
  86. package/dist/src/tools/story.d.ts +23 -0
  87. package/dist/src/tools/story.js +85 -0
  88. package/dist/src/tools/workflow.d.ts +13 -0
  89. package/dist/src/tools/workflow.js +359 -0
  90. package/dist/src/ui/inject.d.ts +12 -0
  91. package/dist/src/ui/inject.js +107 -0
  92. package/dist/src/ui/toasts.d.ts +13 -0
  93. package/dist/src/ui/toasts.js +39 -0
  94. package/dist/src/workflow/artifacts.d.ts +24 -0
  95. package/dist/src/workflow/artifacts.js +45 -0
  96. package/dist/src/workflow/baton.d.ts +72 -0
  97. package/dist/src/workflow/baton.js +166 -0
  98. package/dist/src/workflow/context.d.ts +20 -0
  99. package/dist/src/workflow/context.js +113 -0
  100. package/dist/src/workflow/directives.d.ts +39 -0
  101. package/dist/src/workflow/directives.js +137 -0
  102. package/dist/src/workflow/repair.d.ts +8 -0
  103. package/dist/src/workflow/repair.js +99 -0
  104. package/dist/src/workflow/state-machine.d.ts +86 -0
  105. package/dist/src/workflow/state-machine.js +216 -0
  106. package/dist/src/workflow/story-helpers.d.ts +9 -0
  107. package/dist/src/workflow/story-helpers.js +13 -0
  108. package/dist/state/db.d.ts +1 -0
  109. package/dist/state/db.js +9 -0
  110. package/dist/state/repo-lock.d.ts +3 -0
  111. package/dist/state/repo-lock.js +29 -0
  112. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  113. package/dist/test/integration/db-transactions.test.js +126 -0
  114. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  115. package/dist/test/integration/injection-metrics.test.js +129 -0
  116. package/dist/tools/health.d.ts +8 -0
  117. package/dist/tools/health.js +119 -0
  118. package/dist/tools/index.js +9 -0
  119. package/dist/tools/metrics.d.ts +7 -0
  120. package/dist/tools/metrics.js +61 -0
  121. package/dist/tools/reset.d.ts +8 -0
  122. package/dist/tools/reset.js +92 -0
  123. package/dist/tools/workflow.js +178 -168
  124. package/dist/ui/inject.js +21 -9
  125. package/package.json +6 -4
  126. package/src/astro/workflow-runner.ts +36 -0
  127. package/src/config/schema.ts +1 -0
  128. package/src/hooks/inject-provider.ts +94 -14
  129. package/src/index.ts +14 -0
  130. package/src/shared/metrics.ts +148 -0
  131. package/src/state/db.ts +10 -1
  132. package/src/state/repo-lock.ts +706 -0
  133. package/src/state/schema.ts +8 -1
  134. package/src/state/workflow-repo-lock.ts +111 -0
  135. package/src/tools/health.ts +128 -0
  136. package/src/tools/index.ts +15 -3
  137. package/src/tools/init.ts +7 -6
  138. package/src/tools/lock.ts +75 -0
  139. package/src/tools/metrics.ts +71 -0
  140. package/src/tools/repair.ts +44 -6
  141. package/src/tools/reset.ts +100 -0
  142. package/src/tools/stage.ts +1 -0
  143. package/src/tools/status.ts +2 -1
  144. package/src/tools/story.ts +1 -0
  145. package/src/tools/workflow.ts +19 -1
  146. package/src/ui/inject.ts +21 -9
  147. package/src/workflow/repair.ts +2 -2
@@ -9,6 +9,11 @@ type ChatMessageInput = {
9
9
  agent: string;
10
10
  };
11
11
 
12
+ type ToolExecuteAfterInput = {
13
+ tool: string;
14
+ sessionID?: string;
15
+ };
16
+
12
17
  type RuntimeState = {
13
18
  db: SqliteDb | null;
14
19
  limitedMode: boolean;
@@ -24,19 +29,30 @@ export function createInjectProvider(opts: {
24
29
  const { db } = runtime;
25
30
 
26
31
  // Cache to avoid re-injecting the same injects repeatedly
32
+ // Map of inject_id -> last injected timestamp
27
33
  const injectedCache = new Map<string, number>();
28
34
 
29
35
  function shouldSkipInject(injectId: string, nowMs: number): boolean {
30
36
  const lastInjected = injectedCache.get(injectId);
31
37
  if (!lastInjected) return false;
32
38
 
33
- // Skip if injected within the last 5 minutes (configurable?)
34
- const cooldownMs = 5 * 60 * 1000;
39
+ // REDUCED cooldown from 5 minutes to 1 minute
40
+ // This allows injects to appear more frequently during workflow
41
+ const cooldownMs = 1 * 60 * 1000;
35
42
  return nowMs - lastInjected < cooldownMs;
36
43
  }
37
44
 
38
45
  function markInjected(injectId: string, nowMs: number) {
39
46
  injectedCache.set(injectId, nowMs);
47
+
48
+ // Clean up old entries to prevent memory leak
49
+ // Remove entries older than 10 minutes
50
+ const tenMinutesAgo = nowMs - (10 * 60 * 1000);
51
+ for (const [id, timestamp] of injectedCache.entries()) {
52
+ if (timestamp < tenMinutesAgo) {
53
+ injectedCache.delete(id);
54
+ }
55
+ }
40
56
  }
41
57
 
42
58
  function getInjectionDiagnostics(nowIso: string, scopeAllowlist: string[], typeAllowlist: string[]): any {
@@ -89,7 +105,7 @@ export function createInjectProvider(opts: {
89
105
  };
90
106
  }
91
107
 
92
- async function injectEligibleInjects(sessionId: string) {
108
+ async function injectEligibleInjects(sessionId: string, context?: string) {
93
109
  const now = nowISO();
94
110
  const nowMs = Date.now();
95
111
 
@@ -115,7 +131,7 @@ export function createInjectProvider(opts: {
115
131
  // Log when no injects are eligible
116
132
  if (EMIT_TELEMETRY) {
117
133
  // eslint-disable-next-line no-console
118
- console.log(`[Astrocode:inject] ${now} selected=${diagnostics.selected_eligible} injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
134
+ console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=0 injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
119
135
  }
120
136
  return;
121
137
  }
@@ -130,21 +146,68 @@ export function createInjectProvider(opts: {
130
146
  // Format as injection message
131
147
  const formattedText = `[Inject: ${inject.title}]\n\n${inject.body_md}`;
132
148
 
149
+ try {
150
+ await injectChatPrompt({
151
+ ctx,
152
+ sessionId,
153
+ text: formattedText,
154
+ agent: "Astrocode"
155
+ });
156
+
157
+ injected++;
158
+ markInjected(inject.inject_id, nowMs);
159
+ } catch (err) {
160
+ // Log injection failures but don't crash
161
+ // eslint-disable-next-line no-console
162
+ console.error(`[Astrocode:inject] Failed to inject ${inject.inject_id}:`, err);
163
+ }
164
+ }
165
+
166
+ // Log diagnostic summary
167
+ if (EMIT_TELEMETRY || injected > 0) {
168
+ // eslint-disable-next-line no-console
169
+ console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
170
+ }
171
+ }
172
+
173
+ // Workflow-related tools that should trigger inject + auto-approval
174
+ const WORKFLOW_TOOLS = new Set([
175
+ 'astro_workflow_proceed',
176
+ 'astro_story_queue',
177
+ 'astro_story_approve',
178
+ 'astro_stage_start',
179
+ 'astro_stage_complete',
180
+ 'astro_stage_fail',
181
+ 'astro_run_abort',
182
+ ]);
183
+
184
+ // Auto-approve queued stories if enabled
185
+ async function maybeAutoApprove(sessionId: string) {
186
+ if (!config.inject?.auto_approve_queued_stories) return;
187
+
188
+ try {
189
+ // Get all queued stories
190
+ const queued = db.prepare("SELECT story_key, title FROM stories WHERE state='queued' ORDER BY priority DESC, created_at ASC").all() as Array<{ story_key: string; title: string }>;
191
+
192
+ if (queued.length === 0) return;
193
+
194
+ // Auto-approve the highest priority queued story
195
+ const story = queued[0];
196
+ db.prepare("UPDATE stories SET state='approved', updated_at=? WHERE story_key=?").run(nowISO(), story.story_key);
197
+
198
+ // eslint-disable-next-line no-console
199
+ console.log(`[Astrocode:inject] Auto-approved story ${story.story_key}: ${story.title}`);
200
+
201
+ // Inject a notification about the auto-approval
133
202
  await injectChatPrompt({
134
203
  ctx,
135
204
  sessionId,
136
- text: formattedText,
205
+ text: `✅ Auto-approved story ${story.story_key}: ${story.title}`,
137
206
  agent: "Astrocode"
138
207
  });
139
-
140
- injected++;
141
- markInjected(inject.inject_id, nowMs);
142
- }
143
-
144
- // Log diagnostic summary
145
- if (EMIT_TELEMETRY) {
208
+ } catch (err) {
146
209
  // eslint-disable-next-line no-console
147
- console.log(`[Astrocode:inject] ${now} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
210
+ console.error(`[Astrocode:inject] Auto-approval failed:`, err);
148
211
  }
149
212
  }
150
213
 
@@ -154,7 +217,24 @@ export function createInjectProvider(opts: {
154
217
  if (!config.inject?.enabled) return;
155
218
 
156
219
  // Inject eligible injects before processing the user's message
157
- await injectEligibleInjects(input.sessionID);
220
+ await injectEligibleInjects(input.sessionID, 'chat_message');
221
+ },
222
+
223
+ async onToolAfter(input: ToolExecuteAfterInput) {
224
+ if (!config.inject?.enabled) return;
225
+
226
+ // Only inject after workflow-related tools
227
+ if (!WORKFLOW_TOOLS.has(input.tool)) return;
228
+
229
+ // Extract sessionID (same pattern as continuation enforcer)
230
+ const sessionId = input.sessionID ?? (ctx as any).sessionID;
231
+ if (!sessionId) return;
232
+
233
+ // Auto-approve queued stories if enabled
234
+ await maybeAutoApprove(sessionId);
235
+
236
+ // Inject eligible injects after workflow tool execution
237
+ await injectEligibleInjects(sessionId, `tool_after:${input.tool}`);
158
238
  },
159
239
  };
160
240
  }
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ type ContinuationEnforcer = {
24
24
  type ToolOutputTruncator = (input: any, output: any | null) => Promise<void>;
25
25
  type InjectProvider = {
26
26
  onChatMessage: (input: any) => Promise<void>;
27
+ onToolAfter: (input: any) => Promise<void>;
27
28
  };
28
29
  type ToastManager = {
29
30
  show: (toast: ToastOptions) => Promise<void>;
@@ -58,6 +59,13 @@ const Astrocode: Plugin = async (ctx) => {
58
59
  }
59
60
  const repoRoot = ctx.directory;
60
61
 
62
+ // NOTE: Repo locking is handled at the workflow level via workflowRepoLock.
63
+ // The workflow tool correctly acquires and holds the lock for the entire workflow execution.
64
+ // Plugin-level locking is unnecessary and architecturally incorrect since:
65
+ // - The lock would be held for the entire session lifecycle (too long)
66
+ // - Individual tools are designed to be called within workflow context where lock is held
67
+ // - Workflow-level locking with refcounting prevents lock churn during tool execution
68
+
61
69
  // Always load config first - this provides defaults even in limited mode
62
70
  let pluginConfig: AstrocodeConfig;
63
71
  try {
@@ -294,6 +302,11 @@ const Astrocode: Plugin = async (ctx) => {
294
302
  },
295
303
 
296
304
  "tool.execute.after": async (input: any, output: any) => {
305
+ // Inject eligible injects after tool execution (not just on chat messages)
306
+ if (injectProvider && hookEnabled("inject-provider")) {
307
+ await injectProvider.onToolAfter(input);
308
+ }
309
+
297
310
  // Truncate huge tool outputs to artifacts
298
311
  if (truncatorHook && hookEnabled("tool-output-truncator")) {
299
312
  await truncatorHook(input, output ?? null);
@@ -325,6 +338,7 @@ const Astrocode: Plugin = async (ctx) => {
325
338
 
326
339
  // Best-effort cleanup
327
340
  close: async () => {
341
+ // Close database connection
328
342
  if (db && typeof db.close === "function") {
329
343
  try {
330
344
  db.close();
@@ -0,0 +1,148 @@
1
+ // src/shared/metrics.ts
2
+
3
+ export interface TransactionMetrics {
4
+ startTime: number;
5
+ duration: number;
6
+ success: boolean;
7
+ nestedDepth: number;
8
+ operation?: string;
9
+ }
10
+
11
+ export interface InjectionMetrics {
12
+ sessionId: string;
13
+ attempts: number;
14
+ duration: number;
15
+ success: boolean;
16
+ agent?: string;
17
+ }
18
+
19
+ export interface SystemMetrics {
20
+ transactions: TransactionMetrics[];
21
+ injections: InjectionMetrics[];
22
+ errors: Array<{ type: string; message: string; timestamp: number }>;
23
+ }
24
+
25
+ class MetricsCollector {
26
+ private transactions: TransactionMetrics[] = [];
27
+ private injections: InjectionMetrics[] = [];
28
+ private errors: Array<{ type: string; message: string; timestamp: number }> = [];
29
+ private maxEntries = 1000; // Keep last 1000 entries per type
30
+
31
+ recordTransaction(metrics: TransactionMetrics) {
32
+ this.transactions.push(metrics);
33
+ if (this.transactions.length > this.maxEntries) {
34
+ this.transactions.shift();
35
+ }
36
+ }
37
+
38
+ recordInjection(metrics: InjectionMetrics) {
39
+ this.injections.push(metrics);
40
+ if (this.injections.length > this.maxEntries) {
41
+ this.injections.shift();
42
+ }
43
+ }
44
+
45
+ recordError(type: string, message: string) {
46
+ this.errors.push({ type, message, timestamp: Date.now() });
47
+ if (this.errors.length > this.maxEntries) {
48
+ this.errors.shift();
49
+ }
50
+ }
51
+
52
+ getMetrics(): SystemMetrics {
53
+ return {
54
+ transactions: [...this.transactions],
55
+ injections: [...this.injections],
56
+ errors: [...this.errors],
57
+ };
58
+ }
59
+
60
+ getTransactionStats() {
61
+ const txs = this.transactions;
62
+ if (txs.length === 0) return null;
63
+
64
+ const successful = txs.filter(t => t.success);
65
+ const failed = txs.filter(t => !t.success);
66
+
67
+ return {
68
+ total: txs.length,
69
+ successful: successful.length,
70
+ failed: failed.length,
71
+ successRate: successful.length / txs.length,
72
+ avgDuration: txs.reduce((sum, t) => sum + t.duration, 0) / txs.length,
73
+ avgNestedDepth: txs.reduce((sum, t) => sum + t.nestedDepth, 0) / txs.length,
74
+ minDuration: Math.min(...txs.map(t => t.duration)),
75
+ maxDuration: Math.max(...txs.map(t => t.duration)),
76
+ };
77
+ }
78
+
79
+ getInjectionStats() {
80
+ const injections = this.injections;
81
+ if (injections.length === 0) return null;
82
+
83
+ const successful = injections.filter(i => i.success);
84
+ const failed = injections.filter(i => !i.success);
85
+
86
+ return {
87
+ total: injections.length,
88
+ successful: successful.length,
89
+ failed: failed.length,
90
+ successRate: successful.length / injections.length,
91
+ avgAttempts: injections.reduce((sum, i) => sum + i.attempts, 0) / injections.length,
92
+ avgDuration: injections.reduce((sum, i) => sum + i.duration, 0) / injections.length,
93
+ totalRetries: injections.reduce((sum, i) => sum + Math.max(0, i.attempts - 1), 0),
94
+ };
95
+ }
96
+
97
+ clear() {
98
+ this.transactions = [];
99
+ this.injections = [];
100
+ this.errors = [];
101
+ }
102
+ }
103
+
104
+ // Global singleton
105
+ export const metrics = new MetricsCollector();
106
+
107
+ // Convenience functions
108
+ export function recordTransaction(metricsData: Omit<TransactionMetrics, 'startTime' | 'duration' | 'success'>) {
109
+ return {
110
+ start() {
111
+ return {
112
+ ...metricsData,
113
+ startTime: Date.now(),
114
+ };
115
+ },
116
+ end(startData: ReturnType<ReturnType<typeof recordTransaction>['start']>, success: boolean) {
117
+ const duration = Date.now() - startData.startTime;
118
+ metrics.recordTransaction({
119
+ ...startData,
120
+ duration,
121
+ success,
122
+ });
123
+ },
124
+ };
125
+ }
126
+
127
+ export function recordInjection(metricsData: Omit<InjectionMetrics, 'duration' | 'success'>) {
128
+ return {
129
+ start() {
130
+ return {
131
+ ...metricsData,
132
+ startTime: Date.now(),
133
+ };
134
+ },
135
+ end(startData: any, success: boolean) {
136
+ const duration = Date.now() - startData.startTime;
137
+ metrics.recordInjection({
138
+ ...startData,
139
+ duration,
140
+ success,
141
+ });
142
+ },
143
+ };
144
+ }
145
+
146
+ export function recordError(type: string, message: string) {
147
+ metrics.recordError(type, message);
148
+ }
package/src/state/db.ts CHANGED
@@ -5,6 +5,7 @@ import { SCHEMA_SQL, SCHEMA_VERSION } from "./schema";
5
5
  import { nowISO } from "../shared/time";
6
6
  import { info, warn } from "../shared/log";
7
7
  import { createDatabaseAdapter, DatabaseConnection } from "./adapters";
8
+ import { recordTransaction } from "../shared/metrics";
8
9
 
9
10
  export type SqliteDb = DatabaseConnection;
10
11
 
@@ -74,7 +75,7 @@ function savepointName(depth: number): string {
74
75
  }
75
76
 
76
77
  /** BEGIN IMMEDIATE transaction helper (re-entrant). */
77
- export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean }): T {
78
+ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean; operation?: string }): T {
78
79
  const adapter = createDatabaseAdapter();
79
80
  const available = adapter.isAvailable();
80
81
 
@@ -84,13 +85,17 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
84
85
  }
85
86
 
86
87
  const depth = getDepth(db);
88
+ const isNested = depth > 0;
89
+ const txRecorder = recordTransaction({ nestedDepth: depth, operation: opts?.operation });
87
90
 
88
91
  if (depth === 0) {
92
+ const txStart = txRecorder.start();
89
93
  db.exec("BEGIN IMMEDIATE");
90
94
  setDepth(db, 1);
91
95
  try {
92
96
  const out = fn();
93
97
  db.exec("COMMIT");
98
+ txRecorder.end(txStart, true);
94
99
  return out;
95
100
  } catch (e) {
96
101
  try {
@@ -98,6 +103,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
98
103
  } catch {
99
104
  // ignore
100
105
  }
106
+ txRecorder.end(txStart, false);
101
107
  throw e;
102
108
  } finally {
103
109
  setDepth(db, 0);
@@ -107,6 +113,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
107
113
  // Nested: use SAVEPOINT
108
114
  const nextDepth = depth + 1;
109
115
  const sp = savepointName(nextDepth);
116
+ const txStart = txRecorder.start();
110
117
 
111
118
  db.exec(`SAVEPOINT ${sp}`);
112
119
  setDepth(db, nextDepth);
@@ -114,6 +121,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
114
121
  try {
115
122
  const out = fn();
116
123
  db.exec(`RELEASE SAVEPOINT ${sp}`);
124
+ txRecorder.end(txStart, true);
117
125
  return out;
118
126
  } catch (e) {
119
127
  try {
@@ -126,6 +134,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
126
134
  } catch {
127
135
  // ignore
128
136
  }
137
+ txRecorder.end(txStart, false);
129
138
  throw e;
130
139
  } finally {
131
140
  setDepth(db, depth);