astrocode-workflow 0.3.3 → 0.3.5-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.
- package/dist/src/astro/workflow-runner.d.ts +15 -0
- package/dist/src/astro/workflow-runner.js +25 -0
- package/dist/src/hooks/inject-provider.d.ts +5 -0
- package/dist/src/hooks/inject-provider.js +10 -0
- package/dist/src/index.js +11 -6
- package/dist/src/state/repo-lock.d.ts +65 -1
- package/dist/src/state/repo-lock.js +568 -17
- package/dist/src/state/workflow-repo-lock.d.ts +16 -0
- package/dist/src/state/workflow-repo-lock.js +50 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/lock.d.ts +4 -0
- package/dist/src/tools/lock.js +78 -0
- package/dist/src/tools/repair.js +40 -6
- package/dist/src/tools/status.js +1 -1
- package/dist/src/tools/workflow.js +182 -179
- package/dist/src/workflow/repair.js +2 -2
- package/package.json +1 -1
- package/src/hooks/inject-provider.ts +16 -0
- package/src/index.ts +13 -7
- package/src/state/repo-lock.ts +170 -38
- package/src/state/workflow-repo-lock.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/lock.ts +75 -0
- package/src/tools/repair.ts +43 -6
- package/src/workflow/repair.ts +2 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is the only place you should hold the repo lock.
|
|
3
|
+
* Everything that mutates the repo (tool calls, steps) runs inside this scope.
|
|
4
|
+
*
|
|
5
|
+
* Replace the internals with your actual astro/opencode driver loop.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runAstroWorkflow(opts: {
|
|
8
|
+
lockPath: string;
|
|
9
|
+
repoRoot: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
owner?: string;
|
|
12
|
+
proceedOneStep: () => Promise<{
|
|
13
|
+
done: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/astro/workflow-runner.ts
|
|
2
|
+
import { acquireRepoLock } from "../state/repo-lock";
|
|
3
|
+
import { workflowRepoLock } from "../state/workflow-repo-lock";
|
|
4
|
+
/**
|
|
5
|
+
* This is the only place you should hold the repo lock.
|
|
6
|
+
* Everything that mutates the repo (tool calls, steps) runs inside this scope.
|
|
7
|
+
*
|
|
8
|
+
* Replace the internals with your actual astro/opencode driver loop.
|
|
9
|
+
*/
|
|
10
|
+
export async function runAstroWorkflow(opts) {
|
|
11
|
+
await workflowRepoLock({ acquireRepoLock }, {
|
|
12
|
+
lockPath: opts.lockPath,
|
|
13
|
+
repoRoot: opts.repoRoot,
|
|
14
|
+
sessionId: opts.sessionId,
|
|
15
|
+
owner: opts.owner,
|
|
16
|
+
fn: async () => {
|
|
17
|
+
// ✅ Lock is held ONCE for the entire run. Tool calls can "rattle through".
|
|
18
|
+
while (true) {
|
|
19
|
+
const { done } = await opts.proceedOneStep();
|
|
20
|
+
if (done)
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -4,6 +4,10 @@ type ChatMessageInput = {
|
|
|
4
4
|
sessionID: string;
|
|
5
5
|
agent: string;
|
|
6
6
|
};
|
|
7
|
+
type ToolExecuteAfterInput = {
|
|
8
|
+
tool: string;
|
|
9
|
+
sessionID?: string;
|
|
10
|
+
};
|
|
7
11
|
type RuntimeState = {
|
|
8
12
|
db: SqliteDb | null;
|
|
9
13
|
limitedMode: boolean;
|
|
@@ -18,5 +22,6 @@ export declare function createInjectProvider(opts: {
|
|
|
18
22
|
runtime: RuntimeState;
|
|
19
23
|
}): {
|
|
20
24
|
onChatMessage(input: ChatMessageInput): Promise<void>;
|
|
25
|
+
onToolAfter(input: ToolExecuteAfterInput): Promise<void>;
|
|
21
26
|
};
|
|
22
27
|
export {};
|
|
@@ -116,5 +116,15 @@ export function createInjectProvider(opts) {
|
|
|
116
116
|
// Inject eligible injects before processing the user's message
|
|
117
117
|
await injectEligibleInjects(input.sessionID);
|
|
118
118
|
},
|
|
119
|
+
async onToolAfter(input) {
|
|
120
|
+
if (!config.inject?.enabled)
|
|
121
|
+
return;
|
|
122
|
+
// Extract sessionID (same pattern as continuation enforcer)
|
|
123
|
+
const sessionId = input.sessionID ?? ctx.sessionID;
|
|
124
|
+
if (!sessionId)
|
|
125
|
+
return;
|
|
126
|
+
// Inject eligible injects after tool execution
|
|
127
|
+
await injectEligibleInjects(sessionId);
|
|
128
|
+
},
|
|
119
129
|
};
|
|
120
130
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -9,7 +9,6 @@ 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";
|
|
13
12
|
// Safe config cloning with structuredClone preference (fallback for older Node versions)
|
|
14
13
|
// CONTRACT: Config is guaranteed JSON-serializable (enforced by loadAstrocodeConfig validation)
|
|
15
14
|
const cloneConfig = (v) => {
|
|
@@ -38,9 +37,12 @@ const Astrocode = async (ctx) => {
|
|
|
38
37
|
throw new Error("Astrocode requires ctx.directory to be a string repo root.");
|
|
39
38
|
}
|
|
40
39
|
const repoRoot = ctx.directory;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// NOTE: Repo locking is handled at the workflow level via workflowRepoLock.
|
|
41
|
+
// The workflow tool correctly acquires and holds the lock for the entire workflow execution.
|
|
42
|
+
// Plugin-level locking is unnecessary and architecturally incorrect since:
|
|
43
|
+
// - The lock would be held for the entire session lifecycle (too long)
|
|
44
|
+
// - Individual tools are designed to be called within workflow context where lock is held
|
|
45
|
+
// - Workflow-level locking with refcounting prevents lock churn during tool execution
|
|
44
46
|
// Always load config first - this provides defaults even in limited mode
|
|
45
47
|
let pluginConfig;
|
|
46
48
|
try {
|
|
@@ -258,6 +260,10 @@ const Astrocode = async (ctx) => {
|
|
|
258
260
|
return { args: nextArgs };
|
|
259
261
|
},
|
|
260
262
|
"tool.execute.after": async (input, output) => {
|
|
263
|
+
// Inject eligible injects after tool execution (not just on chat messages)
|
|
264
|
+
if (injectProvider && hookEnabled("inject-provider")) {
|
|
265
|
+
await injectProvider.onToolAfter(input);
|
|
266
|
+
}
|
|
261
267
|
// Truncate huge tool outputs to artifacts
|
|
262
268
|
if (truncatorHook && hookEnabled("tool-output-truncator")) {
|
|
263
269
|
await truncatorHook(input, output ?? null);
|
|
@@ -284,8 +290,7 @@ const Astrocode = async (ctx) => {
|
|
|
284
290
|
},
|
|
285
291
|
// Best-effort cleanup
|
|
286
292
|
close: async () => {
|
|
287
|
-
//
|
|
288
|
-
repoLock.release();
|
|
293
|
+
// Close database connection
|
|
289
294
|
if (db && typeof db.close === "function") {
|
|
290
295
|
try {
|
|
291
296
|
db.close();
|
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Acquire a repo-scoped lock with:
|
|
3
|
+
* - ✅ process-local caching + refcount (efficient repeated tool calls)
|
|
4
|
+
* - ✅ heartbeat lease + stale recovery
|
|
5
|
+
* - ✅ atomic create (`wx`) + portable replace fallback
|
|
6
|
+
* - ✅ dead PID eviction + stale eviction
|
|
7
|
+
* - ✅ no live takeover (even same session) to avoid concurrency stomps
|
|
8
|
+
* - ✅ ABA-safe release via lease_id fencing
|
|
9
|
+
* - ✅ exponential backoff + jitter to reduce FS churn
|
|
10
|
+
*/
|
|
11
|
+
export declare function acquireRepoLock(opts: {
|
|
12
|
+
lockPath: string;
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
owner?: string;
|
|
16
|
+
retryMs?: number;
|
|
17
|
+
pollMs?: number;
|
|
18
|
+
pollMaxMs?: number;
|
|
19
|
+
staleMs?: number;
|
|
20
|
+
heartbeatMs?: number;
|
|
21
|
+
minWriteMs?: number;
|
|
22
|
+
}): Promise<{
|
|
2
23
|
release: () => void;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Helper wrapper: always releases lock.
|
|
27
|
+
*/
|
|
28
|
+
export declare function withRepoLock<T>(opts: {
|
|
29
|
+
lockPath: string;
|
|
30
|
+
repoRoot: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
owner?: string;
|
|
33
|
+
fn: () => Promise<T>;
|
|
34
|
+
}): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Lock diagnostics and status information.
|
|
37
|
+
*/
|
|
38
|
+
export type LockStatus = {
|
|
39
|
+
exists: boolean;
|
|
40
|
+
path: string;
|
|
41
|
+
pid?: number;
|
|
42
|
+
pidAlive?: boolean;
|
|
43
|
+
instanceId?: string;
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
owner?: string;
|
|
46
|
+
leaseId?: string;
|
|
47
|
+
createdAt?: string;
|
|
48
|
+
updatedAt?: string;
|
|
49
|
+
ageMs?: number;
|
|
50
|
+
isStale?: boolean;
|
|
51
|
+
repoRoot?: string;
|
|
52
|
+
version?: number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Get lock file status and diagnostics.
|
|
56
|
+
* Returns detailed information about the current lock state.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getLockStatus(lockPath: string, staleMs?: number): LockStatus;
|
|
59
|
+
/**
|
|
60
|
+
* Attempt to remove a lock file if it's safe to do so.
|
|
61
|
+
* Only removes locks with dead PIDs or stale timestamps.
|
|
62
|
+
* Returns true if lock was removed, false if lock is still held.
|
|
63
|
+
*/
|
|
64
|
+
export declare function tryRemoveStaleLock(lockPath: string, staleMs?: number): {
|
|
65
|
+
removed: boolean;
|
|
66
|
+
reason: string;
|
|
3
67
|
};
|