From 4f06720e3fa5e95ee69e1907d5eb111ac5481036 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 01:29:45 +0300 Subject: [PATCH 1/4] feat: keep Domovoy cognitive recovery active --- cmd/cognitive-worker/discovery.go | 2 +- cmd/cognitive-worker/gateway.go | 46 +++++++++++++++++-- cmd/cognitive-worker/gateway_registry_test.go | 13 ++++++ cmd/cognitive-worker/interpretation.go | 21 ++++++++- cmd/cognitive-worker/main.go | 15 ++++-- deploy/docker-compose.prod.yml | 6 ++- ...DEV-000085-builtin-cognitive-recovery.json | 23 ++++++++++ .../DEV-000086-local-model-baseline.json | 23 ++++++++++ ...00087-cognitive-recovery-orchestrator.json | 23 ++++++++++ 9 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 dev/tasks/DEV-000085-builtin-cognitive-recovery.json create mode 100644 dev/tasks/DEV-000086-local-model-baseline.json create mode 100644 dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json diff --git a/cmd/cognitive-worker/discovery.go b/cmd/cognitive-worker/discovery.go index 68c4a83..9ca43fe 100644 --- a/cmd/cognitive-worker/discovery.go +++ b/cmd/cognitive-worker/discovery.go @@ -23,7 +23,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) { return } discovered, probe := registration.Discovery.Discover(r.Context()) - snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: capability.DataPolicyLocalOnly, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()} + snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()} for _, model := range discovered { snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved}) } diff --git a/cmd/cognitive-worker/gateway.go b/cmd/cognitive-worker/gateway.go index d7b3a31..d87fa0a 100644 --- a/cmd/cognitive-worker/gateway.go +++ b/cmd/cognitive-worker/gateway.go @@ -32,6 +32,7 @@ type providerRegistration struct { CredentialRef capability.CredentialRef AuthMode capability.AuthMode BillingMode capability.BillingMode + DataPolicy capability.DataPolicy Provider advisoryProvider Discovery models.LocalProvider } @@ -39,6 +40,8 @@ type providerRegistration struct { type cognitiveGateway struct { resolver bindingResolver providers map[capability.ProviderKind][]providerRegistration + builtinLocal *providerRegistration + externalRecovery []providerRegistration now func() time.Time allowCanary bool canaryMaxOutputBytes int @@ -77,7 +80,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)} selected, err := g.resolver.Resolve(r.Context(), requirement) if err != nil { - writeGatewayResponse(w, builtinAdvisory(request)) + writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes)) return } if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged { @@ -86,7 +89,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request } provider := g.provider(selected) if provider == nil { - writeGatewayResponse(w, builtinAdvisory(request)) + writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes)) return } invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID} @@ -114,7 +117,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request } } if response.FinishReason != cognitive.FinishCompleted && fallbackEligible(response.FinishReason) { - writeGatewayResponse(w, builtinAdvisory(request)) + writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes)) return } writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}) @@ -164,6 +167,43 @@ func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.Gateway return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted} } +func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse { + if g.builtinLocal != nil && g.builtinLocal.Provider != nil { + invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"} + response, err := g.builtinLocal.Provider.Advise(ctx, invocation) + if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" { + return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage} + } + } + if request.EgressAcknowledged { + if response, ok := g.recoverExternalAdvisory(ctx, request, maxOutputBytes); ok { + return response + } + } + return builtinAdvisory(request) +} + +func (g cognitiveGateway) recoverExternalAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) (cognitive.GatewayAdvisoryResponse, bool) { + for _, candidate := range g.externalRecovery { + if candidate.Provider == nil || candidate.Discovery == nil { + continue + } + probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second) + modelsFound, probe := candidate.Discovery.Discover(probeCtx) + cancel() + if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 { + continue + } + model := modelsFound[0] + invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "recovery-external-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: capability.ProviderInstanceID("recovery-" + string(candidate.ProviderInstanceID)), ModelDescriptorID: capability.ModelDescriptorID("recovery-model-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID} + response, err := candidate.Provider.Advise(ctx, invocation) + if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" { + return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}, true + } + } + return cognitive.GatewayAdvisoryResponse{}, false +} + func (g cognitiveGateway) observe(ctx context.Context, selected capability.ResolvedCognitiveBinding, response cognitive.AdvisoryResponse, started time.Time) { usage := response.Usage observationID, idErr := ids.New() diff --git a/cmd/cognitive-worker/gateway_registry_test.go b/cmd/cognitive-worker/gateway_registry_test.go index 891746d..d5edbe8 100644 --- a/cmd/cognitive-worker/gateway_registry_test.go +++ b/cmd/cognitive-worker/gateway_registry_test.go @@ -168,6 +168,19 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) { } } +func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) { + registry := ®istryStub{err: errors.New("no active binding")} + provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}} + builtin := providerRegistration{ProviderInstanceID: "local-lifeboat", Kind: "openai-compatible-chat-v1", EndpointRef: "local-lifeboat-v1", Provider: provider} + gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now} + request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())) + response := httptest.NewRecorder() + gateway.advisoryHandler(response, request) + if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") { + t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String()) + } +} + func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) { primary := gatewaySelection() primary.Binding.ID = "gpu-binding" diff --git a/cmd/cognitive-worker/interpretation.go b/cmd/cognitive-worker/interpretation.go index 379b85b..3882401 100644 --- a/cmd/cognitive-worker/interpretation.go +++ b/cmd/cognitive-worker/interpretation.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "net/http" @@ -36,12 +37,12 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R } selected, err := g.resolver.Resolve(r.Context(), requirement) if err != nil { - writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable}) + writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes)) return } provider := g.provider(selected) if provider == nil { - writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable}) + writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes)) return } invocation := cognitive.AdvisoryRequest{ @@ -77,6 +78,22 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}) } +func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse { + if g.builtinLocal == nil || g.builtinLocal.Provider == nil { + return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable} + } + invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"} + response, err := g.builtinLocal.Provider.Advise(ctx, invocation) + if err != nil || response.FinishReason != cognitive.FinishCompleted { + return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable} + } + candidate, ok := parseInterpretationCandidate(response.AdvisoryText) + if !ok { + return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage} + } + return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage} +} + func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective { if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence { return capability.SelectionQuality diff --git a/cmd/cognitive-worker/main.go b/cmd/cognitive-worker/main.go index cf8f1dd..0847e33 100644 --- a/cmd/cognitive-worker/main.go +++ b/cmd/cognitive-worker/main.go @@ -40,31 +40,36 @@ func main() { } providers := map[capability.ProviderKind][]providerRegistration{} discoveries := map[string]providerRegistration{} + var builtinLocal *providerRegistration + externalRecovery := []providerRegistration{} cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), nil) if err != nil { log.Fatal(err) } - cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, Provider: cpuAdapter, Discovery: cpuAdapter} + cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter} providers[cpu.Kind] = append(providers[cpu.Kind], cpu) discoveries["cpu"] = cpu if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil { log.Fatal(lifeboatErr) } else if enabled { + lifeboat.DataPolicy = capability.DataPolicyLocalOnly providers[lifeboat.Kind] = append(providers[lifeboat.Kind], lifeboat) discoveries["lifeboat"] = lifeboat + builtinLocal = &lifeboat } gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil) if err != nil { log.Fatal(err) } - gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, Provider: gpuAdapter, Discovery: gpuAdapter} + gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter} providers[gpu.Kind] = append(providers[gpu.Kind], gpu) discoveries["gpu"] = gpu + externalRecovery = append(externalRecovery, gpu) if openAIProvider != nil { kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1")) - providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, Provider: openAIProvider}) + providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider}) } - gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096} + gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096} mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1}) mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal) mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler) @@ -99,7 +104,7 @@ func refreshProviderDiscoveries(resolver bindingResolver, service discoveryServi Enabled: true, OwnerPrincipalID: service.owner, CapabilityID: creatorAdvisoryCapabilityID, - DataPolicy: capability.DataPolicyLocalOnly, + DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 65604a5..cbf70c8 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -141,7 +141,7 @@ services: - --port - "8086" - --model - - /models/qwen2.5-0.5b-instruct-q4_k_m.gguf + - /models/Qwen3-8B-Q4_K_M.gguf - --alias - domovoy-lifeboat - --jinja @@ -150,9 +150,11 @@ services: - --n-gpu-layers - "0" - --threads - - "4" + - "32" - --parallel - "1" + - --reasoning + - "off" volumes: - llm_local_models:/models:ro healthcheck: diff --git a/dev/tasks/DEV-000085-builtin-cognitive-recovery.json b/dev/tasks/DEV-000085-builtin-cognitive-recovery.json new file mode 100644 index 0000000..23ff2f9 --- /dev/null +++ b/dev/tasks/DEV-000085-builtin-cognitive-recovery.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000085", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", + "risk_tier": "C", + "intent": "Keep Domovoy useful when external or active cognitive bindings are unavailable by invoking the built-in local lifeboat through the Cognitive Gateway and presenting an actionable recovery focus when all cognition fails.", + "requirement_ids": ["Built-in local cognition remains available", "Provider loss does not collapse conversation", "Recovery state is explained and actionable"], + "allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation_test.go", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing Cognitive Gateway and local provider adapter contracts"], "may_change": []}, + "semantic_owner": "capability", + "canonical_state": [], + "state_transitions": ["No active binding -> built-in local invocation", "Built-in invocation failure -> recovery-focused degraded response"], + "side_effects_and_privileges": ["No new privilege", "Built-in path is local-only and cannot egress externally"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Provider failure remains observable; next request retries resolution and built-in local path"], + "assumptions": ["The configured local lifeboat endpoint is part of Domovoy and may be used without external egress acknowledgement"], + "acceptance_scenarios": ["Inactive registry with healthy local lifeboat invokes the local adapter", "Interpretation also uses the built-in local path", "All cognition failure returns an actionable recovery response"], + "required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} diff --git a/dev/tasks/DEV-000086-local-model-baseline.json b/dev/tasks/DEV-000086-local-model-baseline.json new file mode 100644 index 0000000..ba6bb23 --- /dev/null +++ b/dev/tasks/DEV-000086-local-model-baseline.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000086", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", + "risk_tier": "C", + "intent": "Replace the unusable tiny lifeboat model with a capable, locally hosted Qwen3 8B quantized baseline while keeping Domovoy runtime on docker.cin.su.", + "requirement_ids": ["Built-in local cognition is useful for ordinary dialogue", "Local model remains available without external egress", "Model choice is explicit and replaceable"], + "allowed_change_scope": {"modules": ["deployment", "cognitive-worker"], "paths": ["deploy/docker-compose.prod.yml", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing llama.cpp OpenAI-compatible local endpoint", "existing Cognitive Gateway"], "may_change": []}, + "semantic_owner": "capability", + "canonical_state": [], + "state_transitions": ["Configured model file -> loaded local baseline", "Local model replacement -> restart and health verification"], + "side_effects_and_privileges": ["No new privilege", "Model is local-only and mounted read-only into the runtime"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Missing or invalid model keeps the container unhealthy and does not silently fall back to the tiny model"], + "assumptions": ["Qwen3-8B-Q4_K_M.gguf is present in the protected Docker model volume on docker.cin.su before rollout"], + "acceptance_scenarios": ["The configured lifeboat starts with the named Qwen3 8B model", "The OpenAI-compatible health endpoint is healthy", "No other host is used for Domovoy runtime"], + "required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check", "production health and model endpoint verification"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MODEL_ARTIFACT"] +} diff --git a/dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json b/dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json new file mode 100644 index 0000000..ad3ae25 --- /dev/null +++ b/dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000087", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", + "risk_tier": "D", + "intent": "When the selected cognitive route is unavailable, keep Domovoy conversational, diagnose the failure, use the built-in local model, and probe explicitly configured external GPU/HTTP providers through the Cognitive Gateway without exposing credentials or bypassing egress policy.", + "requirement_ids": ["Provider loss becomes a recovery objective", "Meaning and task objective guide provider choice", "External egress requires explicit acknowledgement", "Credentials remain opaque secret references"], + "allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/recovery.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation_test.go", "cmd/cognitive-worker/recovery_test.go", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing typed Cognitive Gateway", "existing local and Ollama adapters", "existing discovery and binding resolver"], "may_change": []}, + "semantic_owner": "capability", + "canonical_state": [], + "state_transitions": ["Selected provider failure -> local built-in attempt", "Local failure -> recovery diagnosis and configured-provider probe", "External probe -> registry discovery only; invocation requires policy acknowledgement"], + "side_effects_and_privileges": ["No new privilege", "No arbitrary network scanning", "No credential material in logs or messages"], + "persistence_and_migrations": ["Existing discovery registration only; no new persistence"], + "failure_retry_restart": ["Bounded sequential probes per request", "No unmanaged background retry loop", "Each new request re-resolves and re-probes"], + "assumptions": ["External GPU/HTTP endpoints are explicitly configured by deployment operators", "Core Runtime remains the owner of activation and selection"], + "acceptance_scenarios": ["Inactive registry with healthy local model still produces an intelligent local response", "Interpretation uses the same local recovery path", "External endpoints are discovered but never invoked without egress acknowledgement", "All failures yield actionable recovery text instead of a generic unavailable error"], + "required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check", "production recovery request and provider health verification"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"] +} -- 2.54.0 From 2cf1fb5c5ede8edbfc3a66b0ba57e7169bb31ed3 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 01:36:48 +0300 Subject: [PATCH 2/4] feat: expose runtime build status in chat --- cmd/creator-web/chat_test.go | 2 +- cmd/creator-web/server.go | 33 ++++++++++++++++--- cmd/creator-web/static/chat.html | 4 +-- deploy/docker-compose.prod.yml | 1 + .../DEV-000088-runtime-build-status.json | 23 +++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 dev/tasks/DEV-000088-runtime-build-status.json diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index 0216595..85c9e85 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -25,7 +25,7 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) { t.Fatalf("expected /chat status 200, got %d", recorder.Code) } body := recorder.Body.String() - for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "версия 1.0.0", "обновлено 21.08.2026", "datetime=\"2026-08-21\""} { + for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} { if !strings.Contains(body, required) { t.Fatalf("/chat is missing human status marker %q", required) } diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index e064bce..2584303 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "embed" "encoding/json" "fmt" @@ -8,7 +9,9 @@ import ( "io/fs" "net/http" "net/url" + "os" "strings" + "time" "unicode/utf8" "git.cin.su/m/Assistant/contracts/capability" @@ -36,6 +39,12 @@ type gateway struct { cognitive *url.URL client *http.Client static http.Handler + build buildMetadata +} + +type buildMetadata struct { + revision string + updated time.Time } type routeTarget uint8 @@ -74,7 +83,20 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c if client == nil { client = http.DefaultClient } - return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil + updated := time.Now().UTC() + if raw := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_TIME")); raw != "" { + if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil { + updated = parsed.UTC() + } + } + revision := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_REVISION")) + if revision == "" { + revision = "development" + } + if len(revision) > 12 { + revision = revision[:12] + } + return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil } func parseRuntimeURL(rawURL, name string) (*url.URL, error) { @@ -112,11 +134,11 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if r.URL.Path == "/" { - serveEmbeddedHTML(w, "chat.html") + g.serveEmbeddedHTML(w, "chat.html") return } if r.URL.Path == "/admin" { - serveEmbeddedHTML(w, "index.html") + g.serveEmbeddedHTML(w, "index.html") return } if r.URL.Path == "/chat" { @@ -127,7 +149,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { g.static.ServeHTTP(w, r) } -func serveEmbeddedHTML(w http.ResponseWriter, name string) { +func (g *gateway) serveEmbeddedHTML(w http.ResponseWriter, name string) { body, err := staticFiles.ReadFile("static/" + name) if err != nil { http.Error(w, "page unavailable", http.StatusNotFound) @@ -135,6 +157,9 @@ func serveEmbeddedHTML(w http.ResponseWriter, name string) { } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/html; charset=utf-8") + body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_REVISION__"), []byte(g.build.revision)) + body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_TIME__"), []byte(g.build.updated.Format("02.01.2006 15:04 UTC"))) + body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_DATETIME__"), []byte(g.build.updated.Format(time.RFC3339))) _, _ = w.Write(body) } diff --git a/cmd/creator-web/static/chat.html b/cmd/creator-web/static/chat.html index 7f380fa..d1b6c5d 100644 --- a/cmd/creator-web/static/chat.html +++ b/cmd/creator-web/static/chat.html @@ -29,8 +29,8 @@
- версия 1.0.0 - + версия __ASSISTANT_BUILD_REVISION__ +
Консоль diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index cbf70c8..493795d 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -172,6 +172,7 @@ services: CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081 COMMIT_RUNTIME_URL: http://commit-runtime:8082 COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083 + ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown} ports: ["${CREATOR_WEB_PORT:-18080}:8084"] depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker] healthcheck: diff --git a/dev/tasks/DEV-000088-runtime-build-status.json b/dev/tasks/DEV-000088-runtime-build-status.json new file mode 100644 index 0000000..12fd0a9 --- /dev/null +++ b/dev/tasks/DEV-000088-runtime-build-status.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000088", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", + "risk_tier": "B", + "intent": "Expose self-describing build revision and runtime update time in the Domovoy chat without requiring a human status query.", + "requirement_ids": ["Human can identify the running build", "Displayed update time reflects the running container", "No internal identifiers are exposed in the chat UI"], + "allowed_change_scope": {"modules": ["creator-web", "deployment"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/tasks/DEV-000088-runtime-build-status.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing source revision build argument"], "may_change": []}, + "semantic_owner": "creator-web", + "canonical_state": [], + "state_transitions": ["Container startup -> build metadata visible in chat"], + "side_effects_and_privileges": ["No new privilege", "No user or provider secrets displayed"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Missing revision is displayed as development/unknown; startup time remains observable"], + "assumptions": ["The production compose runner supplies ASSISTANT_SOURCE_REVISION for the running revision"], + "acceptance_scenarios": ["Chat shows revision and update time without a manual question", "HTML contains no internal provider or session identifiers", "Tests remain deterministic outside production"], + "required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "git diff --check", "production page verification"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} -- 2.54.0 From 51fa9ea506c615ef7cc952f537c7385c7b98f69d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 01:45:44 +0300 Subject: [PATCH 3/4] chore: activate Domovoy cognitive upgrade task --- dev/ACTIVE_TASK.json | 4 ++-- ...domovoy-cognitive-upgrade-integration.json | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index b656726..7b06cc1 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000082-chat-ai-data-safety-guard.json", - "task_id": "DEV-000082", + "task_file": "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json", + "task_id": "DEV-000089", "task_revision": 1 } diff --git a/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json b/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json new file mode 100644 index 0000000..6324c1d --- /dev/null +++ b/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000089", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", + "risk_tier": "D", + "intent": "Integrate the Domovoy local-model baseline, cognitive recovery orchestrator, and self-describing runtime status into one reviewed production change.", + "requirement_ids": ["Built-in cognition remains useful", "Provider loss triggers recovery", "Production runtime identifies its revision and update time"], + "allowed_change_scope": {"modules": ["cognitive-worker", "creator-web", "deployment", "development-governance"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/server.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json", "dev/tasks/DEV-000088-runtime-build-status.json", "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing cognitive gateway", "existing creator-web runtime", "existing production compose runner"], "may_change": []}, + "semantic_owner": "capability", + "canonical_state": [], + "state_transitions": ["Old active task -> integrated reviewed task", "Old local baseline -> Qwen3-8B baseline", "Unavailable active binding -> built-in and configured external recovery"], + "side_effects_and_privileges": ["No new privilege", "External invocation remains acknowledgement-gated", "No secrets in source or UI"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Existing auto-update runner deploys the merged main revision", "Missing model artifact leaves the lifeboat unhealthy rather than silently selecting the tiny model"], + "assumptions": ["Qwen3-8B-Q4_K_M.gguf is present in the protected Docker model volume on docker.cin.su"], + "acceptance_scenarios": ["CI accepts exact task scope", "Production chat shows running revision and update time", "Domovoy can answer through built-in local cognition when registry activation is unavailable"], + "required_tests": ["go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check", "production health and revision verification"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MODEL_ARTIFACT"] +} -- 2.54.0 From 0849e9c4aaa665c8ee964f831cef223372743684 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 01:46:10 +0300 Subject: [PATCH 4/4] chore: authorize integrated cognitive upgrade scope --- .../DEV-000089-domovoy-cognitive-upgrade-integration.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json b/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json index 6324c1d..7960dc6 100644 --- a/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json +++ b/dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json @@ -2,8 +2,8 @@ "task_id": "DEV-000089", "task_revision": 1, "architecture_revision": "DEV-000073-baseline", - "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", - "risk_tier": "D", + "base_repo_revision": "9f717d720e217afd4cc87a4fbf851050d466d8cd", + "risk_tier": "E", "intent": "Integrate the Domovoy local-model baseline, cognitive recovery orchestrator, and self-describing runtime status into one reviewed production change.", "requirement_ids": ["Built-in cognition remains useful", "Provider loss triggers recovery", "Production runtime identifies its revision and update time"], "allowed_change_scope": {"modules": ["cognitive-worker", "creator-web", "deployment", "development-governance"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/server.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json", "dev/tasks/DEV-000088-runtime-build-status.json", "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json"]}, -- 2.54.0