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 +6 -0
- package/dist/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +224 -209
- package/dist/ui/inject.d.ts +9 -17
- package/dist/ui/inject.js +79 -102
- package/dist/workflow/state-machine.d.ts +32 -32
- package/dist/workflow/state-machine.js +85 -170
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/state/db.ts +63 -4
- package/src/state/repo-lock.ts +26 -0
- package/src/tools/workflow.ts +159 -142
- package/src/ui/inject.ts +98 -105
- package/src/workflow/state-machine.ts +123 -227
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();
|
package/dist/state/db.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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(
|
|
110
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
62
111
|
return out;
|
|
63
112
|
}
|
|
64
113
|
catch (e) {
|
|
65
114
|
try {
|
|
66
|
-
db.exec(
|
|
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,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
|
+
}
|
package/dist/tools/workflow.d.ts
CHANGED
|
@@ -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;
|