@wix/himalaya-cli 0.894.0 → 0.895.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +136 -6
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +136 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.895.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"artifactId": "himalaya-cli"
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
-
"falconPackageHash": "
|
|
67
|
+
"falconPackageHash": "9635f5e7048da477ca7b19e75c3c132a21b7425937ed32d85466ec48"
|
|
68
68
|
}
|
|
@@ -536,6 +536,60 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
536
536
|
return n;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
/** Has the turn stored under `userMessageId` already been answered?
|
|
540
|
+
*
|
|
541
|
+
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
542
|
+
* nothing about WHICH turn was answered: a stalled turn sitting behind a later
|
|
543
|
+
* message that did get a reply would read as answered on any count, and expiring
|
|
544
|
+
* it there re-posts a question the server is already holding — the duplicate the
|
|
545
|
+
* latch exists to prevent.
|
|
546
|
+
*
|
|
547
|
+
* So: find the turn, then look at what follows it UNTIL THE NEXT USER TURN. An
|
|
548
|
+
* assistant message in that window answered this one; the next user turn starting
|
|
549
|
+
* first means nothing ever did.
|
|
550
|
+
*
|
|
551
|
+
* A turn that is not in the transcript at all reads as UNANSWERED. The id is the
|
|
552
|
+
* only evidence we have that it exists, and throwing the latch away on a
|
|
553
|
+
* transcript that has not caught up yet is, again, the duplicate.
|
|
554
|
+
*
|
|
555
|
+
* AN EMPTY ASSISTANT ROW IS NOT AN ANSWER — the same rule `countTurns` above
|
|
556
|
+
* states, and for the same reason: the service opens an assistant row before it
|
|
557
|
+
* has written anything into it. Reading that placeholder as the answer is
|
|
558
|
+
* especially bad HERE rather than merely wrong, because the placeholder is
|
|
559
|
+
* exactly what a trigger that was accepted and then lost its response leaves
|
|
560
|
+
* behind. Calling that turn answered retires the latch and the next identical
|
|
561
|
+
* question is persisted a SECOND time — the duplicate this whole latch exists to
|
|
562
|
+
* prevent, produced by the machinery meant to prevent it.
|
|
563
|
+
*
|
|
564
|
+
* Gated on the RAW text, via `messageText`, not on anything bubble-shaped: a
|
|
565
|
+
* widget-only reply (`<<{"widget":…}>>`) is a real answer whose prose is empty,
|
|
566
|
+
* and its non-empty raw text is precisely what separates it from the placeholder.
|
|
567
|
+
*
|
|
568
|
+
* An empty row is SKIPPED rather than treated as a refusal, because the real
|
|
569
|
+
* answer commonly lands in a later row of the same turn. */
|
|
570
|
+
export function turnAnswered(msgs: ConversationMessage[], userMessageId: string): boolean {
|
|
571
|
+
if (!userMessageId) return false;
|
|
572
|
+
let seen = false;
|
|
573
|
+
for (const m of msgs) {
|
|
574
|
+
if (!seen) {
|
|
575
|
+
if (m.id === userMessageId) seen = true;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const role = m.role || "ASSISTANT";
|
|
579
|
+
if (role === "USER") return false; // the next turn began; ours got nothing
|
|
580
|
+
// BOTH halves. An exact role match, because `MessageRole` also carries
|
|
581
|
+
// `UNKNOWN` and the wire may grow more (tool, citation, system) — a row that
|
|
582
|
+
// is merely not-USER is not an answer, and treating it as one retires the
|
|
583
|
+
// latch and re-posts the question. And non-empty raw text, because the service
|
|
584
|
+
// opens an assistant row before writing into it. `countTurns` above matches on
|
|
585
|
+
// exactly these two, and the two scanners read the same transcript: if they
|
|
586
|
+
// disagree, one of them ends the turn while the other leaves the ladder
|
|
587
|
+
// running at the fast cadence.
|
|
588
|
+
if (role === "ASSISTANT" && messageText(m).trim()) return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
539
593
|
// ---------------------------------------------------------------------------
|
|
540
594
|
// Local recent-conversation list (himalaya.storage)
|
|
541
595
|
// ---------------------------------------------------------------------------
|
|
@@ -694,7 +748,15 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
694
748
|
* a transient recovery hint about one failed attempt, not something that
|
|
695
749
|
* should survive a reload or be persisted with the transcript. Keyed by
|
|
696
750
|
* conversation AND content so it can only ever re-trigger the exact turn it
|
|
697
|
-
* was recorded for.
|
|
751
|
+
* was recorded for.
|
|
752
|
+
*
|
|
753
|
+
* It also has to EXPIRE, and cannot do so on its own clock. A trigger whose
|
|
754
|
+
* RESPONSE was lost — a timeout, a reset — failed here while succeeding at the
|
|
755
|
+
* service, so the turn runs and the latch is left naming something already
|
|
756
|
+
* answered. Re-triggering that does not duplicate anything; it drops the
|
|
757
|
+
* owner's next identical question instead, which is quieter and worse. The
|
|
758
|
+
* expiry is therefore a question asked of the SERVER at reuse time — see
|
|
759
|
+
* `turnAnswered` — rather than anything inferred from local counts. */
|
|
698
760
|
let stalledTurn: { convId: string; content: string; userMessageId: string } | null = null;
|
|
699
761
|
|
|
700
762
|
/** Refuse a send that is already on its way. State half + latch half. */
|
|
@@ -802,6 +864,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
802
864
|
// after it fell back to a full send, duplicating the very turn the latch
|
|
803
865
|
// exists to protect.
|
|
804
866
|
let retriedId = "";
|
|
867
|
+
// The latch as it stood when this send started, and whether it names THIS
|
|
868
|
+
// send's turn. Out here for the same reason `retriedId` is: the catch has to
|
|
869
|
+
// tell "the latch I was working on" from "a latch that belongs to another
|
|
870
|
+
// turn", and clearing the second is how an unrelated failure destroys a turn
|
|
871
|
+
// the owner is still owed an answer to.
|
|
872
|
+
let latch: typeof stalledTurn = null;
|
|
873
|
+
let supersedes = false;
|
|
805
874
|
try {
|
|
806
875
|
const ensured = await ensureConversation(s);
|
|
807
876
|
convId = ensured.convId;
|
|
@@ -816,9 +885,34 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
816
885
|
// `createConversation`). Sending without it defaulted the turn to
|
|
817
886
|
// `ARIA_ORIGIN`, so a consumer that configured its own origin created the
|
|
818
887
|
// conversation under one name and posted into it under another.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
888
|
+
//
|
|
889
|
+
// Two questions, deliberately separate. Does the latch name the turn THIS
|
|
890
|
+
// send is about (`supersedes`)? And if so, is that turn still worth
|
|
891
|
+
// re-triggering (`stalled`)?
|
|
892
|
+
latch = stalledTurn;
|
|
893
|
+
supersedes = !!latch && latch.convId === convId && latch.content === content;
|
|
894
|
+
// …and only while it is still UNANSWERED, which is ASKED rather than
|
|
895
|
+
// inferred. A trigger whose response was lost failed here while succeeding
|
|
896
|
+
// at the service, so the answer arrives anyway and the id now names a
|
|
897
|
+
// finished turn; re-triggering it there does not duplicate anything, it
|
|
898
|
+
// drops the owner's new question instead and re-answers the old one — a
|
|
899
|
+
// duplicate is at least visible in the transcript, a dropped question is not.
|
|
900
|
+
//
|
|
901
|
+
// One extra GET, on the retry path only. Local state cannot stand in for it:
|
|
902
|
+
// a COUNT of assistant replies cannot say which turn was answered, and
|
|
903
|
+
// nothing arms the poll ladder after a failed send, so on the very screen
|
|
904
|
+
// this protects there may be no tick to advance a count in the first place.
|
|
905
|
+
//
|
|
906
|
+
// A read that FAILS leaves the latch alone. Unknown has to mean "still
|
|
907
|
+
// stalled": that is the shipped behaviour, and the conditions where this read
|
|
908
|
+
// fails are the same flaky ones that stalled the turn.
|
|
909
|
+
let stalled = supersedes ? latch : null;
|
|
910
|
+
if (stalled) {
|
|
911
|
+
try {
|
|
912
|
+
const seen = messagesOf(await getConversationMessages(convId));
|
|
913
|
+
if (turnAnswered(seen, stalled.userMessageId)) stalled = null;
|
|
914
|
+
} catch { /* unknown — keep the latch, see above */ }
|
|
915
|
+
}
|
|
822
916
|
if (stalled) {
|
|
823
917
|
retriedId = stalled.userMessageId;
|
|
824
918
|
await triggerAssistantMessage(convId, stalled.userMessageId, { origin });
|
|
@@ -833,7 +927,25 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
833
927
|
// — the turn has been triggered — so leaving `retriedId` set let the catch
|
|
834
928
|
// below rebuild a latch for a turn already being answered, and the next
|
|
835
929
|
// identical submission triggered it a second time.
|
|
836
|
-
|
|
930
|
+
//
|
|
931
|
+
// `supersedes`, not unconditional: clearing on ANY success loses a turn that
|
|
932
|
+
// is still stranded across an UNRELATED message. The owner whose question
|
|
933
|
+
// failed to trigger asks something else, it goes through, they come back to
|
|
934
|
+
// the first question — and that retry re-posts it, which is the exact
|
|
935
|
+
// duplicate the latch exists to prevent, one message later. A turn nothing
|
|
936
|
+
// has settled stays latched; `after` above is what eventually retires it.
|
|
937
|
+
//
|
|
938
|
+
// Still one slot, so an unrelated send that FAILS after storing its own turn
|
|
939
|
+
// displaces the older one. Narrowing that wants a queue rather than a latch,
|
|
940
|
+
// and no reported flow reaches it.
|
|
941
|
+
//
|
|
942
|
+
// Compared by IDENTITY, not just `supersedes`: `latch` was read before the
|
|
943
|
+
// awaits above, and actions interleave at every await in this worker (the
|
|
944
|
+
// `creating` latch says the same). `duplicate()` only refuses IDENTICAL
|
|
945
|
+
// content, so a second message can be in flight alongside this one and store
|
|
946
|
+
// its own latch while we wait — clearing on the stale answer would throw
|
|
947
|
+
// away a turn this send never touched.
|
|
948
|
+
if (supersedes && stalledTurn === latch) stalledTurn = null;
|
|
837
949
|
retriedId = "";
|
|
838
950
|
// A conversation minted just now starts empty, so the marks taken above
|
|
839
951
|
// from the PREVIOUS conversation's counts would be ahead of it and its
|
|
@@ -862,9 +974,27 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
862
974
|
// whose trigger failed (the id arrives on the error), or a RE-TRIGGER of
|
|
863
975
|
// an already-stored turn that failed again (no id on the error, because
|
|
864
976
|
// nothing new was stored — the one we were retrying is still the answer).
|
|
977
|
+
//
|
|
978
|
+
// "Any other failure clears the latch" holds only for a latch THIS SEND
|
|
979
|
+
// OWNED. Clearing unconditionally destroys an older, still-stranded turn
|
|
980
|
+
// that this same send had just correctly preserved on the success path
|
|
981
|
+
// above — and there is a whole class of failures with nothing stored:
|
|
982
|
+
// a step-1 failure, a failed create, and (with no `o.poll`) a `busyLoop`
|
|
983
|
+
// transcript read that throws AFTER delivery, where `retriedId` has already
|
|
984
|
+
// been cleared so `keptId` is empty. The owner returns to the first question
|
|
985
|
+
// and it is posted twice: the same defect as the success path, one layer
|
|
986
|
+
// down and under the same flaky conditions.
|
|
987
|
+
//
|
|
988
|
+
// Identity again, and for the reason spelled out on the success path: what
|
|
989
|
+
// is in the slot now may belong to a send that interleaved with this one.
|
|
865
990
|
const storedId = (e as { storedUserMessageId?: unknown })?.storedUserMessageId;
|
|
866
991
|
const keptId = typeof storedId === "string" && storedId ? storedId : retriedId;
|
|
867
|
-
stalledTurn = convId && keptId
|
|
992
|
+
stalledTurn = convId && keptId
|
|
993
|
+
? { convId, content, userMessageId: keptId }
|
|
994
|
+
// Nothing stored by this attempt. Drop the latch only if it is the one
|
|
995
|
+
// this send was working on and is still the one we captured; otherwise it
|
|
996
|
+
// belongs to another turn and must survive.
|
|
997
|
+
: (supersedes && stalledTurn === latch ? null : stalledTurn);
|
|
868
998
|
return {
|
|
869
999
|
...base, status: "ready", _pending: "",
|
|
870
1000
|
// `_siteId` moves with the id or neither does: a conversation remembered
|
|
@@ -536,6 +536,60 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
536
536
|
return n;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
/** Has the turn stored under `userMessageId` already been answered?
|
|
540
|
+
*
|
|
541
|
+
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
542
|
+
* nothing about WHICH turn was answered: a stalled turn sitting behind a later
|
|
543
|
+
* message that did get a reply would read as answered on any count, and expiring
|
|
544
|
+
* it there re-posts a question the server is already holding — the duplicate the
|
|
545
|
+
* latch exists to prevent.
|
|
546
|
+
*
|
|
547
|
+
* So: find the turn, then look at what follows it UNTIL THE NEXT USER TURN. An
|
|
548
|
+
* assistant message in that window answered this one; the next user turn starting
|
|
549
|
+
* first means nothing ever did.
|
|
550
|
+
*
|
|
551
|
+
* A turn that is not in the transcript at all reads as UNANSWERED. The id is the
|
|
552
|
+
* only evidence we have that it exists, and throwing the latch away on a
|
|
553
|
+
* transcript that has not caught up yet is, again, the duplicate.
|
|
554
|
+
*
|
|
555
|
+
* AN EMPTY ASSISTANT ROW IS NOT AN ANSWER — the same rule `countTurns` above
|
|
556
|
+
* states, and for the same reason: the service opens an assistant row before it
|
|
557
|
+
* has written anything into it. Reading that placeholder as the answer is
|
|
558
|
+
* especially bad HERE rather than merely wrong, because the placeholder is
|
|
559
|
+
* exactly what a trigger that was accepted and then lost its response leaves
|
|
560
|
+
* behind. Calling that turn answered retires the latch and the next identical
|
|
561
|
+
* question is persisted a SECOND time — the duplicate this whole latch exists to
|
|
562
|
+
* prevent, produced by the machinery meant to prevent it.
|
|
563
|
+
*
|
|
564
|
+
* Gated on the RAW text, via `messageText`, not on anything bubble-shaped: a
|
|
565
|
+
* widget-only reply (`<<{"widget":…}>>`) is a real answer whose prose is empty,
|
|
566
|
+
* and its non-empty raw text is precisely what separates it from the placeholder.
|
|
567
|
+
*
|
|
568
|
+
* An empty row is SKIPPED rather than treated as a refusal, because the real
|
|
569
|
+
* answer commonly lands in a later row of the same turn. */
|
|
570
|
+
export function turnAnswered(msgs: ConversationMessage[], userMessageId: string): boolean {
|
|
571
|
+
if (!userMessageId) return false;
|
|
572
|
+
let seen = false;
|
|
573
|
+
for (const m of msgs) {
|
|
574
|
+
if (!seen) {
|
|
575
|
+
if (m.id === userMessageId) seen = true;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const role = m.role || "ASSISTANT";
|
|
579
|
+
if (role === "USER") return false; // the next turn began; ours got nothing
|
|
580
|
+
// BOTH halves. An exact role match, because `MessageRole` also carries
|
|
581
|
+
// `UNKNOWN` and the wire may grow more (tool, citation, system) — a row that
|
|
582
|
+
// is merely not-USER is not an answer, and treating it as one retires the
|
|
583
|
+
// latch and re-posts the question. And non-empty raw text, because the service
|
|
584
|
+
// opens an assistant row before writing into it. `countTurns` above matches on
|
|
585
|
+
// exactly these two, and the two scanners read the same transcript: if they
|
|
586
|
+
// disagree, one of them ends the turn while the other leaves the ladder
|
|
587
|
+
// running at the fast cadence.
|
|
588
|
+
if (role === "ASSISTANT" && messageText(m).trim()) return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
539
593
|
// ---------------------------------------------------------------------------
|
|
540
594
|
// Local recent-conversation list (himalaya.storage)
|
|
541
595
|
// ---------------------------------------------------------------------------
|
|
@@ -694,7 +748,15 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
694
748
|
* a transient recovery hint about one failed attempt, not something that
|
|
695
749
|
* should survive a reload or be persisted with the transcript. Keyed by
|
|
696
750
|
* conversation AND content so it can only ever re-trigger the exact turn it
|
|
697
|
-
* was recorded for.
|
|
751
|
+
* was recorded for.
|
|
752
|
+
*
|
|
753
|
+
* It also has to EXPIRE, and cannot do so on its own clock. A trigger whose
|
|
754
|
+
* RESPONSE was lost — a timeout, a reset — failed here while succeeding at the
|
|
755
|
+
* service, so the turn runs and the latch is left naming something already
|
|
756
|
+
* answered. Re-triggering that does not duplicate anything; it drops the
|
|
757
|
+
* owner's next identical question instead, which is quieter and worse. The
|
|
758
|
+
* expiry is therefore a question asked of the SERVER at reuse time — see
|
|
759
|
+
* `turnAnswered` — rather than anything inferred from local counts. */
|
|
698
760
|
let stalledTurn: { convId: string; content: string; userMessageId: string } | null = null;
|
|
699
761
|
|
|
700
762
|
/** Refuse a send that is already on its way. State half + latch half. */
|
|
@@ -802,6 +864,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
802
864
|
// after it fell back to a full send, duplicating the very turn the latch
|
|
803
865
|
// exists to protect.
|
|
804
866
|
let retriedId = "";
|
|
867
|
+
// The latch as it stood when this send started, and whether it names THIS
|
|
868
|
+
// send's turn. Out here for the same reason `retriedId` is: the catch has to
|
|
869
|
+
// tell "the latch I was working on" from "a latch that belongs to another
|
|
870
|
+
// turn", and clearing the second is how an unrelated failure destroys a turn
|
|
871
|
+
// the owner is still owed an answer to.
|
|
872
|
+
let latch: typeof stalledTurn = null;
|
|
873
|
+
let supersedes = false;
|
|
805
874
|
try {
|
|
806
875
|
const ensured = await ensureConversation(s);
|
|
807
876
|
convId = ensured.convId;
|
|
@@ -816,9 +885,34 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
816
885
|
// `createConversation`). Sending without it defaulted the turn to
|
|
817
886
|
// `ARIA_ORIGIN`, so a consumer that configured its own origin created the
|
|
818
887
|
// conversation under one name and posted into it under another.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
888
|
+
//
|
|
889
|
+
// Two questions, deliberately separate. Does the latch name the turn THIS
|
|
890
|
+
// send is about (`supersedes`)? And if so, is that turn still worth
|
|
891
|
+
// re-triggering (`stalled`)?
|
|
892
|
+
latch = stalledTurn;
|
|
893
|
+
supersedes = !!latch && latch.convId === convId && latch.content === content;
|
|
894
|
+
// …and only while it is still UNANSWERED, which is ASKED rather than
|
|
895
|
+
// inferred. A trigger whose response was lost failed here while succeeding
|
|
896
|
+
// at the service, so the answer arrives anyway and the id now names a
|
|
897
|
+
// finished turn; re-triggering it there does not duplicate anything, it
|
|
898
|
+
// drops the owner's new question instead and re-answers the old one — a
|
|
899
|
+
// duplicate is at least visible in the transcript, a dropped question is not.
|
|
900
|
+
//
|
|
901
|
+
// One extra GET, on the retry path only. Local state cannot stand in for it:
|
|
902
|
+
// a COUNT of assistant replies cannot say which turn was answered, and
|
|
903
|
+
// nothing arms the poll ladder after a failed send, so on the very screen
|
|
904
|
+
// this protects there may be no tick to advance a count in the first place.
|
|
905
|
+
//
|
|
906
|
+
// A read that FAILS leaves the latch alone. Unknown has to mean "still
|
|
907
|
+
// stalled": that is the shipped behaviour, and the conditions where this read
|
|
908
|
+
// fails are the same flaky ones that stalled the turn.
|
|
909
|
+
let stalled = supersedes ? latch : null;
|
|
910
|
+
if (stalled) {
|
|
911
|
+
try {
|
|
912
|
+
const seen = messagesOf(await getConversationMessages(convId));
|
|
913
|
+
if (turnAnswered(seen, stalled.userMessageId)) stalled = null;
|
|
914
|
+
} catch { /* unknown — keep the latch, see above */ }
|
|
915
|
+
}
|
|
822
916
|
if (stalled) {
|
|
823
917
|
retriedId = stalled.userMessageId;
|
|
824
918
|
await triggerAssistantMessage(convId, stalled.userMessageId, { origin });
|
|
@@ -833,7 +927,25 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
833
927
|
// — the turn has been triggered — so leaving `retriedId` set let the catch
|
|
834
928
|
// below rebuild a latch for a turn already being answered, and the next
|
|
835
929
|
// identical submission triggered it a second time.
|
|
836
|
-
|
|
930
|
+
//
|
|
931
|
+
// `supersedes`, not unconditional: clearing on ANY success loses a turn that
|
|
932
|
+
// is still stranded across an UNRELATED message. The owner whose question
|
|
933
|
+
// failed to trigger asks something else, it goes through, they come back to
|
|
934
|
+
// the first question — and that retry re-posts it, which is the exact
|
|
935
|
+
// duplicate the latch exists to prevent, one message later. A turn nothing
|
|
936
|
+
// has settled stays latched; `after` above is what eventually retires it.
|
|
937
|
+
//
|
|
938
|
+
// Still one slot, so an unrelated send that FAILS after storing its own turn
|
|
939
|
+
// displaces the older one. Narrowing that wants a queue rather than a latch,
|
|
940
|
+
// and no reported flow reaches it.
|
|
941
|
+
//
|
|
942
|
+
// Compared by IDENTITY, not just `supersedes`: `latch` was read before the
|
|
943
|
+
// awaits above, and actions interleave at every await in this worker (the
|
|
944
|
+
// `creating` latch says the same). `duplicate()` only refuses IDENTICAL
|
|
945
|
+
// content, so a second message can be in flight alongside this one and store
|
|
946
|
+
// its own latch while we wait — clearing on the stale answer would throw
|
|
947
|
+
// away a turn this send never touched.
|
|
948
|
+
if (supersedes && stalledTurn === latch) stalledTurn = null;
|
|
837
949
|
retriedId = "";
|
|
838
950
|
// A conversation minted just now starts empty, so the marks taken above
|
|
839
951
|
// from the PREVIOUS conversation's counts would be ahead of it and its
|
|
@@ -862,9 +974,27 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
862
974
|
// whose trigger failed (the id arrives on the error), or a RE-TRIGGER of
|
|
863
975
|
// an already-stored turn that failed again (no id on the error, because
|
|
864
976
|
// nothing new was stored — the one we were retrying is still the answer).
|
|
977
|
+
//
|
|
978
|
+
// "Any other failure clears the latch" holds only for a latch THIS SEND
|
|
979
|
+
// OWNED. Clearing unconditionally destroys an older, still-stranded turn
|
|
980
|
+
// that this same send had just correctly preserved on the success path
|
|
981
|
+
// above — and there is a whole class of failures with nothing stored:
|
|
982
|
+
// a step-1 failure, a failed create, and (with no `o.poll`) a `busyLoop`
|
|
983
|
+
// transcript read that throws AFTER delivery, where `retriedId` has already
|
|
984
|
+
// been cleared so `keptId` is empty. The owner returns to the first question
|
|
985
|
+
// and it is posted twice: the same defect as the success path, one layer
|
|
986
|
+
// down and under the same flaky conditions.
|
|
987
|
+
//
|
|
988
|
+
// Identity again, and for the reason spelled out on the success path: what
|
|
989
|
+
// is in the slot now may belong to a send that interleaved with this one.
|
|
865
990
|
const storedId = (e as { storedUserMessageId?: unknown })?.storedUserMessageId;
|
|
866
991
|
const keptId = typeof storedId === "string" && storedId ? storedId : retriedId;
|
|
867
|
-
stalledTurn = convId && keptId
|
|
992
|
+
stalledTurn = convId && keptId
|
|
993
|
+
? { convId, content, userMessageId: keptId }
|
|
994
|
+
// Nothing stored by this attempt. Drop the latch only if it is the one
|
|
995
|
+
// this send was working on and is still the one we captured; otherwise it
|
|
996
|
+
// belongs to another turn and must survive.
|
|
997
|
+
: (supersedes && stalledTurn === latch ? null : stalledTurn);
|
|
868
998
|
return {
|
|
869
999
|
...base, status: "ready", _pending: "",
|
|
870
1000
|
// `_siteId` moves with the id or neither does: a conversation remembered
|
|
@@ -536,6 +536,60 @@ export function countTurns(msgs: ConversationMessage[], role: string): number {
|
|
|
536
536
|
return n;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
/** Has the turn stored under `userMessageId` already been answered?
|
|
540
|
+
*
|
|
541
|
+
* Per-TURN, and that is the entire point. A COUNT of assistant replies says
|
|
542
|
+
* nothing about WHICH turn was answered: a stalled turn sitting behind a later
|
|
543
|
+
* message that did get a reply would read as answered on any count, and expiring
|
|
544
|
+
* it there re-posts a question the server is already holding — the duplicate the
|
|
545
|
+
* latch exists to prevent.
|
|
546
|
+
*
|
|
547
|
+
* So: find the turn, then look at what follows it UNTIL THE NEXT USER TURN. An
|
|
548
|
+
* assistant message in that window answered this one; the next user turn starting
|
|
549
|
+
* first means nothing ever did.
|
|
550
|
+
*
|
|
551
|
+
* A turn that is not in the transcript at all reads as UNANSWERED. The id is the
|
|
552
|
+
* only evidence we have that it exists, and throwing the latch away on a
|
|
553
|
+
* transcript that has not caught up yet is, again, the duplicate.
|
|
554
|
+
*
|
|
555
|
+
* AN EMPTY ASSISTANT ROW IS NOT AN ANSWER — the same rule `countTurns` above
|
|
556
|
+
* states, and for the same reason: the service opens an assistant row before it
|
|
557
|
+
* has written anything into it. Reading that placeholder as the answer is
|
|
558
|
+
* especially bad HERE rather than merely wrong, because the placeholder is
|
|
559
|
+
* exactly what a trigger that was accepted and then lost its response leaves
|
|
560
|
+
* behind. Calling that turn answered retires the latch and the next identical
|
|
561
|
+
* question is persisted a SECOND time — the duplicate this whole latch exists to
|
|
562
|
+
* prevent, produced by the machinery meant to prevent it.
|
|
563
|
+
*
|
|
564
|
+
* Gated on the RAW text, via `messageText`, not on anything bubble-shaped: a
|
|
565
|
+
* widget-only reply (`<<{"widget":…}>>`) is a real answer whose prose is empty,
|
|
566
|
+
* and its non-empty raw text is precisely what separates it from the placeholder.
|
|
567
|
+
*
|
|
568
|
+
* An empty row is SKIPPED rather than treated as a refusal, because the real
|
|
569
|
+
* answer commonly lands in a later row of the same turn. */
|
|
570
|
+
export function turnAnswered(msgs: ConversationMessage[], userMessageId: string): boolean {
|
|
571
|
+
if (!userMessageId) return false;
|
|
572
|
+
let seen = false;
|
|
573
|
+
for (const m of msgs) {
|
|
574
|
+
if (!seen) {
|
|
575
|
+
if (m.id === userMessageId) seen = true;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const role = m.role || "ASSISTANT";
|
|
579
|
+
if (role === "USER") return false; // the next turn began; ours got nothing
|
|
580
|
+
// BOTH halves. An exact role match, because `MessageRole` also carries
|
|
581
|
+
// `UNKNOWN` and the wire may grow more (tool, citation, system) — a row that
|
|
582
|
+
// is merely not-USER is not an answer, and treating it as one retires the
|
|
583
|
+
// latch and re-posts the question. And non-empty raw text, because the service
|
|
584
|
+
// opens an assistant row before writing into it. `countTurns` above matches on
|
|
585
|
+
// exactly these two, and the two scanners read the same transcript: if they
|
|
586
|
+
// disagree, one of them ends the turn while the other leaves the ladder
|
|
587
|
+
// running at the fast cadence.
|
|
588
|
+
if (role === "ASSISTANT" && messageText(m).trim()) return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
539
593
|
// ---------------------------------------------------------------------------
|
|
540
594
|
// Local recent-conversation list (himalaya.storage)
|
|
541
595
|
// ---------------------------------------------------------------------------
|
|
@@ -694,7 +748,15 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
694
748
|
* a transient recovery hint about one failed attempt, not something that
|
|
695
749
|
* should survive a reload or be persisted with the transcript. Keyed by
|
|
696
750
|
* conversation AND content so it can only ever re-trigger the exact turn it
|
|
697
|
-
* was recorded for.
|
|
751
|
+
* was recorded for.
|
|
752
|
+
*
|
|
753
|
+
* It also has to EXPIRE, and cannot do so on its own clock. A trigger whose
|
|
754
|
+
* RESPONSE was lost — a timeout, a reset — failed here while succeeding at the
|
|
755
|
+
* service, so the turn runs and the latch is left naming something already
|
|
756
|
+
* answered. Re-triggering that does not duplicate anything; it drops the
|
|
757
|
+
* owner's next identical question instead, which is quieter and worse. The
|
|
758
|
+
* expiry is therefore a question asked of the SERVER at reuse time — see
|
|
759
|
+
* `turnAnswered` — rather than anything inferred from local counts. */
|
|
698
760
|
let stalledTurn: { convId: string; content: string; userMessageId: string } | null = null;
|
|
699
761
|
|
|
700
762
|
/** Refuse a send that is already on its way. State half + latch half. */
|
|
@@ -802,6 +864,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
802
864
|
// after it fell back to a full send, duplicating the very turn the latch
|
|
803
865
|
// exists to protect.
|
|
804
866
|
let retriedId = "";
|
|
867
|
+
// The latch as it stood when this send started, and whether it names THIS
|
|
868
|
+
// send's turn. Out here for the same reason `retriedId` is: the catch has to
|
|
869
|
+
// tell "the latch I was working on" from "a latch that belongs to another
|
|
870
|
+
// turn", and clearing the second is how an unrelated failure destroys a turn
|
|
871
|
+
// the owner is still owed an answer to.
|
|
872
|
+
let latch: typeof stalledTurn = null;
|
|
873
|
+
let supersedes = false;
|
|
805
874
|
try {
|
|
806
875
|
const ensured = await ensureConversation(s);
|
|
807
876
|
convId = ensured.convId;
|
|
@@ -816,9 +885,34 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
816
885
|
// `createConversation`). Sending without it defaulted the turn to
|
|
817
886
|
// `ARIA_ORIGIN`, so a consumer that configured its own origin created the
|
|
818
887
|
// conversation under one name and posted into it under another.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
888
|
+
//
|
|
889
|
+
// Two questions, deliberately separate. Does the latch name the turn THIS
|
|
890
|
+
// send is about (`supersedes`)? And if so, is that turn still worth
|
|
891
|
+
// re-triggering (`stalled`)?
|
|
892
|
+
latch = stalledTurn;
|
|
893
|
+
supersedes = !!latch && latch.convId === convId && latch.content === content;
|
|
894
|
+
// …and only while it is still UNANSWERED, which is ASKED rather than
|
|
895
|
+
// inferred. A trigger whose response was lost failed here while succeeding
|
|
896
|
+
// at the service, so the answer arrives anyway and the id now names a
|
|
897
|
+
// finished turn; re-triggering it there does not duplicate anything, it
|
|
898
|
+
// drops the owner's new question instead and re-answers the old one — a
|
|
899
|
+
// duplicate is at least visible in the transcript, a dropped question is not.
|
|
900
|
+
//
|
|
901
|
+
// One extra GET, on the retry path only. Local state cannot stand in for it:
|
|
902
|
+
// a COUNT of assistant replies cannot say which turn was answered, and
|
|
903
|
+
// nothing arms the poll ladder after a failed send, so on the very screen
|
|
904
|
+
// this protects there may be no tick to advance a count in the first place.
|
|
905
|
+
//
|
|
906
|
+
// A read that FAILS leaves the latch alone. Unknown has to mean "still
|
|
907
|
+
// stalled": that is the shipped behaviour, and the conditions where this read
|
|
908
|
+
// fails are the same flaky ones that stalled the turn.
|
|
909
|
+
let stalled = supersedes ? latch : null;
|
|
910
|
+
if (stalled) {
|
|
911
|
+
try {
|
|
912
|
+
const seen = messagesOf(await getConversationMessages(convId));
|
|
913
|
+
if (turnAnswered(seen, stalled.userMessageId)) stalled = null;
|
|
914
|
+
} catch { /* unknown — keep the latch, see above */ }
|
|
915
|
+
}
|
|
822
916
|
if (stalled) {
|
|
823
917
|
retriedId = stalled.userMessageId;
|
|
824
918
|
await triggerAssistantMessage(convId, stalled.userMessageId, { origin });
|
|
@@ -833,7 +927,25 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
833
927
|
// — the turn has been triggered — so leaving `retriedId` set let the catch
|
|
834
928
|
// below rebuild a latch for a turn already being answered, and the next
|
|
835
929
|
// identical submission triggered it a second time.
|
|
836
|
-
|
|
930
|
+
//
|
|
931
|
+
// `supersedes`, not unconditional: clearing on ANY success loses a turn that
|
|
932
|
+
// is still stranded across an UNRELATED message. The owner whose question
|
|
933
|
+
// failed to trigger asks something else, it goes through, they come back to
|
|
934
|
+
// the first question — and that retry re-posts it, which is the exact
|
|
935
|
+
// duplicate the latch exists to prevent, one message later. A turn nothing
|
|
936
|
+
// has settled stays latched; `after` above is what eventually retires it.
|
|
937
|
+
//
|
|
938
|
+
// Still one slot, so an unrelated send that FAILS after storing its own turn
|
|
939
|
+
// displaces the older one. Narrowing that wants a queue rather than a latch,
|
|
940
|
+
// and no reported flow reaches it.
|
|
941
|
+
//
|
|
942
|
+
// Compared by IDENTITY, not just `supersedes`: `latch` was read before the
|
|
943
|
+
// awaits above, and actions interleave at every await in this worker (the
|
|
944
|
+
// `creating` latch says the same). `duplicate()` only refuses IDENTICAL
|
|
945
|
+
// content, so a second message can be in flight alongside this one and store
|
|
946
|
+
// its own latch while we wait — clearing on the stale answer would throw
|
|
947
|
+
// away a turn this send never touched.
|
|
948
|
+
if (supersedes && stalledTurn === latch) stalledTurn = null;
|
|
837
949
|
retriedId = "";
|
|
838
950
|
// A conversation minted just now starts empty, so the marks taken above
|
|
839
951
|
// from the PREVIOUS conversation's counts would be ahead of it and its
|
|
@@ -862,9 +974,27 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
862
974
|
// whose trigger failed (the id arrives on the error), or a RE-TRIGGER of
|
|
863
975
|
// an already-stored turn that failed again (no id on the error, because
|
|
864
976
|
// nothing new was stored — the one we were retrying is still the answer).
|
|
977
|
+
//
|
|
978
|
+
// "Any other failure clears the latch" holds only for a latch THIS SEND
|
|
979
|
+
// OWNED. Clearing unconditionally destroys an older, still-stranded turn
|
|
980
|
+
// that this same send had just correctly preserved on the success path
|
|
981
|
+
// above — and there is a whole class of failures with nothing stored:
|
|
982
|
+
// a step-1 failure, a failed create, and (with no `o.poll`) a `busyLoop`
|
|
983
|
+
// transcript read that throws AFTER delivery, where `retriedId` has already
|
|
984
|
+
// been cleared so `keptId` is empty. The owner returns to the first question
|
|
985
|
+
// and it is posted twice: the same defect as the success path, one layer
|
|
986
|
+
// down and under the same flaky conditions.
|
|
987
|
+
//
|
|
988
|
+
// Identity again, and for the reason spelled out on the success path: what
|
|
989
|
+
// is in the slot now may belong to a send that interleaved with this one.
|
|
865
990
|
const storedId = (e as { storedUserMessageId?: unknown })?.storedUserMessageId;
|
|
866
991
|
const keptId = typeof storedId === "string" && storedId ? storedId : retriedId;
|
|
867
|
-
stalledTurn = convId && keptId
|
|
992
|
+
stalledTurn = convId && keptId
|
|
993
|
+
? { convId, content, userMessageId: keptId }
|
|
994
|
+
// Nothing stored by this attempt. Drop the latch only if it is the one
|
|
995
|
+
// this send was working on and is still the one we captured; otherwise it
|
|
996
|
+
// belongs to another turn and must survive.
|
|
997
|
+
: (supersedes && stalledTurn === latch ? null : stalledTurn);
|
|
868
998
|
return {
|
|
869
999
|
...base, status: "ready", _pending: "",
|
|
870
1000
|
// `_siteId` moves with the id or neither does: a conversation remembered
|