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 f492383..f44fbef 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 @@ -234,6 +234,15 @@ Still open after the accepted phase11 replay: - answer shaping on some long exact list answers is still heavier than the target human product feel, even though the truth path and routing are now correct; - the next architecture slice should move to wider saved-session acceptance coverage and humanized exact-answer presentation, not back to isolated prompt-level repairs. +Latest continuity-authority convergence evidence after the current route pass: + +- the route hot path now consumes the shared continuity snapshot directly instead of relying only on local `findLastGrounded...` helpers: + - grounded address context can now survive into route arbitration even when the legacy local helper returns nothing for the current turn shape; + - 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; +- 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) The project is now moving from: diff --git a/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js b/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js index 272153e..1820c81 100644 --- a/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantDataScopePolicy.js @@ -219,6 +219,11 @@ function createAssistantDataScopePolicy(deps) { return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(collected, 20); } function findLastAssistantActiveOrganization(items) { + const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + sessionItems: items, + toNonEmptyString: assistantOrganizationMatcher_1.normalizeOrganizationScopeValue + }); + const continuityOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(continuitySnapshot.activeOrganization); for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { const item = Array.isArray(items) ? items[index] : null; if (!item || typeof item !== "object" || item.role !== "assistant") { @@ -243,7 +248,7 @@ function createAssistantDataScopePolicy(deps) { } } } - return null; + return continuityOrganization; } function extractOrganizationFactsFromRows(rows) { const names = []; diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js index 77b0f46..0afc1a3 100644 --- a/llm_normalizer/backend/dist/services/assistantRoutePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicy.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.createAssistantRoutePolicy = createAssistantRoutePolicy; // @ts-nocheck +const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy"); const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ "period_coverage_profile", "document_type_and_account_section_profile", @@ -71,17 +72,26 @@ function createAssistantRoutePolicy(deps) { const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" ? input.sessionOrganizationScope : null; - const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems); + const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + sessionItems, + toNonEmptyString + }); + const continuityActiveOrganization = normalizeOrganizationScopeValue(sessionOrganizationScope?.activeOrganization) ?? + normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems) ?? + continuitySnapshot.lastGroundedAddressDebug; const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) ? mergeKnownOrganizations([ ...lastOrganizationClarificationDebug.organization_candidates, ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) ? sessionOrganizationScope.knownOrganizations - : [])) + : [])), + continuityActiveOrganization ]) : []; - const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization); + const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization) ?? + continuityActiveOrganization; const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? @@ -194,7 +204,7 @@ function createAssistantRoutePolicy(deps) { hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); - const organizationClarificationContinuationDetected = Boolean(followupContext && + const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) && lastOrganizationClarificationDebug && organizationClarificationSelection && !dataScopeMetaQuery && @@ -691,7 +701,9 @@ function createAssistantRoutePolicy(deps) { repairedEffectiveAddressUserMessage, sessionItems })); - const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); + const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || + continuitySnapshot.hasGroundedAddressContext || + toNonEmptyString(followupContext?.previous_intent)); const metaFollowupOverGroundedAnswer = isMetaFollowupOverGroundedAnswer({ followupContext, hasPriorAddressAnswerContext, diff --git a/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts b/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts index c78b370..37970bd 100644 --- a/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantDataScopePolicy.ts @@ -4,6 +4,7 @@ import { normalizeOrganizationScopeValue } from "./assistantOrganizationMatcher"; import { + resolveAssistantContinuitySnapshot, isGroundedAddressDebug, readAddressDebugOrganization } from "./assistantContinuityPolicy"; @@ -296,6 +297,12 @@ export function createAssistantDataScopePolicy(deps: AssistantDataScopePolicyDep } function findLastAssistantActiveOrganization(items: unknown[]): string | null { + const continuitySnapshot = resolveAssistantContinuitySnapshot({ + sessionItems: items, + toNonEmptyString: normalizeOrganizationScopeValue + }); + const continuityOrganization = normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) { const item = Array.isArray(items) ? items[index] : null; if (!item || typeof item !== "object" || (item as { role?: string }).role !== "assistant") { @@ -323,7 +330,7 @@ export function createAssistantDataScopePolicy(deps: AssistantDataScopePolicyDep } } - return null; + return continuityOrganization; } function extractOrganizationFactsFromRows(rows: unknown[]): AssistantOrganizationFacts { diff --git a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts index 1501081..ee08cd6 100644 --- a/llm_normalizer/backend/src/services/assistantRoutePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantRoutePolicy.ts @@ -1,4 +1,6 @@ // @ts-nocheck +import { resolveAssistantContinuitySnapshot } from "./assistantContinuityPolicy"; + const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([ "period_coverage_profile", "document_type_and_account_section_profile", @@ -107,17 +109,26 @@ export function createAssistantRoutePolicy(deps) { const sessionOrganizationScope = input?.sessionOrganizationScope && typeof input.sessionOrganizationScope === "object" ? input.sessionOrganizationScope : null; - const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems); + const continuitySnapshot = resolveAssistantContinuitySnapshot({ + sessionItems, + toNonEmptyString + }); + const continuityActiveOrganization = normalizeOrganizationScopeValue(sessionOrganizationScope?.activeOrganization) ?? + normalizeOrganizationScopeValue(continuitySnapshot.activeOrganization); + const lastGroundedAddressDebug = findLastGroundedAddressAnswerDebug(sessionItems) ?? + continuitySnapshot.lastGroundedAddressDebug; const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems); const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) ? mergeKnownOrganizations([ ...lastOrganizationClarificationDebug.organization_candidates, ...((Array.isArray(sessionOrganizationScope?.knownOrganizations) ? sessionOrganizationScope.knownOrganizations - : [])) + : [])), + continuityActiveOrganization ]) : []; - const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization); + const organizationClarificationSelectionFromScope = normalizeOrganizationScopeValue(sessionOrganizationScope?.selectedOrganization) ?? + continuityActiveOrganization; const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ?? resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ?? @@ -230,7 +241,7 @@ export function createAssistantRoutePolicy(deps) { hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) || hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) || hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage))); - const organizationClarificationContinuationDetected = Boolean(followupContext && + const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) && lastOrganizationClarificationDebug && organizationClarificationSelection && !dataScopeMetaQuery && @@ -727,7 +738,9 @@ export function createAssistantRoutePolicy(deps) { repairedEffectiveAddressUserMessage, sessionItems })); - const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent)); + const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || + continuitySnapshot.hasGroundedAddressContext || + toNonEmptyString(followupContext?.previous_intent)); const metaFollowupOverGroundedAnswer = isMetaFollowupOverGroundedAnswer({ followupContext, hasPriorAddressAnswerContext, diff --git a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts index 0c03958..cfab998 100644 --- a/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantRoutePolicy.test.ts @@ -364,4 +364,46 @@ describe("assistantRoutePolicy", () => { expect(decision.toolGateReason).toBe("address_signal_detected"); expect(decision.livingMode).toBe("address_data"); }); + + it("does not mark organization selection after grounded continuity as non-domain noise", () => { + const policy = buildPolicy({ + findLastGroundedAddressAnswerDebug: () => null, + findLastOrganizationClarificationAddressDebug: () => ({ + organization_candidates: ["Org A", "Org B"] + }), + resolveOrganizationSelectionFromMessage: (userMessage: unknown, knownOrganizations: unknown) => { + const normalized = String(userMessage ?? "").trim().toLowerCase(); + const candidates = Array.isArray(knownOrganizations) ? knownOrganizations.map((item) => String(item)) : []; + return candidates.find((candidate) => candidate.toLowerCase() === normalized) ?? null; + } + }); + + const decision = policy.resolveAssistantOrchestrationDecision({ + rawUserMessage: "Org A", + effectiveAddressUserMessage: "Org A", + followupContext: null, + llmPreDecomposeMeta: null, + sessionItems: [ + { + role: "assistant", + debug: { + execution_lane: "address_query", + answer_grounding_check: { status: "grounded" }, + extracted_filters: { + organization: "Org A", + as_of_date: "2021-03-31" + }, + detected_intent: "receivables_confirmed_as_of_date" + } + } + ], + useMock: false + }); + + expect(decision.runAddressLane).toBe(false); + expect(decision.toolGateReason).toBe("no_address_signal_after_l0"); + expect(decision.livingMode).toBe("chat"); + expect(decision.orchestrationContract?.hard_meta_mode).toBeNull(); + expect(decision.orchestrationContract?.followup_context_detected).toBe(false); + }); });