mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
refactor(planner): lock send and plot history boundaries
This commit is contained in:
@@ -47,15 +47,22 @@ export function applyPlannerResultAndSend({
|
||||
plannerState.lastInjectedText = merged;
|
||||
}
|
||||
|
||||
const plotRecordPayload = plannerPlotRecord && typeof plannerPlotRecord === 'object'
|
||||
? {
|
||||
...plannerPlotRecord,
|
||||
rawUserInput: raw,
|
||||
plannerAugmentedMessage: merged,
|
||||
}
|
||||
: null;
|
||||
|
||||
let plotHandoffPrepared = false;
|
||||
if (runtime?.preparePlannerPlotRecordHandoff && plotRecordPayload) {
|
||||
runtime.preparePlannerPlotRecordHandoff(plotRecordPayload);
|
||||
plotHandoffPrepared = true;
|
||||
}
|
||||
|
||||
let handoffPrepared = false;
|
||||
if (runtime?.preparePlannerRecallHandoff && plannerRecall?.result) {
|
||||
const plotRecordPayload = plannerPlotRecord && typeof plannerPlotRecord === 'object'
|
||||
? {
|
||||
...plannerPlotRecord,
|
||||
rawUserInput: raw,
|
||||
plannerAugmentedMessage: merged,
|
||||
}
|
||||
: null;
|
||||
runtime.preparePlannerRecallHandoff({
|
||||
rawUserInput: raw,
|
||||
plannerAugmentedMessage: merged,
|
||||
@@ -69,5 +76,27 @@ export function applyPlannerResultAndSend({
|
||||
plannerState.bypassNextSend = true;
|
||||
}
|
||||
button.click();
|
||||
return { applied: true, merged, handoffPrepared };
|
||||
return { applied: true, merged, handoffPrepared, plotHandoffPrepared };
|
||||
}
|
||||
|
||||
export function shouldInterceptPlannerSend({
|
||||
enabled = false,
|
||||
isPlanning = false,
|
||||
hasTextarea = false,
|
||||
textareaValue = '',
|
||||
isTrivial = false,
|
||||
bypassNextSend = false,
|
||||
skipIfPlotPresent = false,
|
||||
} = {}) {
|
||||
if (!enabled) return { shouldIntercept: false, reason: 'disabled' };
|
||||
if (isPlanning) return { shouldIntercept: false, reason: 'planning' };
|
||||
if (!hasTextarea) return { shouldIntercept: false, reason: 'missing-textarea' };
|
||||
const text = String(textareaValue ?? '').trim();
|
||||
if (!text) return { shouldIntercept: false, reason: 'empty-input' };
|
||||
if (isTrivial) return { shouldIntercept: false, reason: 'trivial' };
|
||||
if (bypassNextSend) return { shouldIntercept: false, reason: 'bypass' };
|
||||
if (skipIfPlotPresent && /<plot\b/i.test(text)) {
|
||||
return { shouldIntercept: false, reason: 'plot-present' };
|
||||
}
|
||||
return { shouldIntercept: true, reason: 'ok' };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } fr
|
||||
import { EnaPlannerStorage, migrateFromLWBIfNeeded } from './ena-planner-storage.js';
|
||||
import {
|
||||
applyPlannerResultAndSend,
|
||||
shouldInterceptPlannerSend,
|
||||
} from './ena-planner-runtime-utils.js';
|
||||
import { readPlannerPlotHistory } from './planner-plot-history.js';
|
||||
import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js';
|
||||
@@ -1880,15 +1881,17 @@ function isTrivialPlannerInput(text) {
|
||||
|
||||
function shouldInterceptNow() {
|
||||
const s = ensureSettings();
|
||||
if (!s.enabled || state.isPlanning) return false;
|
||||
const ta = getSendTextarea();
|
||||
if (!ta) return false;
|
||||
const txt = String(ta.value ?? '').trim();
|
||||
if (!txt) return false;
|
||||
if (isTrivialPlannerInput(txt)) return false;
|
||||
if (state.bypassNextSend) return false;
|
||||
if (s.skipIfPlotPresent && /<plot\b/i.test(txt)) return false;
|
||||
return true;
|
||||
const txt = String(ta?.value ?? '').trim();
|
||||
return shouldInterceptPlannerSend({
|
||||
enabled: Boolean(s.enabled),
|
||||
isPlanning: Boolean(state.isPlanning),
|
||||
hasTextarea: Boolean(ta),
|
||||
textareaValue: txt,
|
||||
isTrivial: Boolean(txt && isTrivialPlannerInput(txt)),
|
||||
bypassNextSend: Boolean(state.bypassNextSend),
|
||||
skipIfPlotPresent: Boolean(s.skipIfPlotPresent),
|
||||
}).shouldIntercept;
|
||||
}
|
||||
|
||||
async function doInterceptAndPlanThenSend() {
|
||||
|
||||
@@ -80,21 +80,43 @@ export function collectStructuredPlotRecords(chat, count = 2) {
|
||||
}
|
||||
|
||||
export function readPlannerPlotHistory(chat, { count = 2 } = {}) {
|
||||
const want = Math.max(0, Number(count) || 0);
|
||||
if (!want) {
|
||||
return { source: 'empty', records: [], plots: [], block: '' };
|
||||
}
|
||||
const structuredRecords = collectStructuredPlotRecords(chat, count);
|
||||
const seen = new Set();
|
||||
const plots = [];
|
||||
let usedLegacy = false;
|
||||
if (structuredRecords.length > 0) {
|
||||
const plots = structuredRecords.map((record) => record.plotText || record.plotBlocks.join('\n')).filter(Boolean);
|
||||
return {
|
||||
source: 'structured',
|
||||
records: structuredRecords,
|
||||
plots,
|
||||
block: formatPlotsBlock(plots),
|
||||
};
|
||||
for (const record of structuredRecords) {
|
||||
const recordBlocks = record.plotBlocks.length > 0
|
||||
? record.plotBlocks
|
||||
: extractLastNPlots([{ mes: record.plotText || '' }], want);
|
||||
const plot = recordBlocks.join('\n').trim();
|
||||
if (!plot || seen.has(plot)) continue;
|
||||
plots.push(plot);
|
||||
seen.add(plot);
|
||||
if (plots.length >= want) break;
|
||||
}
|
||||
}
|
||||
|
||||
const plots = extractLastNPlots(chat, count);
|
||||
if (plots.length < want) {
|
||||
for (const legacyPlot of extractLastNPlots(chat, want)) {
|
||||
if (!legacyPlot || seen.has(legacyPlot)) continue;
|
||||
plots.push(legacyPlot);
|
||||
seen.add(legacyPlot);
|
||||
usedLegacy = true;
|
||||
if (plots.length >= want) break;
|
||||
}
|
||||
}
|
||||
|
||||
const source = structuredRecords.length > 0
|
||||
? (usedLegacy ? 'structured+legacy' : 'structured')
|
||||
: (plots.length > 0 ? 'legacy' : 'empty');
|
||||
return {
|
||||
source: plots.length > 0 ? 'legacy' : 'empty',
|
||||
records: [],
|
||||
source,
|
||||
records: structuredRecords,
|
||||
plots,
|
||||
block: formatPlotsBlock(plots),
|
||||
};
|
||||
|
||||
20
index.js
20
index.js
@@ -14747,6 +14747,16 @@ function preparePlannerRecallHandoff({
|
||||
});
|
||||
}
|
||||
|
||||
function preparePlannerPlotRecordHandoff(plannerPlotRecord = null) {
|
||||
if (!plannerPlotRecord || typeof plannerPlotRecord !== "object") {
|
||||
return null;
|
||||
}
|
||||
return rerollRecallInput.preparePlannerPlotRecordHandoff({
|
||||
...plannerPlotRecord,
|
||||
chatId: getCurrentChatId(),
|
||||
});
|
||||
}
|
||||
|
||||
function persistPlannerPlotRecordToUserMessage(newUserMessageIndex) {
|
||||
const context = getContext();
|
||||
const chat = context?.chat;
|
||||
@@ -14757,16 +14767,19 @@ function persistPlannerPlotRecordToUserMessage(newUserMessageIndex) {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const handoff = peekPlannerRecallHandoff(context?.chatId || getCurrentChatId());
|
||||
const plannerPlotRecord = handoff?.plannerPlotRecord;
|
||||
const chatId = context?.chatId || getCurrentChatId();
|
||||
const plotHandoff = rerollRecallInput.peekPlannerPlotRecordHandoff?.(chatId);
|
||||
const handoff = peekPlannerRecallHandoff(chatId);
|
||||
const plannerPlotRecord = plotHandoff || handoff?.plannerPlotRecord;
|
||||
if (!plannerPlotRecord || typeof plannerPlotRecord !== "object") {
|
||||
return false;
|
||||
}
|
||||
const wrote = writeStructuredPlotRecordToMessage(chat[newUserMessageIndex], {
|
||||
...plannerPlotRecord,
|
||||
recallHandoffId: handoff.id || plannerPlotRecord.recallHandoffId || "",
|
||||
recallHandoffId: handoff?.id || plannerPlotRecord.recallHandoffId || "",
|
||||
});
|
||||
if (wrote) {
|
||||
rerollRecallInput.consumePlannerPlotRecordHandoff?.(chatId);
|
||||
triggerChatMetadataSave(context, { immediate: false });
|
||||
}
|
||||
return wrote;
|
||||
@@ -17918,6 +17931,7 @@ async function onCompactLukerSidecar() {
|
||||
getExtensionPath: () => `scripts/extensions/third-party/${MODULE_NAME}`,
|
||||
getPlannerRecallTimeoutMs,
|
||||
isTrivialUserInput,
|
||||
preparePlannerPlotRecordHandoff,
|
||||
preparePlannerRecallHandoff,
|
||||
runPlannerRecallForEna,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export function createRerollRecallInput(deps = {}) {
|
||||
const plannerRecallHandoffs = new Map();
|
||||
const plannerPlotRecordHandoffs = new Map();
|
||||
|
||||
const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args);
|
||||
const normalizeChatIdCandidate = (value = "") =>
|
||||
@@ -250,6 +251,16 @@ export function createRerollRecallInput(deps = {}) {
|
||||
plannerRecallHandoffs.delete(chatId);
|
||||
}
|
||||
}
|
||||
for (const [chatId, handoff] of plannerPlotRecordHandoffs.entries()) {
|
||||
if (
|
||||
!handoff ||
|
||||
String(handoff.chatId || "") !== String(chatId || "") ||
|
||||
now - Number(handoff.updatedAt || handoff.createdAt || 0) >
|
||||
getPlannerRecallHandoffTtlMs()
|
||||
) {
|
||||
plannerPlotRecordHandoffs.delete(chatId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function peekPlannerRecallHandoff(
|
||||
@@ -278,14 +289,37 @@ export function createRerollRecallInput(deps = {}) {
|
||||
) {
|
||||
cleanupPlannerRecallHandoffs();
|
||||
if (clearAll) {
|
||||
const removed = plannerRecallHandoffs.size;
|
||||
const removed = plannerRecallHandoffs.size + plannerPlotRecordHandoffs.size;
|
||||
plannerRecallHandoffs.clear();
|
||||
plannerPlotRecordHandoffs.clear();
|
||||
return removed;
|
||||
}
|
||||
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
if (!normalizedChatId) return 0;
|
||||
return plannerRecallHandoffs.delete(normalizedChatId) ? 1 : 0;
|
||||
let removed = 0;
|
||||
if (plannerRecallHandoffs.delete(normalizedChatId)) removed += 1;
|
||||
if (plannerPlotRecordHandoffs.delete(normalizedChatId)) removed += 1;
|
||||
return removed;
|
||||
}
|
||||
|
||||
function peekPlannerPlotRecordHandoff(
|
||||
chatId = getCurrentChatId(),
|
||||
now = Date.now(),
|
||||
) {
|
||||
cleanupPlannerRecallHandoffs(now);
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
if (!normalizedChatId) return null;
|
||||
return plannerPlotRecordHandoffs.get(normalizedChatId) || null;
|
||||
}
|
||||
|
||||
function consumePlannerPlotRecordHandoff(chatId = getCurrentChatId()) {
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
if (!normalizedChatId) return null;
|
||||
const handoff = peekPlannerPlotRecordHandoff(normalizedChatId);
|
||||
if (!handoff) return null;
|
||||
plannerPlotRecordHandoffs.delete(normalizedChatId);
|
||||
return handoff;
|
||||
}
|
||||
|
||||
function consumePlannerRecallHandoff(
|
||||
@@ -354,12 +388,56 @@ export function createRerollRecallInput(deps = {}) {
|
||||
return handoff;
|
||||
}
|
||||
|
||||
function preparePlannerPlotRecordHandoff({
|
||||
rawUserInput = "",
|
||||
plannerAugmentedMessage = "",
|
||||
plotText = "",
|
||||
plotBlocks = null,
|
||||
promptProfileId = "",
|
||||
taskResults = [],
|
||||
chatId = getCurrentChatId(),
|
||||
} = {}) {
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
const normalizedRawUserInput = normalizeRecallInputText(rawUserInput);
|
||||
const normalizedPlannerAugmentedMessage = normalizeRecallInputText(
|
||||
plannerAugmentedMessage,
|
||||
);
|
||||
const normalizedPlotText = normalizeRecallInputText(plotText);
|
||||
if (!normalizedChatId || !normalizedRawUserInput || !normalizedPlotText) {
|
||||
return null;
|
||||
}
|
||||
cleanupPlannerRecallHandoffs();
|
||||
const createdAt = Date.now();
|
||||
const handoff = {
|
||||
id: [
|
||||
normalizedChatId,
|
||||
hashRecallInput(normalizedRawUserInput),
|
||||
"plot",
|
||||
createdAt,
|
||||
].join(":"),
|
||||
chatId: normalizedChatId,
|
||||
rawUserInput: normalizedRawUserInput,
|
||||
plannerAugmentedMessage: normalizedPlannerAugmentedMessage,
|
||||
plotText: normalizedPlotText,
|
||||
plotBlocks: Array.isArray(plotBlocks) ? [...plotBlocks] : null,
|
||||
promptProfileId: String(promptProfileId || ""),
|
||||
taskResults: Array.isArray(taskResults) ? taskResults : [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
plannerPlotRecordHandoffs.set(normalizedChatId, handoff);
|
||||
return handoff;
|
||||
}
|
||||
|
||||
return {
|
||||
clearPendingRerollRecallReuse,
|
||||
buildNormalGenerationRecallInput,
|
||||
buildHistoryGenerationRecallInput,
|
||||
buildGenerationAfterCommandsRecallInput,
|
||||
preparePlannerRecallHandoff,
|
||||
preparePlannerPlotRecordHandoff,
|
||||
peekPlannerPlotRecordHandoff,
|
||||
consumePlannerPlotRecordHandoff,
|
||||
cleanupPlannerRecallHandoffs,
|
||||
peekPlannerRecallHandoff,
|
||||
clearPlannerRecallHandoffsForChat,
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
applyPlannerResultAndSend,
|
||||
extractLastNPlots,
|
||||
formatPlotsBlock,
|
||||
shouldInterceptPlannerSend,
|
||||
} from '../ena-planner/ena-planner-runtime-utils.js';
|
||||
import { createRerollRecallInput } from '../runtime/reroll-recall-input.js';
|
||||
import {
|
||||
createStructuredPlotRecord,
|
||||
readPlannerPlotHistory,
|
||||
@@ -77,6 +79,65 @@ import {
|
||||
assert.equal(plannerState.bypassNextSend, true);
|
||||
}
|
||||
|
||||
{
|
||||
const order = [];
|
||||
const textarea = { value: 'raw' };
|
||||
const button = { click: () => order.push('click') };
|
||||
const plannerState = { bypassNextSend: false, lastInjectedText: '' };
|
||||
const runtime = {
|
||||
preparePlannerPlotRecordHandoff(payload) {
|
||||
order.push('plot-handoff');
|
||||
assert.deepEqual(payload, {
|
||||
rawUserInput: 'raw input',
|
||||
plannerAugmentedMessage: 'raw input\n\n<plot>next</plot>',
|
||||
plotText: '<plot>next</plot>',
|
||||
});
|
||||
},
|
||||
preparePlannerRecallHandoff() {
|
||||
order.push('recall-handoff');
|
||||
},
|
||||
};
|
||||
|
||||
const result = applyPlannerResultAndSend({
|
||||
textarea,
|
||||
button,
|
||||
rawUserInput: 'raw input',
|
||||
filtered: '<plot>next</plot>',
|
||||
plannerRecall: null,
|
||||
plannerPlotRecord: { plotText: '<plot>next</plot>' },
|
||||
runtime,
|
||||
plannerState,
|
||||
});
|
||||
|
||||
assert.deepEqual(order, ['plot-handoff', 'click']);
|
||||
assert.equal(result.applied, true);
|
||||
assert.equal(result.plotHandoffPrepared, true);
|
||||
assert.equal(result.handoffPrepared, false);
|
||||
assert.equal(textarea.value, 'raw input\n\n<plot>next</plot>');
|
||||
assert.equal(plannerState.lastInjectedText, textarea.value);
|
||||
assert.equal(plannerState.bypassNextSend, true);
|
||||
}
|
||||
|
||||
{
|
||||
const cases = [
|
||||
[{ enabled: false, hasTextarea: true, textareaValue: 'go' }, false, 'disabled'],
|
||||
[{ enabled: true, isPlanning: true, hasTextarea: true, textareaValue: 'go' }, false, 'planning'],
|
||||
[{ enabled: true, hasTextarea: false, textareaValue: 'go' }, false, 'missing-textarea'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: ' ' }, false, 'empty-input'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: 'go', isTrivial: true }, false, 'trivial'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: 'go', bypassNextSend: true }, false, 'bypass'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: '<plot>done</plot>', skipIfPlotPresent: true }, false, 'plot-present'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: '<plotter>not a plot tag</plotter>', skipIfPlotPresent: true }, true, 'ok'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: '<plot id="x">done</plot>', skipIfPlotPresent: false }, true, 'ok'],
|
||||
[{ enabled: true, hasTextarea: true, textareaValue: 'continue the scene' }, true, 'ok'],
|
||||
];
|
||||
for (const [input, expectedShouldIntercept, expectedReason] of cases) {
|
||||
const result = shouldInterceptPlannerSend(input);
|
||||
assert.equal(result.shouldIntercept, expectedShouldIntercept, expectedReason);
|
||||
assert.equal(result.reason, expectedReason);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const chat = [
|
||||
{ is_user: true, mes: 'raw old', extra: {} },
|
||||
@@ -85,14 +146,16 @@ import {
|
||||
];
|
||||
writeStructuredPlotRecordToMessage(chat[2], createStructuredPlotRecord({
|
||||
rawUserInput: 'raw latest',
|
||||
plannerAugmentedMessage: 'raw latest\n\n<plot>structured</plot>',
|
||||
plotText: '<plot>structured</plot>',
|
||||
plannerAugmentedMessage: 'raw latest\n\n<note>private</note>\n<plot>structured</plot>\n<state>hidden</state>',
|
||||
plotText: '<note>private</note>\n<plot>structured</plot>\n<state>hidden</state>',
|
||||
}));
|
||||
const history = readPlannerPlotHistory(chat, { count: 2 });
|
||||
assert.equal(history.source, 'structured');
|
||||
assert.deepEqual(history.plots, ['<plot>structured</plot>']);
|
||||
assert.equal(history.source, 'structured+legacy');
|
||||
assert.deepEqual(history.plots, ['<plot>structured</plot>', '<plot>legacy stale</plot>']);
|
||||
assert.ok(history.block.includes('<plot>structured</plot>'));
|
||||
assert.ok(!history.block.includes('legacy stale'));
|
||||
assert.ok(history.block.includes('legacy stale'));
|
||||
assert.ok(!history.block.includes('<note>private</note>'));
|
||||
assert.ok(!history.block.includes('<state>hidden</state>'));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -122,6 +185,27 @@ import {
|
||||
assert.equal(chat[2].extra.st_bme_plot, undefined);
|
||||
}
|
||||
|
||||
{
|
||||
const runtime = createRerollRecallInput({
|
||||
getCurrentChatId: () => 'chat-a',
|
||||
normalizeChatIdCandidate: (value) => String(value || '').trim(),
|
||||
normalizeRecallInputText: (value) => String(value || '').trim(),
|
||||
hashRecallInput: (value) => `hash:${String(value || '').length}`,
|
||||
});
|
||||
const handoff = runtime.preparePlannerPlotRecordHandoff({
|
||||
chatId: 'chat-a',
|
||||
rawUserInput: 'raw input',
|
||||
plannerAugmentedMessage: 'raw input\n\n<plot>next</plot>',
|
||||
plotText: '<plot>next</plot>',
|
||||
});
|
||||
assert.ok(handoff?.id?.includes(':plot:'));
|
||||
assert.equal(handoff.plotText, '<plot>next</plot>');
|
||||
assert.equal(runtime.peekPlannerRecallHandoff('chat-a'), null);
|
||||
assert.equal(runtime.peekPlannerPlotRecordHandoff('chat-a')?.plotText, '<plot>next</plot>');
|
||||
assert.equal(runtime.consumePlannerPlotRecordHandoff('chat-a')?.plotText, '<plot>next</plot>');
|
||||
assert.equal(runtime.peekPlannerPlotRecordHandoff('chat-a'), null);
|
||||
}
|
||||
|
||||
{
|
||||
const order = [];
|
||||
const result = applyPlannerResultAndSend({
|
||||
|
||||
Reference in New Issue
Block a user