astrocode-workflow 0.4.1 → 0.4.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/src/astro/workflow-runner.d.ts +1 -5
- package/dist/src/astro/workflow-runner.js +6 -17
- package/dist/src/index.js +0 -6
- package/dist/src/tools/health.js +0 -31
- package/dist/src/tools/index.js +0 -3
- package/dist/src/tools/repair.js +4 -37
- package/dist/src/tools/workflow.js +178 -192
- package/package.json +1 -1
- package/src/astro/workflow-runner.ts +5 -25
- package/src/index.ts +0 -7
- package/src/tools/health.ts +0 -29
- package/src/tools/index.ts +2 -5
- package/src/tools/repair.ts +4 -38
- package/src/tools/workflow.ts +1 -17
- package/src/state/repo-lock.ts +0 -706
- package/src/state/workflow-repo-lock.ts +0 -111
- package/src/tools/lock.ts +0 -75
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// src/state/workflow-repo-lock.ts
|
|
2
|
-
import type { acquireRepoLock } from "./repo-lock";
|
|
3
|
-
|
|
4
|
-
type RepoLockAcquire = typeof acquireRepoLock;
|
|
5
|
-
|
|
6
|
-
type Held = {
|
|
7
|
-
release: () => void;
|
|
8
|
-
depth: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const HELD_BY_KEY = new Map<string, Held>();
|
|
12
|
-
|
|
13
|
-
function key(lockPath: string, sessionId?: string) {
|
|
14
|
-
return `${lockPath}::${sessionId ?? ""}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Acquire ONCE per workflow/session in this process.
|
|
19
|
-
* Nested calls reuse the same held lock (no reacquire, no churn).
|
|
20
|
-
*
|
|
21
|
-
* ADVISORY LOCK MODE:
|
|
22
|
-
* - Creates lock file to signal other sessions
|
|
23
|
-
* - If lock held by another session: WARN and proceed anyway
|
|
24
|
-
* - Database constraints provide actual safety (single running run)
|
|
25
|
-
* - Better UX: no blocking, just helpful warnings
|
|
26
|
-
*/
|
|
27
|
-
export async function workflowRepoLock<T>(
|
|
28
|
-
deps: { acquireRepoLock: RepoLockAcquire },
|
|
29
|
-
opts: {
|
|
30
|
-
lockPath: string;
|
|
31
|
-
repoRoot: string;
|
|
32
|
-
sessionId?: string;
|
|
33
|
-
owner?: string;
|
|
34
|
-
fn: () => Promise<T>;
|
|
35
|
-
advisory?: boolean; // If true, warn instead of error on contention
|
|
36
|
-
}
|
|
37
|
-
): Promise<T> {
|
|
38
|
-
const k = key(opts.lockPath, opts.sessionId);
|
|
39
|
-
const existing = HELD_BY_KEY.get(k);
|
|
40
|
-
|
|
41
|
-
if (existing) {
|
|
42
|
-
existing.depth += 1;
|
|
43
|
-
try {
|
|
44
|
-
return await opts.fn();
|
|
45
|
-
} finally {
|
|
46
|
-
existing.depth -= 1;
|
|
47
|
-
if (existing.depth <= 0) {
|
|
48
|
-
HELD_BY_KEY.delete(k);
|
|
49
|
-
existing.release();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// IMPORTANT: this is tuned for "hold for whole workflow".
|
|
55
|
-
let handle: { release: () => void } | null = null;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
handle = await deps.acquireRepoLock({
|
|
59
|
-
lockPath: opts.lockPath,
|
|
60
|
-
repoRoot: opts.repoRoot,
|
|
61
|
-
sessionId: opts.sessionId,
|
|
62
|
-
owner: opts.owner,
|
|
63
|
-
|
|
64
|
-
retryMs: opts.advisory ? 1000 : 30_000, // Advisory: fail fast, hard: wait longer
|
|
65
|
-
staleMs: 30_000, // Reduced from 2 minutes to 30 seconds for faster stale lock recovery
|
|
66
|
-
heartbeatMs: 200,
|
|
67
|
-
minWriteMs: 800,
|
|
68
|
-
pollMs: 20,
|
|
69
|
-
pollMaxMs: 250,
|
|
70
|
-
});
|
|
71
|
-
} catch (err: any) {
|
|
72
|
-
// Lock acquisition failed - check if advisory mode
|
|
73
|
-
if (opts.advisory) {
|
|
74
|
-
// Advisory mode: warn and proceed without lock
|
|
75
|
-
// eslint-disable-next-line no-console
|
|
76
|
-
console.warn(`⚠️ [Astrocode] Another session may be active. Proceeding anyway (advisory lock mode).`);
|
|
77
|
-
// eslint-disable-next-line no-console
|
|
78
|
-
console.warn(` ${err.message}`);
|
|
79
|
-
|
|
80
|
-
// Proceed without lock - database constraints will ensure safety
|
|
81
|
-
try {
|
|
82
|
-
return await opts.fn();
|
|
83
|
-
} catch (dbErr: any) {
|
|
84
|
-
// Check if this is a concurrency error
|
|
85
|
-
if (dbErr.message?.includes('UNIQUE constraint') || dbErr.message?.includes('SQLITE_BUSY')) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Another session is actively working on this story. Database prevented concurrent modification. ` +
|
|
88
|
-
`Please wait for the other session to complete, or work on a different story.`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
throw dbErr;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Hard lock mode: propagate error
|
|
96
|
-
throw err;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const held: Held = { release: handle.release, depth: 1 };
|
|
100
|
-
HELD_BY_KEY.set(k, held);
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
return await opts.fn();
|
|
104
|
-
} finally {
|
|
105
|
-
held.depth -= 1;
|
|
106
|
-
if (held.depth <= 0) {
|
|
107
|
-
HELD_BY_KEY.delete(k);
|
|
108
|
-
held.release();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
package/src/tools/lock.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
3
|
-
import { getLockStatus, tryRemoveStaleLock } from "../state/repo-lock";
|
|
4
|
-
|
|
5
|
-
export function createAstroLockStatusTool(opts: { ctx: any }): ToolDefinition {
|
|
6
|
-
const { ctx } = opts;
|
|
7
|
-
|
|
8
|
-
return tool({
|
|
9
|
-
description: "Check Astrocode lock status and attempt repair. Shows diagnostics (PID, age, session) and can remove stale/dead locks.",
|
|
10
|
-
args: {
|
|
11
|
-
attempt_repair: tool.schema.boolean().default(false).describe("If true, attempt to remove stale or dead locks"),
|
|
12
|
-
},
|
|
13
|
-
execute: async ({ attempt_repair }) => {
|
|
14
|
-
const repoRoot = ctx.directory as string;
|
|
15
|
-
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
16
|
-
|
|
17
|
-
const status = getLockStatus(lockPath);
|
|
18
|
-
|
|
19
|
-
if (!status.exists) {
|
|
20
|
-
return "✅ No lock file found. Repository is unlocked.";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const lines: string[] = [];
|
|
24
|
-
lines.push("# Astrocode Lock Status");
|
|
25
|
-
lines.push("");
|
|
26
|
-
lines.push("## Lock Details");
|
|
27
|
-
lines.push(`- **Path**: ${status.path}`);
|
|
28
|
-
lines.push(`- **PID**: ${status.pid} (${status.pidAlive ? '🟢 ALIVE' : '🔴 DEAD'})`);
|
|
29
|
-
lines.push(`- **Age**: ${status.ageMs ? Math.floor(status.ageMs / 1000) : '?'}s`);
|
|
30
|
-
lines.push(`- **Status**: ${status.isStale ? '⚠️ STALE' : '✅ FRESH'}`);
|
|
31
|
-
if (status.sessionId) lines.push(`- **Session**: ${status.sessionId}`);
|
|
32
|
-
if (status.owner) lines.push(`- **Owner**: ${status.owner}`);
|
|
33
|
-
if (status.instanceId) lines.push(`- **Instance**: ${status.instanceId.substring(0, 8)}...`);
|
|
34
|
-
if (status.leaseId) lines.push(`- **Lease**: ${status.leaseId.substring(0, 8)}...`);
|
|
35
|
-
if (status.createdAt) lines.push(`- **Created**: ${status.createdAt}`);
|
|
36
|
-
if (status.updatedAt) lines.push(`- **Updated**: ${status.updatedAt}`);
|
|
37
|
-
if (status.repoRoot) lines.push(`- **Repo**: ${status.repoRoot}`);
|
|
38
|
-
lines.push(`- **Version**: ${status.version ?? 'unknown'}`);
|
|
39
|
-
lines.push("");
|
|
40
|
-
|
|
41
|
-
if (attempt_repair) {
|
|
42
|
-
lines.push("## Repair Attempt");
|
|
43
|
-
const result = tryRemoveStaleLock(lockPath);
|
|
44
|
-
|
|
45
|
-
if (result.removed) {
|
|
46
|
-
lines.push(`✅ **Lock removed**: ${result.reason}`);
|
|
47
|
-
lines.push("");
|
|
48
|
-
lines.push("The repository is now unlocked and ready for use.");
|
|
49
|
-
} else {
|
|
50
|
-
lines.push(`⚠️ **Lock NOT removed**: ${result.reason}`);
|
|
51
|
-
lines.push("");
|
|
52
|
-
lines.push("**Recommendations**:");
|
|
53
|
-
lines.push("- If the owning process has crashed, wait 30 seconds for automatic stale detection");
|
|
54
|
-
lines.push("- If the process is still running, wait for it to complete");
|
|
55
|
-
lines.push("- As a last resort, manually stop the process and run this tool again with attempt_repair=true");
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
lines.push("## Recommendations");
|
|
59
|
-
if (!status.pidAlive) {
|
|
60
|
-
lines.push("🔧 **Action Required**: Lock belongs to dead process. Run with `attempt_repair=true` to remove it.");
|
|
61
|
-
} else if (status.isStale) {
|
|
62
|
-
lines.push("🔧 **Action Suggested**: Lock is stale (not updated recently). Run with `attempt_repair=true` to remove it.");
|
|
63
|
-
} else {
|
|
64
|
-
lines.push("✅ Lock is active and healthy. The owning process is running normally.");
|
|
65
|
-
lines.push("");
|
|
66
|
-
lines.push("If you believe this is incorrect:");
|
|
67
|
-
lines.push("- Wait 30 seconds and check again (automatic stale detection)");
|
|
68
|
-
lines.push("- Run with `attempt_repair=true` only if you're certain the process has crashed");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return lines.join("\n");
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
}
|