Compare commits

...
Author SHA1 Message Date
m 53f86e4176 feat(epistemics): persist truth bundles with source provenance
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 21s
2026-08-22 15:16:42 +03:00
m 0b321fe115 Merge pull request 'fix(readiness): allow lifeboat when primary CPU is unavailable' (#109) from codex/wp-002-recovery into main
CI / test (push) Successful in 23s
Reviewed-on: #109
2026-08-22 12:10:37 +00:00
m 94ae8589ce Merge remote-tracking branch 'origin/main' into codex/wp-002-recovery
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 15:09:50 +03:00
m 8d8f7cb097 fix(readiness): allow lifeboat when primary CPU is unavailable
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 15:08:39 +03:00
m 6e9111dd0c Merge pull request 'fix(readiness): degrade optional fleet providers without blocking fallback' (#108) from codex/wp-002-recovery into main
CI / test (push) Successful in 23s
2026-08-22 12:06:15 +00:00
m 8c5386ec34 fix(readiness): degrade optional fleet providers without blocking fallback
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 23s
2026-08-22 15:05:09 +03:00
m 38cff16f45 Merge pull request 'fix(deploy): use shell-safe migration history values' (#107) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 29s
2026-08-22 11:54:55 +00:00
m 2d6baef7a9 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:53:32 +03:00
m 89924a417a fix(deploy): use shell-safe migration history values
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:51:59 +03:00
m 6735bca317 Merge pull request 'fix(deploy): normalize migration env and executable mode' (#106) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 26s
2026-08-22 11:50:33 +00:00
m 3c83e2ba5c Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:49:14 +03:00
m f2cc059e4b fix(deploy): normalize migration env and executable mode
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:47:42 +03:00
m 91c60b8c0c Merge pull request 'ops(deploy): automate idempotent migrations' (#105) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 26s
2026-08-22 11:46:24 +00:00
m 1687dad063 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:45:06 +03:00
m 37560816aa ops(deploy): automate idempotent migrations
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 21s
2026-08-22 14:43:33 +03:00
m f92406da68 Merge pull request 'fix(cognitive): preserve admission JSON fields' (#104) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 28s
2026-08-22 11:40:31 +00:00
m 0131a96de8 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 14:39:13 +03:00
m 0d4e9ee1b4 fix(cognitive): preserve admission JSON fields
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:37:32 +03:00
m 9282fc5bf6 Merge pull request 'feat(cognitive): enforce durable admission leases' (#103) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 27s
2026-08-22 11:35:27 +00:00
m 4c4a85aec6 feat(cognitive): enforce durable admission leases
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 22s
2026-08-22 14:33:53 +03:00
m ebcae4cc9c Add cognitive admission policy 2026-08-22 14:27:21 +03:00
m 9b262609b7 Merge pull request 'feat(epistemics): evaluate read-only research evidence' (#102) from codex/wp-102-research-runtime-impl into main
CI / test (push) Successful in 29s
2026-08-22 11:24:45 +00:00
23 changed files with 565 additions and 38 deletions
@@ -28,6 +28,89 @@ func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRe
func (r *CapabilityRepository) Close() { r.pool.Close() }
func (r *CapabilityRepository) AdmitCognitive(ctx context.Context, leaseID string, policy capability.AdmissionPolicy, request capability.AdmissionRequest, nowUnixMillis int64) (capability.AdmissionDecision, capability.AdmissionLease, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
if leaseID == "" || nowUnixMillis < 1 {
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonInputBudget, PolicyID: policy.ID}, capability.AdmissionLease{}, nil
}
var existingState string
var existingPolicy string
err = tx.QueryRow(ctx, `SELECT state, policy_id FROM capability.cognitive_admission_leases WHERE lease_id=$1`, leaseID).Scan(&existingState, &existingPolicy)
if err == nil {
if existingState == "ACTIVE" {
return capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: existingPolicy}, capability.AdmissionLease{ID: leaseID, PolicyID: existingPolicy}, tx.Commit(ctx)
}
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: existingPolicy}, capability.AdmissionLease{}, tx.Commit(ctx)
}
if !errors.Is(err, pgx.ErrNoRows) {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
windowStart := nowUnixMillis - 60*60*1000
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_counters(policy_id,window_start_unix_millis,accepted_count,concurrent_count,updated_at_unix_millis) VALUES($1,$2,0,0,$3) ON CONFLICT(policy_id) DO NOTHING`, policy.ID, windowStart, nowUnixMillis)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
var storedWindow int64
var accepted, concurrent int
if err = tx.QueryRow(ctx, `SELECT window_start_unix_millis,accepted_count,concurrent_count FROM capability.cognitive_admission_counters WHERE policy_id=$1 FOR UPDATE`, policy.ID).Scan(&storedWindow, &accepted, &concurrent); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
if nowUnixMillis-storedWindow >= 60*60*1000 {
storedWindow, accepted = windowStart, 0
}
var expired int64
if err = tx.QueryRow(ctx, `WITH expired AS (UPDATE capability.cognitive_admission_leases SET state='EXPIRED' WHERE policy_id=$1 AND state='ACTIVE' AND expires_at_unix_millis <= $2 RETURNING lease_id) SELECT count(*) FROM expired`, policy.ID, nowUnixMillis).Scan(&expired); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
concurrent -= int(expired)
if concurrent < 0 {
concurrent = 0
}
decision := capability.Admit(request, policy, capability.AdmissionCounters{Concurrent: concurrent, AcceptedLastHour: accepted}, nowUnixMillis)
if decision.State != capability.AdmissionAdmitted {
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted, concurrent, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return decision, capability.AdmissionLease{}, tx.Commit(ctx)
}
expires := nowUnixMillis + 60*1000
if request.DeadlineMillis > nowUnixMillis && request.DeadlineMillis < expires {
expires = request.DeadlineMillis
}
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted+1, concurrent+1, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_leases(lease_id,policy_id,state,expires_at_unix_millis,created_at_unix_millis) VALUES($1,$2,'ACTIVE',$3,$4)`, leaseID, policy.ID, expires, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return decision, capability.AdmissionLease{ID: leaseID, PolicyID: policy.ID}, tx.Commit(ctx)
}
func (r *CapabilityRepository) ReleaseCognitiveAdmission(ctx context.Context, lease capability.AdmissionLease, nowUnixMillis int64) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
var policyID string
err = tx.QueryRow(ctx, `UPDATE capability.cognitive_admission_leases SET state='RELEASED' WHERE lease_id=$1 AND state='ACTIVE' RETURNING policy_id`, lease.ID).Scan(&policyID)
if errors.Is(err, pgx.ErrNoRows) {
return tx.Commit(ctx)
}
if err != nil {
return err
}
_, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET concurrent_count=GREATEST(0,concurrent_count-1),updated_at_unix_millis=$2 WHERE policy_id=$1`, policyID, nowUnixMillis)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveHealth(ctx context.Context, health capability.Health) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.health (capability_id, state, reason, revision, semantic_version, observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (capability_id) DO UPDATE SET state=EXCLUDED.state, reason=EXCLUDED.reason, revision=EXCLUDED.revision, semantic_version=EXCLUDED.semantic_version, observed_at_unix_millis=EXCLUDED.observed_at_unix_millis WHERE capability.health.revision < EXCLUDED.revision`, health.CapabilityID, health.State, health.Reason, health.Revision, health.SemanticVersion, health.ObservedAtUnixMillis)
return err
@@ -161,3 +161,21 @@ func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, valu
value.Summary, value.EvidenceRefs, value.RequiresCognition, value.RecordedAtUnixMillis, value.Revision)
return err
}
func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value epistemics.TruthBundle, recordedAtUnixMillis int64) error {
if value.Source.ID == "" || value.Snapshot.ID == "" || value.Excerpt.ID == "" || value.Claim.ID == "" || value.Belief.ID == "" || value.Citation.ID == "" || recordedAtUnixMillis < 1 {
return fmt.Errorf("invalid truth bundle")
}
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.truth_bundles (
bundle_id,source_id,source_kind,source_locator,source_title,source_publisher,snapshot_id,snapshot_source_id,snapshot_content_digest,snapshot_captured_at_unix_millis,snapshot_freshness,
excerpt_id,excerpt_snapshot_id,excerpt_text,excerpt_start,excerpt_end,observation_id,observation_observed_at_unix_millis,claim_id,claim_statement,claim_observation_id,claim_citation_id,claim_verification,
belief_id,belief_claim_id,belief_state,citation_id,citation_excerpt_id,citation_evidence,citation_coverage,citation_conflict,citation_resolved_at_unix_millis,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
ON CONFLICT (bundle_id) DO UPDATE SET recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`,
"bundle:"+string(value.Citation.ID), value.Source.ID, value.Source.Kind, value.Source.Locator, value.Source.Title, value.Source.Publisher,
value.Snapshot.ID, value.Snapshot.SourceID, value.Snapshot.ContentDigest, value.Snapshot.CapturedAtUnixMillis, value.Snapshot.Freshness,
value.Excerpt.ID, value.Excerpt.SnapshotID, value.Excerpt.Text, value.Excerpt.Start, value.Excerpt.End,
value.Observation.ID, value.Observation.ObservedAt, value.Claim.ID, value.Claim.Statement, value.Claim.ObservationID, value.Claim.CitationID, value.Claim.Verification,
value.Belief.ID, value.Belief.ClaimID, value.Belief.State, value.Citation.ID, value.Citation.ExcerptID, value.Citation.Evidence, value.Citation.Coverage, value.Citation.Conflict, value.Citation.ResolvedAtUnixMillis, recordedAtUnixMillis)
return err
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
type httpAdmissionClient struct {
baseURL string
client *http.Client
}
func newHTTPAdmissionClient(baseURL string, client *http.Client) *httpAdmissionClient {
return &httpAdmissionClient{baseURL: strings.TrimRight(baseURL, "/"), client: client}
}
func (c *httpAdmissionClient) Admit(ctx context.Context, leaseID string, request capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
payload, err := json.Marshal(struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}{leaseID, request})
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission", bytes.NewReader(payload))
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, fmt.Errorf("admission status %d", resp.StatusCode)
}
var result struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return result.Decision, result.Lease, nil
}
func (c *httpAdmissionClient) Release(ctx context.Context, lease capability.AdmissionLease) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission/"+lease.ID+"/release", nil)
if err != nil {
return err
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("admission release status %d", resp.StatusCode)
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type admissionTestProvider struct{ calls int }
func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
p.calls++
return cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted}, nil
}
type admissionTestClient struct {
decision capability.AdmissionDecision
releases int
}
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 {
c.releases++
return nil
}
func TestInvokeAdmittedDoesNotCallProviderWhenRejected(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: "policy-1"}}
gateway := cognitiveGateway{admission: client}
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if err != nil || response.FinishReason != cognitive.FinishRateLimited || provider.calls != 0 || client.releases != 0 {
t.Fatalf("rejected admission leaked provider call: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
gateway := cognitiveGateway{admission: client}
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if err != nil || response.FinishReason != cognitive.FinishCompleted || provider.calls != 1 || client.releases != 1 {
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
+26 -4
View File
@@ -26,6 +26,11 @@ type bindingResolver interface {
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
}
type admissionClient interface {
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
Release(context.Context, capability.AdmissionLease) error
}
type providerRegistration struct {
ProviderInstanceID capability.ProviderInstanceID
Kind capability.ProviderKind
@@ -46,6 +51,7 @@ type cognitiveGateway struct {
now func() time.Time
allowCanary bool
canaryMaxOutputBytes int
admission admissionClient
}
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
@@ -95,9 +101,9 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
}
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}
started := g.now()
response, invokeErr := provider.Advise(r.Context(), invocation)
response, invokeErr := g.invokeAdmitted(r.Context(), string(request.RequestID), provider, invocation, maxOutputBytes)
if invokeErr != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
}
g.observe(r.Context(), selected, response, started)
if fallbackEligible(response.FinishReason) {
@@ -109,9 +115,9 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
fallbackRequest.ProviderBindingID, fallbackRequest.ProviderBindingRevision = fallback.Binding.ID, fallback.Binding.Revision
fallbackRequest.ProviderInstanceID, fallbackRequest.ModelDescriptorID, fallbackRequest.ProviderModelID = fallback.Provider.ID, fallback.Model.ID, fallback.Model.ProviderModelID
fallbackStarted := g.now()
fallbackResponse, fallbackErr := fallbackProvider.Advise(r.Context(), fallbackRequest)
fallbackResponse, fallbackErr := g.invokeAdmitted(r.Context(), string(request.RequestID)+":fallback", fallbackProvider, fallbackRequest, maxOutputBytes)
if fallbackErr != nil {
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
}
g.observe(r.Context(), fallback, fallbackResponse, fallbackStarted)
response = fallbackResponse
@@ -124,6 +130,22 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
}
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
var lease capability.AdmissionLease
if g.admission != nil {
decision, admittedLease, err := g.admission.Admit(ctx, leaseID, capability.AdmissionRequest{InputBytes: len(invocation.Message), MaxOutputBytes: maxOutputBytes, DeadlineMillis: 0})
if err != nil {
return cognitive.AdvisoryResponse{}, err
}
if decision.State != capability.AdmissionAdmitted {
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) }()
}
return provider.Advise(ctx, invocation)
}
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") {
+3 -3
View File
@@ -70,15 +70,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}
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})}
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
health.HTTPProbe(readinessClient, lifeboatURL),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
}})
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
+36
View File
@@ -141,6 +141,34 @@ func main() {
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
}})
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
var request struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&request); err != nil || request.LeaseID == "" {
http.Error(w, "invalid admission request", http.StatusBadRequest)
return
}
decision, lease, err := capabilityRepository.AdmitCognitive(r.Context(), request.LeaseID, admissionPolicy, request.Request, time.Now().UnixMilli())
if err != nil {
http.Error(w, "admission unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}{decision, lease})
})
mux.HandleFunc("POST /v1/internal/cognitive/admission/{lease}/release", func(w http.ResponseWriter, r *http.Request) {
if err := capabilityRepository.ReleaseCognitiveAdmission(r.Context(), capability.AdmissionLease{ID: r.PathValue("lease")}, time.Now().UnixMilli()); err != nil {
http.Error(w, "admission release unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
@@ -1159,6 +1187,14 @@ func envOr(name, fallback string) string {
return fallback
}
func envInt(name string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(name))
if err != nil || value < 1 {
return fallback
}
return value
}
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
if inventoryErr != nil {
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
+12 -7
View File
@@ -27,10 +27,10 @@ type AdmissionPolicy struct {
}
type AdmissionRequest struct {
Complexity AdmissionComplexity
InputBytes int
MaxOutputBytes int
DeadlineMillis int64
Complexity AdmissionComplexity `json:"complexity"`
InputBytes int `json:"input_bytes"`
MaxOutputBytes int `json:"max_output_bytes"`
DeadlineMillis int64 `json:"deadline_millis"`
}
type AdmissionCounters struct {
@@ -39,9 +39,14 @@ type AdmissionCounters struct {
}
type AdmissionDecision struct {
State AdmissionDecisionState
Reason AdmissionReason
PolicyID string
State AdmissionDecisionState `json:"state"`
Reason AdmissionReason `json:"reason"`
PolicyID string `json:"policy_id"`
}
type AdmissionLease struct {
ID string `json:"id"`
PolicyID string `json:"policy_id"`
}
func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision {
+3
View File
@@ -31,6 +31,9 @@ type Belief struct {
}
type TruthBundle struct {
Source Source
Snapshot SourceSnapshot
Excerpt SourceExcerpt
Observation Observation
Claim Claim
Belief Belief
@@ -0,0 +1,30 @@
BEGIN;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.cognitive_admission_counters (
policy_id TEXT PRIMARY KEY,
window_start_unix_millis BIGINT NOT NULL,
accepted_count INTEGER NOT NULL DEFAULT 0,
concurrent_count INTEGER NOT NULL DEFAULT 0,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (policy_id <> ''), CHECK (window_start_unix_millis > 0),
CHECK (accepted_count >= 0), CHECK (concurrent_count >= 0),
CHECK (updated_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS capability.cognitive_admission_leases (
lease_id TEXT PRIMARY KEY,
policy_id TEXT NOT NULL REFERENCES capability.cognitive_admission_counters(policy_id),
state TEXT NOT NULL,
expires_at_unix_millis BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
CHECK (lease_id <> ''), CHECK (state IN ('ACTIVE','RELEASED','EXPIRED')),
CHECK (expires_at_unix_millis > 0), CHECK (created_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS cognitive_admission_lease_expiry_idx
ON capability.cognitive_admission_leases(policy_id, state, expires_at_unix_millis);
RESET ROLE;
COMMIT;
@@ -0,0 +1,49 @@
BEGIN;
SET ROLE epistemics_role;
CREATE TABLE IF NOT EXISTS epistemics.truth_bundles (
bundle_id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
source_kind TEXT NOT NULL,
source_locator TEXT NOT NULL,
source_title TEXT NOT NULL,
source_publisher TEXT NOT NULL,
snapshot_id TEXT NOT NULL,
snapshot_source_id TEXT NOT NULL,
snapshot_content_digest TEXT NOT NULL,
snapshot_captured_at_unix_millis BIGINT NOT NULL,
snapshot_freshness TEXT NOT NULL,
excerpt_id TEXT NOT NULL,
excerpt_snapshot_id TEXT NOT NULL,
excerpt_text TEXT NOT NULL,
excerpt_start BIGINT NOT NULL,
excerpt_end BIGINT NOT NULL,
observation_id TEXT NOT NULL,
observation_observed_at_unix_millis BIGINT NOT NULL,
claim_id TEXT NOT NULL,
claim_statement TEXT NOT NULL,
claim_observation_id TEXT NOT NULL,
claim_citation_id TEXT NOT NULL,
claim_verification TEXT NOT NULL,
belief_id TEXT NOT NULL,
belief_claim_id TEXT NOT NULL,
belief_state TEXT NOT NULL,
citation_id TEXT NOT NULL,
citation_excerpt_id TEXT NOT NULL,
citation_evidence TEXT NOT NULL,
citation_coverage TEXT NOT NULL,
citation_conflict TEXT NOT NULL,
citation_resolved_at_unix_millis BIGINT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
UNIQUE (snapshot_id, excerpt_id, claim_id),
CHECK (bundle_id <> ''), CHECK (source_id <> ''), CHECK (source_locator <> ''),
CHECK (excerpt_start >= 0), CHECK (excerpt_end >= excerpt_start),
CHECK (recorded_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS truth_bundles_source_time_idx
ON epistemics.truth_bundles (source_id, recorded_at_unix_millis DESC);
RESET ROLE;
COMMIT;
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-assistant-postgres-1}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "migration runner: env file is missing" >&2
exit 1
fi
set -a
# The deployment env file is controlled by the production operator. Values
# are used only by the local Docker/psql boundary and are never logged.
. "$ENV_FILE"
set +a
: "${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required}"
POSTGRES_SUPERUSER="${POSTGRES_SUPERUSER//$'\r'/}"
DATABASE="${POSTGRES_DB:-assistant}"
DATABASE="${DATABASE//$'\r'/}"
psql_exec() {
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
}
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
if [[ ! "$MIGRATION_BASELINE" =~ ^[0-9]{3}$ ]]; then
echo "migration runner: invalid MIGRATION_BASELINE" >&2
exit 1
fi
now_ms="$(date +%s%3N)"
for file in "$REPO_DIR"/database/migrations/[0-9][0-9][0-9]_*.sql; do
migration_id="$(basename "$file" .sql)"
number="${migration_id:0:3}"
if (( 10#$number <= 10#$MIGRATION_BASELINE )); then
psql_exec -c "INSERT INTO public.assistant_migration_history(migration_id, applied_at_unix_millis) VALUES ('$migration_id', $now_ms) ON CONFLICT (migration_id) DO NOTHING;"
fi
done
fi
for file in "$REPO_DIR"/database/migrations/[0-9][0-9][0-9]_*.sql; do
[[ -f "$file" ]] || continue
migration_id="$(basename "$file" .sql)"
applied="$(psql_exec -At -c "SELECT 1 FROM public.assistant_migration_history WHERE migration_id='$migration_id' LIMIT 1;")"
[[ "$applied" == "1" ]] && continue
psql_exec < "$file"
now_ms="$(date +%s%3N)"
psql_exec -c "INSERT INTO public.assistant_migration_history(migration_id, applied_at_unix_millis) VALUES ('$migration_id', $now_ms);"
done
+5
View File
@@ -80,6 +80,11 @@ services:
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
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}
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000140-research-evaluation-runtime.json",
"task_id": "DEV-000140",
"task_file": "dev/tasks/DEV-000142-truth-bundle-persistence.json",
"task_id": "DEV-000142",
"task_revision": 1
}
@@ -1,23 +1,23 @@
{
"task_id": "DEV-000115",
"task_revision": 1,
"task_revision": 6,
"architecture_revision": "ADR-0018",
"base_repo_revision": "e83cf35",
"base_repo_revision": "9b26260",
"risk_tier": "E",
"intent": "Add a deterministic cognitive admission policy for concurrency, accepted/hour, deadline and input/output budgets without changing provider routing or bindings.",
"requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Explainable admission decision"],
"allowed_change_scope": {"modules": ["cognitive-admission"], "paths": ["contracts/capability/**", "modules/capability/domain/**", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "migrations/**", "deploy/**", "security/**", "cmd/**", "runtime/**", "adapters/**", ".gitea/**"]},
"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 capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary"], "may_change": ["Typed admission request/policy/decision contract"]},
"intent": "Run bounded cognitive admission as a durable capability-owner lease with restart recovery, while preserving provider routing and explicit fallback.",
"requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Durable admission lease", "Explainable admission decision"],
"allowed_change_scope": {"modules": ["cognitive-admission", "core-runtime", "cognitive-worker", "deployment"], "paths": ["contracts/capability/**", "adapters/postgres/capability_repository.go", "database/migrations/033_cognitive_admission_leases.sql", "cmd/core-runtime/main.go", "cmd/cognitive-worker/**", "deploy/docker-compose.prod.yml", "deploy/apply-migrations.sh", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/authority/**", "modules/work/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["Existing cognitive capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary", "capability-owned PostgreSQL schema"], "may_change": ["Typed admission request/policy/decision and lease contract"]},
"semantic_owner": "capability-cognitive-admission",
"canonical_state": [],
"state_transitions": ["request + policy + observed counters -> admitted or typed rejection"],
"side_effects_and_privileges": ["Pure policy evaluation; no provider call, queue, persistence, binding mutation or external mutation"],
"persistence_and_migrations": ["No schema or migration changes"],
"failure_retry_restart": ["Rejected requests carry a reason; retry requires a fresh policy evaluation"],
"assumptions": ["Counters are supplied by a later durable admission implementation", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"],
"acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "Simple/normal/heavy complexity remains a routing input, not authority"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "admission policy tests"],
"canonical_state": ["capability.cognitive_admission_counters", "capability.cognitive_admission_leases"],
"state_transitions": ["request + policy + durable counters -> admitted lease or typed rejection", "active lease -> released/expired"],
"side_effects_and_privileges": ["Capability database role only; internal core endpoint; provider calls remain behind cognitive gateway"],
"persistence_and_migrations": ["database/migrations/033_cognitive_admission_leases.sql", "deploy/apply-migrations.sh records applied migration IDs and is idempotent"],
"failure_retry_restart": ["Expired leases are recovered on next admission transaction; duplicate request IDs are idempotent; rejected requests fall back locally"],
"assumptions": ["Core Runtime remains the capability-owner admission boundary", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"],
"acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "expired lease does not permanently block after restart", "cognitive worker uses admission before provider and releases lease"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "compose config", "admission policy and HTTP boundary tests"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000141",
"task_revision": 2,
"architecture_revision": "ADR-0018",
"base_repo_revision": "38cff16",
"risk_tier": "E",
"intent": "Keep cognitive readiness available when an optional opportunistic fleet member is unavailable while preserving typed degraded evidence.",
"requirement_ids": ["WP-003", "WP-203", "Fleet failover readiness"],
"allowed_change_scope": {"modules": ["runtime-health", "cognitive-worker"], "paths": ["runtime/health/**", "cmd/cognitive-worker/main.go", "runtime/health/server_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000141-optional-fleet-readiness.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", ".gitea/**"]},
"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 readiness Probe and typed readiness response"], "may_change": ["Optional dependency probe behavior"]},
"semantic_owner": "runtime-health and cognitive-worker",
"canonical_state": [],
"state_transitions": ["optional provider unavailable -> readiness remains ready with typed degraded check", "required dependency unavailable -> readiness remains degraded"],
"side_effects_and_privileges": ["No external mutation; no provider invocation"],
"persistence_and_migrations": ["None"],
"failure_retry_restart": ["Optional provider can recover on subsequent readiness probe"],
"assumptions": ["CPU/Lifeboat remains a qualified fallback when opportunistic GPU is unavailable"],
"acceptance_scenarios": ["GPU timeout does not hide degraded evidence or make fallback unavailable", "required dependency failure still returns 503"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000142",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "0b321fe",
"risk_tier": "E",
"intent": "Persist typed Truth/Research bundles with exact source, snapshot, excerpt and citation provenance in the epistemics owner schema.",
"requirement_ids": ["WP-102", "Truth runtime", "Research provenance persistence"],
"allowed_change_scope": {"modules": ["epistemics", "database"], "paths": ["contracts/epistemics/truth.go", "modules/epistemics/domain/**", "adapters/postgres/epistemics_repository.go", "database/migrations/034_epistemics_truth_bundles.sql", "tests/research_contracts/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000142-truth-bundle-persistence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "deploy/**", "security/**", "cmd/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["TruthBundle, Source, SourceSnapshot, SourceExcerpt and Citation contracts"], "may_change": ["TruthBundle carries exact source evidence needed for durable provenance"]},
"semantic_owner": "epistemics",
"canonical_state": ["epistemics.truth_bundles"],
"state_transitions": ["compiled truth bundle -> durable append/update by exact citation identity"],
"side_effects_and_privileges": ["epistemics_role writes only epistemics.truth_bundles"],
"persistence_and_migrations": ["Migration 034; idempotent table/index creation"],
"failure_retry_restart": ["bundle write is idempotent by citation-derived bundle id"],
"assumptions": ["external connectors remain read-only and are a separate follow-up"],
"acceptance_scenarios": ["exact source chain is persisted", "duplicate bundle write is idempotent", "cross-owner schemas are untouched"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
+1 -1
View File
@@ -106,5 +106,5 @@ func compileResearchCandidate(candidate epistemics.ResearchCandidate) (epistemic
}
observation := epistemics.Observation{ID: epistemics.ObservationID("observation:" + string(candidate.Excerpt.ID)), SourceSnapshot: candidate.Snapshot.ID, Excerpt: candidate.Excerpt.ID, Text: candidate.Excerpt.Text, ObservedAt: candidate.Snapshot.CapturedAtUnixMillis}
claim := epistemics.Claim{ID: epistemics.ClaimID("claim:" + string(candidate.Excerpt.ID)), Statement: candidate.Statement, ObservationID: observation.ID, CitationID: citation.ID, Verification: epistemics.VerificationUnverified}
return epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(candidate.Excerpt.ID)), ClaimID: claim.ID, State: epistemics.BeliefProvisional}, Citation: citation}, nil
return epistemics.TruthBundle{Source: candidate.Source, Snapshot: candidate.Snapshot, Excerpt: candidate.Excerpt, Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(candidate.Excerpt.ID)), ClaimID: claim.ID, State: epistemics.BeliefProvisional}, Citation: citation}, nil
}
+1 -1
View File
@@ -23,7 +23,7 @@ func CompileObservedClaim(source epistemics.Source, snapshot epistemics.SourceSn
if conflict == epistemics.ConflictPresent || conflict == epistemics.ConflictUnknown {
state = epistemics.BeliefConflicted
}
bundle := epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(excerpt.ID)), ClaimID: claim.ID, State: state}, Citation: citation}
bundle := epistemics.TruthBundle{Source: source, Snapshot: snapshot, Excerpt: excerpt, Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(excerpt.ID)), ClaimID: claim.ID, State: state}, Citation: citation}
if bundle.Observation.SourceSnapshot != snapshot.ID || bundle.Claim.ObservationID != bundle.Observation.ID || bundle.Claim.CitationID != bundle.Citation.ID || bundle.Belief.ClaimID != bundle.Claim.ID {
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
}
+16 -4
View File
@@ -26,8 +26,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
type Probe struct {
Name string
Check func(context.Context) error
Name string
Check func(context.Context) error
Optional bool
}
type Result struct {
@@ -59,11 +60,13 @@ func (h ReadinessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := probe.Check(ctx)
cancel()
if err != nil {
ready = false
result.State, result.Reason, result.Detail = "DEGRADED", "DEPENDENCY_UNAVAILABLE", err.Error()
if !probe.Optional {
ready = false
}
}
}
if result.State != "READY" {
if result.State != "READY" && !probe.Optional {
ready = false
}
results = append(results, result)
@@ -96,6 +99,15 @@ func HTTPProbe(client *http.Client, endpoint string) Probe {
}}
}
// OptionalHTTPProbe reports dependency degradation without making the whole
// runtime unavailable. This is used for opportunistic fleet members when a
// qualified local fallback remains available.
func OptionalHTTPProbe(client *http.Client, endpoint string) Probe {
probe := HTTPProbe(client, endpoint)
probe.Optional = true
return probe
}
func TCPProbe(name, host, port string) Probe {
return Probe{Name: name, Check: func(ctx context.Context) error {
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(host, port))
+13
View File
@@ -26,3 +26,16 @@ func TestReadinessReturns200WhenAllProbesPass(t *testing.T) {
t.Fatalf("readiness response = %d %s", recorder.Code, recorder.Body.String())
}
}
func TestReadinessStaysAvailableWithOptionalDependencyDegraded(t *testing.T) {
handler := ReadinessHandler{Runtime: "test", Probes: []Probe{
{Name: "lifeboat", Check: func(context.Context) error { return nil }},
{Name: "opportunistic-gpu", Optional: true, Check: func(context.Context) error { return errors.New("timeout") }},
}}
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil))
body := recorder.Body.String()
if recorder.Code != http.StatusOK || !strings.Contains(body, `"status":"ready"`) || !strings.Contains(body, `"state":"DEGRADED"`) {
t.Fatalf("readiness response = %d %s", recorder.Code, body)
}
}
@@ -1,6 +1,8 @@
package cognitive_admission_test
import (
"encoding/json"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
@@ -26,6 +28,13 @@ func TestAdmitReturnsExplainableDecision(t *testing.T) {
}
}
func TestAdmissionRequestUsesStableWireFields(t *testing.T) {
raw, err := json.Marshal(capability.AdmissionRequest{Complexity: capability.AdmissionNormal, InputBytes: 10, MaxOutputBytes: 20, DeadlineMillis: 30})
if err != nil || !strings.Contains(string(raw), `"input_bytes":10`) || !strings.Contains(string(raw), `"max_output_bytes":20`) || !strings.Contains(string(raw), `"deadline_millis":30`) {
t.Fatalf("admission wire shape drifted: %s %v", raw, err)
}
}
func TestAdmitRejectsBudgetAndDeadline(t *testing.T) {
policy := capability.AdmissionPolicy{ID: "local-v1", MaxConcurrent: 1, MaxAcceptedPerHour: 1, MaxInputBytes: 10, MaxOutputBytes: 10}
if decision := capability.Admit(capability.AdmissionRequest{InputBytes: 11, MaxOutputBytes: 1}, policy, capability.AdmissionCounters{}, 1); decision.Reason != capability.AdmissionReasonInputBudget {
@@ -0,0 +1,26 @@
package research_contracts_test
import (
"os"
"strings"
"testing"
)
func TestTruthBundlePersistenceMigrationIsOwnedAndIdempotent(t *testing.T) {
payload, err := os.ReadFile("../../database/migrations/034_epistemics_truth_bundles.sql")
if err != nil {
t.Fatal(err)
}
script := string(payload)
for _, required := range []string{
"SET ROLE epistemics_role",
"CREATE TABLE IF NOT EXISTS epistemics.truth_bundles",
"UNIQUE (snapshot_id, excerpt_id, claim_id)",
"CREATE INDEX IF NOT EXISTS truth_bundles_source_time_idx",
"RESET ROLE",
} {
if !strings.Contains(script, required) {
t.Fatalf("migration missing %q", required)
}
}
}