@veluai/velu 0.2.3 → 0.2.4

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": "@veluai/velu",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -31,20 +31,23 @@ function siteHost(explicit) {
31
31
  return typeof window !== 'undefined' ? window.location.host : '';
32
32
  }
33
33
 
34
- // Parse one raw SSE record ("event: x\ndata: {…}") into { event, data }.
34
+ // Parse one raw SSE record ("id: N\nevent: x\ndata: {…}") into { event, data, seq }.
35
35
  function parseSseRecord(raw) {
36
36
  let event = 'message';
37
+ let seq = null;
37
38
  const dataLines = [];
38
39
  for (const line of raw.split('\n')) {
39
40
  if (line.startsWith('event:')) event = line.slice(6).trim();
40
- else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
41
+ else if (line.startsWith('id:')) {
42
+ const n = Number(line.slice(3).trim());
43
+ if (Number.isFinite(n)) seq = n;
44
+ } else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
41
45
  }
42
- if (!dataLines.length) return { event, data: null };
43
- try {
44
- return { event, data: JSON.parse(dataLines.join('\n')) };
45
- } catch {
46
- return { event, data: null };
46
+ let data = null;
47
+ if (dataLines.length) {
48
+ try { data = JSON.parse(dataLines.join('\n')); } catch { data = null; }
47
49
  }
50
+ return { event, data, seq };
48
51
  }
49
52
 
50
53
  // Backend citation → the Chatbot's Source shape.
@@ -63,6 +66,11 @@ export function createDocsAssistant({ apiBase, host } = {}) {
63
66
  if (!base) return null;
64
67
  const root = `${base}/api/v1/public/ai-assistant`;
65
68
  let bootstrapped = false;
69
+ // Highest event seq seen per conversation. The events endpoint replays history
70
+ // after `after_seq`; without this a reused (multi-turn) conversation replays
71
+ // from 0 and the first (old) assistant.completed is returned for every
72
+ // follow-up turn — i.e. the answer never updates after the first message.
73
+ const lastSeqByConv = new Map();
66
74
 
67
75
  const headers = (extra) => ({
68
76
  'X-Velu-Site-Host': siteHost(host),
@@ -98,8 +106,9 @@ export function createDocsAssistant({ apiBase, host } = {}) {
98
106
 
99
107
  // Manual fetch-stream SSE parse (not EventSource): supports credentials,
100
108
  // the X-Velu-Site-Host header, and AbortController cancellation.
109
+ const afterSeq = lastSeqByConv.get(convId) ?? 0;
101
110
  const evRes = await fetch(
102
- `${root}/conversations/${convId}/events?after_seq=0&token=${encodeURIComponent(token)}`,
111
+ `${root}/conversations/${convId}/events?after_seq=${afterSeq}&token=${encodeURIComponent(token)}`,
103
112
  { credentials: 'include', headers: headers({ Accept: 'text/event-stream' }), signal },
104
113
  );
105
114
  if (!evRes.ok || !evRes.body) throw new Error(`ask-ai: stream ${evRes.status}`);
@@ -114,8 +123,11 @@ export function createDocsAssistant({ apiBase, host } = {}) {
114
123
  buf += decoder.decode(value, { stream: true });
115
124
  let sep;
116
125
  while ((sep = buf.indexOf('\n\n')) !== -1) {
117
- const { event, data } = parseSseRecord(buf.slice(0, sep));
126
+ const { event, data, seq } = parseSseRecord(buf.slice(0, sep));
118
127
  buf = buf.slice(sep + 2);
128
+ if (seq != null) {
129
+ lastSeqByConv.set(convId, Math.max(lastSeqByConv.get(convId) ?? 0, seq));
130
+ }
119
131
  if (event === 'assistant.completed') {
120
132
  const m = data?.message || {};
121
133
  yield {