astrocode-workflow 0.2.0 → 0.2.2

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.
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { createInjectProvider } from "./hooks/inject-provider";
9
9
  import { createToastManager } from "./ui/toasts";
10
10
  import { createAstroAgents } from "./agents/registry";
11
11
  import { info, warn } from "./shared/log";
12
+ import { acquireRepoLock } from "./state/repo-lock";
12
13
  // Safe config cloning with structuredClone preference (fallback for older Node versions)
13
14
  // CONTRACT: Config is guaranteed JSON-serializable (enforced by loadAstrocodeConfig validation)
14
15
  const cloneConfig = (v) => {
@@ -37,6 +38,9 @@ const Astrocode = async (ctx) => {
37
38
  throw new Error("Astrocode requires ctx.directory to be a string repo root.");
38
39
  }
39
40
  const repoRoot = ctx.directory;
41
+ // Acquire exclusive repo lock to prevent multiple processes from corrupting the database
42
+ const lockPath = `${repoRoot}/.astro/astro.lock`;
43
+ const repoLock = acquireRepoLock(lockPath);
40
44
  // Always load config first - this provides defaults even in limited mode
41
45
  let pluginConfig;
42
46
  try {
@@ -280,6 +284,8 @@ const Astrocode = async (ctx) => {
280
284
  },
281
285
  // Best-effort cleanup
282
286
  close: async () => {
287
+ // Release repo lock first (important for process termination)
288
+ repoLock.release();
283
289
  if (db && typeof db.close === "function") {
284
290
  try {
285
291
  db.close();
@@ -4,7 +4,7 @@ export declare function openSqlite(dbPath: string, opts?: {
4
4
  busyTimeoutMs?: number;
5
5
  }): SqliteDb;
6
6
  export declare function configurePragmas(db: SqliteDb, pragmas: Record<string, any>): void;
7
- /** BEGIN IMMEDIATE transaction helper. */
7
+ /** BEGIN IMMEDIATE transaction helper (re-entrant). */
8
8
  export declare function withTx<T>(db: SqliteDb, fn: () => T, opts?: {
9
9
  require?: boolean;
10
10
  }): T;
package/dist/state/db.js CHANGED
@@ -46,7 +46,30 @@ export function configurePragmas(db, pragmas) {
46
46
  if (pragmas.temp_store)
47
47
  db.pragma(`temp_store = ${pragmas.temp_store}`);
48
48
  }
49
- /** BEGIN IMMEDIATE transaction helper. */
49
+ /**
50
+ * Re-entrant transaction helper.
51
+ *
52
+ * SQLite rejects BEGIN inside BEGIN. We use:
53
+ * - depth=0: BEGIN IMMEDIATE ... COMMIT/ROLLBACK
54
+ * - depth>0: SAVEPOINT sp_n ... RELEASE / ROLLBACK TO + RELEASE
55
+ *
56
+ * This allows callers to safely nest withTx across layers (tools -> workflow -> state machine)
57
+ * without "cannot start a transaction within a transaction".
58
+ */
59
+ const TX_DEPTH = new WeakMap();
60
+ function getDepth(db) {
61
+ return TX_DEPTH.get(db) ?? 0;
62
+ }
63
+ function setDepth(db, depth) {
64
+ if (depth <= 0)
65
+ TX_DEPTH.delete(db);
66
+ else
67
+ TX_DEPTH.set(db, depth);
68
+ }
69
+ function savepointName(depth) {
70
+ return `sp_${depth}`;
71
+ }
72
+ /** BEGIN IMMEDIATE transaction helper (re-entrant). */
50
73
  export function withTx(db, fn, opts) {
51
74
  const adapter = createDatabaseAdapter();
52
75
  const available = adapter.isAvailable();
@@ -55,21 +78,56 @@ export function withTx(db, fn, opts) {
55
78
  throw new Error("Database adapter unavailable; transaction required.");
56
79
  return fn();
57
80
  }
58
- db.exec("BEGIN IMMEDIATE");
81
+ const depth = getDepth(db);
82
+ if (depth === 0) {
83
+ db.exec("BEGIN IMMEDIATE");
84
+ setDepth(db, 1);
85
+ try {
86
+ const out = fn();
87
+ db.exec("COMMIT");
88
+ return out;
89
+ }
90
+ catch (e) {
91
+ try {
92
+ db.exec("ROLLBACK");
93
+ }
94
+ catch {
95
+ // ignore
96
+ }
97
+ throw e;
98
+ }
99
+ finally {
100
+ setDepth(db, 0);
101
+ }
102
+ }
103
+ // Nested: use SAVEPOINT
104
+ const nextDepth = depth + 1;
105
+ const sp = savepointName(nextDepth);
106
+ db.exec(`SAVEPOINT ${sp}`);
107
+ setDepth(db, nextDepth);
59
108
  try {
60
109
  const out = fn();
61
- db.exec("COMMIT");
110
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
62
111
  return out;
63
112
  }
64
113
  catch (e) {
65
114
  try {
66
- db.exec("ROLLBACK");
115
+ db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
116
+ }
117
+ catch {
118
+ // ignore
119
+ }
120
+ try {
121
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
67
122
  }
68
123
  catch {
69
124
  // ignore
70
125
  }
71
126
  throw e;
72
127
  }
128
+ finally {
129
+ setDepth(db, depth);
130
+ }
73
131
  }
74
132
  export function getSchemaVersion(db) {
75
133
  try {
@@ -0,0 +1,3 @@
1
+ export declare function acquireRepoLock(lockPath: string): {
2
+ release: () => void;
3
+ };
@@ -0,0 +1,29 @@
1
+ // src/state/repo-lock.ts
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function acquireRepoLock(lockPath) {
5
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
6
+ let fd;
7
+ try {
8
+ fd = fs.openSync(lockPath, "wx"); // exclusive create
9
+ }
10
+ catch (e) {
11
+ const msg = e?.code === "EEXIST"
12
+ ? `Astrocode lock is already held (${lockPath}). Another opencode process is running in this repo.`
13
+ : `Failed to acquire lock (${lockPath}): ${e?.message ?? String(e)}`;
14
+ throw new Error(msg);
15
+ }
16
+ fs.writeFileSync(fd, `${process.pid}\n`, "utf8");
17
+ return {
18
+ release: () => {
19
+ try {
20
+ fs.closeSync(fd);
21
+ }
22
+ catch { }
23
+ try {
24
+ fs.unlinkSync(lockPath);
25
+ }
26
+ catch { }
27
+ },
28
+ };
29
+ }
@@ -2,9 +2,9 @@ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
3
  import type { SqliteDb } from "../state/db";
4
4
  import type { StageKey } from "../state/types";
5
+ import type { AgentConfig } from "@opencode-ai/sdk";
5
6
  export declare const STAGE_TO_AGENT_MAP: Record<string, string>;
6
7
  export declare function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig, agents?: any, warnings?: string[]): string;
7
- import { AgentConfig } from "@opencode-ai/sdk";
8
8
  export declare function createAstroWorkflowProceedTool(opts: {
9
9
  ctx: any;
10
10
  config: AstrocodeConfig;