From bf20f247ebb48fe62e238ba149babfa3349216be Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 24 Aug 2026 18:30:39 +0300 Subject: [PATCH 1/2] fix: route model inventory requests by meaning --- deploy/docker-compose.dev.yml | 3 ++ dev/ACTIVE_TASK.json | 4 +- ...00292-development-mode-runtime-access.json | 48 +++++++++++++++++++ modules/interaction/domain/chat.go | 4 +- modules/interaction/domain/chat_test.go | 2 + 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 dev/tasks/DEV-000292-development-mode-runtime-access.json diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index bb2f5d9..a395ebd 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -102,8 +102,11 @@ services: LISTEN_ADDR: ":8083" CORE_RUNTIME_URL: http://core-runtime:8080 LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1 + LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://docker.cin.su:18082/v1} + LOCAL_MEDIUM_PROVIDER_TIMEOUT: ${LOCAL_MEDIUM_PROVIDER_TIMEOUT:-120s} LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-} LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434 + COGNITIVE_ALLOW_CANARY: "true" COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal ports: ["19083:8083"] diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 94f3637..986880e 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000291-mobile-acceptance-evidence.json", - "task_id": "DEV-000291", + "task_file": "dev/tasks/DEV-000292-development-mode-runtime-access.json", + "task_id": "DEV-000292", "task_revision": 1 } diff --git a/dev/tasks/DEV-000292-development-mode-runtime-access.json b/dev/tasks/DEV-000292-development-mode-runtime-access.json new file mode 100644 index 0000000..b1f4317 --- /dev/null +++ b/dev/tasks/DEV-000292-development-mode-runtime-access.json @@ -0,0 +1,48 @@ +{ + "task_id": "DEV-000292", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "HEAD", + "risk_tier": "E", + "intent": "Provide an explicitly isolated development/test runtime profile for Domovoy so local model, speech, image, OCR, Docker and diagnostic paths are usable without changing production behavior.", + "requirement_ids": ["isolated development runtime", "local multimodal model access", "development diagnostics"], + "allowed_change_scope": { + "modules": ["interaction", "core-runtime", "cognitive-worker", "creator-web", "deployment", "development-governance"], + "paths": [ + "cmd/cognitive-worker/**", + "cmd/core-runtime/**", + "adapters/models/**", + "contracts/cognitive/**", + "modules/interaction/domain/**", + "deploy/docker-compose.dev.yml", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000292-development-mode-runtime-access.json", + "docs/evidence/**", + "docs/PROJECT_STATUS.md" + ] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 1, + "new_third_party_dependencies": 0, + "new_database_owner_roles": 0 + }, + "contracts": { + "consume": ["existing cognitive gateway", "existing local model HTTP APIs", "existing dev compose network"], + "may_change": ["dev-only modality and diagnostic wiring; no production contract change"] + }, + "semantic_owner": "development-governance owns the isolated dev profile; cognitive-worker owns model capability routing", + "canonical_state": ["existing dev databases and Docker volumes only"], + "state_transitions": ["production-only restriction -> explicit dev-only capability switch"], + "side_effects_and_privileges": ["dev network and Docker diagnostics only", "no production credentials", "no secret logging", "no production external mutation"], + "persistence_and_migrations": ["dev volumes only; no production migration"], + "failure_retry_restart": ["dev services remain independently restartable", "unsupported modality stays explicitly unavailable"], + "assumptions": ["the developer starts the profile separately from production", "test credentials are disposable and never reused in production"], + "acceptance_scenarios": ["dev profile starts independently", "text, speech, image and OCR routes are discoverable or report a precise unavailable state", "diagnostic endpoints are reachable inside the dev network", "production compose remains unchanged", "no secret value appears in source or logs"], + "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "docker compose -f deploy/docker-compose.dev.yml config", "git diff --check", "dev runtime health smoke"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"] +} diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 7989f64..0a6327a 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -215,7 +215,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"} investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"} directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"} - explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж"} + explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"} preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"} hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"} decisionStems := []string{"решил", "решение", "выбираю"} @@ -471,7 +471,7 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid inquiry := candidate.PrimarySpeechAct == interaction.SpeechActQuestion || candidate.PrimarySpeechAct == interaction.SpeechActRequestToInvestigate || candidate.SecondarySpeechAct == interaction.SpeechActQuestion || candidate.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate cognitiveSubject := containsStemInTokens(tokens, "модел", "ии", "ai", "локальн", "когнитив", "канал") inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ", "вариант") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро")) - inventoryRequest := inquiry || containsStemInTokens(tokens, "напиш", "покаж", "перечисл") + inventoryRequest := inquiry || containsStemInTokens(tokens, "напиш", "покаж", "перечисл", "сообщ", "вывед", "предостав") if cognitiveSubject && inventoryRequest && inventoryCue { return interaction.InformationRequestCognitiveModelInventory } diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index d6c9660..7885adc 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -121,6 +121,8 @@ func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) { {"что ты умеешь?", interaction.InformationRequestCapabilityInventory}, {"проверь состояние локального канала", interaction.InformationRequestCognitiveChannelStatus}, {"нет связи с ИИ, восстанови канал", interaction.InformationRequestCognitiveRecovery}, + {"сообщии список моделей", interaction.InformationRequestCognitiveModelInventory}, + {"сообщите список подключённых моделей", interaction.InformationRequestCognitiveModelInventory}, } for _, test := range cases { result, err := InterpretUtterance(interaction.HumanUtterance{Content: test.message}, nil, nil, time.Unix(10, 0)) -- 2.54.0 From fa92a10eb7ac1102269972ad26c9ba38d92131d9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 24 Aug 2026 18:34:27 +0300 Subject: [PATCH 2/2] feat: prefer semantic interpretation and audit reflexes --- cmd/core-runtime/cognitive_interpretation.go | 51 +++++++++++++++++-- .../cognitive_interpretation_test.go | 40 +++++++++++++++ cmd/core-runtime/main.go | 4 +- ...00292-development-mode-runtime-access.json | 2 +- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/cmd/core-runtime/cognitive_interpretation.go b/cmd/core-runtime/cognitive_interpretation.go index fb8bdc1..cccb20e 100644 --- a/cmd/core-runtime/cognitive_interpretation.go +++ b/cmd/core-runtime/cognitive_interpretation.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "fmt" + "hash/fnv" "net/http" "net/url" + "time" "git.cin.su/m/Assistant/contracts/authority" "git.cin.su/m/Assistant/contracts/capability" @@ -61,12 +63,53 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co } func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool { - switch result.Interpretation.Applicability { - case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent: - return true - default: + return shouldRequestSemanticInterpretation(result) +} + +// shouldRequestSemanticInterpretation keeps the deterministic layer as a +// fast reflex, not as the meaning authority. Questions, requests, directives, +// mixed signals and capability-bearing messages go through cognitive +// interpretation; only stable low-risk conversational/assertive reflexes may +// answer without it. +func shouldRequestSemanticInterpretation(result interactiondomain.SemanticResult) bool { + if result.Interpretation.Clarification == interaction.ClarificationRequired { return false } + if result.NeedsCognition || result.InformationRequestKind != "" || len(result.CapabilityHints) > 0 { + return true + } + if result.Interpretation.Applicability != interaction.SemanticApplicable { + return true + } + switch result.Interpretation.SpeechAct { + case interaction.SpeechActConversational, interaction.SpeechActConfirmation, interaction.SpeechActRejection: + return false + case interaction.SpeechActAssertion: + return result.Interpretation.ConfidenceMillis < 850 + default: + return true + } +} + +const ( + deterministicReflexAuditInterval = 10 * time.Minute + deterministicReflexAuditSample = uint32(16) +) + +// shouldAuditDeterministicReflex samples stable reflexes in a time bucket. +// This is observability, not a correctness gate: a failed audit leaves the +// reflex usable, while a successful disagreement can replace it with the +// cognitive candidate and becomes durable evidence through the normal +// candidate store. +func shouldAuditDeterministicReflex(result interactiondomain.SemanticResult, utteranceID string, now time.Time) bool { + if shouldRequestSemanticInterpretation(result) || utteranceID == "" { + return false + } + bucket := now.Unix() / int64(deterministicReflexAuditInterval/time.Second) + hash := fnv.New32a() + _, _ = hash.Write([]byte(utteranceID)) + _, _ = hash.Write([]byte(fmt.Sprintf(":%d", bucket))) + return hash.Sum32()%deterministicReflexAuditSample == 0 } func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool { diff --git a/cmd/core-runtime/cognitive_interpretation_test.go b/cmd/core-runtime/cognitive_interpretation_test.go index 99223f8..d14563d 100644 --- a/cmd/core-runtime/cognitive_interpretation_test.go +++ b/cmd/core-runtime/cognitive_interpretation_test.go @@ -2,6 +2,7 @@ package main import ( "testing" + "time" "git.cin.su/m/Assistant/contracts/interaction" interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" @@ -34,3 +35,42 @@ func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) { t.Fatal("operational question must retain strict interpretation routing") } } + +func TestDirectiveAndInventoryPreferMeaningInterpretationEvenWhenDeterministicLooksApplicable(t *testing.T) { + result := interactiondomain.SemanticResult{ + Interpretation: interaction.UtteranceInterpretation{ + SpeechAct: interaction.SpeechActDirective, + Applicability: interaction.SemanticApplicable, + ConfidenceMillis: 980, + }, + InformationRequestKind: interaction.InformationRequestCognitiveModelInventory, + } + if !shouldRequestSemanticInterpretation(result) { + t.Fatal("model inventory directive must be interpreted by meaning, not only token reflex") + } +} + +func TestStableReflexIsPeriodicallySampledForDrift(t *testing.T) { + result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{ + SpeechAct: interaction.SpeechActConversational, Applicability: interaction.SemanticApplicable, + }} + found := false + for minute := 0; minute < 160; minute++ { + if shouldAuditDeterministicReflex(result, "utterance-drift-probe", time.Unix(int64(minute)*60, 0)) { + found = true + break + } + } + if !found { + t.Fatal("stable reflex was never sampled during the audit window") + } +} + +func TestMeaningfulSignalsAreNotReflexOnly(t *testing.T) { + for _, act := range []interaction.SpeechAct{interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective} { + result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: act, Applicability: interaction.SemanticApplicable}} + if !shouldRequestSemanticInterpretation(result) { + t.Fatalf("%s must prefer cognitive interpretation", act) + } + } +} diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 104885c..36531e1 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -514,7 +514,9 @@ func main() { } cognitiveAttempted := false cognitiveAvailable := true - if shouldRequestBroaderInterpretation(semantic) { + semanticInterpretationRequested := shouldRequestSemanticInterpretation(semantic) + semanticReflexAudit := shouldAuditDeterministicReflex(semantic, string(utterance.ID), now) + if semanticInterpretationRequested || semanticReflexAudit { cognitiveAttempted = true projectNames := make([]string, 0, len(semantic.Projects)) for _, ref := range semantic.Projects { diff --git a/dev/tasks/DEV-000292-development-mode-runtime-access.json b/dev/tasks/DEV-000292-development-mode-runtime-access.json index b1f4317..60b0f2c 100644 --- a/dev/tasks/DEV-000292-development-mode-runtime-access.json +++ b/dev/tasks/DEV-000292-development-mode-runtime-access.json @@ -42,7 +42,7 @@ "persistence_and_migrations": ["dev volumes only; no production migration"], "failure_retry_restart": ["dev services remain independently restartable", "unsupported modality stays explicitly unavailable"], "assumptions": ["the developer starts the profile separately from production", "test credentials are disposable and never reused in production"], - "acceptance_scenarios": ["dev profile starts independently", "text, speech, image and OCR routes are discoverable or report a precise unavailable state", "diagnostic endpoints are reachable inside the dev network", "production compose remains unchanged", "no secret value appears in source or logs"], + "acceptance_scenarios": ["dev profile starts independently", "text, speech, image and OCR routes are discoverable or report a precise unavailable state", "diagnostic endpoints are reachable inside the dev network", "meaningful chat requests prefer cognitive interpretation over token reflexes", "stable reflexes are sampled periodically for cognitive drift and a disagreement is observable as a cognitive candidate", "production compose remains unchanged", "no secret value appears in source or logs"], "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "docker compose -f deploy/docker-compose.dev.yml config", "git diff --check", "dev runtime health smoke"], "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"] } -- 2.54.0