lavish-axi 0.1.69 → 0.1.70

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/README.md CHANGED
@@ -197,7 +197,7 @@ pnpm link
197
197
  - **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
198
198
  For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
199
199
  Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
200
- Everything you send is part of the conversation: a note on an element, a text selection, a table cell, a diagram node, a whiteboard, a layout-issue batch, or a composer message each becomes a bubble on your side of the Conversation panel, with an anchor line naming what it was attached to (the element tag, text selection, table cell, diagram node, whiteboard, or layout issues), a quoted excerpt, and, when available, the selector on hover. A note you have queued but not sent sits at the end of the conversation as a dashed bubble labelled **Queued** with a remove control; while its batch is in flight it reads **Sending**, and once the server accepts it, it settles in place as a sent bubble that stays in the history, in every tab of the session. Agent replies render a small Markdown subset - paragraphs, line breaks, headings as bold lead lines, bullet and numbered lists, fenced and inline code, bold, italic, and http(s) links - while tables, images, and raw HTML stay as the text the agent wrote; your own messages keep their line breaks and are never parsed.
200
+ Everything you send is part of the conversation: a note on an element, a text selection, a table cell, a diagram node, a whiteboard, a layout-issue batch, or a composer message each becomes a bubble on your side of the Conversation panel, with an anchor line naming what it was attached to (the element tag, text selection, table cell, diagram node, whiteboard, or layout issues), a quoted excerpt, and, when available, the selector on hover. A note you have queued but not sent sits at the end of the conversation as a dashed bubble labelled **Queued** with a remove control; while its batch is in flight it reads **Sending**, and once the server accepts it, it settles in place as a sent bubble in every tab of the session. The panel keeps the newest stored transcript up to 5 MiB; older bubbles drop from view, but an accepted note still settles and is not sent twice. Agent replies render a small Markdown subset - paragraphs, line breaks, headings as bold lead lines, bullet and numbered lists, fenced and inline code, bold, italic, and http(s) links - while tables, images, and raw HTML stay as the text the agent wrote; your own messages keep their line breaks and are never parsed.
201
201
  On wider screens, the conversation and the queued notes share one scrollable Conversation panel above a sticky composer, so long feedback queues do not push the text box or send controls off screen.
202
202
  The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session), while the composer exposes **Send & End** beside **Send to Agent** to submit queued prompts and user-ended attribution together. Once **Send & End** starts, Lavish pauses new feedback, waits up to 5 seconds for feedback preparation already underway, keeps any open annotation draft intact until delivery succeeds, then holds that exact terminal batch. A completed reservation survives a page reload, including when its prompts were already delivered and only the pending end remains; incomplete preparation is not restored. If preparation times out, Lavish keeps the review open and its existing queue editable with a visible error. A transient delivery failure permits only retrying the same terminal batch through **Send & End** until it succeeds. If a snapshot makes the request too large (HTTP 413), Lavish retries that exact batch once without the optional snapshot. An actionable attachment rejection (HTTP 400), recoverable layout-selection conflict (HTTP 409), or unrecoverable 413 instead cancels the terminal reservation without ending the session, keeps the prompts queued, and restores editing, removal, **Send to Agent**, **Send & End**, and **End session** so the reviewer can revise or remove stale feedback before submitting again.
203
203
  Composer feedback is visibly queued before the text box clears and stays queued until acknowledged. Send waits up to 5 seconds for the artifact's DOM snapshot, then delivers without that optional context if the frame no longer answers; a late snapshot cannot submit the batch twice. A failed or still-unacknowledged POST keeps the queue and shows persistent recovery guidance unless a transcript sync proves the server already accepted that feedback.
@@ -23,6 +23,8 @@ const promptIdentityField = "prompt_id";
23
23
  const PROMPT_IDENTITY_MAX = 128;
24
24
  const PROMPT_IDENTITY_RE = /^[A-Za-z0-9_-]+$/;
25
25
  const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
26
+ const initialChatAckIds = Array.isArray(sessionData.initialChatAckIds) ? sessionData.initialChatAckIds : [];
27
+ const initialChatRevision = parseChatRevision(sessionData.initialChatRevision) || 0;
26
28
  const MODE_TOGGLE_HOTKEY_KEY = String(sessionData.modeToggleHotkeyKey || "").toLowerCase();
27
29
  const attachmentMaxBytes = Number(sessionData.attachmentMaxBytes) || 0;
28
30
  const attachmentMaxCount = Number(sessionData.attachmentMaxCount) || 4;
@@ -222,6 +224,7 @@ let nextSnapshotRequestId = 0;
222
224
  let nextSendOperationOrder = 0;
223
225
  let workingBubble = null;
224
226
  let displayedChat = initialChat.slice();
227
+ let chatRevision = initialChatRevision;
225
228
  // Settlement is by per-submission identity, not displayed content: two tabs can queue notes
226
229
  // whose chat projection is identical (same selected text under one container, different range
227
230
  // boundaries) without settling each other, and a reload after a lost POST response still
@@ -393,6 +396,15 @@ function promptIdentity(value) {
393
396
  return isPromptIdentity(value?.[promptIdentityField]) ? value[promptIdentityField] : "";
394
397
  }
395
398
 
399
+ const chatAckIds = new Set();
400
+ function rememberChatAckIds(ids) {
401
+ if (!Array.isArray(ids)) return;
402
+ for (const value of ids) {
403
+ if (isPromptIdentity(value)) chatAckIds.add(value);
404
+ }
405
+ }
406
+ rememberChatAckIds(initialChatAckIds);
407
+
396
408
  function createPromptIdentity() {
397
409
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
398
410
  return "p" + Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
@@ -849,13 +861,26 @@ function chatEntriesMatch(left, right) {
849
861
  return !leftAt || !rightAt || leftAt === rightAt;
850
862
  }
851
863
 
864
+ function parseChatRevision(value) {
865
+ const revision = Number(value);
866
+ return Number.isSafeInteger(revision) && revision >= 0 ? revision : null;
867
+ }
868
+
852
869
  function chatContainsEntries(candidate, entries) {
853
- if (candidate.length < entries.length) return false;
854
- let matched = 0;
855
- for (const entry of candidate) {
856
- if (matched < entries.length && chatEntriesMatch(entry, entries[matched])) matched += 1;
870
+ if (!Array.isArray(candidate) || !Array.isArray(entries)) return false;
871
+ if (entries.length === 0) return true;
872
+ // A size-bound transcript may drop a prefix of what this tab already showed. The remaining
873
+ // displayed suffix must still appear in order; a stale sync that is missing a newer tail
874
+ // (including an empty wipe) is rejected.
875
+ for (let start = 0; start < entries.length; start += 1) {
876
+ const suffix = entries.slice(start);
877
+ let matched = 0;
878
+ for (const entry of candidate) {
879
+ if (matched < suffix.length && chatEntriesMatch(entry, suffix[matched])) matched += 1;
880
+ }
881
+ if (matched === suffix.length) return true;
857
882
  }
858
- return matched === entries.length;
883
+ return false;
859
884
  }
860
885
 
861
886
  function chatStartsWith(candidate, prefix) {
@@ -868,10 +893,18 @@ function chatStartsWith(candidate, prefix) {
868
893
  }
869
894
 
870
895
  function mergeAcceptedChat(accepted, chatAtRequest) {
871
- if (!chatStartsWith(accepted, chatAtRequest) || !chatStartsWith(displayedChat, chatAtRequest)) return null;
896
+ if (!Array.isArray(accepted) || !Array.isArray(chatAtRequest)) return null;
897
+ if (!chatStartsWith(displayedChat, chatAtRequest)) return null;
898
+ let requestOffset = chatAtRequest.length;
899
+ for (let i = 0; i <= chatAtRequest.length; i += 1) {
900
+ if (chatStartsWith(accepted, chatAtRequest.slice(i))) {
901
+ requestOffset = i;
902
+ break;
903
+ }
904
+ }
872
905
  const displayedTail = displayedChat.slice(chatAtRequest.length);
873
906
  const unmatchedDisplayed = [];
874
- let acceptedIndex = chatAtRequest.length;
907
+ let acceptedIndex = chatAtRequest.length - requestOffset;
875
908
  for (const displayedEntry of displayedTail) {
876
909
  while (acceptedIndex < accepted.length && !chatEntriesMatch(accepted[acceptedIndex], displayedEntry)) {
877
910
  acceptedIndex += 1;
@@ -892,7 +925,9 @@ function queuedPromptMatchesEntry(prompt, entry) {
892
925
 
893
926
  function promptAcknowledgedInChat(prompt, chat) {
894
927
  const id = promptIdentity(prompt);
895
- if (!id || !Array.isArray(chat)) return false;
928
+ if (!id) return false;
929
+ if (chatAckIds.has(id)) return true;
930
+ if (!Array.isArray(chat)) return false;
896
931
  return chat.some((entry) => queuedPromptMatchesEntry(prompt, entry));
897
932
  }
898
933
 
@@ -916,10 +951,15 @@ function settleQueuedFromTranscript(chat, shouldRender = true) {
916
951
  return true;
917
952
  }
918
953
 
919
- function syncChat(chat) {
954
+ function syncChat(chat, revision) {
920
955
  const nextChat = Array.isArray(chat) ? chat : [];
921
956
  settleQueuedFromTranscript(nextChat);
922
- if (!chatContainsEntries(nextChat, displayedChat)) return false;
957
+ const nextRevision = parseChatRevision(revision);
958
+ if (nextRevision !== null && nextRevision < chatRevision) return false;
959
+ if (nextRevision === null || nextRevision === chatRevision) {
960
+ if (!chatContainsEntries(nextChat, displayedChat)) return false;
961
+ }
962
+ if (nextRevision !== null) chatRevision = nextRevision;
923
963
  displayedChat = nextChat.slice();
924
964
  for (const el of [...chatLog.querySelectorAll(".bubble.user,.bubble.agent:not(.agent-working)")]) {
925
965
  el.remove();
@@ -1850,17 +1890,23 @@ async function submitQueuedOnce(submission, preserveFailureState = false) {
1850
1890
  throw new Error("failed to submit queued prompts");
1851
1891
  }
1852
1892
  const accepted = typeof response.json === "function" ? await response.json().catch(() => null) : null;
1853
- const reconciledChat = Array.isArray(accepted?.chat)
1854
- ? mergeAcceptedChat(accepted.chat, submission.chatAtRequest)
1893
+ rememberChatAckIds(accepted?.ack_ids);
1894
+ const acceptedChat = Array.isArray(accepted?.chat) ? accepted.chat : null;
1895
+ if (acceptedChat) settleQueuedFromTranscript(acceptedChat);
1896
+ const acceptedRevision = parseChatRevision(accepted?.chat_revision);
1897
+ const reconciledChat = acceptedChat
1898
+ ? acceptedRevision !== null && acceptedRevision > chatRevision
1899
+ ? acceptedChat
1900
+ : mergeAcceptedChat(acceptedChat, submission.chatAtRequest)
1855
1901
  : null;
1856
- if (!Array.isArray(accepted?.chat) || reconciledChat) {
1902
+ if (!acceptedChat || reconciledChat) {
1857
1903
  for (const prompt of prompts) {
1858
1904
  deliveredPrompts.add(prompt);
1859
1905
  const index = queued.indexOf(prompt);
1860
1906
  if (index !== -1) queued.splice(index, 1);
1861
1907
  }
1862
1908
  persistQueuedPrompts();
1863
- if (reconciledChat) syncChat(reconciledChat);
1909
+ if (reconciledChat) syncChat(reconciledChat, acceptedRevision);
1864
1910
  }
1865
1911
  render();
1866
1912
  settleAcknowledgementGuidance(submission, preserveFailureState);
@@ -3987,7 +4033,10 @@ events.set("agent-reply", (data) => {
3987
4033
  if (addChat(entry)) displayedChat.push(entry);
3988
4034
  noteAgentReply(entry.text);
3989
4035
  });
3990
- events.set("chat-sync", (data) => syncChat(data.chat || []));
4036
+ events.set("chat-sync", (data) => {
4037
+ rememberChatAckIds(data.ack_ids);
4038
+ syncChat(data.chat || [], data.chat_revision);
4039
+ });
3991
4040
  events.set("agent-presence", (data) => setAgentPresence(data.state));
3992
4041
  events.set("layout-warnings", (data) => setLayoutWarnings(data.warnings || []));
3993
4042
  events.set("ended", () => markSessionEnded());
package/dist/cli.mjs CHANGED
@@ -7436,6 +7436,7 @@ var SELECTOR_MAX = 512;
7436
7436
  var LABEL_MAX = 40;
7437
7437
  var PROMPT_ID_MAX = 128;
7438
7438
  var PROMPT_ID_RE = /^[A-Za-z0-9_-]+$/;
7439
+ var MAX_CHAT_STORED_BYTES = 5242880;
7439
7440
  function normalizePromptId(value) {
7440
7441
  if (typeof value !== "string") return "";
7441
7442
  const id = value.trim();
@@ -7839,6 +7840,52 @@ function serializeChat(chat) {
7839
7840
  }
7840
7841
  return serialized;
7841
7842
  }
7843
+ function boundStoredChat(chat, maxBytes = MAX_CHAT_STORED_BYTES) {
7844
+ const entries = Array.isArray(chat) ? chat.filter((entry) => entry && typeof entry === "object") : [];
7845
+ if (entries.length === 0) return { chat: [], evicted: [] };
7846
+ const limit = Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : MAX_CHAT_STORED_BYTES;
7847
+ let storedBytes = 2;
7848
+ let cut = entries.length;
7849
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
7850
+ const serialized = JSON.stringify(entries[index]);
7851
+ const entryBytes = Buffer.byteLength(serialized === void 0 ? "null" : serialized, "utf8");
7852
+ const nextBytes = storedBytes + entryBytes + (cut < entries.length ? 1 : 0);
7853
+ if (nextBytes > limit) break;
7854
+ storedBytes = nextBytes;
7855
+ cut = index;
7856
+ }
7857
+ return { chat: entries.slice(cut), evicted: entries.slice(0, cut) };
7858
+ }
7859
+ function serializeChatAckIds(ids) {
7860
+ const ackIds = [];
7861
+ const seen = /* @__PURE__ */ new Set();
7862
+ for (const value of Array.isArray(ids) ? ids : []) {
7863
+ const id = normalizePromptId(value);
7864
+ if (!id || seen.has(id)) continue;
7865
+ seen.add(id);
7866
+ ackIds.push(id);
7867
+ }
7868
+ return ackIds;
7869
+ }
7870
+ function collectChatAckIds(evicted, existing = []) {
7871
+ const ackIds = serializeChatAckIds(existing);
7872
+ const seen = new Set(ackIds);
7873
+ for (const entry of Array.isArray(evicted) ? evicted : []) {
7874
+ const id = normalizePromptId(entry?.prompt_id);
7875
+ if (!id || seen.has(id)) continue;
7876
+ seen.add(id);
7877
+ ackIds.push(id);
7878
+ }
7879
+ return ackIds;
7880
+ }
7881
+ function serializeChatSync(session) {
7882
+ const revision = Number(session?.chat_revision);
7883
+ return {
7884
+ chat: serializeChat(session?.chat || []),
7885
+ ack_ids: serializeChatAckIds(session?.chat_ack_ids),
7886
+ chat_revision: Number.isSafeInteger(revision) && revision >= 0 ? revision : 0
7887
+ };
7888
+ }
7842
7889
 
7843
7890
  // src/html-transform.js
7844
7891
  function injectLavishSdk(html, key, artifactRevision, artifactLoadToken = "") {
@@ -8036,6 +8083,10 @@ var SessionStore = class {
8036
8083
  delivered_attachments: Array.isArray(existing.delivered_attachments) ? existing.delivered_attachments : [],
8037
8084
  dom_snapshot: existing.dom_snapshot || "",
8038
8085
  chat: existing.chat || [],
8086
+ chat_revision: normalizeRevision(existing.chat_revision),
8087
+ // Compact prompt_id acks for bubbles evicted by the stored-chat byte bound. Reopening
8088
+ // must keep them: they are the settlement/dedup source once the visible entry is gone.
8089
+ chat_ack_ids: Array.isArray(existing.chat_ack_ids) ? existing.chat_ack_ids : [],
8039
8090
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
8040
8091
  };
8041
8092
  state.sessions[key] = session;
@@ -8067,7 +8118,10 @@ var SessionStore = class {
8067
8118
  let normalized = prompts.map(normalizePrompt);
8068
8119
  if (!restoring) {
8069
8120
  const acknowledgedIds = new Set(
8070
- (session.chat || []).map((entry) => normalizePromptId(entry?.prompt_id)).filter(Boolean)
8121
+ [
8122
+ ...(session.chat || []).map((entry) => normalizePromptId(entry?.prompt_id)),
8123
+ ...(session.chat_ack_ids || []).map((id) => normalizePromptId(id))
8124
+ ].filter(Boolean)
8071
8125
  );
8072
8126
  normalized = normalized.filter(({ prompt }) => {
8073
8127
  const promptId = normalizePromptId(prompt.prompt_id);
@@ -8147,6 +8201,8 @@ var SessionStore = class {
8147
8201
  const storedPrompts = restoring ? acceptedPrompts : acceptedPrompts.map(agentFacingPrompt);
8148
8202
  session.prompts = restoring ? [...storedPrompts, ...existingPrompts] : [...existingPrompts, ...storedPrompts];
8149
8203
  session.chat = [...session.chat || [], ...userMessages];
8204
+ applyTranscriptBound(session);
8205
+ if (userMessages.length > 0) session.chat_revision = normalizeRevision(session.chat_revision) + 1;
8150
8206
  if (restoring) {
8151
8207
  const restoredFailures = Array.isArray(payload.artifact_failures) ? JSON.parse(JSON.stringify(payload.artifact_failures)) : [];
8152
8208
  const existingFailures = Array.isArray(session.artifact_failures) ? session.artifact_failures : [];
@@ -8467,11 +8523,11 @@ var SessionStore = class {
8467
8523
  if (!session) {
8468
8524
  return null;
8469
8525
  }
8470
- session.chat = [
8471
- ...session.chat || [],
8472
- { role: "agent", text: String(text || ""), at: (/* @__PURE__ */ new Date()).toISOString() }
8473
- ];
8474
- session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
8526
+ const at = (/* @__PURE__ */ new Date()).toISOString();
8527
+ session.chat = [...session.chat || [], { role: "agent", text: String(text || ""), at }];
8528
+ applyTranscriptBound(session);
8529
+ session.chat_revision = normalizeRevision(session.chat_revision) + 1;
8530
+ session.updated_at = at;
8475
8531
  await this.writeState(state);
8476
8532
  return session;
8477
8533
  });
@@ -8515,7 +8571,15 @@ var SessionStore = class {
8515
8571
  try {
8516
8572
  const raw = await readFile3(this.file, "utf8");
8517
8573
  const parsed = JSON.parse(raw);
8518
- return { sessions: parsed.sessions || {} };
8574
+ const state = { sessions: parsed.sessions || {} };
8575
+ let changed = false;
8576
+ for (const session of Object.values(state.sessions)) {
8577
+ if (!session || typeof session !== "object" || !applyTranscriptBound(session)) continue;
8578
+ session.chat_revision = normalizeRevision(session.chat_revision) + 1;
8579
+ changed = true;
8580
+ }
8581
+ if (changed) await this.writeState(state);
8582
+ return state;
8519
8583
  } catch (error) {
8520
8584
  if (error && error.code === "ENOENT") {
8521
8585
  return { sessions: {} };
@@ -8535,6 +8599,16 @@ async function canonicalFile(file) {
8535
8599
  function sessionKey(file) {
8536
8600
  return crypto4.createHash("sha256").update(file).digest("hex").slice(0, 16);
8537
8601
  }
8602
+ function applyTranscriptBound(session) {
8603
+ const originalChat = Array.isArray(session.chat) ? session.chat : [];
8604
+ const { chat, evicted } = boundStoredChat(session.chat);
8605
+ const chatChanged = !Array.isArray(session.chat) || chat.length !== originalChat.length || chat.some((entry, index) => entry !== originalChat[index]);
8606
+ session.chat = chat;
8607
+ if (evicted.length === 0) return chatChanged;
8608
+ const existingAckCount = Array.isArray(session.chat_ack_ids) ? session.chat_ack_ids.length : 0;
8609
+ session.chat_ack_ids = collectChatAckIds(evicted, session.chat_ack_ids);
8610
+ return chatChanged || session.chat_ack_ids.length !== existingAckCount;
8611
+ }
8538
8612
  function normalizePrompt(prompt) {
8539
8613
  const normalized = {
8540
8614
  uid: String(prompt.uid || ""),
@@ -9400,7 +9474,7 @@ async function serve({
9400
9474
  }
9401
9475
  events.on("reload", (key) => broadcastLiveEvent("reload", key));
9402
9476
  events.on("agent-reply", (key, entry) => broadcastLiveEvent("agent-reply", key, entry));
9403
- events.on("chat-sync", (key, chat) => broadcastLiveEvent("chat-sync", key, { chat: serializeChat(chat) }));
9477
+ events.on("chat-sync", (key, session) => broadcastLiveEvent("chat-sync", key, serializeChatSync(session)));
9404
9478
  events.on("agent-presence", (key, state) => broadcastLiveEvent("agent-presence", key, { state }));
9405
9479
  events.on("layout-warnings", (key, warnings) => broadcastLiveEvent("layout-warnings", key, { warnings }));
9406
9480
  events.on("ended", (key, endedBy) => broadcastLiveEvent("ended", key, { ended_by: endedBy || null }));
@@ -9447,7 +9521,7 @@ async function serve({
9447
9521
  cleanup();
9448
9522
  return;
9449
9523
  }
9450
- client.sendEvent("chat-sync", { chat: serializeChat(session?.chat || []) });
9524
+ client.sendEvent("chat-sync", serializeChatSync(session));
9451
9525
  client.sendEvent("agent-presence", { state: computePresence(key, activePolls, deliveredFeedback) });
9452
9526
  if (session?.status === "ended") client.sendEvent("ended", { ended_by: session.ended_by || null });
9453
9527
  }
@@ -9475,11 +9549,8 @@ async function serve({
9475
9549
  }
9476
9550
  function finishFeedbackDelivery(key, result) {
9477
9551
  if (result.status !== "feedback") return;
9478
- const chat = result.chat;
9479
- delete result.chat;
9480
9552
  markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
9481
9553
  if (result.session_ended) clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
9482
- if (Array.isArray(chat)) events.emit("chat-sync", key, chat);
9483
9554
  }
9484
9555
  async function restoreClosedFeedback(key, result) {
9485
9556
  if (result.status !== "feedback") return;
@@ -9816,11 +9887,11 @@ async function serve({
9816
9887
  }
9817
9888
  if (shouldEndSession) events.emit("ended", req.params.key, publishedSession.ended_by);
9818
9889
  else if (freshFeedback) events.emit("feedback", req.params.key, publishedSession.ended_by);
9819
- events.emit("chat-sync", req.params.key, publishedSession.chat);
9890
+ events.emit("chat-sync", req.params.key, publishedSession);
9820
9891
  res.json({
9821
9892
  status: "queued",
9822
9893
  pending_prompts: publishedSession.pending_prompts,
9823
- chat: serializeChat(publishedSession.chat)
9894
+ ...serializeChatSync(publishedSession)
9824
9895
  });
9825
9896
  if (shouldEndSession) await shutdownIfNoLiveSessions();
9826
9897
  } catch (error) {
@@ -9924,8 +9995,11 @@ async function serve({
9924
9995
  res.status(404).json({ error: "session not found" });
9925
9996
  return;
9926
9997
  }
9927
- const entry = serializeChat([session.chat?.at(-1)])[0];
9998
+ const entry = serializeChat([
9999
+ session.chat?.at(-1)?.role === "agent" ? session.chat.at(-1) : { role: "agent", text, at: session.updated_at }
10000
+ ])[0];
9928
10001
  events.emit("agent-reply", req.params.key, entry);
10002
+ events.emit("chat-sync", req.params.key, session);
9929
10003
  clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
9930
10004
  res.json({ status: "sent" });
9931
10005
  } catch (error) {
@@ -11099,6 +11173,8 @@ function createChromeHtml(session, {
11099
11173
  initialEnded: session.status === "ended",
11100
11174
  initialEndedBy: session.ended_by || null,
11101
11175
  initialChat: serializeChat(session.chat || []),
11176
+ initialChatAckIds: serializeChatAckIds(session.chat_ack_ids),
11177
+ initialChatRevision: Number.isSafeInteger(session.chat_revision) && session.chat_revision >= 0 ? session.chat_revision : 0,
11102
11178
  // Bootstrapping the inbox from the server is what makes it survive a browser refresh or a
11103
11179
  // reconnect: the chrome never owns warning state, it only renders it.
11104
11180
  initialLayoutWarnings: serializeLayoutWarnings(session.layout_warnings),
@@ -11399,7 +11475,7 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
11399
11475
  ]);
11400
11476
  var POLL_SEND_AND_END_RULE = "`Send & End` ends the session. Its final feedback is still delivered once. After that response, polling stops, and the agent must not reopen the session uninvited.";
11401
11477
  var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
11402
- var VERSION = "0.1.69";
11478
+ var VERSION = "0.1.70";
11403
11479
  function detectInvokingAgent(env = process.env) {
11404
11480
  return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
11405
11481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "packageManager": "pnpm@11.1.1",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "lavish-axi",
4
- "version": "0.1.69",
4
+ "version": "0.1.70",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "author": {
7
7
  "name": "Kun Chen",