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/src/ui/inject.ts
CHANGED
|
@@ -1,136 +1,129 @@
|
|
|
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.
|
|
11
|
-
|
|
12
|
-
type QueueItem = {
|
|
2
|
+
type InjectionItem = {
|
|
13
3
|
ctx: any;
|
|
14
4
|
sessionId: string;
|
|
15
5
|
text: string;
|
|
16
6
|
agent?: string;
|
|
7
|
+
attempts: number;
|
|
8
|
+
resolve: () => void;
|
|
9
|
+
reject: (err: any) => void;
|
|
17
10
|
};
|
|
18
11
|
|
|
19
|
-
const
|
|
12
|
+
const MAX_ATTEMPTS = 4;
|
|
13
|
+
const RETRY_DELAYS_MS = [250, 500, 1000, 2000]; // attempt 1..4
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let drainWaiters: Array<() => void> = [];
|
|
15
|
+
// Per-session queues so one stuck session doesn't block others
|
|
16
|
+
const queues = new Map<string, InjectionItem[]>();
|
|
17
|
+
const running = new Set<string>();
|
|
25
18
|
|
|
26
19
|
function sleep(ms: number) {
|
|
27
|
-
return new Promise
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function resolveDrainWaitersIfIdle() {
|
|
31
|
-
if (workerRunning) return;
|
|
32
|
-
if (injectionQueue.length !== 0) return;
|
|
33
|
-
|
|
34
|
-
const waiters = drainWaiters;
|
|
35
|
-
drainWaiters = [];
|
|
36
|
-
for (const w of waiters) w();
|
|
20
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
37
21
|
}
|
|
38
22
|
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
|
|
23
|
+
function getPromptInvoker(ctx: any): { session: any; prompt: Function } {
|
|
24
|
+
const session = ctx?.client?.session;
|
|
25
|
+
const prompt = session?.prompt;
|
|
26
|
+
if (!session || typeof prompt !== "function") {
|
|
27
|
+
throw new Error("API not available (ctx.client.session.prompt)");
|
|
28
|
+
}
|
|
29
|
+
return { session, prompt };
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
async function
|
|
45
|
-
const { ctx, sessionId, text } =
|
|
46
|
-
const agent = opts.agent ?? "Astro";
|
|
32
|
+
async function tryInjectOnce(item: InjectionItem): Promise<void> {
|
|
33
|
+
const { ctx, sessionId, text, agent = "Astro" } = item;
|
|
47
34
|
const prefixedText = `[${agent}]\n\n${text}`;
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
console.warn("[Astrocode] Injection skipped: missing sessionId");
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
36
|
+
const { session, prompt } = getPromptInvoker(ctx);
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
while (attempt < maxAttempts) {
|
|
64
|
-
attempt += 1;
|
|
65
|
-
try {
|
|
66
|
-
await prompt({
|
|
67
|
-
path: { id: sessionId },
|
|
68
|
-
body: {
|
|
69
|
-
parts: [{ type: "text", text: prefixedText }],
|
|
70
|
-
agent,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
return;
|
|
74
|
-
} catch (err) {
|
|
75
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
-
const isLast = attempt >= maxAttempts;
|
|
77
|
-
|
|
78
|
-
if (isLast) {
|
|
79
|
-
console.warn(`[Astrocode] Injection failed (final): ${msg}`);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Exponential backoff + jitter
|
|
84
|
-
const base = 150 * Math.pow(2, attempt - 1); // 150, 300, 600
|
|
85
|
-
const jitter = Math.floor(Math.random() * 120);
|
|
86
|
-
await sleep(base + jitter);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
38
|
+
// IMPORTANT: force correct `this` binding
|
|
39
|
+
await prompt.call(session, {
|
|
40
|
+
path: { id: sessionId },
|
|
41
|
+
body: {
|
|
42
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
43
|
+
agent,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
89
46
|
}
|
|
90
47
|
|
|
91
|
-
async function
|
|
92
|
-
if (
|
|
93
|
-
|
|
48
|
+
async function runSessionQueue(sessionId: string) {
|
|
49
|
+
if (running.has(sessionId)) return;
|
|
50
|
+
running.add(sessionId);
|
|
94
51
|
|
|
95
52
|
try {
|
|
96
|
-
//
|
|
97
|
-
while (
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
|
|
53
|
+
// eslint-disable-next-line no-constant-condition
|
|
54
|
+
while (true) {
|
|
55
|
+
const q = queues.get(sessionId);
|
|
56
|
+
if (!q || q.length === 0) break;
|
|
57
|
+
|
|
58
|
+
const item = q.shift()!;
|
|
59
|
+
try {
|
|
60
|
+
await tryInjectOnce(item);
|
|
61
|
+
item.resolve();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
item.attempts += 1;
|
|
64
|
+
|
|
65
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
66
|
+
const delay = RETRY_DELAYS_MS[Math.min(item.attempts - 1, RETRY_DELAYS_MS.length - 1)] ?? 2000;
|
|
67
|
+
|
|
68
|
+
if (item.attempts >= MAX_ATTEMPTS) {
|
|
69
|
+
console.warn(
|
|
70
|
+
`[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`
|
|
71
|
+
);
|
|
72
|
+
item.reject(err);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.warn(
|
|
77
|
+
`[Astrocode] Injection attempt ${item.attempts}/${MAX_ATTEMPTS} failed: ${msg}; retrying in ${delay}ms`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await sleep(delay);
|
|
81
|
+
|
|
82
|
+
// Requeue at front to preserve order (and avoid starving later messages)
|
|
83
|
+
const q2 = queues.get(sessionId) ?? [];
|
|
84
|
+
q2.unshift(item);
|
|
85
|
+
queues.set(sessionId, q2);
|
|
86
|
+
}
|
|
101
87
|
}
|
|
102
88
|
} finally {
|
|
103
|
-
|
|
104
|
-
resolveDrainWaitersIfIdle();
|
|
89
|
+
running.delete(sessionId);
|
|
105
90
|
}
|
|
106
91
|
}
|
|
107
92
|
|
|
108
93
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
94
|
+
* Inject a visible prompt into the conversation.
|
|
95
|
+
* - Deterministic ordering per session
|
|
96
|
+
* - Correct SDK binding (prevents `this._client` undefined)
|
|
97
|
+
* - Awaitable: resolves when delivered, rejects after max retries
|
|
111
98
|
*/
|
|
112
|
-
export function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
export async function injectChatPrompt(opts: {
|
|
100
|
+
ctx: any;
|
|
101
|
+
sessionId?: string;
|
|
102
|
+
text: string;
|
|
103
|
+
agent?: string;
|
|
104
|
+
}): Promise<void> {
|
|
105
|
+
const sessionId = opts.sessionId ?? (opts.ctx as any)?.sessionID;
|
|
106
|
+
if (!sessionId) {
|
|
107
|
+
console.warn("[Astrocode] Skipping injection: No sessionId provided");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
return new Promise<void>((resolve, reject) => {
|
|
112
|
+
const item: InjectionItem = {
|
|
113
|
+
ctx: opts.ctx,
|
|
114
|
+
sessionId,
|
|
115
|
+
text: opts.text,
|
|
116
|
+
agent: opts.agent,
|
|
117
|
+
attempts: 0,
|
|
118
|
+
resolve,
|
|
119
|
+
reject,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const q = queues.get(sessionId) ?? [];
|
|
123
|
+
q.push(item);
|
|
124
|
+
queues.set(sessionId, q);
|
|
125
|
+
|
|
126
|
+
// Fire worker (don't await here; caller awaits the returned Promise)
|
|
127
|
+
void runSessionQueue(sessionId);
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Deterministic helper: enqueue + flush (recommended for stage boundaries).
|
|
132
|
-
*/
|
|
133
|
-
export async function injectChatPrompt(opts: QueueItem): Promise<void> {
|
|
134
|
-
enqueueChatPrompt(opts);
|
|
135
|
-
await flushChatPrompts();
|
|
136
|
-
}
|