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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.68",
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
- /** One signed POST. Returns false on any non-2xx or network error, having logged it — the caller
62
- * keeps streaming rather than aborting the agent's turn over a transport hiccup. */
63
- export async function postPara(cb: ParaCallback, payload: object): Promise<boolean> {
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
- console.warn('[para-streamer] delivery failed:', (err as Error).message);
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