lavish-axi 0.1.73 → 0.1.75
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 +4 -1
- package/dist/chrome-client.js +6 -3
- package/dist/cli.mjs +329 -63
- package/dist/server.mjs +11 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -205,7 +205,8 @@ pnpm link
|
|
|
205
205
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
206
206
|
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.
|
|
207
207
|
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.
|
|
208
|
-
- **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.
|
|
208
|
+
- **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. A supervisor-owned process listener is shown as an external listener rather than an idle captain turn.
|
|
209
|
+
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.
|
|
209
210
|
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.
|
|
210
211
|
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.
|
|
211
212
|
Codex-specific guidance keeps that poll attached to the active turn instead of hiding it in a background task, because completed background tasks may not resume the agent.
|
|
@@ -285,6 +286,8 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
285
286
|
| `lavish-axi share` | `--update-key <key>` | The secret returned when the page was published; required to republish or unpublish it. |
|
|
286
287
|
| `lavish-axi share` | `--unpublish` | Replace a published page with a locked placeholder (ht-ml.app cannot delete); takes `--site` and `--update-key` and no file. |
|
|
287
288
|
| `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. |
|
|
289
|
+
| `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`. |
|
|
290
|
+
| `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`. |
|
|
288
291
|
| `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. |
|
|
289
292
|
| `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`. |
|
|
290
293
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -988,12 +988,14 @@ function syncChat(chat, revision) {
|
|
|
988
988
|
}
|
|
989
989
|
|
|
990
990
|
function setAgentPresence(state) {
|
|
991
|
-
agentPresence = state === "listening" || state === "working" ? state : "waiting";
|
|
991
|
+
agentPresence = state === "listening" || state === "external" || state === "working" ? state : "waiting";
|
|
992
992
|
updateSendState();
|
|
993
993
|
renderSheetSummary();
|
|
994
994
|
if (presenceBanner) presenceBanner.hidden = ended || agentPresence !== "waiting";
|
|
995
995
|
|
|
996
|
-
|
|
996
|
+
// A supervisor-owned process-only listener is busy on the agent's behalf. It must not make the
|
|
997
|
+
// composer look like an idle captain turn while the owning agent is offline.
|
|
998
|
+
if (agentPresence !== "working" && agentPresence !== "external") {
|
|
997
999
|
if (workingBubble) workingBubble.remove();
|
|
998
1000
|
workingBubble = null;
|
|
999
1001
|
return;
|
|
@@ -1209,6 +1211,7 @@ function sheetSummary() {
|
|
|
1209
1211
|
}
|
|
1210
1212
|
if (unreadAgentReply) return { text: unreadAgentReply, accent: false, unread: true };
|
|
1211
1213
|
if (agentPresence === "working") return { text: "Agent is working…", accent: false, unread: false };
|
|
1214
|
+
if (agentPresence === "external") return { text: "External listener active", accent: false, unread: false };
|
|
1212
1215
|
if (agentPresence === "listening") return { text: "Agent listening", accent: false, unread: false };
|
|
1213
1216
|
return { text: "Agent not listening", accent: false, unread: false };
|
|
1214
1217
|
}
|
|
@@ -4075,7 +4078,7 @@ events.set("chat-sync", (data) => {
|
|
|
4075
4078
|
rememberChatAckIds(data.ack_ids);
|
|
4076
4079
|
syncChat(data.chat || [], data.chat_revision);
|
|
4077
4080
|
});
|
|
4078
|
-
events.set("agent-presence", (data) => setAgentPresence(data.state));
|
|
4081
|
+
events.set("agent-presence", (data) => setAgentPresence(data.mode === "external-listener" ? "external" : data.state));
|
|
4079
4082
|
events.set("layout-warnings", (data) => setLayoutWarnings(data.warnings || []));
|
|
4080
4083
|
events.set("ended", () => markSessionEnded());
|
|
4081
4084
|
connectLiveEvents();
|
package/dist/cli.mjs
CHANGED
|
@@ -9392,6 +9392,7 @@ var BROWSER_DISCONNECT_GRACE_MS = 1e4;
|
|
|
9392
9392
|
var ARTIFACT_CONTENT_SECURITY_POLICY = "sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads";
|
|
9393
9393
|
var ATTACHMENT_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
9394
9394
|
var SHUTDOWN_REASONS = /* @__PURE__ */ new Set(["upgrade", "local-build", "stop"]);
|
|
9395
|
+
var AGENT_LISTENER_LABEL = "agent-listener";
|
|
9395
9396
|
var RELOAD_DEBOUNCE_MS = 100;
|
|
9396
9397
|
var BATCH_RELOAD_DEBOUNCE_MS = 900;
|
|
9397
9398
|
function defaultWhiteboardAssetsDir() {
|
|
@@ -9520,8 +9521,10 @@ async function serve({
|
|
|
9520
9521
|
const watchers = /* @__PURE__ */ new Map();
|
|
9521
9522
|
const activePolls = /* @__PURE__ */ new Map();
|
|
9522
9523
|
const deliveredFeedback = /* @__PURE__ */ new Set();
|
|
9524
|
+
const pollOwnershipLock = new AsyncMutex();
|
|
9523
9525
|
const liveEventClients = /* @__PURE__ */ new Map();
|
|
9524
9526
|
const browserDisconnectTimers = /* @__PURE__ */ new Map();
|
|
9527
|
+
const browserDisconnectPending = /* @__PURE__ */ new Set();
|
|
9525
9528
|
const whiteboardChannelSecret = crypto6.randomBytes(32);
|
|
9526
9529
|
const outstandingRepairBatches = /* @__PURE__ */ new Set();
|
|
9527
9530
|
const diagnosticViewportClasses = resolveDiagnosticViewportClasses();
|
|
@@ -9544,7 +9547,10 @@ async function serve({
|
|
|
9544
9547
|
events.on("reload", (key) => broadcastLiveEvent("reload", key));
|
|
9545
9548
|
events.on("agent-reply", (key, entry) => broadcastLiveEvent("agent-reply", key, entry));
|
|
9546
9549
|
events.on("chat-sync", (key, session) => broadcastLiveEvent("chat-sync", key, serializeChatSync(session)));
|
|
9547
|
-
events.on(
|
|
9550
|
+
events.on(
|
|
9551
|
+
"agent-presence",
|
|
9552
|
+
(key, state) => broadcastLiveEvent("agent-presence", key, presenceEventData(key, state, activePolls, deliveredFeedback))
|
|
9553
|
+
);
|
|
9548
9554
|
events.on("layout-warnings", (key, warnings) => broadcastLiveEvent("layout-warnings", key, { warnings }));
|
|
9549
9555
|
events.on("ended", (key, endedBy) => broadcastLiveEvent("ended", key, { ended_by: endedBy || null }));
|
|
9550
9556
|
function hasLiveEventClient(key) {
|
|
@@ -9564,13 +9570,17 @@ async function serve({
|
|
|
9564
9570
|
if (shuttingDown || hasLiveEventClient(key) || !activePolls.has(key)) return;
|
|
9565
9571
|
const timer = setTimeout(() => {
|
|
9566
9572
|
browserDisconnectTimers.delete(key);
|
|
9567
|
-
if (!hasLiveEventClient(key) && activePolls.has(key))
|
|
9573
|
+
if (!hasLiveEventClient(key) && activePolls.has(key)) {
|
|
9574
|
+
browserDisconnectPending.add(key);
|
|
9575
|
+
events.emit("browser-disconnected", key);
|
|
9576
|
+
}
|
|
9568
9577
|
}, browserDisconnectGraceMs);
|
|
9569
9578
|
timer.unref?.();
|
|
9570
9579
|
browserDisconnectTimers.set(key, timer);
|
|
9571
9580
|
}
|
|
9572
9581
|
function attachLiveEventClient(client, key, onClose) {
|
|
9573
9582
|
clearBrowserDisconnectTimer(key);
|
|
9583
|
+
browserDisconnectPending.delete(key);
|
|
9574
9584
|
liveEventClients.set(client, key);
|
|
9575
9585
|
refreshIdleTimer();
|
|
9576
9586
|
let cleanedUp = false;
|
|
@@ -9591,7 +9601,8 @@ async function serve({
|
|
|
9591
9601
|
return;
|
|
9592
9602
|
}
|
|
9593
9603
|
client.sendEvent("chat-sync", serializeChatSync(session));
|
|
9594
|
-
|
|
9604
|
+
const presence = computePresence(key, activePolls, deliveredFeedback);
|
|
9605
|
+
client.sendEvent("agent-presence", presenceEventData(key, presence, activePolls, deliveredFeedback));
|
|
9595
9606
|
if (session?.status === "ended") client.sendEvent("ended", { ended_by: session.ended_by || null });
|
|
9596
9607
|
}
|
|
9597
9608
|
function requestedBindIsRecoverable() {
|
|
@@ -9627,7 +9638,6 @@ async function serve({
|
|
|
9627
9638
|
function finishFeedbackDelivery(key, result) {
|
|
9628
9639
|
if (result.status !== "feedback") return;
|
|
9629
9640
|
markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
|
|
9630
|
-
if (result.session_ended) clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
9631
9641
|
}
|
|
9632
9642
|
async function restoreClosedFeedback(key, result) {
|
|
9633
9643
|
if (result.status !== "feedback") return;
|
|
@@ -9754,7 +9764,11 @@ async function serve({
|
|
|
9754
9764
|
app: "lavish-axi",
|
|
9755
9765
|
version,
|
|
9756
9766
|
...networkStale ? { network_stale: true } : {},
|
|
9757
|
-
...networkWarning ? { network_warning: networkWarning } : {}
|
|
9767
|
+
...networkWarning ? { network_warning: networkWarning } : {},
|
|
9768
|
+
listeners: [...activePolls].map(([key, holder]) => ({
|
|
9769
|
+
key,
|
|
9770
|
+
label: listenerLabel(holder)
|
|
9771
|
+
}))
|
|
9758
9772
|
});
|
|
9759
9773
|
});
|
|
9760
9774
|
let shutdownResolve;
|
|
@@ -9804,13 +9818,44 @@ async function serve({
|
|
|
9804
9818
|
next(error);
|
|
9805
9819
|
}
|
|
9806
9820
|
});
|
|
9807
|
-
|
|
9808
|
-
|
|
9821
|
+
async function publishAgentReply(key, text) {
|
|
9822
|
+
const session = await store.addAgentReply(key, text);
|
|
9823
|
+
if (!session) return null;
|
|
9824
|
+
const lastEntry = session.chat?.at(-1);
|
|
9825
|
+
const entry = serializeChat([
|
|
9826
|
+
lastEntry?.role === "agent" ? lastEntry : { role: "agent", text, at: session.updated_at }
|
|
9827
|
+
])[0];
|
|
9828
|
+
events.emit("agent-reply", key, entry);
|
|
9829
|
+
events.emit("chat-sync", key, session);
|
|
9830
|
+
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
9831
|
+
return session;
|
|
9832
|
+
}
|
|
9833
|
+
const handlePoll = async (req, res, next) => {
|
|
9834
|
+
const takeover = req.query.takeover === "1";
|
|
9835
|
+
const agentReply = req.method === "POST" && req.body?.agent_reply !== void 0 ? String(req.body.agent_reply) : null;
|
|
9836
|
+
if (takeover && req.method !== "POST") {
|
|
9837
|
+
res.status(405).json({ error: "poll takeover requires POST" });
|
|
9838
|
+
return;
|
|
9839
|
+
}
|
|
9840
|
+
if (req.method === "POST" && (agentReply === null ? !takeover : !agentReply.trim())) {
|
|
9841
|
+
res.status(405).json({ error: "POST /api/poll requires agent_reply or takeover=1" });
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
let requestClosed = Boolean(req.aborted || req.method !== "POST" && req.destroyed);
|
|
9809
9845
|
let cleanupPoll = null;
|
|
9846
|
+
let claimedHolder = null;
|
|
9810
9847
|
const onRequestClose = () => {
|
|
9848
|
+
if (req.method === "POST" && !req.aborted) return;
|
|
9811
9849
|
requestClosed = true;
|
|
9812
9850
|
cleanupPoll?.();
|
|
9813
9851
|
};
|
|
9852
|
+
const onResponseClose = () => {
|
|
9853
|
+
if (req.method === "POST" && !res.writableEnded) {
|
|
9854
|
+
requestClosed = true;
|
|
9855
|
+
cleanupPoll?.();
|
|
9856
|
+
}
|
|
9857
|
+
};
|
|
9858
|
+
res.on("close", onResponseClose);
|
|
9814
9859
|
const detachRequestClose = () => req.off("close", onRequestClose);
|
|
9815
9860
|
req.on("close", onRequestClose);
|
|
9816
9861
|
try {
|
|
@@ -9824,34 +9869,99 @@ async function serve({
|
|
|
9824
9869
|
};
|
|
9825
9870
|
const file = await canonicalFile(String(req.query.file || ""));
|
|
9826
9871
|
const key = sessionKey(file);
|
|
9872
|
+
const ownerValue = typeof req.query.owner === "string" ? req.query.owner.trim() : "";
|
|
9873
|
+
if (ownerValue.toLowerCase() === "none" || ownerValue.startsWith("-")) {
|
|
9874
|
+
detachRequestClose();
|
|
9875
|
+
res.status(400).json({
|
|
9876
|
+
status: "error",
|
|
9877
|
+
code: "VALIDATION_ERROR",
|
|
9878
|
+
error: "--owner none is reserved; pass a different listener label"
|
|
9879
|
+
});
|
|
9880
|
+
return;
|
|
9881
|
+
}
|
|
9882
|
+
const owner = ownerValue || null;
|
|
9883
|
+
if (hasPresentOriginOrReferer(req) && !isSameOriginRequest(req, allowedHostnames, allowAnyHostname)) {
|
|
9884
|
+
detachRequestClose();
|
|
9885
|
+
res.status(403).json({ error: "cross-origin poll takeover rejected" });
|
|
9886
|
+
return;
|
|
9887
|
+
}
|
|
9888
|
+
const { holder, previousPresence, conflict, missing } = await pollOwnershipLock.runExclusive(async () => {
|
|
9889
|
+
const currentHolder = activePolls.get(key);
|
|
9890
|
+
if (currentHolder && !takeover) return { conflict: listenerConflict(currentHolder, "LISTENER_ACTIVE") };
|
|
9891
|
+
const nextHolder = { key, owner, startedAt: Date.now(), replaced: false, replace: null };
|
|
9892
|
+
claimedHolder = nextHolder;
|
|
9893
|
+
const priorPresence = presenceSignature(key, activePolls, deliveredFeedback);
|
|
9894
|
+
if (currentHolder) {
|
|
9895
|
+
currentHolder.replaced = true;
|
|
9896
|
+
activePolls.set(key, nextHolder);
|
|
9897
|
+
currentHolder.replace?.();
|
|
9898
|
+
} else {
|
|
9899
|
+
activePolls.set(key, nextHolder);
|
|
9900
|
+
}
|
|
9901
|
+
deliveredFeedback.delete(key);
|
|
9902
|
+
if (agentReply !== null) {
|
|
9903
|
+
const session = await publishAgentReply(key, agentReply);
|
|
9904
|
+
if (!session) {
|
|
9905
|
+
activePolls.delete(key);
|
|
9906
|
+
return { missing: true };
|
|
9907
|
+
}
|
|
9908
|
+
}
|
|
9909
|
+
return { holder: nextHolder, previousPresence: priorPresence };
|
|
9910
|
+
});
|
|
9911
|
+
if (conflict) {
|
|
9912
|
+
detachRequestClose();
|
|
9913
|
+
res.status(409).json(conflict);
|
|
9914
|
+
return;
|
|
9915
|
+
}
|
|
9916
|
+
if (missing) {
|
|
9917
|
+
detachRequestClose();
|
|
9918
|
+
res.status(404).json({ error: "session not found" });
|
|
9919
|
+
return;
|
|
9920
|
+
}
|
|
9827
9921
|
const timeoutMs = req.query.timeoutMs === void 0 ? null : Math.max(0, Math.min(Number(req.query.timeoutMs || 0), 2147483647));
|
|
9828
9922
|
const immediate = await store.takeFeedback(key);
|
|
9829
9923
|
if (immediate.status !== "waiting") {
|
|
9830
|
-
if (
|
|
9924
|
+
if (holder.replaced) {
|
|
9925
|
+
await restoreClosedFeedback(key, immediate);
|
|
9926
|
+
detachRequestClose();
|
|
9927
|
+
res.json(listenerConflict(holder, "LISTENER_REPLACED"));
|
|
9928
|
+
releasePollListener(holder, activePolls, deliveredFeedback, events);
|
|
9929
|
+
return;
|
|
9930
|
+
}
|
|
9931
|
+
if (requestClosed || res.writableEnded) {
|
|
9831
9932
|
await restoreClosedFeedback(key, immediate);
|
|
9933
|
+
releasePollListener(holder, activePolls, deliveredFeedback, events);
|
|
9832
9934
|
detachRequestClose();
|
|
9833
9935
|
return;
|
|
9834
9936
|
}
|
|
9835
9937
|
finishFeedbackDelivery(key, immediate);
|
|
9938
|
+
releasePollListener(holder, activePolls, deliveredFeedback, events);
|
|
9939
|
+
if (immediate.session_ended) clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
9836
9940
|
detachRequestClose();
|
|
9837
9941
|
res.json(immediate);
|
|
9838
9942
|
return;
|
|
9839
9943
|
}
|
|
9840
|
-
if (
|
|
9944
|
+
if (holder.replaced) {
|
|
9945
|
+
detachRequestClose();
|
|
9946
|
+
res.json(listenerConflict(holder, "LISTENER_REPLACED"));
|
|
9947
|
+
releasePollListener(holder, activePolls, deliveredFeedback, events);
|
|
9948
|
+
return;
|
|
9949
|
+
}
|
|
9950
|
+
if (requestClosed || res.writableEnded) {
|
|
9951
|
+
releasePollListener(holder, activePolls, deliveredFeedback, events);
|
|
9841
9952
|
detachRequestClose();
|
|
9842
9953
|
return;
|
|
9843
9954
|
}
|
|
9844
9955
|
const streamHeartbeat = timeoutMs === null;
|
|
9845
9956
|
let heartbeat = null;
|
|
9846
9957
|
if (streamHeartbeat) {
|
|
9847
|
-
res.status(200).type("application/json");
|
|
9958
|
+
res.status(200).type("application/json").set("Lavish-Poll-State", "listening");
|
|
9848
9959
|
res.write(" ");
|
|
9849
9960
|
heartbeat = setInterval(() => {
|
|
9850
9961
|
if (!res.writableEnded) res.write(" ");
|
|
9851
9962
|
}, pollHeartbeatMs);
|
|
9852
9963
|
heartbeat.unref?.();
|
|
9853
9964
|
}
|
|
9854
|
-
setPollActive(key, activePolls, deliveredFeedback, events, true);
|
|
9855
9965
|
refreshIdleTimer();
|
|
9856
9966
|
let timer = null;
|
|
9857
9967
|
let cleaned = false;
|
|
@@ -9864,19 +9974,45 @@ async function serve({
|
|
|
9864
9974
|
events.off("feedback", onFeedback);
|
|
9865
9975
|
events.off("ended", onFeedback);
|
|
9866
9976
|
events.off("browser-disconnected", onBrowserDisconnected);
|
|
9867
|
-
|
|
9868
|
-
if (!activePolls.has(key))
|
|
9977
|
+
releasePollListener(holder, activePolls, deliveredFeedback, events);
|
|
9978
|
+
if (!activePolls.has(key)) {
|
|
9979
|
+
clearBrowserDisconnectTimer(key);
|
|
9980
|
+
browserDisconnectPending.delete(key);
|
|
9981
|
+
}
|
|
9869
9982
|
refreshIdleTimer();
|
|
9870
9983
|
cleanupPoll = null;
|
|
9871
9984
|
detachRequestClose();
|
|
9985
|
+
res.off("close", onResponseClose);
|
|
9872
9986
|
};
|
|
9987
|
+
const respondReplacement = () => {
|
|
9988
|
+
if (responding || res.writableEnded) return;
|
|
9989
|
+
responding = true;
|
|
9990
|
+
try {
|
|
9991
|
+
const replacement = listenerConflict(holder, "LISTENER_REPLACED");
|
|
9992
|
+
if (streamHeartbeat) res.end(JSON.stringify(replacement));
|
|
9993
|
+
else res.json(replacement);
|
|
9994
|
+
} finally {
|
|
9995
|
+
cleanup();
|
|
9996
|
+
}
|
|
9997
|
+
};
|
|
9998
|
+
holder.replace = respondReplacement;
|
|
9873
9999
|
const respond = async (forcedResult = null) => {
|
|
9874
10000
|
if (responding || res.writableEnded) return;
|
|
9875
10001
|
responding = true;
|
|
10002
|
+
let finalSessionEnded = false;
|
|
9876
10003
|
try {
|
|
9877
10004
|
const result = await store.takeFeedback(key);
|
|
9878
10005
|
const responseResult = forcedResult && result.status === "waiting" ? forcedResult : result;
|
|
9879
|
-
|
|
10006
|
+
finalSessionEnded = responseResult?.session_ended === true;
|
|
10007
|
+
if (holder.replaced) {
|
|
10008
|
+
if (responseResult.status !== "waiting") await restoreClosedFeedback(key, responseResult);
|
|
10009
|
+
if (!requestClosed && !res.writableEnded) {
|
|
10010
|
+
if (streamHeartbeat) res.end(JSON.stringify(listenerConflict(holder, "LISTENER_REPLACED")));
|
|
10011
|
+
else res.json(listenerConflict(holder, "LISTENER_REPLACED"));
|
|
10012
|
+
}
|
|
10013
|
+
return;
|
|
10014
|
+
}
|
|
10015
|
+
if (requestClosed || res.writableEnded) {
|
|
9880
10016
|
await restoreClosedFeedback(key, responseResult);
|
|
9881
10017
|
return;
|
|
9882
10018
|
}
|
|
@@ -9888,6 +10024,7 @@ async function serve({
|
|
|
9888
10024
|
}
|
|
9889
10025
|
} finally {
|
|
9890
10026
|
cleanup();
|
|
10027
|
+
if (finalSessionEnded) clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
9891
10028
|
}
|
|
9892
10029
|
};
|
|
9893
10030
|
const onFeedback = (changedKey) => {
|
|
@@ -9904,17 +10041,33 @@ async function serve({
|
|
|
9904
10041
|
events.on("ended", onFeedback);
|
|
9905
10042
|
events.on("browser-disconnected", onBrowserDisconnected);
|
|
9906
10043
|
cleanupPoll = cleanup;
|
|
9907
|
-
if (
|
|
10044
|
+
if (holder.replaced) {
|
|
10045
|
+
respondReplacement();
|
|
10046
|
+
return;
|
|
10047
|
+
}
|
|
10048
|
+
if (requestClosed || res.writableEnded) {
|
|
9908
10049
|
cleanup();
|
|
9909
10050
|
return;
|
|
9910
10051
|
}
|
|
10052
|
+
if (browserDisconnectPending.has(key)) {
|
|
10053
|
+
browserDisconnectPending.delete(key);
|
|
10054
|
+
onBrowserDisconnected(key);
|
|
10055
|
+
return;
|
|
10056
|
+
}
|
|
10057
|
+
const nextPresence = presenceSignature(key, activePolls, deliveredFeedback);
|
|
10058
|
+
if (nextPresence !== previousPresence) {
|
|
10059
|
+
events.emit("agent-presence", key, computePresence(key, activePolls, deliveredFeedback));
|
|
10060
|
+
}
|
|
9911
10061
|
timer = timeoutMs === null ? null : setTimeout(() => respond().catch(handleRespondError), timeoutMs);
|
|
9912
10062
|
} catch (error) {
|
|
9913
10063
|
cleanupPoll?.();
|
|
10064
|
+
if (claimedHolder) releasePollListener(claimedHolder, activePolls, deliveredFeedback, events);
|
|
9914
10065
|
detachRequestClose();
|
|
9915
10066
|
next(error);
|
|
9916
10067
|
}
|
|
9917
|
-
}
|
|
10068
|
+
};
|
|
10069
|
+
app.get("/api/poll", handlePoll);
|
|
10070
|
+
app.post("/api/poll", handlePoll);
|
|
9918
10071
|
app.post("/api/:key/prompts", async (req, res, next) => {
|
|
9919
10072
|
try {
|
|
9920
10073
|
if (!isSameOriginRequest(req, allowedHostnames, allowAnyHostname)) {
|
|
@@ -10066,18 +10219,11 @@ async function serve({
|
|
|
10066
10219
|
});
|
|
10067
10220
|
app.post("/api/:key/agent-reply", async (req, res, next) => {
|
|
10068
10221
|
try {
|
|
10069
|
-
const
|
|
10070
|
-
const session = await store.addAgentReply(req.params.key, text);
|
|
10222
|
+
const session = await publishAgentReply(req.params.key, String(req.body?.text || ""));
|
|
10071
10223
|
if (!session) {
|
|
10072
10224
|
res.status(404).json({ error: "session not found" });
|
|
10073
10225
|
return;
|
|
10074
10226
|
}
|
|
10075
|
-
const entry = serializeChat([
|
|
10076
|
-
session.chat?.at(-1)?.role === "agent" ? session.chat.at(-1) : { role: "agent", text, at: session.updated_at }
|
|
10077
|
-
])[0];
|
|
10078
|
-
events.emit("agent-reply", req.params.key, entry);
|
|
10079
|
-
events.emit("chat-sync", req.params.key, session);
|
|
10080
|
-
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
10081
10227
|
res.json({ status: "sent" });
|
|
10082
10228
|
} catch (error) {
|
|
10083
10229
|
next(error);
|
|
@@ -10658,7 +10804,9 @@ ${body}`
|
|
|
10658
10804
|
let retryIndex = 0;
|
|
10659
10805
|
while (true) {
|
|
10660
10806
|
try {
|
|
10661
|
-
const httpServer = await listenHttp(app, boundPort, listenHost)
|
|
10807
|
+
const httpServer = await listenHttp(app, boundPort, listenHost, (error) => {
|
|
10808
|
+
writeLog(`[lavish] HTTP server error: ${error instanceof Error ? error.message : String(error)}`);
|
|
10809
|
+
});
|
|
10662
10810
|
httpServer.on("upgrade", handleEventUpgrade);
|
|
10663
10811
|
if (boundPort === 0) boundPort = httpServer.address().port;
|
|
10664
10812
|
httpServers.push(httpServer);
|
|
@@ -10858,15 +11006,20 @@ ${body}`
|
|
|
10858
11006
|
done
|
|
10859
11007
|
};
|
|
10860
11008
|
}
|
|
10861
|
-
function listenHttp(app, port, host) {
|
|
11009
|
+
function listenHttp(app, port, host, onRuntimeError) {
|
|
10862
11010
|
return new Promise((resolve, reject) => {
|
|
10863
11011
|
const server = createServer(app);
|
|
11012
|
+
let listening = false;
|
|
10864
11013
|
const onError = (error) => {
|
|
10865
|
-
|
|
10866
|
-
|
|
11014
|
+
if (!listening) {
|
|
11015
|
+
server.off("listening", onListening);
|
|
11016
|
+
reject(error);
|
|
11017
|
+
return;
|
|
11018
|
+
}
|
|
11019
|
+
onRuntimeError?.(error);
|
|
10867
11020
|
};
|
|
10868
11021
|
const onListening = () => {
|
|
10869
|
-
|
|
11022
|
+
listening = true;
|
|
10870
11023
|
const address = server.address();
|
|
10871
11024
|
if (address && typeof address === "object" && isWildcardHost(address.address)) {
|
|
10872
11025
|
server.close(() => reject(new Error(`Refusing all-interfaces listener at ${address.address}`)));
|
|
@@ -10874,7 +11027,7 @@ function listenHttp(app, port, host) {
|
|
|
10874
11027
|
}
|
|
10875
11028
|
resolve(server);
|
|
10876
11029
|
};
|
|
10877
|
-
server.
|
|
11030
|
+
server.on("error", onError);
|
|
10878
11031
|
server.once("listening", onListening);
|
|
10879
11032
|
server.listen({ port, host });
|
|
10880
11033
|
});
|
|
@@ -11100,19 +11253,23 @@ function hasLiveReloadRootOptIn(html) {
|
|
|
11100
11253
|
if (/<html\b[^>]*\sdata-lavish-live-reload-root(?:[\s=>/]|$)[^>]*>/i.test(searchableHtml)) return true;
|
|
11101
11254
|
return /<meta\b(?=[^>]*name=["']lavish-live-reload["'])(?=[^>]*content=["']root["'])[^>]*>/i.test(searchableHtml);
|
|
11102
11255
|
}
|
|
11103
|
-
function
|
|
11104
|
-
|
|
11105
|
-
const
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
if (
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11256
|
+
function releasePollListener(holder, activePolls, deliveredFeedback, events) {
|
|
11257
|
+
if (activePolls.get(holder.key) !== holder) return;
|
|
11258
|
+
const previousPresence = computePresence(holder.key, activePolls, deliveredFeedback);
|
|
11259
|
+
activePolls.delete(holder.key);
|
|
11260
|
+
const nextPresence = computePresence(holder.key, activePolls, deliveredFeedback);
|
|
11261
|
+
if (nextPresence !== previousPresence) events.emit("agent-presence", holder.key, nextPresence);
|
|
11262
|
+
}
|
|
11263
|
+
function listenerLabel(holder) {
|
|
11264
|
+
return holder.owner || AGENT_LISTENER_LABEL;
|
|
11265
|
+
}
|
|
11266
|
+
function listenerConflict(holder, code) {
|
|
11267
|
+
return {
|
|
11268
|
+
status: "error",
|
|
11269
|
+
code,
|
|
11270
|
+
error: code === "LISTENER_ACTIVE" ? "Lavish Editor already has an active poll listener" : "Lavish Editor poll listener was replaced",
|
|
11271
|
+
holder: { label: listenerLabel(holder), age_ms: Math.max(0, Date.now() - holder.startedAt) }
|
|
11272
|
+
};
|
|
11116
11273
|
}
|
|
11117
11274
|
function markFeedbackDelivered(key, activePolls, deliveredFeedback, events) {
|
|
11118
11275
|
const previousPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
@@ -11131,9 +11288,21 @@ function clearFeedbackDelivery(key, activePolls, deliveredFeedback, events) {
|
|
|
11131
11288
|
}
|
|
11132
11289
|
}
|
|
11133
11290
|
function computePresence(key, activePolls, deliveredFeedback) {
|
|
11134
|
-
if (activePolls.has(key)) return "listening";
|
|
11135
11291
|
if (deliveredFeedback.has(key)) return "working";
|
|
11136
|
-
return "waiting";
|
|
11292
|
+
return activePolls.has(key) ? "listening" : "waiting";
|
|
11293
|
+
}
|
|
11294
|
+
function presenceSignature(key, activePolls, deliveredFeedback) {
|
|
11295
|
+
return `${computePresence(key, activePolls, deliveredFeedback)}:${presenceMode(key, activePolls, deliveredFeedback)}`;
|
|
11296
|
+
}
|
|
11297
|
+
function presenceMode(key, activePolls, deliveredFeedback) {
|
|
11298
|
+
if (deliveredFeedback.has(key)) return "agent-busy";
|
|
11299
|
+
const holder = activePolls.get(key);
|
|
11300
|
+
if (!holder) return "waiting-on-captain";
|
|
11301
|
+
return holder.owner ? "external-listener" : "agent-listener";
|
|
11302
|
+
}
|
|
11303
|
+
function presenceEventData(key, state, activePolls, deliveredFeedback) {
|
|
11304
|
+
const mode = presenceMode(key, activePolls, deliveredFeedback);
|
|
11305
|
+
return mode === "waiting-on-captain" ? { state } : { state, mode };
|
|
11137
11306
|
}
|
|
11138
11307
|
function chromeIcon(paths, size = 16, strokeWidth = 1.7) {
|
|
11139
11308
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
@@ -11614,19 +11783,62 @@ var POLL_SEND_AND_END_RULE = "`Send & End` ends the session. Its final feedback
|
|
|
11614
11783
|
var POLL_AGENT_REPLY_RULE = "Keep the reply concise. Only when a longer reply is genuinely necessary, use Markdown structure - blank-line paragraphs, `- ` / `1. ` lists, `## ` headings - so it renders scannably instead of a wall of text, and pass that body with `--agent-reply-file <path>` (`-` reads stdin) so newlines survive quoting.";
|
|
11615
11784
|
var POLL_AGENT_REPLY_HELP_POINTER = "The Conversation panel's Markdown subset is in README's Feedback controls bullet.";
|
|
11616
11785
|
var POLL_AGENT_REPLY_NEXT_POINTER = "The Conversation panel's Markdown subset is in `lavish-axi poll --help` and README.";
|
|
11617
|
-
var POLL_VALUE_FLAGS = ["--agent-reply", "--agent-reply-file", "--timeout-ms"];
|
|
11786
|
+
var POLL_VALUE_FLAGS = ["--agent-reply", "--agent-reply-file", "--timeout-ms", "--owner"];
|
|
11618
11787
|
var AGENT_REPLY_JSON_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
11619
|
-
var AGENT_REPLY_JSON_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({
|
|
11788
|
+
var AGENT_REPLY_JSON_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ agent_reply: "" }));
|
|
11620
11789
|
var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_JSON_ENVELOPE_BYTES;
|
|
11621
11790
|
var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
|
|
11791
|
+
var POLL_STATE_HEADER = "lavish-poll-state";
|
|
11622
11792
|
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.";
|
|
11623
|
-
var VERSION = "0.1.
|
|
11793
|
+
var VERSION = "0.1.75";
|
|
11624
11794
|
function detectInvokingAgent(env = process.env) {
|
|
11625
11795
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
11626
11796
|
}
|
|
11627
11797
|
function shouldNarratePollWaitTicks({ isTTY }) {
|
|
11628
11798
|
return Boolean(isTTY);
|
|
11629
11799
|
}
|
|
11800
|
+
function herdrPollChimeEnabled(env = process.env) {
|
|
11801
|
+
return env.HERDR_ENV === "1" && env.LAVISH_AXI_HERDR_CHIME === "1";
|
|
11802
|
+
}
|
|
11803
|
+
function notifyHerdrPollReady({ env = process.env, runner = spawn } = {}) {
|
|
11804
|
+
if (!herdrPollChimeEnabled(env)) return false;
|
|
11805
|
+
try {
|
|
11806
|
+
const child = runner(
|
|
11807
|
+
"herdr",
|
|
11808
|
+
[
|
|
11809
|
+
"notification",
|
|
11810
|
+
"show",
|
|
11811
|
+
"Lavish review ready",
|
|
11812
|
+
"--body",
|
|
11813
|
+
"The artifact is open and Lavish is polling for your feedback.",
|
|
11814
|
+
"--sound",
|
|
11815
|
+
"request"
|
|
11816
|
+
],
|
|
11817
|
+
{ stdio: "ignore" }
|
|
11818
|
+
);
|
|
11819
|
+
const kill = () => {
|
|
11820
|
+
try {
|
|
11821
|
+
child.kill("SIGKILL");
|
|
11822
|
+
} catch {
|
|
11823
|
+
return;
|
|
11824
|
+
}
|
|
11825
|
+
};
|
|
11826
|
+
const timer = setTimeout(kill, 2e3);
|
|
11827
|
+
timer.unref();
|
|
11828
|
+
const cleanup = () => {
|
|
11829
|
+
clearTimeout(timer);
|
|
11830
|
+
process.off("exit", kill);
|
|
11831
|
+
};
|
|
11832
|
+
child.on("error", () => {
|
|
11833
|
+
});
|
|
11834
|
+
child.once("close", cleanup);
|
|
11835
|
+
process.once("exit", kill);
|
|
11836
|
+
child.unref();
|
|
11837
|
+
return true;
|
|
11838
|
+
} catch {
|
|
11839
|
+
return false;
|
|
11840
|
+
}
|
|
11841
|
+
}
|
|
11630
11842
|
function pollExecutionGuidance({ agent = "generic" } = {}) {
|
|
11631
11843
|
const sharedGuidance = POLL_WAKE_PATH_RULES.join(" ");
|
|
11632
11844
|
const agentGuidance = agent === "codex" ? ` ${CODEX_POLL_WAKE_PATH_GUIDANCE}` : "";
|
|
@@ -11721,7 +11933,8 @@ function createHomeOutput({ bin, sessions, includeSessions = true, agent = "gene
|
|
|
11721
11933
|
file: session.file,
|
|
11722
11934
|
status: session.status,
|
|
11723
11935
|
url: session.url,
|
|
11724
|
-
pending_prompts: session.pending_prompts || 0
|
|
11936
|
+
pending_prompts: session.pending_prompts || 0,
|
|
11937
|
+
listener: session.listener || "none"
|
|
11725
11938
|
}))
|
|
11726
11939
|
} : {},
|
|
11727
11940
|
visual_guidance: [
|
|
@@ -11841,14 +12054,24 @@ async function pollCommand(args) {
|
|
|
11841
12054
|
if (!file) {
|
|
11842
12055
|
throw new AxiError("HTML file path is required", "VALIDATION_ERROR", ["Run `lavish-axi poll <html-file>`"]);
|
|
11843
12056
|
}
|
|
12057
|
+
const ownerFlag = inspectValueFlag(args, "--owner");
|
|
12058
|
+
const owner = ownerFlag.present ? String(ownerFlag.value || "").trim() : null;
|
|
12059
|
+
if (ownerFlag.present && (!owner || owner.startsWith("-") || owner.toLowerCase() === "none")) {
|
|
12060
|
+
throw new AxiError(
|
|
12061
|
+
owner?.toLowerCase() === "none" ? "--owner none is reserved" : "--owner requires a non-empty label",
|
|
12062
|
+
"VALIDATION_ERROR",
|
|
12063
|
+
["Pass `--owner <label>` to identify the process listening for feedback"]
|
|
12064
|
+
);
|
|
12065
|
+
}
|
|
12066
|
+
const takeover = args.includes("--takeover");
|
|
11844
12067
|
const agentReply = await resolveAgentReply(args);
|
|
11845
12068
|
const absolute = await canonicalFile(file);
|
|
11846
12069
|
const baseUrl = await ensureServer();
|
|
11847
|
-
if (agentReply) {
|
|
11848
|
-
await postJson(`${baseUrl}/api/${sessionKey(absolute)}/agent-reply`, { text: agentReply });
|
|
11849
|
-
}
|
|
11850
12070
|
const timeoutMs = flagValue(args, "--timeout-ms");
|
|
11851
|
-
const
|
|
12071
|
+
const query = new URLSearchParams({ file: absolute });
|
|
12072
|
+
if (timeoutMs) query.set("timeoutMs", timeoutMs);
|
|
12073
|
+
if (owner) query.set("owner", owner);
|
|
12074
|
+
if (takeover) query.set("takeover", "1");
|
|
11852
12075
|
const onPollSignal = (signal) => {
|
|
11853
12076
|
process.stderr.write(`
|
|
11854
12077
|
${pollInterruptedText(absolute)}
|
|
@@ -11864,10 +12087,25 @@ ${pollInterruptedText(absolute)}
|
|
|
11864
12087
|
narrateTicks: shouldNarratePollWaitTicks({ isTTY: process.stderr.isTTY })
|
|
11865
12088
|
});
|
|
11866
12089
|
try {
|
|
11867
|
-
const
|
|
11868
|
-
|
|
11869
|
-
|
|
12090
|
+
const request = agentReply || takeover ? {
|
|
12091
|
+
method: "POST",
|
|
12092
|
+
headers: { "content-type": "application/json" },
|
|
12093
|
+
body: JSON.stringify(agentReply ? { agent_reply: agentReply } : {})
|
|
12094
|
+
} : {};
|
|
12095
|
+
const response = await fetchJson(`${baseUrl}/api/poll?${query}`, {
|
|
12096
|
+
...request,
|
|
12097
|
+
// Poll ownership is claimed before the response is available. Retrying a transport failure
|
|
12098
|
+
// can leave the first claim alive and make the retry reject itself as LISTENER_ACTIVE.
|
|
12099
|
+
retries: 0,
|
|
12100
|
+
onResponse: (pollResponse) => {
|
|
12101
|
+
if (pollResponse.headers.get(POLL_STATE_HEADER) === "listening") notifyHerdrPollReady();
|
|
12102
|
+
}
|
|
11870
12103
|
});
|
|
12104
|
+
if (response.code === "LISTENER_REPLACED") {
|
|
12105
|
+
throw new AxiError("Lavish Editor poll listener was replaced by a takeover", "LISTENER_REPLACED", [
|
|
12106
|
+
`Re-run lavish-axi poll ${absolute} only if you intend to take over listening`
|
|
12107
|
+
]);
|
|
12108
|
+
}
|
|
11871
12109
|
return createPollOutput({ file: absolute, response, agent: detectInvokingAgent(process.env) });
|
|
11872
12110
|
} finally {
|
|
11873
12111
|
waitReporter?.stop();
|
|
@@ -12726,7 +12964,12 @@ async function serverCommand(args) {
|
|
|
12726
12964
|
}
|
|
12727
12965
|
async function visibleSessions() {
|
|
12728
12966
|
const store = new SessionStore(stateFile());
|
|
12729
|
-
|
|
12967
|
+
const sessions = (await store.listSessions()).filter((session) => session.status !== "ended");
|
|
12968
|
+
const { health } = await findRunningServer(defaultPort());
|
|
12969
|
+
const listeners = new Map(
|
|
12970
|
+
Array.isArray(health?.listeners) ? health.listeners.filter((listener) => listener && typeof listener.key === "string").map((listener) => [listener.key, listener.label || "agent-listener"]) : []
|
|
12971
|
+
);
|
|
12972
|
+
return sessions.map((session) => ({ ...session, listener: listeners.get(session.key) || "none" }));
|
|
12730
12973
|
}
|
|
12731
12974
|
async function assertHtmlFile(file) {
|
|
12732
12975
|
if (!isHtmlPath(file)) {
|
|
@@ -12949,11 +13192,15 @@ function createServerSpawnOptions(logFd = null) {
|
|
|
12949
13192
|
env: { ...process.env, LAVISH_AXI_NO_OPEN: "1" }
|
|
12950
13193
|
};
|
|
12951
13194
|
}
|
|
12952
|
-
async function fetchJson(url, { retries = 0, retryDelayMs = 250 } = {}) {
|
|
13195
|
+
async function fetchJson(url, { retries = 0, retryDelayMs = 250, onResponse = null, method = "GET", headers, body } = {}) {
|
|
12953
13196
|
let response;
|
|
12954
13197
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
12955
13198
|
try {
|
|
12956
|
-
response = await fetch(url
|
|
13199
|
+
response = await fetch(url, {
|
|
13200
|
+
method,
|
|
13201
|
+
...headers ? { headers } : {},
|
|
13202
|
+
...body === void 0 ? {} : { body }
|
|
13203
|
+
});
|
|
12957
13204
|
break;
|
|
12958
13205
|
} catch (error) {
|
|
12959
13206
|
if (error instanceof AxiError) throw error;
|
|
@@ -12963,8 +13210,25 @@ async function fetchJson(url, { retries = 0, retryDelayMs = 250 } = {}) {
|
|
|
12963
13210
|
}
|
|
12964
13211
|
if (!response) throw serverConnectionError();
|
|
12965
13212
|
if (!response.ok) {
|
|
13213
|
+
let payload = null;
|
|
13214
|
+
try {
|
|
13215
|
+
payload = await response.json();
|
|
13216
|
+
} catch {
|
|
13217
|
+
}
|
|
13218
|
+
if (payload?.code) {
|
|
13219
|
+
const holder = payload.code.startsWith("LISTENER_") && payload.holder;
|
|
13220
|
+
const holderDetail = holder && typeof holder.label === "string" && Number.isFinite(holder.age_ms) ? ` (current listener: ${holder.label}; active for ${Math.max(0, holder.age_ms)}ms)` : "";
|
|
13221
|
+
const error = new AxiError(
|
|
13222
|
+
`${payload.error || `Lavish Editor request failed: ${response.status}`}${holderDetail}`,
|
|
13223
|
+
payload.code,
|
|
13224
|
+
["Use --takeover only when you intend to displace the current listener"]
|
|
13225
|
+
);
|
|
13226
|
+
if (holder) Object.assign(error, { holder });
|
|
13227
|
+
throw error;
|
|
13228
|
+
}
|
|
12966
13229
|
throw new AxiError(`Lavish Editor request failed: ${response.status}`, "SERVER_ERROR");
|
|
12967
13230
|
}
|
|
13231
|
+
onResponse?.(response);
|
|
12968
13232
|
try {
|
|
12969
13233
|
return await response.json();
|
|
12970
13234
|
} catch {
|
|
@@ -13060,7 +13324,7 @@ async function readAgentReplyStream(stream) {
|
|
|
13060
13324
|
chunks.push(chunk);
|
|
13061
13325
|
}
|
|
13062
13326
|
const text = Buffer.concat(chunks, bytes).toString("utf8");
|
|
13063
|
-
if (Buffer.byteLength(JSON.stringify({ text })) > AGENT_REPLY_JSON_LIMIT_BYTES) {
|
|
13327
|
+
if (Buffer.byteLength(JSON.stringify({ agent_reply: text })) > AGENT_REPLY_JSON_LIMIT_BYTES) {
|
|
13064
13328
|
throw agentReplyTooLargeError();
|
|
13065
13329
|
}
|
|
13066
13330
|
return text;
|
|
@@ -13143,7 +13407,7 @@ function createTopLevelHelp({ agent = "generic" } = {}) {
|
|
|
13143
13407
|
Usage:
|
|
13144
13408
|
lavish-axi
|
|
13145
13409
|
lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
|
|
13146
|
-
lavish-axi poll <html-file> [--agent-reply "..."] [--agent-reply-file <path>]
|
|
13410
|
+
lavish-axi poll <html-file> [--owner <label>] [--takeover] [--agent-reply "..."] [--agent-reply-file <path>]
|
|
13147
13411
|
lavish-axi end <html-file>
|
|
13148
13412
|
lavish-axi export <html-file> [--out <path>]
|
|
13149
13413
|
lavish-axi share <html-file> [--private | --password <pw>] [--token <t>]
|
|
@@ -13167,9 +13431,11 @@ function createCommandHelp({ agent = "generic" } = {}) {
|
|
|
13167
13431
|
|
|
13168
13432
|
Open or resume a Lavish Editor review session for an HTML artifact. Use --no-open when you need to ensure the server/session exists without opening another browser window. Use --no-gate to skip the open-time layout curtain for this browser open. If the user explicitly ended the session from the browser, this refuses to reopen it and returns guidance instead - pass --reopen to force it open when the user asks for further review or something important needs their visual attention. Sessions ended by the agent (\`lavish-axi end\`) reopen normally without the flag.
|
|
13169
13433
|
`,
|
|
13170
|
-
poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."] [--agent-reply-file <path>]
|
|
13434
|
+
poll: `Usage: lavish-axi poll <html-file> [--owner <label>] [--takeover] [--agent-reply "..."] [--agent-reply-file <path>]
|
|
13435
|
+
|
|
13436
|
+
This command exclusively long-polls for queued user prompts. Pass --owner <label> to make the active listener visible in session listings; --takeover displaces an existing listener, which receives LISTENER_REPLACED. A second poll without --takeover fails with LISTENER_ACTIVE instead of silently returning waiting.
|
|
13171
13437
|
|
|
13172
|
-
This command long-polls indefinitely for queued user prompts. It stays silent while it waits - that is normal, never kill it. Browser-detected layout issues do NOT return this poll: they are filed passively in the user's Layout issues inbox and arrive as an ordinary tag "layout-warnings" prompt only after the user selects them and queues the fixes. Warning lifecycle: an issue stays unresolved and counted while queued, becomes recurring if a newer artifact revision still shows it, and is resolved only after a newer artifact load plus a complete diagnostic pass at the same viewport no longer detects it. A failed or incomplete pass preserves it as unverified rather than clearing it. The only response that arrives without user action is artifact_failures - a fatal failure that made the review surface itself unusable. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. ${pollExecutionGuidance({ agent })} Use --agent-reply after applying prior feedback to display a concise response in Lavish Editor before waiting again. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_HELP_POINTER} Do not combine --agent-reply with --agent-reply-file.
|
|
13438
|
+
This command long-polls indefinitely for queued user prompts. It stays silent while it waits - that is normal, never kill it. Browser-detected layout issues do NOT return this poll: they are filed passively in the user's Layout issues inbox and arrive as an ordinary tag "layout-warnings" prompt only after the user selects them and queues the fixes. Warning lifecycle: an issue stays unresolved and counted while queued, becomes recurring if a newer artifact revision still shows it, and is resolved only after a newer artifact load plus a complete diagnostic pass at the same viewport no longer detects it. A failed or incomplete pass preserves it as unverified rather than clearing it. The only response that arrives without user action is artifact_failures - a fatal failure that made the review surface itself unusable. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. In a Herdr-managed pane, set LAVISH_AXI_HERDR_CHIME=1 to request attention when the poll has entered its waiting state; unset it or use any other value to keep the current silent behavior. Notification failures never interrupt the poll. ${pollExecutionGuidance({ agent })} Use --agent-reply after applying prior feedback to display a concise response in Lavish Editor before waiting again. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_HELP_POINTER} Do not combine --agent-reply with --agent-reply-file.
|
|
13173
13439
|
|
|
13174
13440
|
Examples:
|
|
13175
13441
|
lavish-axi poll report.html --agent-reply "Renamed the payment step."
|
package/dist/server.mjs
CHANGED
|
@@ -51,9 +51,18 @@ function chunkToString(chunk, encoding) {
|
|
|
51
51
|
|
|
52
52
|
// bin/lavish-axi-server.js
|
|
53
53
|
installServerStdioTimestamps();
|
|
54
|
+
var fatalExitScheduled = false;
|
|
55
|
+
function handleFatalServerError(kind, error) {
|
|
56
|
+
const detail = error instanceof Error ? error.stack || error.message : String(error);
|
|
57
|
+
console.error(`[lavish] ${kind}: ${detail}`);
|
|
58
|
+
if (fatalExitScheduled) return;
|
|
59
|
+
fatalExitScheduled = true;
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
setImmediate(() => process.exit(1));
|
|
62
|
+
}
|
|
63
|
+
process.on("uncaughtException", (error) => handleFatalServerError("uncaught exception", error));
|
|
54
64
|
try {
|
|
55
65
|
await import("./cli.mjs");
|
|
56
66
|
} catch (error) {
|
|
57
|
-
|
|
58
|
-
process.exit(1);
|
|
67
|
+
handleFatalServerError("server startup failed", error);
|
|
59
68
|
}
|
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.75",
|
|
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",
|