@yeaft/webchat-agent 0.1.891 → 0.1.893
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/session.js +9 -5
- package/yeaft/web-bridge.js +129 -20
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/session.js
CHANGED
|
@@ -323,11 +323,15 @@ export async function loadSession(options = {}) {
|
|
|
323
323
|
} catch (err) {
|
|
324
324
|
console.warn(`[Yeaft] topUpDefaultVps failed: ${err?.message || err}`);
|
|
325
325
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
// fix-yeaft-session-server-persistence: stop auto-seeding a
|
|
327
|
+
// `grp_default` per agent. Previously every agent that booted with
|
|
328
|
+
// zero sessions would manufacture an empty default group, which on
|
|
329
|
+
// the unified sidebar shows up as a phantom row distinct from the
|
|
330
|
+
// user's real session — and on agent switch it stole the active-
|
|
331
|
+
// session slot. With server-side persistence the user's actual
|
|
332
|
+
// yeaft sessions are now hydrated from the DB; if they have none,
|
|
333
|
+
// the sidebar shows the empty state + "create session" CTA, which
|
|
334
|
+
// is the explicit behaviour the user asked for.
|
|
331
335
|
|
|
332
336
|
// task-fix-memory-load: backfill summary.md for VPs / groups created
|
|
333
337
|
// before the create-time seed was added. Without this, an existing
|
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
|
|
|
@@ -2221,15 +2223,13 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
2221
2223
|
const dir = join(sessionRoot, sessionId);
|
|
2222
2224
|
if (existsSync(dir) && loadSessionMeta(dir)) {
|
|
2223
2225
|
sessionHandle = openSession(sessionRoot, sessionId);
|
|
2224
|
-
} else if (sessionId === 'grp_default') {
|
|
2225
|
-
try {
|
|
2226
|
-
const seeded = seedDefaultSession(groupYeaftDir, { memoryRoot: join(groupYeaftDir, 'memory') });
|
|
2227
|
-
sessionHandle = seeded.group;
|
|
2228
|
-
} catch (seedErr) {
|
|
2229
|
-
seedFailed = true;
|
|
2230
|
-
console.warn('[Yeaft] yeaft_group_chat: seedDefaultSession failed', seedErr?.message || seedErr);
|
|
2231
|
-
}
|
|
2232
2226
|
} else {
|
|
2227
|
+
// fix-yeaft-session-server-persistence: the `grp_default` on-the-
|
|
2228
|
+
// fly seed used to manufacture a missing session here. That
|
|
2229
|
+
// hid the "session not found" error and re-created the phantom
|
|
2230
|
+
// default-group row across agents. Now we surface the not-found
|
|
2231
|
+
// case so the web can show an "agent offline / session missing"
|
|
2232
|
+
// hint instead of silently creating a different session.
|
|
2233
2233
|
console.warn('[Yeaft] yeaft_group_chat: sessionId %s not found', sessionId);
|
|
2234
2234
|
}
|
|
2235
2235
|
} catch (err) {
|
|
@@ -2709,6 +2709,38 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2709
2709
|
|
|
2710
2710
|
const envelope = { sessionId, vpId, threadId, turnId };
|
|
2711
2711
|
|
|
2712
|
+
// Per-message turn lifecycle: track start ts + which terminal reason
|
|
2713
|
+
// we'll emit. `emitVpTurnEnd` is idempotent (route_forward emits inside
|
|
2714
|
+
// the engine loop; normal end_turn / abort / error emit at runVpTurn
|
|
2715
|
+
// boundaries — without idempotency a route_forward turn would emit
|
|
2716
|
+
// twice). `markTurnEnd` lets the engine-event handler tell us that
|
|
2717
|
+
// it already emitted, so we don't emit a duplicate at the runVpTurn
|
|
2718
|
+
// normal-completion path.
|
|
2719
|
+
const turnStartAt = Date.now();
|
|
2720
|
+
let turnEndReason = 'end_turn';
|
|
2721
|
+
let turnEndEmitted = false;
|
|
2722
|
+
let turnEndDetail = null;
|
|
2723
|
+
const markTurnEnd = (reason) => { turnEndEmitted = true; turnEndReason = reason; };
|
|
2724
|
+
const emitVpTurnEnd = (reason, detail = null) => {
|
|
2725
|
+
if (turnEndEmitted) return;
|
|
2726
|
+
turnEndEmitted = true;
|
|
2727
|
+
try {
|
|
2728
|
+
sendYeaftEvent({
|
|
2729
|
+
type: 'vp_turn_end',
|
|
2730
|
+
sessionId,
|
|
2731
|
+
vpId,
|
|
2732
|
+
threadId: threadId || 'main',
|
|
2733
|
+
turnId,
|
|
2734
|
+
reason,
|
|
2735
|
+
durationMs: Date.now() - turnStartAt,
|
|
2736
|
+
detail: detail || null,
|
|
2737
|
+
ts: Date.now(),
|
|
2738
|
+
}, envelope);
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
console.warn('[Yeaft] vp_turn_end emit failed:', err?.message || err);
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2712
2744
|
try {
|
|
2713
2745
|
if (session?.dreamScheduler) {
|
|
2714
2746
|
session.dreamScheduler.noteUserMessage();
|
|
@@ -2773,6 +2805,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2773
2805
|
threadId,
|
|
2774
2806
|
thread,
|
|
2775
2807
|
appendedUserPrompts,
|
|
2808
|
+
markTurnEnd,
|
|
2776
2809
|
};
|
|
2777
2810
|
// Always trim the snapshot before passing to engine.query. This is
|
|
2778
2811
|
// the second-line defense (history-compact only fires above 30K
|
|
@@ -2816,6 +2849,10 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2816
2849
|
type: 'result',
|
|
2817
2850
|
result_text: '',
|
|
2818
2851
|
}, envelope);
|
|
2852
|
+
// Normal end-of-turn (no route_forward, no abort, no error). Emit
|
|
2853
|
+
// the message-status terminal so the web client can flip the
|
|
2854
|
+
// assistant message status from 'pending' → 'completed'.
|
|
2855
|
+
emitVpTurnEnd('end_turn');
|
|
2819
2856
|
} finally {
|
|
2820
2857
|
if (queryTimer) clearTimeout(queryTimer);
|
|
2821
2858
|
}
|
|
@@ -2827,10 +2864,13 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2827
2864
|
result_text: '',
|
|
2828
2865
|
stopped: true,
|
|
2829
2866
|
}, envelope);
|
|
2867
|
+
emitVpTurnEnd('aborted');
|
|
2830
2868
|
return;
|
|
2831
2869
|
}
|
|
2832
2870
|
|
|
2833
2871
|
console.error('[Yeaft] query error:', err);
|
|
2872
|
+
turnEndReason = 'errored';
|
|
2873
|
+
turnEndDetail = { message: err?.message || String(err) };
|
|
2834
2874
|
|
|
2835
2875
|
// vp-status: surface a transient `error` state so the row's status
|
|
2836
2876
|
// label flips red for the brief window before the outer finally
|
|
@@ -2872,14 +2912,23 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
2872
2912
|
result_text: '',
|
|
2873
2913
|
}, envelope);
|
|
2874
2914
|
} finally {
|
|
2915
|
+
// Emit terminal vp_turn_end for the error path (normal + abort + route
|
|
2916
|
+
// already emitted above). Done before settleIdle so the web client
|
|
2917
|
+
// sees status flip BEFORE the broker's idle event lands.
|
|
2918
|
+
if (turnEndReason === 'errored') emitVpTurnEnd('errored', turnEndDetail);
|
|
2875
2919
|
// vp-status: guaranteed-settle. Regardless of how the turn exited
|
|
2876
2920
|
// (normal completion, AbortError early-return, caught exception),
|
|
2877
|
-
// the row must drop back to 'idle'.
|
|
2878
|
-
//
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2921
|
+
// the row must drop back to 'idle'. EXCEPTION: when the turn errored,
|
|
2922
|
+
// we keep the broker's 'error' state visible until the next turn
|
|
2923
|
+
// starts, so the user can see something failed instead of a silent
|
|
2924
|
+
// green-state turn end. Wrapped in its own try so a broker bug
|
|
2925
|
+
// can't mask the original error.
|
|
2926
|
+
if (turnEndReason !== 'errored') {
|
|
2927
|
+
try {
|
|
2928
|
+
getVpStatusBroker().settleIdle({ sessionId, vpId, threadId: threadId || 'main', title: thread?.title || '' });
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
console.warn('[Yeaft] vp-status settleIdle failed:', err?.message || err);
|
|
2931
|
+
}
|
|
2883
2932
|
}
|
|
2884
2933
|
// fix-vp-multi-thread (bug 2): the bridge tracks per-thread status
|
|
2885
2934
|
// on `thread.status` separately from the broker. Multiple sites
|
|
@@ -3614,6 +3663,55 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3614
3663
|
console.warn('[Yeaft] vp-status snapshot broadcast (replay) failed:', err?.message || err);
|
|
3615
3664
|
}
|
|
3616
3665
|
|
|
3666
|
+
// Delta path: caller knows the latest seq (or message id) it has cached
|
|
3667
|
+
// and wants only the messages that arrived after that cursor. Returns
|
|
3668
|
+
// early with mode:'delta' so the frontend can append+dedupe instead of
|
|
3669
|
+
// replacing the pane.
|
|
3670
|
+
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
3671
|
+
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
3672
|
+
let afterSeq = afterSeqRaw;
|
|
3673
|
+
if (afterSeq === null && afterMessageId && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
3674
|
+
afterSeq = session.conversationStore.getMessageSeqById(afterMessageId);
|
|
3675
|
+
}
|
|
3676
|
+
if (sessionId && afterSeq !== null && typeof session.conversationStore.loadAfterSeqByGroup === 'function') {
|
|
3677
|
+
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq);
|
|
3678
|
+
for (const entry of delta.messages) {
|
|
3679
|
+
if (entry.role === 'user') {
|
|
3680
|
+
sendYeaftOutput({
|
|
3681
|
+
type: 'user',
|
|
3682
|
+
message: {
|
|
3683
|
+
content: entry.content,
|
|
3684
|
+
id: entry.id || null,
|
|
3685
|
+
...(Array.isArray(entry.attachments) && entry.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(entry.attachments) } : {}),
|
|
3686
|
+
},
|
|
3687
|
+
ts: entry.ts || null,
|
|
3688
|
+
}, { sessionId: entry.sessionId || null, threadId: entry.threadId || 'main', turnId: entry.turnId || entry.threadId || 'main' });
|
|
3689
|
+
} else if (entry.role === 'assistant') {
|
|
3690
|
+
const envelopeOpts = {
|
|
3691
|
+
sessionId: entry.sessionId || null,
|
|
3692
|
+
threadId: entry.threadId || 'main',
|
|
3693
|
+
turnId: entry.turnId || entry.threadId || 'main',
|
|
3694
|
+
};
|
|
3695
|
+
if (entry.speakerVpId) envelopeOpts.vpId = entry.speakerVpId;
|
|
3696
|
+
sendYeaftOutput({
|
|
3697
|
+
type: 'assistant',
|
|
3698
|
+
message: { id: entry.id || null, content: [{ type: 'text', text: entry.content }] },
|
|
3699
|
+
ts: entry.ts || null,
|
|
3700
|
+
}, envelopeOpts);
|
|
3701
|
+
sendYeaftOutput({ type: 'result', result_text: '' }, envelopeOpts);
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
sendYeaftEvent({
|
|
3705
|
+
type: 'history_loaded',
|
|
3706
|
+
mode: 'delta',
|
|
3707
|
+
count: delta.messages.length,
|
|
3708
|
+
sessionId,
|
|
3709
|
+
latestSeq: delta.latestSeq,
|
|
3710
|
+
afterSeq,
|
|
3711
|
+
});
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3617
3715
|
// `msg.limit` is the replay-scrollback request from the frontend (UI
|
|
3618
3716
|
// history pane, not engine context). Keep the bootstrap window small so
|
|
3619
3717
|
// opening a group can paint the latest messages quickly; older rows are
|
|
@@ -3684,8 +3782,18 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3684
3782
|
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForGroup(sessionId);
|
|
3685
3783
|
}
|
|
3686
3784
|
|
|
3785
|
+
// Latest seq cursor in the recent-mode reply lets the frontend stamp its
|
|
3786
|
+
// delta cursor on first paint, so the next session-switch can ask for
|
|
3787
|
+
// afterSeq instead of a full recent-N replay.
|
|
3788
|
+
let latestSeq = null;
|
|
3789
|
+
if (replayEntries.length > 0 && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
3790
|
+
const last = replayEntries[replayEntries.length - 1];
|
|
3791
|
+
if (last && last.id) latestSeq = session.conversationStore.getMessageSeqById(last.id);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3687
3794
|
sendYeaftEvent({
|
|
3688
3795
|
type: 'history_loaded',
|
|
3796
|
+
mode: 'recent',
|
|
3689
3797
|
count: replayEntries.length,
|
|
3690
3798
|
hasCompactSummary: hasCompactSummaryFlag,
|
|
3691
3799
|
totalHot: session.conversationStore.countHot(),
|
|
@@ -3693,6 +3801,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3693
3801
|
sessionId,
|
|
3694
3802
|
hasMore,
|
|
3695
3803
|
oldestSeq,
|
|
3804
|
+
latestSeq,
|
|
3696
3805
|
});
|
|
3697
3806
|
}
|
|
3698
3807
|
|