Add delayed auto extraction mode and fix AGPL license

This commit is contained in:
Youzini-afk
2026-04-07 22:24:04 +08:00
parent dd6baa8dad
commit c8af45bd35
9 changed files with 1370 additions and 105 deletions

View File

@@ -83,6 +83,7 @@ const defaultSettings = await loadDefaultSettings();
const { mergePersistedSettings } = await loadSettingsCompatHelpers();
assert.equal(defaultSettings.extractContextTurns, 2);
assert.equal(defaultSettings.extractAutoDelayLatestAssistant, false);
assert.equal(defaultSettings.recallTopK, 20);
assert.equal(defaultSettings.recallMaxNodes, 8);
assert.equal(defaultSettings.recallEnableVectorPrefilter, true);
@@ -140,8 +141,10 @@ assert.ok(defaultSettings.taskProfiles.recall);
const migratedSettings = mergePersistedSettings({
maintenanceAutoMinNewNodes: 7,
extractAutoDelayLatestAssistant: true,
});
assert.equal(migratedSettings.consolidationAutoMinNewNodes, 7);
assert.equal(migratedSettings.extractAutoDelayLatestAssistant, true);
assert.equal(migratedSettings.enableAutoCompression, true);
assert.equal(migratedSettings.compressionEveryN, 10);
assert.equal("maintenanceAutoMinNewNodes" in migratedSettings, false);

View File

@@ -9,6 +9,7 @@ import {
onMessageReceivedController,
onMessageSentController,
} from "../../event-binding.js";
import { resolveAutoExtractionPlanController } from "../../extraction-controller.js";
import {
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
@@ -26,11 +27,11 @@ import {
createGraphPersistenceState,
createRecallInputRecord,
createRecallRunResult,
createUiStatus,
getGenerationRecallHookStateFromResult,
getRecallHookLabel,
getStageNoticeDuration,
getStageNoticeTitle,
createUiStatus,
getGenerationRecallHookStateFromResult,
getRecallHookLabel,
getStageNoticeDuration,
getStageNoticeTitle,
hashRecallInput,
isFreshRecallInputRecord,
isTerminalGenerationRecallHookState,
@@ -112,6 +113,10 @@ export function createGenerationRecallHarness(options = {}) {
getCurrentChatId: () => "chat-main",
normalizeRecallInputText: (text = "") => String(text || "").trim(),
isTrivialUserInput,
getAssistantTurns: (chat = []) =>
chat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
),
getLatestUserChatMessage: (chat = []) =>
[...chat].reverse().find((message) => message?.is_user) || null,
getLastNonSystemChatMessage: (chat = []) =>
@@ -161,6 +166,7 @@ export function createGenerationRecallHarness(options = {}) {
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
resolveAutoExtractionPlanController,
onBeforeCombinePromptsController,
onGenerationAfterCommandsController,
onGenerationStartedController,
@@ -334,7 +340,10 @@ export function createGenerationRecallHarness(options = {}) {
context.result.getIsHostGenerationRunning(),
getPendingHostGenerationInputSnapshot:
context.result.getPendingHostGenerationInputSnapshot,
getPendingRecallSendIntent: () => context.result.getPendingRecallSendIntent(),
getPendingRecallSendIntent: () =>
context.result.getPendingRecallSendIntent(),
getLastProcessedAssistantFloor: () => -1,
getSettings: () => context.settings,
isAssistantChatMessage: (message) =>
Boolean(message) && !message.is_user && !message.is_system,
isFreshRecallInputRecord,
@@ -346,6 +355,24 @@ export function createGenerationRecallHarness(options = {}) {
context.extractionIssues.push(String(message || ""));
},
queueMicrotask: (task) => task(),
resolveAutoExtractionPlan: (options = {}) =>
resolveAutoExtractionPlanController(
{
getAssistantTurns(chat = []) {
return chat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
);
},
getLastProcessedAssistantFloor: () => -1,
getSettings: () => context.settings,
getSmartTriggerDecision: () => ({
triggered: false,
score: 0,
reasons: [],
}),
},
options,
),
runExtraction: context.runExtraction,
refreshPersistedRecallMessageUi: () => {
context.recallUiRefreshCalls += 1;

View File

@@ -19,6 +19,7 @@ import {
} from "../event-binding.js";
import {
onRerollController,
resolveAutoExtractionPlanController,
runExtractionController,
} from "../extraction-controller.js";
import {
@@ -79,6 +80,9 @@ const scriptShimSource = [
"export function getRequestHeaders() {",
" return { 'Content-Type': 'application/json' };",
"}",
"export function substituteParamsExtended(text = '') {",
" return String(text ?? '');",
"}",
].join("\n");
const openAiShimSource = [
"export const chat_completion_sources = { CUSTOM: 'custom', OPENAI: 'openai' };",
@@ -219,6 +223,38 @@ const schema = [
},
];
function buildAutoExtractionPlan({
chat = [],
settings = {},
lastProcessedAssistantFloor = -1,
lockedEndFloor = null,
smartTriggerDecision = null,
} = {}) {
return resolveAutoExtractionPlanController(
{
getAssistantTurns(sourceChat = []) {
return sourceChat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
);
},
getLastProcessedAssistantFloor: () => lastProcessedAssistantFloor,
getSettings: () => settings,
getSmartTriggerDecision: () =>
smartTriggerDecision || {
triggered: false,
score: 0,
reasons: [],
},
},
{
chat,
settings,
lastProcessedAssistantFloor,
lockedEndFloor,
},
);
}
function createBatchStageHarness() {
return fs.readFile(indexPath, "utf8").then((source) => {
const marker = "function notifyHistoryDirty(dirtyFrom, reason) {";
@@ -3677,6 +3713,15 @@ async function testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender
async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask() {
let runExtractionCalls = 0;
let refreshCalls = 0;
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: false,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
@@ -3687,14 +3732,20 @@ async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask()
createRecallInputRecord: () => ({ text: "", at: 0 }),
setPendingRecallSendIntent() {},
getContext: () => ({
chat: [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
],
chat,
}),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async () => {
runExtractionCalls += 1;
},
@@ -3719,6 +3770,15 @@ async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask()
async function testMessageReceivedDefersExtractionDuringHostGeneration() {
let runExtractionCalls = 0;
const deferred = [];
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: false,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
@@ -3734,18 +3794,27 @@ async function testMessageReceivedDefersExtractionDuringHostGeneration() {
messageId: Number.isFinite(Number(meta?.messageId))
? Number(meta.messageId)
: null,
targetEndFloor: Number.isFinite(Number(meta?.targetEndFloor))
? Number(meta.targetEndFloor)
: null,
});
},
setPendingRecallSendIntent() {},
getContext: () => ({
chat: [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
],
chat,
}),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async () => {
runExtractionCalls += 1;
},
@@ -3766,10 +3835,198 @@ async function testMessageReceivedDefersExtractionDuringHostGeneration() {
{
reason: "generation-running",
messageId: 1,
targetEndFloor: 1,
},
]);
}
async function testMessageReceivedLagModeWaitsSilentlyForNextAssistant() {
let runExtractionCalls = 0;
const deferred = [];
let refreshCalls = 0;
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }),
getCurrentGraph: () => null,
getPendingRecallSendIntent: () => ({ text: "", at: 0 }),
isFreshRecallInputRecord: () => true,
createRecallInputRecord: () => ({ text: "", at: 0 }),
setPendingRecallSendIntent() {},
getContext: () => ({ chat }),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async () => {
runExtractionCalls += 1;
},
deferAutoExtraction(reason) {
deferred.push(reason);
},
console: {
error() {},
},
notifyExtractionIssue() {},
refreshPersistedRecallMessageUi() {
refreshCalls += 1;
},
},
1,
"assistant",
);
await waitForTick();
assert.equal(runExtractionCalls, 0);
assert.deepEqual(deferred, []);
assert.equal(refreshCalls, 1);
}
async function testMessageReceivedLagModeQueuesPreviousAssistantOnly() {
const runExtractionCalls = [];
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
const settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
};
onMessageReceivedController(
{
getGraphPersistenceState: () => ({ loadState: "loaded", dbReady: true }),
getCurrentGraph: () => null,
getPendingRecallSendIntent: () => ({ text: "", at: 0 }),
isFreshRecallInputRecord: () => true,
createRecallInputRecord: () => ({ text: "", at: 0 }),
setPendingRecallSendIntent() {},
getContext: () => ({ chat }),
getSettings: () => settings,
getLastProcessedAssistantFloor: () => -1,
isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system;
},
resolveAutoExtractionPlan: (options = {}) =>
buildAutoExtractionPlan({
chat,
settings,
lastProcessedAssistantFloor: -1,
...(options || {}),
}),
runExtraction: async (options = {}) => {
runExtractionCalls.push({ ...options });
},
console: {
error() {},
},
notifyExtractionIssue() {},
refreshPersistedRecallMessageUi() {},
},
3,
"assistant",
);
await waitForTick();
assert.equal(runExtractionCalls.length, 1);
assert.equal(runExtractionCalls[0]?.lockedEndFloor, 1);
assert.equal(runExtractionCalls[0]?.triggerSource, "message-received");
}
async function testLagModeSmartTriggerOnlyScoresEligibleWindow() {
const endFloors = [];
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
const plan = resolveAutoExtractionPlanController(
{
getAssistantTurns(sourceChat = []) {
return sourceChat.flatMap((message, index) =>
!message?.is_user && !message?.is_system ? [index] : [],
);
},
getLastProcessedAssistantFloor: () => -1,
getSettings: () => ({
extractEvery: 10,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: true,
}),
getSmartTriggerDecision(_chat, _lastProcessed, _settings, endFloor) {
endFloors.push(endFloor);
return {
triggered: true,
score: 3,
reasons: ["test"],
};
},
},
{
chat,
settings: {
extractEvery: 10,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: true,
},
lastProcessedAssistantFloor: -1,
},
);
assert.equal(plan.canRun, true);
assert.deepEqual(endFloors, [1]);
assert.deepEqual(plan.batchAssistantTurns, [1]);
}
async function testLagModeRespectsExtractEveryAgainstEligibleWindow() {
const chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
{ is_user: true, mes: "u3" },
{ is_user: false, mes: "a3" },
];
const plan = buildAutoExtractionPlan({
chat,
settings: {
extractEvery: 2,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
},
lastProcessedAssistantFloor: -1,
});
assert.equal(plan.canRun, true);
assert.deepEqual(plan.eligibleAssistantTurns, [1, 3]);
assert.deepEqual(plan.batchAssistantTurns, [1, 3]);
assert.equal(plan.plannedBatchEndFloor, 3);
}
async function testGenerationEndedResumesPendingAutoExtractionAfterSettle() {
const harness = await createGenerationRecallHarness();
harness.chat = [
@@ -3799,6 +4056,48 @@ async function testGenerationEndedResumesPendingAutoExtractionAfterSettle() {
harness.result.clearPendingAutoExtraction();
}
async function testLagModePendingResumeKeepsLockedPreviousAssistantAfterLatestDisappears() {
const harness = await createGenerationRecallHarness();
harness.settings = {
extractEvery: 1,
extractAutoDelayLatestAssistant: true,
enableSmartTrigger: false,
};
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a2" },
];
harness.result.setGraphPersistenceState({
loadState: "loaded",
dbReady: true,
chatId: "chat-main",
});
harness.result.onGenerationStarted("normal", {}, false);
harness.invokeOnMessageReceived(3, "assistant");
await waitForTick();
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(
harness.result.getPendingAutoExtraction().targetEndFloor,
1,
);
harness.chat = [
{ is_user: true, mes: "u1" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
];
harness.result.onGenerationEnded();
await new Promise((resolve) => setTimeout(resolve, 180));
assert.equal(harness.runExtractionCalls.length, 1);
assert.equal(harness.runExtractionCalls[0]?.[0]?.lockedEndFloor, 1);
harness.result.clearPendingAutoExtraction();
}
async function testAutoExtractionDefersWhenGraphNotReady() {
const deferredReasons = [];
const statuses = [];
@@ -5755,7 +6054,12 @@ await testUserMessageRenderedRefreshesRecallUiAfterRealDomRender();
await testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender();
await testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask();
await testMessageReceivedDefersExtractionDuringHostGeneration();
await testMessageReceivedLagModeWaitsSilentlyForNextAssistant();
await testMessageReceivedLagModeQueuesPreviousAssistantOnly();
await testLagModeSmartTriggerOnlyScoresEligibleWindow();
await testLagModeRespectsExtractEveryAgainstEligibleWindow();
await testGenerationEndedResumesPendingAutoExtractionAfterSettle();
await testLagModePendingResumeKeepsLockedPreviousAssistantAfterLatestDisappears();
await testAutoExtractionDefersWhenGraphNotReady();
await testAutoExtractionDefersWhenAlreadyExtracting();
await testAutoExtractionDefersWhenHistoryRecoveryBusy();