shraga 0.1.68 → 0.1.69
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/package.json +1 -1
- package/src/server/para/streamer.ts +33 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -36,6 +36,8 @@ export interface ParaStreamerOptions {
|
|
|
36
36
|
flushThreshold?: number;
|
|
37
37
|
/** Show a transient inline marker per tool call, as the Slack streamer does. Default on. */
|
|
38
38
|
toolMarkers?: boolean;
|
|
39
|
+
/** Per-POST wall clock. See `POST_TIMEOUT_MS`. Overridable for tests. */
|
|
40
|
+
postTimeout?: number;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/** Signature contract, mirrored byte-for-byte in para-li's `lib/agent-conn.ts#signPayload`.
|
|
@@ -58,17 +60,36 @@ export function signPara(secret: string, connId: string, deliveryId: string, ts:
|
|
|
58
60
|
return 'v1=' + createHmac('sha256', secret).update(`${connId}.${deliveryId}.${ts}.${rawBody}`).digest('hex');
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Wall clock on ONE delivery. Mirrors the 20s AbortController on para-li's own `dispatchAgentTurn`,
|
|
65
|
+
* which is the other half of this lane.
|
|
66
|
+
*
|
|
67
|
+
* WHY A TIMEOUT IS LOAD-BEARING HERE AND NOT A NICETY: flushes are serialized through `flushChain`,
|
|
68
|
+
* and `finish()` awaits that chain. `fetch` has no default timeout, so ONE POST that connects and
|
|
69
|
+
* then never answers (a stalled proxy, a receiver wedged mid-handler, a half-open socket a dead NAT
|
|
70
|
+
* entry never RSTs) blocks every later delta AND the `final` — for the process's lifetime. The
|
|
71
|
+
* visible symptom is not an error: the reply freezes mid-sentence, no `final` ever lands, and
|
|
72
|
+
* nothing is logged, because the failure never returns. A bounded POST turns that permanent wedge
|
|
73
|
+
* into one logged, skipped delta and a `final` that still arrives.
|
|
74
|
+
*/
|
|
75
|
+
export const POST_TIMEOUT_MS = 20_000;
|
|
76
|
+
|
|
77
|
+
/** One signed POST. Returns false on any non-2xx, timeout, or network error, having logged it — the
|
|
78
|
+
* caller keeps streaming rather than aborting the agent's turn over a transport hiccup. */
|
|
79
|
+
export async function postPara(cb: ParaCallback, payload: object, timeoutMs: number = POST_TIMEOUT_MS): Promise<boolean> {
|
|
64
80
|
const raw = JSON.stringify(payload);
|
|
65
81
|
const ts = Date.now();
|
|
66
82
|
// Per-DELIVERY id, not per-turn: para-li's replay guard dedupes on this, so a shared id across
|
|
67
83
|
// the deltas of one turn would drop every delta after the first. Minted here so the exact same
|
|
68
84
|
// value goes into the header AND the signature — they must not be able to diverge.
|
|
69
85
|
const delivery = randomUUID();
|
|
86
|
+
// Abort on a timer rather than `AbortSignal.timeout`: the same shape para-li uses, and the timer
|
|
87
|
+
// is cleared in `finally` so a fast POST leaves nothing pending on the event loop.
|
|
88
|
+
const ac = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
70
90
|
try {
|
|
71
91
|
const res = await fetch(cb.url, {
|
|
92
|
+
signal: ac.signal,
|
|
72
93
|
method: 'POST',
|
|
73
94
|
headers: {
|
|
74
95
|
'Content-Type': 'application/json',
|
|
@@ -85,8 +106,11 @@ export async function postPara(cb: ParaCallback, payload: object): Promise<boole
|
|
|
85
106
|
}
|
|
86
107
|
return true;
|
|
87
108
|
} catch (err) {
|
|
88
|
-
|
|
109
|
+
const e = err as Error;
|
|
110
|
+
console.warn('[para-streamer] delivery failed:', e.name === 'AbortError' ? `no response within ${timeoutMs}ms` : e.message);
|
|
89
111
|
return false;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
90
114
|
}
|
|
91
115
|
}
|
|
92
116
|
|
|
@@ -101,11 +125,13 @@ export class ParaStreamer {
|
|
|
101
125
|
private readonly flushInterval: number;
|
|
102
126
|
private readonly flushThreshold: number;
|
|
103
127
|
private readonly toolMarkers: boolean;
|
|
128
|
+
private readonly postTimeout: number;
|
|
104
129
|
|
|
105
130
|
constructor(private readonly opts: ParaStreamerOptions) {
|
|
106
131
|
this.flushInterval = opts.flushInterval ?? 300;
|
|
107
132
|
this.flushThreshold = opts.flushThreshold ?? 30;
|
|
108
133
|
this.toolMarkers = opts.toolMarkers ?? true;
|
|
134
|
+
this.postTimeout = opts.postTimeout ?? POST_TIMEOUT_MS;
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
feed(ev: { type: string; text?: string; tool?: string }): void {
|
|
@@ -133,7 +159,7 @@ export class ParaStreamer {
|
|
|
133
159
|
await this.flushChain;
|
|
134
160
|
if (this.aborted || !this.opts.msgId) return this.fullText;
|
|
135
161
|
const text = this.fullText.trim() || '(no output)';
|
|
136
|
-
await postPara(this.opts.callback, { type: 'final', convId: this.opts.convId, msgId: this.opts.msgId, text });
|
|
162
|
+
await postPara(this.opts.callback, { type: 'final', convId: this.opts.convId, msgId: this.opts.msgId, text }, this.postTimeout);
|
|
137
163
|
return text;
|
|
138
164
|
}
|
|
139
165
|
|
|
@@ -143,7 +169,7 @@ export class ParaStreamer {
|
|
|
143
169
|
this.clearTimer();
|
|
144
170
|
await this.flushChain;
|
|
145
171
|
if (!this.opts.msgId) return;
|
|
146
|
-
await postPara(this.opts.callback, { type: 'error', convId: this.opts.convId, msgId: this.opts.msgId, message });
|
|
172
|
+
await postPara(this.opts.callback, { type: 'error', convId: this.opts.convId, msgId: this.opts.msgId, message }, this.postTimeout);
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
private enqueueFlush(): void {
|
|
@@ -154,7 +180,7 @@ export class ParaStreamer {
|
|
|
154
180
|
// Serialized: a later, longer snapshot must never be overtaken by an earlier one, or the row
|
|
155
181
|
// visibly rewinds mid-stream.
|
|
156
182
|
this.flushChain = this.flushChain
|
|
157
|
-
.then(async () => { await postPara(this.opts.callback, { type: 'delta', convId: this.opts.convId, msgId: this.opts.msgId, text: snapshot }); })
|
|
183
|
+
.then(async () => { await postPara(this.opts.callback, { type: 'delta', convId: this.opts.convId, msgId: this.opts.msgId, text: snapshot }, this.postTimeout); })
|
|
158
184
|
.catch((err) => console.warn('[para-streamer] flush error:', (err as Error).message));
|
|
159
185
|
}
|
|
160
186
|
|