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 +3 -1
- package/dist/chrome-client.js +67 -1
- package/dist/chrome.css +32 -0
- package/dist/cli.mjs +138 -10
- package/package.json +1 -1
- package/plugin.json +1 -1
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. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -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"
|
|
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) =>
|
|
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
|
|
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(
|
|
10585
|
+
res.end(JSON.stringify(agentResponse));
|
|
10458
10586
|
} else {
|
|
10459
|
-
res.json(
|
|
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.
|
|
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
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.
|
|
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",
|