lavish-axi 0.1.82 → 0.1.83

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
@@ -211,7 +211,9 @@ pnpm link
211
211
  In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately. Escape closes the card, same as Cancel, but only while it is empty (no text, no attachment); with unsent text or an attachment present, Escape does nothing rather than risk discarding it.
212
212
  While editing a queued note, Enter saves it, Shift+Enter inserts a newline, and Escape discards the edit.
213
213
  Cmd+I or Ctrl+I toggles between annotate and explore mode from either the browser chrome or the artifact iframe, including while focus is in a textarea or control.
214
+ In annotate mode a plain click on a link annotates it; Ctrl+click (Cmd+click on macOS) follows the link instead, opening it in a new tab as the browser normally would, without leaving annotate mode.
214
215
  - **Agent presence** - The browser shows when no agent is listening, keeps queued feedback for the next successful `lavish-axi poll` send even across reloads, and keeps human feedback actions available while the agent is working because the server queues them for the next poll. An agent reply concludes delivered work and returns presence to waiting (see the CLI Reference for reply and poll commands). A supervisor-owned process listener is shown as an external listener rather than an idle captain turn.
216
+ - **Delivery receipts** - Every note you have sent carries three emoji under it that light up in turn: 👀 **Seen** when a `lavish-axi poll` takes the note, 🛠️ **Working** when the artifact file changes after that, and ✅ **Done** when the agent replies. A step that has not happened yet is the same emoji greyed out. All three come from what the server observes; the agent does nothing extra. Until 👀 lights, the row says why: no agent is listening, or the agent is busy and the note is delivered on its next poll. A note answered without touching the artifact goes Seen to Done with Working still grey. A reply marks every note the agent has seen as Done, not notes still waiting for a poll. Each emoji names its step and time on hover and to screen readers. Notes sent before receipts existed show no receipt row.
215
217
  In a Herdr-managed pane, set `LAVISH_AXI_HERDR_CHIME=1` to show a Herdr request notification and play its chime once an indefinite poll enters the waiting state. Immediate queued feedback and timed debug polls do not trigger a notification. The integration is off unless both `LAVISH_AXI_HERDR_CHIME` and Herdr's `HERDR_ENV` session marker equal `1`; leaving either unset or using any other value opts out. Notification delivery runs without blocking feedback: if Herdr is unavailable, rejects the notification, stalls, or fails, the poll continues unchanged.
216
218
  Closing the last review tab while a poll is active starts a 10-second reconnect grace period. A reload or quick reconnect keeps an active poll waiting; if every review stays disconnected, the poll returns `browser_disconnected` without ending the resumable session, and the agent asks whether to reopen or end it instead of acting uninvited.
217
219
  The no-timeout poll always writes an immediate stderr banner so it is visibly not hung; it adds the periodic stderr wait ticks only in an interactive terminal, so when stderr is piped (as under agent harnesses) the captured output carries no tick noise. Stdout always stays reserved for the final response; if the poll is interrupted or times out before feedback arrives, re-run it because feedback remains queued until delivery. Poll delivery consumes the response, so read the complete response before truncating or filtering it.
@@ -296,7 +298,7 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
296
298
  | `lavish-axi share` | `--token <t>` | Attach an optional bearer token (`LAVISH_AXI_HTML_APP_TOKEN`) when creating a page; never required, and rejected on a republish or `--unpublish`, where the `update_key` is the credential. |
297
299
  | `lavish-axi poll` | `--owner <label>` | Identify the process listening for this session; the non-empty label appears in the home session listing. `none` is reserved for sessions without a listener. A second concurrent poll is refused instead of silently returning `waiting`. |
298
300
  | `lavish-axi poll` | `--takeover` | Explicitly displace the current poll listener. The displaced poll exits with typed `LISTENER_REPLACED`; without this flag, the new poll fails with typed `LISTENER_ACTIVE`. |
299
- | `lavish-axi poll` | `--agent-reply "..."` | Show a concise agent reply in the existing browser chat, conclude delivered work, and return presence to waiting before polling again. Keep replies concise by default. |
301
+ | `lavish-axi poll` | `--agent-reply "..."` | Show a concise agent reply in the existing browser chat, conclude delivered work (every note the agent has seen is marked Done), and return presence to waiting before polling again. Keep replies concise by default. |
300
302
  | `lavish-axi poll` | `--agent-reply-file <path>` | When a longer reply is genuinely necessary, read Markdown from a file (`-` for stdin) so newlines survive quoting. Use blank-line paragraphs, `- ` / `1. ` lists, and `## ` headings so the Conversation panel renders it scannably. The Markdown subset is under Feedback controls. Cannot be combined with `--agent-reply`. |
301
303
  | `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
302
304
  | `lavish-axi reply` | `--agent-reply "..."` | Concise reply text. Required unless `--agent-reply-file` is set. The command exits 0 only after the server accepts it. Empty text is refused. Prefer this over `poll --agent-reply` when you are not about to long-poll. |
@@ -912,10 +912,75 @@ function chatBubbleHtml(entry) {
912
912
  "<small>You</small>" +
913
913
  anchorHtml(entry.anchor) +
914
914
  userBubbleTextHtml(entry, entry.text) +
915
- bubbleAttachmentsHtml(entry)
915
+ bubbleAttachmentsHtml(entry) +
916
+ receiptHtml(entry)
916
917
  );
917
918
  }
918
919
 
920
+ // Delivery receipt (docs/delivery-acks.md): the per-note view of what the server observed. Seen,
921
+ // Working, and Done are stamps on the entry, so the row is derived from it on every render and
922
+ // never tracked here. Before Seen, the row says why, from the presence the live stream reports.
923
+ // The three steps are the emoji of the Telegram ack protocol the receipt mirrors (seen, working,
924
+ // done); a step that has not happened is the same glyph dimmed, and every step carries its label
925
+ // and time as `title` and `aria-label`, so hover and screen readers both say the word.
926
+ const RECEIPT_EMOJI = { Seen: "\u{1F440}", Working: "\u{1F6E0}️", Done: "✅" };
927
+
928
+ function receiptTime(at) {
929
+ const date = new Date(String(at));
930
+ return Number.isNaN(date.getTime())
931
+ ? String(at)
932
+ : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
933
+ }
934
+
935
+ function receiptStepHtml(label, at) {
936
+ const ticked = Boolean(at);
937
+ const text = ticked ? label + " " + receiptTime(at) : label + ": not yet";
938
+ return (
939
+ '<span class="receipt-step' +
940
+ (ticked ? " is-done" : "") +
941
+ '" role="img" aria-label="' +
942
+ escapeHtml(text) +
943
+ '" title="' +
944
+ escapeHtml(text) +
945
+ '">' +
946
+ RECEIPT_EMOJI[label] +
947
+ "</span>"
948
+ );
949
+ }
950
+
951
+ // Every branch must be true where it is shown: `waiting` is no poll at all, `listening` is a poll
952
+ // that will take the note within milliseconds, and `working` or an external listener is an agent
953
+ // whose next poll will take it.
954
+ function receiptNoteText() {
955
+ if (agentPresence === "waiting") return "No agent is listening";
956
+ if (agentPresence === "listening") return "Delivering";
957
+ return "Agent is busy; delivered on its next poll";
958
+ }
959
+
960
+ // A transcript entry from before receipts existed (`receipt: "none"`, marked by the server once at
961
+ // load) gets no row: nothing was ever observed about it, and an empty row would read as "never seen".
962
+ function hasReceipt(entry) {
963
+ return Boolean(entry) && entry.receipt !== "none";
964
+ }
965
+
966
+ function receiptHtml(entry) {
967
+ if (!hasReceipt(entry)) return "";
968
+ return (
969
+ '<div class="receipt">' +
970
+ receiptStepHtml("Seen", entry.delivered_at) +
971
+ receiptStepHtml("Working", entry.working_at) +
972
+ receiptStepHtml("Done", entry.done_at) +
973
+ (entry.delivered_at ? "" : '<span class="receipt-note">' + escapeHtml(receiptNoteText()) + "</span>") +
974
+ "</div>"
975
+ );
976
+ }
977
+
978
+ // Only the pre-Seen line names the live presence, so a presence change rewrites that line alone.
979
+ // Rebuilding the bubble would also rebuild its thumbnails and undo an "Image expired" placeholder.
980
+ function refreshUnseenReceipts() {
981
+ for (const note of chatLog.querySelectorAll(".receipt-note")) note.textContent = receiptNoteText();
982
+ }
983
+
919
984
  function addChat(entry, shouldScroll = true) {
920
985
  if (!entry || typeof entry !== "object") return;
921
986
  const role = entry.role === "agent" ? "agent" : "user";
@@ -1067,6 +1132,7 @@ function setAgentPresence(state) {
1067
1132
  agentPresence = state === "listening" || state === "external" || state === "working" ? state : "waiting";
1068
1133
  updateSendState();
1069
1134
  renderSheetSummary();
1135
+ refreshUnseenReceipts();
1070
1136
  if (presenceBanner) presenceBanner.hidden = ended || agentPresence !== "waiting";
1071
1137
 
1072
1138
  // A supervisor-owned process-only listener is busy on the agent's behalf. It must not make the
package/dist/chrome.css CHANGED
@@ -1301,6 +1301,38 @@ body.lavish {
1301
1301
  border-radius: 3px;
1302
1302
  padding: 0 3px;
1303
1303
  }
1304
+ /* Delivery receipt: the per-note view of agent presence (docs/delivery-acks.md). Three emoji
1305
+ (seen, working, done) that light up as the server observes each step; a step that has not
1306
+ happened is the same glyph greyed and dimmed, so the row always shows what is still to come. */
1307
+ .receipt {
1308
+ display: flex;
1309
+ flex-wrap: wrap;
1310
+ align-items: center;
1311
+ justify-content: flex-end;
1312
+ gap: 4px 8px;
1313
+ margin-top: 6px;
1314
+ font-size: 13px;
1315
+ line-height: 1.3;
1316
+ color: var(--fg-faint);
1317
+ }
1318
+ .receipt-step {
1319
+ display: inline-block;
1320
+ filter: grayscale(1);
1321
+ opacity: 0.3;
1322
+ transition:
1323
+ opacity 0.3s ease,
1324
+ filter 0.3s ease;
1325
+ }
1326
+ .receipt-step.is-done {
1327
+ filter: none;
1328
+ opacity: 1;
1329
+ }
1330
+ .receipt-note {
1331
+ font-size: 11px;
1332
+ flex-basis: 100%;
1333
+ text-align: right;
1334
+ font-style: italic;
1335
+ }
1304
1336
  .bubble-attachments {
1305
1337
  display: flex;
1306
1338
  flex-wrap: wrap;
package/dist/cli.mjs CHANGED
@@ -7146,6 +7146,11 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
7146
7146
  (event) => {
7147
7147
  if (!annotationMode || isLavishUi(event.target) || isLavishAction(event.target) || isInteractiveControl(event.target))
7148
7148
  return;
7149
+ if ((event.ctrlKey || event.metaKey) && /** @type {Element} */
7150
+ event.target?.closest?.("a[href]")) {
7151
+ ignoreNextClick = false;
7152
+ return;
7153
+ }
7149
7154
  event.preventDefault();
7150
7155
  event.stopPropagation();
7151
7156
  if (ignoreNextClick) {
@@ -8276,6 +8281,62 @@ function serializeChatSync(session) {
8276
8281
  chat_revision: Number.isSafeInteger(revision) && revision >= 0 ? revision : 0
8277
8282
  };
8278
8283
  }
8284
+ var RECEIPT_NONE = "none";
8285
+ function isUserEntry(entry) {
8286
+ return Boolean(entry) && typeof entry === "object" && entry.role === "user" && entry.receipt !== RECEIPT_NONE;
8287
+ }
8288
+ function markPreReceipt(chat) {
8289
+ let changed = false;
8290
+ for (const entry of Array.isArray(chat) ? chat : []) {
8291
+ if (!entry || typeof entry !== "object" || entry.role !== "user" || entry.receipt === RECEIPT_NONE) continue;
8292
+ entry.receipt = RECEIPT_NONE;
8293
+ changed = true;
8294
+ }
8295
+ return changed;
8296
+ }
8297
+ function stampDelivered(chat, seq, at) {
8298
+ let changed = false;
8299
+ for (const entry of Array.isArray(chat) ? chat : []) {
8300
+ if (!isUserEntry(entry) || entry.delivered_at) continue;
8301
+ entry.delivered_at = String(at);
8302
+ entry.delivered_seq = seq;
8303
+ changed = true;
8304
+ }
8305
+ return changed;
8306
+ }
8307
+ function unstampDelivery(chat, seq) {
8308
+ let changed = false;
8309
+ for (const entry of Array.isArray(chat) ? chat : []) {
8310
+ if (!isUserEntry(entry) || entry.delivered_seq !== seq) continue;
8311
+ delete entry.delivered_at;
8312
+ delete entry.delivered_seq;
8313
+ delete entry.working_at;
8314
+ delete entry.done_at;
8315
+ changed = true;
8316
+ }
8317
+ return changed;
8318
+ }
8319
+ function isOpenDelivery(entry, closedSeq) {
8320
+ return isUserEntry(entry) && Boolean(entry.delivered_at) && !entry.done_at && entry.delivered_seq > closedSeq;
8321
+ }
8322
+ function stampWorking(chat, at, closedSeq = 0) {
8323
+ let changed = false;
8324
+ for (const entry of Array.isArray(chat) ? chat : []) {
8325
+ if (!isOpenDelivery(entry, closedSeq) || entry.working_at) continue;
8326
+ entry.working_at = String(at);
8327
+ changed = true;
8328
+ }
8329
+ return changed;
8330
+ }
8331
+ function stampDone(chat, at, closedSeq = 0) {
8332
+ let changed = false;
8333
+ for (const entry of Array.isArray(chat) ? chat : []) {
8334
+ if (!isOpenDelivery(entry, closedSeq)) continue;
8335
+ entry.done_at = String(at);
8336
+ changed = true;
8337
+ }
8338
+ return changed;
8339
+ }
8279
8340
 
8280
8341
  // src/server-log.js
8281
8342
  function formatServerLogLine(line, now = /* @__PURE__ */ new Date()) {
@@ -8504,6 +8565,14 @@ var SessionStore = class {
8504
8565
  // Compact prompt_id acks for bubbles evicted by the stored-chat byte bound. Reopening
8505
8566
  // must keep them: they are the settlement/dedup source once the visible entry is gone.
8506
8567
  chat_ack_ids: Array.isArray(existing.chat_ack_ids) ? existing.chat_ack_ids : [],
8568
+ // Counts the takes that stamped Seen on chat entries (docs/delivery-acks.md). Reset on reopen
8569
+ // and a later restore could un-stamp an older take's entries, so it is carried like chat.
8570
+ delivery_seq: normalizeRevision(existing.delivery_seq),
8571
+ // The last take of the rounds that have ended. Reopening starts a new round: a note an ended
8572
+ // round left at Seen stays Seen, because a later round's edit or reply is not about it.
8573
+ closed_delivery_seq: normalizeRevision(
8574
+ existing.status === "ended" ? existing.delivery_seq : existing.closed_delivery_seq
8575
+ ),
8507
8576
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
8508
8577
  };
8509
8578
  state.sessions[key] = session;
@@ -8558,7 +8627,11 @@ var SessionStore = class {
8558
8627
  else delete prompt.attachments;
8559
8628
  }
8560
8629
  }
8630
+ const deliverySeq = restoring ? normalizeRevision(payload.delivery_seq) : 0;
8631
+ const unstamped = deliverySeq > 0 && unstampDelivery(session.chat, deliverySeq);
8632
+ if (unstamped) session.chat_revision = normalizeRevision(session.chat_revision) + 1;
8561
8633
  if (rejected.length) {
8634
+ if (unstamped) await this.writeState(state);
8562
8635
  return {
8563
8636
  rejected: rejected.slice(0, MAX_REPORTED_ATTACHMENT_REJECTIONS),
8564
8637
  caps: {
@@ -8885,12 +8958,14 @@ var SessionStore = class {
8885
8958
  if (prompts.length === 0 && artifactFailures.length === 0) {
8886
8959
  return alreadyEnded ? { status: "ended", ended_by: session.ended_by } : { status: "waiting" };
8887
8960
  }
8961
+ const deliverySeq = normalizeRevision(session.delivery_seq) + 1;
8888
8962
  const result = {
8889
8963
  status: "feedback",
8890
8964
  dom_snapshot: session.dom_snapshot || "",
8891
8965
  prompts,
8892
8966
  ...artifactFailures.length > 0 ? { artifact_failures: artifactFailures } : {},
8893
- ...alreadyEnded ? { session_ended: true, ended_by: session.ended_by } : {}
8967
+ ...alreadyEnded ? { session_ended: true, ended_by: session.ended_by } : {},
8968
+ ...prompts.length > 0 ? { delivery_seq: deliverySeq } : {}
8894
8969
  };
8895
8970
  const deliveredNow = Date.now();
8896
8971
  const deliveredIds = /* @__PURE__ */ new Set();
@@ -8905,6 +8980,12 @@ var SessionStore = class {
8905
8980
  const current = [...deliveredIds].map((id) => ({ id, at: deliveredNow }));
8906
8981
  const historyRoom = Math.max(0, MAX_DELIVERED_ATTACHMENTS - current.length);
8907
8982
  session.delivered_attachments = [...carried.slice(-historyRoom), ...current];
8983
+ if (prompts.length > 0) {
8984
+ session.delivery_seq = deliverySeq;
8985
+ if (stampDelivered(session.chat, deliverySeq, new Date(deliveredNow).toISOString())) {
8986
+ session.chat_revision = normalizeRevision(session.chat_revision) + 1;
8987
+ }
8988
+ }
8908
8989
  session.prompts = [];
8909
8990
  session.artifact_failures = [];
8910
8991
  session.pending_prompts = 0;
@@ -8945,6 +9026,7 @@ var SessionStore = class {
8945
9026
  }
8946
9027
  if (requireOpen && session.status === "ended") return session;
8947
9028
  const at = (/* @__PURE__ */ new Date()).toISOString();
9029
+ stampDone(session.chat, at, normalizeRevision(session.closed_delivery_seq));
8948
9030
  session.chat = [...session.chat || [], { role: "agent", text: String(text || ""), at }];
8949
9031
  applyTranscriptBound(session);
8950
9032
  session.chat_revision = normalizeRevision(session.chat_revision) + 1;
@@ -8953,6 +9035,22 @@ var SessionStore = class {
8953
9035
  return session;
8954
9036
  });
8955
9037
  }
9038
+ // Working (docs/delivery-acks.md): the artifact changed after a delivery, so the agent is acting on
9039
+ // what it was handed. Returns the session when a stamp landed and null when there was nothing to
9040
+ // stamp, in which case state.json is not rewritten - the watcher fires on every save.
9041
+ async markWorking(key) {
9042
+ return this.runExclusive(async () => {
9043
+ const state = await this.readState();
9044
+ const session = state.sessions[key];
9045
+ if (!session) return null;
9046
+ const at = (/* @__PURE__ */ new Date()).toISOString();
9047
+ if (!stampWorking(session.chat, at, normalizeRevision(session.closed_delivery_seq))) return null;
9048
+ session.chat_revision = normalizeRevision(session.chat_revision) + 1;
9049
+ session.updated_at = at;
9050
+ await this.writeState(state);
9051
+ return session;
9052
+ });
9053
+ }
8956
9054
  /**
8957
9055
  * @template T
8958
9056
  * @param {() => Promise<T>} operation
@@ -8995,7 +9093,13 @@ var SessionStore = class {
8995
9093
  const state = { sessions: parsed.sessions || {} };
8996
9094
  let changed = false;
8997
9095
  for (const session of Object.values(state.sessions)) {
8998
- if (!session || typeof session !== "object" || !applyTranscriptBound(session)) continue;
9096
+ if (!session || typeof session !== "object") continue;
9097
+ if (!("delivery_seq" in session)) {
9098
+ session.delivery_seq = 0;
9099
+ markPreReceipt(session.chat);
9100
+ changed = true;
9101
+ }
9102
+ if (!applyTranscriptBound(session)) continue;
8999
9103
  session.chat_revision = normalizeRevision(session.chat_revision) + 1;
9000
9104
  changed = true;
9001
9105
  }
@@ -9950,7 +10054,12 @@ async function serve({
9950
10054
  if (clientKey === key) client.sendEvent(type, data);
9951
10055
  }
9952
10056
  }
9953
- events.on("reload", (key) => broadcastLiveEvent("reload", key));
10057
+ events.on("reload", (key) => {
10058
+ broadcastLiveEvent("reload", key);
10059
+ store.markWorking(key).then((session) => {
10060
+ if (session) events.emit("chat-sync", key, session);
10061
+ }).catch((error) => writeLog(`[lavish] working stamp after reload failed: ${error?.message || error}`));
10062
+ });
9954
10063
  events.on("agent-reply", (key, entry) => broadcastLiveEvent("agent-reply", key, entry));
9955
10064
  events.on("chat-sync", (key, session) => broadcastLiveEvent("chat-sync", key, serializeChatSync(session)));
9956
10065
  events.on(
@@ -10064,6 +10173,21 @@ async function serve({
10064
10173
  function finishFeedbackDelivery(key, result) {
10065
10174
  if (result.status !== "feedback") return;
10066
10175
  markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
10176
+ void publishChatSync(key);
10177
+ }
10178
+ async function publishChatSync(key) {
10179
+ try {
10180
+ const session = await store.findByKey(key);
10181
+ if (session) events.emit("chat-sync", key, session);
10182
+ } catch (error) {
10183
+ writeLog(`[lavish] chat-sync after delivery failed: ${error?.message || error}`);
10184
+ }
10185
+ }
10186
+ function agentFacingFeedback(result) {
10187
+ if (!result || result.status !== "feedback" || !("delivery_seq" in result)) return result;
10188
+ const rest = { ...result };
10189
+ delete rest.delivery_seq;
10190
+ return rest;
10067
10191
  }
10068
10192
  async function restoreClosedFeedback(key, result) {
10069
10193
  if (result.status !== "feedback") return;
@@ -10076,7 +10200,9 @@ async function serve({
10076
10200
  {
10077
10201
  dom_snapshot: result.dom_snapshot || "",
10078
10202
  prompts,
10079
- ...Array.isArray(result.artifact_failures) ? { artifact_failures: result.artifact_failures } : {}
10203
+ ...Array.isArray(result.artifact_failures) ? { artifact_failures: result.artifact_failures } : {},
10204
+ // Lets the store take this take's Seen stamps off again (docs/delivery-acks.md).
10205
+ ...Number.isInteger(result.delivery_seq) ? { delivery_seq: result.delivery_seq } : {}
10080
10206
  },
10081
10207
  {
10082
10208
  restore: true,
@@ -10099,12 +10225,13 @@ async function serve({
10099
10225
  `[lavish] closed poll feedback restore failed; the batch was lost: ${restoreError?.message || restoreError}`
10100
10226
  );
10101
10227
  } else if (persistedNothing) {
10102
- writeLog("[lavish] closed poll feedback restore was refused; nothing was persisted and the batch was lost");
10228
+ writeLog("[lavish] closed poll feedback restore was refused; nothing was re-queued and the batch was lost");
10103
10229
  } else if (!restoredPrompts || JSON.stringify(restoredPrompts) !== JSON.stringify(prompts) || !failuresRestored) {
10104
10230
  writeLog("[lavish] closed poll feedback restore was incomplete; delivery was not marked");
10105
10231
  }
10106
10232
  const pendingAfterRestore = Array.isArray(restoredPrompts) && restoredPrompts.length > 0 || Array.isArray(restoredFailures) && restoredFailures.length > 0;
10107
10233
  if (pendingAfterRestore) events.emit("feedback", key);
10234
+ await publishChatSync(key);
10108
10235
  }
10109
10236
  const whiteboardStateRoot = path7.dirname(stateFile2);
10110
10237
  const allowedHostnames = buildAllowedHostnames({
@@ -10374,7 +10501,7 @@ async function serve({
10374
10501
  releasePollListener(holder, activePolls, deliveredFeedback, events);
10375
10502
  if (immediate.session_ended) clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
10376
10503
  detachRequestClose();
10377
- res.json(immediate);
10504
+ res.json(agentFacingFeedback(immediate));
10378
10505
  return;
10379
10506
  }
10380
10507
  if (holder.replaced) {
@@ -10453,10 +10580,11 @@ async function serve({
10453
10580
  return;
10454
10581
  }
10455
10582
  finishFeedbackDelivery(key, responseResult);
10583
+ const agentResponse = agentFacingFeedback(responseResult);
10456
10584
  if (streamHeartbeat) {
10457
- res.end(JSON.stringify(responseResult));
10585
+ res.end(JSON.stringify(agentResponse));
10458
10586
  } else {
10459
- res.json(responseResult);
10587
+ res.json(agentResponse);
10460
10588
  }
10461
10589
  } finally {
10462
10590
  cleanup();
@@ -12340,7 +12468,7 @@ var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_J
12340
12468
  var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
12341
12469
  var POLL_STATE_HEADER = "lavish-poll-state";
12342
12470
  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.";
12343
- var VERSION = "0.1.82";
12471
+ var VERSION = "0.1.83";
12344
12472
  function detectInvokingAgent(env = process.env) {
12345
12473
  return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
12346
12474
  }
@@ -14269,7 +14397,7 @@ ${POLL_SEND_AND_END_RULE}
14269
14397
  `,
14270
14398
  reply: `Usage: lavish-axi reply <html-file> (--agent-reply "..." | --agent-reply-file <path>)
14271
14399
 
14272
- Post an agent reply to the open Lavish Editor session and exit once the server confirms it was accepted. Exit 0 only when the server answers that the reply was sent, which is when the board stops showing Working. If that answer does not arrive within ${AGENT_REPLY_RECEIPT_TIMEOUT_MS / 1e3} seconds, exit non-zero with a timeout error. Use this when you are handing a result back and are not about to wait for more feedback.
14400
+ Post an agent reply to the open Lavish Editor session and exit once the server confirms it was accepted. Exit 0 only when the server answers that the reply was sent, which is when the board stops showing Working. If that answer does not arrive within ${AGENT_REPLY_RECEIPT_TIMEOUT_MS / 1e3} seconds, exit non-zero with a timeout error. Use this when you are handing a result back and are not about to wait for more feedback. A reply also marks every note the poll delivered to you as Done in the reviewer's panel, so reply once you have handled them.
14273
14401
 
14274
14402
  Use \`lavish-axi poll <html-file> --agent-reply "..."\` instead when the reply should be followed by a long-poll. That command posts the reply and then keeps waiting, so it does not exit when the reply is accepted.
14275
14403
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.82",
3
+ "version": "0.1.83",
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.82",
4
+ "version": "0.1.83",
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",