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/src/ui/inject.ts CHANGED
@@ -1,136 +1,107 @@
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
+
3
+ let isInjecting = false;
4
+
5
+ const injectionQueue: Array<{
13
6
  ctx: any;
14
- sessionId: string;
7
+ sessionId?: string;
15
8
  text: string;
16
9
  agent?: string;
17
- };
18
-
19
- const injectionQueue: QueueItem[] = [];
20
-
21
- let workerRunning = false;
22
-
23
- // Used to let callers await "queue drained"
24
- let drainWaiters: Array<() => void> = [];
10
+ toast?: { title: string; message: string; variant?: "info" | "success" | "warning" | "error"; durationMs?: number };
11
+ retry?: { maxAttempts?: number; baseDelayMs?: number };
12
+ }> = [];
25
13
 
26
14
  function sleep(ms: number) {
27
- return new Promise<void>((resolve) => setTimeout(resolve, ms));
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();
15
+ return new Promise((r) => setTimeout(r, ms));
37
16
  }
38
17
 
39
- function getPromptApi(ctx: any) {
40
- const fn = ctx?.client?.session?.prompt;
41
- return typeof fn === "function" ? fn.bind(ctx.client.session) : null;
18
+ function resolveSessionId(ctx: any, sessionId?: string): string | null {
19
+ if (sessionId) return sessionId;
20
+ const direct = (ctx as any)?.sessionID ?? (ctx as any)?.sessionId ?? (ctx as any)?.session?.id;
21
+ if (typeof direct === "string" && direct.length > 0) return direct;
22
+ return null;
42
23
  }
43
24
 
44
- async function sendWithRetries(opts: QueueItem): Promise<void> {
45
- const { ctx, sessionId, text } = opts;
46
- const agent = opts.agent ?? "Astro";
47
- const prefixedText = `[${agent}]\n\n${text}`;
48
-
49
- if (!sessionId) {
50
- console.warn("[Astrocode] Injection skipped: missing sessionId");
51
- return;
52
- }
25
+ async function tryInjectOnce(opts: { ctx: any; sessionId: string; text: string; agent: string }): Promise<void> {
26
+ const { ctx, sessionId, text, agent } = opts;
53
27
 
54
- const prompt = getPromptApi(ctx);
55
- if (!prompt) {
56
- console.warn("[Astrocode] Injection skipped: ctx.client.session.prompt unavailable");
57
- return;
28
+ // Prefer explicit chat prompt API
29
+ const promptApi = (ctx as any)?.client?.session?.prompt;
30
+ if (!promptApi) {
31
+ throw new Error("API not available (ctx.client.session.prompt)");
58
32
  }
59
33
 
60
- const maxAttempts = 3;
61
- let attempt = 0;
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
- }
34
+ const prefixedText = `[${agent}]\n\n${text}`;
82
35
 
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
- }
36
+ // Some hosts reject unknown fields; keep body minimal and stable.
37
+ await promptApi({
38
+ path: { id: sessionId },
39
+ body: {
40
+ parts: [{ type: "text", text: prefixedText }],
41
+ },
42
+ });
89
43
  }
90
44
 
91
- async function runWorkerLoop(): Promise<void> {
92
- if (workerRunning) return;
93
- workerRunning = true;
45
+ async function processQueue() {
46
+ if (isInjecting) return;
47
+ if (injectionQueue.length === 0) return;
48
+
49
+ isInjecting = true;
94
50
 
95
51
  try {
96
- // Drain sequentially to preserve ordering
97
52
  while (injectionQueue.length > 0) {
98
53
  const item = injectionQueue.shift();
99
54
  if (!item) continue;
100
- await sendWithRetries(item);
55
+
56
+ const { ctx, text, agent = "Astro" } = item;
57
+ const sessionId = resolveSessionId(ctx, item.sessionId);
58
+
59
+ if (!sessionId) {
60
+ // Drop on floor: we cannot recover without a session id.
61
+ // Keep draining the queue so we don't stall.
62
+ // eslint-disable-next-line no-console
63
+ console.warn("[Astrocode] Injection skipped: no sessionId");
64
+ continue;
65
+ }
66
+
67
+ const maxAttempts = item.retry?.maxAttempts ?? 4;
68
+ const baseDelayMs = item.retry?.baseDelayMs ?? 250;
69
+
70
+ let lastErr: any = null;
71
+
72
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
73
+ try {
74
+ await tryInjectOnce({ ctx, sessionId, text, agent });
75
+ lastErr = null;
76
+ break;
77
+ } catch (e) {
78
+ lastErr = e;
79
+ const delay = baseDelayMs * Math.pow(2, attempt - 1); // 250, 500, 1000, 2000
80
+ // eslint-disable-next-line no-console
81
+ console.warn(`[Astrocode] Injection attempt ${attempt}/${maxAttempts} failed: ${String(e)}; retrying in ${delay}ms`);
82
+ await sleep(delay);
83
+ }
84
+ }
85
+
86
+ if (lastErr) {
87
+ // eslint-disable-next-line no-console
88
+ console.warn(`[Astrocode] Injection failed permanently after ${maxAttempts} attempts: ${String(lastErr)}`);
89
+ }
101
90
  }
102
91
  } finally {
103
- workerRunning = false;
104
- resolveDrainWaitersIfIdle();
92
+ isInjecting = false;
105
93
  }
106
94
  }
107
95
 
108
- /**
109
- * Enqueue an injection and ensure the worker is running.
110
- * Does NOT wait for delivery — use `flushChatPrompts()` to wait.
111
- */
112
- export function enqueueChatPrompt(opts: QueueItem) {
113
- injectionQueue.push(opts);
114
- // Kick worker
115
- void runWorkerLoop();
116
- }
117
-
118
- /**
119
- * Wait until all queued injections have been processed (sent or exhausted retries).
120
- */
121
- export function flushChatPrompts(): Promise<void> {
122
- if (!workerRunning && injectionQueue.length === 0) return Promise.resolve();
123
- return new Promise<void>((resolve) => {
124
- drainWaiters.push(resolve);
125
- // Ensure worker is running (in case someone enqueued without kick)
126
- void runWorkerLoop();
96
+ export async function injectChatPrompt(opts: { ctx: any; sessionId?: string; text: string; agent?: string }) {
97
+ injectionQueue.push({
98
+ ctx: opts.ctx,
99
+ sessionId: opts.sessionId,
100
+ text: opts.text,
101
+ agent: opts.agent ?? "Astro",
102
+ retry: { maxAttempts: 4, baseDelayMs: 250 },
127
103
  });
128
- }
129
104
 
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();
105
+ // Fire-and-forget; queue drain is serialized by isInjecting.
106
+ void processQueue();
136
107
  }