getpatter 0.6.3 → 0.6.5

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/cli.js CHANGED
@@ -185,14 +185,49 @@ var MetricsStore = class extends import_events.EventEmitter {
185
185
  } else {
186
186
  for (let i = this.calls.length - 1; i >= 0; i--) {
187
187
  if (this.calls[i].call_id === callId) {
188
- this.calls[i].status = status;
189
- Object.assign(this.calls[i], extra);
188
+ this.calls[i] = { ...this.calls[i], status, ...extra };
190
189
  break;
191
190
  }
192
191
  }
193
192
  }
194
193
  this.publish("call_status", { call_id: callId, status, ...extra });
195
194
  }
195
+ /**
196
+ * Record a single transcript line (user/assistant) as it becomes known.
197
+ *
198
+ * FIX-5 (issue #154): the live forward path for the dashboard transcript.
199
+ * The Realtime stream handler calls this the moment each line is known — the
200
+ * user line right after the hallucination filter accepts it, the assistant
201
+ * line when its turn flushes — keyed by the monotonic ``turnIndex`` reserved
202
+ * at turn-open (``reserveTurnIndex``). Each line is appended to the active
203
+ * call's ``transcript`` array and broadcast over SSE as a ``transcript_line``
204
+ * event so the dashboard can render lines as they arrive and re-sort by
205
+ * ``(turnIndex, user<assistant)`` — making a late-arriving user line land
206
+ * ABOVE its agent line. ``recordTurn`` de-dups against the lines pushed here
207
+ * by ``(turnIndex, role)`` so the metrics path never double-pushes the same
208
+ * text. Parity with Python ``record_transcript_line``.
209
+ */
210
+ recordTranscriptLine(data) {
211
+ const callId = data.call_id || "";
212
+ const { role, text, turnIndex } = data;
213
+ if (!callId || role !== "user" && role !== "assistant" || !text) return;
214
+ const active = this.activeCalls.get(callId);
215
+ if (active) {
216
+ if (!active.transcript) active.transcript = [];
217
+ active.transcript.push({
218
+ role,
219
+ text,
220
+ timestamp: Date.now() / 1e3,
221
+ turnIndex
222
+ });
223
+ }
224
+ this.publish("transcript_line", {
225
+ call_id: callId,
226
+ turnIndex,
227
+ role,
228
+ text
229
+ });
230
+ }
196
231
  /** Append a single conversation turn to an active call and broadcast it via SSE. */
197
232
  recordTurn(data) {
198
233
  const callId = data.call_id || "";
@@ -207,14 +242,19 @@ var MetricsStore = class extends import_events.EventEmitter {
207
242
  const userText = typeof turnRecord.user_text === "string" ? turnRecord.user_text : "";
208
243
  const agentText = typeof turnRecord.agent_text === "string" ? turnRecord.agent_text : "";
209
244
  const ts = typeof turnRecord.timestamp === "number" ? turnRecord.timestamp : Date.now() / 1e3;
210
- if (userText.length > 0) {
211
- active.transcript.push({ role: "user", text: userText, timestamp: ts });
245
+ const turnIndex = typeof turnRecord.turn_index === "number" ? turnRecord.turn_index : void 0;
246
+ const alreadyLive = (role) => turnIndex !== void 0 && (active.transcript ?? []).some(
247
+ (e) => e.turnIndex === turnIndex && e.role === role
248
+ );
249
+ if (userText.length > 0 && !alreadyLive("user")) {
250
+ active.transcript.push({ role: "user", text: userText, timestamp: ts, turnIndex });
212
251
  }
213
- if (agentText.length > 0 && agentText !== "[interrupted]") {
252
+ if (agentText.length > 0 && agentText !== "[interrupted]" && !alreadyLive("assistant")) {
214
253
  active.transcript.push({
215
254
  role: "assistant",
216
255
  text: agentText,
217
- timestamp: ts
256
+ timestamp: ts,
257
+ turnIndex
218
258
  });
219
259
  }
220
260
  }
@@ -287,7 +327,7 @@ var MetricsStore = class extends import_events.EventEmitter {
287
327
  getCall(callId) {
288
328
  if (this.deletedCallIds.has(callId)) return null;
289
329
  for (let i = this.calls.length - 1; i >= 0; i--) {
290
- if (this.calls[i].call_id === callId) return this.calls[i];
330
+ if (this.calls[i].call_id === callId) return { ...this.calls[i] };
291
331
  }
292
332
  return null;
293
333
  }
@@ -329,7 +369,9 @@ var MetricsStore = class extends import_events.EventEmitter {
329
369
  }
330
370
  if (accepted.length === 0) return [];
331
371
  accepted.sort();
332
- this.persistDeletedIds();
372
+ this.persistDeletedIds().catch(
373
+ (err) => getLogger().debug(`MetricsStore.deleteCalls: persistDeletedIds failed: ${String(err)}`)
374
+ );
333
375
  this.publish("calls_deleted", { call_ids: accepted });
334
376
  return accepted;
335
377
  }
@@ -341,19 +383,19 @@ var MetricsStore = class extends import_events.EventEmitter {
341
383
  getDeletedCallIds() {
342
384
  return Array.from(this.deletedCallIds).sort();
343
385
  }
344
- /** Atomically persist the deleted-ids set to disk. Best-effort. */
345
- persistDeletedIds() {
386
+ /** Atomically persist the deleted-ids set to disk. Best-effort async. */
387
+ async persistDeletedIds() {
346
388
  if (this.deletedIdsPath === null) return;
347
389
  try {
348
390
  const dir = path2.dirname(this.deletedIdsPath);
349
- fs2.mkdirSync(dir, { recursive: true });
391
+ await fs2.promises.mkdir(dir, { recursive: true });
350
392
  const tmp = this.deletedIdsPath + ".tmp";
351
393
  const payload = {
352
394
  version: 1,
353
395
  deleted_call_ids: Array.from(this.deletedCallIds).sort()
354
396
  };
355
- fs2.writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf8");
356
- fs2.renameSync(tmp, this.deletedIdsPath);
397
+ await fs2.promises.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
398
+ await fs2.promises.rename(tmp, this.deletedIdsPath);
357
399
  } catch (err) {
358
400
  getLogger().debug(
359
401
  `MetricsStore.persistDeletedIds: ${String(err)}`
@@ -362,7 +404,8 @@ var MetricsStore = class extends import_events.EventEmitter {
362
404
  }
363
405
  /** Look up an active call by id (returns undefined if not active or unknown). */
364
406
  getActive(callId) {
365
- return this.activeCalls.get(callId);
407
+ const rec = this.activeCalls.get(callId);
408
+ return rec !== void 0 ? { ...rec } : void 0;
366
409
  }
367
410
  /** Return all currently active (not yet ended) calls. */
368
411
  getActiveCalls() {
@@ -607,8 +650,8 @@ function loadTranscriptJsonl(filePath) {
607
650
  } catch {
608
651
  continue;
609
652
  }
610
- const tsIso = typeof row.ts === "string" ? Date.parse(row.ts) : NaN;
611
- const tsNumeric = typeof row.timestamp === "number" ? row.timestamp * 1e3 : NaN;
653
+ const tsIso = typeof row.ts === "string" ? Date.parse(row.ts) / 1e3 : NaN;
654
+ const tsNumeric = typeof row.timestamp === "number" ? row.timestamp : NaN;
612
655
  const timestamp = Number.isFinite(tsIso) ? tsIso : Number.isFinite(tsNumeric) ? tsNumeric : 0;
613
656
  const userText = typeof row.user_text === "string" ? row.user_text : "";
614
657
  const agentText = typeof row.agent_text === "string" ? row.agent_text : "";
@@ -759,8 +802,8 @@ function mountDashboard(app, store, token = "") {
759
802
  res.type("text/html").send(DASHBOARD_HTML);
760
803
  });
761
804
  app.get("/api/dashboard/calls", auth, (req, res) => {
762
- const limit = Math.min(parseInt(req.query.limit || "50", 10) || 50, 1e3);
763
- const offset = parseInt(req.query.offset || "0", 10) || 0;
805
+ const limit = Math.min(Math.max(0, parseInt(req.query.limit || "50", 10) || 50), 1e3);
806
+ const offset = Math.max(0, parseInt(req.query.offset || "0", 10) || 0);
764
807
  res.json(store.getCalls(limit, offset));
765
808
  });
766
809
  app.get("/api/dashboard/calls/:callId", auth, (req, res) => {
@@ -850,8 +893,8 @@ data: ${data}
850
893
  function mountApi(app, store, token = "") {
851
894
  const auth = makeAuthMiddleware(token);
852
895
  app.get("/api/v1/calls", auth, (req, res) => {
853
- const limit = Math.min(parseInt(req.query.limit || "50", 10) || 50, 1e3);
854
- const offset = parseInt(req.query.offset || "0", 10) || 0;
896
+ const limit = Math.min(Math.max(0, parseInt(req.query.limit || "50", 10) || 50), 1e3);
897
+ const offset = Math.max(0, parseInt(req.query.offset || "0", 10) || 0);
855
898
  const calls = store.getCalls(limit, offset);
856
899
  res.json({
857
900
  data: calls,