Compare commits

...
Author SHA1 Message Date
m e1b9c7ca84 docs: record cognitive core verification amendment
CI / test (push) Failing after 33s
2026-08-30 19:54:48 +03:00
m 00af176a20 fix: compile scoped interaction assertions
CI / test (push) Failing after 34s
2026-08-30 19:12:52 +03:00
m ce7000e78e chore: revise cognitive retrieval task scope
CI / test (push) Failing after 44s
2026-08-30 18:47:58 +03:00
m 6204e14177 fix: isolate candidate core cognitive route
CI / test (push) Failing after 36s
2026-08-30 16:02:11 +03:00
m 185317ee6a fix: revalidate stale cognitive binding health
CI / test (push) Failing after 35s
2026-08-30 15:56:34 +03:00
m 2e824b3c1f chore: start cognitive route remediation trace
CI / test (push) Failing after 35s
2026-08-30 15:33:59 +03:00
m f0360a8e13 merge GEN2-DEPLOY-003 isolated release slots
CI / test (push) Failing after 30s
2026-08-30 14:10:34 +03:00
m 5593f2ac2c docs: record isolated release slot drill
CI / test (push) Failing after 41s
2026-08-30 14:10:18 +03:00
m 216d76f1d8 docs: clarify stable updater isolation
CI / test (push) Failing after 28s
2026-08-30 13:59:24 +03:00
m 499b9678e1 test: support isolated canary rollback drill
CI / test (push) Failing after 28s
2026-08-30 13:55:35 +03:00
m 78a39ec011 fix: roll back release sets atomically
CI / test (push) Failing after 29s
2026-08-30 13:55:01 +03:00
m d120cc0958 fix: isolate candidate release slot
CI / test (push) Failing after 29s
2026-08-30 13:51:32 +03:00
25 changed files with 1034 additions and 139 deletions
@@ -100,6 +100,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
return out, rows.Err()
}
// UtterancesWithProjectReferences returns canonical utterances together with
// their already-persisted Interaction-owned project references. The caller
// uses the pair only as a read projection; this method does not infer or write
// scope membership.
func (r *InteractionRepository) UtterancesWithProjectReferences(ctx context.Context, id interaction.ConversationID) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
utterances, err := r.Utterances(ctx, id)
if err != nil {
return nil, nil, err
}
references, err := r.projectReferencesForUtterances(ctx, utterances)
if err != nil {
return nil, nil, err
}
return utterances, references, nil
}
// RecentUtterancesForPrincipal provides durable conversational context when a
// client starts a fresh conversation instead of carrying the previous ID.
func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, error) {
@@ -118,3 +134,52 @@ func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context
}
return out, rows.Err()
}
// RecentUtterancesForPrincipalWithProjectReferences keeps a new conversation
// within the same durable Interaction read boundary as its scoped context.
func (r *InteractionRepository) RecentUtterancesForPrincipalWithProjectReferences(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
utterances, err := r.RecentUtterancesForPrincipal(ctx, principalID, limit)
if err != nil {
return nil, nil, err
}
references, err := r.projectReferencesForUtterances(ctx, utterances)
if err != nil {
return nil, nil, err
}
return utterances, references, nil
}
func (r *InteractionRepository) projectReferencesForUtterances(ctx context.Context, utterances []interaction.HumanUtterance) ([]interaction.ProjectReference, error) {
if len(utterances) == 0 {
return nil, nil
}
ids := make([]string, 0, len(utterances))
seen := make(map[interaction.HumanUtteranceID]struct{}, len(utterances))
for _, utterance := range utterances {
if utterance.ID == "" {
continue
}
if _, exists := seen[utterance.ID]; exists {
continue
}
seen[utterance.ID] = struct{}{}
ids = append(ids, string(utterance.ID))
}
if len(ids) == 0 {
return nil, nil
}
rows, err := r.pool.Query(ctx, `SELECT project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis FROM interaction.project_references WHERE utterance_id = ANY($1::text[]) ORDER BY recorded_at_unix_millis,project_reference_id`, ids)
if err != nil {
return nil, err
}
defer rows.Close()
refs := make([]interaction.ProjectReference, 0)
for rows.Next() {
var ref interaction.ProjectReference
if err := rows.Scan(&ref.ID, &ref.UtteranceID, &ref.ProjectID, &ref.ProjectDisplayName, &ref.Role, &ref.Provenance, &ref.ConfidenceMillis, &ref.RecordedAtUnixMillis); err != nil {
return nil, err
}
refs = append(refs, ref)
}
return refs, rows.Err()
}
+16 -3
View File
@@ -16,15 +16,17 @@ func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryReques
}
type admissionTestClient struct {
decision capability.AdmissionDecision
releases int
decision capability.AdmissionDecision
releases int
releaseContextErr error
}
func (c *admissionTestClient) Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
return c.decision, capability.AdmissionLease{ID: "lease-1", PolicyID: "policy-1"}, nil
}
func (c *admissionTestClient) Release(context.Context, capability.AdmissionLease) error {
func (c *admissionTestClient) Release(ctx context.Context, _ capability.AdmissionLease) error {
c.releases++
c.releaseContextErr = ctx.Err()
return nil
}
@@ -47,3 +49,14 @@ func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
func TestInvokeAdmittedReleasesLeaseAfterCallerCancellation(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _ = (&cognitiveGateway{admission: client}).invokeAdmitted(ctx, "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if client.releases != 1 || client.releaseContextErr != nil {
t.Fatalf("cancelled caller leaked durable lease: releases=%d release_context_err=%v", client.releases, client.releaseContextErr)
}
}
+16 -11
View File
@@ -1,7 +1,9 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"time"
@@ -22,17 +24,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
s.statusHandler(w, r)
return
}
registration, ok := s.providers[r.PathValue("provider")]
if !ok || registration.Discovery == nil {
http.Error(w, "provider discovery not found", http.StatusNotFound)
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: 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})
}
result, err := s.resolver.RegisterDiscovery(r.Context(), snapshot)
result, err := s.register(r.Context(), r.PathValue("provider"))
if err != nil {
http.Error(w, "provider discovery registration unavailable", http.StatusBadGateway)
return
@@ -41,6 +33,19 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(result)
}
func (s discoveryService) register(ctx context.Context, provider string) (capability.CognitiveDiscoveryRegistration, error) {
registration, ok := s.providers[provider]
if !ok || registration.Discovery == nil {
return capability.CognitiveDiscoveryRegistration{}, fmt.Errorf("provider discovery not found")
}
discovered, probe := registration.Discovery.Discover(ctx)
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})
}
return s.resolver.RegisterDiscovery(ctx, snapshot)
}
type discoveryStatus struct {
Provider string `json:"provider"`
ProviderKind capability.ProviderKind `json:"provider_kind"`
+37 -3
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -20,6 +21,8 @@ import (
const creatorAdvisoryCapabilityID capability.CapabilityID = "creator-advisory"
var errNoInvocableCognitiveBinding = errors.New("no invocable cognitive binding")
type bindingResolver interface {
Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error)
Observe(context.Context, capability.CognitiveUsageObservation) error
@@ -56,6 +59,7 @@ type cognitiveGateway struct {
allowCanary bool
canaryMaxOutputBytes int
admission admissionClient
revalidatePrimary func(context.Context) error
}
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
@@ -89,7 +93,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
maxOutputBytes = g.canaryMaxOutputBytes
}
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)
selected, err := g.resolveWithRevalidation(r.Context(), requirement)
if err != nil {
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
@@ -145,11 +149,33 @@ func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, pr
return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil
}
lease = admittedLease
defer func() { _ = g.admission.Release(ctx, lease) }()
defer func() {
releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
_ = g.admission.Release(releaseCtx, lease)
}()
}
return provider.Advise(ctx, invocation)
}
// resolveWithRevalidation makes one bounded recovery attempt when Capability
// has an active/validated binding but its durable health projection rejected it
// as non-invocable. Discovery remains the canonical Capability write path; the
// retry observes the configured primary local provider and re-runs selection
// once. Provider invocation, admission, and fallback remain governed elsewhere.
func (g cognitiveGateway) resolveWithRevalidation(ctx context.Context, requirement capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
selected, err := g.resolver.Resolve(ctx, requirement)
if err == nil || !errors.Is(err, errNoInvocableCognitiveBinding) || g.revalidatePrimary == nil {
return selected, err
}
revalidationCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
if revalidationErr := g.revalidatePrimary(revalidationCtx); revalidationErr != nil {
return selected, err
}
return g.resolver.Resolve(ctx, requirement)
}
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
lower := strings.ToLower(message)
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
@@ -374,6 +400,10 @@ type httpBindingResolver struct {
client *http.Client
}
type httpStatusError struct{ status int }
func (e httpStatusError) Error() string { return fmt.Sprintf("registry status %d", e.status) }
func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolver, error) {
base, err := url.Parse(raw)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -385,6 +415,10 @@ func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolv
func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
var result capability.ResolvedCognitiveBinding
err := r.post(ctx, "/v1/cognitive-bindings/resolve", value, &result)
var statusErr httpStatusError
if errors.As(err, &statusErr) && statusErr.status == http.StatusNotFound {
return result, errNoInvocableCognitiveBinding
}
return result, err
}
@@ -437,7 +471,7 @@ func (r *httpBindingResolver) post(ctx context.Context, path string, value, dest
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
return fmt.Errorf("registry status %d", response.StatusCode)
return httpStatusError{status: response.StatusCode}
}
if destination != nil {
return json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(destination)
@@ -22,6 +22,26 @@ type registryStub struct {
observation *capability.CognitiveUsageObservation
}
type revalidatingRegistry struct {
selected capability.ResolvedCognitiveBinding
calls int
alwaysNo bool
}
func (s *revalidatingRegistry) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
s.calls++
if s.alwaysNo || s.calls == 1 {
return capability.ResolvedCognitiveBinding{}, errNoInvocableCognitiveBinding
}
return s.selected, nil
}
func (*revalidatingRegistry) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (*revalidatingRegistry) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
type failoverRegistry struct {
primary, fallback capability.ResolvedCognitiveBinding
observations []capability.CognitiveUsageObservation
@@ -88,6 +108,59 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
}
}
func TestStaleUnavailableBindingRevalidatesOnceBeforeInvocation(t *testing.T) {
selected := gatewaySelection()
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
registry := &revalidatingRegistry{selected: selected}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted}}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{EndpointRef: "endpoint-1", CredentialRef: "credential-1", Provider: provider}}}, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return nil }}
response := httptest.NewRecorder()
gateway.advisoryHandler(response, httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if response.Code != http.StatusOK || !provider.called || revalidations != 1 || registry.calls != 2 {
t.Fatalf("status=%d provider_called=%t revalidations=%d resolves=%d", response.Code, provider.called, revalidations, registry.calls)
}
}
func TestRevalidationFailureDoesNotMakeBindingInvocable(t *testing.T) {
registry := &registryStub{err: errNoInvocableCognitiveBinding}
provider := &stubAdvisoryProvider{}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{Provider: provider}}}, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return errors.New("provider unavailable") }}
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if provider.called || revalidations != 1 {
t.Fatalf("failed provider revalidation was treated as invocable: provider_called=%t revalidations=%d", provider.called, revalidations)
}
}
func TestRevalidationIsNotRetriedInALoop(t *testing.T) {
registry := &revalidatingRegistry{alwaysNo: true}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return nil }}
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if revalidations != 1 || registry.calls != 2 {
t.Fatalf("revalidation loop: revalidations=%d resolves=%d", revalidations, registry.calls)
}
}
func TestHTTPResolverMarksNoInvocableBindingForSingleRevalidation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/cognitive-bindings/resolve" {
t.Fatalf("path=%s", r.URL.Path)
}
http.Error(w, "no invocable cognitive binding", http.StatusNotFound)
}))
defer server.Close()
resolver, err := newHTTPBindingResolver(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
_, err = resolver.Resolve(context.Background(), capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: "principal-1", MaxOutputBytes: 1})
if !errors.Is(err, errNoInvocableCognitiveBinding) {
t.Fatalf("resolve error=%v, want no-invocable sentinel", err)
}
}
func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) {
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{
+6 -3
View File
@@ -113,11 +113,15 @@ func main() {
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, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second})}
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second}), revalidatePrimary: func(ctx context.Context) error {
_, err := discovery.register(ctx, "medium")
return err
}}
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
cpuReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessTimeout := 120 * time.Second
mediumReadinessTimeout := 120 * time.Second
mediumReadinessClient := &http.Client{Timeout: mediumReadinessTimeout}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
fleetClient := &http.Client{Timeout: 2 * time.Second}
@@ -199,7 +203,6 @@ func main() {
mux.HandleFunc("POST /v1/multimodal/ocr", multimodalGateway.handler(capability.MultimodalRecognizeOCR))
mux.HandleFunc("POST /v1/multimodal/video-understand", multimodalGateway.handler(capability.MultimodalUnderstandVideo))
mux.HandleFunc("POST /v1/multimodal/generate-image", multimodalGateway.handler(capability.MultimodalGenerateImage))
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
@@ -28,6 +28,35 @@ type cognitiveInterpretationClient struct {
media *http.Client
}
// compileInteractionContext is the Core-side handoff from the existing
// Interaction retrieval projection to the existing Context Compiler. The
// compiler receives only already-authorized, typed fragments and applies its
// scope and byte-budget guards before the cognitive gateway sees them.
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
if len(projection.Fragments) == 0 {
return projection.Text, nil
}
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments))
for _, fragment := range projection.Fragments {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance,
Lane: "interaction",
ContextScopeRef: fragment.ScopeRef,
FreshnessState: "CURRENT",
})
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
if err != nil {
return "", err
}
parts := make([]string, 0, len(bundle.Excerpts))
for _, excerpt := range bundle.Excerpts {
parts = append(parts, excerpt.Text)
}
return strings.Join(parts, "\n"), nil
}
const (
// Household interpretation and response review are intentionally compact
// calls. Keeping their output bounded prevents a loaded local model from
@@ -70,6 +70,16 @@ func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
}
}
func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
context, err := compileInteractionContext(interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{
{UtteranceID: "a", Text: "контрольный код = ALPHA", ScopeRef: "company-a", Relevance: 2},
{UtteranceID: "b", Text: "контрольный код = BETA", ScopeRef: "company-b", Relevance: 3},
}}, "company-a")
if err != nil || context != "контрольный код = ALPHA" {
t.Fatalf("context=%q err=%v", context, err)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
+20 -5
View File
@@ -571,9 +571,10 @@ func main() {
travelContinuation := false
var previousTask *interaction.HumanUtterance
var conversationHistory []interaction.HumanUtterance
var contextProjectReferences []interaction.ProjectReference
if request.ConversationID != "" {
var historyErr error
conversationHistory, historyErr = interactionRepository.Utterances(r.Context(), conversation.ID)
conversationHistory, contextProjectReferences, historyErr = interactionRepository.UtterancesWithProjectReferences(r.Context(), conversation.ID)
if historyErr != nil {
log.Printf("interaction conversation history unavailable: %v", historyErr)
}
@@ -581,14 +582,24 @@ func main() {
// Keep durable user context available even when the web client carries a
// conversation ID; a new or rotated conversation must not erase learned
// preferences.
recentUserHistory, historyErr := interactionRepository.RecentUtterancesForPrincipal(r.Context(), session.PrincipalID, 64)
recentUserHistory, recentProjectReferences, historyErr := interactionRepository.RecentUtterancesForPrincipalWithProjectReferences(r.Context(), session.PrincipalID, 64)
if historyErr != nil {
log.Printf("interaction principal history unavailable: %v", historyErr)
} else {
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
}
conversationHistory = append(conversationHistory, recentUserHistory...)
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
contextProjectReferences = append(contextProjectReferences, recentProjectReferences...)
focusProjectID := ""
if previousFocus != nil {
focusProjectID = string(previousFocus.ProjectID)
}
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
if compiledContext, contextErr := compileInteractionContext(gen2Context, focusProjectID); contextErr != nil {
log.Printf("interaction context compilation unavailable: %v", contextErr)
} else {
gen2Context.Text = compiledContext
}
if gen2Context.UsedPersonalMemoryFallback {
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
}
@@ -887,7 +898,7 @@ func main() {
planningReply = retrievedMemoryReply
needsCognition = false
}
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
if hasChatRoute(routePlan, chatRouteResearch) && len(gen2Context.Fragments) == 0 && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
researchAttempted = true
if planID, idErr := ids.New(); idErr == nil {
parts := interactiondomain.IndependentRequestParts(effectiveTaskMessage)
@@ -954,7 +965,7 @@ func main() {
}
}
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && (!hasChatRoute(routePlan, chatRouteResearch) || len(gen2Context.Fragments) > 0) && !creativeRequest {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
@@ -1483,6 +1494,10 @@ func main() {
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
}
if err != nil {
// The resolver is an internal capability boundary. Keep request identity
// and content out of logs, but preserve the repository error so a 503
// can be distinguished from a true empty/unsuitable binding set.
log.Printf("cognitive registry resolve unavailable: %v", err)
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
+14 -1
View File
@@ -261,7 +261,20 @@ func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
respond(w, ok, err)
}
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.RollbackTraffic()
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
// A ReleaseSet must roll back as a whole. The legacy release-only method
// would merely flip the traffic mode while retaining a candidate upstream.
var ok bool
var err error
if state.StableReleaseSet != nil {
ok, err = s.lifeboat.RollbackSet()
} else {
ok, err = s.lifeboat.RollbackTraffic()
}
respond(w, ok, err)
}
+149
View File
@@ -0,0 +1,149 @@
# This file deliberately owns only one inactive release slot. It joins the
# stable project's shared network as an external resource, but has a distinct
# Compose project name supplied by run-production-canary.sh. Do not add
# durable services or stable services here.
services:
research-runtime:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: research-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: :8087
RESEARCH_SOURCE_ROOT: /var/lib/research-sources
RESEARCH_HTTP_ALLOWED_HOSTS: assistant.cin.su,git.cin.su
RESEARCH_SEARCH_URL_TEMPLATE: ${RESEARCH_SEARCH_URL_TEMPLATE:-https://www.bing.com/search?q=%s}
RESEARCH_SEARCH_ALLOWED_HOSTS: ${RESEARCH_SEARCH_ALLOWED_HOSTS:-www.bing.com}
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
SECRET_BACKEND_ROOT: /run/secrets
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
networks:
shared:
aliases: [research-runtime-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8087/healthz >/dev/null && wget -qO- http://localhost:8087/readyz >/dev/null"]
interval: 10s
timeout: 10s
retries: 20
core-runtime:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: core-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8080"
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
POSTGRES_USER: situation_role
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
IDENTITY_POSTGRES_USER: identity_role
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
CREATOR_USERNAME: ${CREATOR_USERNAME}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
INTERACTION_POSTGRES_USER: interaction_role
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
SECRET_BACKEND_ROOT: /run/secrets
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR: ${COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR:-60}
COGNITIVE_ADMISSION_MAX_INPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_INPUT_BYTES:-81920}
COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES:-16384}
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
volumes:
- assistant_core_secrets:/run/secrets:ro
networks:
shared:
aliases: [core-runtime-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
cognitive-worker:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-gateway:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-/run/secrets/llm_local_gateway_api_key}
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
LOCAL_IMAGE_PROVIDER_KIND: ${LOCAL_IMAGE_PROVIDER_KIND:-comfyui}
LOCAL_IMAGE_PROVIDER_URL: ${LOCAL_IMAGE_PROVIDER_URL:-http://comfyui:8188}
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
volumes:
- assistant_core_secrets:/run/secrets:ro
networks:
shared:
aliases: [cognitive-worker-candidate]
llm-local: {}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
creator-web:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8084"
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
AGENT_WORKER_URL: http://agent-worker:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
networks:
shared:
aliases: [creator-web-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
volumes:
assistant_core_secrets:
external: true
assistant_research_sources:
name: assistant_research_sources
external: true
networks:
shared:
name: assistant_default
external: true
llm-local:
name: llm-local_default
external: true
-52
View File
@@ -89,17 +89,6 @@ services:
timeout: 20s
retries: 20
core-runtime-candidate:
profiles: ["assistant-canary"]
extends:
service: core-runtime
restart: "no"
environment:
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
depends_on:
postgres: { condition: service_healthy }
research-runtime-candidate: { condition: service_started }
agent-canary:
profiles: ["agent-canary"]
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-canary, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
@@ -223,14 +212,6 @@ services:
timeout: 10s
retries: 20
research-runtime-candidate:
profiles: ["assistant-canary"]
extends:
service: research-runtime
restart: "no"
depends_on:
postgres: { condition: service_healthy }
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -299,17 +280,6 @@ services:
timeout: 20s
retries: 20
cognitive-worker-candidate:
profiles: ["assistant-canary"]
extends:
service: cognitive-worker
restart: "no"
environment:
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
depends_on:
core-runtime-candidate: { condition: service_started }
cognitive-lifeboat: { condition: service_healthy }
cognitive-lifeboat:
image: ghcr.io/ggml-org/llama.cpp:server
restart: unless-stopped
@@ -386,28 +356,6 @@ services:
timeout: 3s
retries: 10
creator-web-candidate:
profiles: ["assistant-canary"]
extends:
service: creator-web
restart: "no"
environment:
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
AGENT_WORKER_URL: http://agent-worker:8090
depends_on:
core-runtime-candidate: { condition: service_started }
continuity-runtime: { condition: service_started }
commit-runtime: { condition: service_started }
cognitive-worker-candidate: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
volumes:
assistant_pg:
assistant_nats:
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Stable-only production updater. Candidate slots are managed exclusively by
# their isolated release command and Supervisor; this controller never starts
# or reconciles a candidate service.
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
LOG_FILE="${LOG_FILE:-$BASE_DIR/deploy.log}"
LOCK_FILE="${LOCK_FILE:-/tmp/assistant-stable-deploy.lock}"
stable_services=(postgres core-runtime continuity-runtime commit-runtime cognitive-worker creator-web notification-receiver research-runtime agent-worker work-graph-runtime)
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$LOG_FILE"; }
fail() { log "FAILED: $*"; exit 1; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
cd "$REPO_DIR"
git fetch --prune origin main >>"$LOG_FILE" 2>&1
git merge --ff-only origin/main >>"$LOG_FILE" 2>&1 || fail "stable checkout is not a fast-forward to origin/main"
revision="$(git rev-parse HEAD)"
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || fail "invalid source revision"
if ! REPO_DIR="$REPO_DIR" ENV_FILE="$ENV_FILE" bash "$REPO_DIR/deploy/apply-migrations.sh" >>"$LOG_FILE" 2>&1; then
fail "migration application failed"
fi
compose config >/dev/null 2>>"$LOG_FILE" || fail "stable compose config validation failed"
all_current=1
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
[[ "$label" == "$revision" ]] || all_current=0
done
if [[ "$all_current" == 1 ]]; then log "SKIP stable revision=$revision"; exit 0; fi
log "BUILD stable revision=$revision"
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" "${stable_services[@]}" >>"$LOG_FILE" 2>&1
log "UP stable revision=$revision"
ASSISTANT_SOURCE_REVISION="$revision" compose up -d --no-build "${stable_services[@]}" >>"$LOG_FILE" 2>&1
for attempt in $(seq 1 30); do
if curl -fsS --max-time 5 https://assistant.cin.su/healthz >/dev/null 2>&1; then break; fi
[[ "$attempt" == 30 ]] && fail "public health failed"
sleep 10
done
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
[[ "$label" == "$revision" ]] || fail "artifact drift container=$container"
done
log "OK stable revision=$revision"
+37 -51
View File
@@ -1,57 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
# Candidate services are never part of the stable Compose project. The slot
# Compose file may use only declared external networks and durable resources.
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
CANDIDATE_FILE="$BASE_DIR/deploy/docker-compose.candidate-slot.yml"
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
REVISION="${1:?candidate revision is required}"
SLOT="${CANDIDATE_SLOT:-b}"
STABLE_PROJECT="${STABLE_PROJECT:-assistant}"
CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"
HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"
PROMOTE_AFTER_CANARY="${PROMOTE_AFTER_CANARY:-true}"
set -a
. "$ENV_FILE"
set +a
[[ "$SLOT" =~ ^[a-z0-9-]+$ ]] || { echo "invalid candidate slot" >&2; exit 2; }
[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]] || { echo "candidate and stable Compose projects must differ" >&2; exit 2; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
candidate_compose() { docker compose -p "$CANDIDATE_PROJECT" --env-file "$ENV_FILE" -f "$CANDIDATE_FILE" "$@"; }
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
fail_rollback() {
control /v1/traffic/rollback || true
control /v1/releases/rollback || true
echo "production ReleaseSet canary failed; stable ReleaseSet rollback requested" >&2
echo "isolated candidate ReleaseSet failed; stable ReleaseSet rollback requested" >&2
exit 1
}
services=(creator-web-candidate core-runtime-candidate cognitive-worker-candidate research-runtime-candidate)
containers=(assistant-creator-web-candidate-1 assistant-core-runtime-candidate-1 assistant-cognitive-worker-candidate-1 assistant-research-runtime-candidate-1)
services=(creator-web core-runtime cognitive-worker research-runtime)
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
stable_ids() {
local service container id
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
container="${STABLE_PROJECT}-${service}-1"
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
printf '%s=%s\n' "$service" "$id"
done
}
assert_stable_ids_unchanged() {
local after
after=$(stable_ids) || fail_rollback
[[ "$after" == "$stable_ids_before" ]] || {
echo "stable container IDs changed during candidate operation" >&2
echo "stable container IDs changed during isolated candidate operation" >&2
diff -u <(printf '%s\n' "$stable_ids_before") <(printf '%s\n' "$after") >&2 || true
fail_rollback
}
}
stable_ids_before=$(stable_ids) || exit 1
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
candidate_container() { printf '%s-%s-1' "$CANDIDATE_PROJECT" "$1"; }
wait_healthy() {
local container="$1" status="" ready=""
ready=""
for attempt in $(seq 1 "$HEALTH_ATTEMPTS"); do
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$container" 2>/dev/null || true)
if [[ "$status" == healthy ]]; then ready=true; break; fi
@@ -60,68 +57,57 @@ wait_healthy() {
[[ "$ready" == true ]] || fail_rollback
}
compose --profile assistant-canary up -d --no-build --no-deps research-runtime-candidate core-runtime-candidate
wait_healthy assistant-research-runtime-candidate-1
wait_healthy assistant-core-runtime-candidate-1
compose --profile assistant-canary up -d --no-build --no-deps cognitive-worker-candidate
wait_healthy assistant-cognitive-worker-candidate-1
compose --profile assistant-canary up -d --no-build --no-deps creator-web-candidate
wait_healthy assistant-creator-web-candidate-1
stable_ids_before=$(stable_ids) || exit 1
candidate_compose config >/dev/null
candidate_compose build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
assert_stable_ids_unchanged
for container in "${containers[@]}"; do
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$container" 2>/dev/null || true)
candidate_compose up -d --no-build --no-deps research-runtime core-runtime
wait_healthy "$(candidate_container research-runtime)"
wait_healthy "$(candidate_container core-runtime)"
candidate_compose up -d --no-build --no-deps cognitive-worker
wait_healthy "$(candidate_container cognitive-worker)"
candidate_compose up -d --no-build --no-deps creator-web
wait_healthy "$(candidate_container creator-web)"
assert_stable_ids_unchanged
for service in "${services[@]}"; do
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$(candidate_container "$service")" 2>/dev/null || true)
[[ "$image_revision" == "$REVISION" ]] || fail_rollback
done
assert_stable_ids_unchanged
if [[ "${PREPARE_ONLY:-false}" == true ]]; then
echo "production ReleaseSet candidate prepared revision=$REVISION"
echo "isolated candidate ReleaseSet prepared slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
exit 0
fi
release_json=$(cat <<JSON
{
"id":"candidate-$REVISION",
"revision":"$REVISION",
"components":{
"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},
"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},
"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},
"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}
}
}
{"id":"candidate-$SLOT-$REVISION","slot":"$SLOT","revision":"$REVISION","components":{"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}}}
JSON
)
stable_json=$(curl -fsS --max-time 10 "$SUPERVISOR_CONTROL_URL/v1/state")
stable_set=$(printf '%s' "$stable_json" | jq -c '.stable_release_set // empty')
if [[ -z "$stable_set" ]]; then
stable_revision=$(printf '%s' "$stable_json" | jq -r '.current.version // "unknown"')
stable_set=$(cat <<JSON
{"id":"stable-$stable_revision","revision":"$stable_revision","components":{
"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/readyz"},
"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},
"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},
"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
{"id":"stable-$stable_revision","slot":"legacy-stable","revision":"$stable_revision","components":{"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
JSON
)
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/baseline-set" -H 'Content-Type: application/json' --data "$stable_set" >/dev/null || fail_rollback
fi
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/start-set" -H 'Content-Type: application/json' --data "$release_json" >/dev/null || fail_rollback
assert_stable_ids_unchanged
observation=$(printf '{"release_id":"candidate-%s","ready":true,"reason":"all ReleaseSet healthchecks and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
observation=$(printf '{"release_id":"candidate-%s-%s","ready":true,"reason":"isolated slot health and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$SLOT" "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/observe-set" -H 'Content-Type: application/json' --data "$observation" >/dev/null || fail_rollback
assert_stable_ids_unchanged
control /v1/traffic/canary || fail_rollback
assert_stable_ids_unchanged
headers=$(mktemp)
trap 'rm -f "$headers"' EXIT
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
if [[ "$PROMOTE_AFTER_CANARY" != true ]]; then
echo "isolated candidate ReleaseSet canary ready slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
exit 0
fi
control /v1/releases/promote || fail_rollback
assert_stable_ids_unchanged
echo "production ReleaseSet canary promoted revision=$REVISION"
echo "isolated candidate ReleaseSet promoted slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-COMPAT-OBS-001-production-compatibility-observation.json","task_id":"GEN2-COMPAT-OBS-001","task_revision":2}
{"task_file":"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json","task_id":"GEN2-COGNITIVE-002","task_revision":3}
@@ -0,0 +1,64 @@
{
"task_id": "GEN2-COGNITIVE-002",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Восстановить доказанно рабочий production cognitive route для обычного household и memory-recall запроса через существующие Capability и Cognitive Gateway boundaries, без подмены primary route Lifeboat-ом.",
"requirement_ids": ["REQ-GEN2-COGNITIVE-002-TRACE", "REQ-GEN2-COGNITIVE-002-ROUTING", "REQ-GEN2-COGNITIVE-002-RECOVERY"],
"allowed_change_scope": {
"modules": ["capability", "cognitive-worker", "core-runtime", "interaction", "production deployment"],
"paths": [
"cmd/cognitive-worker/**",
"cmd/core-runtime/**",
"modules/interaction/domain/gen2_context.go",
"modules/interaction/domain/gen2_context_test.go",
"adapters/postgres/interaction_repository.go",
"contracts/capability/**",
"contracts/cognitive/**",
"modules/capability/**",
"adapters/models/**",
"adapters/postgres/capability_repository.go",
"deploy/run-production-canary.sh",
"deploy/docker-compose.candidate-slot.yml",
"tests/**",
"docs/evidence/**",
"docs/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
},
"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 Capability binding lifecycle", "existing cognitive gateway/advisory contracts", "existing Supervisor ReleaseSet control plane"],
"may_change": ["typed capability/provider selection and invocation wiring only within declared paths", "the existing Interaction Gen2 retrieval projection for bounded, scoped human assertions; no new Memory owner or reader"]
},
"semantic_owner": "Capability remains the sole owner of provider/model/binding lifecycle; cognitive-worker invokes providers through adapters; Core consumes the governed gateway; Supervisor alone controls traffic promotion and rollback.",
"canonical_state": ["existing Capability provider/model/binding/evaluation evidence", "canonical Interaction human utterance history and existing typed ProjectReference read through the existing Gen2 retrieval projection", "no new canonical writer, owner, schema, role, or migration"],
"state_transitions": ["trace stable and candidate loaded binding heads -> correct stale/divergent lifecycle or invocation defect -> governed repair/qualification -> scoped Interaction projection selects bounded current human assertions -> isolated candidate validation -> Supervisor canary -> promote or rollback"],
"side_effects_and_privileges": ["production candidate deployment and Supervisor canary/promotion are explicitly authorized", "adapters/models/** may change only after trace proves a model adapter, provider endpoint, injection, timeout, rate-limit, context budget, or invocation defect there", "cmd/core-runtime/** may change only after trace proves Core-to-worker request construction, route selection, or response handling is causal", "modules/interaction/domain/gen2_context.go may change only to extend the existing bounded Gen2 read projection over canonical Interaction utterances with Principal hard filtering, current applicability and relevance selection; it must not write state, become a Memory owner, or promote PersonalMemory V1 to primary", "adapters/postgres/interaction_repository.go may only read the already-persisted typed interaction.ProjectReference records for the selected canonical Interaction utterances; it may not create schema, write state, or bypass Interaction ownership", "no direct SQL semantic mutation", "no credential, cookie, token, prompt, or user content read/emitted", "Lifeboat may be observed as fallback but cannot mask Local MEDIUM primary failure"],
"persistence_and_migrations": ["no schema/migration change is authorized", "adapters/postgres/capability_repository.go may change only after trace proves durable Capability head/state loading diverges from the correct persisted state; reuse its existing repository boundary"],
"failure_retry_restart": ["discovery health, invocation health, and availability remain distinct", "CPU/GPU gaps must not suppress a valid Local MEDIUM", "restart reloads a durable active/validated binding head", "failed candidate rolls traffic to Supervisor LKG without stable recreation"],
"assumptions": ["isolated candidate slot B and Supervisor controls from GEN2-DEPLOY-003 are available", "an existing authenticated production session may be used only through ordinary runtime interaction", "GEN2-COMPAT-OBS-001 remains paused and V1 paths remain unchanged"],
"acceptance_scenarios": [
"COGNITIVE-002-SCN-01: factual stable/candidate trace records Core request, requirement, binding selection, loaded binding head, fleet availability, provider eligibility, invocation result, and response/error without sensitive content",
"COGNITIVE-002-SCN-02: Local MEDIUM is shown ACTIVE, VALIDATED, AVAILABLE, and actually invoked for bounded OK, Russian household, and prepared-context recall requests",
"COGNITIVE-002-SCN-03: health endpoint success alone is not used as inference proof; discovery and invocation failures are separately classified",
"COGNITIVE-002-SCN-04: candidate four-component health/revision check, real canary household request, scoped bounded arbitrary-assertion memory write/multiple turns/recall, and candidate Core plus cognitive-worker restart/recovery pass",
"COGNITIVE-002-SCN-05: only after candidate success, Supervisor promotes it; evidence retains LKG and rollback readiness",
"COGNITIVE-002-EXIT: ordinary household and memory-recall response use Local MEDIUM through the governed route and remain usable after candidate restart"
],
"required_tests": [
"FAST: targeted go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/models/... ./adapters/postgres/... only for changed packages",
"FAST: targeted go vet for changed packages",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"production: stable/candidate trace, isolated candidate health/canary, Local MEDIUM invocation, restart recovery, and Supervisor promotion/rollback evidence"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,59 @@
{
"task_id": "GEN2-DEPLOY-003",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Изолировать stable и candidate ReleaseSet в независимые Compose deployment projects/slots, чтобы подготовка candidate не могла пересоздать stable containers, сохранив Supervisor как единственный traffic/promotion/rollback control plane.",
"requirement_ids": ["REQ-GEN2-DEPLOY-003-SLOT-ISOLATION", "REQ-GEN2-DEPLOY-003-UPDATER-OWNERSHIP", "REQ-GEN2-DEPLOY-003-ROLLBACK"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor"],
"paths": [
"deploy/**",
"runtime/supervisor/**",
"cmd/supervisor/**",
"docs/evidence/**",
"docs/DEPLOYMENT.md",
"tests/deployment/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-003-isolated-release-slots.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "database/**", "modules/**", "contracts/**", "security/**", "adapters/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
},
"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 Supervisor durable ReleaseSet state", "existing production compose stack", "existing shared durable PostgreSQL/volumes"],
"may_change": ["deployment slot topology", "deployment-controller scripts", "Supervisor operational ReleaseSet identity fields"]
},
"semantic_owner": "Operations/Supervisor; slot and ReleaseSet identity are runtime-only operational control-plane state, not semantic canonical state.",
"canonical_state": ["no semantic owner, canonical writer, schema or data migration change", "Supervisor durable release evidence/LKG remains retained outside candidate containers"],
"state_transitions": ["stable slot retained -> isolated candidate slot start -> four-component health/revision validation -> Supervisor canary -> promote or traffic rollback -> inactive slot retained for rollback"],
"side_effects_and_privileges": ["production operations only through Docker/Compose and Supervisor control plane explicitly authorized for this remediation", "candidate preparation must never directly replace/recreate stable", "no credential reads or outputs", "external updater pause is allowed only as an explicit bounded operation with restore evidence"],
"persistence_and_migrations": ["no database migration", "shared PostgreSQL, named volumes and durable services are attached as existing external resources and are never duplicated or deleted by candidate cleanup"],
"failure_retry_restart": ["capture stable container IDs before every candidate operation and abort before canary when any changes", "candidate component failure returns traffic to LKG without container recreation", "Supervisor restart reloads ReleaseSet/slot/LKG evidence", "updater ownership must be project-scoped so it cannot reconcile the other slot"],
"assumptions": ["current stable may remain in its existing project during the one-time transition; it must not be recreated merely to rename a slot", "external deployment controller can be changed only when its exact owner/process is evidenced", "GEN2-COMPAT-OBS-001, V1 retirement and Gen3 remain stopped until this task passes"],
"acceptance_scenarios": [
"DEPLOY-003-SCN-01: stable and candidate Compose projects, service/container identities and updater ownership are distinct",
"DEPLOY-003-SCN-02: candidate dependencies resolve only to candidate services plus declared shared external resources",
"DEPLOY-003-SCN-03: candidate operation leaves every captured stable container ID unchanged; deviation aborts before canary",
"DEPLOY-003-SCN-04: all four candidate components report health/readiness and one revision",
"DEPLOY-003-SCN-05: Supervisor records ReleaseSet slot identity/revision/LKG and canaries the candidate",
"DEPLOY-003-SCN-06: forced candidate component failure rolls all traffic back to previous stable without stable recreate",
"DEPLOY-003-SCN-07: Supervisor restart preserves LKG/rollback target and candidate recovery does not alter stable IDs",
"DEPLOY-003-EXIT: isolated production ReleaseSet drill passes, after which GEN2-COMPAT-OBS-001 may resume without V1 retirement"
],
"required_tests": [
"FAST: compose config for stable and candidate slots",
"FAST: targeted go test ./runtime/supervisor ./cmd/supervisor",
"FAST: targeted go vet ./runtime/supervisor ./cmd/supervisor",
"FAST: deployment shell regression for project/identity isolation and stable ID invariance",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"PRODUCTION: isolated candidate ReleaseSet health, canary, forced rollback, Supervisor restart/LKG and stable-ID drill"
],
"terminal_gap_states": ["BLOCKED_BY_DEPLOYMENT_CONTROL", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
+3 -2
View File
@@ -9,8 +9,9 @@
- `PROVIDER_GAPS = CPU_FAST_UNAVAILABLE; GPU_UNAVAILABLE`; Local MEDIUM remains the primary route and Lifeboat remains fallback per existing M5 evidence.
- `GEN2_M6_FINAL_STATUS = NOT_FINAL_PASS_RETIREMENT_GAP_REMAINS`; FULL_FINAL technical checks passed, but no final M6 baseline tag is created because active V1 compatibility callers were not proven safe to retire.
- `GEN2_M6_FINAL_EVIDENCE = docs/evidence/gen2-030-final-acceptance-2026-08-30.json`.
- `GEN2_FINAL_STATUS = GEN2_CORE_ACCEPTED / V1_COMPATIBILITY_RETAINED / UI_DEFERRED`.
- `GEN2-COMPAT-OBS-001 = INSTRUMENTATION_COMMITTED; PRODUCTION_OBSERVATION_BLOCKED_BY_MISSING_CAPABILITY`; evidence: `docs/evidence/gen2-compat-obs-001-blocked-2026-08-30.json`. No V1 path is `SAFE_TO_RETIRE`; no retirement task was created.
- `GEN2-COGNITIVE-002 = IMPLEMENTED / CORE_VERIFIED / BROWSER_ACCEPTANCE_DEFERRED_BY_OWNER`; non-browser evidence: `docs/evidence/gen2-cognitive-002-core-verified-2026-08-30.json`.
- `GEN2-COMPAT-OBS-001 = OBSERVATION_DEFERRED_BY_OWNER`; instrumentation remains committed, but no production observation window or retirement decision is claimed. No V1 path is `SAFE_TO_RETIRE`; no retirement task was created.
- `GEN2_FINAL_STATUS = GEN2_CORE_ACCEPTED / V1_COMPATIBILITY_RETAINED / BROWSER_ACCEPTANCE_DEFERRED / COMPAT_OBSERVATION_DEFERRED`.
- `NEXT_EVOLUTION_QUEUE = instrument bounded compatibility usage, observe, then retire only individually proven paths under a future FULL_RETIREMENT gate; no Gen3 or new feature work is started.
## Current audit delta — 2026-08-24
@@ -0,0 +1,43 @@
{
"evidence_id": "gen2-cognitive-002-core-verified-2026-08-30",
"task_id": "GEN2-COGNITIVE-002",
"task_revision": 3,
"commit": "00af176a200142bd3de67ba914e542b2299f43d5",
"status": "IMPLEMENTED / CORE_VERIFIED / BROWSER_ACCEPTANCE_DEFERRED_BY_OWNER",
"owner_amendment": {
"browser_authenticated_acceptance": "DEFERRED_BY_OWNER",
"compatibility_observation": "DEFERRED_BY_OWNER",
"v1_retirement": "NOT_AUTHORIZED"
},
"non_browser_acceptance": {
"targeted_tests": "PASS: cmd/cognitive-worker, cmd/core-runtime, modules/interaction/domain, adapters/postgres",
"targeted_vet": "PASS: same packages",
"governance": ["preflight PASS", "check-diff PASS", "independent PASS", "git diff --check PASS"],
"scoped_retrieval": [
"arbitrary assertion retrieval",
"current value selected while historical value is retained",
"ProjectReference hard scope isolation before relevance ranking",
"irrelevant assertions excluded from bounded context"
],
"context_compiler": "PASS: bounded typed Interaction fragments are compiled before the cognitive gateway request",
"candidate_slot_b": {
"components": "creator-web, core-runtime, cognitive-worker, research-runtime healthy",
"restart": "candidate core-runtime and cognitive-worker restarted; both health and ready endpoints recovered",
"local_medium": "governed gateway advisory completed after restart"
}
},
"preserved_invariants": [
"Capability remains canonical owner of binding lifecycle",
"Interaction remains canonical owner of utterances and ProjectReference",
"no direct semantic SQL update",
"PersonalMemory V1 is not restored as primary retrieval",
"Lifeboat is fallback, not proof of Local MEDIUM"
],
"not_claimed": [
"browser/authenticated production acceptance",
"production promotion",
"compatibility telemetry observation",
"V1 retirement readiness"
],
"rollback": "Supervisor release rollback returns traffic to the stable last-known-good ReleaseSet; stable containers are not recreated."
}
@@ -0,0 +1,51 @@
{
"task": "GEN2-DEPLOY-003",
"status": "PASS",
"timestamp_utc": "2026-08-30T11:09:35Z",
"root_cause": {
"stable_and_candidate_project": "assistant",
"stable_and_candidate_working_directory": "/home/m/assistant-deploy/repo/deploy",
"updater": "/home/m/assistant-deploy/update.sh",
"updater_schedule": "cron every five minutes",
"unsafe_operation": "the updater ran full docker compose up for the stable project and invoked the old in-project candidate canary"
},
"remediation": {
"stable_project": "assistant",
"candidate_project": "assistant-slot-b",
"candidate_file": "deploy/docker-compose.candidate-slot.yml",
"shared_resources": ["assistant_default external network", "llm-local_default external network", "existing secret and research volumes"],
"updater_ownership": "stable-only updater; candidate release command is not invoked by the updater",
"supervisor_release_set_slot": "ReleaseSet.slot persisted in durable Supervisor state"
},
"candidate_revision": "216d76f1d8b4eeba6ee9427da4c87c3e5c728c02",
"stable_revision": "2ee5da81145cdad2da1c92c24a91145ad4af61a5",
"candidate_components": {
"creator-web": "healthy",
"core-runtime": "healthy",
"cognitive-worker": "healthy",
"research-runtime": "healthy"
},
"drill": {
"stable_ids_captured_before_candidate": true,
"stable_ids_unchanged_after_candidate_start": true,
"header_canary": "candidate slot b",
"forced_failure": "candidate cognitive-worker stopped",
"rollback": "Supervisor release rollback returned traffic to legacy stable without recreating stable containers",
"stable_ids_unchanged_after_rollback": true,
"supervisor_restart": "last-known-good legacy stable release set preserved",
"candidate_recovery": "candidate cognitive-worker restored healthy",
"stable_ids_unchanged_after_candidate_recovery": true
},
"final_state": {
"traffic_mode": "STABLE",
"stable_upstream": "http://creator-web:8084",
"last_known_good_slot": "legacy-stable",
"public_health": 200,
"public_ready": 200
},
"limits": [
"The existing stable project remains legacy-stable during the non-disruptive transition; it was not recreated merely to rename it.",
"Local MEDIUM discovery was slow/unavailable during candidate cognitive startup; it delayed startup but did not affect stable traffic or the isolated release-set drill.",
"GEN2-COMPAT-OBS-001 has not resumed and no V1 path has been retired."
]
}
+202 -1
View File
@@ -1,11 +1,31 @@
package domain
import "git.cin.su/m/Assistant/contracts/interaction"
import (
"sort"
"strings"
"git.cin.su/m/Assistant/contracts/interaction"
)
const (
gen2ContextMaxCandidates = 4
gen2ContextMaxBytes = 2048
)
type ContextFragment struct {
UtteranceID interaction.HumanUtteranceID
Text string
ScopeRef string
Relevance int
Historical bool
}
type ContextProjection struct {
Text string
Source string
UsedPersonalMemoryFallback bool
Fragments []ContextFragment
Historical []ContextFragment
}
// BuildGen2Context is a read-side projection over canonical utterance history.
@@ -19,6 +39,14 @@ func BuildGen2Context(history []interaction.HumanUtterance) ContextProjection {
// filter prevents accidental cross-principal projection when a mixed source
// is passed during degraded operation.
func BuildScopedGen2Context(history []interaction.HumanUtterance, principalID string) ContextProjection {
return BuildScopedGen2ContextForQuery(history, nil, principalID, "", "")
}
// BuildScopedGen2ContextForQuery projects bounded, explicit human assertions
// from the canonical Interaction history. Principal and typed project refs are
// hard filters before relevance ranking. Older values remain lineage in
// Historical and are never represented as the current value.
func BuildScopedGen2ContextForQuery(history []interaction.HumanUtterance, references []interaction.ProjectReference, principalID, focusProjectID, query string) ContextProjection {
if principalID != "" {
scoped := make([]interaction.HumanUtterance, 0, len(history))
for _, item := range history {
@@ -28,9 +56,182 @@ func BuildScopedGen2Context(history []interaction.HumanUtterance, principalID st
}
history = scoped
}
projectRefs := make(map[interaction.HumanUtteranceID][]interaction.ProjectReference, len(references))
for _, ref := range references {
projectRefs[ref.UtteranceID] = append(projectRefs[ref.UtteranceID], ref)
}
assertions := explicitAssertionCandidates(history, projectRefs, focusProjectID, query)
if len(assertions) > 0 {
current, historical := selectCurrentAssertions(assertions)
return contextProjectionFromAssertions(current, historical)
}
text := CompilePersonalMemory(history).ContextText()
if text != "" {
return ContextProjection{Text: text, Source: "retrieval:interaction-utterances"}
}
return ContextProjection{Text: text, Source: "personal-memory-v1-fallback", UsedPersonalMemoryFallback: true}
}
type assertionCandidate struct {
fragment ContextFragment
subject string
observed int64
}
func explicitAssertionCandidates(history []interaction.HumanUtterance, projectRefs map[interaction.HumanUtteranceID][]interaction.ProjectReference, focusProjectID, query string) []assertionCandidate {
queryTokens := significantContextTokens(query)
result := make([]assertionCandidate, 0)
for _, utterance := range history {
subject, value, ok := explicitAssertion(utterance.Content)
if !ok {
continue
}
refs := projectRefs[utterance.ID]
scopeRef := ""
if focusProjectID != "" {
matched := false
for _, ref := range refs {
if string(ref.ProjectID) == focusProjectID {
matched = true
scopeRef = focusProjectID
break
}
}
if !matched {
continue
}
} else if len(refs) == 1 {
scopeRef = string(refs[0].ProjectID)
}
relevance := contextRelevance(subject, queryTokens)
if len(queryTokens) > 0 && relevance == 0 {
continue
}
result = append(result, assertionCandidate{
fragment: ContextFragment{UtteranceID: utterance.ID, Text: subject + " = " + value, ScopeRef: scopeRef, Relevance: relevance},
subject: normalizedContextSubject(subject),
observed: utterance.ReceivedAtUnixMillis,
})
}
return result
}
func selectCurrentAssertions(candidates []assertionCandidate) ([]ContextFragment, []ContextFragment) {
latest := make(map[string]assertionCandidate)
for _, candidate := range candidates {
current, exists := latest[candidate.subject]
if !exists || candidate.observed > current.observed || candidate.observed == current.observed && candidate.fragment.UtteranceID > current.fragment.UtteranceID {
latest[candidate.subject] = candidate
}
}
current := make([]ContextFragment, 0, len(latest))
historical := make([]ContextFragment, 0, len(candidates)-len(latest))
for _, candidate := range candidates {
selected := latest[candidate.subject]
if candidate.fragment.UtteranceID == selected.fragment.UtteranceID {
current = append(current, candidate.fragment)
continue
}
candidate.fragment.Historical = true
historical = append(historical, candidate.fragment)
}
sort.SliceStable(current, func(i, j int) bool {
if current[i].Relevance != current[j].Relevance {
return current[i].Relevance > current[j].Relevance
}
return current[i].UtteranceID > current[j].UtteranceID
})
return current, historical
}
func contextProjectionFromAssertions(current, historical []ContextFragment) ContextProjection {
selected := make([]ContextFragment, 0, len(current))
text := make([]string, 0, len(current))
usedBytes := 0
for _, candidate := range current {
if len(selected) == gen2ContextMaxCandidates {
break
}
bytes := len([]byte(candidate.Text))
if bytes == 0 || usedBytes+bytes > gen2ContextMaxBytes {
continue
}
usedBytes += bytes
selected = append(selected, candidate)
text = append(text, candidate.Text)
}
if len(selected) == 0 {
return ContextProjection{Source: "retrieval:interaction-assertions", Historical: historical}
}
return ContextProjection{Text: strings.Join(text, "\n"), Source: "retrieval:interaction-assertions", Fragments: selected, Historical: historical}
}
func explicitAssertion(message string) (string, string, bool) {
text := strings.TrimSpace(StripAssistantVocative(message))
if text == "" || strings.Contains(text, "?") {
return "", "", false
}
lower := strings.ToLower(text)
if index := strings.Index(lower, "запомни:"); index >= 0 {
text = strings.TrimSpace(text[index+len("запомни:"):])
lower = strings.ToLower(text)
}
for _, prefix := range []string{"запомни:", "запомни", "помни:"} {
if strings.HasPrefix(lower, prefix) {
text = strings.TrimSpace(text[len(prefix):])
break
}
}
for _, separator := range []string{"=", "—"} {
subject, value, found := strings.Cut(text, separator)
if !found {
continue
}
subject, value = strings.TrimSpace(subject), strings.TrimSpace(value)
if subject != "" && value != "" && len([]rune(subject)) <= 160 && len([]rune(value)) <= 512 {
return subject, value, true
}
}
return "", "", false
}
func normalizedContextSubject(subject string) string {
return strings.Join(significantContextTokens(subject), " ")
}
func significantContextTokens(value string) []string {
ignored := map[string]struct{}{"какой": {}, "какая": {}, "какое": {}, "какие": {}, "текущий": {}, "текущая": {}, "текущее": {}, "я": {}, "ты": {}, "назвал": {}, "назвала": {}, "сообщил": {}, "сообщила": {}}
result := make([]string, 0)
seen := make(map[string]struct{})
for _, token := range semanticTokens(value) {
if len([]rune(token)) < 3 {
continue
}
if _, skip := ignored[token]; skip {
continue
}
if _, duplicate := seen[token]; duplicate {
continue
}
seen[token] = struct{}{}
result = append(result, token)
}
return result
}
func contextRelevance(subject string, queryTokens []string) int {
if len(queryTokens) == 0 {
return 1
}
subjectTokens := significantContextTokens(subject)
matched := 0
for _, query := range queryTokens {
for _, token := range subjectTokens {
if query == token {
matched++
break
}
}
}
return matched
}
@@ -32,7 +32,61 @@ func TestGen2ContextProjectsExplicitRememberedAssertion(t *testing.T) {
got := domain.BuildScopedGen2Context([]interaction.HumanUtterance{
{ID: "assertion", PrincipalID: "principal-1", Content: "Для этого разговора запомни: мой тестовый цвет — синий.", ReceivedAtUnixMillis: 1},
}, "principal-1")
if got.Source != "retrieval:interaction-utterances" || got.UsedPersonalMemoryFallback || !strings.Contains(got.Text, "тестовый цвет — синий") {
if got.Source != "retrieval:interaction-assertions" || got.UsedPersonalMemoryFallback || !strings.Contains(got.Text, "тестовый цвет = синий") {
t.Fatalf("remembered assertion was not projected: %#v", got)
}
}
func TestGen2ContextSelectsArbitraryRelevantCurrentAssertion(t *testing.T) {
history := []interaction.HumanUtterance{
{ID: "assertion", PrincipalID: "principal-1", Content: "контрольный код = ORBIT-731", ReceivedAtUnixMillis: 1},
{ID: "turn-1", PrincipalID: "principal-1", Content: "Спасибо", ReceivedAtUnixMillis: 2},
{ID: "turn-2", PrincipalID: "principal-1", Content: "Продолжим", ReceivedAtUnixMillis: 3},
}
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код я назвал?")
if got.Source != "retrieval:interaction-assertions" || !strings.Contains(got.Text, "ORBIT-731") || len(got.Fragments) != 1 {
t.Fatalf("arbitrary assertion was not selected: %#v", got)
}
}
func TestGen2ContextKeepsHistoricalAssertionOutOfCurrentProjection(t *testing.T) {
history := []interaction.HumanUtterance{
{ID: "old", PrincipalID: "principal-1", Content: "контрольный код = OLD-111", ReceivedAtUnixMillis: 1},
{ID: "new", PrincipalID: "principal-1", Content: "контрольный код = NEW-222", ReceivedAtUnixMillis: 2},
}
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код?")
if strings.Contains(got.Text, "OLD-111") || !strings.Contains(got.Text, "NEW-222") || len(got.Historical) != 1 || !strings.Contains(got.Historical[0].Text, "OLD-111") {
t.Fatalf("current/historical assertion separation failed: %#v", got)
}
}
func TestGen2ContextHardFiltersProjectReferencesBeforeRanking(t *testing.T) {
history := []interaction.HumanUtterance{
{ID: "a", PrincipalID: "principal-1", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1},
{ID: "b", PrincipalID: "principal-1", Content: "контрольный код = BETA", ReceivedAtUnixMillis: 2},
}
references := []interaction.ProjectReference{
{ID: "ref-a", UtteranceID: "a", ProjectID: "company-a"},
{ID: "ref-b", UtteranceID: "b", ProjectID: "company-b"},
}
for projectID, expected := range map[string]struct{ want, absent string }{
"company-a": {want: "ALPHA", absent: "BETA"},
"company-b": {want: "BETA", absent: "ALPHA"},
} {
got := domain.BuildScopedGen2ContextForQuery(history, references, "principal-1", projectID, "Какой контрольный код?")
if !strings.Contains(got.Text, expected.want) || strings.Contains(got.Text, expected.absent) || len(got.Fragments) != 1 || got.Fragments[0].ScopeRef != projectID {
t.Fatalf("scope %s leaked context: %#v", projectID, got)
}
}
}
func TestGen2ContextExcludesIrrelevantAssertionsFromBoundedProjection(t *testing.T) {
history := []interaction.HumanUtterance{
{ID: "code", PrincipalID: "principal-1", Content: "контрольный код = ORBIT-731", ReceivedAtUnixMillis: 1},
{ID: "city", PrincipalID: "principal-1", Content: "город встречи = Казань", ReceivedAtUnixMillis: 2},
}
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код?")
if strings.Contains(got.Text, "Казань") || len(got.Fragments) != 1 {
t.Fatalf("irrelevant assertion entered bounded projection: %#v", got)
}
}
+7 -1
View File
@@ -46,7 +46,10 @@ type ComponentRelease struct {
// ReleaseSet is the atomic deployment unit supervised by the control plane.
type ReleaseSet struct {
ID string `json:"id"`
ID string `json:"id"`
// Slot identifies the independently managed deployment slot. An empty value
// remains readable for release state recorded before slot isolation.
Slot string `json:"slot,omitempty"`
Revision string `json:"revision"`
Components map[string]ComponentRelease `json:"components"`
}
@@ -207,6 +210,9 @@ func (l *Lifeboat) PromoteSet() bool {
l.Current.Set = &set
l.Current.LastKnownGood = true
l.CandidateReleaseSet = nil
// The promoted ReleaseSet is now the stable target. Leaving header-canary
// mode active would keep an obsolete candidate route in the public proxy.
l.TrafficMode = TrafficStable
return l.setSetUpstreams(&set, nil)
}
+6 -1
View File
@@ -110,7 +110,9 @@ func TestPersistentReleaseSetRollbackPreservesWholeLastKnownGood(t *testing.T) {
}
first := supervisor.NewPersistentLifeboat(store)
good := testReleaseSet("stable-r1", "r1", "http://web", "http://core", "http://cognitive", "http://research")
good.Slot = "a"
candidate := testReleaseSet("candidate-r2", "r2", "http://web-c", "http://core-c", "http://cognitive-c", "http://research-c")
candidate.Slot = "b"
if ok, err := first.EstablishBaselineSet(good); err != nil || !ok {
t.Fatalf("baseline: %v %v", ok, err)
}
@@ -135,7 +137,7 @@ func TestPersistentReleaseSetRollbackPreservesWholeLastKnownGood(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if state.StableReleaseSet == nil || state.StableReleaseSet.Revision != "r1" || state.TrafficMode != supervisor.TrafficStable {
if state.StableReleaseSet == nil || state.StableReleaseSet.Revision != "r1" || state.StableReleaseSet.Slot != "a" || state.TrafficMode != supervisor.TrafficStable {
t.Fatalf("state=%+v", state)
}
}
@@ -154,6 +156,9 @@ func TestReleaseSetPromotionRetainsPreviousStableForRollback(t *testing.T) {
if !l.RecordSet(observation) || !l.PromoteSet() {
t.Fatal("promotion failed")
}
if l.TrafficMode != supervisor.TrafficStable {
t.Fatalf("promoted ReleaseSet traffic=%s, want stable", l.TrafficMode)
}
if l.RollbackTargetReleaseSet == nil || l.RollbackTargetReleaseSet.Revision != "r1" {
t.Fatalf("rollback target=%+v", l.RollbackTargetReleaseSet)
}
@@ -4,10 +4,16 @@ set -euo pipefail
root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
canary="$root/deploy/run-production-canary.sh"
supervisor="$root/deploy/update-supervisor.sh"
candidate_compose="$root/deploy/docker-compose.candidate-slot.yml"
stable_compose="$root/deploy/docker-compose.prod.yml"
stable_updater="$root/deploy/production-stable-updater.sh"
grep -Fq 'up -d --no-build --no-deps research-runtime-candidate core-runtime-candidate' "$canary"
grep -Fq 'up -d --no-build --no-deps cognitive-worker-candidate' "$canary"
grep -Fq 'up -d --no-build --no-deps creator-web-candidate' "$canary"
grep -Fq 'CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"' "$canary"
grep -Fq '[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]]' "$canary"
grep -Fq 'docker compose -p "$CANDIDATE_PROJECT"' "$canary"
grep -Fq 'up -d --no-build --no-deps research-runtime core-runtime' "$canary"
grep -Fq 'up -d --no-build --no-deps cognitive-worker' "$canary"
grep -Fq 'up -d --no-build --no-deps creator-web' "$canary"
grep -Fq 'compose up -d --no-build --no-deps supervisor' "$supervisor"
grep -Fq 'stable_ids_before=$(stable_ids)' "$canary"
grep -Fq 'assert_stable_ids_unchanged' "$canary"
@@ -16,4 +22,11 @@ grep -Fq 'after=$(stable_ids)' "$supervisor"
grep -Fq 'stable container IDs changed' "$supervisor"
grep -Fq 'HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"' "$canary"
grep -Fq 'seq 1 "$HEALTH_ATTEMPTS"' "$canary"
grep -Fq 'PROMOTE_AFTER_CANARY="${PROMOTE_AFTER_CANARY:-true}"' "$canary"
grep -Fq 'if [[ "$PROMOTE_AFTER_CANARY" != true ]]' "$canary"
grep -Fq 'name: assistant_default' "$candidate_compose"
grep -Fq 'external: true' "$candidate_compose"
! grep -Fq 'candidate:' "$stable_compose"
! grep -Fq 'run-production-canary.sh' "$stable_updater"
grep -Fq 'compose up -d --no-build "${stable_services[@]}"' "$stable_updater"
echo 'stable release-set guard: PASS'