@yeaft/webchat-agent 0.1.891 → 0.1.892
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 +1 -1
- package/yeaft/conversation/persist.js +40 -0
- package/yeaft/web-bridge.js +123 -12
package/package.json
CHANGED
|
@@ -966,6 +966,46 @@ export class ConversationStore {
|
|
|
966
966
|
};
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Load messages strictly after a seq cursor, ordered by seq ascending.
|
|
971
|
+
* Used by the web client to fetch "everything new since my latest known
|
|
972
|
+
* message" when re-entering a session — the delta path.
|
|
973
|
+
*
|
|
974
|
+
* @param {string} sessionId
|
|
975
|
+
* @param {number|null} afterSeq — exclusive lower bound
|
|
976
|
+
* @param {{ limit?: number }} [opts]
|
|
977
|
+
* @returns {{ messages: object[], latestSeq: number|null }}
|
|
978
|
+
*/
|
|
979
|
+
loadAfterSeqByGroup(sessionId, afterSeq, opts = {}) {
|
|
980
|
+
if (!sessionId) return { messages: [], latestSeq: null };
|
|
981
|
+
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : 500;
|
|
982
|
+
const cutoff = Number.isFinite(afterSeq) && afterSeq >= 0 ? afterSeq : null;
|
|
983
|
+
if (cutoff === null) return { messages: [], latestSeq: null };
|
|
984
|
+
const hot = this.#loadGroupHotMessages(sessionId);
|
|
985
|
+
const cold = this.#loadGroupColdMessages(sessionId);
|
|
986
|
+
const all = [...cold, ...hot].sort(compareMessagesBySeq);
|
|
987
|
+
const after = all.filter((m) => {
|
|
988
|
+
if (!m || m.sessionId !== sessionId) return false;
|
|
989
|
+
const seq = parseSeqFromId(m.id);
|
|
990
|
+
return Number.isFinite(seq) && seq > cutoff;
|
|
991
|
+
});
|
|
992
|
+
const sliced = pairSanitize(after.slice(0, limit));
|
|
993
|
+
const lastSeq = sliced.length ? parseSeqFromId(sliced[sliced.length - 1].id) : null;
|
|
994
|
+
return { messages: sliced, latestSeq: Number.isFinite(lastSeq) ? lastSeq : null };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Convenience: extract the numeric seq embedded in a message id.
|
|
999
|
+
*
|
|
1000
|
+
* @param {string} messageId
|
|
1001
|
+
* @returns {number|null}
|
|
1002
|
+
*/
|
|
1003
|
+
getMessageSeqById(messageId) {
|
|
1004
|
+
if (!messageId || typeof messageId !== 'string') return null;
|
|
1005
|
+
const seq = parseSeqFromId(messageId);
|
|
1006
|
+
return Number.isFinite(seq) ? seq : null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
969
1009
|
/**
|
|
970
1010
|
* Count hot messages.
|
|
971
1011
|
*
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1928,12 +1928,12 @@ function handleEngineEvent(event, hctx) {
|
|
|
1928
1928
|
is_error: event.isError || false,
|
|
1929
1929
|
}],
|
|
1930
1930
|
}, envelope);
|
|
1931
|
-
// Tool finished.
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1934
|
-
//
|
|
1935
|
-
// '
|
|
1936
|
-
|
|
1931
|
+
// Tool finished. Do NOT speculatively flip to 'thinking' — the
|
|
1932
|
+
// engine may emit more text-deltas (→ 'streaming') OR go straight
|
|
1933
|
+
// to end_turn (→ 'idle' via runVpTurn's finally). The old
|
|
1934
|
+
// speculative transition caused a visible 'tool → thinking →
|
|
1935
|
+
// streaming' flicker on every tool call. Hold the 'tool' state
|
|
1936
|
+
// until the next real event arrives.
|
|
1937
1937
|
break;
|
|
1938
1938
|
|
|
1939
1939
|
case 'turn_start':
|
|
@@ -1971,9 +1971,11 @@ function handleEngineEvent(event, hctx) {
|
|
|
1971
1971
|
threadId: hctx.threadId || event.threadId || 'main',
|
|
1972
1972
|
turnId: hctx.turnId,
|
|
1973
1973
|
stopReason: event.stopReason,
|
|
1974
|
+
reason: 'route_forward',
|
|
1974
1975
|
detail: event.detail || null,
|
|
1975
1976
|
ts: Date.now(),
|
|
1976
1977
|
}, envelope);
|
|
1978
|
+
if (typeof hctx.markTurnEnd === 'function') hctx.markTurnEnd('route_forward');
|
|
1977
1979
|
}
|
|
1978
1980
|
break;
|
|
1979
1981
|
|
|
@@ -2709,6 +2711,38 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2709
2711
|
|
|
2710
2712
|
const envelope = { sessionId, vpId, threadId, turnId };
|
|
2711
2713
|
|
|
2714
|
+
// Per-message turn lifecycle: track start ts + which terminal reason
|
|
2715
|
+
// we'll emit. `emitVpTurnEnd` is idempotent (route_forward emits inside
|
|
2716
|
+
// the engine loop; normal end_turn / abort / error emit at runVpTurn
|
|
2717
|
+
// boundaries — without idempotency a route_forward turn would emit
|
|
2718
|
+
// twice). `markTurnEnd` lets the engine-event handler tell us that
|
|
2719
|
+
// it already emitted, so we don't emit a duplicate at the runVpTurn
|
|
2720
|
+
// normal-completion path.
|
|
2721
|
+
const turnStartAt = Date.now();
|
|
2722
|
+
let turnEndReason = 'end_turn';
|
|
2723
|
+
let turnEndEmitted = false;
|
|
2724
|
+
let turnEndDetail = null;
|
|
2725
|
+
const markTurnEnd = (reason) => { turnEndEmitted = true; turnEndReason = reason; };
|
|
2726
|
+
const emitVpTurnEnd = (reason, detail = null) => {
|
|
2727
|
+
if (turnEndEmitted) return;
|
|
2728
|
+
turnEndEmitted = true;
|
|
2729
|
+
try {
|
|
2730
|
+
sendYeaftEvent({
|
|
2731
|
+
type: 'vp_turn_end',
|
|
2732
|
+
sessionId,
|
|
2733
|
+
vpId,
|
|
2734
|
+
threadId: threadId || 'main',
|
|
2735
|
+
turnId,
|
|
2736
|
+
reason,
|
|
2737
|
+
durationMs: Date.now() - turnStartAt,
|
|
2738
|
+
detail: detail || null,
|
|
2739
|
+
ts: Date.now(),
|
|
2740
|
+
}, envelope);
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
console.warn('[Yeaft] vp_turn_end emit failed:', err?.message || err);
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
|
|
2712
2746
|
try {
|
|
2713
2747
|
if (session?.dreamScheduler) {
|
|
2714
2748
|
session.dreamScheduler.noteUserMessage();
|
|
@@ -2773,6 +2807,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2773
2807
|
threadId,
|
|
2774
2808
|
thread,
|
|
2775
2809
|
appendedUserPrompts,
|
|
2810
|
+
markTurnEnd,
|
|
2776
2811
|
};
|
|
2777
2812
|
// Always trim the snapshot before passing to engine.query. This is
|
|
2778
2813
|
// the second-line defense (history-compact only fires above 30K
|
|
@@ -2816,6 +2851,10 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2816
2851
|
type: 'result',
|
|
2817
2852
|
result_text: '',
|
|
2818
2853
|
}, envelope);
|
|
2854
|
+
// Normal end-of-turn (no route_forward, no abort, no error). Emit
|
|
2855
|
+
// the message-status terminal so the web client can flip the
|
|
2856
|
+
// assistant message status from 'pending' → 'completed'.
|
|
2857
|
+
emitVpTurnEnd('end_turn');
|
|
2819
2858
|
} finally {
|
|
2820
2859
|
if (queryTimer) clearTimeout(queryTimer);
|
|
2821
2860
|
}
|
|
@@ -2827,10 +2866,13 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2827
2866
|
result_text: '',
|
|
2828
2867
|
stopped: true,
|
|
2829
2868
|
}, envelope);
|
|
2869
|
+
emitVpTurnEnd('aborted');
|
|
2830
2870
|
return;
|
|
2831
2871
|
}
|
|
2832
2872
|
|
|
2833
2873
|
console.error('[Yeaft] query error:', err);
|
|
2874
|
+
turnEndReason = 'errored';
|
|
2875
|
+
turnEndDetail = { message: err?.message || String(err) };
|
|
2834
2876
|
|
|
2835
2877
|
// vp-status: surface a transient `error` state so the row's status
|
|
2836
2878
|
// label flips red for the brief window before the outer finally
|
|
@@ -2872,14 +2914,23 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2872
2914
|
result_text: '',
|
|
2873
2915
|
}, envelope);
|
|
2874
2916
|
} finally {
|
|
2917
|
+
// Emit terminal vp_turn_end for the error path (normal + abort + route
|
|
2918
|
+
// already emitted above). Done before settleIdle so the web client
|
|
2919
|
+
// sees status flip BEFORE the broker's idle event lands.
|
|
2920
|
+
if (turnEndReason === 'errored') emitVpTurnEnd('errored', turnEndDetail);
|
|
2875
2921
|
// vp-status: guaranteed-settle. Regardless of how the turn exited
|
|
2876
2922
|
// (normal completion, AbortError early-return, caught exception),
|
|
2877
|
-
// the row must drop back to 'idle'.
|
|
2878
|
-
//
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2923
|
+
// the row must drop back to 'idle'. EXCEPTION: when the turn errored,
|
|
2924
|
+
// we keep the broker's 'error' state visible until the next turn
|
|
2925
|
+
// starts, so the user can see something failed instead of a silent
|
|
2926
|
+
// green-state turn end. Wrapped in its own try so a broker bug
|
|
2927
|
+
// can't mask the original error.
|
|
2928
|
+
if (turnEndReason !== 'errored') {
|
|
2929
|
+
try {
|
|
2930
|
+
getVpStatusBroker().settleIdle({ sessionId, vpId, threadId: threadId || 'main', title: thread?.title || '' });
|
|
2931
|
+
} catch (err) {
|
|
2932
|
+
console.warn('[Yeaft] vp-status settleIdle failed:', err?.message || err);
|
|
2933
|
+
}
|
|
2883
2934
|
}
|
|
2884
2935
|
// fix-vp-multi-thread (bug 2): the bridge tracks per-thread status
|
|
2885
2936
|
// on `thread.status` separately from the broker. Multiple sites
|
|
@@ -3614,6 +3665,55 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3614
3665
|
console.warn('[Yeaft] vp-status snapshot broadcast (replay) failed:', err?.message || err);
|
|
3615
3666
|
}
|
|
3616
3667
|
|
|
3668
|
+
// Delta path: caller knows the latest seq (or message id) it has cached
|
|
3669
|
+
// and wants only the messages that arrived after that cursor. Returns
|
|
3670
|
+
// early with mode:'delta' so the frontend can append+dedupe instead of
|
|
3671
|
+
// replacing the pane.
|
|
3672
|
+
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
3673
|
+
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
3674
|
+
let afterSeq = afterSeqRaw;
|
|
3675
|
+
if (afterSeq === null && afterMessageId && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
3676
|
+
afterSeq = session.conversationStore.getMessageSeqById(afterMessageId);
|
|
3677
|
+
}
|
|
3678
|
+
if (sessionId && afterSeq !== null && typeof session.conversationStore.loadAfterSeqByGroup === 'function') {
|
|
3679
|
+
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq);
|
|
3680
|
+
for (const entry of delta.messages) {
|
|
3681
|
+
if (entry.role === 'user') {
|
|
3682
|
+
sendYeaftOutput({
|
|
3683
|
+
type: 'user',
|
|
3684
|
+
message: {
|
|
3685
|
+
content: entry.content,
|
|
3686
|
+
id: entry.id || null,
|
|
3687
|
+
...(Array.isArray(entry.attachments) && entry.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(entry.attachments) } : {}),
|
|
3688
|
+
},
|
|
3689
|
+
ts: entry.ts || null,
|
|
3690
|
+
}, { sessionId: entry.sessionId || null, threadId: entry.threadId || 'main', turnId: entry.turnId || entry.threadId || 'main' });
|
|
3691
|
+
} else if (entry.role === 'assistant') {
|
|
3692
|
+
const envelopeOpts = {
|
|
3693
|
+
sessionId: entry.sessionId || null,
|
|
3694
|
+
threadId: entry.threadId || 'main',
|
|
3695
|
+
turnId: entry.turnId || entry.threadId || 'main',
|
|
3696
|
+
};
|
|
3697
|
+
if (entry.speakerVpId) envelopeOpts.vpId = entry.speakerVpId;
|
|
3698
|
+
sendYeaftOutput({
|
|
3699
|
+
type: 'assistant',
|
|
3700
|
+
message: { id: entry.id || null, content: [{ type: 'text', text: entry.content }] },
|
|
3701
|
+
ts: entry.ts || null,
|
|
3702
|
+
}, envelopeOpts);
|
|
3703
|
+
sendYeaftOutput({ type: 'result', result_text: '' }, envelopeOpts);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
sendYeaftEvent({
|
|
3707
|
+
type: 'history_loaded',
|
|
3708
|
+
mode: 'delta',
|
|
3709
|
+
count: delta.messages.length,
|
|
3710
|
+
sessionId,
|
|
3711
|
+
latestSeq: delta.latestSeq,
|
|
3712
|
+
afterSeq,
|
|
3713
|
+
});
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3617
3717
|
// `msg.limit` is the replay-scrollback request from the frontend (UI
|
|
3618
3718
|
// history pane, not engine context). Keep the bootstrap window small so
|
|
3619
3719
|
// opening a group can paint the latest messages quickly; older rows are
|
|
@@ -3684,8 +3784,18 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3684
3784
|
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(sessionId);
|
|
3685
3785
|
}
|
|
3686
3786
|
|
|
3787
|
+
// Latest seq cursor in the recent-mode reply lets the frontend stamp its
|
|
3788
|
+
// delta cursor on first paint, so the next session-switch can ask for
|
|
3789
|
+
// afterSeq instead of a full recent-N replay.
|
|
3790
|
+
let latestSeq = null;
|
|
3791
|
+
if (replayEntries.length > 0 && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
3792
|
+
const last = replayEntries[replayEntries.length - 1];
|
|
3793
|
+
if (last && last.id) latestSeq = session.conversationStore.getMessageSeqById(last.id);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3687
3796
|
sendYeaftEvent({
|
|
3688
3797
|
type: 'history_loaded',
|
|
3798
|
+
mode: 'recent',
|
|
3689
3799
|
count: replayEntries.length,
|
|
3690
3800
|
hasCompactSummary: hasCompactSummaryFlag,
|
|
3691
3801
|
totalHot: session.conversationStore.countHot(),
|
|
@@ -3693,6 +3803,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3693
3803
|
sessionId,
|
|
3694
3804
|
hasMore,
|
|
3695
3805
|
oldestSeq,
|
|
3806
|
+
latestSeq,
|
|
3696
3807
|
});
|
|
3697
3808
|
}
|
|
3698
3809
|
|