astrocode-workflow 0.2.0 → 0.3.0
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/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +114 -94
- package/dist/ui/inject.d.ts +3 -17
- package/dist/ui/inject.js +68 -98
- package/package.json +1 -1
- package/src/state/db.ts +63 -4
- package/src/tools/workflow.ts +155 -136
- package/src/tools/workflow.ts.backup +681 -0
- package/src/ui/inject.ts +78 -107
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 {
|
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;
|
package/dist/tools/workflow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
// src/tools/workflow.ts
|
|
1
2
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
-
import { withTx } from "../state/db";
|
|
3
3
|
import { buildContextSnapshot } from "../workflow/context";
|
|
4
4
|
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
|
|
5
5
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
@@ -31,24 +31,20 @@ export function resolveAgentName(stageKey, config, agents, warnings) {
|
|
|
31
31
|
// Validate that the agent actually exists in the registry
|
|
32
32
|
if (agents && !agents[candidate]) {
|
|
33
33
|
const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
|
|
34
|
-
if (warnings)
|
|
34
|
+
if (warnings)
|
|
35
35
|
warnings.push(warning);
|
|
36
|
-
|
|
37
|
-
else {
|
|
36
|
+
else
|
|
38
37
|
console.warn(`[Astrocode] ${warning}`);
|
|
39
|
-
}
|
|
40
38
|
candidate = "General";
|
|
41
39
|
}
|
|
42
40
|
// Final guard: ensure General exists, fallback to built-in "general" if not
|
|
43
41
|
if (agents && !agents[candidate]) {
|
|
44
42
|
const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
|
|
45
|
-
if (warnings)
|
|
43
|
+
if (warnings)
|
|
46
44
|
warnings.push(finalWarning);
|
|
47
|
-
|
|
48
|
-
else {
|
|
45
|
+
else
|
|
49
46
|
console.warn(`[Astrocode] ${finalWarning}`);
|
|
50
|
-
|
|
51
|
-
return "general"; // built-in, guaranteed by OpenCode
|
|
47
|
+
return "general";
|
|
52
48
|
}
|
|
53
49
|
return candidate;
|
|
54
50
|
}
|
|
@@ -84,9 +80,6 @@ function stageConstraints(stage, cfg) {
|
|
|
84
80
|
}
|
|
85
81
|
return common;
|
|
86
82
|
}
|
|
87
|
-
function agentNameForStage(stage, cfg) {
|
|
88
|
-
return cfg.agents.stage_agent_names[stage];
|
|
89
|
-
}
|
|
90
83
|
function buildDelegationPrompt(opts) {
|
|
91
84
|
const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
|
|
92
85
|
const stageUpper = stage_key.toUpperCase();
|
|
@@ -108,7 +101,6 @@ function buildDelegationPrompt(opts) {
|
|
|
108
101
|
``,
|
|
109
102
|
`Important: do NOT do any stage work yourself in orchestrator mode.`,
|
|
110
103
|
].join("\n").trim();
|
|
111
|
-
// Debug log the delegation prompt to troubleshoot agent output issues
|
|
112
104
|
debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
|
|
113
105
|
return prompt;
|
|
114
106
|
}
|
|
@@ -134,59 +126,54 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
134
126
|
break;
|
|
135
127
|
}
|
|
136
128
|
if (next.kind === "start_run") {
|
|
137
|
-
|
|
129
|
+
// NOTE: createRunForStory owns its own tx (state-machine.ts).
|
|
130
|
+
const { run_id } = createRunForStory(db, config, next.story_key);
|
|
138
131
|
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
139
132
|
if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
|
|
140
133
|
await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
|
|
141
134
|
}
|
|
135
|
+
if (sessionId) {
|
|
136
|
+
await injectChatPrompt({
|
|
137
|
+
ctx,
|
|
138
|
+
sessionId,
|
|
139
|
+
agent: "Astro",
|
|
140
|
+
text: [
|
|
141
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_STARTED]`,
|
|
142
|
+
``,
|
|
143
|
+
`Run started: \`${run_id}\``,
|
|
144
|
+
`Story: \`${next.story_key}\``,
|
|
145
|
+
``,
|
|
146
|
+
`Next: call **astro_workflow_proceed** again to delegate the first stage.`,
|
|
147
|
+
].join("\n"),
|
|
148
|
+
});
|
|
149
|
+
actions.push(`injected run started message for ${run_id}`);
|
|
150
|
+
}
|
|
142
151
|
if (mode === "step")
|
|
143
152
|
break;
|
|
144
153
|
continue;
|
|
145
154
|
}
|
|
146
155
|
if (next.kind === "complete_run") {
|
|
147
|
-
|
|
156
|
+
// NOTE: completeRun owns its own tx (state-machine.ts).
|
|
157
|
+
completeRun(db, next.run_id);
|
|
148
158
|
actions.push(`completed run ${next.run_id}`);
|
|
149
159
|
if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
|
|
150
160
|
await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
|
|
151
161
|
}
|
|
152
|
-
//
|
|
162
|
+
// ✅ explicit injection on completeRun (requested)
|
|
153
163
|
if (sessionId) {
|
|
154
|
-
const continueDirective = [
|
|
155
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
|
|
156
|
-
``,
|
|
157
|
-
`Run ${next.run_id} completed successfully.`,
|
|
158
|
-
``,
|
|
159
|
-
`The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
|
|
160
|
-
``,
|
|
161
|
-
`Next actions: Review the generated stories and approve the next one to continue development.`,
|
|
162
|
-
].join("\n");
|
|
163
|
-
await injectChatPrompt({
|
|
164
|
-
ctx,
|
|
165
|
-
sessionId,
|
|
166
|
-
text: continueDirective,
|
|
167
|
-
agent: "Astro"
|
|
168
|
-
});
|
|
169
|
-
actions.push(`injected continuation directive for completed run ${next.run_id}`);
|
|
170
|
-
}
|
|
171
|
-
// Check for next approved story to start
|
|
172
|
-
const nextStory = db.prepare("SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1").get();
|
|
173
|
-
if (nextStory && sessionId) {
|
|
174
|
-
const nextDirective = [
|
|
175
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
|
|
176
|
-
``,
|
|
177
|
-
`The previous run completed successfully. Start the next approved story.`,
|
|
178
|
-
``,
|
|
179
|
-
`Next Story: ${nextStory.story_key} — ${nextStory.title}`,
|
|
180
|
-
``,
|
|
181
|
-
`Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
|
|
182
|
-
].join("\n");
|
|
183
164
|
await injectChatPrompt({
|
|
184
165
|
ctx,
|
|
185
166
|
sessionId,
|
|
186
|
-
|
|
187
|
-
|
|
167
|
+
agent: "Astro",
|
|
168
|
+
text: [
|
|
169
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
170
|
+
``,
|
|
171
|
+
`Run \`${next.run_id}\` completed.`,
|
|
172
|
+
``,
|
|
173
|
+
`Next: call **astro_workflow_proceed** (mode=step) to start the next approved story (if any).`,
|
|
174
|
+
].join("\n"),
|
|
188
175
|
});
|
|
189
|
-
actions.push(`injected
|
|
176
|
+
actions.push(`injected run completed message for ${next.run_id}`);
|
|
190
177
|
}
|
|
191
178
|
if (mode === "step")
|
|
192
179
|
break;
|
|
@@ -198,53 +185,55 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
198
185
|
throw new Error("Invariant: delegate_stage but no active run.");
|
|
199
186
|
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
200
187
|
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
|
|
201
|
-
// Mark stage started + set subagent_type to the stage agent.
|
|
202
188
|
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
203
|
-
// Validate agent availability with fallback chain
|
|
204
|
-
const systemConfig = config;
|
|
205
|
-
// Check both the system config agent map (if present) OR the local agents map passed to the tool
|
|
206
189
|
const agentExists = (name) => {
|
|
207
|
-
|
|
208
|
-
if (agents && agents[name]) {
|
|
190
|
+
if (agents && agents[name])
|
|
209
191
|
return true;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (systemConfig.agent && systemConfig.agent[name]) {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
// For known stage agents, assume they exist (they are system-provided subagents)
|
|
216
|
-
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
|
|
217
|
-
if (knownStageAgents.includes(name)) {
|
|
192
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
193
|
+
if (knownStageAgents.includes(name))
|
|
218
194
|
return true;
|
|
219
|
-
}
|
|
220
195
|
return false;
|
|
221
196
|
};
|
|
222
197
|
if (!agentExists(agentName)) {
|
|
223
198
|
const originalAgent = agentName;
|
|
224
|
-
console.warn(`[Astrocode] Agent ${agentName} not found
|
|
225
|
-
// First fallback: orchestrator
|
|
199
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
226
200
|
agentName = config.agents?.orchestrator_name || "Astro";
|
|
227
201
|
if (!agentExists(agentName)) {
|
|
228
202
|
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
229
|
-
// Final fallback: General (guaranteed to exist)
|
|
230
203
|
agentName = "General";
|
|
231
204
|
if (!agentExists(agentName)) {
|
|
232
205
|
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
233
206
|
}
|
|
234
207
|
}
|
|
235
208
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (config.debug?.telemetry?.enabled) {
|
|
240
|
-
// eslint-disable-next-line no-console
|
|
241
|
-
console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
209
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
210
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
|
|
211
|
+
actions.push(`stage started: ${next.stage_key}`);
|
|
244
212
|
if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
|
|
245
213
|
await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
|
|
246
214
|
}
|
|
247
|
-
|
|
215
|
+
// ✅ explicit injection on startStage (requested)
|
|
216
|
+
if (sessionId) {
|
|
217
|
+
await injectChatPrompt({
|
|
218
|
+
ctx,
|
|
219
|
+
sessionId,
|
|
220
|
+
agent: "Astro",
|
|
221
|
+
text: [
|
|
222
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
223
|
+
``,
|
|
224
|
+
`Run: \`${active.run_id}\``,
|
|
225
|
+
`Stage: \`${next.stage_key}\``,
|
|
226
|
+
`Delegated to: \`${agentName}\``,
|
|
227
|
+
].join("\n"),
|
|
228
|
+
});
|
|
229
|
+
actions.push(`injected stage started message for ${next.stage_key}`);
|
|
230
|
+
}
|
|
231
|
+
const context = buildContextSnapshot({
|
|
232
|
+
db,
|
|
233
|
+
config,
|
|
234
|
+
run_id: active.run_id,
|
|
235
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
236
|
+
});
|
|
248
237
|
const stageDirective = buildStageDirective({
|
|
249
238
|
config,
|
|
250
239
|
stage_key: next.stage_key,
|
|
@@ -262,25 +251,29 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
262
251
|
stage_key: next.stage_key,
|
|
263
252
|
stage_agent_name: agentName,
|
|
264
253
|
});
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
// Best-effort: continuations table may not exist on older DBs.
|
|
255
|
+
try {
|
|
256
|
+
const h = directiveHash(delegatePrompt);
|
|
257
|
+
const now = nowISO();
|
|
258
|
+
if (sessionId) {
|
|
259
|
+
db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
|
|
270
264
|
}
|
|
271
265
|
// Visible injection so user can see state
|
|
272
266
|
if (sessionId) {
|
|
273
267
|
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
274
|
-
// Inject continuation guidance
|
|
275
268
|
const continueMessage = [
|
|
276
269
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
277
270
|
``,
|
|
278
|
-
`Stage
|
|
271
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
279
272
|
``,
|
|
280
|
-
`When
|
|
273
|
+
`When \`${agentName}\` completes, call:`,
|
|
281
274
|
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
282
275
|
``,
|
|
283
|
-
`
|
|
276
|
+
`Then run **astro_workflow_proceed** again.`,
|
|
284
277
|
].join("\n");
|
|
285
278
|
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
286
279
|
}
|
|
@@ -290,35 +283,62 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
290
283
|
}
|
|
291
284
|
if (next.kind === "await_stage_completion") {
|
|
292
285
|
actions.push(`await stage completion: ${next.stage_key}`);
|
|
293
|
-
// Optionally nudge with a short directive
|
|
294
286
|
if (sessionId) {
|
|
295
|
-
const context = buildContextSnapshot({
|
|
287
|
+
const context = buildContextSnapshot({
|
|
288
|
+
db,
|
|
289
|
+
config,
|
|
290
|
+
run_id: next.run_id,
|
|
291
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
292
|
+
});
|
|
296
293
|
const prompt = [
|
|
297
294
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
298
295
|
``,
|
|
299
|
-
`Run
|
|
296
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
300
297
|
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
301
298
|
``,
|
|
302
299
|
`Context snapshot:`,
|
|
303
300
|
context,
|
|
304
301
|
].join("\n").trim();
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
try {
|
|
303
|
+
const h = directiveHash(prompt);
|
|
304
|
+
const now = nowISO();
|
|
305
|
+
db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
|
|
309
|
+
}
|
|
308
310
|
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
309
311
|
}
|
|
310
312
|
break;
|
|
311
313
|
}
|
|
312
314
|
if (next.kind === "failed") {
|
|
313
315
|
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
316
|
+
if (sessionId) {
|
|
317
|
+
await injectChatPrompt({
|
|
318
|
+
ctx,
|
|
319
|
+
sessionId,
|
|
320
|
+
agent: "Astro",
|
|
321
|
+
text: [
|
|
322
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
323
|
+
``,
|
|
324
|
+
`Run \`${next.run_id}\` failed at stage \`${next.stage_key}\`.`,
|
|
325
|
+
`Error: ${next.error_text}`,
|
|
326
|
+
].join("\n"),
|
|
327
|
+
});
|
|
328
|
+
actions.push(`injected run failed message for ${next.run_id}`);
|
|
329
|
+
}
|
|
314
330
|
break;
|
|
315
331
|
}
|
|
316
|
-
// safety
|
|
317
332
|
actions.push(`unhandled next action: ${next.kind}`);
|
|
318
333
|
break;
|
|
319
334
|
}
|
|
320
|
-
// Housekeeping event
|
|
321
|
-
|
|
335
|
+
// Housekeeping event (best-effort)
|
|
336
|
+
try {
|
|
337
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
warnings.push(`workflow.proceed event insert failed (non-fatal): ${String(e)}`);
|
|
341
|
+
}
|
|
322
342
|
const active = getActiveRun(db);
|
|
323
343
|
const lines = [];
|
|
324
344
|
lines.push(`# astro_workflow_proceed`);
|
package/dist/ui/inject.d.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
export declare function injectChatPrompt(opts: {
|
|
2
2
|
ctx: any;
|
|
3
|
-
sessionId
|
|
3
|
+
sessionId?: string;
|
|
4
4
|
text: string;
|
|
5
5
|
agent?: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Enqueue an injection and ensure the worker is running.
|
|
9
|
-
* Does NOT wait for delivery — use `flushChatPrompts()` to wait.
|
|
10
|
-
*/
|
|
11
|
-
export declare function enqueueChatPrompt(opts: QueueItem): void;
|
|
12
|
-
/**
|
|
13
|
-
* Wait until all queued injections have been processed (sent or exhausted retries).
|
|
14
|
-
*/
|
|
15
|
-
export declare function flushChatPrompts(): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Deterministic helper: enqueue + flush (recommended for stage boundaries).
|
|
18
|
-
*/
|
|
19
|
-
export declare function injectChatPrompt(opts: QueueItem): Promise<void>;
|
|
20
|
-
export {};
|
|
6
|
+
}): Promise<void>;
|
package/dist/ui/inject.js
CHANGED
|
@@ -1,118 +1,88 @@
|
|
|
1
1
|
// src/ui/inject.ts
|
|
2
|
-
|
|
3
|
-
// Deterministic chat injection:
|
|
4
|
-
// - Always enqueue
|
|
5
|
-
// - Process sequentially (per-process single worker)
|
|
6
|
-
// - Retries with backoff
|
|
7
|
-
// - flush() lets callers wait until injections are actually sent
|
|
8
|
-
//
|
|
9
|
-
// IMPORTANT: Callers who need reliability must `await injectChatPrompt(...)`
|
|
10
|
-
// or `await flushChatPrompts()` after enqueueing.
|
|
2
|
+
let isInjecting = false;
|
|
11
3
|
const injectionQueue = [];
|
|
12
|
-
let workerRunning = false;
|
|
13
|
-
// Used to let callers await "queue drained"
|
|
14
|
-
let drainWaiters = [];
|
|
15
4
|
function sleep(ms) {
|
|
16
|
-
return new Promise((
|
|
5
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
17
6
|
}
|
|
18
|
-
function
|
|
19
|
-
if (
|
|
20
|
-
return;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (const w of waiters)
|
|
26
|
-
w();
|
|
27
|
-
}
|
|
28
|
-
function getPromptApi(ctx) {
|
|
29
|
-
const fn = ctx?.client?.session?.prompt;
|
|
30
|
-
return typeof fn === "function" ? fn.bind(ctx.client.session) : null;
|
|
7
|
+
function resolveSessionId(ctx, sessionId) {
|
|
8
|
+
if (sessionId)
|
|
9
|
+
return sessionId;
|
|
10
|
+
const direct = ctx?.sessionID ?? ctx?.sessionId ?? ctx?.session?.id;
|
|
11
|
+
if (typeof direct === "string" && direct.length > 0)
|
|
12
|
+
return direct;
|
|
13
|
+
return null;
|
|
31
14
|
}
|
|
32
|
-
async function
|
|
33
|
-
const { ctx, sessionId, text } = opts;
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const prompt = getPromptApi(ctx);
|
|
41
|
-
if (!prompt) {
|
|
42
|
-
console.warn("[Astrocode] Injection skipped: ctx.client.session.prompt unavailable");
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const maxAttempts = 3;
|
|
46
|
-
let attempt = 0;
|
|
47
|
-
while (attempt < maxAttempts) {
|
|
48
|
-
attempt += 1;
|
|
49
|
-
try {
|
|
50
|
-
await prompt({
|
|
51
|
-
path: { id: sessionId },
|
|
52
|
-
body: {
|
|
53
|
-
parts: [{ type: "text", text: prefixedText }],
|
|
54
|
-
agent,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
-
const isLast = attempt >= maxAttempts;
|
|
62
|
-
if (isLast) {
|
|
63
|
-
console.warn(`[Astrocode] Injection failed (final): ${msg}`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
// Exponential backoff + jitter
|
|
67
|
-
const base = 150 * Math.pow(2, attempt - 1); // 150, 300, 600
|
|
68
|
-
const jitter = Math.floor(Math.random() * 120);
|
|
69
|
-
await sleep(base + jitter);
|
|
70
|
-
}
|
|
15
|
+
async function tryInjectOnce(opts) {
|
|
16
|
+
const { ctx, sessionId, text, agent } = opts;
|
|
17
|
+
// Prefer explicit chat prompt API
|
|
18
|
+
const promptApi = ctx?.client?.session?.prompt;
|
|
19
|
+
if (!promptApi) {
|
|
20
|
+
throw new Error("API not available (ctx.client.session.prompt)");
|
|
71
21
|
}
|
|
22
|
+
const prefixedText = `[${agent}]\n\n${text}`;
|
|
23
|
+
// Some hosts reject unknown fields; keep body minimal and stable.
|
|
24
|
+
await promptApi({
|
|
25
|
+
path: { id: sessionId },
|
|
26
|
+
body: {
|
|
27
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
72
30
|
}
|
|
73
|
-
async function
|
|
74
|
-
if (
|
|
31
|
+
async function processQueue() {
|
|
32
|
+
if (isInjecting)
|
|
33
|
+
return;
|
|
34
|
+
if (injectionQueue.length === 0)
|
|
75
35
|
return;
|
|
76
|
-
|
|
36
|
+
isInjecting = true;
|
|
77
37
|
try {
|
|
78
|
-
// Drain sequentially to preserve ordering
|
|
79
38
|
while (injectionQueue.length > 0) {
|
|
80
39
|
const item = injectionQueue.shift();
|
|
81
40
|
if (!item)
|
|
82
41
|
continue;
|
|
83
|
-
|
|
42
|
+
const { ctx, text, agent = "Astro" } = item;
|
|
43
|
+
const sessionId = resolveSessionId(ctx, item.sessionId);
|
|
44
|
+
if (!sessionId) {
|
|
45
|
+
// Drop on floor: we cannot recover without a session id.
|
|
46
|
+
// Keep draining the queue so we don't stall.
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.warn("[Astrocode] Injection skipped: no sessionId");
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const maxAttempts = item.retry?.maxAttempts ?? 4;
|
|
52
|
+
const baseDelayMs = item.retry?.baseDelayMs ?? 250;
|
|
53
|
+
let lastErr = null;
|
|
54
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
await tryInjectOnce({ ctx, sessionId, text, agent });
|
|
57
|
+
lastErr = null;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
lastErr = e;
|
|
62
|
+
const delay = baseDelayMs * Math.pow(2, attempt - 1); // 250, 500, 1000, 2000
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(`[Astrocode] Injection attempt ${attempt}/${maxAttempts} failed: ${String(e)}; retrying in ${delay}ms`);
|
|
65
|
+
await sleep(delay);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (lastErr) {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.warn(`[Astrocode] Injection failed permanently after ${maxAttempts} attempts: ${String(lastErr)}`);
|
|
71
|
+
}
|
|
84
72
|
}
|
|
85
73
|
}
|
|
86
74
|
finally {
|
|
87
|
-
|
|
88
|
-
resolveDrainWaitersIfIdle();
|
|
75
|
+
isInjecting = false;
|
|
89
76
|
}
|
|
90
77
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Enqueue an injection and ensure the worker is running.
|
|
93
|
-
* Does NOT wait for delivery — use `flushChatPrompts()` to wait.
|
|
94
|
-
*/
|
|
95
|
-
export function enqueueChatPrompt(opts) {
|
|
96
|
-
injectionQueue.push(opts);
|
|
97
|
-
// Kick worker
|
|
98
|
-
void runWorkerLoop();
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Wait until all queued injections have been processed (sent or exhausted retries).
|
|
102
|
-
*/
|
|
103
|
-
export function flushChatPrompts() {
|
|
104
|
-
if (!workerRunning && injectionQueue.length === 0)
|
|
105
|
-
return Promise.resolve();
|
|
106
|
-
return new Promise((resolve) => {
|
|
107
|
-
drainWaiters.push(resolve);
|
|
108
|
-
// Ensure worker is running (in case someone enqueued without kick)
|
|
109
|
-
void runWorkerLoop();
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Deterministic helper: enqueue + flush (recommended for stage boundaries).
|
|
114
|
-
*/
|
|
115
78
|
export async function injectChatPrompt(opts) {
|
|
116
|
-
|
|
117
|
-
|
|
79
|
+
injectionQueue.push({
|
|
80
|
+
ctx: opts.ctx,
|
|
81
|
+
sessionId: opts.sessionId,
|
|
82
|
+
text: opts.text,
|
|
83
|
+
agent: opts.agent ?? "Astro",
|
|
84
|
+
retry: { maxAttempts: 4, baseDelayMs: 250 },
|
|
85
|
+
});
|
|
86
|
+
// Fire-and-forget; queue drain is serialized by isInjecting.
|
|
87
|
+
void processQueue();
|
|
118
88
|
}
|