From 5387010a157e3fae16de401945e1f6808cbf24ca Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 29 Aug 2026 23:28:10 +0300 Subject: [PATCH] harden Gen2 household introspection and readiness --- cmd/core-runtime/main.go | 11 +++++- deploy/docker-compose.prod.yml | 15 ++++++-- modules/interaction/domain/chat.go | 12 ++++++- modules/interaction/domain/chat_test.go | 1 + modules/interaction/domain/personal_memory.go | 35 +++++++++++++++++-- .../domain/personal_memory_test.go | 14 ++++++++ modules/interaction/domain/semantics.go | 14 +++++--- modules/interaction/domain/semantics_test.go | 7 ++++ 8 files changed, 97 insertions(+), 12 deletions(-) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 7c1679d..552e171 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -157,7 +157,10 @@ func main() { mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{ health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")), health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"), - health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"), + // A running worker is not necessarily ready to serve cognitive traffic. + // Keep deterministic paths alive, but do not advertise overall readiness + // while the cognitive dependency is still warming. + health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/readyz"), }}) admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)} mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) { @@ -806,6 +809,12 @@ func main() { ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis, } helpPath := workdomain.DetermineHelpPath(planningInput) + // Introspection requests are read-only household UX. A cognitive + // candidate must not reinterpret them as an executable delegation. + switch semantic.InformationRequestKind { + case interaction.InformationRequestCapabilityInventory, interaction.InformationRequestLocalCapabilityInventory, interaction.InformationRequestCognitiveModelInventory, interaction.InformationRequestCognitiveCurrentModel, interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery, interaction.InformationRequestMemoryInventory: + helpPath = work.HelpPath{} + } if responseErr == nil && responseResult.Handled { helpPath = work.HelpPath{} } diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index cd3b90c..292ad27 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -258,10 +258,17 @@ services: LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors} COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal COGNITIVE_ALLOW_CANARY: "true" - depends_on: [core-runtime, cognitive-lifeboat] volumes: - assistant_core_secrets:/run/secrets:ro networks: [default, llm-local] + depends_on: + core-runtime: { condition: service_started } + cognitive-lifeboat: { condition: service_healthy } + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"] + interval: 10s + timeout: 5s + retries: 20 cognitive-lifeboat: image: ghcr.io/ggml-org/llama.cpp:server @@ -328,7 +335,11 @@ services: COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083 AGENT_WORKER_URL: http://agent-worker:8090 ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown} - depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker] + depends_on: + core-runtime: { condition: service_started } + continuity-runtime: { condition: service_started } + commit-runtime: { condition: service_started } + cognitive-worker: { condition: service_healthy } healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"] interval: 10s diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 3200196..39c7f74 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -616,6 +616,9 @@ func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens [] if !inquiry { return false } + if focusIntrospectionQuestion(tokens) { + return false + } currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо") observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2) placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац", "бренд", "товар", "артикул", "продавец", "маркетплейс", "wildberries", "wb") @@ -710,7 +713,7 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid if cognitiveSubject && inventoryRequest && inventoryCue { return interaction.InformationRequestCognitiveModelInventory } - capabilitySubject := containsStemInTokens(tokens, "уме", "возможност", "способн", "capabilit") || (containsMeaningfulInterrogative(tokens) && containsToken(tokens, "можешь")) + capabilitySubject := containsStemInTokens(tokens, "уме", "возможност", "способн", "доступн", "инструмент", "capabilit") || (containsMeaningfulInterrogative(tokens) && containsToken(tokens, "можешь")) if inquiry && capabilitySubject { if containsStemInTokens(tokens, "без", "внешн", "локальн") && containsStemInTokens(tokens, "модел", "ии", "ai") { return interaction.InformationRequestLocalCapabilityInventory @@ -727,6 +730,13 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid return "" } +func focusIntrospectionQuestion(tokens []string) bool { + if containsStemInTokens(tokens, "фокус", "focus", "контекст") && (containsStemInTokens(tokens, "сейчас", "текущ", "работ", "контекст") || containsMeaningfulInterrogative(tokens)) { + return true + } + return containsStemInTokens(tokens, "над", "делаем", "работаем") && containsStemInTokens(tokens, "сейчас", "текущ", "мы") +} + func LocationContextReply(message string) string { if location := personalCurrentLocation(message); location != "" { return "Запомнил: сейчас вы в " + location + "." diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index ee4cc74..c9ac96a 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -142,6 +142,7 @@ func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) { {"почему ты не смог найти ответ?", interaction.InformationRequestResearchFailureExplanation}, {"что ты можешь сделать без внешних моделей?", interaction.InformationRequestLocalCapabilityInventory}, {"что ты умеешь?", interaction.InformationRequestCapabilityInventory}, + {"что доступно в этом чате?", interaction.InformationRequestCapabilityInventory}, {"проверь состояние локального канала", interaction.InformationRequestCognitiveChannelStatus}, {"нет связи с ИИ, восстанови канал", interaction.InformationRequestCognitiveRecovery}, {"сообщии список моделей", interaction.InformationRequestCognitiveModelInventory}, diff --git a/modules/interaction/domain/personal_memory.go b/modules/interaction/domain/personal_memory.go index 5e65ce5..af5cb46 100644 --- a/modules/interaction/domain/personal_memory.go +++ b/modules/interaction/domain/personal_memory.go @@ -18,6 +18,7 @@ type PersonalMemory struct { type PersonalMemoryFact struct { Kind string + Subject string Value string ConfidenceMillis int64 SourceUtteranceID interaction.HumanUtteranceID @@ -54,6 +55,7 @@ func CompilePersonalMemory(history []interaction.HumanUtterance) PersonalMemory if preference := personalPreference(utterance.Content); preference != "" { preferences = append(preferences, PersonalMemoryFact{ Kind: "PREFERENCE", + Subject: preferenceSubject(preference), Value: preference, ConfidenceMillis: 900, SourceUtteranceID: utterance.ID, @@ -75,6 +77,21 @@ func CompilePersonalMemory(history []interaction.HumanUtterance) PersonalMemory return result } +// LatestPreference returns the current value for a semantic subject while +// retaining older facts for historical evidence. +func (m PersonalMemory) LatestPreference(subject string) (PersonalMemoryFact, bool) { + var latest PersonalMemoryFact + for _, fact := range m.Facts { + if fact.Kind != "PREFERENCE" || fact.Subject != subject { + continue + } + if latest.Kind == "" || fact.ObservedAtUnixMillis >= latest.ObservedAtUnixMillis { + latest = fact + } + } + return latest, latest.Kind != "" +} + func (m PersonalMemory) CurrentLocation() string { for _, fact := range m.Facts { if fact.Kind == personalMemoryCurrentLocation { @@ -112,11 +129,16 @@ func RecallPersonalPreference(history []interaction.HumanUtterance, question str if !strings.Contains(lowerQuestion, "цвет") || (!strings.Contains(lowerQuestion, "какой") && !strings.Contains(lowerQuestion, "назвал")) { return "" } - for _, fact := range CompilePersonalMemory(history).Facts { - if fact.Kind != "PREFERENCE" || !strings.Contains(strings.ToLower(fact.Value), "цвет") { + memory := CompilePersonalMemory(history) + for _, fact := range memory.Facts { + if fact.Kind != "PREFERENCE" || fact.Subject != "тестовый цвет" { continue } - value := strings.TrimSpace(fact.Value) + current, ok := memory.LatestPreference(fact.Subject) + if !ok || current.SourceUtteranceID != fact.SourceUtteranceID { + continue + } + value := strings.TrimSpace(current.Value) if separator := strings.Index(value, "—"); separator >= 0 { value = strings.TrimSpace(value[separator+len("—"):]) } @@ -127,6 +149,13 @@ func RecallPersonalPreference(history []interaction.HumanUtterance, question str return "" } +func preferenceSubject(preference string) string { + if separator := strings.Index(preference, "—"); separator >= 0 { + return strings.TrimSpace(preference[:separator]) + } + return strings.TrimSpace(preference) +} + func personalCurrentLocation(message string) string { tokens := semanticTokens(message) if len(tokens) < 4 || tokens[0] != "я" || !containsStemInTokens(tokens, "сейчас", "нахож", "жив") { diff --git a/modules/interaction/domain/personal_memory_test.go b/modules/interaction/domain/personal_memory_test.go index cbc5cba..55b0c62 100644 --- a/modules/interaction/domain/personal_memory_test.go +++ b/modules/interaction/domain/personal_memory_test.go @@ -48,3 +48,17 @@ func TestRecallPersonalPreferenceUsesProjectedAssertion(t *testing.T) { t.Fatalf("answer=%q", answer) } } + +func TestCompilePersonalMemoryRanksCurrentSubjectWithoutDroppingHistory(t *testing.T) { + memory := CompilePersonalMemory([]interaction.HumanUtterance{ + {ID: "old", Content: "запомни: мой тестовый цвет — фиолетовый", ReceivedAtUnixMillis: 100}, + {ID: "new", Content: "запомни: мой тестовый цвет — acceptance-m5-синий-914", ReceivedAtUnixMillis: 200}, + }) + current, ok := memory.LatestPreference("тестовый цвет") + if !ok || current.Value != "тестовый цвет — acceptance-m5-синий-914" { + t.Fatalf("current=%+v ok=%t", current, ok) + } + if len(memory.Facts) != 2 { + t.Fatalf("historical evidence was dropped: %+v", memory.Facts) + } +} diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index b3c8e57..1965b58 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -86,9 +86,12 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati result.Relations = append(result.Relations, relations...) } result.Reply = semanticReply(utterance.Content, result, previousFocus) - if isCurrentFocusQuestion(utterance.Content, selectedCandidate.PrimarySpeechAct) && previousFocus != nil { + if isCurrentFocusQuestion(utterance.Content, selectedCandidate.PrimarySpeechAct) { result.NeedsCognition = false result.Interpretation.NeedsCognition = false + result.CapabilityHints = nil + selectedCandidate.CapabilityHints = nil + result.Candidates[0] = selectedCandidate } return result, nil } @@ -343,8 +346,10 @@ func semanticReply(message string, result SemanticResult, previousFocus *situati case interaction.SpeechActRejection: return "Понял." case interaction.SpeechActQuestion: - lower := strings.ToLower(message) - if previousFocus != nil && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")) { + if focusIntrospectionQuestion(semanticTokens(message)) { + if previousFocus == nil { + return "Сейчас активный Focus не выбран." + } return "Сейчас в фокусе: " + previousFocus.ProjectDisplayName + "." } if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence { @@ -385,8 +390,7 @@ func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool { if act != interaction.SpeechActQuestion { return false } - lower := strings.ToLower(message) - return strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем") + return focusIntrospectionQuestion(semanticTokens(message)) } func hasCorrectionMeaning(candidate interaction.SemanticCandidate, utterance interaction.HumanUtterance) bool { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index ba7bcfa..5b87026 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -293,6 +293,13 @@ func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) { } } +func TestInterpretUtteranceEnglishFocusIntrospectionStaysLocal(t *testing.T) { + result := interpretForTest(t, "Какой сейчас Focus разговора?", activeFocus("assistant", "Assistant"), "") + if result.Reply != "Сейчас в фокусе: Assistant." || result.NeedsCognition || len(result.CapabilityHints) != 0 { + t.Fatalf("focus introspection escaped local route: %+v", result) + } +} + func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) { result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "") if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {