From d8ce2b7d8895aaf75cb84fb17d73ef066082a1e0 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 18 Apr 2026 15:10:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20?= =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0:=20=D0=BF=D1=80=D0=BE=D1=82=D1=8F=D0=BD=D1=83=D1=82?= =?UTF-8?q?=D1=8C=20continuity=20snapshot=20=D0=B2=20living-chat=20organiz?= =?UTF-8?q?ation=20authority=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D1=86=D0=B5=D0=BD=D0=BA=D1=83=20?= =?UTF-8?q?=D1=80=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B2=20AGENTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 ++ ...ontinuity_stabilization_plan_2026-04-17.md | 4 + .../assistantLivingChatRuntimeAdapter.js | 16 ++- .../assistantLivingChatRuntimeAdapter.ts | 16 ++- .../assistantLivingChatRuntimeAdapter.test.ts | 35 ++++++ ..._saved_session_runtime_job-knjH7nTx3E.json | 114 ++++++++++++++++++ 6 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-knjH7nTx3E.json diff --git a/AGENTS.md b/AGENTS.md index 02f3336..fb6baf2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,13 @@ ## commit_message_rule - After applying fixes, always provide the user with a ready commit title in Russian. +## change_risk_rule +- After applying fixes, always provide `Степень опасности правки: X/10` immediately above the ready commit title. +- The score must use an integer scale from `1` to `10`, where: + - `1` = low-risk local change with narrow blast radius; + - `10` = high-risk architecture/runtime change with broad blast radius and mandatory close validation. +- The score must reflect real project risk, not optimism, and should help the user decide how much manual attention and replay validation the change deserves. + ## graphify This project has a graphify knowledge graph at graphify-out/. diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index f44fbef..f6d0600 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -241,6 +241,10 @@ Latest continuity-authority convergence evidence after the current route pass: - active organization continuity is now allowed to participate in organization-selection arbitration, instead of forcing route policy to reconstruct that context only from immediate clarification payloads; - a bare organization-selection turn after grounded bookkeeping continuity is no longer automatically classified as `non_domain_query_indexed` noise when the session still carries valid grounded business context; - session organization recovery inside the data-scope layer now has a final fallback to the same continuity snapshot, reducing one more duplicate path that used to rescan assistant history independently; +- the living-chat runtime now also consumes continuity-backed organization authority: + - deterministic organization-fact boundary replies can now trigger from grounded continuity even when `sessionScope.selectedOrganization` and `sessionScope.activeOrganization` are both empty at runtime entry; + - the chat layer now records whether it entered with grounded continuity and which organization came from that continuity snapshot, making future saved-session review less blind; + - proactive organization offer logic is now explicitly blocked when grounded address continuity already exists, so the chat layer does not re-offer company selection on top of an already grounded business session; - this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js index 8810569..0324270 100644 --- a/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantLivingChatRuntimeAdapter.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime; const assistantMemoryRecapPolicy_1 = require("./assistantMemoryRecapPolicy"); +const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy"); function formatIsoDateForReply(value) { const source = String(value ?? "").trim(); const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/); @@ -151,6 +152,10 @@ function buildAddressMemoryRecapReply(input) { } async function runAssistantLivingChatRuntime(input) { const userMessage = String(input.userMessage ?? ""); + const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + sessionItems: input.sessionItems, + toNonEmptyString: input.toNonEmptyString + }); const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage); const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage); @@ -164,9 +169,13 @@ async function runAssistantLivingChatRuntime(input) { let livingChatGroundingGuardApplied = false; let livingChatGroundingGuardReason = null; let livingChatProactiveScopeOfferApplied = false; - let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []); + const continuityActiveOrganization = input.toNonEmptyString(continuitySnapshot.activeOrganization); + let knownOrganizations = input.mergeKnownOrganizations([ + ...(Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : []), + continuityActiveOrganization + ]); let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); - let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization); + let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization) ?? continuityActiveOrganization; const memoryRecapContext = (0, assistantMemoryRecapPolicy_1.resolveAssistantLivingChatMemoryContext)({ modeDecisionReason: input.modeDecision?.reason ?? null, sessionItems: input.sessionItems @@ -265,6 +274,7 @@ async function runAssistantLivingChatRuntime(input) { } const shouldOfferProactiveOrganizationScope = !selectedOrganization && !activeOrganization && + !continuitySnapshot.hasGroundedAddressContext && !hasPriorAssistantTurn(input.sessionItems) && input.modeDecision?.mode === "chat" && input.hasLivingChatSignal(userMessage); @@ -330,6 +340,8 @@ async function runAssistantLivingChatRuntime(input) { ? input.mergeKnownOrganizations(dataScopeProbe.organizations) : [], living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null, + living_chat_continuity_grounded_context_detected: continuitySnapshot.hasGroundedAddressContext, + living_chat_continuity_active_organization: continuityActiveOrganization, living_chat_selected_organization: selectedOrganization ?? null, assistant_known_organizations: knownOrganizations, assistant_active_organization: activeOrganization ?? null, diff --git a/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts index bed8840..808d4de 100644 --- a/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts @@ -3,6 +3,7 @@ import { buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy, resolveAssistantLivingChatMemoryContext } from "./assistantMemoryRecapPolicy"; +import { resolveAssistantContinuitySnapshot } from "./assistantContinuityPolicy"; export interface AssistantLivingChatSessionScopeInput { knownOrganizations?: unknown[]; @@ -248,6 +249,10 @@ export async function runAssistantLivingChatRuntime( input: AssistantLivingChatRuntimeInput ): Promise { const userMessage = String(input.userMessage ?? ""); + const continuitySnapshot = resolveAssistantContinuitySnapshot({ + sessionItems: input.sessionItems, + toNonEmptyString: input.toNonEmptyString + }); const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage); const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage); @@ -262,9 +267,13 @@ export async function runAssistantLivingChatRuntime( let livingChatGroundingGuardApplied = false; let livingChatGroundingGuardReason: string | null = null; let livingChatProactiveScopeOfferApplied = false; - let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []); + const continuityActiveOrganization = input.toNonEmptyString(continuitySnapshot.activeOrganization); + let knownOrganizations = input.mergeKnownOrganizations([ + ...(Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : []), + continuityActiveOrganization + ]); let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization); - let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization); + let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization) ?? continuityActiveOrganization; const memoryRecapContext = resolveAssistantLivingChatMemoryContext({ modeDecisionReason: input.modeDecision?.reason ?? null, sessionItems: input.sessionItems @@ -362,6 +371,7 @@ export async function runAssistantLivingChatRuntime( const shouldOfferProactiveOrganizationScope = !selectedOrganization && !activeOrganization && + !continuitySnapshot.hasGroundedAddressContext && !hasPriorAssistantTurn(input.sessionItems) && input.modeDecision?.mode === "chat" && input.hasLivingChatSignal(userMessage); @@ -432,6 +442,8 @@ export async function runAssistantLivingChatRuntime( ? input.mergeKnownOrganizations(dataScopeProbe.organizations as unknown[]) : [], living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null, + living_chat_continuity_grounded_context_detected: continuitySnapshot.hasGroundedAddressContext, + living_chat_continuity_active_organization: continuityActiveOrganization, living_chat_selected_organization: selectedOrganization ?? null, assistant_known_organizations: knownOrganizations, assistant_active_organization: activeOrganization ?? null, diff --git a/llm_normalizer/backend/tests/assistantLivingChatRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantLivingChatRuntimeAdapter.test.ts index 9be473f..a39d1af 100644 --- a/llm_normalizer/backend/tests/assistantLivingChatRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantLivingChatRuntimeAdapter.test.ts @@ -219,4 +219,39 @@ describe("assistant living chat runtime adapter", () => { expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract"); expect(executeLlmChat).not.toHaveBeenCalled(); }); + it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => { + const executeLlmChat = vi.fn(async () => "raw-llm"); + const input = buildRuntimeInput({ + userMessage: "сколько лет альтернативе плюс", + sessionItems: [ + { + role: "assistant", + debug: { + execution_lane: "address_query", + answer_grounding_check: { + status: "grounded" + }, + detected_intent: "receivables_confirmed_as_of_date", + extracted_filters: { + organization: "ООО Альтернатива Плюс", + as_of_date: "2020-03-31" + } + } + } + ], + hasOrganizationFactLookupSignal: () => true, + buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => `org-boundary:${organization ?? "none"}`, + executeLlmChat + }); + + const output = await runAssistantLivingChatRuntime(input); + + expect(output.handled).toBe(true); + expect(output.chatText).toBe("org-boundary:ООО Альтернатива Плюс"); + expect(output.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary"); + expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс"); + expect(output.debug?.living_chat_continuity_grounded_context_detected).toBe(true); + expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс"); + expect(executeLlmChat).not.toHaveBeenCalled(); + }); }); diff --git a/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-knjH7nTx3E.json b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-knjH7nTx3E.json new file mode 100644 index 0000000..38aec95 --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-knjH7nTx3E.json @@ -0,0 +1,114 @@ +{ + "suite_id": "assistant_saved_session_runtime_job-knjH7nTx3E", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_runtime_v0_1", + "title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06", + "scenario_count": 1, + "case_ids": [ + "SAVED-001" + ], + "cases": [ + { + "case_id": "SAVED-001", + "scenario_tag": "saved_user_sessions_runtime", + "title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "приветик - че как там дела" + }, + { + "user_message": "расскажи что можешь интересного" + }, + { + "user_message": "кайф - что там на складе по остаткам?" + }, + { + "user_message": "а исторические остатки на другие даты умеешь?" + }, + { + "user_message": "давай на июль 2017" + }, + { + "user_message": "март 2016" + }, + { + "user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?" + }, + { + "user_message": "а кому продали?" + }, + { + "user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?" + }, + { + "user_message": "ндс можешь прикинуть на дату покупки рабочей станции?" + }, + { + "user_message": "а какой ндс мы должны сгрузить на март 2020?" + }, + { + "user_message": "прикинь какой ндс нам надо заплатить на февраль 2017" + }, + { + "user_message": "кто у нас самый доходный клиент за все время" + }, + { + "user_message": "кто нам должен денег на май 2017" + }, + { + "user_message": "а какой ндс мы должны примерно заплатить за этот период?" + }, + { + "user_message": "мы должны комуто денег на сегодня?" + }, + { + "user_message": "а нам?" + }, + { + "user_message": "какой у нас самый доходный год" + }, + { + "user_message": "а за 2017 мы скок заработали?" + }, + { + "user_message": "сколько вообще денег мы заработали за все время?" + }, + { + "user_message": "ты умеешь считать дельту по договорам?" + }, + { + "user_message": "по чепурнову покажи все доки" + }, + { + "user_message": "а по свк" + }, + { + "user_message": "а сейчас у нас есть что на складе?" + }, + { + "user_message": "что нам отгружал чепурнов? какой товар или услугу?" + }, + { + "user_message": "какие остатки на складе на сегодня" + }, + { + "user_message": "остатки на март 2016" + }, + { + "user_message": "хвосты покажи по счету 60 на август 2022" + }, + { + "user_message": "Есть ли остатки товара, которые закупались очень давно" + }, + { + "user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020" + }, + { + "user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?" + } + ] + } + ] +} \ No newline at end of file