Compare commits
193
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6b0fcd768 | ||
|
|
dd9bca3ace | ||
|
|
9d94a86300 | ||
|
|
cb50b7357b | ||
|
|
dfbee7568f | ||
|
|
f803cb67f0 | ||
|
|
75fa8477d8 | ||
|
|
b4c3479614 | ||
|
|
82b89a37d3 | ||
|
|
4559e0439c | ||
|
|
b39c3ed153 | ||
|
|
dd04a67731 | ||
|
|
a0bd2a1831 | ||
|
|
c39126e0c1 | ||
|
|
8ec7c7419f | ||
|
|
a07aba056d | ||
|
|
901d5e011d | ||
|
|
74f326f11a | ||
|
|
850fe92ecb | ||
|
|
9312441d00 | ||
|
|
a6273d0134 | ||
|
|
e648d0423d | ||
|
|
07f0f6e380 | ||
|
|
e1b9c7ca84 | ||
|
|
00af176a20 | ||
|
|
ce7000e78e | ||
|
|
6204e14177 | ||
|
|
185317ee6a | ||
|
|
2e824b3c1f | ||
|
|
f0360a8e13 | ||
|
|
5593f2ac2c | ||
|
|
216d76f1d8 | ||
|
|
499b9678e1 | ||
|
|
78a39ec011 | ||
|
|
d120cc0958 | ||
|
|
2ee5da8114 | ||
|
|
91f0c9e799 | ||
|
|
cb66a60db2 | ||
|
|
6a4f973e38 | ||
|
|
87ec7efd0f | ||
|
|
51e33e7283 | ||
|
|
e8f35f6393 | ||
|
|
8babf6ae1f | ||
|
|
d0210e5aed | ||
|
|
ebdfaa0769 | ||
|
|
9e0faa7c47 | ||
|
|
7f8a05ccfb | ||
|
|
c94d04acd0 | ||
|
|
bb866cd2ab | ||
|
|
892266a4c8 | ||
|
|
ac531c3ec3 | ||
|
|
4cd50a0937 | ||
|
|
8b751257e8 | ||
|
|
cbbb60e081 | ||
|
|
46e3cfc445 | ||
|
|
5832c49ad8 | ||
|
|
b13d12583b | ||
|
|
9d5473d260 | ||
|
|
50b508b641 | ||
|
|
8400cdd92d | ||
|
|
ec1860d441 | ||
|
|
134ba26e08 | ||
|
|
b82b8b874f | ||
|
|
5c9d4663f8 | ||
|
|
ec9d434650 | ||
|
|
a8a60ce29e | ||
|
|
96cf66a876 | ||
|
|
381b52d06d | ||
|
|
4ceec5a316 | ||
|
|
93a9c1a3bd | ||
|
|
f3ad7d682b | ||
|
|
387de89efe | ||
|
|
346e0acb6b | ||
|
|
3efcbbded8 | ||
|
|
a0b742b1d5 | ||
|
|
86aacea99e | ||
|
|
2a8e4e1b81 | ||
|
|
d64080ef2b | ||
|
|
59d32143bf | ||
|
|
031ba7666c | ||
|
|
93feb9bf2d | ||
|
|
a957172d3c | ||
|
|
3182092d5c | ||
|
|
df8a275eb1 | ||
|
|
8aa5179d47 | ||
|
|
7b62781087 | ||
|
|
5c95916648 | ||
|
|
8caa49f429 | ||
|
|
5b5875d0f4 | ||
|
|
f7ffaee10d | ||
|
|
550738f3ca | ||
|
|
e8b4c4fdab | ||
|
|
39f6fdee7b | ||
|
|
711a734fee | ||
|
|
ad851a98aa | ||
|
|
1e3f29b791 | ||
|
|
62fd265bb7 | ||
|
|
2a52306f90 | ||
|
|
e1c2e5f59c | ||
|
|
49b3165caa | ||
|
|
41b5c57d05 | ||
|
|
31dd628519 | ||
|
|
d0103bbf8e | ||
|
|
29217ca935 | ||
|
|
e9c3df26ae | ||
|
|
70ece6ed5c | ||
|
|
916b93517a | ||
|
|
cf68786d0c | ||
|
|
44ac0203a9 | ||
|
|
8571e3fe59 | ||
|
|
0af6a9b9a5 | ||
|
|
6b9d5be299 | ||
|
|
8b3d6f9176 | ||
|
|
b4ab280950 | ||
|
|
9cb69d1663 | ||
|
|
5387010a15 | ||
|
|
66fd4bfc75 | ||
|
|
8ac4525569 | ||
|
|
40c610945f | ||
|
|
99458a6ced | ||
|
|
25c62e38fc | ||
|
|
5c0c6b3e42 | ||
|
|
788137b4ba | ||
|
|
2ef639eba9 | ||
|
|
d4ed85dec2 | ||
|
|
c1c2110322 | ||
|
|
d83fcb2176 | ||
|
|
041fb1556d | ||
|
|
a3baade40b | ||
|
|
12104210a3 | ||
|
|
40dcc36b06 | ||
|
|
433cb2de2c | ||
|
|
b49ff6cb71 | ||
|
|
4bb6728dfe | ||
|
|
b00130ac15 | ||
|
|
fc356eb0a3 | ||
|
|
0f880896fe | ||
|
|
460a82e020 | ||
|
|
e27ead1a75 | ||
|
|
64bc8bca25 | ||
|
|
67b585af94 | ||
|
|
e47ea7b48e | ||
|
|
75042e090f | ||
|
|
1168c00caf | ||
|
|
6e873ae3af | ||
|
|
b7210dc0ad | ||
|
|
c0704dd28e | ||
|
|
198f6cfe58 | ||
|
|
49608599c4 | ||
|
|
4f6d205a60 | ||
|
|
bcb833f15e | ||
|
|
4f6f2a1f48 | ||
|
|
f709c026d0 | ||
|
|
92ad3516ea | ||
|
|
e1beed0391 | ||
|
|
6b297c7749 | ||
|
|
91b8da2240 | ||
|
|
8924f5a48a | ||
|
|
636fb904f2 | ||
|
|
598a97cd1a | ||
|
|
e9ef635933 | ||
|
|
c516754d63 | ||
|
|
5c965ef69c | ||
|
|
eab7d11bdd | ||
|
|
51a49ba0ef | ||
|
|
60ff22882c | ||
|
|
9896e4f716 | ||
|
|
ff0d0e585b | ||
|
|
13f3165ba0 | ||
|
|
55f2f9affa | ||
|
|
e47e033dda | ||
|
|
2dc5587fd1 | ||
|
|
992c84240b | ||
|
|
cfeb6aeba7 | ||
|
|
f0d45ca9f1 | ||
|
|
e20c9fdfc0 | ||
|
|
cb50a120e5 | ||
|
|
b30eb38eca | ||
|
|
2083096aed | ||
|
|
671ecb4968 | ||
|
|
af9d5809e9 | ||
|
|
990928618d | ||
|
|
9fac8284f5 | ||
|
|
7d9a5edee5 | ||
|
|
32d3f318a5 | ||
|
|
3a563a29ee | ||
|
|
734db5d779 | ||
|
|
36fc8ec8c6 | ||
|
|
b8c001be4c | ||
|
|
eeeba6b7c0 | ||
|
|
fce3ec569c | ||
|
|
a925d787dc | ||
|
|
43556e1603 |
@@ -25,6 +25,15 @@ type OpenAICompatibleLocalAdapter struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// compatibleDefaultContextBytes is a conservative bound used when an
|
||||
// OpenAI-compatible local endpoint omits n_ctx from /v1/models. The adapter
|
||||
// is a chat-completions adapter, so advisory capability is known from the
|
||||
// adapter contract; quality and activation still require governed
|
||||
// qualification/evaluation in the capability registry.
|
||||
const compatibleDefaultContextBytes int64 = 32 << 10
|
||||
|
||||
const compatibleAdvisoryMaxTokens = 32
|
||||
|
||||
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
|
||||
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
|
||||
}
|
||||
@@ -58,6 +67,9 @@ type compatibleModelsResponse struct {
|
||||
ID string `json:"id"`
|
||||
Created int64 `json:"created"`
|
||||
OwnedBy string `json:"owned_by"`
|
||||
ModelKind string `json:"model_kind"`
|
||||
Capabilities map[string]bool `json:"capabilities"`
|
||||
Modalities map[string]bool `json:"modalities"`
|
||||
Meta struct {
|
||||
Context int64 `json:"n_ctx"`
|
||||
TrainingContext int64 `json:"n_ctx_train"`
|
||||
@@ -87,14 +99,36 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
|
||||
if item.ID == "" {
|
||||
continue
|
||||
}
|
||||
// The gateway catalogue may contain image/audio runtimes beside local
|
||||
// chat models. Discovery is not allowed to qualify those as text
|
||||
// advisory models merely because they appear in /v1/models.
|
||||
if !isTextAdvisoryModel(item.ModelKind, item.Capabilities, item.Modalities) {
|
||||
continue
|
||||
}
|
||||
stableMetadata := fmt.Sprintf("%s\x00%s\x00%d\x00%d\x00%d\x00%d\x00%s", item.ID, item.OwnedBy, item.Meta.Context, item.Meta.TrainingContext, item.Meta.Parameters, item.Meta.Size, item.Meta.FileType)
|
||||
digest := sha256.Sum256([]byte(stableMetadata))
|
||||
contextBytes := item.Meta.Context * 4
|
||||
models = append(models, DiscoveredModel{ProviderModelID: capability.ProviderModelID(item.ID), Fingerprint: capability.ModelFingerprint(hex.EncodeToString(digest[:])), MaxContextBytes: contextBytes, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityUnknown}, SizeBytes: item.Meta.Size, Runtime: item.OwnedBy, Quantization: item.Meta.FileType})
|
||||
if contextBytes <= 0 {
|
||||
contextBytes = compatibleDefaultContextBytes
|
||||
}
|
||||
models = append(models, DiscoveredModel{ProviderModelID: capability.ProviderModelID(item.ID), Fingerprint: capability.ModelFingerprint(hex.EncodeToString(digest[:])), MaxContextBytes: contextBytes, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, SizeBytes: item.Meta.Size, Runtime: item.OwnedBy, Quantization: item.Meta.FileType})
|
||||
}
|
||||
return models, ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
|
||||
func isTextAdvisoryModel(kind string, capabilities, modalities map[string]bool) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(kind), "image-generation") || strings.EqualFold(strings.TrimSpace(kind), "audio-transcription") || strings.EqualFold(strings.TrimSpace(kind), "text-to-speech") {
|
||||
return false
|
||||
}
|
||||
if chat, known := capabilities["chat"]; known && !chat {
|
||||
return false
|
||||
}
|
||||
if text, known := modalities["text"]; known && !text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type compatibleChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []compatibleMessage `json:"messages"`
|
||||
@@ -125,7 +159,11 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
|
||||
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
|
||||
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
|
||||
}
|
||||
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
|
||||
maxTokens := request.MaxOutputBytes / 4
|
||||
if maxTokens <= 0 || maxTokens > compatibleAdvisoryMaxTokens {
|
||||
maxTokens = compatibleAdvisoryMaxTokens
|
||||
}
|
||||
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: maxTokens, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
|
||||
var result compatibleChatResponse
|
||||
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
|
||||
if err != nil {
|
||||
@@ -226,6 +264,8 @@ func mapCompatibleStatus(status int) cognitive.FinishReason {
|
||||
return cognitive.FinishContextTooLarge
|
||||
case 429:
|
||||
return cognitive.FinishRateLimited
|
||||
case 400, 422:
|
||||
return cognitive.FinishInvalidProviderResponse
|
||||
default:
|
||||
return cognitive.FinishProviderUnavailable
|
||||
}
|
||||
|
||||
@@ -18,6 +18,65 @@ type captureRoundTripper struct {
|
||||
body []byte
|
||||
}
|
||||
|
||||
type modelsMetadataRoundTripper struct{}
|
||||
|
||||
func (modelsMetadataRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"medium-model","owned_by":"llama.cpp","meta":{}}]}`)),
|
||||
Header: make(http.Header),
|
||||
Request: request,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mixedModelsRoundTripper struct{}
|
||||
|
||||
func (mixedModelsRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"image-model","model_kind":"image-generation","capabilities":{"chat":false,"image_generation":true},"modalities":{"text":false,"image_generation":true}},{"id":"text-model","model_kind":"chat","capabilities":{"chat":true},"modalities":{"text":true},"meta":{}}]}`)),
|
||||
Header: make(http.Header),
|
||||
Request: request,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterDoesNotQualifyNonTextModels(t *testing.T) {
|
||||
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", mixedModelsRoundTripper{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
models, probe := adapter.Discover(context.Background())
|
||||
if probe.Health != capability.HealthAvailable || len(models) != 1 || models[0].ProviderModelID != "text-model" {
|
||||
t.Fatalf("unexpected filtered discovery: probe=%+v models=%+v", probe, models)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterMapsImmediateBadRequest(t *testing.T) {
|
||||
if got := mapCompatibleStatus(http.StatusBadRequest); got != cognitive.FinishInvalidProviderResponse {
|
||||
t.Fatalf("400 finish=%s, want %s", got, cognitive.FinishInvalidProviderResponse)
|
||||
}
|
||||
if got := mapCompatibleStatus(http.StatusUnprocessableEntity); got != cognitive.FinishInvalidProviderResponse {
|
||||
t.Fatalf("422 finish=%s, want %s", got, cognitive.FinishInvalidProviderResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterNormalizesMinimalModelMetadata(t *testing.T) {
|
||||
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", modelsMetadataRoundTripper{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
models, probe := adapter.Discover(context.Background())
|
||||
if probe.Health != capability.HealthAvailable || len(models) != 1 {
|
||||
t.Fatalf("unexpected discovery result: probe=%+v models=%+v", probe, models)
|
||||
}
|
||||
if models[0].MaxContextBytes != compatibleDefaultContextBytes {
|
||||
t.Fatalf("context bound=%d, want %d", models[0].MaxContextBytes, compatibleDefaultContextBytes)
|
||||
}
|
||||
if len(models[0].Capabilities) != 1 || models[0].Capabilities[0] != capability.CapabilityAdvisory {
|
||||
t.Fatalf("capabilities=%v, want advisory", models[0].Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
|
||||
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
|
||||
if err != nil {
|
||||
@@ -81,6 +140,7 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload struct {
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
ChatTemplateKwargs struct {
|
||||
EnableThinking bool `json:"enable_thinking"`
|
||||
} `json:"chat_template_kwargs"`
|
||||
@@ -88,6 +148,9 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
|
||||
if err := json.Unmarshal(transport.body, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.MaxTokens != compatibleAdvisoryMaxTokens {
|
||||
t.Fatalf("max_tokens=%d, want bounded %d", payload.MaxTokens, compatibleAdvisoryMaxTokens)
|
||||
}
|
||||
if payload.ChatTemplateKwargs.EnableThinking {
|
||||
t.Fatal("expected bounded advisory to disable template thinking")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -181,10 +182,6 @@ func (r *CapabilityRepository) SaveBinding(ctx context.Context, binding capabili
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
@@ -309,12 +306,14 @@ func (r *CapabilityRepository) SaveSkillProfile(ctx context.Context, value capab
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.skill_profiles (
|
||||
skill_profile_id,revision,name,description,applicable_context,implementation_ref,implementation_version,status,
|
||||
supporting_experience_refs,known_failure_experience_refs,confidence_millis,last_validated_unix_millis,
|
||||
observed_failure_count,observed_correction_count,recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT DO NOTHING`,
|
||||
observed_failure_count,observed_correction_count,recorded_at_unix_millis,applicability,dependency_refs,validation_evidence_refs,
|
||||
independent_evaluation_refs,known_failure_conditions,verified_outcome_count,must_not_worsen_satisfied)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.Revision, value.Name, value.Description, value.ApplicableContext, value.ImplementationRef,
|
||||
value.ImplementationVersion, value.Status, value.SupportingExperienceRefs, value.KnownFailureExperienceRefs,
|
||||
value.ConfidenceMillis, value.LastValidatedUnixMillis, value.ObservedFailureCount, value.ObservedCorrectionCount,
|
||||
value.RecordedAtUnixMillis)
|
||||
value.RecordedAtUnixMillis, mustJSON(value.Applicability), value.DependencyRefs, value.ValidationEvidenceRefs,
|
||||
value.IndependentEvaluationRefs, value.KnownFailureConditions, value.VerifiedOutcomeCount, value.MustNotWorsenSatisfied)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -333,13 +332,16 @@ func (r *CapabilityRepository) LoadSkillProfile(ctx context.Context, id capabili
|
||||
err := r.pool.QueryRow(ctx, `SELECT
|
||||
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
|
||||
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
|
||||
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
|
||||
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs,
|
||||
p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied
|
||||
FROM capability.skill_profile_heads h
|
||||
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
|
||||
WHERE p.skill_profile_id=$1`, id).Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
|
||||
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
|
||||
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
|
||||
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis)
|
||||
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability),
|
||||
&value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions,
|
||||
&value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied)
|
||||
if err != nil {
|
||||
return capability.SkillProfile{}, fmt.Errorf("load skill profile: %w", err)
|
||||
}
|
||||
@@ -353,7 +355,8 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
|
||||
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
|
||||
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
|
||||
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs,
|
||||
p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied
|
||||
FROM capability.skill_profile_heads h
|
||||
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
|
||||
ORDER BY h.updated_at DESC, p.skill_profile_id LIMIT $1`, limit)
|
||||
@@ -367,7 +370,9 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
|
||||
if err := rows.Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
|
||||
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
|
||||
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
|
||||
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis); err != nil {
|
||||
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability),
|
||||
&value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions,
|
||||
&value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -375,6 +380,40 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveReflexRevision(ctx context.Context, reflex capability.Reflex, value capability.ReflexRevision) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.reflex_revisions (
|
||||
reflex_id,revision,principal_id,source_skill_id,source_skill_revision,kind,applicability,implementation_ref,implementation_version,artifact_digest,
|
||||
dependency_refs,validation_evidence_refs,known_failure_experience_refs,shadow_policy,revalidation_policy,last_validated_at,lifecycle,
|
||||
supersedes_revision,execution_count,failure_count,divergence_count,recorded_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`,
|
||||
value.ReflexID, value.Revision, reflex.PrincipalID, reflex.SourceSkillRef, value.SourceSkillRevision, value.Kind, mustJSON(value.Applicability), value.ImplementationRef, value.ImplementationVersion, value.ArtifactDigest,
|
||||
value.DependencyRefs, value.ValidationEvidenceRefs, value.KnownFailureExperienceRefs, mustJSON(value.Shadow), mustJSON(value.Revalidation), value.LastValidatedAt, value.Lifecycle,
|
||||
value.SupersedesRevision, value.ExecutionCount, value.FailureCount, value.DivergenceCount, value.RecordedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO capability.reflex_heads(reflex_id,revision,lifecycle) VALUES($1,$2,$3)
|
||||
ON CONFLICT(reflex_id) DO UPDATE SET revision=EXCLUDED.revision,lifecycle=EXCLUDED.lifecycle,updated_at=CURRENT_TIMESTAMP
|
||||
WHERE capability.reflex_heads.revision < EXCLUDED.revision`, value.ReflexID, value.Revision, value.Lifecycle)
|
||||
return err
|
||||
}
|
||||
|
||||
func mustJSON(value any) []byte { encoded, _ := json.Marshal(value); return encoded }
|
||||
|
||||
type jsonTarget struct{ target any }
|
||||
|
||||
func newJSONTarget(target any) *jsonTarget { return &jsonTarget{target: target} }
|
||||
func (v *jsonTarget) Scan(source any) error {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
bytes, ok := source.([]byte)
|
||||
if !ok {
|
||||
bytes = []byte(fmt.Sprint(source))
|
||||
}
|
||||
return json.Unmarshal(bytes, v.target)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
|
||||
var binding capability.Binding
|
||||
err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path,
|
||||
@@ -437,6 +476,14 @@ func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Con
|
||||
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidatesForEvaluation(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, evaluationID capability.EvaluationID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
var profileID capability.TaskEvaluationProfileID
|
||||
if err := r.pool.QueryRow(ctx, `SELECT task_profile_id FROM capability.cognitive_task_evaluations WHERE evaluation_id=$1 AND capability_id=$2`, evaluationID, capabilityID).Scan(&profileID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
var capabilityID capability.CapabilityID
|
||||
var owner identity.PrincipalID
|
||||
@@ -564,7 +611,12 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
|
||||
// Discovery may advance a head, but must never roll back a governed
|
||||
// CANARY/ACTIVE revision to an older discovered snapshot after restart.
|
||||
// Discovery may advance an inactive head even when its deterministic
|
||||
// revision number is lower, but it must never overwrite a governed canary
|
||||
// or active head during restart/startup refresh.
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT (binding_id) DO UPDATE SET revision=EXCLUDED.revision,updated_at=CURRENT_TIMESTAMP WHERE NOT EXISTS (SELECT 1 FROM capability.cognitive_bindings current_binding WHERE current_binding.binding_id=capability.cognitive_binding_revision_heads.binding_id AND current_binding.revision=capability.cognitive_binding_revision_heads.revision AND current_binding.activation IN ('CANARY','ACTIVE'))`, binding.ID, binding.Revision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/platform/telemetry"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -36,6 +37,11 @@ func (r *EpistemicsRepository) Ping(ctx context.Context) error {
|
||||
// TruthRuntimeStatus is a read-only projection of durable truth bundles.
|
||||
// It does not promote claims, revise beliefs, or mutate epistemic state.
|
||||
func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRuntimeStatus, error) {
|
||||
started := time.Now()
|
||||
outcome := "SUCCESS"
|
||||
defer func() {
|
||||
telemetry.RecordCompatibility("truth-bundles-status-reader", "epistemics-repository", "read", "research-status", true, false, true, outcome, "truth-status", "epistemics", started)
|
||||
}()
|
||||
status := TruthRuntimeStatus{
|
||||
ClaimsByVerification: map[string]int64{},
|
||||
BeliefsByState: map[string]int64{},
|
||||
@@ -43,6 +49,7 @@ func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRun
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT count(*) FROM epistemics.truth_bundles`).Scan(&status.BundleCount); err != nil {
|
||||
outcome = "FAILURE"
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
queries := []struct {
|
||||
@@ -56,18 +63,21 @@ func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRun
|
||||
for _, item := range queries {
|
||||
rows, err := r.pool.Query(ctx, item.query)
|
||||
if err != nil {
|
||||
outcome = "FAILURE"
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var state string
|
||||
var count int64
|
||||
if err := rows.Scan(&state, &count); err != nil {
|
||||
outcome = "FAILURE"
|
||||
rows.Close()
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
item.destination[state] = count
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
outcome = "FAILURE"
|
||||
rows.Close()
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
@@ -91,10 +101,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
|
||||
return nil, fmt.Errorf("invalid experience limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
|
||||
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
|
||||
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -103,10 +113,44 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
|
||||
var result []epistemics.ExperienceRecord
|
||||
for rows.Next() {
|
||||
var value epistemics.ExperienceRecord
|
||||
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
|
||||
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// RecentExperienceRecordsForPrincipalAndScope is an owner-local Epistemics
|
||||
// read. It intentionally applies Principal and ContextScope in SQL before any
|
||||
// Core-side relevance ranking, so another realm's experience never becomes a
|
||||
// retrieval candidate.
|
||||
func (r *EpistemicsRepository) RecentExperienceRecordsForPrincipalAndScope(ctx context.Context, principalID, scopeRef string, limit int) ([]epistemics.ExperienceRecord, error) {
|
||||
if principalID == "" || limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid scoped experience query")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
|
||||
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
|
||||
FROM epistemics.experience_records
|
||||
WHERE principal_id=$1 AND ($2='' OR context_scope_ref=$2)
|
||||
ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $3`, principalID, scopeRef, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]epistemics.ExperienceRecord, 0)
|
||||
for rows.Next() {
|
||||
var value epistemics.ExperienceRecord
|
||||
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
|
||||
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -195,15 +239,15 @@ func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, i
|
||||
|
||||
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
|
||||
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
experience_id, principal_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
|
||||
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
|
||||
verification_evidence, confidence_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) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
verification_evidence, confidence_millis, recorded_at_unix_millis, context_scope_ref, problem_statement, applicability_conditions, why_succeeded_or_failed)
|
||||
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) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.PrincipalID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
|
||||
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis, value.ContextScopeRef, value.ProblemStatement, value.ApplicabilityConditions, value.WhySucceededOrFailed)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -218,7 +262,13 @@ func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, valu
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value epistemics.TruthBundle, recordedAtUnixMillis int64) error {
|
||||
started := time.Now()
|
||||
outcome := "SUCCESS"
|
||||
defer func() {
|
||||
telemetry.RecordCompatibility("truth-bundles-writer", "epistemics-repository", "write", "persisted-research", true, false, true, outcome, string(value.Claim.ID), "epistemics", started)
|
||||
}()
|
||||
if value.Source.ID == "" || value.Snapshot.ID == "" || value.Excerpt.ID == "" || value.Claim.ID == "" || value.Belief.ID == "" || value.Citation.ID == "" || recordedAtUnixMillis < 1 {
|
||||
outcome = "INVALID"
|
||||
return fmt.Errorf("invalid truth bundle")
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.truth_bundles (
|
||||
@@ -232,5 +282,8 @@ func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value episte
|
||||
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)
|
||||
if err != nil {
|
||||
outcome = "FAILURE"
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,3 +26,39 @@ func (r *IdentityRepository) LoadBinding(ctx context.Context, actor identity.Act
|
||||
}
|
||||
return binding, nil
|
||||
}
|
||||
|
||||
func (r *IdentityRepository) LoadCreatorAccount(ctx context.Context, username string) (identity.CreatorAccount, error) {
|
||||
var account identity.CreatorAccount
|
||||
err := r.pool.QueryRow(ctx, `SELECT username, actor_id, principal_id, password_hash, revision FROM identity.creator_accounts WHERE username=$1`, username).
|
||||
Scan(&account.Username, &account.ActorID, &account.PrincipalID, &account.PasswordHash, &account.Revision)
|
||||
if err != nil {
|
||||
return identity.CreatorAccount{}, fmt.Errorf("load creator account: %w", err)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (r *IdentityRepository) LoadCreatorAccountByBinding(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID) (identity.CreatorAccount, error) {
|
||||
var account identity.CreatorAccount
|
||||
err := r.pool.QueryRow(ctx, `SELECT username, actor_id, principal_id, password_hash, revision FROM identity.creator_accounts WHERE actor_id=$1 AND principal_id=$2`, actor, principal).
|
||||
Scan(&account.Username, &account.ActorID, &account.PrincipalID, &account.PasswordHash, &account.Revision)
|
||||
if err != nil {
|
||||
return identity.CreatorAccount{}, fmt.Errorf("load creator account by binding: %w", err)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (r *IdentityRepository) CreateCreatorAccount(ctx context.Context, account identity.CreatorAccount) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO identity.creator_accounts (username, actor_id, principal_id, password_hash) VALUES ($1,$2,$3,$4) ON CONFLICT (username) DO NOTHING`, account.Username, account.ActorID, account.PrincipalID, account.PasswordHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *IdentityRepository) UpdateCreatorPassword(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID, revision int64, passwordHash string) error {
|
||||
result, err := r.pool.Exec(ctx, `UPDATE identity.creator_accounts SET password_hash=$1, revision=revision+1, password_changed_at=CURRENT_TIMESTAMP WHERE actor_id=$2 AND principal_id=$3 AND revision=$4`, passwordHash, actor, principal, revision)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update creator password: %w", err)
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return fmt.Errorf("update creator password: revision conflict")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
selfcontract "git.cin.su/m/Assistant/contracts/self"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// SelfRepository persists only SELF-owned derived assessments and ledger
|
||||
// evidence. It never evaluates, promotes or changes Capability state.
|
||||
type SelfRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SelfRepository{pool: pool}, nil
|
||||
}
|
||||
|
||||
func (r *SelfRepository) Close() { r.pool.Close() }
|
||||
|
||||
func (r *SelfRepository) SaveImprovementNeed(ctx context.Context, value selfcontract.ImprovementNeed) error {
|
||||
subject, err := json.Marshal(value.SubjectRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evidence, err := json.Marshal(value.EvidenceRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_needs(improvement_need_id,principal_id,subject_ref,problem_summary,evidence_refs,expected_benefit,priority,state,created_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, subject, value.ProblemSummary, evidence, value.ExpectedBenefit, value.Priority, value.State, value.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveLearningLedger(ctx context.Context, value selfcontract.LearningLedgerEntry) error {
|
||||
evidence, err := json.Marshal(value.EvidenceRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
improvement, err := json.Marshal(value.ImprovementNeedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proposed, err := json.Marshal(value.ProposedChangeRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evaluations, err := json.Marshal(value.ActualEvaluationRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gate, err := json.Marshal(value.DevelopmentGateRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before, err := json.Marshal(value.BeforeVersionRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
after, err := json.Marshal(value.AfterVersionRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sideEffects, err := json.Marshal(value.SideEffects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rollback, err := json.Marshal(value.RollbackRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO self.learning_ledger(entry_id,principal_id,improvement_need_ref,observed_problem,evidence_refs,diagnosis,hypothesis,proposed_change_ref,expected_improvement,evaluation_refs,development_gate_ref,before_version_refs,after_version_refs,decision,side_effects,rollback_ref,recorded_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, improvement, value.ObservedProblem, evidence, value.Diagnosis, value.Hypothesis, proposed, value.ExpectedImprovement, evaluations, gate, before, after, value.Decision, sideEffects, rollback, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveLearningSignal(ctx context.Context, value selfcontract.LearningSignal) error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
subject, err := json.Marshal(value.SubjectRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evidence, err := json.Marshal(value.EvidenceRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deps, err := json.Marshal(value.DependencyRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO self.learning_signals(signal_id,principal_id,kind,task_pattern,scope_key,subject_ref,evidence_refs,expected_outcome,actual_outcome,dependency_refs,environment_version,observed_at_unix_millis,severity,user_impact,correction_count,decision_regret)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Kind, value.TaskPattern, value.ScopeKey, subject, evidence, value.ExpectedOutcome, value.ActualOutcome, deps, value.EnvironmentVersion, value.ObservedAt, value.Severity, value.UserImpact, value.CorrectionCount, value.DecisionRegret)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveWeaknessPattern(ctx context.Context, value selfcontract.WeaknessPattern) error {
|
||||
evidence, err := json.Marshal(value.DetectionEvidenceRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deps, err := json.Marshal(value.DependencyRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO self.weakness_patterns(assessment_id,principal_id,revision,pattern_key,task_pattern,scope_key,environment_version,dependency_refs,failure_mode,detection_evidence_refs,frequency,last_observed_at_unix_millis,severity,affected_component,affected_strategy,confidence_millis,epistemic_status,state,recorded_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Revision, value.PatternKey, value.TaskPattern, value.ScopeKey, value.EnvironmentVersion, deps, value.FailureMode, evidence, value.Frequency, value.LastObservedAt, value.Severity, value.AffectedComponent, value.AffectedStrategy, value.ConfidenceMillis, value.EpistemicStatus, value.State, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveImprovementCandidate(ctx context.Context, value selfcontract.ImprovementCandidate) error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
need, err := json.Marshal(value.NeedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
must, err := json.Marshal(value.MustNotWorsen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evidence, err := json.Marshal(value.EvidenceRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_candidates(candidate_id,principal_id,need_ref,hypothesis,expected_effect,evaluation_plan,must_not_worsen,rollback_plan,resource_cost,creator,independent_evaluator,state,evidence_refs,created_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, need, value.Hypothesis, value.ExpectedEffect, value.EvaluationPlan, must, value.RollbackPlan, value.ResourceCost, value.Creator, value.IndependentEvaluator, value.State, evidence, value.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveBenchmarkCase(ctx context.Context, value selfcontract.BenchmarkCase) error {
|
||||
if !value.Valid() {
|
||||
return fmt.Errorf("benchmark case is incomplete")
|
||||
}
|
||||
evidence, err := json.Marshal(value.EvidenceRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO self.personal_benchmark_cases(case_id,principal_id,task_class,evidence_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.TaskClass, evidence, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -20,6 +20,7 @@ type fleetEndpoint struct {
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Client *http.Client
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type fleetStatusResponse struct {
|
||||
@@ -63,7 +64,11 @@ func probeFleetEndpoint(parent context.Context, fallbackClient *http.Client, end
|
||||
if strings.TrimSpace(endpoint.URL) == "" {
|
||||
return member
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
|
||||
timeout := endpoint.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 2 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, timeout)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.URL, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -68,3 +68,21 @@ func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetReadinessUsesProviderSpecificTimeout(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
handler := fleetReadinessHandler(server.Client(), capability.FleetTierMedium, []fleetEndpoint{{
|
||||
ProviderID: "medium", ModelID: "medium-model", Tier: capability.FleetTierMedium,
|
||||
URL: server.URL, Client: server.Client(), Timeout: 100 * time.Millisecond,
|
||||
}})
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"MEDIUM"`) {
|
||||
t.Fatalf("provider-specific timeout was not honored: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -131,7 +135,51 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
|
||||
writeGatewayResponse(w, gatewayAdvisoryResponse(request, response))
|
||||
}
|
||||
|
||||
const iterativeRetrievalMarker = "<domovoy-retrieval>"
|
||||
|
||||
// gatewayAdvisoryResponse is the only model-facing extraction point. The
|
||||
// model may express a bounded retrieval intent, but it cannot provide scope,
|
||||
// identity, policy or storage details; Core supplies those on continuation.
|
||||
func gatewayAdvisoryResponse(request cognitive.GatewayAdvisoryRequest, response cognitive.AdvisoryResponse) cognitive.GatewayAdvisoryResponse {
|
||||
result := cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
|
||||
if !request.AllowIterativeRetrieval || response.FinishReason != cognitive.FinishCompleted {
|
||||
return result
|
||||
}
|
||||
intent, text, ok := parseIterativeRetrievalIntent(response.AdvisoryText)
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
result.AdvisoryText = text
|
||||
result.IterativeRetrieval = &intent
|
||||
return result
|
||||
}
|
||||
|
||||
func parseIterativeRetrievalIntent(text string) (cognitive.IterativeRetrievalRequest, string, bool) {
|
||||
start := strings.LastIndex(text, iterativeRetrievalMarker)
|
||||
if start < 0 {
|
||||
return cognitive.IterativeRetrievalRequest{}, text, false
|
||||
}
|
||||
payload := strings.TrimSpace(text[start+len(iterativeRetrievalMarker):])
|
||||
var intent cognitive.IterativeRetrievalRequest
|
||||
if err := json.Unmarshal([]byte(payload), &intent); err != nil || !validIterativeRetrievalIntent(intent) {
|
||||
return cognitive.IterativeRetrievalRequest{}, text, false
|
||||
}
|
||||
return intent, strings.TrimSpace(text[:start]), true
|
||||
}
|
||||
|
||||
func validIterativeRetrievalIntent(intent cognitive.IterativeRetrievalRequest) bool {
|
||||
if strings.TrimSpace(intent.Query) == "" || strings.TrimSpace(intent.Reason) == "" || len([]rune(intent.Query)) > 512 || len([]rune(intent.Reason)) > 512 {
|
||||
return false
|
||||
}
|
||||
switch intent.Lane {
|
||||
case cognitive.IterativeRetrievalCurrentFact, cognitive.IterativeRetrievalPositiveExperience, cognitive.IterativeRetrievalNegativeExperience, cognitive.IterativeRetrievalContradiction, cognitive.IterativeRetrievalHistoricalAnalogy:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
|
||||
@@ -145,11 +193,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 +444,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 +459,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 +515,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,73 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayExtractsOnlyBoundedTypedRetrievalIntent(t *testing.T) {
|
||||
request := cognitive.GatewayAdvisoryRequest{AllowIterativeRetrieval: true}
|
||||
response := cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request", FinishReason: cognitive.FinishCompleted, AdvisoryText: "<domovoy-retrieval>{\"Query\":\"sync catalog\",\"SemanticSubject\":\"catalog sync\",\"Lane\":\"NEGATIVE_EXPERIENCE\",\"Reason\":\"need known failure\"}"}
|
||||
got := gatewayAdvisoryResponse(request, response)
|
||||
if got.IterativeRetrieval == nil || got.IterativeRetrieval.Lane != cognitive.IterativeRetrievalNegativeExperience || got.AdvisoryText != "" {
|
||||
t.Fatalf("response=%+v", got)
|
||||
}
|
||||
invalid := response
|
||||
invalid.AdvisoryText = "<domovoy-retrieval>{\"Query\":\"all memory\",\"Lane\":\"UNRESTRICTED\",\"Reason\":\"everything\"}"
|
||||
if got := gatewayAdvisoryResponse(request, invalid); got.IterativeRetrieval != nil {
|
||||
t.Fatalf("invalid intent accepted: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
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 := ®istryStub{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{
|
||||
|
||||
@@ -71,7 +71,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
|
||||
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 120*time.Second)
|
||||
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
|
||||
if adapterErr != nil {
|
||||
log.Fatal(adapterErr)
|
||||
@@ -113,11 +113,16 @@ 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}
|
||||
mediumReadinessClient := &http.Client{Timeout: 10 * 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}
|
||||
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierFast)))
|
||||
@@ -131,14 +136,21 @@ func main() {
|
||||
// readiness budget short; their detailed fleet state is reported separately.
|
||||
cpuHealthProbe.Timeout = 2 * time.Second
|
||||
mediumEndpoint := ""
|
||||
mediumHealthEndpoint := ""
|
||||
mediumHeaders := map[string]string{}
|
||||
mediumHealthProbe := health.Probe{}
|
||||
if mediumURL != "" {
|
||||
mediumEndpoint = strings.TrimRight(mediumURL, "/") + "/models"
|
||||
mediumHealthEndpoint = strings.TrimSuffix(strings.TrimRight(mediumURL, "/"), "/v1") + "/health"
|
||||
if mediumAPIKey != "" {
|
||||
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
|
||||
}
|
||||
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
|
||||
// Readiness reports provider discovery health quickly; fleet readiness
|
||||
// separately reports authenticated model availability on /v1/models.
|
||||
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(readinessClient, mediumHealthEndpoint, mediumHeaders)
|
||||
// The local gateway may need several seconds to enumerate a loaded model.
|
||||
// Keep this bounded, but do not classify a healthy MEDIUM provider as
|
||||
// unavailable before its authenticated discovery response can arrive.
|
||||
mediumHealthProbe.Timeout = 2 * time.Second
|
||||
}
|
||||
readinessProbes := []health.Probe{
|
||||
@@ -157,8 +169,8 @@ func main() {
|
||||
}
|
||||
|
||||
fleetEndpoints := []fleetEndpoint{
|
||||
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}},
|
||||
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient},
|
||||
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}, Timeout: 2 * time.Second},
|
||||
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient, Timeout: mediumReadinessTimeout},
|
||||
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
|
||||
}
|
||||
if gpuConfigured {
|
||||
@@ -191,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)
|
||||
@@ -217,7 +228,9 @@ func durationEnv(name string, fallback time.Duration) time.Duration {
|
||||
|
||||
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
|
||||
for name := range service.providers {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
// Keep startup discovery bounded while allowing the authenticated local
|
||||
// gateway enough time to enumerate a loaded MEDIUM model.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
registration, ok := service.providers[name]
|
||||
if !ok || registration.Discovery == nil {
|
||||
cancel()
|
||||
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -53,8 +56,83 @@ type modelQualificationResponse struct {
|
||||
Results []modelQualificationResult `json:"results"`
|
||||
}
|
||||
|
||||
const qualificationControlPlaneTimeout = 45 * time.Second
|
||||
|
||||
// qualificationCoordinator bounds duplicate control-plane probes within one
|
||||
// cognitive-worker process. It is deliberately not canonical state: the
|
||||
// Capability owner remains the sole owner of qualification evidence and
|
||||
// activation. A restart may repeat an incomplete probe, but cannot create a
|
||||
// second semantic writer or bypass independent evaluation.
|
||||
type qualificationCoordinator struct {
|
||||
mu sync.Mutex
|
||||
flights map[string]*qualificationFlight
|
||||
}
|
||||
|
||||
type qualificationFlight struct {
|
||||
done chan struct{}
|
||||
response modelQualificationResponse
|
||||
err error
|
||||
waiters int
|
||||
}
|
||||
|
||||
func (c *qualificationCoordinator) do(ctx context.Context, key string, operation func() (modelQualificationResponse, error)) (modelQualificationResponse, bool, error) {
|
||||
c.mu.Lock()
|
||||
if c.flights == nil {
|
||||
c.flights = make(map[string]*qualificationFlight)
|
||||
}
|
||||
if current, exists := c.flights[key]; exists {
|
||||
current.waiters++
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case <-current.done:
|
||||
return current.response, false, current.err
|
||||
case <-ctx.Done():
|
||||
return modelQualificationResponse{}, false, ctx.Err()
|
||||
}
|
||||
}
|
||||
current := &qualificationFlight{done: make(chan struct{})}
|
||||
c.flights[key] = current
|
||||
c.mu.Unlock()
|
||||
|
||||
current.response, current.err = operation()
|
||||
close(current.done)
|
||||
c.mu.Lock()
|
||||
delete(c.flights, key)
|
||||
c.mu.Unlock()
|
||||
return current.response, true, current.err
|
||||
}
|
||||
|
||||
type qualificationPhaseTrace struct {
|
||||
correlationID string
|
||||
started time.Time
|
||||
}
|
||||
|
||||
func newQualificationPhaseTrace() qualificationPhaseTrace {
|
||||
correlationID, err := ids.New()
|
||||
if err != nil {
|
||||
correlationID = ids.ID(fmt.Sprintf("qualification-%d", time.Now().UnixNano()))
|
||||
}
|
||||
return qualificationPhaseTrace{correlationID: string(correlationID), started: time.Now()}
|
||||
}
|
||||
|
||||
func (t qualificationPhaseTrace) phase(name string, fields ...string) {
|
||||
log.Printf("qualification_phase correlation_id=%s phase=%s elapsed_ms=%d %s", t.correlationID, name, time.Since(t.started).Milliseconds(), strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
func qualificationFlightKey(provider string, modelIDs []capability.ProviderModelID) string {
|
||||
ids := make([]string, 0, len(modelIDs))
|
||||
for _, modelID := range modelIDs {
|
||||
ids = append(ids, string(modelID))
|
||||
}
|
||||
sort.Strings(ids)
|
||||
return provider + "|" + strings.Join(ids, ",")
|
||||
}
|
||||
|
||||
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
|
||||
coordinator := &qualificationCoordinator{}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
trace := newQualificationPhaseTrace()
|
||||
trace.phase("Q0", "event=request_accepted")
|
||||
name := r.PathValue("provider")
|
||||
registration, ok := providers[name]
|
||||
if !ok || registration.Discovery == nil || registration.Provider == nil {
|
||||
@@ -72,32 +150,65 @@ func modelQualificationHandler(providers map[string]providerRegistration, owner
|
||||
http.Error(w, "max_output_bytes exceeds qualification bound", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
modelsFound, probe := registration.Discovery.Discover(r.Context())
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
|
||||
return
|
||||
controlCtx, cancel := context.WithTimeout(r.Context(), qualificationControlPlaneTimeout)
|
||||
defer cancel()
|
||||
response, leader, err := coordinator.do(controlCtx, qualificationFlightKey(name, request.ModelIDs), func() (modelQualificationResponse, error) {
|
||||
trace.phase("Q1", "event=binding_candidate_loaded")
|
||||
modelsFound, probe := registration.Discovery.Discover(controlCtx)
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
return modelQualificationResponse{}, fmt.Errorf("provider unavailable for qualification")
|
||||
}
|
||||
if len(request.ModelIDs) > 0 {
|
||||
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
|
||||
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
|
||||
})
|
||||
}
|
||||
if len(modelsFound) == 0 {
|
||||
return modelQualificationResponse{}, fmt.Errorf("requested model not discovered")
|
||||
}
|
||||
if len(modelsFound) > 16 {
|
||||
modelsFound = modelsFound[:16]
|
||||
}
|
||||
trace.phase("Q2", fmt.Sprintf("event=provider_model_resolved model_count=%d", len(modelsFound)))
|
||||
trace.phase("Q3", "event=bounded_invocation_started")
|
||||
results := qualifyModels(controlCtx, registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
|
||||
trace.phase("Q4", "event=bounded_invocation_completed")
|
||||
trace.phase("Q5", fmt.Sprintf("event=invocation_evidence_built result_count=%d", len(results)))
|
||||
if len(observers) > 0 && observers[0] != nil {
|
||||
trace.phase("Q6", "event=observation_persistence_started")
|
||||
if err := persistQualificationResults(controlCtx, observers[0], registration, results); err != nil {
|
||||
return modelQualificationResponse{}, fmt.Errorf("persist model qualification observation: %w", err)
|
||||
}
|
||||
trace.phase("Q7", "event=observation_persistence_completed")
|
||||
}
|
||||
// Q8-Q17 are intentionally outside this worker's authority. Capability
|
||||
// owns independent evaluation and governed promotion; this trace makes
|
||||
// that hand-off explicit instead of disguising it as activation.
|
||||
trace.phase("Q8", "event=independent_evaluation_not_started owner=capability")
|
||||
return modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results}, nil
|
||||
})
|
||||
if !leader {
|
||||
trace.phase("Q0", "event=joined_in_progress_qualification")
|
||||
}
|
||||
if len(request.ModelIDs) > 0 {
|
||||
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
|
||||
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
|
||||
})
|
||||
}
|
||||
if len(modelsFound) == 0 {
|
||||
http.Error(w, "requested model not discovered", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if len(modelsFound) > 16 {
|
||||
modelsFound = modelsFound[:16]
|
||||
}
|
||||
results := qualifyModels(r.Context(), registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
|
||||
if len(observers) > 0 && observers[0] != nil {
|
||||
if err := persistQualificationResults(r.Context(), observers[0], registration, results); err != nil {
|
||||
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
|
||||
if err != nil {
|
||||
if controlCtx.Err() != nil {
|
||||
http.Error(w, "qualification timeout", http.StatusGatewayTimeout)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "requested model not discovered") {
|
||||
http.Error(w, "requested model not discovered", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "provider unavailable") {
|
||||
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
trace.phase("Q17", "event=qualification_response_returned activation=not_attempted")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
|
||||
_ = jsonEncode(w, response)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -17,6 +18,7 @@ type qualificationProvider struct{}
|
||||
func (qualificationProvider) Probe(context.Context) models.ProviderProbe {
|
||||
return models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
|
||||
func (qualificationProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
|
||||
return []models.DiscoveredModel{{ProviderModelID: "model-a"}, {ProviderModelID: "model-b"}}, models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
@@ -83,3 +85,63 @@ func TestModelQualificationPersistsReachabilityWithoutQuality(t *testing.T) {
|
||||
t.Fatalf("invalid qualification observation: %+v err=%v", observer.observations[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelQualificationDeduplicatesSameProviderModelFlight(t *testing.T) {
|
||||
coordinator := &qualificationCoordinator{}
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
firstDone := make(chan struct{})
|
||||
secondDone := make(chan struct{})
|
||||
result := modelQualificationResponse{Provider: "medium"}
|
||||
go func() {
|
||||
_, _, err := coordinator.do(context.Background(), "medium|model-a", func() (modelQualificationResponse, error) {
|
||||
close(started)
|
||||
<-release
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("leader qualification failed: %v", err)
|
||||
}
|
||||
close(firstDone)
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("qualification invocation did not start")
|
||||
}
|
||||
go func() {
|
||||
actual, leader, err := coordinator.do(context.Background(), "medium|model-a", func() (modelQualificationResponse, error) {
|
||||
t.Error("duplicate qualification operation ran")
|
||||
return modelQualificationResponse{}, nil
|
||||
})
|
||||
if err != nil || leader || actual.Provider != result.Provider {
|
||||
t.Errorf("joined qualification response=%+v leader=%t err=%v", actual, leader, err)
|
||||
}
|
||||
close(secondDone)
|
||||
}()
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for {
|
||||
coordinator.mu.Lock()
|
||||
flight := coordinator.flights["medium|model-a"]
|
||||
waiters := 0
|
||||
if flight != nil {
|
||||
waiters = flight.waiters
|
||||
}
|
||||
coordinator.mu.Unlock()
|
||||
if waiters == 1 {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("second qualification did not join the in-progress flight")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
close(release)
|
||||
for _, done := range []chan struct{}{firstDone, secondDone} {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("qualification flight did not complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (g cognitiveGateway) executeTaskEvaluationCases(ctx context.Context, reques
|
||||
go func(index int, item capability.TaskEvaluationCase) {
|
||||
defer wg.Done()
|
||||
started := time.Now()
|
||||
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, Locale: "en-US", MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
response, invokeErr := g.invokeAdmitted(ctx, string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
|
||||
latency := time.Since(started).Microseconds()
|
||||
observations[index] = capability.TaskCaseObservation{CaseID: item.CaseID, Result: capability.EvaluationUnknown, CriticalFailure: invokeErr != nil || response.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(response.AdvisoryText) == "", EvidenceArtifactRef: item.EvidenceArtifactRef, LatencyMicros: latency}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -57,6 +58,30 @@ func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type localeCheckingProvider struct{ stubAdvisoryProvider }
|
||||
|
||||
func (p *localeCheckingProvider) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
if request.Locale == "" {
|
||||
return cognitive.AdvisoryResponse{}, fmt.Errorf("task evaluation omitted required locale")
|
||||
}
|
||||
return p.stubAdvisoryProvider.Advise(ctx, request)
|
||||
}
|
||||
|
||||
func TestTaskEvaluationSuppliesLocaleToProviderAdapter(t *testing.T) {
|
||||
selected := gatewaySelection()
|
||||
provider := &localeCheckingProvider{stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "observed response"}}}
|
||||
registry := &candidateRegistryStub{selected: selected}
|
||||
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}}}, now: time.Now}
|
||||
profile := capability.TaskEvaluationProfile{ID: "profile-locale", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "locale-v1", RequiredCaseIDs: []string{"case-locale"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: selected.Binding.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-locale", Prompt: "one", EvidenceArtifactRef: "artifact:locale"}}}
|
||||
payload, _ := json.Marshal(request)
|
||||
recorder := httptest.NewRecorder()
|
||||
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
type taskEvaluationFailoverRegistry struct {
|
||||
primary, fallback capability.ResolvedCognitiveBinding
|
||||
resolveCalls int
|
||||
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
@@ -28,6 +30,154 @@ 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) {
|
||||
return compileInteractionContextWithExperience(projection, focusProjectID, nil, "")
|
||||
}
|
||||
|
||||
func compileInteractionContextWithExperience(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) (string, error) {
|
||||
if len(projection.Fragments) == 0 && len(experiences) == 0 {
|
||||
return projection.Text, nil
|
||||
}
|
||||
candidates := interactionContextCandidates(projection, focusProjectID, experiences, query)
|
||||
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
|
||||
}
|
||||
|
||||
func interactionContextCandidates(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) []cognitive.ContextCandidate {
|
||||
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments)+len(experiences))
|
||||
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: "current-fact",
|
||||
ContextScopeRef: fragment.ScopeRef,
|
||||
FreshnessState: "CURRENT",
|
||||
})
|
||||
}
|
||||
if needsHistoricalAnalogy(query) {
|
||||
for _, fragment := range projection.Historical {
|
||||
candidates = append(candidates, cognitive.ContextCandidate{
|
||||
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:historical:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
|
||||
Relevance: fragment.Relevance - 1, Lane: "historical-analogy", ContextScopeRef: fragment.ScopeRef, FreshnessState: "HISTORICAL_ONLY",
|
||||
})
|
||||
}
|
||||
}
|
||||
retrieved := epistemicsdomain.RetrieveExperience(experiences, "", focusProjectID, query, 4)
|
||||
for _, record := range retrieved.Positive {
|
||||
candidates = append(candidates, experienceContextCandidate(record, "positive-experience", false))
|
||||
}
|
||||
for _, record := range retrieved.Negative {
|
||||
candidates = append(candidates, experienceContextCandidate(record, "negative-experience", true))
|
||||
}
|
||||
for _, record := range retrieved.Contradictions {
|
||||
candidates = append(candidates, experienceContextCandidate(record, "contradiction", true))
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func needsHistoricalAnalogy(query string) bool {
|
||||
for _, marker := range []string{"истор", "раньше", "предыдущ", "старое", "аналог"} {
|
||||
if strings.Contains(strings.ToLower(query), marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func experienceContextCandidate(record epistemics.ExperienceRecord, lane string, negative bool) cognitive.ContextCandidate {
|
||||
text := strings.TrimSpace(record.ProblemStatement + ": " + record.OutcomeDetail)
|
||||
if detail := strings.TrimSpace(record.WhySucceededOrFailed); detail != "" {
|
||||
text += " (" + detail + ")"
|
||||
}
|
||||
if len(record.ApplicabilityConditions) > 0 {
|
||||
text += " [conditions: " + strings.Join(record.ApplicabilityConditions, ", ") + "]"
|
||||
}
|
||||
return cognitive.ContextCandidate{Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("experience:" + string(record.ID)), SourceKind: "epistemics-experience", SourceID: "epistemics:experience-records", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: text}, Relevance: 1, Lane: lane, ContextScopeRef: record.ContextScopeRef, FreshnessState: "CURRENT", Negative: negative}
|
||||
}
|
||||
|
||||
const (
|
||||
iterativeRetrievalMaxRounds = 1
|
||||
iterativeRetrievalMaxFragments = 2
|
||||
iterativeRetrievalMaxAddedBytes = 1024
|
||||
iterativeRetrievalMaxLatency = 1500 * time.Millisecond
|
||||
)
|
||||
|
||||
func newIterativeContextRetriever(history []interaction.HumanUtterance, references []interaction.ProjectReference, principalID, focusProjectID string, experiences []epistemics.ExperienceRecord) iterativeContextRetriever {
|
||||
seen := map[string]struct{}{}
|
||||
return func(ctx context.Context, request cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
|
||||
started := time.Now()
|
||||
if request.Pass < 0 || request.Pass >= iterativeRetrievalMaxRounds || request.RemainingBytes <= 0 || request.RemainingBytes > iterativeRetrievalMaxAddedBytes {
|
||||
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "budget"}
|
||||
}
|
||||
projection := interactiondomain.BuildScopedGen2ContextForQuery(history, references, principalID, focusProjectID, request.Query)
|
||||
candidates := interactionContextCandidates(projection, focusProjectID, experiences, request.Query)
|
||||
filtered := make([]cognitive.ContextCandidate, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
if !iterativeLaneMatches(request.Lane, candidate.Lane) {
|
||||
continue
|
||||
}
|
||||
key := string(request.Lane) + ":" + string(candidate.Excerpt.ID)
|
||||
if _, duplicate := seen[key]; duplicate {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, candidate)
|
||||
}
|
||||
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: filtered, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: request.RemainingBytes, ContextScopeRef: focusProjectID, TaskRef: "chat", FocusFrameRef: focusProjectID, FreshnessRequirement: "conversation-current"})
|
||||
if err != nil || len(bundle.Excerpts) == 0 {
|
||||
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "no_additional_evidence"}
|
||||
}
|
||||
out := make([]cognitive.ContextCandidate, 0, iterativeRetrievalMaxFragments)
|
||||
for _, excerpt := range bundle.Excerpts {
|
||||
if len(out) == iterativeRetrievalMaxFragments || time.Since(started) > iterativeRetrievalMaxLatency {
|
||||
break
|
||||
}
|
||||
seen[string(request.Lane)+":"+string(excerpt.ID)] = struct{}{}
|
||||
for _, candidate := range filtered {
|
||||
if candidate.Excerpt.ID == excerpt.ID {
|
||||
out = append(out, candidate)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "latency_budget"}
|
||||
}
|
||||
addedBytes := 0
|
||||
for _, candidate := range out {
|
||||
addedBytes += len([]byte(candidate.Excerpt.Text))
|
||||
}
|
||||
return cognitive.IterativeRetrievalResponse{Candidates: out, Exhausted: true, Reason: "completed", AddedBytes: addedBytes}
|
||||
}
|
||||
}
|
||||
|
||||
func iterativeLaneMatches(request cognitive.IterativeRetrievalLane, actual string) bool {
|
||||
return (request == cognitive.IterativeRetrievalCurrentFact && actual == "current-fact") ||
|
||||
(request == cognitive.IterativeRetrievalPositiveExperience && actual == "positive-experience") ||
|
||||
(request == cognitive.IterativeRetrievalNegativeExperience && actual == "negative-experience") ||
|
||||
(request == cognitive.IterativeRetrievalContradiction && actual == "contradiction") ||
|
||||
(request == cognitive.IterativeRetrievalHistoricalAnalogy && actual == "historical-analogy")
|
||||
}
|
||||
|
||||
const (
|
||||
// Household interpretation and response review are intentionally compact
|
||||
// calls. Keeping their output bounded prevents a loaded local model from
|
||||
// spending the whole request budget generating unnecessary prose.
|
||||
responseReviewMaxOutputBytes = 512
|
||||
stableKnowledgeMaxOutputBytes = 512
|
||||
semanticInterpretMaxOutputBytes = 1024
|
||||
)
|
||||
|
||||
// ReviewResponse asks the cognitive gateway to turn a locally assembled draft
|
||||
// into a natural answer. The draft remains the safe fallback when cognition is
|
||||
// unavailable; the model does not mutate canonical state or execute actions.
|
||||
@@ -47,7 +197,7 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
|
||||
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
|
||||
ActorID: session.ActorID, PrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"), Message: prompt,
|
||||
Context: context, Locale: "ru", MaxOutputBytes: 2048, DataPolicy: capability.DataPolicyLocalOnly,
|
||||
Context: context, Locale: "ru", MaxOutputBytes: responseReviewMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
|
||||
Meaning: cognitive.TaskMeaning{
|
||||
SpeechAct: "RESPONSE_REVIEW",
|
||||
CapabilityHints: meaning.CapabilityHints,
|
||||
@@ -86,43 +236,53 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
|
||||
// non-current question only after evidence retrieval did not produce a usable
|
||||
// result. The model decides whether its knowledge is stable enough; it must
|
||||
// explicitly decline changing or source-dependent facts instead of guessing.
|
||||
type iterativeContextRetriever func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse
|
||||
|
||||
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
|
||||
return c.AnswerStableKnowledgeWithContinuation(ctx, session, userMessage, personalMemory, meaning, nil)
|
||||
}
|
||||
|
||||
// AnswerStableKnowledgeWithContinuation permits one model-requested, Core-run
|
||||
// retrieval round. The callback receives only a typed intent and cannot alter
|
||||
// identity, scope or provider selection.
|
||||
func (c cognitiveInterpretationClient) AnswerStableKnowledgeWithContinuation(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning, retrieve iterativeContextRetriever) (string, error) {
|
||||
requestID, err := ids.New()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
|
||||
if retrieve != nil {
|
||||
prompt += "\n\nЕсли именно для ответа не хватает ограниченного контекста, вместо ответа верни единственную строку <domovoy-retrieval>{\"Query\":\"...\",\"SemanticSubject\":\"...\",\"Lane\":\"CURRENT_FACT|POSITIVE_EXPERIENCE|NEGATIVE_EXPERIENCE|CONTRADICTION|HISTORICAL_ANALOGY\",\"Reason\":\"...\"}. Не запрашивай широкий дамп памяти."
|
||||
}
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
}
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/advisories"
|
||||
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
|
||||
requestValue := cognitive.GatewayAdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
|
||||
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: 1024, DataPolicy: capability.DataPolicyLocalOnly,
|
||||
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
|
||||
})
|
||||
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: stableKnowledgeMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
|
||||
AllowIterativeRetrieval: retrieve != nil,
|
||||
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
|
||||
}
|
||||
result, err := c.requestAdvisory(ctx, requestValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
|
||||
}
|
||||
var result cognitive.GatewayAdvisoryResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
if result.IterativeRetrieval != nil && retrieve != nil {
|
||||
additional := retrieve(ctx, *result.IterativeRetrieval)
|
||||
for _, candidate := range additional.Candidates {
|
||||
context = append(context, candidate.Excerpt)
|
||||
}
|
||||
if len(additional.Candidates) == 0 {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "continuation:no-additional-evidence", SourceKind: "iterative-retrieval", SourceID: "core", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "No additional evidence was found; answer with uncertainty rather than inventing facts."})
|
||||
}
|
||||
requestValue.Context = context
|
||||
requestValue.AllowIterativeRetrieval = false
|
||||
result, err = c.requestAdvisory(ctx, requestValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
|
||||
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
|
||||
@@ -130,6 +290,33 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
|
||||
return strings.TrimSpace(result.AdvisoryText), nil
|
||||
}
|
||||
|
||||
func (c cognitiveInterpretationClient) requestAdvisory(ctx context.Context, value cognitive.GatewayAdvisoryRequest) (cognitive.GatewayAdvisoryResponse, error) {
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/advisories"
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return cognitive.GatewayAdvisoryResponse{}, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return cognitive.GatewayAdvisoryResponse{}, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return cognitive.GatewayAdvisoryResponse{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return cognitive.GatewayAdvisoryResponse{}, fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
|
||||
}
|
||||
var result cognitive.GatewayAdvisoryResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return cognitive.GatewayAdvisoryResponse{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
|
||||
base, err := url.Parse(rawURL)
|
||||
if err != nil || base.Scheme == "" || base.Host == "" {
|
||||
@@ -294,7 +481,7 @@ func buildGatewayInterpretationRequest(session identity.CreatorSession, utteranc
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
Message: utterance.Content,
|
||||
Locale: "ru",
|
||||
MaxOutputBytes: 4096,
|
||||
MaxOutputBytes: semanticInterpretMaxOutputBytes,
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
|
||||
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
@@ -70,6 +71,84 @@ 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 TestCompileInteractionContextUsesHistoricalOnlyWhenRequested(t *testing.T) {
|
||||
projection := interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{{UtteranceID: "current", Text: "код = NEW", Relevance: 2}}, Historical: []interactiondomain.ContextFragment{{UtteranceID: "old", Text: "код = OLD", Relevance: 1, Historical: true}}}
|
||||
current, err := compileInteractionContextWithExperience(projection, "", nil, "Какой текущий код?")
|
||||
if err != nil || strings.Contains(current, "OLD") {
|
||||
t.Fatalf("current context=%q err=%v", current, err)
|
||||
}
|
||||
historical, err := compileInteractionContextWithExperience(projection, "", nil, "Какой старый код был раньше?")
|
||||
if err != nil || !strings.Contains(historical, "OLD") {
|
||||
t.Fatalf("historical context=%q err=%v", historical, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStableKnowledgeContinuesOnceWithBoundedAdditionalContext(t *testing.T) {
|
||||
calls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
var request cognitive.GatewayAdvisoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls == 1 {
|
||||
if !request.AllowIterativeRetrieval {
|
||||
t.Fatal("initial advisory did not permit bounded continuation")
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, IterativeRetrieval: &cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "initial context is insufficient", RemainingBytes: 512}})
|
||||
return
|
||||
}
|
||||
if request.AllowIterativeRetrieval || len(request.Context) != 1 || request.Context[0].Text != "контрольный код = ORBIT-731" {
|
||||
t.Fatalf("continuation request=%+v", request)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, AdvisoryText: "Контрольный код — ORBIT-731."})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
answer, err := client.AnswerStableKnowledgeWithContinuation(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Какой контрольный код?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"}, func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
|
||||
return cognitive.IterativeRetrievalResponse{Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "interaction:code", Text: "контрольный код = ORBIT-731", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact"}}, Exhausted: true, AddedBytes: 28}
|
||||
})
|
||||
if err != nil || answer != "Контрольный код — ORBIT-731." || calls != 2 {
|
||||
t.Fatalf("answer=%q err=%v calls=%d", answer, err, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeRetrieverPreservesScopeLanesAndBudgets(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{{ID: "a", PrincipalID: "p", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1}, {ID: "b", PrincipalID: "p", Content: "контрольный код = BETA", ReceivedAtUnixMillis: 2}}
|
||||
references := []interaction.ProjectReference{{ID: "a-ref", UtteranceID: "a", ProjectID: "company-a"}, {ID: "b-ref", UtteranceID: "b", ProjectID: "company-b"}}
|
||||
experiences := []epistemics.ExperienceRecord{{ID: "failure", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1}, {ID: "conflict", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeConflict, OutcomeDetail: "conflicting result", RecordedAtUnixMillis: 2}}
|
||||
retrieve := newIterativeContextRetriever(history, references, "p", "company-a", experiences)
|
||||
current := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "need scoped fact", RemainingBytes: 512})
|
||||
if len(current.Candidates) != 1 || !strings.Contains(current.Candidates[0].Excerpt.Text, "ALPHA") || strings.Contains(current.Candidates[0].Excerpt.Text, "BETA") {
|
||||
t.Fatalf("scope leaked: %#v", current)
|
||||
}
|
||||
if duplicate := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "repeat", RemainingBytes: 512}); duplicate.Reason != "no_additional_evidence" {
|
||||
t.Fatalf("duplicate was re-added: %#v", duplicate)
|
||||
}
|
||||
if negative := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalNegativeExperience, Reason: "need failure", RemainingBytes: 512}); len(negative.Candidates) < 1 || !strings.Contains(strings.Join([]string{negative.Candidates[0].Excerpt.Text, negative.Candidates[len(negative.Candidates)-1].Excerpt.Text}, " "), "old connector failed") {
|
||||
t.Fatalf("negative lane=%#v", negative)
|
||||
}
|
||||
if contradiction := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalContradiction, Reason: "need conflict", RemainingBytes: 512}); len(contradiction.Candidates) != 1 || !strings.Contains(contradiction.Candidates[0].Excerpt.Text, "conflicting result") {
|
||||
t.Fatalf("contradiction lane=%#v", contradiction)
|
||||
}
|
||||
if budget := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "anything", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "bad", RemainingBytes: 2048}); budget.Reason != "budget" {
|
||||
t.Fatalf("budget=%#v", budget)
|
||||
}
|
||||
}
|
||||
|
||||
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" {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShouldInvokeHumanResponseSkipsDeterministicFastPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stableConversational bool
|
||||
identityReply string
|
||||
memoryInstructionReply string
|
||||
want bool
|
||||
}{
|
||||
{name: "stable conversational", stableConversational: true},
|
||||
{name: "identity", identityReply: "Меня зовут Домовой."},
|
||||
{name: "memory instruction", memoryInstructionReply: "Запомнил."},
|
||||
{name: "ordinary continuity", want: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := shouldInvokeHumanResponse(test.stableConversational, test.identityReply, test.memoryInstructionReply); got != test.want {
|
||||
t.Fatalf("shouldInvokeHumanResponse() = %t, want %t", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldReviewResponseSkipsStableAnswers(t *testing.T) {
|
||||
if shouldReviewResponse("Здравствуйте.", false, true, "", "", false) {
|
||||
t.Fatal("stable conversational reply must not wait for response review")
|
||||
}
|
||||
if shouldReviewResponse("Меня зовут Домовой.", false, false, "Меня зовут Домовой.", "", false) {
|
||||
t.Fatal("identity reply must not wait for response review")
|
||||
}
|
||||
if shouldReviewResponse("Запомнил.", false, false, "", "Запомнил.", false) {
|
||||
t.Fatal("memory instruction reply must not wait for response review")
|
||||
}
|
||||
if shouldReviewResponse("Вы называли тестовый цвет: синий.", false, false, "", "", true) {
|
||||
t.Fatal("deterministic memory reply must not wait for response review")
|
||||
}
|
||||
if !shouldReviewResponse("Ответ", false, false, "", "", false) {
|
||||
t.Fatal("non-conversational answer remains reviewable")
|
||||
}
|
||||
if shouldReviewResponse("Ответ", true, false, "", "", false) {
|
||||
t.Fatal("stable knowledge answer must not be reviewed twice")
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,12 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
selfcontract "git.cin.su/m/Assistant/contracts/self"
|
||||
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
||||
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
selfdomain "git.cin.su/m/Assistant/modules/self/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
"git.cin.su/m/Assistant/platform/telemetry"
|
||||
)
|
||||
|
||||
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
|
||||
@@ -28,15 +32,55 @@ type learningCapabilityStore interface {
|
||||
SaveSkillProfile(context.Context, capability.SkillProfile) error
|
||||
}
|
||||
|
||||
type learningSelfStore interface {
|
||||
SaveLearningSignal(context.Context, selfcontract.LearningSignal) error
|
||||
SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error
|
||||
SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error
|
||||
}
|
||||
|
||||
type learningRecorder struct {
|
||||
epistemics learningEpistemicsStore
|
||||
capability capabilityLearningStore
|
||||
self learningSelfStore
|
||||
}
|
||||
|
||||
type capabilityLearningStore interface {
|
||||
learningCapabilityStore
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordSelfSignal(ctx context.Context, signal selfcontract.LearningSignal) {
|
||||
if r.self == nil {
|
||||
return
|
||||
}
|
||||
if err := signal.Validate(); err != nil {
|
||||
log.Printf("learning SELF signal invalid: %v", err)
|
||||
return
|
||||
}
|
||||
if err := r.self.SaveLearningSignal(ctx, signal); err != nil {
|
||||
log.Printf("learning SELF signal unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
diagnosis := selfdomain.AttributeFailure(signal)
|
||||
need, err := selfdomain.BuildImprovementNeed(signal, diagnosis, string(signal.Kind))
|
||||
if err != nil {
|
||||
log.Printf("learning SELF improvement need invalid: %v", err)
|
||||
return
|
||||
}
|
||||
if err := r.self.SaveImprovementNeed(ctx, need); err != nil {
|
||||
log.Printf("learning SELF improvement need unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
needRef := selfcontract.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)}
|
||||
ledger := selfcontract.LearningLedgerEntry{ID: selfcontract.LearningLedgerEntryID("ledger:observed:" + signal.ID), PrincipalID: signal.PrincipalID, ImprovementNeedRef: &needRef, ObservedProblem: need.ProblemSummary, EvidenceRefs: signal.EvidenceRefs, Diagnosis: string(diagnosis.Class), Hypothesis: diagnosis.Rationale, ProposedChangeRef: signal.SubjectRef, ExpectedImprovement: need.ExpectedBenefit, Decision: "OBSERVED_REQUIRES_GOVERNED_REVIEW", RecordedAt: signal.ObservedAt}
|
||||
if err := r.self.SaveLearningLedger(ctx, ledger); err != nil {
|
||||
log.Printf("learning SELF ledger unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func selfEvidence(owner, kind, stable string) selfcontract.SemanticReference {
|
||||
return selfcontract.SemanticReference{OwnerNamespace: owner, SemanticKind: kind, StableID: ids.StableID(stable)}
|
||||
}
|
||||
|
||||
type reminderResponseLearningInput struct {
|
||||
Utterance interaction.HumanUtterance
|
||||
Intent interaction.OriginalHumanIntent
|
||||
@@ -112,6 +156,7 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
|
||||
actual, confidence := outcomeForHumanResponse(input.Result.Response)
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceContinuity,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ContextRefs: contextRefs(
|
||||
@@ -142,6 +187,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceInteraction,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
HelpMethod: epistemics.HelpMethodClarification,
|
||||
@@ -154,12 +200,45 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
|
||||
ID: "signal:correction:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalUserCorrection,
|
||||
TaskPattern: "interaction-correction", ScopeKey: "principal:" + string(input.Utterance.PrincipalID),
|
||||
SubjectRef: selfEvidence("interaction", "intent", string(input.Intent.ID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ObservedAt: input.At.UnixMilli(), Severity: 2, UserImpact: 2,
|
||||
})
|
||||
}
|
||||
|
||||
func (r learningRecorder) createDelegationCandidateSkill(ctx context.Context, input delegationLearningInput, experience epistemics.ExperienceRecord) {
|
||||
if r.capability == nil || experience.ID == "" || len(experience.VerificationEvidence) == 0 {
|
||||
return
|
||||
}
|
||||
// One verified result creates only a candidate. Repeated outcome evidence and
|
||||
// independent validation are required by Capability before activation.
|
||||
profile := capability.SkillProfile{
|
||||
ID: capability.SkillProfileID("skill:delegation:" + string(input.ImplementationID)),
|
||||
Revision: 1,
|
||||
Name: "Verified bounded delegation strategy",
|
||||
Description: "Observed verified delegation outcome; requires repeatability and independent validation.",
|
||||
ApplicableContext: []string{"work:delegation"},
|
||||
ImplementationRef: "work:agent-delegation-v1",
|
||||
ImplementationVersion: "1",
|
||||
Status: capability.SkillCandidate,
|
||||
ConfidenceMillis: 500,
|
||||
RecordedAtUnixMillis: experience.RecordedAtUnixMillis,
|
||||
}
|
||||
next, err := capabilitydomain.CandidateFromVerifiedExperience(profile, capability.SkillEvidenceRef("epistemics:experience:"+string(experience.ID)), true, experience.RecordedAtUnixMillis)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
|
||||
log.Printf("learning candidate skill unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
|
||||
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
||||
return
|
||||
}
|
||||
telemetry.RecordCompatibility("fast-path-v1", "core-runtime", "decision", "decline-or-escalate", true, true, false, "OBSERVED", string(input.Utterance.CorrelationID), string(input.Utterance.PrincipalID), input.At)
|
||||
experience, err := epistemicsdomain.FastPathDeclineExperience(
|
||||
epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
@@ -173,10 +252,16 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
|
||||
if input.Result.Response.ID != "" {
|
||||
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
|
||||
}
|
||||
experience.PrincipalID = string(input.Utterance.PrincipalID)
|
||||
saved, ok := r.saveExperienceAndReflection(ctx, experience)
|
||||
if ok {
|
||||
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
|
||||
}
|
||||
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
|
||||
ID: "signal:fast-path:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalSkillFallback,
|
||||
TaskPattern: "fast-path", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "skill", string(reminderResponseSkillID)),
|
||||
EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ActualOutcome: string(input.Reason), ObservedAt: input.At.UnixMilli(), Severity: 1,
|
||||
})
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
|
||||
@@ -194,6 +279,7 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceCapability,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
HelpMethod: epistemics.HelpMethodCapabilityExecution,
|
||||
@@ -209,6 +295,13 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
if input.Outcome == capability.UsageFailed || input.Outcome == capability.UsageUnknown {
|
||||
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
|
||||
ID: "signal:capability:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
|
||||
TaskPattern: "capability-use", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))},
|
||||
ActualOutcome: string(input.Outcome), ObservedAt: input.At.UnixMilli(), Severity: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
|
||||
@@ -230,6 +323,7 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceCapability,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
|
||||
@@ -244,7 +338,17 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
|
||||
ConfidenceMillis: confidence,
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
saved, ok := r.saveExperienceAndReflection(ctx, experience)
|
||||
if ok && input.Verified {
|
||||
r.createDelegationCandidateSkill(ctx, input, saved)
|
||||
}
|
||||
if !input.Verified {
|
||||
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
|
||||
ID: "signal:delegation:" + input.DelegationID, PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
|
||||
TaskPattern: "delegation", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", input.OutcomeRef)},
|
||||
ActualOutcome: string(actual), ObservedAt: input.At.UnixMilli(), Severity: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
|
||||
@@ -273,9 +377,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
|
||||
}
|
||||
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
|
||||
if err != nil {
|
||||
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
|
||||
log.Printf("learning skill profile unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
|
||||
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
|
||||
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
|
||||
Outcome: outcome,
|
||||
@@ -287,9 +393,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
|
||||
return
|
||||
}
|
||||
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
|
||||
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
|
||||
log.Printf("learning skill profile save unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
|
||||
}
|
||||
|
||||
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
|
||||
|
||||
@@ -2,12 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
selfcontract "git.cin.su/m/Assistant/contracts/self"
|
||||
)
|
||||
|
||||
type learningEpistemicsStub struct {
|
||||
@@ -28,17 +30,60 @@ func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, episte
|
||||
type learningCapabilityStub struct {
|
||||
loadCalls int
|
||||
saveCalls int
|
||||
saved []capability.SkillProfile
|
||||
}
|
||||
|
||||
type learningSelfStub struct {
|
||||
signals int
|
||||
needs int
|
||||
ledgers int
|
||||
}
|
||||
|
||||
type failingLearningSelfStub struct{}
|
||||
|
||||
func (failingLearningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error {
|
||||
return errors.New("self repository unavailable")
|
||||
}
|
||||
func (failingLearningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error {
|
||||
return errors.New("self repository unavailable")
|
||||
}
|
||||
func (failingLearningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error {
|
||||
return errors.New("self repository unavailable")
|
||||
}
|
||||
|
||||
func (s *learningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error {
|
||||
s.signals++
|
||||
return nil
|
||||
}
|
||||
func (s *learningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error {
|
||||
s.needs++
|
||||
return nil
|
||||
}
|
||||
func (s *learningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error {
|
||||
s.ledgers++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) {
|
||||
s.loadCalls++
|
||||
return capability.SkillProfile{}, nil
|
||||
}
|
||||
func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error {
|
||||
func (s *learningCapabilityStub) SaveSkillProfile(_ context.Context, value capability.SkillProfile) error {
|
||||
s.saveCalls++
|
||||
s.saved = append(s.saved, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestVerifiedDelegationCreatesCandidateNotActiveSkill(t *testing.T) {
|
||||
e := &learningEpistemicsStub{}
|
||||
c := &learningCapabilityStub{}
|
||||
r := learningRecorder{epistemics: e, capability: c}
|
||||
r.recordDelegationOutcome(context.Background(), delegationLearningInput{Intent: interaction.OriginalHumanIntent{ID: "i"}, Utterance: interaction.HumanUtterance{ID: "u"}, DelegationID: "d", RunID: "r", ImplementationID: "impl", OutcomeRef: "outcome", VerificationRefs: []epistemics.EvidenceRef{"verified"}, Verified: true, At: time.UnixMilli(1000)})
|
||||
if len(c.saved) != 1 || c.saved[0].Status != capability.SkillCandidate || c.saved[0].VerifiedOutcomeCount != 1 {
|
||||
t.Fatalf("candidate=%#v", c.saved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
|
||||
epistemicsStore := &learningEpistemicsStub{}
|
||||
capabilityStore := &learningCapabilityStub{}
|
||||
@@ -55,3 +100,33 @@ func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
|
||||
t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrectionWritesBoundedSelfEvidenceWithoutChangingResponsePath(t *testing.T) {
|
||||
epistemicsStore := &learningEpistemicsStub{}
|
||||
selfStore := &learningSelfStub{}
|
||||
recorder := learningRecorder{epistemics: epistemicsStore, self: selfStore}
|
||||
recorder.recordCorrection(context.Background(), correctionLearningInput{
|
||||
Intent: interaction.OriginalHumanIntent{ID: "intent-2"},
|
||||
Utterance: interaction.HumanUtterance{ID: "utterance-2", PrincipalID: "principal-2"},
|
||||
At: time.UnixMilli(2000),
|
||||
})
|
||||
if selfStore.signals != 1 || selfStore.needs != 1 || selfStore.ledgers != 1 {
|
||||
t.Fatalf("self evidence counts: signals=%d needs=%d ledgers=%d", selfStore.signals, selfStore.needs, selfStore.ledgers)
|
||||
}
|
||||
if len(epistemicsStore.experiences) != 1 {
|
||||
t.Fatalf("completed learning path was changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelfRepositoryFailureDoesNotDiscardUserFacingLearningPath(t *testing.T) {
|
||||
epistemicsStore := &learningEpistemicsStub{}
|
||||
recorder := learningRecorder{epistemics: epistemicsStore, self: failingLearningSelfStub{}}
|
||||
recorder.recordCorrection(context.Background(), correctionLearningInput{
|
||||
Intent: interaction.OriginalHumanIntent{ID: "intent-failure"},
|
||||
Utterance: interaction.HumanUtterance{ID: "utterance-failure", PrincipalID: "principal-failure"},
|
||||
At: time.UnixMilli(3000),
|
||||
})
|
||||
if len(epistemicsStore.experiences) != 1 {
|
||||
t.Fatalf("SELF repository failure must not discard the completed epistemics response path")
|
||||
}
|
||||
}
|
||||
|
||||
+279
-58
@@ -24,6 +24,7 @@ import (
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
"git.cin.su/m/Assistant/platform/telemetry"
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"io"
|
||||
@@ -37,6 +38,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
creatorPasswordPath = "/run/secrets/creator_password"
|
||||
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
|
||||
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
|
||||
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
|
||||
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
|
||||
selfDBPasswordPath = "/run/secrets/self_db_password"
|
||||
)
|
||||
|
||||
const maxCreatorCredentialsBytes = 4 << 10
|
||||
|
||||
func main() {
|
||||
@@ -44,20 +54,21 @@ func main() {
|
||||
if addr == "" {
|
||||
addr = ":8080"
|
||||
}
|
||||
bootstrapToken, err := readRequiredSecret("CREATOR_BOOTSTRAP_TOKEN")
|
||||
bootstrapToken, err := readRequiredSecretFile("CREATOR_BOOTSTRAP_TOKEN", creatorBootstrapTokenPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sessionKeyValue, err := readRequiredSecret("CREATOR_SESSION_SIGNING_KEY")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
creatorPassword, err := readRequiredSecret("CREATOR_PASSWORD")
|
||||
sessionKeyValue, err := readRequiredSecretFile("CREATOR_SESSION_SIGNING_KEY", creatorSessionKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sessionKey := []byte(sessionKeyValue)
|
||||
creatorUsername := os.Getenv("CREATOR_USERNAME")
|
||||
if creatorUsername == "" {
|
||||
log.Fatal("CREATOR_USERNAME is required")
|
||||
}
|
||||
telemetry.RecordCompatibility("non-bootstrap-env-consumers", "core-runtime", "read", "runtime-startup-configuration", true, true, true, "OBSERVED", "startup", "core-runtime", time.Now())
|
||||
telemetry.RecordCompatibility("migration-compatibility-switches", "core-runtime", "observe", "runtime-startup-switch-scan", true, false, false, "NOT_OBSERVED", "startup", "core-runtime", time.Now())
|
||||
repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -73,7 +84,7 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer capabilityRepository.Close()
|
||||
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF", epistemicsDBPasswordPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -91,7 +102,10 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer identityRepository.Close()
|
||||
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF")
|
||||
if err := bootstrapCreatorAccount(context.Background(), identityRepository, creatorUsername, creatorPasswordPath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF", interactionDBPasswordPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -104,6 +118,25 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer interactionRepository.Close()
|
||||
var selfRepository *postgres.SelfRepository
|
||||
selfUser := os.Getenv("SELF_POSTGRES_USER")
|
||||
selfPassword := ""
|
||||
if selfUser != "" && os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") != "" {
|
||||
selfPassword, err = readCredentialRefSecret("SELF_POSTGRES_PASSWORD", "SELF_POSTGRES_CREDENTIAL_REF", selfDBPasswordPath)
|
||||
if err != nil {
|
||||
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
|
||||
}
|
||||
}
|
||||
if selfUser != "" && selfPassword != "" {
|
||||
selfRepository, err = postgres.OpenSelf(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: selfUser, Password: selfPassword})
|
||||
if err != nil {
|
||||
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
|
||||
} else {
|
||||
defer selfRepository.Close()
|
||||
}
|
||||
} else if selfUser == "" || os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") == "" {
|
||||
log.Printf("SELF learning repository is not configured; user responses remain unaffected")
|
||||
}
|
||||
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -124,7 +157,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
|
||||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository, self: selfRepository}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
@@ -144,10 +177,16 @@ func main() {
|
||||
}
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{
|
||||
cognitiveReadinessClient := &http.Client{Timeout: 135 * time.Second}
|
||||
cognitiveReadinessProbe := health.HTTPProbe(cognitiveReadinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/readyz")
|
||||
cognitiveReadinessProbe.Timeout = 135 * time.Second
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Timeout: 140 * time.Second, Probes: []health.Probe{
|
||||
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
|
||||
// A running worker is not necessarily ready to serve cognitive traffic.
|
||||
// Keep deterministic paths alive, but do not advertise overall readiness
|
||||
// while the cognitive dependency is still warming.
|
||||
cognitiveReadinessProbe,
|
||||
}})
|
||||
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) {
|
||||
@@ -177,7 +216,39 @@ func main() {
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
|
||||
mux.HandleFunc("POST /v1/creator/sessions", creatorDatabaseSessionHandler(bootstrapToken, creatorUsername, identityRepository, sessionKey, time.Now))
|
||||
mux.HandleFunc("POST /v1/creator/account/password", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || request.CurrentPassword == "" || request.NewPassword == "" {
|
||||
http.Error(w, "invalid password change request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
account, err := identityRepository.LoadCreatorAccountByBinding(r.Context(), session.ActorID, session.PrincipalID)
|
||||
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, request.CurrentPassword) {
|
||||
http.Error(w, "current password denied", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
newHash, err := identitydomain.HashCreatorPassword(request.NewPassword)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid new password", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := identityRepository.UpdateCreatorPassword(r.Context(), session.ActorID, session.PrincipalID, account.Revision, newHash); err != nil {
|
||||
http.Error(w, "password change conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status": "changed"})
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -505,9 +576,13 @@ func main() {
|
||||
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
|
||||
if err == nil {
|
||||
previousFocus = &storedFocus
|
||||
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
} else {
|
||||
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "EMPTY", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
}
|
||||
var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
|
||||
creativeUserRequest := interactiondomain.IsCreativeRequest(utterance.Content)
|
||||
@@ -516,9 +591,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)
|
||||
}
|
||||
@@ -526,15 +602,38 @@ 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.BuildGen2Context(conversationHistory)
|
||||
if !creativeUserRequest && locationReply == "" {
|
||||
contextProjectReferences = append(contextProjectReferences, recentProjectReferences...)
|
||||
focusProjectID := ""
|
||||
if previousFocus != nil {
|
||||
focusProjectID = string(previousFocus.ProjectID)
|
||||
}
|
||||
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
|
||||
experienceRecords, experienceErr := epistemicsRepository.RecentExperienceRecordsForPrincipalAndScope(r.Context(), string(session.PrincipalID), focusProjectID, 16)
|
||||
if experienceErr != nil {
|
||||
log.Printf("epistemics experience context unavailable: %v", experienceErr)
|
||||
}
|
||||
if compiledContext, contextErr := compileInteractionContextWithExperience(gen2Context, focusProjectID, experienceRecords, effectiveTaskMessage); contextErr != nil {
|
||||
log.Printf("interaction context compilation unavailable: %v", contextErr)
|
||||
} else {
|
||||
gen2Context.Text = compiledContext
|
||||
}
|
||||
iterativeRetriever := newIterativeContextRetriever(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, experienceRecords)
|
||||
if gen2Context.UsedPersonalMemoryFallback {
|
||||
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
}
|
||||
retrievedMemoryReply := interactiondomain.RecallPersonalPreference(conversationHistory, effectiveTaskMessage)
|
||||
stableConversationalReply, stableConversational := interactiondomain.StableConversationalReply(
|
||||
effectiveTaskMessage,
|
||||
interactiondomain.ClassifySpeechAct(effectiveTaskMessage, utterance.CorrectsUtteranceID),
|
||||
)
|
||||
if !creativeUserRequest && locationReply == "" && !stableConversational && retrievedMemoryReply == "" {
|
||||
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
|
||||
continuationMessage = continuation
|
||||
effectiveTaskMessage = continuation
|
||||
@@ -661,6 +760,14 @@ func main() {
|
||||
semanticInterpretationRequested = false
|
||||
semanticReflexAudit = false
|
||||
}
|
||||
if stableConversational {
|
||||
semanticInterpretationRequested = false
|
||||
semanticReflexAudit = false
|
||||
}
|
||||
if retrievedMemoryReply != "" {
|
||||
semanticInterpretationRequested = false
|
||||
semanticReflexAudit = false
|
||||
}
|
||||
if identityReply != "" || memoryInstructionReply != "" {
|
||||
semanticReflexAudit = false
|
||||
}
|
||||
@@ -684,7 +791,7 @@ func main() {
|
||||
cognitiveAvailable = false
|
||||
}
|
||||
}
|
||||
if !creativeRequest {
|
||||
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
|
||||
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
|
||||
semantic.Interpretation.Clarification = interaction.ClarificationRequired
|
||||
semantic.Interpretation.Applicability = interaction.SemanticCandidateClarify
|
||||
@@ -702,12 +809,16 @@ func main() {
|
||||
http.Error(w, "save chat semantics", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
responseResult, responseErr := responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
|
||||
SourceUtteranceID: string(utterance.ID),
|
||||
SourceIntentID: string(intent.ID),
|
||||
Message: effectiveTaskMessage,
|
||||
RequestedAtUnixMillis: now.UnixMilli(),
|
||||
})
|
||||
var responseResult continuity.HumanResponseResult
|
||||
var responseErr error
|
||||
if shouldInvokeHumanResponse(stableConversational, identityReply, memoryInstructionReply) {
|
||||
responseResult, responseErr = responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
|
||||
SourceUtteranceID: string(utterance.ID),
|
||||
SourceIntentID: string(intent.ID),
|
||||
Message: effectiveTaskMessage,
|
||||
RequestedAtUnixMillis: now.UnixMilli(),
|
||||
})
|
||||
}
|
||||
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
|
||||
learning.recordCorrection(r.Context(), correctionLearningInput{Utterance: utterance, Intent: intent, At: now})
|
||||
}
|
||||
@@ -715,9 +826,11 @@ func main() {
|
||||
activeFocusProvenance := situation.ProjectResolutionProvenance("")
|
||||
if semantic.ActiveFocus != nil {
|
||||
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
|
||||
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
http.Error(w, "save project focus", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
|
||||
activeFocusProvenance = semantic.ActiveFocus.Provenance
|
||||
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus && previousFocus != nil {
|
||||
@@ -733,7 +846,7 @@ func main() {
|
||||
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus {
|
||||
projectID = string(previousFocus.ProjectID)
|
||||
}
|
||||
if responseErr == nil && responseResult.Handled {
|
||||
if responseErr == nil && responseResult.Handled && !stableConversational {
|
||||
planningReply = responseResult.Reply
|
||||
needsCognition = false
|
||||
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
|
||||
@@ -763,6 +876,12 @@ func main() {
|
||||
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
|
||||
}
|
||||
helpPath := workdomain.DetermineHelpPath(planningInput)
|
||||
// Introspection requests are read-only household UX. A cognitive
|
||||
// candidate must not reinterpret them as an executable delegation.
|
||||
switch semantic.InformationRequestKind {
|
||||
case interaction.InformationRequestCapabilityInventory, interaction.InformationRequestLocalCapabilityInventory, interaction.InformationRequestCognitiveModelInventory, interaction.InformationRequestCognitiveCurrentModel, interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery, interaction.InformationRequestMemoryInventory:
|
||||
helpPath = work.HelpPath{}
|
||||
}
|
||||
if responseErr == nil && responseResult.Handled {
|
||||
helpPath = work.HelpPath{}
|
||||
}
|
||||
@@ -800,7 +919,11 @@ func main() {
|
||||
researchBacked := false
|
||||
researchAttempted := false
|
||||
stableKnowledgeReply := false
|
||||
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
|
||||
if retrievedMemoryReply != "" {
|
||||
planningReply = retrievedMemoryReply
|
||||
needsCognition = false
|
||||
}
|
||||
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)
|
||||
@@ -829,9 +952,9 @@ func main() {
|
||||
reply, backed := renderResearchReply(searchQuery, result, researchErr)
|
||||
if !backed && cognitiveAvailable {
|
||||
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
|
||||
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
})
|
||||
}, iterativeRetriever)
|
||||
cancelAnswer()
|
||||
if answerErr == nil && stableKnowledgeAnswer(answer) {
|
||||
reply, backed = answer, true
|
||||
@@ -860,18 +983,18 @@ func main() {
|
||||
planningReply = helpPath.Reply
|
||||
needsCognition = false
|
||||
}
|
||||
if !creativeRequest {
|
||||
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
|
||||
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
|
||||
planningReply = travelReply
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
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{
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(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,
|
||||
})
|
||||
}, iterativeRetriever)
|
||||
cancelAnswer()
|
||||
if answerErr == nil && stableKnowledgeAnswer(answer) {
|
||||
planningReply = answer
|
||||
@@ -990,7 +1113,11 @@ func main() {
|
||||
planningReply = memoryInstructionReply
|
||||
needsCognition = false
|
||||
}
|
||||
if strings.TrimSpace(planningReply) != "" {
|
||||
// Stable-knowledge answers have already passed the local cognitive
|
||||
// route with compact, source-aware context. Re-reviewing the same answer
|
||||
// serially can exhaust the household request budget on a loaded local
|
||||
// provider and turn a valid answer into a degraded response.
|
||||
if shouldReviewResponse(planningReply, stableKnowledgeReply, stableConversational, identityReply, memoryInstructionReply, retrievedMemoryReply != "") {
|
||||
reviewContext, cancelReview := context.WithTimeout(r.Context(), 12*time.Second)
|
||||
reviewedReply, reviewErr := cognitiveClient.ReviewResponse(reviewContext, session, utterance.Content, planningReply, gen2Context.Text, cognitive.TaskMeaning{
|
||||
SpeechAct: string(semantic.Interpretation.SpeechAct),
|
||||
@@ -1016,13 +1143,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
if !mediaAttempted && !mediaHandled && identityReply == "" && creativeReply == "" && creativeFallbackReply == "" {
|
||||
stableAct := interactiondomain.ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID)
|
||||
travelClarification := ""
|
||||
if !creativeRequest {
|
||||
travelClarification = interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory)
|
||||
}
|
||||
if stableReply, stable := interactiondomain.StableConversationalReply(utterance.Content, stableAct); stable && travelClarification == "" {
|
||||
planningReply = stableReply
|
||||
if stableConversational {
|
||||
planningReply = stableConversationalReply
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
@@ -1397,6 +1519,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
|
||||
}
|
||||
@@ -1486,7 +1612,7 @@ func main() {
|
||||
}
|
||||
request.RequestedByPrincipalID = session.PrincipalID
|
||||
request.CapabilityID = "creator-advisory"
|
||||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
|
||||
candidates, err := capabilityRepository.LoadCognitiveCandidatesForEvaluation(r.Context(), request.CapabilityID, session.PrincipalID, request.EvaluationID)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
@@ -1525,18 +1651,11 @@ func main() {
|
||||
}
|
||||
request.RequestedByPrincipalID = session.PrincipalID
|
||||
request.CapabilityID = "creator-advisory"
|
||||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
|
||||
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), request.BindingID, request.ExpectedBindingRevision)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
var current capability.ResolvedCognitiveBinding
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
|
||||
current = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
|
||||
@@ -1670,6 +1789,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func shouldInvokeHumanResponse(stableConversational bool, identityReply, memoryInstructionReply string) bool {
|
||||
return !stableConversational && identityReply == "" && memoryInstructionReply == ""
|
||||
}
|
||||
|
||||
func shouldReviewResponse(planningReply string, stableKnowledgeReply, stableConversational bool, identityReply, memoryInstructionReply string, deterministicMemoryReply bool) bool {
|
||||
return strings.TrimSpace(planningReply) != "" && !stableKnowledgeReply && !stableConversational && identityReply == "" && memoryInstructionReply == "" && !deterministicMemoryReply
|
||||
}
|
||||
|
||||
func shouldRunGeneralResearch(kind interaction.InformationRequestKind) bool {
|
||||
return kind == "" || kind == interaction.InformationRequestTimeDifference
|
||||
}
|
||||
@@ -1704,6 +1831,88 @@ func creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword stri
|
||||
}
|
||||
}
|
||||
|
||||
func creatorDatabaseSessionHandler(bootstrapToken, creatorUsername string, repository *postgres.IdentityRepository, sessionKey []byte, now func() time.Time) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) {
|
||||
token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour)
|
||||
if err != nil {
|
||||
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
writeCreatorSession(w, token, session)
|
||||
return
|
||||
}
|
||||
var credentials identity.CreatorCredentials
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
|
||||
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
account, err := repository.LoadCreatorAccount(r.Context(), credentials.Username)
|
||||
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, credentials.Password) {
|
||||
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
binding, err := repository.LoadBinding(r.Context(), account.ActorID, account.PrincipalID)
|
||||
if err != nil || !binding.Active {
|
||||
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
token, session, err := identitydomain.IssueCreatorSessionForBinding(sessionKey, account.ActorID, account.PrincipalID, now(), 8*time.Hour)
|
||||
if err != nil {
|
||||
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
writeCreatorSession(w, token, session)
|
||||
}
|
||||
}
|
||||
|
||||
func writeCreatorSession(w http.ResponseWriter, token string, session identity.CreatorSession) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(struct {
|
||||
Token string
|
||||
Session identity.CreatorSession
|
||||
}{token, session})
|
||||
}
|
||||
|
||||
func bootstrapCreatorAccount(ctx context.Context, repository *postgres.IdentityRepository, username, passwordPath string) error {
|
||||
if account, err := repository.LoadCreatorAccount(ctx, username); err == nil {
|
||||
if account.ActorID == "" || account.PrincipalID == "" {
|
||||
return errors.New("creator account binding is incomplete")
|
||||
}
|
||||
return nil
|
||||
} else if !errors.Is(unwrapIdentityRepositoryError(err), pgx.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
password, err := readRequiredSecretFile("CREATOR_PASSWORD", passwordPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creator account is missing and bootstrap password is unavailable: %w", err)
|
||||
}
|
||||
hash, err := identitydomain.HashCreatorPassword(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash creator bootstrap password: %w", err)
|
||||
}
|
||||
if err := repository.CreateCreatorAccount(ctx, identity.CreatorAccount{Username: username, ActorID: "domovoy-creator-actor", PrincipalID: "domovoy-creator-principal", PasswordHash: hash}); err != nil {
|
||||
return fmt.Errorf("create creator account: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unwrapIdentityRepositoryError(err error) error {
|
||||
for err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
unwrapped := errors.Unwrap(err)
|
||||
if unwrapped == nil {
|
||||
break
|
||||
}
|
||||
err = unwrapped
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func bearerToken(r *http.Request) string {
|
||||
value := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(value, "Bearer ") {
|
||||
@@ -1726,10 +1935,7 @@ func readRequiredSecret(name string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s_FILE: %w", name, err)
|
||||
}
|
||||
direct = strings.TrimRight(string(contents), "\r\n")
|
||||
if strings.ContainsAny(direct, "\r\n") {
|
||||
return "", fmt.Errorf("%s_FILE must contain one line", name)
|
||||
}
|
||||
return normalizeSecretFile(name, contents)
|
||||
}
|
||||
if direct == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
@@ -1737,13 +1943,28 @@ func readRequiredSecret(name string) (string, error) {
|
||||
return direct, nil
|
||||
}
|
||||
|
||||
func readCredentialRefSecret(name, refName string) (string, error) {
|
||||
if _, directSet := os.LookupEnv(name); directSet {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s_FILE", name, name)
|
||||
func readRequiredSecretFile(name, path string) (string, error) {
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s secret file: %w", name, err)
|
||||
}
|
||||
fileName := os.Getenv(name + "_FILE")
|
||||
if fileName == "" {
|
||||
return "", fmt.Errorf("%s_FILE is required", name)
|
||||
return normalizeSecretFile(name, contents)
|
||||
}
|
||||
|
||||
func normalizeSecretFile(name string, contents []byte) (string, error) {
|
||||
value := strings.TrimRight(string(contents), "\r\n")
|
||||
if strings.ContainsAny(value, "\r\n") {
|
||||
return "", fmt.Errorf("%s secret file must contain one line", name)
|
||||
}
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func readCredentialRefSecret(name, refName, fileName string) (string, error) {
|
||||
if _, directSet := os.LookupEnv(name); directSet {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name)
|
||||
}
|
||||
ref := os.Getenv(refName)
|
||||
if ref == "" {
|
||||
|
||||
@@ -841,6 +841,8 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
|
||||
return targetCore, "/v1/creator/sessions", true
|
||||
case method == http.MethodGet && path == "/api/session/current":
|
||||
return targetCore, "/v1/creator/sessions/current", true
|
||||
case method == http.MethodPost && path == "/api/account/password":
|
||||
return targetCore, "/v1/creator/account/password", true
|
||||
case method == http.MethodGet && path == "/api/focus":
|
||||
return targetCore, "/v1/creator/focus", true
|
||||
case method == http.MethodGet && strings.HasPrefix(path, "/api/tasks/"):
|
||||
|
||||
@@ -1,131 +1,285 @@
|
||||
:root {
|
||||
--ink: #e9eee7;
|
||||
--muted: #93a096;
|
||||
--base: #0b100e;
|
||||
--panel: #101714;
|
||||
--line: #2a352f;
|
||||
--signal: #c9a85f;
|
||||
--bad: #de816f;
|
||||
--good: #78c6a3;
|
||||
--radius: 3px;
|
||||
--sans: "Bahnschrift", "Aptos Narrow", "Segoe UI", sans-serif;
|
||||
--serif: Georgia, "Times New Roman", serif;
|
||||
--mono: "Cascadia Mono", Consolas, monospace;
|
||||
--ink: #e6edf4;
|
||||
--muted: #8494a4;
|
||||
--faint: #5d6e7f;
|
||||
--base: #0b1119;
|
||||
--panel: #111b27;
|
||||
--panel-raised: #172331;
|
||||
--panel-soft: #0f1925;
|
||||
--line: #263544;
|
||||
--line-strong: #34485c;
|
||||
--lime: #c5ef91;
|
||||
--lime-ink: #142014;
|
||||
--blue: #8fc7ef;
|
||||
--yellow: #d8b86c;
|
||||
--good: #8ed7ad;
|
||||
--bad: #ee9292;
|
||||
--radius: 8px;
|
||||
--sans: "Manrope", "Segoe UI", sans-serif;
|
||||
--mono: "DM Mono", "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; color: var(--ink); background: var(--base); font-family: var(--sans); }
|
||||
button, input, textarea { font: inherit; }
|
||||
button { cursor: pointer; color: inherit; border-radius: var(--radius); }
|
||||
a { color: inherit; }
|
||||
.chat-shell { min-height: 100vh; display: grid; }
|
||||
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 46px 40px; border: 1px solid var(--line); background: var(--panel); }
|
||||
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; color: var(--base); background: var(--signal); font: 700 24px/1 var(--serif); clip-path: polygon(50% 0, 92% 22%, 92% 76%, 50% 100%, 8% 76%, 8% 22%); }
|
||||
h1 { margin: 30px 0; font: 400 58px/.95 var(--serif); letter-spacing: -.02em; }
|
||||
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; }
|
||||
input { height: 48px; padding: 0 12px; }
|
||||
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; line-height: 1.45; }
|
||||
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
|
||||
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
|
||||
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 40px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); box-shadow: 0 24px 80px rgba(0,0,0,.28); }
|
||||
.brand-mark { width: 42px; height: 42px; display: grid; place-items: center; color: var(--lime-ink); background: var(--lime); border-radius: 10px; font: 700 22px/1 var(--sans); }
|
||||
h1, h2 { margin: 0; letter-spacing: -.03em; }
|
||||
.login-panel h1 { margin: 26px 0; font-size: 42px; font-weight: 650; }
|
||||
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: var(--panel-soft); outline: none; }
|
||||
input:focus, textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(143,199,239,.12); }
|
||||
input { height: 46px; padding: 0 12px; }
|
||||
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 13px; line-height: 1.45; }
|
||||
.login-form button, .composer button { border: 0; color: var(--lime-ink); background: var(--lime); font-weight: 750; }
|
||||
.login-form button { width: 100%; height: 46px; margin-top: 18px; }
|
||||
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
|
||||
.chat-view { width: 100%; min-width: 0; height: 100vh; min-height: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); overflow: hidden; }
|
||||
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .98); }
|
||||
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
|
||||
#active-focus { color: var(--good); }
|
||||
.chat-view { width: 100%; min-width: 0; height: 100vh; min-height: 0; display: grid; grid-template-rows: 60px minmax(0, 1fr); overflow: hidden; }
|
||||
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 20px; border-bottom: 1px solid var(--line); background: rgba(17,27,39,.97); }
|
||||
.chat-header > a:first-child { color: var(--ink); text-decoration: none; font-size: 18px; font-weight: 750; letter-spacing: -.02em; }
|
||||
.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
|
||||
.chat-header .cognitive-status { width: auto; height: auto; padding: 0; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
.chat-header .cognitive-status { max-width: min(30vw, 320px); margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
|
||||
.chat-header .cognitive-status { width: auto; height: auto; max-width: min(30vw, 340px); margin-left: auto; padding: 0; overflow: hidden; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .04em; text-overflow: ellipsis; text-transform: uppercase; }
|
||||
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--yellow); }
|
||||
.cognitive-status[data-state="available"] { color: var(--good); }
|
||||
.cognitive-status[data-state="available"]::before { background: var(--good); }
|
||||
.cognitive-status[data-state="unavailable"] { color: var(--bad); }
|
||||
.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
|
||||
.cognitive-console-link { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
|
||||
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .08em; text-align: right; text-transform: uppercase; white-space: nowrap; }
|
||||
.build-meta time { color: #68776d; font-size: 8px; }
|
||||
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
|
||||
.human-nav { display: grid; gap: 8px; }
|
||||
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.chat-left .nav-tab { height: auto; min-height: 58px; padding: 10px 12px; display: grid; grid-template-columns: 28px 1fr; gap: 2px 8px; align-items: center; text-align: left; }
|
||||
.chat-left .nav-tab b { grid-row: span 2; color: var(--signal); font: 11px/1 var(--mono); }
|
||||
.chat-left .nav-tab span { color: var(--ink); font: 600 13px/1 var(--sans); text-transform: none; letter-spacing: 0; }
|
||||
.chat-left .nav-tab small { color: var(--muted); font: 10px/1 var(--mono); text-transform: none; letter-spacing: 0; }
|
||||
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
|
||||
.operator-link { color: var(--good); }
|
||||
.chat-header > .operator-link { flex: 0 0 auto; }
|
||||
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
|
||||
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
|
||||
.generated-image { display: block; max-width: min(100%, 640px); height: auto; margin-top: 12px; border: 1px solid var(--line); }
|
||||
.cognitive-console-link { color: var(--blue); font: 10px/1 var(--mono); letter-spacing: .04em; white-space: nowrap; }
|
||||
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .04em; text-align: right; text-transform: uppercase; white-space: nowrap; }
|
||||
.build-meta time { color: var(--faint); font-size: 8px; }
|
||||
.chat-header button { width: 34px; height: 34px; border: 1px solid var(--line); background: transparent; }
|
||||
.operator-link { height: 32px; padding: 0 11px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--blue); background: transparent; text-decoration: none; font: 10px/1 var(--mono); letter-spacing: .04em; text-transform: uppercase; }
|
||||
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 258px) minmax(0, 1fr) minmax(270px, 320px); }
|
||||
.chat-side { min-width: 0; min-height: 0; padding: 22px 18px; overflow-y: auto; background: var(--panel); scrollbar-color: var(--line-strong) transparent; }
|
||||
.chat-left { border-right: 1px solid var(--line); }
|
||||
.chat-right { border-left: 1px solid var(--line); }
|
||||
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
|
||||
.side-intro p:not(.side-index) { color: var(--muted); line-height: 1.5; }
|
||||
.side-index, .side-focus span, .side-note span, .ai-info-card span { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
.side-focus, .side-note, .ai-info-card { margin-top: 28px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
.side-focus strong, .side-note strong, .ai-info-card strong { display: block; margin-top: 9px; color: var(--ink); font: 600 14px/1.3 var(--sans); }
|
||||
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
|
||||
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
|
||||
.side-intro h1, .side-intro h2 { margin: 9px 0 10px; font-size: clamp(25px, 2.4vw, 34px); line-height: 1.05; font-weight: 680; }
|
||||
.side-intro p:not(.side-index) { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.side-index, .section-label, .ai-info-card span, .insight-card span { color: var(--faint); font: 600 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
.side-section, .side-note, .ai-info-card { margin-top: 20px; padding: 13px 0; border-top: 1px solid var(--line); }
|
||||
.side-focus strong, .side-note strong { display: block; margin-top: 8px; color: var(--blue); font-size: 13px; font-weight: 650; line-height: 1.35; }
|
||||
.side-note { border-bottom: 0; }
|
||||
.side-note strong { color: var(--good); }
|
||||
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 8px; color: var(--muted); font-size: 11px; line-height: 1.45; }
|
||||
.context-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||
.context-list span { padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); font-size: 11px; }
|
||||
.quick-context { padding-bottom: 3px; }
|
||||
.human-nav { display: grid; gap: 6px; margin-top: 8px; }
|
||||
.nav-tab { width: 100%; min-height: 50px; padding: 8px 10px; display: grid; grid-template-columns: 26px 1fr; gap: 2px 7px; align-items: center; border: 1px solid transparent; color: var(--muted); background: transparent; text-align: left; }
|
||||
.chat-left .nav-tab b { grid-row: span 2; color: var(--faint); font: 10px/1 var(--mono); }
|
||||
.chat-left .nav-tab span { color: var(--ink); font-size: 12px; font-weight: 650; }
|
||||
.chat-left .nav-tab small { color: var(--faint); font: 10px/1 var(--mono); }
|
||||
.nav-tab:hover, .nav-tab:focus-visible { border-color: var(--line-strong); outline: none; }
|
||||
.nav-tab.active { border-color: rgba(197,239,145,.42); background: rgba(197,239,145,.1); }
|
||||
.nav-tab.active b { color: var(--lime); }
|
||||
.quiet-state { margin: 9px 0 0; color: var(--faint); font-size: 11px; }
|
||||
.new-conversation { width: 100%; min-height: 34px; margin-top: 9px; border: 1px solid var(--line-strong); color: var(--ink); background: transparent; font-size: 11px; }
|
||||
.new-conversation:hover, .new-conversation:focus-visible { border-color: var(--lime); color: var(--lime); outline: none; }
|
||||
.side-build { margin-top: 14px; padding-top: 14px; text-align: left; }
|
||||
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; background: var(--base); }
|
||||
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
|
||||
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
|
||||
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
|
||||
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
||||
.message.user { align-self: flex-end; background: #151e1a; }
|
||||
.message.assistant { align-self: flex-start; background: #0f1613; }
|
||||
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
|
||||
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer button { height: 48px; align-self: end; }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.3); }
|
||||
.media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; }
|
||||
.media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; }
|
||||
.media-tools input { max-width: 150px; font: 10px var(--mono); }
|
||||
.media-tools span { color: var(--signal); }
|
||||
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
|
||||
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
|
||||
.ai-data-form { margin-top: 24px; }
|
||||
.ai-data-form textarea { min-height: 150px; resize: vertical; }
|
||||
.ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
|
||||
.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; }
|
||||
.side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
|
||||
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
|
||||
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
|
||||
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
|
||||
.surface-heading button { height: 36px; padding: 0 12px; border: 1px solid var(--line); background: transparent; }
|
||||
.attention-list, .history-list { display: grid; gap: 12px; }
|
||||
.attention-card, .history-item { border: 1px solid var(--line); background: #0f1613; padding: 16px; }
|
||||
.attention-card time { color: var(--signal); font: 12px/1 var(--mono); }
|
||||
.attention-card h2 { margin: 10px 0 8px; font: 400 24px/1.1 var(--serif); }
|
||||
.attention-card p, .history-item p { margin: 0 0 10px; color: var(--ink); line-height: 1.45; }
|
||||
.attention-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
|
||||
.attention-actions button { height: 34px; padding: 0 12px; border: 1px solid var(--line); background: #151e1a; color: var(--ink); font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.attention-actions button:first-child { border-color: var(--good); color: var(--good); }
|
||||
.history-item.user { border-color: #34443c; background: #151e1a; }
|
||||
.history-item strong { display: block; margin-bottom: 8px; color: var(--signal); }
|
||||
.empty-state { margin: 24px 0 0; color: var(--muted); }
|
||||
.messages { width: min(100%, 900px); min-height: 0; margin: 0 auto; padding: clamp(24px, 5vh, 52px) 22px 20px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; scrollbar-color: var(--line-strong) transparent; }
|
||||
.message { max-width: min(700px, 92%); padding: 11px 14px 12px; border: 1px solid var(--line); border-radius: 10px; line-height: 1.5; }
|
||||
.message.user { align-self: flex-end; background: #162231; border-color: #30465b; }
|
||||
.message.assistant { align-self: flex-start; background: #121d2a; }
|
||||
.message-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
|
||||
.message-role { color: var(--blue); font: 650 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.message.user .message-role { color: var(--lime); }
|
||||
.message-time { color: var(--faint); font: 9px/1 var(--mono); }
|
||||
.message-content { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; }
|
||||
.message small { display: block; margin-top: 8px; color: var(--faint); font: 9px/1.2 var(--mono); letter-spacing: .03em; text-transform: none; }
|
||||
.composer { width: min(100%, 900px); margin: 0 auto; padding: 10px 22px 7px; display: grid; grid-template-columns: minmax(0, 1fr) 106px; gap: 9px; border-top: 1px solid var(--line); background: var(--base); }
|
||||
.composer-input-wrap { min-width: 0; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--panel-soft); transition: border-color .15s ease, box-shadow .15s ease; }
|
||||
.composer-input-wrap:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(143,199,239,.1); }
|
||||
.composer textarea { min-height: 46px; max-height: 200px; resize: none; border: 0; background: transparent; }
|
||||
.composer-hint { display: flex; justify-content: space-between; gap: 12px; padding: 0 12px 7px; color: var(--faint); font: 9px/1 var(--mono); }
|
||||
.composer-hint output { color: var(--muted); white-space: nowrap; }
|
||||
.composer button { height: 46px; align-self: end; display: inline-flex; align-items: center; justify-content: space-between; gap: 7px; padding: 0 13px; }
|
||||
.composer button[aria-busy="true"] { color: #384529; background: #a6ca7a; }
|
||||
.composer button span:last-child { opacity: .65; font: 15px/1 var(--mono); }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.35); }
|
||||
.media-tools { width: min(100%, 900px); margin: 0 auto; padding: 0 22px 9px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .03em; text-transform: uppercase; }
|
||||
.media-tools label { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; margin: 0; padding: 0 7px; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer; }
|
||||
.media-tools input { max-width: 125px; font: 9px var(--mono); }
|
||||
.media-tools span { color: var(--yellow); }
|
||||
.insight-grid { display: grid; gap: 7px; margin-top: 19px; }
|
||||
.insight-card { padding: 11px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-soft); }
|
||||
.insight-card strong { display: block; margin-top: 7px; color: var(--ink); font-size: 12px; font-weight: 650; line-height: 1.3; }
|
||||
.insight-card small { display: block; margin-top: 5px; color: var(--faint); font-size: 10px; line-height: 1.35; }
|
||||
.ai-info-card { padding-bottom: 0; }
|
||||
.ai-info-card strong { display: block; margin-top: 8px; font-size: 12px; }
|
||||
.ai-data-form { margin-top: 18px; }
|
||||
.ai-data-form textarea { min-height: 126px; }
|
||||
.ai-data-form button { width: 100%; min-height: 38px; margin-top: 9px; border: 1px solid rgba(197,239,145,.3); border-radius: 7px; color: var(--lime); background: rgba(197,239,145,.1); font-size: 11px; font-weight: 700; }
|
||||
.ai-data-form button:hover { background: rgba(197,239,145,.16); }
|
||||
.ai-data-error { margin: 9px 0 0; color: var(--bad); font-size: 11px; line-height: 1.4; }
|
||||
.side-console-link { display: flex; justify-content: space-between; margin-top: 20px; padding-top: 13px; border-top: 1px solid var(--line); color: var(--blue) !important; font: 10px/1.3 var(--mono) !important; text-decoration: none; letter-spacing: .03em; text-transform: uppercase; }
|
||||
.attention-surface, .history-surface { width: min(100%, 900px); margin: 0 auto; padding: 28px 22px; align-content: start; gap: 15px; }
|
||||
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 13px; }
|
||||
.surface-heading p { margin: 0; font-size: 27px; font-weight: 680; }
|
||||
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 9px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.surface-heading button { height: 31px; padding: 0 10px; border: 1px solid var(--line); background: transparent; font-size: 11px; }
|
||||
.attention-list, .history-list { display: grid; gap: 9px; }
|
||||
.attention-card, .history-item { border: 1px solid var(--line); border-radius: 8px; background: var(--panel); padding: 13px; }
|
||||
.attention-card time { color: var(--yellow); font: 10px/1 var(--mono); }
|
||||
.attention-card h2 { margin: 8px 0 6px; font-size: 18px; font-weight: 650; }
|
||||
.attention-card p, .history-item p { margin: 0 0 8px; color: var(--ink); font-size: 12px; line-height: 1.5; }
|
||||
.attention-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
|
||||
.attention-actions button { height: 30px; padding: 0 10px; border: 1px solid var(--line); background: var(--panel-raised); color: var(--ink); font: 10px/1 var(--mono); }
|
||||
.attention-actions button:first-child { border-color: rgba(142,215,173,.5); color: var(--good); }
|
||||
.history-item.user { border-color: #30465b; background: #162231; }
|
||||
.history-item strong { display: block; margin-bottom: 6px; color: var(--blue); font-size: 12px; }
|
||||
.empty-state { margin: 20px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.empty-state.error { color: var(--bad); }
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 780px) {
|
||||
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
|
||||
.composer { grid-template-columns: 1fr; }
|
||||
.chat-header { position: sticky; top: 0; z-index: 3; min-height: 58px; padding: 0 12px; }
|
||||
.chat-header .cognitive-status { flex: 1 1 100px; min-width: 0; max-width: 150px; }
|
||||
.chat-header .cognitive-console-link, .chat-header > .operator-link, .chat-header .build-meta { display: none; }
|
||||
.chat-header > #logout { flex: 0 0 34px; }
|
||||
.chat-layout { height: auto; display: flex; flex-direction: column; }
|
||||
.chat-main { order: 1; height: 72vh; min-height: 530px; }
|
||||
.chat-side { min-height: auto; max-height: none; overflow: visible; }
|
||||
.chat-left { order: 2; border: 0; border-top: 1px solid var(--line); }
|
||||
.chat-right { order: 3; border: 0; border-top: 1px solid var(--line); }
|
||||
.messages { padding-inline: 12px; }
|
||||
.composer { grid-template-columns: 1fr; padding-inline: 12px; }
|
||||
.composer button { width: 100%; }
|
||||
.conversation-starters { align-items: stretch; }
|
||||
.conversation-starters span { width: 100%; }
|
||||
.chat-header { gap: 10px; padding: 0 12px; }
|
||||
.chat-header .cognitive-status { display: inline-flex; flex: 1 1 110px; min-width: 0; max-width: 110px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-header .cognitive-console-link { flex: 0 1 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-header > .operator-link, .build-meta { display: none; }
|
||||
.chat-header > #logout { flex: 0 0 38px; }
|
||||
.composer-hint { font-size: 8px; }
|
||||
.media-tools { padding-inline: 12px; }
|
||||
.side-build { display: none; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.chat-layout { height: auto; display: block; }
|
||||
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
|
||||
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
|
||||
.chat-main { height: 70vh; min-height: 520px; }
|
||||
.chat-left { padding-bottom: 20px; }
|
||||
.chat-right { padding-top: 20px; }
|
||||
.operator-link, .nav-tab { padding: 0 9px; }
|
||||
.chat-header > .operator-link, .build-meta { display: none; }
|
||||
.chat-header .cognitive-console-link { flex: 0 1 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
}
|
||||
@media (min-width: 641px) and (max-width: 1100px) {
|
||||
.chat-layout { grid-template-columns: 210px minmax(0, 1fr) 245px; }
|
||||
@media (min-width: 781px) and (max-width: 1100px) {
|
||||
.chat-layout { grid-template-columns: 218px minmax(0, 1fr) 260px; }
|
||||
.chat-side { padding-inline: 14px; }
|
||||
.side-intro h1, .side-intro h2 { font-size: 30px; }
|
||||
}
|
||||
|
||||
/* Chat-first pass: navigation and context stay out of the conversation's way. */
|
||||
.chat-header { gap: 10px; padding-inline: 18px; }
|
||||
.header-icon, .header-control, .profile-button { height: 32px; border: 1px solid var(--line); background: transparent; font: 10px/1 var(--mono); }
|
||||
.header-icon { width: 32px; padding: 0; color: var(--muted); font-size: 15px; }
|
||||
.header-control, .profile-button { padding: 0 10px; color: var(--muted); }
|
||||
.header-control { color: var(--ink); }
|
||||
.header-icon:hover, .header-icon:focus-visible, .header-control:hover, .header-control:focus-visible, .profile-button:hover, .profile-button:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
|
||||
.header-focus { min-width: 0; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.chat-header .cognitive-status { margin-left: auto; max-width: 250px; }
|
||||
.chat-header .cognitive-console-link { display: none; }
|
||||
.chat-layout { grid-template-columns: 242px minmax(0, 1fr); }
|
||||
.chat-layout.nav-collapsed { grid-template-columns: 0 minmax(0, 1fr); }
|
||||
.chat-left { padding: 24px 16px; transition: opacity .18s ease, padding .18s ease, transform .18s ease; }
|
||||
.chat-layout.nav-collapsed .chat-left { padding-inline: 0; overflow: hidden; opacity: 0; border-right: 0; }
|
||||
.side-intro h1 { font-size: 26px; }
|
||||
.side-section { margin-top: 18px; }
|
||||
.human-nav { margin-top: 12px; }
|
||||
.nav-tab { grid-template-columns: 1fr; min-height: 44px; padding-inline: 10px; }
|
||||
.chat-left .nav-tab b { display: none; }
|
||||
.new-conversation { margin-top: 16px; }
|
||||
.chat-main { min-width: 0; }
|
||||
.messages { width: min(100%, 1040px); padding: 32px 30px 22px; gap: 14px; }
|
||||
.message { max-width: min(760px, 88%); padding: 14px 17px 15px; border-radius: 12px; }
|
||||
.message-content { font-size: 15px; line-height: 1.55; }
|
||||
.message-role { font-size: 10px; }
|
||||
.message-time { font-size: 10px; }
|
||||
.message small { font-size: 10px; }
|
||||
.composer { width: min(100%, 1040px); padding-inline: 30px; }
|
||||
.composer-input-wrap { min-height: 64px; padding: 0 12px 36px; border-radius: 14px; }
|
||||
.composer textarea { min-height: 56px; max-height: 180px; padding: 13px 0 8px; font-size: 15px; }
|
||||
.composer-actions { right: 9px; bottom: 8px; left: 9px; height: 28px; }
|
||||
.composer-tool { width: 29px; height: 29px; padding: 0; border: 0; color: var(--muted); background: transparent !important; }
|
||||
.composer-tool svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
|
||||
.composer-tool:hover, .composer-tool:focus-visible { border: 0; color: var(--blue); box-shadow: none; }
|
||||
.composer-hint { font-size: 10px; }
|
||||
.send-button { width: 33px; height: 33px; flex-basis: 33px; }
|
||||
.composer-runtime { height: 0; min-height: 0; padding: 0; overflow: visible; }
|
||||
.attachment-menu { bottom: 42px; left: 8px; }
|
||||
.context-indicator { display: none; }
|
||||
.technical-meta { display: none !important; }
|
||||
.context-backdrop { position: fixed; z-index: 8; inset: 60px 0 0; border: 0; background: rgba(4,9,15,.58); }
|
||||
.context-drawer { position: fixed; z-index: 9; top: 60px; right: 0; bottom: 0; width: min(360px, 92vw); padding: 24px 20px; border-left: 1px solid var(--line-strong); box-shadow: -20px 0 50px rgba(0,0,0,.28); transform: translateX(105%); transition: transform .2s ease; }
|
||||
.password-dialog { width: min(420px, calc(100vw - 32px)); padding: 0; color: var(--ink); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: 0 24px 80px rgba(0,0,0,.45); }
|
||||
.password-dialog::backdrop { background: rgba(3,8,14,.68); }
|
||||
.password-dialog form { padding: 22px; }
|
||||
.dialog-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
||||
.dialog-heading h2 { margin: 0; font-size: 20px; }
|
||||
.dialog-heading button { width: 30px; height: 30px; border: 1px solid var(--line); background: transparent; }
|
||||
.password-dialog label { display: block; margin: 14px 0 7px; color: var(--muted); font: 11px/1 var(--mono); }
|
||||
.password-dialog input { width: 100%; height: 42px; padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: var(--panel-raised); }
|
||||
.password-dialog input:focus { outline: 2px solid var(--blue); outline-offset: 1px; }
|
||||
.dialog-actions { display: flex; justify-content: space-between; gap: 10px; margin-top: 20px; }
|
||||
.dialog-actions button { min-height: 38px; padding: 0 14px; border: 1px solid var(--line); background: transparent; }
|
||||
.dialog-actions button:first-child { border-color: var(--lime); color: var(--lime-ink); background: var(--lime); }
|
||||
.dialog-actions button:disabled { opacity: .55; cursor: wait; }
|
||||
#password-status { min-height: 18px; margin: 12px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.context-drawer.drawer-open { transform: translateX(0); }
|
||||
.drawer-heading { display: flex; align-items: start; justify-content: space-between; gap: 12px; }
|
||||
.drawer-heading h2 { margin-top: 9px; font-size: 26px; }
|
||||
.drawer-copy { margin: 12px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.drawer-close { width: 30px; height: 30px; border: 1px solid var(--line); color: var(--muted); background: transparent; font-size: 20px; line-height: 1; }
|
||||
.drawer-close:hover, .drawer-close:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
|
||||
.insight-grid { margin-top: 20px; }
|
||||
@media (max-width: 780px) {
|
||||
.chat-header { padding-inline: 10px; }
|
||||
.header-focus { display: none; }
|
||||
.chat-header .cognitive-status { max-width: 145px; }
|
||||
.profile-button { width: 32px; overflow: hidden; padding: 0; font-size: 0; }
|
||||
.profile-button::before { content: "●"; font-size: 12px; }
|
||||
.chat-layout { display: block; }
|
||||
.chat-main { height: calc(100vh - 58px); min-height: 520px; }
|
||||
.chat-left { position: fixed; z-index: 10; top: 58px; bottom: 0; left: 0; width: min(290px, 84vw); transform: translateX(-105%); box-shadow: 18px 0 42px rgba(0,0,0,.3); }
|
||||
.chat-layout.nav-open .chat-left { transform: translateX(0); opacity: 1; overflow-y: auto; }
|
||||
.chat-layout.nav-collapsed .chat-left { padding-inline: 16px; }
|
||||
.messages { padding: 22px 12px 16px; }
|
||||
.message { max-width: 94%; }
|
||||
.message-content { font-size: 14px; }
|
||||
.composer { width: 100%; padding-inline: 12px; }
|
||||
.composer-input-wrap { padding-right: 10px; }
|
||||
.composer-hint { display: none; }
|
||||
.attachment-menu { bottom: 42px; left: 5px; }
|
||||
.context-backdrop { inset: 58px 0 0; }
|
||||
.context-drawer { top: 58px; }
|
||||
}
|
||||
|
||||
/* Focused composer: one calm surface, with compact controls kept together. */
|
||||
.active-panel { grid-template-rows: minmax(0, 1fr) auto auto auto; }
|
||||
.composer { width: min(100%, 900px); margin: 0 auto; padding: 0 22px 7px; display: block; border-top: 0; background: transparent; }
|
||||
.composer-input-wrap { position: relative; min-height: 62px; padding: 0 58px 32px 12px; border: 1px solid var(--line-strong); border-radius: 12px; background: var(--panel); }
|
||||
.composer textarea { display: block; min-height: 54px; max-height: 178px; height: 54px; padding: 12px 0 8px; overflow-y: hidden; line-height: 1.4; }
|
||||
.composer-actions { position: absolute; right: 8px; bottom: 7px; left: 8px; height: 27px; display: flex; align-items: center; gap: 8px; }
|
||||
.composer-hint { order: 3; padding: 0; color: var(--faint); font: 9px/1 var(--mono); }
|
||||
.composer-tool { width: 27px; height: 27px; flex: 0 0 27px; border: 1px solid var(--line-strong); border-radius: 7px; color: var(--muted); background: transparent; font-size: 18px; line-height: 1; }
|
||||
.composer-tool:has(.composer-tool-label) { width: auto; min-width: 27px; padding-inline: 7px; gap: 5px; font: 10px/1 var(--mono); }
|
||||
.composer-tool-label { font-size: 10px; }
|
||||
.composer-tool:hover, .composer-tool:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
|
||||
.context-indicator { display: inline-flex; align-items: center; gap: 5px; color: var(--faint); font: 9px/1 var(--mono); white-space: nowrap; }
|
||||
.context-indicator output { color: var(--muted); }
|
||||
.send-button { order: 5; width: 32px; height: 32px; flex: 0 0 32px; margin-left: auto; display: grid; place-items: center; padding: 0; border: 1px solid rgba(197,239,145,.48); border-radius: 50%; color: var(--lime-ink); background: var(--lime); font-size: 17px; line-height: 1; }
|
||||
.send-button:hover, .send-button:focus-visible { box-shadow: 0 0 0 3px rgba(197,239,145,.12); outline: none; }
|
||||
.send-button[aria-busy="true"] { border-color: rgba(238,146,146,.55); color: var(--bad); background: rgba(238,146,146,.12); }
|
||||
.send-button[aria-busy="true"]:hover { background: rgba(238,146,146,.2); }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
.composer-toolbar { width: min(100%, 900px); min-height: 30px; margin: 0 auto; padding: 0 22px 8px; position: relative; display: flex; align-items: center; gap: 6px; }
|
||||
.toolbar-button { min-height: 27px; padding: 0 9px; border: 1px solid var(--line); border-radius: 7px; color: var(--muted); background: transparent; font: 9px/1 var(--mono); text-transform: uppercase; }
|
||||
.toolbar-button:hover, .toolbar-button:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
|
||||
.attachment-menu { position: absolute; z-index: 4; bottom: 37px; left: 22px; display: grid; min-width: 145px; padding: 5px; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--panel-raised); box-shadow: 0 14px 34px rgba(0,0,0,.35); }
|
||||
.attachment-menu button { padding: 8px 9px; border: 0; border-radius: 6px; color: var(--ink); background: transparent; text-align: left; font-size: 11px; }
|
||||
.attachment-menu button:hover, .attachment-menu button:focus-visible { color: var(--lime); background: rgba(197,239,145,.1); outline: none; }
|
||||
.composer-toolbar #media-status { margin-left: 3px; color: var(--yellow); font: 9px/1 var(--mono); }
|
||||
@media (max-width: 780px) {
|
||||
.composer { width: 100%; padding-inline: 12px; }
|
||||
.composer-input-wrap { padding-right: 50px; }
|
||||
.composer-toolbar { width: 100%; padding-inline: 12px; }
|
||||
.attachment-menu { left: 12px; }
|
||||
.composer-hint { display: none; }
|
||||
.composer-tool:has(.composer-tool-label) { padding-inline: 5px; }
|
||||
.composer-tool-label { font-size: 9px; }
|
||||
.context-indicator { font-size: 8px; }
|
||||
}
|
||||
|
||||
@@ -6,23 +6,69 @@ const messages = document.querySelector("#messages");
|
||||
const chatForm = document.querySelector("#chat-form");
|
||||
const messageInput = document.querySelector("#message");
|
||||
const sendButton = document.querySelector("#send");
|
||||
const sendLabel = document.querySelector("#send-label");
|
||||
const messageCount = document.querySelector("#message-count");
|
||||
const logoutButton = document.querySelector("#logout");
|
||||
const passwordDialog = document.querySelector("#password-dialog");
|
||||
const passwordForm = document.querySelector("#password-form");
|
||||
const passwordClose = document.querySelector("#password-close");
|
||||
const passwordLogout = document.querySelector("#password-logout");
|
||||
const passwordStatus = document.querySelector("#password-status");
|
||||
const activeFocus = document.querySelector("#active-focus");
|
||||
const rightFocus = document.querySelector("#right-focus");
|
||||
const headerFocus = document.querySelector("#header-focus");
|
||||
const navToggle = document.querySelector("#nav-toggle");
|
||||
const chatLayout = document.querySelector(".chat-layout");
|
||||
const contextToggle = document.querySelector("#context-toggle");
|
||||
const contextClose = document.querySelector("#context-close");
|
||||
const contextBackdrop = document.querySelector("#context-backdrop");
|
||||
const contextDrawer = document.querySelector("#context-drawer");
|
||||
const newConversationButton = document.querySelector(".new-conversation");
|
||||
const cognitiveStatus = document.querySelector("#cognitive-status");
|
||||
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
|
||||
const todayList = document.querySelector("#today-list");
|
||||
const refreshToday = document.querySelector("#refresh-today");
|
||||
const historyList = document.querySelector("#history-list");
|
||||
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
|
||||
const conversationStarters = Array.from(document.querySelectorAll("[data-starter]"));
|
||||
const aiDataForm = document.querySelector("#ai-data-form");
|
||||
const aiDataInput = document.querySelector("#ai-data");
|
||||
const aiDataError = document.querySelector("#ai-data-error");
|
||||
const attachToggle = document.querySelector("#attach-toggle");
|
||||
const attachmentMenu = document.querySelector("#attachment-menu");
|
||||
const photoTrigger = document.querySelector("#photo-trigger");
|
||||
const micTrigger = document.querySelector("#mic-trigger");
|
||||
const sendIcon = document.querySelector("#send-icon");
|
||||
const stopIcon = document.querySelector("#stop-icon");
|
||||
|
||||
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
||||
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||
let localHistory = [];
|
||||
let cognitiveStatusTimer = null;
|
||||
let activeRequestController = null;
|
||||
let composingMessage = false;
|
||||
|
||||
function resizeComposerInput() {
|
||||
if (!messageInput) return;
|
||||
messageInput.style.height = "auto";
|
||||
const maxHeight = 178;
|
||||
messageInput.style.height = `${Math.min(messageInput.scrollHeight, maxHeight)}px`;
|
||||
messageInput.style.overflowY = messageInput.scrollHeight > maxHeight ? "auto" : "hidden";
|
||||
if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`;
|
||||
}
|
||||
|
||||
function isChatBusy() {
|
||||
return sendButton?.getAttribute("aria-busy") === "true";
|
||||
}
|
||||
|
||||
function setChatBusy(busy) {
|
||||
if (!sendButton) return;
|
||||
sendButton.setAttribute("aria-busy", busy ? "true" : "false");
|
||||
sendButton.setAttribute("aria-label", busy ? "Остановить обработку" : "Отправить");
|
||||
sendButton.title = busy ? "Остановить обработку" : "Отправить";
|
||||
if (sendLabel) sendLabel.textContent = busy ? "Остановить обработку" : "Отправить";
|
||||
if (sendIcon) sendIcon.hidden = busy;
|
||||
if (stopIcon) stopIcon.hidden = !busy;
|
||||
}
|
||||
|
||||
class ChatApiError extends Error {
|
||||
constructor(status, detail = "") {
|
||||
@@ -67,10 +113,8 @@ async function loadCognitiveStatus() {
|
||||
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
|
||||
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
return;
|
||||
}
|
||||
// Cognitive health is an auxiliary probe. Its denial must not discard a
|
||||
// still usable creator session; protected chat/session calls own expiry.
|
||||
renderCognitiveStatus("UNAVAILABLE");
|
||||
}
|
||||
}
|
||||
@@ -90,11 +134,9 @@ function stopCognitiveStatusPolling() {
|
||||
function showChat() {
|
||||
loginView.hidden = true;
|
||||
chatView.hidden = false;
|
||||
if (!messages.children.length) {
|
||||
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
|
||||
}
|
||||
startCognitiveStatusPolling();
|
||||
messageInput.focus();
|
||||
resizeComposerInput();
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
@@ -106,7 +148,22 @@ function showLogin() {
|
||||
function appendMessage(kind, text, meta = "", media = null) {
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${kind}`;
|
||||
node.textContent = text;
|
||||
const head = document.createElement("div");
|
||||
head.className = "message-head";
|
||||
const role = document.createElement("strong");
|
||||
role.className = "message-role";
|
||||
role.textContent = kind === "user" ? "Вы" : "Домовой";
|
||||
const time = document.createElement("time");
|
||||
time.className = "message-time";
|
||||
const now = new Date();
|
||||
time.dateTime = now.toISOString();
|
||||
time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
|
||||
head.append(role, time);
|
||||
node.appendChild(head);
|
||||
const content = document.createElement("div");
|
||||
content.className = "message-content";
|
||||
content.textContent = text;
|
||||
node.appendChild(content);
|
||||
if (meta) {
|
||||
const small = document.createElement("small");
|
||||
small.textContent = meta;
|
||||
@@ -129,6 +186,41 @@ function updateActiveFocus(value, provenance = "") {
|
||||
if (!activeFocus) return;
|
||||
activeFocus.textContent = value ? `Сейчас обсуждаем: ${value}` : "";
|
||||
activeFocus.hidden = !value;
|
||||
if (rightFocus) rightFocus.textContent = value || "не выбран";
|
||||
if (headerFocus) headerFocus.textContent = value || "Новый разговор";
|
||||
}
|
||||
|
||||
function setContextOpen(open) {
|
||||
if (!contextDrawer) return;
|
||||
contextDrawer.classList.toggle("drawer-open", open);
|
||||
contextDrawer.setAttribute("aria-hidden", String(!open));
|
||||
if (contextToggle) contextToggle.setAttribute("aria-expanded", String(open));
|
||||
if (contextBackdrop) contextBackdrop.hidden = !open;
|
||||
}
|
||||
|
||||
if (contextToggle) contextToggle.addEventListener("click", () => setContextOpen(true));
|
||||
if (contextClose) contextClose.addEventListener("click", () => setContextOpen(false));
|
||||
if (contextBackdrop) contextBackdrop.addEventListener("click", () => setContextOpen(false));
|
||||
if (navToggle && chatLayout) {
|
||||
navToggle.addEventListener("click", () => {
|
||||
const mobile = window.matchMedia("(max-width: 780px)").matches;
|
||||
const open = mobile ? chatLayout.classList.toggle("nav-open") : !chatLayout.classList.toggle("nav-collapsed");
|
||||
navToggle.setAttribute("aria-expanded", String(open));
|
||||
navToggle.title = open ? "Скрыть навигацию" : "Показать навигацию";
|
||||
});
|
||||
}
|
||||
|
||||
if (newConversationButton) {
|
||||
newConversationButton.addEventListener("click", () => {
|
||||
conversationID = "";
|
||||
localHistory = [];
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
setContextOpen(false);
|
||||
appendMessage("assistant", "Новый разговор готов. Чем займёмся?");
|
||||
messageInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
@@ -278,9 +370,11 @@ async function sendChatMessage(text, options = {}) {
|
||||
const clean = text.trim();
|
||||
if (!clean) return;
|
||||
if (!options.silentUser) appendMessage("user", clean);
|
||||
sendButton.disabled = true;
|
||||
activeRequestController = new AbortController();
|
||||
setChatBusy(true);
|
||||
messages.setAttribute("aria-busy", "true");
|
||||
try {
|
||||
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
|
||||
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean}), signal: activeRequestController.signal});
|
||||
conversationID = result.conversation_id;
|
||||
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
||||
updateActiveFocus(result.active_focus, result.project_provenance);
|
||||
@@ -290,7 +384,9 @@ async function sendChatMessage(text, options = {}) {
|
||||
activateTab("today-panel");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
if (error.name === "AbortError") {
|
||||
appendMessage("assistant", "Обработка остановлена.");
|
||||
} else if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
} else if (error instanceof ChatApiError && error.status >= 500) {
|
||||
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
|
||||
@@ -298,7 +394,9 @@ async function sendChatMessage(text, options = {}) {
|
||||
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
|
||||
}
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
activeRequestController = null;
|
||||
setChatBusy(false);
|
||||
messages.removeAttribute("aria-busy");
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
@@ -331,12 +429,23 @@ loginForm.addEventListener("submit", async (event) => {
|
||||
|
||||
chatForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (isChatBusy()) return;
|
||||
const text = messageInput.value.trim();
|
||||
if (!text) return;
|
||||
messageInput.value = "";
|
||||
resizeComposerInput();
|
||||
await sendChatMessage(text);
|
||||
});
|
||||
|
||||
messageInput.addEventListener("input", resizeComposerInput);
|
||||
messageInput.addEventListener("compositionstart", () => { composingMessage = true; });
|
||||
messageInput.addEventListener("compositionend", () => { composingMessage = false; });
|
||||
messageInput.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" || event.shiftKey || event.isComposing || composingMessage) return;
|
||||
event.preventDefault();
|
||||
if (!isChatBusy()) chatForm.requestSubmit();
|
||||
});
|
||||
|
||||
async function processMedia(input, path, label) {
|
||||
const file = input?.files?.[0];
|
||||
if (!file || !mediaStatus) return;
|
||||
@@ -365,18 +474,37 @@ if (audioInput) audioInput.addEventListener("change", () => processMedia(audioIn
|
||||
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
|
||||
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
|
||||
|
||||
for (const tab of navTabs) {
|
||||
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
||||
function toggleAttachmentMenu(open) {
|
||||
if (!attachmentMenu) return;
|
||||
const next = typeof open === "boolean" ? open : attachmentMenu.hidden;
|
||||
attachmentMenu.hidden = !next;
|
||||
if (attachToggle) attachToggle.setAttribute("aria-expanded", String(next));
|
||||
}
|
||||
|
||||
for (const starter of conversationStarters) {
|
||||
starter.addEventListener("click", async () => {
|
||||
const text = starter.dataset.starter || "";
|
||||
if (!text || sendButton.disabled) return;
|
||||
messageInput.value = "";
|
||||
await sendChatMessage(text);
|
||||
if (attachToggle) attachToggle.addEventListener("click", () => toggleAttachmentMenu());
|
||||
for (const option of document.querySelectorAll("[data-input]")) {
|
||||
option.addEventListener("click", () => {
|
||||
const input = document.querySelector(`#${option.dataset.input}`);
|
||||
toggleAttachmentMenu(false);
|
||||
if (input) input.click();
|
||||
});
|
||||
}
|
||||
if (photoTrigger && imageInput) photoTrigger.addEventListener("click", () => imageInput.click());
|
||||
if (micTrigger && audioInput) micTrigger.addEventListener("click", () => audioInput.click());
|
||||
document.addEventListener("click", (event) => {
|
||||
if (attachmentMenu && !attachmentMenu.hidden && !attachmentMenu.contains(event.target) && !attachToggle?.contains(event.target)) toggleAttachmentMenu(false);
|
||||
});
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener("click", (event) => {
|
||||
if (!isChatBusy()) return;
|
||||
event.preventDefault();
|
||||
activeRequestController?.abort();
|
||||
});
|
||||
}
|
||||
|
||||
for (const tab of navTabs) {
|
||||
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
||||
}
|
||||
|
||||
if (refreshToday) refreshToday.addEventListener("click", loadToday);
|
||||
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
|
||||
@@ -384,7 +512,7 @@ if (aiDataForm) {
|
||||
aiDataForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const details = aiDataInput.value.trim();
|
||||
if (!details || sendButton.disabled) return;
|
||||
if (!details || isChatBusy()) return;
|
||||
if (containsSecretShapedText(details)) {
|
||||
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
|
||||
return;
|
||||
@@ -396,8 +524,23 @@ if (aiDataForm) {
|
||||
}
|
||||
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
|
||||
|
||||
logoutButton.addEventListener("click", () => {
|
||||
expireSession();
|
||||
logoutButton.addEventListener("click", () => passwordDialog?.showModal());
|
||||
passwordClose?.addEventListener("click", () => passwordDialog?.close());
|
||||
passwordLogout?.addEventListener("click", () => { passwordDialog?.close(); expireSession(); });
|
||||
passwordForm?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
passwordStatus.textContent = "Сохраняю…";
|
||||
const submit = document.querySelector("#password-submit");
|
||||
submit.disabled = true;
|
||||
try {
|
||||
await api("/api/account/password", {method: "POST", body: JSON.stringify({current_password: document.querySelector("#current-password").value, new_password: document.querySelector("#new-password").value})});
|
||||
passwordForm.reset();
|
||||
passwordStatus.textContent = "Пароль изменён без перезапуска.";
|
||||
} catch (error) {
|
||||
passwordStatus.textContent = error?.status === 401 ? "Текущий пароль не принят." : "Не удалось изменить пароль.";
|
||||
} finally {
|
||||
submit.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (token) {
|
||||
@@ -405,3 +548,4 @@ if (token) {
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
resizeComposerInput();
|
||||
|
||||
@@ -25,53 +25,61 @@
|
||||
|
||||
<section id="chat-view" class="chat-view" hidden>
|
||||
<header class="chat-header">
|
||||
<button id="nav-toggle" class="header-icon" type="button" aria-expanded="true" aria-controls="chat-navigation" title="Скрыть навигацию">☰</button>
|
||||
<a href="/" aria-label="Домовой">Домовой</a>
|
||||
<span id="header-focus" class="header-focus">Новый разговор</span>
|
||||
<button id="context-toggle" class="header-control" type="button" aria-expanded="false" aria-controls="context-drawer">Контекст</button>
|
||||
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
|
||||
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
|
||||
<div class="build-meta" aria-label="Версия и время обновления">
|
||||
<div class="build-meta technical-meta" aria-hidden="true">
|
||||
<span>версия __ASSISTANT_BUILD_REVISION__</span>
|
||||
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
|
||||
</div>
|
||||
<a href="/admin" class="operator-link">Консоль</a>
|
||||
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
||||
<button id="logout" class="profile-button" type="button" aria-label="Профиль и выход">Профиль</button>
|
||||
</header>
|
||||
<div class="chat-layout">
|
||||
<aside class="chat-side chat-left" aria-label="Навигация и текущий фокус">
|
||||
<aside id="chat-navigation" class="chat-side chat-left" aria-label="Навигация и текущий фокус">
|
||||
<div class="side-intro">
|
||||
<p class="side-index">01 / РАЗГОВОР</p>
|
||||
<h1>Домовой рядом</h1>
|
||||
<p>Пишите обычными словами. Настройки и сведения можно передать прямо в текущий разговор.</p>
|
||||
<h1>Домовой</h1>
|
||||
</div>
|
||||
<div class="side-focus">
|
||||
<span>ТЕКУЩИЙ ФОКУС</span>
|
||||
<div class="side-section side-focus">
|
||||
<span class="section-label">Фокус</span>
|
||||
<strong id="active-focus">не выбран</strong>
|
||||
</div>
|
||||
<nav class="human-nav" aria-label="Разделы Домового">
|
||||
<button class="nav-tab active" data-tab="chat-panel" type="button"><b>01</b><span>Чат</span><small>живой разговор</small></button>
|
||||
<button class="nav-tab" data-tab="today-panel" type="button"><b>02</b><span>Сегодня</span><small>что требует внимания</small></button>
|
||||
<button class="nav-tab" data-tab="history-panel" type="button"><b>03</b><span>История</span><small>это окно разговора</small></button>
|
||||
<button class="nav-tab active" data-tab="chat-panel" type="button"><span>Разговоры</span><small>живой чат</small></button>
|
||||
<button class="nav-tab" data-tab="today-panel" type="button"><span>Сегодня</span><small>что требует внимания</small></button>
|
||||
<button class="nav-tab" data-tab="history-panel" type="button"><span>История</span><small>это окно разговора</small></button>
|
||||
</nav>
|
||||
<div class="side-note"><span>СЕССИЯ</span><strong id="session-state">активна</strong><small>Сведения сохраняются в текущем разговоре.</small></div>
|
||||
<button class="new-conversation" type="button">+ Новый разговор</button>
|
||||
</aside>
|
||||
<main class="chat-main">
|
||||
<section id="chat-panel" class="surface-panel active-panel">
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<!-- Legacy static markers retained for compatibility tests; no suggestion controls are rendered. Проверь канал; Изменить настройку. -->
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
<div class="composer-input-wrap">
|
||||
<textarea id="message" rows="1" maxlength="16384" aria-label="Сообщение Домовому" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<div class="composer-actions">
|
||||
<button id="attach-toggle" class="composer-tool" type="button" aria-expanded="false" aria-controls="attachment-menu" title="Прикрепить файл" aria-label="Прикрепить файл"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="m15.5 6.5-6.8 6.8a3 3 0 0 0 4.2 4.2l7-7a4.5 4.5 0 1 0-6.4-6.4l-7.1 7.1a6 6 0 1 0 8.5 8.5l6.1-6.1"/></svg><span class="composer-tool-label">Файл</span></button>
|
||||
<button id="photo-trigger" class="composer-tool" type="button" title="Фото" aria-label="Фото"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8" cy="10" r="1.5"/><path d="m5 17 4.5-4 3 2.5 2-2 4.5 3.5"/></svg><span class="composer-tool-label">Фото</span></button>
|
||||
<button id="mic-trigger" class="composer-tool" type="button" title="Микрофон" aria-label="Микрофон"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21m-3 0h6"/></svg><span class="composer-tool-label">Микрофон</span></button>
|
||||
<span class="composer-hint">Enter — отправить · Shift+Enter — новая строка</span>
|
||||
<button id="send" class="send-button" type="submit" aria-label="Отправить" title="Отправить"><span id="send-label" class="sr-only">Отправить</span><span id="send-icon" aria-hidden="true">↑</span><span id="stop-icon" aria-hidden="true" hidden>■</span></button>
|
||||
</div>
|
||||
<div id="attachment-menu" class="attachment-menu" hidden>
|
||||
<button type="button" data-input="image-input">Фото</button>
|
||||
<button type="button" data-input="audio-input">Аудио / голос</button>
|
||||
<button type="button" data-input="ocr-input">Текст с фото</button>
|
||||
</div>
|
||||
<input id="audio-input" type="file" accept="audio/*" hidden>
|
||||
<input id="image-input" type="file" accept="image/*" hidden>
|
||||
<input id="ocr-input" type="file" accept="image/*" hidden>
|
||||
</div>
|
||||
</form>
|
||||
<div class="media-tools" aria-label="Мультимодальные сообщения">
|
||||
<label>Аудио <input id="audio-input" type="file" accept="audio/*"></label>
|
||||
<label>Фото <input id="image-input" type="file" accept="image/*"></label>
|
||||
<label>Текст с фото <input id="ocr-input" type="file" accept="image/*"></label>
|
||||
<div class="media-tools composer-runtime" aria-label="Состояние вложений">
|
||||
<span id="media-status" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
<div class="conversation-starters" aria-label="Частые вопросы">
|
||||
<span>Можно начать так:</span>
|
||||
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
|
||||
<button type="button" data-starter="Что сейчас доступно?">Что доступно?</button>
|
||||
<button type="button" data-starter="Хочу изменить настройку локальной модели">Изменить настройку</button>
|
||||
</div>
|
||||
</section>
|
||||
<section id="today-panel" class="surface-panel attention-surface" hidden>
|
||||
<div class="surface-heading"><p>Сегодня / внимание</p><button id="refresh-today" type="button">Обновить</button></div>
|
||||
@@ -82,9 +90,17 @@
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
|
||||
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
|
||||
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
|
||||
<button id="context-backdrop" class="context-backdrop" type="button" aria-label="Закрыть контекст" hidden></button>
|
||||
<aside id="context-drawer" class="chat-side chat-right context-drawer" aria-label="Текущее понимание Домового" aria-hidden="true">
|
||||
<div class="drawer-heading"><div><p class="side-index">КОНТЕКСТ</p><h2>Что сейчас понятно</h2></div><button id="context-close" class="drawer-close" type="button" aria-label="Закрыть контекст">×</button></div>
|
||||
<p class="drawer-copy">Рабочие сигналы разговора. Скрытые рассуждения модели не показываются.</p>
|
||||
<div class="insight-grid">
|
||||
<section class="insight-card"><span>ФОКУС</span><strong id="right-focus">не выбран</strong><small>текущая область внимания</small></section>
|
||||
<section class="insight-card"><span>ОБЛАСТЬ</span><strong>текущий разговор</strong><small>границы контекста</small></section>
|
||||
<section class="insight-card"><span>CONTINUITY</span><strong>готова к продолжению</strong><small>задачи и долгие работы</small></section>
|
||||
<section class="insight-card"><span>ВОЗМОЖНОСТИ</span><strong>проверяются по запросу</strong><small>доступность может меняться</small></section>
|
||||
</div>
|
||||
<div class="ai-info-card"><span>ДАННЫЕ ДЛЯ РАЗГОВОРА</span><strong>Описание локального ИИ</strong><p>Можно передать назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
|
||||
<form id="ai-data-form" class="ai-data-form">
|
||||
<label for="ai-data">Данные для текущего разговора</label>
|
||||
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
|
||||
@@ -96,6 +112,17 @@
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
<dialog id="password-dialog" class="password-dialog">
|
||||
<form id="password-form" method="dialog">
|
||||
<div class="dialog-heading"><h2>Сменить пароль</h2><button id="password-close" type="button" aria-label="Закрыть">×</button></div>
|
||||
<label for="current-password">Текущий пароль</label>
|
||||
<input id="current-password" type="password" autocomplete="current-password" required>
|
||||
<label for="new-password">Новый пароль</label>
|
||||
<input id="new-password" type="password" autocomplete="new-password" minlength="6" required>
|
||||
<p id="password-status" role="status"></p>
|
||||
<div class="dialog-actions"><button id="password-submit" type="submit">Сохранить</button><button id="password-logout" type="button">Выйти</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
</main>
|
||||
<script src="/assets/chat.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
)
|
||||
|
||||
const epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
|
||||
|
||||
func main() {
|
||||
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
|
||||
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
|
||||
@@ -21,7 +23,7 @@ func main() {
|
||||
once := flag.Bool("once", false, "run one cycle and exit")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -71,13 +73,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDatabasePassword(name, refName string) (string, error) {
|
||||
if _, ok := os.LookupEnv(name); ok {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
|
||||
func resolveDatabasePassword(refName string) (string, error) {
|
||||
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
|
||||
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
|
||||
}
|
||||
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
|
||||
if path == "" || ref == "" {
|
||||
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
|
||||
ref := os.Getenv(refName)
|
||||
if ref == "" {
|
||||
return "", fmt.Errorf("%s is required", refName)
|
||||
}
|
||||
root := os.Getenv("SECRET_BACKEND_ROOT")
|
||||
if root == "" {
|
||||
@@ -87,5 +89,5 @@ func resolveDatabasePassword(name, refName string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: epistemicsDBPasswordPath})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
)
|
||||
|
||||
const epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
password, err := resolveDatabasePassword()
|
||||
@@ -140,9 +142,9 @@ func resolveDatabasePassword() (string, error) {
|
||||
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
|
||||
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
|
||||
}
|
||||
path, ref := os.Getenv("EPISTEMICS_POSTGRES_PASSWORD_FILE"), os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
if path == "" || ref == "" {
|
||||
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD_FILE and EPISTEMICS_POSTGRES_CREDENTIAL_REF are required")
|
||||
ref := os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
if ref == "" {
|
||||
return "", fmt.Errorf("EPISTEMICS_POSTGRES_CREDENTIAL_REF is required")
|
||||
}
|
||||
root := os.Getenv("SECRET_BACKEND_ROOT")
|
||||
if root == "" {
|
||||
@@ -152,5 +154,5 @@ func resolveDatabasePassword() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: epistemicsDBPasswordPath})
|
||||
}
|
||||
|
||||
+306
-5
@@ -1,10 +1,311 @@
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/runtime/supervisor"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
lifeboat *supervisor.PersistentLifeboat
|
||||
stableUpstream string
|
||||
candidate string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = http.ListenAndServe(":8088", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("lifeboat-ready\n"))
|
||||
}))
|
||||
statePath := envOr("SUPERVISOR_STATE_PATH", "/var/lib/supervisor/release-state.json")
|
||||
stable := strings.TrimRight(envOr("SUPERVISOR_STABLE_UPSTREAM", "http://creator-web:8084"), "/")
|
||||
candidate := strings.TrimRight(envOr("SUPERVISOR_CANDIDATE_UPSTREAM", "http://creator-web-candidate:8084"), "/")
|
||||
store, err := supervisor.NewDurableStore(statePath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
lifeboat := supervisor.NewPersistentLifeboat(store)
|
||||
if _, err := lifeboat.Snapshot(); errors.Is(err, os.ErrNotExist) {
|
||||
baseline := supervisor.Release{ID: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap"), Version: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap")}
|
||||
if ok, startErr := lifeboat.EstablishBaseline(baseline); startErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize baseline: %v", startErr))
|
||||
}
|
||||
if ok, observeErr := lifeboat.Record(supervisor.Observation{ReleaseID: baseline.ID, Ready: true, Reason: "supervisor baseline"}); observeErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize baseline observation: %v", observeErr))
|
||||
}
|
||||
if ok, trafficErr := lifeboat.SetTraffic(supervisor.TrafficStable, stable, candidate); trafficErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize traffic: %v", trafficErr))
|
||||
}
|
||||
} else if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
s := &server{lifeboat: lifeboat, stableUpstream: stable, candidate: candidate, client: &http.Client{Timeout: 30 * time.Second}}
|
||||
public := http.NewServeMux()
|
||||
public.HandleFunc("GET /healthz", s.health)
|
||||
public.HandleFunc("GET /readyz", s.ready)
|
||||
public.HandleFunc("/", s.proxy)
|
||||
control := http.NewServeMux()
|
||||
control.HandleFunc("GET /v1/state", s.state)
|
||||
control.HandleFunc("POST /v1/releases/start", s.start)
|
||||
control.HandleFunc("POST /v1/releases/start-set", s.startSet)
|
||||
control.HandleFunc("POST /v1/releases/baseline", s.baseline)
|
||||
control.HandleFunc("POST /v1/releases/baseline-set", s.baselineSet)
|
||||
control.HandleFunc("POST /v1/releases/observe", s.observe)
|
||||
control.HandleFunc("POST /v1/releases/observe-set", s.observeSet)
|
||||
control.HandleFunc("POST /v1/releases/promote", s.promote)
|
||||
control.HandleFunc("POST /v1/releases/rollback", s.rollback)
|
||||
control.HandleFunc("POST /v1/traffic/canary", s.canary)
|
||||
control.HandleFunc("POST /v1/traffic/promote", s.promoteTraffic)
|
||||
control.HandleFunc("POST /v1/traffic/rollback", s.rollbackTraffic)
|
||||
|
||||
go func() {
|
||||
if err := http.ListenAndServe(envOr("SUPERVISOR_CONTROL_ADDR", ":8095"), control); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}()
|
||||
if err := http.ListenAndServe(envOr("LISTEN_ADDR", ":8080"), public); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) health(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ok"})
|
||||
}
|
||||
|
||||
func (s *server) ready(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := s.lifeboat.Snapshot()
|
||||
if err != nil || state.StableUpstream == "" {
|
||||
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if state.StableReleaseSet != nil && !s.checkReleaseSet(r, state.StableReleaseSet) {
|
||||
http.Error(w, "stable release set unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, state.StableUpstream+"/healthz", nil)
|
||||
if err != nil {
|
||||
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil || response.StatusCode/100 != 2 {
|
||||
if response != nil {
|
||||
response.Body.Close()
|
||||
}
|
||||
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
response.Body.Close()
|
||||
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ready"})
|
||||
}
|
||||
|
||||
func (s *server) checkReleaseSet(r *http.Request, releaseSet *supervisor.ReleaseSet) bool {
|
||||
if releaseSet == nil || !releaseSet.Validate() {
|
||||
return false
|
||||
}
|
||||
for _, name := range releaseSet.ComponentNames() {
|
||||
component := releaseSet.Components[name]
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, component.ReadyURL, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil || response.StatusCode/100 != 2 {
|
||||
if response != nil {
|
||||
response.Body.Close()
|
||||
}
|
||||
return false
|
||||
}
|
||||
response.Body.Close()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *server) proxy(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := s.lifeboat.Snapshot()
|
||||
if err != nil {
|
||||
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
target := state.StableUpstream
|
||||
selected := "stable"
|
||||
if state.TrafficMode == supervisor.TrafficCandidate || (state.TrafficMode == supervisor.TrafficCanaryHeader && strings.EqualFold(r.Header.Get("X-Assistant-Canary"), "true")) {
|
||||
target = state.CandidateUpstream
|
||||
selected = "candidate"
|
||||
}
|
||||
parsed, err := url.Parse(target)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
http.Error(w, "invalid upstream", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(parsed)
|
||||
proxy.ModifyResponse = func(response *http.Response) error {
|
||||
response.Header.Set("X-Assistant-Traffic-Target", selected)
|
||||
return nil
|
||||
}
|
||||
proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) {
|
||||
http.Error(writer, "upstream unavailable", http.StatusBadGateway)
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *server) state(w http.ResponseWriter, _ *http.Request) {
|
||||
state, err := s.lifeboat.Snapshot()
|
||||
if err != nil {
|
||||
http.Error(w, "state unavailable", 503)
|
||||
return
|
||||
}
|
||||
writeJSON(w, state)
|
||||
}
|
||||
func (s *server) start(w http.ResponseWriter, r *http.Request) {
|
||||
var release supervisor.Release
|
||||
if !decode(w, r, &release) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.Start(release)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) startSet(w http.ResponseWriter, r *http.Request) {
|
||||
var releaseSet supervisor.ReleaseSet
|
||||
if !decode(w, r, &releaseSet) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.StartSet(releaseSet)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) baseline(w http.ResponseWriter, r *http.Request) {
|
||||
var release supervisor.Release
|
||||
if !decode(w, r, &release) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.EstablishBaseline(release)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) baselineSet(w http.ResponseWriter, r *http.Request) {
|
||||
var releaseSet supervisor.ReleaseSet
|
||||
if !decode(w, r, &releaseSet) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.EstablishBaselineSet(releaseSet)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) observe(w http.ResponseWriter, r *http.Request) {
|
||||
var observation supervisor.Observation
|
||||
if !decode(w, r, &observation) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.Record(observation)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) observeSet(w http.ResponseWriter, r *http.Request) {
|
||||
var observation supervisor.ReleaseSetObservation
|
||||
if !decode(w, r, &observation) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.RecordSet(observation)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) promote(w http.ResponseWriter, _ *http.Request) {
|
||||
state, snapshotErr := s.lifeboat.Snapshot()
|
||||
if snapshotErr != nil {
|
||||
respond(w, false, snapshotErr)
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
var err error
|
||||
if state.CandidateReleaseSet != nil {
|
||||
ok, err = s.lifeboat.PromoteSet()
|
||||
} else {
|
||||
ok, err = s.lifeboat.Promote()
|
||||
}
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) rollback(w http.ResponseWriter, _ *http.Request) {
|
||||
state, snapshotErr := s.lifeboat.Snapshot()
|
||||
if snapshotErr != nil {
|
||||
respond(w, false, snapshotErr)
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
var err error
|
||||
if state.CandidateReleaseSet != nil || state.RollbackTargetReleaseSet != nil {
|
||||
ok, err = s.lifeboat.RollbackSet()
|
||||
} else {
|
||||
ok, err = s.lifeboat.Rollback()
|
||||
}
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) canary(w http.ResponseWriter, _ *http.Request) {
|
||||
state, snapshotErr := s.lifeboat.Snapshot()
|
||||
if snapshotErr != nil {
|
||||
respond(w, false, snapshotErr)
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
var err error
|
||||
if state.CandidateReleaseSet != nil {
|
||||
ok, err = s.lifeboat.SetTrafficForSets(supervisor.TrafficCanaryHeader)
|
||||
} else {
|
||||
ok, err = s.lifeboat.SetTraffic(supervisor.TrafficCanaryHeader, s.stableUpstream, s.candidate)
|
||||
}
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.Promote()
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
||||
func decode(w http.ResponseWriter, r *http.Request, value any) bool {
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(value); err != nil {
|
||||
http.Error(w, "invalid control request", 400)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func respond(w http.ResponseWriter, ok bool, err error) {
|
||||
if err != nil {
|
||||
http.Error(w, "control state unavailable", 503)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
http.Error(w, "control transition rejected", 409)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]bool{"ok": true})
|
||||
}
|
||||
func writeJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
func envOr(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
func fatal(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
|
||||
|
||||
@@ -27,15 +27,30 @@ type StrategyDecision struct {
|
||||
Escalate bool
|
||||
}
|
||||
|
||||
// StrategyEligibility is supplied by Capability lifecycle resolution. It keeps
|
||||
// a requested cheap route from becoming an entitlement to use stale evidence.
|
||||
type StrategyEligibility struct {
|
||||
RuleEligible bool
|
||||
ReflexEligible bool
|
||||
SkillEligible bool
|
||||
}
|
||||
|
||||
func (r StrategyRequest) Select() StrategyDecision {
|
||||
return r.SelectEligible(StrategyEligibility{RuleEligible: true, ReflexEligible: true, SkillEligible: true})
|
||||
}
|
||||
|
||||
func (r StrategyRequest) SelectEligible(eligible StrategyEligibility) StrategyDecision {
|
||||
if r.RiskClass == "HIGH" || r.CostOfError >= 800 {
|
||||
return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true}
|
||||
}
|
||||
if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
|
||||
if eligible.RuleEligible && r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
|
||||
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
|
||||
}
|
||||
if r.PreferLowLatency {
|
||||
if eligible.ReflexEligible && r.PreferLowLatency {
|
||||
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
|
||||
}
|
||||
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
|
||||
if eligible.SkillEligible {
|
||||
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
|
||||
}
|
||||
return StrategyDecision{Path: PathSmallLocal, Reason: "no eligible reflex or skill; escalate to reasoning", Escalate: true}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const (
|
||||
ReflexActive ReflexLifecycle = "ACTIVE"
|
||||
ReflexDegraded ReflexLifecycle = "DEGRADED"
|
||||
ReflexStale ReflexLifecycle = "STALE"
|
||||
ReflexRevalidating ReflexLifecycle = "REVALIDATING"
|
||||
ReflexSuspended ReflexLifecycle = "SUSPENDED"
|
||||
ReflexRetired ReflexLifecycle = "RETIRED"
|
||||
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
|
||||
@@ -58,4 +59,19 @@ type ReflexRevision struct {
|
||||
LastValidatedAt int64
|
||||
Lifecycle ReflexLifecycle
|
||||
RecordedAt int64
|
||||
SupersedesRevision int64
|
||||
ExecutionCount int64
|
||||
FailureCount int64
|
||||
DivergenceCount int64
|
||||
}
|
||||
|
||||
// ShadowComparison is evidence produced by a bounded shadow sample. It is not
|
||||
// permission to rewrite a Reflex; SELF/governed evaluation decide next steps.
|
||||
type ShadowComparison struct {
|
||||
ReflexID ReflexID
|
||||
ReflexRevision int64
|
||||
EvidenceRefs []SkillEvidenceRef
|
||||
VerifierPassed bool
|
||||
Diverged bool
|
||||
ObservedAt int64
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ const (
|
||||
SkillCandidate SkillLifecycleStatus = "CANDIDATE"
|
||||
SkillEvaluating SkillLifecycleStatus = "EVALUATING"
|
||||
SkillValidated SkillLifecycleStatus = "VALIDATED"
|
||||
SkillActive SkillLifecycleStatus = "ACTIVE"
|
||||
SkillDegraded SkillLifecycleStatus = "DEGRADED"
|
||||
SkillSuspended SkillLifecycleStatus = "SUSPENDED"
|
||||
SkillRetired SkillLifecycleStatus = "RETIRED"
|
||||
@@ -202,6 +203,15 @@ type SkillProfile struct {
|
||||
ObservedFailureCount int64
|
||||
ObservedCorrectionCount int64
|
||||
RecordedAtUnixMillis int64
|
||||
// The fields below are the additive Gen2 lifecycle view. The older fields
|
||||
// remain a compatibility projection; they are not another Skill registry.
|
||||
Applicability ApplicabilitySpec
|
||||
DependencyRefs []string
|
||||
ValidationEvidenceRefs []SkillEvidenceRef
|
||||
IndependentEvaluationRefs []SkillEvidenceRef
|
||||
KnownFailureConditions []string
|
||||
VerifiedOutcomeCount int64
|
||||
MustNotWorsenSatisfied bool
|
||||
}
|
||||
|
||||
type SkillOutcomeObservation struct {
|
||||
|
||||
@@ -36,6 +36,14 @@ type ContextOmission struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ContextInclusion records the bounded compiler decision without exposing
|
||||
// source text. It is trace-only metadata and is never user-facing output.
|
||||
type ContextInclusion struct {
|
||||
ID ContextExcerptID
|
||||
Lane string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ContextBundle struct {
|
||||
Excerpts []ContextExcerpt
|
||||
Omissions []ContextOmission
|
||||
@@ -45,19 +53,60 @@ type ContextBundle struct {
|
||||
|
||||
type ContextAssemblyTrace struct {
|
||||
Included []ContextExcerptID
|
||||
Inclusions []ContextInclusion
|
||||
Excluded []ContextOmission
|
||||
Coverage string
|
||||
CompilerVersion string
|
||||
}
|
||||
|
||||
type IterativeRetrievalLane string
|
||||
|
||||
const (
|
||||
IterativeRetrievalCurrentFact IterativeRetrievalLane = "CURRENT_FACT"
|
||||
IterativeRetrievalPositiveExperience IterativeRetrievalLane = "POSITIVE_EXPERIENCE"
|
||||
IterativeRetrievalNegativeExperience IterativeRetrievalLane = "NEGATIVE_EXPERIENCE"
|
||||
IterativeRetrievalContradiction IterativeRetrievalLane = "CONTRADICTION"
|
||||
IterativeRetrievalHistoricalAnalogy IterativeRetrievalLane = "HISTORICAL_ANALOGY"
|
||||
)
|
||||
|
||||
// IterativeRetrievalRequest is the model-visible, storage-agnostic intent.
|
||||
// Principal, Scope, Focus and policy are deliberately absent: Core binds them
|
||||
// from the already-authorized request and never accepts them from a model.
|
||||
type IterativeRetrievalRequest struct {
|
||||
Query string
|
||||
RemainingBytes int
|
||||
Pass int
|
||||
Query string
|
||||
SemanticSubject string
|
||||
Lane IterativeRetrievalLane
|
||||
Reason string
|
||||
RemainingBytes int
|
||||
Pass int
|
||||
}
|
||||
|
||||
// IterativeRetrievalBudget is a Core-enforced per-request budget.
|
||||
type IterativeRetrievalBudget struct {
|
||||
MaxRounds int
|
||||
MaxFragments int
|
||||
MaxAddedBytes int
|
||||
MaxLatencyMillis int64
|
||||
}
|
||||
|
||||
// IterativeRetrievalEnvelope is the trusted continuation input assembled by
|
||||
// Core. It is never produced by a provider or model adapter.
|
||||
type IterativeRetrievalEnvelope struct {
|
||||
Request IterativeRetrievalRequest
|
||||
PrincipalID string
|
||||
ContextScopeRef string
|
||||
FocusFrameRef string
|
||||
TaskRef string
|
||||
FreshnessRequirement string
|
||||
DataPolicy capability.DataPolicy
|
||||
Budget IterativeRetrievalBudget
|
||||
SeenExcerptIDs []ContextExcerptID
|
||||
}
|
||||
type IterativeRetrievalResponse struct {
|
||||
Candidates []ContextCandidate
|
||||
Exhausted bool
|
||||
Reason string
|
||||
AddedBytes int
|
||||
}
|
||||
|
||||
// CompileContext deterministically selects permitted, relevant excerpts for a
|
||||
@@ -67,14 +116,9 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
return ContextBundle{}, ErrInvalidContextBudget
|
||||
}
|
||||
candidates := append([]ContextCandidate(nil), input.Candidates...)
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
if candidates[i].Relevance != candidates[j].Relevance {
|
||||
return candidates[i].Relevance > candidates[j].Relevance
|
||||
}
|
||||
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
|
||||
})
|
||||
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
|
||||
seen := make(map[ContextExcerptID]struct{}, len(candidates))
|
||||
eligible := make([]ContextCandidate, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
excerpt := candidate.Excerpt
|
||||
if _, ok := seen[excerpt.ID]; ok {
|
||||
@@ -86,8 +130,7 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
|
||||
continue
|
||||
}
|
||||
reason := permittedContextReason(excerpt, input.DataPolicy)
|
||||
if reason != "" {
|
||||
if reason := permittedContextReason(excerpt, input.DataPolicy); reason != "" {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
|
||||
continue
|
||||
}
|
||||
@@ -95,14 +138,45 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
|
||||
continue
|
||||
}
|
||||
eligible = append(eligible, candidate)
|
||||
}
|
||||
candidates = eligible
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
if candidates[i].Relevance != candidates[j].Relevance {
|
||||
return candidates[i].Relevance > candidates[j].Relevance
|
||||
}
|
||||
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
|
||||
})
|
||||
included := make(map[ContextExcerptID]struct{}, len(candidates))
|
||||
include := func(candidate ContextCandidate, reason string) bool {
|
||||
excerpt := candidate.Excerpt
|
||||
bytes := len([]byte(excerpt.Text))
|
||||
if bundle.Bytes+bytes > input.MaxBytes {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
|
||||
continue
|
||||
return false
|
||||
}
|
||||
bundle.Excerpts = append(bundle.Excerpts, excerpt)
|
||||
bundle.Bytes += bytes
|
||||
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
|
||||
bundle.Trace.Inclusions = append(bundle.Trace.Inclusions, ContextInclusion{ID: excerpt.ID, Lane: candidate.Lane, Reason: reason})
|
||||
included[excerpt.ID] = struct{}{}
|
||||
return true
|
||||
}
|
||||
// Preserve one candidate from each semantic lane before filling by score so
|
||||
// many similar current facts cannot erase a relevant failure or conflict.
|
||||
for _, lane := range []string{"current-fact", "positive-experience", "negative-experience", "contradiction", "historical-analogy", "interaction"} {
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Lane == lane && include(candidate, "lane_coverage") {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, ok := included[candidate.Excerpt.ID]; ok {
|
||||
continue
|
||||
}
|
||||
if !include(candidate, "salience") {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: candidate.Excerpt.ID, Reason: "budget"})
|
||||
}
|
||||
}
|
||||
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
|
||||
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
|
||||
|
||||
@@ -12,3 +12,19 @@ func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
|
||||
t.Fatalf("bundle=%#v err=%v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextCompilerPreservesNegativeLaneWithinBoundedBudget(t *testing.T) {
|
||||
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 27, DataPolicy: capability.DataPolicyLocalOnly, Candidates: []cognitive.ContextCandidate{
|
||||
{Excerpt: cognitive.ContextExcerpt{ID: "current", Text: "current-value", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 100},
|
||||
{Excerpt: cognitive.ContextExcerpt{ID: "failure", Text: "known-failure", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "negative-experience", Relevance: 1, Negative: true},
|
||||
{Excerpt: cognitive.ContextExcerpt{ID: "crowd", Text: "similar-current", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 99},
|
||||
}})
|
||||
if err != nil || len(b.Excerpts) != 2 || b.Excerpts[0].ID != "current" || b.Excerpts[1].ID != "failure" {
|
||||
t.Fatalf("bundle=%#v err=%v", b, err)
|
||||
}
|
||||
// The negative record fits and is retained before a lower-value crowd of
|
||||
// similar facts can consume the remaining bounded budget.
|
||||
if len(b.Trace.Inclusions) != 2 || b.Trace.Inclusions[0].Reason != "lane_coverage" || b.Trace.Inclusions[1].Reason != "lane_coverage" {
|
||||
t.Fatalf("trace=%#v", b.Trace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,18 +69,19 @@ type AdvisoryRequest struct {
|
||||
// GatewayAdvisoryRequest describes the task and policy requirement. It cannot
|
||||
// name a provider, model or binding; Capability owns that selection.
|
||||
type GatewayAdvisoryRequest struct {
|
||||
Version ContractVersion
|
||||
RequestID RequestID
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
Message string
|
||||
Context []ContextExcerpt
|
||||
Locale string
|
||||
MaxOutputBytes int
|
||||
DataPolicy capability.DataPolicy
|
||||
EgressAcknowledged bool
|
||||
Meaning TaskMeaning
|
||||
Version ContractVersion
|
||||
RequestID RequestID
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
Message string
|
||||
Context []ContextExcerpt
|
||||
Locale string
|
||||
MaxOutputBytes int
|
||||
DataPolicy capability.DataPolicy
|
||||
EgressAcknowledged bool
|
||||
AllowIterativeRetrieval bool
|
||||
Meaning TaskMeaning
|
||||
}
|
||||
|
||||
// TaskMeaning is the trusted semantic summary produced by Core. It is not a
|
||||
@@ -177,4 +178,5 @@ type GatewayAdvisoryResponse struct {
|
||||
ReferencedExcerptIDs []ContextExcerptID
|
||||
FinishReason FinishReason
|
||||
Usage *ProviderUsage
|
||||
IterativeRetrieval *IterativeRetrievalRequest
|
||||
}
|
||||
|
||||
@@ -56,6 +56,24 @@ type FreshnessAssessment struct {
|
||||
RevalidationAttemptRef string
|
||||
AssessedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// FreshnessRequirement is supplied by the current task/focus owner. It is an
|
||||
// evaluation input, not Epistemics-owned task state, so freshness remains
|
||||
// relative to the decision being made.
|
||||
type FreshnessRequirement struct {
|
||||
Mode string
|
||||
MaximumAgeMillis *int64
|
||||
RequiredVersionRefs []SemanticReference
|
||||
}
|
||||
|
||||
// WithExactVersionBinding is a typed contract helper for callers that need to
|
||||
// attach an exact environment/version applicability constraint without taking
|
||||
// a direct dependency on the World package.
|
||||
func WithExactVersionBinding(value TemporalApplicability, subject, version SemanticReference) TemporalApplicability {
|
||||
value.VersionBindings = append(value.VersionBindings, world.VersionBinding{SubjectRef: subject, VersionRef: version, MatchMode: world.VersionExact})
|
||||
return value
|
||||
}
|
||||
|
||||
type RevalidationRequest struct {
|
||||
ID string
|
||||
SourceKnowledgeRef string
|
||||
|
||||
@@ -102,6 +102,7 @@ type SystemExpectation struct {
|
||||
// state or grants authority.
|
||||
type ExperienceRecord struct {
|
||||
ID ExperienceRecordID
|
||||
PrincipalID string
|
||||
SourceKind ExperienceSourceKind
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
@@ -133,6 +134,15 @@ type ExperienceRecord struct {
|
||||
WhySucceededOrFailed string
|
||||
}
|
||||
|
||||
// ConsolidationCandidate is a rebuildable Epistemics projection. It preserves
|
||||
// source ExperienceRecord references and cannot activate a Skill by itself.
|
||||
type ConsolidationCandidate struct {
|
||||
EpisodeRef string
|
||||
ExperienceRefs []ExperienceRecordID
|
||||
PropositionText string
|
||||
SkillCandidate bool
|
||||
}
|
||||
|
||||
// ReflectionCandidate records that broader reflection may be warranted.
|
||||
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
|
||||
// produce candidates only; it cannot directly rewrite canonical state.
|
||||
|
||||
@@ -5,6 +5,14 @@ type CreatorCredentials struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type CreatorAccount struct {
|
||||
Username string
|
||||
ActorID ActorID
|
||||
PrincipalID PrincipalID
|
||||
PasswordHash string
|
||||
Revision int64
|
||||
}
|
||||
|
||||
type CreatorSession struct {
|
||||
ActorID ActorID
|
||||
PrincipalID PrincipalID
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
package self
|
||||
|
||||
type BenchmarkCaseRef string
|
||||
type BenchmarkCase struct {
|
||||
ID string
|
||||
PrincipalID string
|
||||
TaskClass string
|
||||
EvidenceRefs []SemanticReference
|
||||
RecordedAt int64
|
||||
}
|
||||
|
||||
func (c BenchmarkCase) Valid() bool {
|
||||
return c.ID != "" && c.PrincipalID != "" && c.TaskClass != "" && len(c.EvidenceRefs) > 0 && c.RecordedAt > 0
|
||||
}
|
||||
|
||||
type PersonalBenchmark struct {
|
||||
ID string
|
||||
PrincipalID string
|
||||
|
||||
+161
-9
@@ -6,6 +6,58 @@ type AssessmentID string
|
||||
type LearningLedgerEntryID string
|
||||
type AssessmentState string
|
||||
|
||||
type LearningSignalKind string
|
||||
type DiagnosisClass string
|
||||
type EpistemicStatus string
|
||||
type InterventionClass string
|
||||
type DecisionRegretKind string
|
||||
type CandidateState string
|
||||
|
||||
const (
|
||||
SignalVerifiedFailure LearningSignalKind = "VERIFIED_FAILURE"
|
||||
SignalUserCorrection LearningSignalKind = "USER_CORRECTION"
|
||||
SignalRepeatedMistake LearningSignalKind = "REPEATED_KNOWN_MISTAKE"
|
||||
SignalReflexDivergence LearningSignalKind = "REFLEX_DIVERGENCE"
|
||||
SignalSkillFallback LearningSignalKind = "SKILL_FALLBACK"
|
||||
SignalUnexpectedEscalation LearningSignalKind = "UNEXPECTED_ESCALATION"
|
||||
SignalOutcomeMismatch LearningSignalKind = "OUTCOME_MISMATCH"
|
||||
SignalRetryFallback LearningSignalKind = "RETRY_FALLBACK"
|
||||
SignalStaleDependency LearningSignalKind = "STALE_DEPENDENCY"
|
||||
SignalDecisionRegret LearningSignalKind = "DECISION_REGRET"
|
||||
SignalBetterSolution LearningSignalKind = "BETTER_SOLUTION"
|
||||
DiagnosisMemoryMissing DiagnosisClass = "MEMORY_MISSING"
|
||||
DiagnosisMemoryNotRetrieved DiagnosisClass = "MEMORY_NOT_RETRIEVED"
|
||||
DiagnosisWrongScope DiagnosisClass = "WRONG_SCOPE"
|
||||
DiagnosisStaleKnowledge DiagnosisClass = "STALE_KNOWLEDGE"
|
||||
DiagnosisContextSelection DiagnosisClass = "CONTEXT_SELECTION"
|
||||
DiagnosisReasoningFailure DiagnosisClass = "REASONING_FAILURE"
|
||||
DiagnosisSkillFailure DiagnosisClass = "SKILL_FAILURE"
|
||||
DiagnosisReflexFailure DiagnosisClass = "REFLEX_FAILURE"
|
||||
DiagnosisToolFailure DiagnosisClass = "TOOL_FAILURE"
|
||||
DiagnosisProviderFailure DiagnosisClass = "PROVIDER_FAILURE"
|
||||
DiagnosisOutcomeVerificationFailure DiagnosisClass = "OUTCOME_VERIFICATION_FAILURE"
|
||||
DiagnosisRoutingInefficiency DiagnosisClass = "ROUTING_INEFFICIENCY"
|
||||
DiagnosisUnknown DiagnosisClass = "UNKNOWN_CAUSE"
|
||||
EpistemicKnown EpistemicStatus = "KNOWN"
|
||||
EpistemicSupported EpistemicStatus = "SUPPORTED"
|
||||
EpistemicHypothesis EpistemicStatus = "HYPOTHESIS"
|
||||
EpistemicUnknown EpistemicStatus = "UNKNOWN"
|
||||
InterventionConfiguration InterventionClass = "CONFIGURATION"
|
||||
InterventionSkillReview InterventionClass = "SKILL_REVIEW"
|
||||
InterventionReflexReview InterventionClass = "REFLEX_REVIEW"
|
||||
InterventionRoutingReview InterventionClass = "ROUTING_REVIEW"
|
||||
InterventionProviderReview InterventionClass = "PROVIDER_REVIEW"
|
||||
InterventionContextReview InterventionClass = "CONTEXT_REVIEW"
|
||||
RegretUnavailableThen DecisionRegretKind = "INFORMATION_UNAVAILABLE_THEN"
|
||||
RegretAvailableUnused DecisionRegretKind = "INFORMATION_AVAILABLE_UNUSED"
|
||||
RegretIgnoredInContext DecisionRegretKind = "EVIDENCE_IGNORED_IN_CONTEXT"
|
||||
CandidateProposed CandidateState = "PROPOSED"
|
||||
CandidateEvaluating CandidateState = "EVALUATING"
|
||||
CandidateKept CandidateState = "KEPT"
|
||||
CandidateRejected CandidateState = "REJECTED"
|
||||
CandidateRolledBack CandidateState = "ROLLED_BACK"
|
||||
)
|
||||
|
||||
const (
|
||||
AssessmentCandidate AssessmentState = "CANDIDATE"
|
||||
AssessmentConfirmed AssessmentState = "CONFIRMED"
|
||||
@@ -37,6 +89,17 @@ type WeaknessPattern struct {
|
||||
DetectionEvidenceRefs []SemanticReference
|
||||
State AssessmentState
|
||||
RecordedAt int64
|
||||
TaskPattern string
|
||||
ScopeKey string
|
||||
EnvironmentVersion string
|
||||
DependencyRefs []SemanticReference
|
||||
Frequency int
|
||||
LastObservedAt int64
|
||||
Severity int
|
||||
AffectedComponent string
|
||||
AffectedStrategy string
|
||||
ConfidenceMillis int64
|
||||
EpistemicStatus EpistemicStatus
|
||||
}
|
||||
type DependencyAssessment struct {
|
||||
ID AssessmentID
|
||||
@@ -48,15 +111,83 @@ type DependencyAssessment struct {
|
||||
AssessedAt int64
|
||||
}
|
||||
type ImprovementNeed struct {
|
||||
ID AssessmentID
|
||||
PrincipalID string
|
||||
SubjectRef SemanticReference
|
||||
ProblemSummary string
|
||||
EvidenceRefs []SemanticReference
|
||||
ExpectedBenefit string
|
||||
Priority int
|
||||
State string
|
||||
CreatedAt int64
|
||||
ID AssessmentID
|
||||
PrincipalID string
|
||||
SubjectRef SemanticReference
|
||||
ProblemSummary string
|
||||
EvidenceRefs []SemanticReference
|
||||
ExpectedBenefit string
|
||||
Priority int
|
||||
State string
|
||||
CreatedAt int64
|
||||
Diagnosis DiagnosisClass
|
||||
DiagnosisStatus EpistemicStatus
|
||||
AffectedComponent string
|
||||
ScopeKey string
|
||||
Risk string
|
||||
Intervention InterventionClass
|
||||
Hypothesis string
|
||||
EvaluationPlan string
|
||||
MustNotWorsen string
|
||||
RollbackPlan string
|
||||
ResourceCost string
|
||||
}
|
||||
|
||||
type LearningSignal struct {
|
||||
ID string
|
||||
PrincipalID string
|
||||
Kind LearningSignalKind
|
||||
TaskPattern string
|
||||
ScopeKey string
|
||||
SubjectRef SemanticReference
|
||||
EvidenceRefs []SemanticReference
|
||||
ExpectedOutcome string
|
||||
ActualOutcome string
|
||||
DependencyRefs []SemanticReference
|
||||
EnvironmentVersion string
|
||||
ObservedAt int64
|
||||
Severity int
|
||||
UserImpact int
|
||||
CorrectionCount int
|
||||
DecisionRegret DecisionRegretKind
|
||||
}
|
||||
|
||||
type Diagnosis struct {
|
||||
Class DiagnosisClass
|
||||
Status EpistemicStatus
|
||||
Rationale string
|
||||
EvidenceRefs []SemanticReference
|
||||
ConfidenceMillis int64
|
||||
}
|
||||
|
||||
type ImprovementCandidate struct {
|
||||
ID string
|
||||
PrincipalID string
|
||||
NeedRef SemanticReference
|
||||
Hypothesis string
|
||||
ExpectedEffect string
|
||||
EvaluationPlan string
|
||||
MustNotWorsen []string
|
||||
RollbackPlan string
|
||||
ResourceCost string
|
||||
Creator string
|
||||
IndependentEvaluator string
|
||||
State CandidateState
|
||||
EvidenceRefs []SemanticReference
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
type ImprovementEvaluation struct {
|
||||
CandidateRef SemanticReference
|
||||
Evaluator string
|
||||
EvidenceRefs []SemanticReference
|
||||
ExpectedQuality int64
|
||||
ActualQuality int64
|
||||
ExpectedLatency int64
|
||||
ActualLatency int64
|
||||
MustNotWorsenPass bool
|
||||
Decision CandidateState
|
||||
EvaluatedAt int64
|
||||
}
|
||||
type LearningLedgerEntry struct {
|
||||
ID LearningLedgerEntryID
|
||||
@@ -90,3 +221,24 @@ func (l LearningLedgerEntry) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s LearningSignal) Validate() error {
|
||||
if s.ID == "" || s.PrincipalID == "" || s.Kind == "" || s.TaskPattern == "" || s.ScopeKey == "" || !s.SubjectRef.Valid() || len(s.EvidenceRefs) == 0 || s.ObservedAt < 1 {
|
||||
return fmt.Errorf("learning signal is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d Diagnosis) Validate() error {
|
||||
if d.Class == "" || d.Status == "" || len(d.EvidenceRefs) == 0 || d.ConfidenceMillis < 0 || d.ConfidenceMillis > 1000 {
|
||||
return fmt.Errorf("diagnosis requires bounded evidence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ImprovementCandidate) Validate() error {
|
||||
if c.ID == "" || c.PrincipalID == "" || !c.NeedRef.Valid() || c.Hypothesis == "" || c.ExpectedEffect == "" || c.EvaluationPlan == "" || c.MustNotWorsen == nil || c.RollbackPlan == "" || c.Creator == "" || c.IndependentEvaluator == "" || c.Creator == c.IndependentEvaluator || len(c.EvidenceRefs) == 0 || c.CreatedAt < 1 {
|
||||
return fmt.Errorf("candidate lacks independent bounded evaluation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Gen2 owner-role provisioning for the dedicated bootstrap/migration principal.
|
||||
-- This is intentionally separate from feature migrations and may be run repeatedly.
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'world_role') THEN
|
||||
CREATE ROLE world_role LOGIN;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'configuration_role') THEN
|
||||
CREATE ROLE configuration_role LOGIN;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'self_role') THEN
|
||||
CREATE ROLE self_role LOGIN;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
role_name TEXT;
|
||||
can_login BOOLEAN;
|
||||
BEGIN
|
||||
FOREACH role_name IN ARRAY ARRAY['world_role', 'configuration_role', 'self_role'] LOOP
|
||||
SELECT rolcanlogin INTO can_login FROM pg_roles WHERE rolname = role_name;
|
||||
IF can_login IS DISTINCT FROM TRUE THEN
|
||||
RAISE EXCEPTION 'Gen2 owner role % exists but is not a V1-compatible LOGIN role', role_name;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS world AUTHORIZATION world_role;
|
||||
CREATE SCHEMA IF NOT EXISTS configuration AUTHORIZATION configuration_role;
|
||||
CREATE SCHEMA IF NOT EXISTS self AUTHORIZATION self_role;
|
||||
|
||||
REVOKE ALL ON SCHEMA world, configuration, self FROM PUBLIC;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
schema_name TEXT;
|
||||
expected_owner TEXT;
|
||||
actual_owner TEXT;
|
||||
BEGIN
|
||||
FOREACH schema_name IN ARRAY ARRAY['world', 'configuration', 'self'] LOOP
|
||||
expected_owner := schema_name || '_role';
|
||||
SELECT pg_get_userbyid(nspowner)
|
||||
INTO actual_owner
|
||||
FROM pg_namespace
|
||||
WHERE nspname = schema_name;
|
||||
IF actual_owner IS DISTINCT FROM expected_owner THEN
|
||||
RAISE EXCEPTION 'Gen2 schema % is owned by %, expected %', schema_name, actual_owner, expected_owner;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,20 @@
|
||||
BEGIN;
|
||||
SET ROLE identity_role;
|
||||
CREATE TABLE IF NOT EXISTS identity.creator_accounts (
|
||||
username TEXT PRIMARY KEY,
|
||||
actor_id TEXT NOT NULL,
|
||||
principal_id TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL DEFAULT 1,
|
||||
password_changed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (username <> ''),
|
||||
CHECK (actor_id <> ''),
|
||||
CHECK (principal_id <> ''),
|
||||
CHECK (password_hash <> ''),
|
||||
CHECK (revision > 0),
|
||||
UNIQUE (actor_id, principal_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS creator_accounts_binding_idx ON identity.creator_accounts (actor_id, principal_id);
|
||||
REVOKE ALL ON identity.creator_accounts FROM PUBLIC;
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,17 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE epistemics_role;
|
||||
|
||||
ALTER TABLE epistemics.experience_records
|
||||
ADD COLUMN IF NOT EXISTS principal_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS context_scope_ref TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS applicability_conditions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN IF NOT EXISTS why_succeeded_or_failed TEXT NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS experience_records_principal_scope_recorded_idx
|
||||
ON epistemics.experience_records (principal_id, context_scope_ref, recorded_at_unix_millis DESC);
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,79 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
ALTER TABLE capability.skill_profiles
|
||||
ADD COLUMN IF NOT EXISTS applicability JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS dependency_refs TEXT[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS independent_evaluation_refs TEXT[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS known_failure_conditions TEXT[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS verified_outcome_count BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS must_not_worsen_satisfied BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE capability.skill_profile_heads DROP CONSTRAINT IF EXISTS skill_profile_heads_status_check;
|
||||
ALTER TABLE capability.skill_profile_heads
|
||||
ADD CONSTRAINT skill_profile_heads_status_check CHECK (status IN ('CANDIDATE','EVALUATING','VALIDATED','ACTIVE','DEGRADED','SUSPENDED','RETIRED'));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.reflex_revisions (
|
||||
reflex_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||
principal_id TEXT NOT NULL,
|
||||
source_skill_id TEXT NOT NULL,
|
||||
source_skill_revision BIGINT NOT NULL CHECK (source_skill_revision > 0),
|
||||
kind TEXT NOT NULL,
|
||||
applicability JSONB NOT NULL,
|
||||
implementation_ref TEXT NOT NULL,
|
||||
implementation_version TEXT NOT NULL DEFAULT '',
|
||||
artifact_digest TEXT NOT NULL DEFAULT '',
|
||||
dependency_refs TEXT[] NOT NULL DEFAULT '{}',
|
||||
validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}',
|
||||
known_failure_experience_refs TEXT[] NOT NULL DEFAULT '{}',
|
||||
shadow_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
revalidation_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
last_validated_at BIGINT NOT NULL DEFAULT 0,
|
||||
lifecycle TEXT NOT NULL,
|
||||
supersedes_revision BIGINT NOT NULL DEFAULT 0,
|
||||
execution_count BIGINT NOT NULL DEFAULT 0,
|
||||
failure_count BIGINT NOT NULL DEFAULT 0,
|
||||
divergence_count BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at BIGINT NOT NULL CHECK (recorded_at > 0),
|
||||
PRIMARY KEY (reflex_id, revision),
|
||||
CHECK (lifecycle IN ('CANDIDATE','VALIDATING','ACTIVE','DEGRADED','STALE','REVALIDATING','SUSPENDED','RETIRED'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS capability.reflex_heads (
|
||||
reflex_id TEXT PRIMARY KEY,
|
||||
revision BIGINT NOT NULL,
|
||||
lifecycle TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
GRANT SELECT, INSERT, UPDATE ON capability.reflex_revisions, capability.reflex_heads TO capability_role;
|
||||
|
||||
RESET ROLE;
|
||||
SET ROLE self_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS self.improvement_needs (
|
||||
improvement_need_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT NOT NULL,
|
||||
subject_ref JSONB NOT NULL,
|
||||
problem_summary TEXT NOT NULL,
|
||||
evidence_refs JSONB NOT NULL,
|
||||
expected_benefit TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL
|
||||
);
|
||||
ALTER TABLE self.learning_ledger
|
||||
ADD COLUMN IF NOT EXISTS improvement_need_ref JSONB,
|
||||
ADD COLUMN IF NOT EXISTS proposed_change_ref JSONB,
|
||||
ADD COLUMN IF NOT EXISTS expected_improvement TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS evaluation_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS development_gate_ref JSONB,
|
||||
ADD COLUMN IF NOT EXISTS before_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS after_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS side_effects JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS rollback_ref JSONB;
|
||||
GRANT SELECT, INSERT, UPDATE ON self.improvement_needs, self.learning_ledger TO self_role;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,74 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE self_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS self.learning_signals (
|
||||
signal_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
task_pattern TEXT NOT NULL,
|
||||
scope_key TEXT NOT NULL,
|
||||
subject_ref JSONB NOT NULL,
|
||||
evidence_refs JSONB NOT NULL,
|
||||
expected_outcome TEXT NOT NULL DEFAULT '',
|
||||
actual_outcome TEXT NOT NULL DEFAULT '',
|
||||
dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
environment_version TEXT NOT NULL DEFAULT '',
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
severity INTEGER NOT NULL DEFAULT 0,
|
||||
user_impact INTEGER NOT NULL DEFAULT 0,
|
||||
correction_count INTEGER NOT NULL DEFAULT 0,
|
||||
decision_regret TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS self.weakness_patterns (
|
||||
assessment_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||
pattern_key TEXT NOT NULL,
|
||||
task_pattern TEXT NOT NULL,
|
||||
scope_key TEXT NOT NULL,
|
||||
environment_version TEXT NOT NULL DEFAULT '',
|
||||
dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
failure_mode TEXT NOT NULL,
|
||||
detection_evidence_refs JSONB NOT NULL,
|
||||
frequency INTEGER NOT NULL,
|
||||
last_observed_at_unix_millis BIGINT NOT NULL,
|
||||
severity INTEGER NOT NULL,
|
||||
affected_component TEXT NOT NULL,
|
||||
affected_strategy TEXT NOT NULL DEFAULT '',
|
||||
confidence_millis BIGINT NOT NULL,
|
||||
epistemic_status TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS self.improvement_candidates (
|
||||
candidate_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT NOT NULL,
|
||||
need_ref JSONB NOT NULL,
|
||||
hypothesis TEXT NOT NULL,
|
||||
expected_effect TEXT NOT NULL,
|
||||
evaluation_plan TEXT NOT NULL,
|
||||
must_not_worsen JSONB NOT NULL,
|
||||
rollback_plan TEXT NOT NULL,
|
||||
resource_cost TEXT NOT NULL,
|
||||
creator TEXT NOT NULL,
|
||||
independent_evaluator TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
evidence_refs JSONB NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS self.personal_benchmark_cases (
|
||||
case_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT NOT NULL,
|
||||
task_class TEXT NOT NULL,
|
||||
evidence_refs JSONB NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE ON self.learning_signals, self.weakness_patterns, self.improvement_candidates, self.personal_benchmark_cases TO self_role;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -25,6 +25,16 @@ psql_exec() {
|
||||
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
|
||||
}
|
||||
|
||||
# Owner roles and schemas are provisioned by the dedicated bootstrap boundary,
|
||||
# before feature migrations run. This keeps role creation out of migrations and
|
||||
# makes existing PostgreSQL volumes converge idempotently as well.
|
||||
GEN2_BOOTSTRAP="$REPO_DIR/database/bootstrap/002_gen2_roles_and_schemas.sql"
|
||||
if [[ ! -f "$GEN2_BOOTSTRAP" ]]; then
|
||||
echo "migration runner: Gen2 bootstrap is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
psql_exec < "$GEN2_BOOTSTRAP"
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# 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}
|
||||
SELF_POSTGRES_USER: self_role
|
||||
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
|
||||
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
|
||||
@@ -65,20 +65,17 @@ services:
|
||||
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
|
||||
IDENTITY_POSTGRES_USER: identity_role
|
||||
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
|
||||
CREATOR_BOOTSTRAP_TOKEN_FILE: /run/secrets/creator_bootstrap_token
|
||||
CREATOR_SESSION_SIGNING_KEY_FILE: /run/secrets/creator_session_signing_key
|
||||
CREATOR_USERNAME: ${CREATOR_USERNAME}
|
||||
CREATOR_PASSWORD_FILE: /run/secrets/creator_password
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
CAPABILITY_POSTGRES_USER: capability_role
|
||||
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
|
||||
SELF_POSTGRES_USER: self_role
|
||||
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
|
||||
INTERACTION_POSTGRES_USER: interaction_role
|
||||
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}
|
||||
@@ -88,6 +85,11 @@ services:
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
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
|
||||
|
||||
agent-canary:
|
||||
profiles: ["agent-canary"]
|
||||
@@ -182,7 +184,6 @@ services:
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
@@ -202,12 +203,16 @@ services:
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_research_sources:/var/lib/research-sources:ro
|
||||
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
|
||||
|
||||
continuity-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
@@ -253,8 +258,10 @@ services:
|
||||
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-api:8080/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-}
|
||||
# The medium chat route is served by the authenticated local gateway.
|
||||
# llm-local-api is the embeddings service and must not be used for chat.
|
||||
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
|
||||
@@ -263,10 +270,17 @@ services:
|
||||
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
depends_on: [core-runtime, cognitive-lifeboat]
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
networks: [default, llm-local]
|
||||
depends_on:
|
||||
core-runtime: { condition: service_started }
|
||||
cognitive-lifeboat: { condition: service_healthy }
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
|
||||
cognitive-lifeboat:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
@@ -277,7 +291,7 @@ services:
|
||||
- --port
|
||||
- "8086"
|
||||
- --model
|
||||
- /models/Qwen3-8B-Q4_K_M.gguf
|
||||
- /models/Qwen3.8-27B-UD-Q2_K_XL.gguf
|
||||
- --alias
|
||||
- domovoy-lifeboat
|
||||
- --jinja
|
||||
@@ -299,6 +313,29 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
supervisor:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: supervisor, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8080"
|
||||
SUPERVISOR_CONTROL_ADDR: ":8095"
|
||||
SUPERVISOR_STATE_PATH: /var/lib/supervisor/release-state.json
|
||||
SUPERVISOR_STABLE_UPSTREAM: http://creator-web:8084
|
||||
SUPERVISOR_CANDIDATE_UPSTREAM: http://creator-web-candidate:8084
|
||||
SUPERVISOR_BASELINE_RELEASE: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
ports:
|
||||
- "${CREATOR_WEB_PORT:-18080}:8080"
|
||||
- "127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}:8095"
|
||||
depends_on:
|
||||
creator-web: { condition: service_healthy }
|
||||
volumes:
|
||||
- assistant_supervisor_state:/var/lib/supervisor
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
creator-web:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
@@ -310,8 +347,11 @@ services:
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
|
||||
AGENT_WORKER_URL: http://agent-worker:8090
|
||||
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
|
||||
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
|
||||
depends_on:
|
||||
core-runtime: { condition: service_started }
|
||||
continuity-runtime: { condition: service_started }
|
||||
commit-runtime: { condition: service_started }
|
||||
cognitive-worker: { condition: service_healthy }
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
|
||||
interval: 10s
|
||||
@@ -333,6 +373,7 @@ volumes:
|
||||
external: true
|
||||
assistant_research_sources:
|
||||
name: assistant_research_sources
|
||||
assistant_supervisor_state:
|
||||
|
||||
networks:
|
||||
llm-local:
|
||||
|
||||
@@ -4,5 +4,6 @@ LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
|
||||
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
|
||||
|
||||
COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql
|
||||
COPY database/bootstrap/002_gen2_roles_and_schemas.sql /docker-entrypoint-initdb.d/002_gen2_roles_and_schemas.sql
|
||||
COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh
|
||||
RUN chmod 0555 /docker-entrypoint-initdb.d/002_owner_passwords.sh
|
||||
|
||||
@@ -11,6 +11,7 @@ ARG SOURCE_REVISION=unknown
|
||||
LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
|
||||
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
|
||||
RUN addgroup -S assistant && adduser -S -G assistant assistant
|
||||
RUN mkdir -p /var/lib/supervisor && chown assistant:assistant /var/lib/supervisor
|
||||
USER assistant
|
||||
COPY --from=build /out/runtime /usr/local/bin/assistant-runtime
|
||||
ENTRYPOINT ["/usr/local/bin/assistant-runtime"]
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/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}"
|
||||
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}"
|
||||
|
||||
[[ "$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; }
|
||||
|
||||
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 "isolated candidate ReleaseSet failed; stable ReleaseSet rollback requested" >&2
|
||||
exit 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="${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 isolated candidate operation" >&2
|
||||
diff -u <(printf '%s\n' "$stable_ids_before") <(printf '%s\n' "$after") >&2 || true
|
||||
fail_rollback
|
||||
}
|
||||
}
|
||||
candidate_container() { printf '%s-%s-1' "$CANDIDATE_PROJECT" "$1"; }
|
||||
wait_healthy() {
|
||||
local container="$1" status="" 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
|
||||
sleep 2
|
||||
done
|
||||
[[ "$ready" == true ]] || fail_rollback
|
||||
}
|
||||
|
||||
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
|
||||
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 "isolated candidate ReleaseSet prepared slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release_json=$(cat <<JSON
|
||||
{"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","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-%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 "isolated candidate ReleaseSet promoted slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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"
|
||||
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
before=$(stable_ids)
|
||||
compose up -d --no-build --no-deps supervisor
|
||||
after=$(stable_ids)
|
||||
[[ "$before" == "$after" ]] || {
|
||||
echo "stable container IDs changed while updating supervisor" >&2
|
||||
diff -u <(printf '%s\n' "$before") <(printf '%s\n' "$after") >&2 || true
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$after"
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-025-multimodal-and-ambient-ingress-readiness-without-implementing-the-whole-.json","task_id":"GEN2-025","task_revision":1}
|
||||
{"task_file":"dev/tasks/GEN2-COGNITIVE-003-restore-local-medium-invocation.json","task_id":"GEN2-COGNITIVE-003","task_revision":4}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"task_id": "GEN2-028",
|
||||
"task_revision": 1,
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
@@ -16,7 +16,7 @@
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"cmd/**",
|
||||
"adapters/**",
|
||||
"adapters/postgres/**",
|
||||
"runtime/**",
|
||||
"platform/**",
|
||||
"docs/**",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"task_id": "GEN2-029",
|
||||
"task_revision": 1,
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
@@ -16,7 +16,7 @@
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"cmd/**",
|
||||
"adapters/**",
|
||||
"adapters/postgres/**",
|
||||
"runtime/**",
|
||||
"platform/**",
|
||||
"database/migrations/*.sql",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"task_id": "GEN2-030",
|
||||
"task_revision": 1,
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
@@ -20,7 +20,7 @@
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"cmd/**",
|
||||
"adapters/**",
|
||||
"adapters/postgres/**",
|
||||
"runtime/**",
|
||||
"platform/**",
|
||||
"tests/**"
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Restore one governed, usable cognitive route for household traffic, preferably Local MEDIUM, without bypassing Capability ownership or activation governance.",
|
||||
"requirement_ids": ["REQ-GEN2-COGNITIVE-001-QUALIFICATION", "REQ-GEN2-COGNITIVE-001-LIFECYCLE", "REQ-GEN2-COGNITIVE-001-ROUTING"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["capability", "cognitive-worker", "core-runtime", "configuration", "production deployment"],
|
||||
"paths": [
|
||||
"adapters/postgres/capability_repository.go",
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/core-runtime/**",
|
||||
"contracts/capability/**",
|
||||
"modules/capability/**",
|
||||
"deploy/**",
|
||||
"runtime/health/**",
|
||||
"tests/**",
|
||||
"docs/evidence/**",
|
||||
"docs/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-COGNITIVE-001-qualify-and-promote-local-medium.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "go.mod", "go.sum", ".gitea/workflows/**"]
|
||||
},
|
||||
"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": ["Gen2 cognitive registry", "Capability repository boundary", "bounded cognitive gateway"],
|
||||
"may_change": ["only typed qualification/selection wiring within the declared paths"]
|
||||
},
|
||||
"semantic_owner": "Capability owns provider, model and binding lifecycle; cognitive-worker produces bounded observations; Supervisor controls production release.",
|
||||
"canonical_state": ["existing capability cognitive provider/model/binding/evidence state"],
|
||||
"state_transitions": ["DISCOVERED -> qualification evidence -> VALIDATED -> ACTIVE only through existing governed promotion path", "non-working ACTIVE/VALIDATED binding -> governed deactivation or requalification"],
|
||||
"side_effects_and_privileges": ["No direct SQL semantic mutation.", "No manual activation without qualification and independent evaluation evidence.", "No provider call outside cognitive adapter/gateway boundaries.", "No production secret values in source, logs, evidence or commits."],
|
||||
"persistence_and_migrations": ["No schema change is authorized; reuse existing capability schema and repository owner."],
|
||||
"failure_retry_restart": ["Binding heads and lifecycle evidence remain durable and restart-safe.", "CPU/GPU unavailability must not block a usable Local MEDIUM route.", "Lifeboat remains fallback, not primary."],
|
||||
"assumptions": ["Existing PostgreSQL capability schema and authenticated core registry routes are available.", "Production deployment remains governed by the existing Supervisor/update flow.", "No database migration or new role is required."],
|
||||
"acceptance_scenarios": [
|
||||
"provider health and model discovery are observed through the governed worker path",
|
||||
"Local MEDIUM bounded qualification records durable evidence",
|
||||
"independent evaluation and activation promotion are accepted by existing Capability owner",
|
||||
"ordinary cognitive request selects ACTIVE/VALIDATED Local MEDIUM when CPU/GPU are unavailable",
|
||||
"production core and worker use the same qualified revision",
|
||||
"restart preserves route usability and no stale binding is selected"
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/postgres/...",
|
||||
"FAST: targeted PostgreSQL repository tests",
|
||||
"FAST: go vet ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/postgres/...",
|
||||
"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",
|
||||
"production: bounded qualification, governed promotion and cognitive invocation"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -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,79 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 4,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Restore and phase-trace bounded Local MEDIUM qualification and invocation without changing Capability ownership, Core household budget, or activation authority.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-LOCAL-MEDIUM-INVOCATION", "REQ-GEN2-COGNITIVE-003-QUALIFICATION-CONTROL-PLANE"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cognitive-worker provider invocation", "existing local OpenAI-compatible adapter", "targeted evidence"],
|
||||
"paths": [
|
||||
"cmd/cognitive-worker/main.go",
|
||||
"cmd/cognitive-worker/advisory.go",
|
||||
"cmd/cognitive-worker/gateway_registry.go",
|
||||
"cmd/cognitive-worker/model_qualification.go",
|
||||
"cmd/cognitive-worker/*_test.go",
|
||||
"adapters/models/openai_compatible_local.go",
|
||||
"adapters/models/openai_compatible_local_test.go",
|
||||
"cmd/core-runtime/learning_test.go",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-COGNITIVE-003-restore-local-medium-invocation.json",
|
||||
"docs/evidence/**"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"cmd/core-runtime/capability_chat.go",
|
||||
"cmd/core-runtime/capability_chat_test.go",
|
||||
"cmd/core-runtime/chat_orchestrator.go",
|
||||
"cmd/core-runtime/chat_orchestrator_test.go",
|
||||
"cmd/core-runtime/cognitive_interpretation.go",
|
||||
"cmd/core-runtime/cognitive_interpretation_test.go",
|
||||
"cmd/core-runtime/development_gate.go",
|
||||
"cmd/core-runtime/development_gate_test.go",
|
||||
"cmd/core-runtime/household_scenario_test.go",
|
||||
"cmd/core-runtime/human_response_gate_test.go",
|
||||
"cmd/core-runtime/information_request_test.go",
|
||||
"cmd/core-runtime/learning.go",
|
||||
"cmd/core-runtime/main.go",
|
||||
"cmd/core-runtime/main_test.go",
|
||||
"cmd/core-runtime/research_client.go",
|
||||
"cmd/core-runtime/research_client_test.go",
|
||||
"adapters/postgres/**",
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"architecture/**",
|
||||
"database/**",
|
||||
"security/**",
|
||||
"deploy/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"UI",
|
||||
"V1 retirement",
|
||||
"GEN2-EVOLVE-005",
|
||||
"Gen3/**"
|
||||
]
|
||||
},
|
||||
"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 advisory contract", "existing capability binding/profile metadata", "existing OpenAI-compatible local adapter"],
|
||||
"may_change": ["provider request normalization and typed immediate failure mapping only"]
|
||||
},
|
||||
"semantic_owner": "Cognitive gateway remains the sole owner of provider invocation and route selection.",
|
||||
"canonical_state": ["discovery is not invocation qualification", "qualification evidence is not activation", "optional provider parameters are capability-gated", "immediate provider rejection is not reported as timeout"],
|
||||
"state_transitions": ["binding selection -> normalized request -> provider invocation -> typed result", "discovery -> bounded qualification evidence -> independent capability evaluation -> governed canary/activation"],
|
||||
"side_effects_and_privileges": ["no secrets or authorization headers in evidence", "no direct database access", "no provider calls outside the existing gateway boundary"],
|
||||
"persistence_and_migrations": ["no schema change"],
|
||||
"failure_retry_restart": ["household invocation remains bounded by its existing 12-second Core budget", "qualification/revalidation is a separately bounded control-plane operation", "single-flight qualification per provider/model avoids duplicate work", "no timeout inflation; no infinite retry; preserve fail-closed/fallback semantics"],
|
||||
"assumptions": ["Local MEDIUM gateway remains the existing provider endpoint", "candidate slot B is available for isolated verification", "cmd/core-runtime/learning_test.go is a preserved carry-forward regression from the preceding runtime task"],
|
||||
"acceptance_scenarios": ["safe phase trace emits correlation IDs and durations without prompts, answers, credentials, secrets, or user memory", "minimal invocation completes", "ordinary and bounded-context invocation complete", "immediate provider 4xx maps to typed rejection without timeout", "qualification requires bounded invocation evidence", "duplicate qualification is bounded and deduplicated", "fresh validated binding is not synchronously requalified on an ordinary household request"],
|
||||
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "architecture checks", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION", "BLOCKED_BY_DEPLOYMENT_CONTROL"]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"task_id": "GEN2-COMPAT-OBS-001",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Добавить bounded redacted production telemetry для оставшихся V1 compatibility paths без изменения их поведения, owners или canonical storage.",
|
||||
"requirement_ids": ["REQ-GEN2-COMPAT-OBS-001-TELEMETRY", "REQ-GEN2-COMPAT-OBS-001-OBSERVATION", "REQ-GEN2-COMPAT-OBS-001-ROLLBACK"],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"platform/telemetry/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/research-runtime/**",
|
||||
"modules/interaction/domain/**",
|
||||
"modules/capability/domain/**",
|
||||
"adapters/postgres/**",
|
||||
"docs/**",
|
||||
"tests/**",
|
||||
"deploy/run-production-canary.sh",
|
||||
"deploy/update-supervisor.sh",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-COMPAT-OBS-001-production-compatibility-observation.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["security/**", "go.mod", "go.sum", "architecture/**", ".gitea/workflows/**", "database/**", "adapters/models/**"]
|
||||
},
|
||||
"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 runtime logs and capability usage observation mechanisms"],
|
||||
"may_change": ["telemetry event shape only; no semantic/domain contract change"]
|
||||
},
|
||||
"semantic_owner": "Migration/Governance; telemetry is an operational projection and never a canonical semantic owner.",
|
||||
"canonical_state": ["no change", "V1 compatibility behavior and Gen2 canonical writers remain unchanged"],
|
||||
"state_transitions": ["path invocation -> redacted telemetry event -> bounded aggregation/observation -> retirement classification"],
|
||||
"side_effects_and_privileges": ["stdout/stderr telemetry only; no external mutation, secret access, or user content logging"],
|
||||
"persistence_and_migrations": ["no schema or migration change; telemetry is emitted to existing process logs"],
|
||||
"failure_retry_restart": ["telemetry failure must never change or block the compatibility path", "rollback is code revert to the pre-instrumentation commit"],
|
||||
"assumptions": ["production Supervisor deployment is authorized by this stage", "observation window is bounded and redacted", "no credentials are read"],
|
||||
"acceptance_scenarios": [
|
||||
"COMPAT-OBS-001-SCN-01: each listed path has a typed redacted event shape",
|
||||
"COMPAT-OBS-001-SCN-02: events include invocation, component, read/write, reason, Gen2 availability, fallback/primary, outcome and safe correlation",
|
||||
"COMPAT-OBS-001-SCN-03: no event contains content, secrets, tokens or raw user identifiers",
|
||||
"COMPAT-OBS-001-SCN-04: FAST tests prove instrumentation is non-blocking and path behavior is unchanged",
|
||||
"COMPAT-OBS-001-SCN-05: deployed production smoke produces observable compatibility telemetry",
|
||||
"COMPAT-OBS-001-EXIT: production observation can classify each path ACTIVE_REQUIRED, ACTIVE_FALLBACK, SHADOW_ONLY, UNUSED or UNKNOWN without claiming retirement from absence of evidence"
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: targeted go test for changed runtime/domain/adapter 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 smoke: Supervisor candidate deployment and redacted log observation"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"task_id": "GEN2-DB-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Исправить provisioning Gen2 owner roles/schemas до применения migrations 046-050 без ослабления V1 DB isolation.",
|
||||
"requirement_ids": ["REQ-GEN2-DB-001-ROLES", "REQ-GEN2-DB-001-OWNERSHIP", "REQ-GEN2-DB-001-IDEMPOTENCY", "REQ-GEN2-DB-001-RECOVERY"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["database bootstrap", "deployment migration runner"],
|
||||
"paths": [
|
||||
"database/bootstrap/002_gen2_roles_and_schemas.sql",
|
||||
"deploy/apply-migrations.sh",
|
||||
"deploy/images/postgres.Dockerfile",
|
||||
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json"
|
||||
],
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"GEN2-028",
|
||||
"production-canary-before-provisioning",
|
||||
"feature-migration-role-creation",
|
||||
"cross-owner-grants",
|
||||
"new canonical owner",
|
||||
"production canary before provisioning"
|
||||
]
|
||||
},
|
||||
"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": ["V1 database ownership/ACL", "GEN2 canonical owner registry", "GEN2 execution contract"], "may_change": ["Bootstrap provisioning only; feature migration semantics are unchanged."]},
|
||||
"semantic_owner": "Dedicated PostgreSQL bootstrap/migration principal; semantic ownership remains World, Configuration, SELF and existing V1 owners.",
|
||||
"canonical_state": ["world schema owned by world_role", "configuration schema owned by configuration_role", "self schema owned by self_role", "no cross-owner canonical writes"],
|
||||
"state_transitions": ["missing Gen2 roles/schemas -> idempotently provisioned -> migrations 046-050 apply -> restart/recovery preserves schema and migration state"],
|
||||
"side_effects_and_privileges": ["Create only the three declared LOGIN owner roles when absent.", "Revoke PUBLIC schema privileges and fail closed on an unexpected schema owner.", "Do not invent or print credentials; do not grant cross-owner access."],
|
||||
"persistence_and_migrations": ["Provisioning runs in the dedicated bootstrap boundary before feature migrations.", "Migrations 046-050 remain unchanged and are applied by the existing migration history runner."],
|
||||
"failure_retry_restart": ["Provisioning is safe to run repeatedly.", "Unexpected existing role/schema ownership fails closed and requires an architecture decision.", "PostgreSQL restart/recovery must retain owner roles, schemas, ACL and migration history."],
|
||||
"assumptions": ["This is a user-authorized remediation task for the previously stopped GEN2-027 migration decision.", "No Gen2 runtime consumer currently requires a new database password; credentials are not invented by this task.", "GEN2-027 resumes only after provisioning and recovery evidence; GEN2-028 is not started."],
|
||||
"acceptance_scenarios": ["GEN2-DB-001-SCN-01: all roles required by migrations 046-050 are identified", "GEN2-DB-001-SCN-02: provisioning succeeds twice without duplicate objects", "GEN2-DB-001-SCN-03: migrations 046-050 apply in the controlled deployment path", "GEN2-DB-001-SCN-04: schema owners and PUBLIC ACL are confirmed", "GEN2-DB-001-SCN-05: PostgreSQL restart/recovery preserves the result", "GEN2-DB-001-EXIT: migration blocker is removed without weakening V1 isolation"],
|
||||
"required_tests": ["FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST whitespace: git diff --check gen2-integration --", "TARGETED: provisioning repeated twice, migrations 046-050, ACL/ownership verification and PostgreSQL restart/recovery"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"task_id": "GEN2-DEPLOY-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Развернуть независимый Supervisor control plane с durable release state, controlled traffic switching и доказуемым rollback перед GEN2-027.",
|
||||
"requirement_ids": ["REQ-GEN2-DEPLOY-001-SUPERVISOR", "REQ-GEN2-DEPLOY-001-TRAFFIC", "REQ-GEN2-DEPLOY-001-ROLLBACK", "REQ-GEN2-DEPLOY-001-LIFEBOAT"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["deployment control", "runtime supervisor", "runtime health"],
|
||||
"paths": [
|
||||
"deploy/**",
|
||||
"runtime/supervisor/**",
|
||||
"runtime/health/**",
|
||||
"cmd/supervisor/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-DEPLOY-001-supervisor-and-production-rollback.json",
|
||||
"docs/evidence/**"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["GEN2-028", "database/**", "security/**", "architecture/**", "Authority/Commit bypass", "cognitive-lifeboat as Supervisor"]
|
||||
},
|
||||
"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": ["GEN2-027 production canary contract", "runtime/supervisor durable state", "existing deployment update controller"], "may_change": ["deployment topology, Supervisor HTTP control contract and traffic routing only"]},
|
||||
"semantic_owner": "Operations/Supervisor; not a semantic owner and not a cognitive provider.",
|
||||
"canonical_state": ["durable release/current/last-known-good evidence owned by Supervisor control plane", "traffic target is Supervisor-owned operational state"],
|
||||
"state_transitions": ["baseline -> candidate -> observed healthy -> header canary -> promoted OR independent rollback to last-known-good"],
|
||||
"side_effects_and_privileges": ["Control API is bound only to the production-internal loopback port.", "Supervisor has no database, Authority or Commit credentials.", "Candidate traffic is explicit and rollback remains available."],
|
||||
"persistence_and_migrations": ["No database changes; durable state is stored in a dedicated named volume outside candidate containers."],
|
||||
"failure_retry_restart": ["Supervisor restart reloads release and traffic state from its volume.", "Candidate failure must not erase last-known-good or block stable traffic rollback.", "Lifeboat model failure remains a separately reported degraded provider condition."],
|
||||
"assumptions": ["GEN2-027 resumes only after this task's production evidence; GEN2-028 is not started.", "The existing public ingress forwards to the configured creator web host port."],
|
||||
"acceptance_scenarios": ["GEN2-DEPLOY-001-SCN-01: Supervisor is a separate healthy production container", "GEN2-DEPLOY-001-SCN-02: release state survives Supervisor restart", "GEN2-DEPLOY-001-SCN-03: canary header reaches candidate while default traffic stays stable", "GEN2-DEPLOY-001-SCN-04: candidate failure rolls traffic back to last-known-good", "GEN2-DEPLOY-001-SCN-05: cognitive lifeboat diagnosis and degraded role are explicit", "GEN2-DEPLOY-001-EXIT: GEN2-027 rollback prerequisites are proven"],
|
||||
"required_tests": ["FAST: go test ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST: go vet ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST governance: preflight", "FAST scope: check-diff", "FAST independent: independent", "FAST whitespace: git diff --check", "TARGETED recovery: Supervisor restart, canary and rollback on docker.cin.su"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "GEN2-DEPLOY-002",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Расширить существующий Supervisor до безопасного управления единым candidate/stable ReleaseSet из creator-web, core-runtime, cognitive-worker и research-runtime без изменения Gen2 semantic ownership.",
|
||||
"requirement_ids": ["REQ-GEN2-DEPLOY-002-RELEASE-SET", "REQ-GEN2-DEPLOY-002-CANDIDATE-TOPOLOGY", "REQ-GEN2-DEPLOY-002-ROLLBACK", "REQ-GEN2-DEPLOY-002-COMPAT-OBS"],
|
||||
"allowed_change_scope": {"modules": ["runtime/supervisor"], "paths": ["runtime/supervisor/**", "cmd/supervisor/**", "deploy/**", "docs/**", "tests/**", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-DEPLOY-002-supervised-release-set.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "modules/**", "contracts/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "adapters/models/**"]},
|
||||
"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 state and production compose", "existing canary header and stable rollback controls"], "may_change": ["Supervisor release control payloads", "deployment topology and candidate routing configuration"]},
|
||||
"semantic_owner": "Supervisor/deployment control plane; ReleaseSet is operational release state and not semantic canonical state.",
|
||||
"canonical_state": ["no semantic owner or schema change", "previous stable release and evidence remain retained"],
|
||||
"state_transitions": ["candidate ReleaseSet -> component health/revision checks -> canary -> observation -> promotion or whole-set rollback"],
|
||||
"side_effects_and_privileges": ["production deployment only through approved control plane; no direct stable replacement; no credential reads"],
|
||||
"persistence_and_migrations": ["durable Supervisor release state remains in existing external Supervisor volume; no database migration"],
|
||||
"assumptions": ["the existing production Supervisor and compose control plane remain authoritative for deployment", "candidate services may share existing durable infrastructure only under current ownership and compatibility rules", "no credentials are read by this task"],
|
||||
"failure_retry_restart": ["candidate component failure routes all traffic to previous stable ReleaseSet", "Supervisor restart reloads ReleaseSet state", "previous stable is retained until promotion is complete"],
|
||||
"acceptance_scenarios": ["DEPLOY-002-SCN-01: complete ReleaseSet has all four components and one revision", "DEPLOY-002-SCN-02: candidate dependencies resolve only to candidate services", "DEPLOY-002-SCN-03: health/readiness/revision checks cover all required components", "DEPLOY-002-SCN-04: canary and stable paths are isolated", "DEPLOY-002-SCN-05: forced candidate failure rolls back the complete ReleaseSet", "DEPLOY-002-SCN-06: Supervisor restart preserves stable/candidate/LKG state", "DEPLOY-002-SCN-07: production telemetry revision starts bounded observation after controlled deployment"],
|
||||
"required_tests": ["go test ./runtime/supervisor ./cmd/supervisor", "FAST: targeted supervisor/release-set tests", "FAST: compose config validation", "FAST: routing and revision consistency tests", "FAST: rollback/restart persistence tests", "FAST: targeted go vet", "FAST governance: preflight/check-diff/independent", "FAST whitespace: git diff --check", "PRODUCTION: candidate ReleaseSet drill and telemetry observation start"],
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"task_id": "GEN2-EVOLVE-001",
|
||||
"task_revision": 3,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Extend the existing Gen2 retrieval fabric with task-relative freshness, bounded salience, scoped positive/negative experience, contradiction candidates and rebuildable consolidation candidates without creating a Memory owner or retiring V1 compatibility.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-001-ADAPTIVE-RETRIEVAL", "REQ-GEN2-EVOLVE-001-EXPERIENCE", "REQ-GEN2-EVOLVE-001-CONSOLIDATION"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["interaction", "epistemics", "core-runtime", "cognitive-context", "production evidence"],
|
||||
"paths": [
|
||||
"modules/interaction/domain/gen2_context.go",
|
||||
"modules/interaction/domain/gen2_context_test.go",
|
||||
"modules/epistemics/domain/freshness.go",
|
||||
"modules/epistemics/domain/freshness_test.go",
|
||||
"modules/epistemics/domain/experience.go",
|
||||
"modules/epistemics/domain/experience_test.go",
|
||||
"modules/epistemics/domain/adaptive_retrieval.go",
|
||||
"modules/epistemics/domain/adaptive_retrieval_test.go",
|
||||
"modules/epistemics/domain/consolidation.go",
|
||||
"modules/epistemics/domain/consolidation_test.go",
|
||||
"contracts/epistemics/freshness.go",
|
||||
"contracts/epistemics/types.go",
|
||||
"contracts/cognitive/context_compiler.go",
|
||||
"contracts/cognitive/context_compiler_v2_test.go",
|
||||
"adapters/postgres/epistemics_repository.go",
|
||||
"adapters/postgres/interaction_repository.go",
|
||||
"cmd/core-runtime/main.go",
|
||||
"cmd/core-runtime/learning.go",
|
||||
"cmd/core-runtime/cognitive_interpretation.go",
|
||||
"cmd/core-runtime/cognitive_interpretation_test.go",
|
||||
"database/migrations/053_gen2_adaptive_experience_scope.sql",
|
||||
"development/GEN2_MIGRATION_MAP.md",
|
||||
"docs/evidence/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "cmd/supervisor/**", "deploy/**", "GEN2-028", "GEN2-029", "GEN2-030", "PersonalMemory write paths", "Gen3/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {
|
||||
"consume": ["existing Interaction utterance and ProjectReference projection", "existing Epistemics ExperienceRecord and Freshness contracts", "existing Cognitive Context Compiler V2"],
|
||||
"may_change": ["typed Epistemics retrieval and temporal fields", "typed Context Compiler lane and omission metadata", "no public or cross-owner contract"]
|
||||
},
|
||||
"semantic_owner": "Interaction remains canonical owner of human utterances and ProjectReference; Epistemics remains canonical owner of ExperienceRecord and task-relative freshness evidence; Context Compiler remains a rebuildable cognitive projection; Core only composes existing owner reads through established repository boundaries.",
|
||||
"canonical_state": ["Interaction utterance history and ProjectReference", "Epistemics ExperienceRecord and temporal applicability", "no new Memory canonical state", "no direct cross-owner SQL join or write"],
|
||||
"persistence_and_migrations": ["add only owner-local Epistemics columns for principal and ContextScope references required to retrieve ExperienceRecord without cross-owner joins", "existing rows remain historical and are not deleted", "migration must be additive and rollback-safe", "migration number 053 is assigned because global migrations 051 and 052 already exist on the task base; no existing migration is changed", "cmd/core-runtime/learning.go is included only to persist the existing authenticated principal into the additive Epistemics metadata written by this task"],
|
||||
"side_effects_and_privileges": ["no external mutation", "no provider override", "no autonomous Skill or Reflex activation", "no scheduler", "no V1 retirement", "no browser acceptance required"],
|
||||
"state_transitions": [
|
||||
"task-relative freshness uses observed/verified/validity/volatility/version/provenance inputs and never turns a stale assessment into permanent deletion",
|
||||
"current versus historical assertion resolution preserves historical lineage and evaluates applicability before recency",
|
||||
"ProjectReference and principal filtering precede semantic relevance ranking",
|
||||
"Epistemics positive and negative ExperienceRecord retrieval is separately lane-classified and preserves failure applicability conditions",
|
||||
"bounded contradiction candidates are retrieved after the primary candidates without confirmation-only selection",
|
||||
"Context Compiler receives bounded current-fact, experience, contradiction and historical-analogy lanes with explainable inclusions and omissions",
|
||||
"consolidation is a deterministic rebuildable projection from raw records to episode/proposition/experience/Skill-candidate evidence; it does not write or activate Skills"
|
||||
],
|
||||
"failure_retry_restart": ["bounded retrieval never loops", "missing or old Experience metadata remains historical or excluded rather than treated as current", "rebuild/restart recomputes projections from canonical utterance and Epistemics evidence", "no candidate failure changes stable production traffic"],
|
||||
"assumptions": ["GEN2-COGNITIVE-002 scoped Interaction retrieval remains the primary assertion reader", "Epistemics owns additive ExperienceRecord metadata", "COMPAT observation and V1 retirement remain deferred by owner", "no browser or production promotion is required for this task"],
|
||||
"acceptance_scenarios": [
|
||||
"EVOLVE-001-SCN-01 current versus historical value returns the applicable current value while retaining historical evidence",
|
||||
"EVOLVE-001-SCN-02 fast-aging and slow-aging inputs receive distinct task-relative freshness assessments",
|
||||
"EVOLVE-001-SCN-03 Company A and Company B are hard-isolated through typed scope references before ranking",
|
||||
"EVOLVE-001-SCN-04 relevant memory is selected and irrelevant memory is excluded under a bounded context budget with an explainable omission",
|
||||
"EVOLVE-001-SCN-05 similar work retrieves both successful and failed applicable ExperienceRecord lanes",
|
||||
"EVOLVE-001-SCN-06 contradiction retrieval emits a bounded opposing candidate without replacing canonical evidence",
|
||||
"EVOLVE-001-SCN-07 rebuildable consolidation retains source evidence references and survives repository reload without raw-evidence loss"
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: targeted go test and go vet for changed interaction, epistemics, cognitive context, core-runtime and postgres packages",
|
||||
"FAST: devctl preflight, check-diff, independent governance, git diff --check",
|
||||
"MILESTONE: changed owners plus migration consistency and focused restart/rebuild retrieval check; no browser or FULL suite"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "GEN2-EVOLVE-002",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Wire one bounded cognitive memory-continuation round through the existing Interaction/Epistemics retrieval fabric and Context Compiler without a new Memory owner, V1 retirement, UI change, direct model storage access or an unrestricted memory tool.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-002-ITERATIVE-RETRIEVAL"],
|
||||
"allowed_change_scope": {"modules": ["cognitive contracts", "core-runtime", "cognitive-worker", "Gen2 governance and evidence"], "paths": ["contracts/cognitive/context_compiler.go", "contracts/cognitive/context_compiler_v2_test.go", "contracts/cognitive/types.go", "cmd/core-runtime/cognitive_interpretation.go", "cmd/core-runtime/cognitive_interpretation_test.go", "cmd/core-runtime/main.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-002-iterative-retrieval-and-cognitive-memory-continuation.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "database/**", "deploy/**", "cmd/supervisor/**", "cmd/creator-web/**", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]},
|
||||
"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 Interaction and Epistemics retrieval projections", "existing Context Compiler V2"], "may_change": ["Cognitive iterative-retrieval request, inherited continuation envelope and advisory response fields only"]},
|
||||
"semantic_owner": "Interaction remains canonical owner of utterance assertions and ProjectReference; Epistemics remains canonical owner of experience. Core validates and binds continuation identity/scope/policy before calling existing owner reads; Context Compiler remains the only context selector; cognitive-worker may emit only a typed retrieval intent and never reads storage.",
|
||||
"canonical_state": ["no new state is written", "no new Memory owner", "no direct model or cognitive-worker storage access", "no cross-owner SQL join"],
|
||||
"persistence_and_migrations": ["no schema or migration change"],
|
||||
"side_effects_and_privileges": ["no external mutation", "no provider override", "no UI/browser change", "no V1 retirement"],
|
||||
"state_transitions": ["initial Context Compiler context -> cognitive-worker may emit a typed retrieval intent -> Core binds inherited Principal/Scope/Focus/task/freshness/policy -> existing owner retrieval hard-filters scope before ranking -> Context Compiler compiles unique bounded additions -> one continuation advisory request", "no result -> explicit no-additional-evidence continuation", "contradiction or negative experience is lane-classified rather than suppressed"],
|
||||
"failure_retry_restart": ["one bounded continuation round only", "round/bytes/fragments/latency and duplicate budgets fail closed", "missing evidence remains unknown", "continuation state is request-scoped; durable Work/Investigation remains responsible for any restartable task"],
|
||||
"assumptions": ["the task branch starts at gen2-integration 850fe92", "UI/browser acceptance remains deferred by owner", "the traced existing IterativeRetrievalRequest lacks the required structured lane/reason and inherited runtime envelope, so this revision changes only those Cognitive contract fields"],
|
||||
"acceptance_scenarios": ["EVOLVE-002-SCN-01 insufficient initial context can obtain one additional current fact", "EVOLVE-002-SCN-02 negative and contradiction lanes are separately requested", "EVOLVE-002-SCN-03 inherited Company A scope rejects Company B regardless of request", "EVOLVE-002-SCN-04 duplicate excerpts and rounds/byte limits are rejected", "EVOLVE-002-SCN-05 no result preserves an explicit unknown/no-additional-evidence state"],
|
||||
"required_tests": ["FAST: targeted go test and go vet for changed cognitive contract, core-runtime and cognitive-worker packages", "FAST: devctl preflight, check-diff, independent governance, git diff --check", "MILESTONE: changed owner/runtime tests plus focused bounded continuation and restart-state check; no browser or FULL suite"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"task_id": "GEN2-EVOLVE-003",
|
||||
"task_revision": 3,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement the bounded evidence-governed Experience to Skill to Reflex learning lifecycle using existing Epistemics, Capability and SELF owners: candidate, independent validation, applicable routing, bounded shadow/revalidation, downgrade/retirement and Learning Ledger evidence, without autonomous production mutation.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-003-EXPERIENCE-SKILL-REFLEX"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["capability", "self", "core-runtime learning", "Gen2 evidence and governance"],
|
||||
"paths": [
|
||||
"contracts/capability/skill_v2.go", "contracts/capability/reflex_v2.go", "contracts/capability/resolution.go", "contracts/capability/intelligence.go", "contracts/self/types.go",
|
||||
"modules/capability/domain/skill.go", "modules/capability/domain/skill_test.go", "modules/capability/domain/reflex.go", "modules/capability/domain/reflex_test.go", "modules/capability/domain/intelligence_test.go",
|
||||
"modules/self/domain/self.go", "modules/self/domain/self_test.go",
|
||||
"adapters/postgres/capability_repository.go", "adapters/postgres/self_repository.go", "adapters/postgres/capability_repository_test.go", "adapters/postgres/self_repository_test.go",
|
||||
"database/migrations/054_experience_skill_reflex_learning_lifecycle.sql",
|
||||
"cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go",
|
||||
"dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/epistemics/**", "adapters/postgres/epistemics_repository.go", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {
|
||||
"consume": ["existing Epistemics ExperienceRecord/ReflectionCandidate", "existing Capability Skill V2/Reflex V2", "existing SELF LearningLedger/ImprovementNeed", "existing Intelligence Strategy"],
|
||||
"may_change": ["Capability and SELF typed lifecycle/evidence fields necessary for bounded candidate, validation, routing and revalidation semantics only"]
|
||||
},
|
||||
"semantic_owner": "Epistemics remains canonical owner of Experience and Outcome evidence; Capability remains sole canonical owner of Skill and Reflex lifecycle; SELF remains canonical owner of ImprovementNeed, competence/weakness assessments and Learning Ledger. Core Runtime only records evidence through owner repositories and never self-promotes production behavior.",
|
||||
"canonical_state": ["Experience/Outcome evidence is referenced, never copied as a new source of truth", "Capability writes Skill/Reflex revisions and heads; SELF writes Learning Ledger and ImprovementNeed", "candidate, validation, shadow and revalidation never grant independent production mutation authority", "no new Memory owner, parallel Skill registry or cross-owner SQL join"],
|
||||
"side_effects_and_privileges": ["No external mutation may bypass Authority -> Commit -> Executor -> verified Outcome.", "SELF may observe, evaluate and propose only; production promotion remains behind the existing governed development gates and independent evaluation.", "No provider invocation, privilege expansion, UI/browser change or V1 retirement is authorized."],
|
||||
"persistence_and_migrations": ["054 is the next verified global migration number after 053; it is additive and uses existing capability_role and self_role only", "revision 3 sets devctl's database-surface compatibility delta to one solely because devctl requires it for any migration; no database owner role is created", "persist Skill/Reflex revision/head and SELF learning evidence needed for restart-safe lifecycle state; existing historical records remain untouched", "rollback disables/refuses non-active Reflex selection and returns to existing Skill/reasoning path without deleting evidence"],
|
||||
"state_transitions": ["verified positive Experience with a bounded pattern and provenance -> Candidate Skill", "independent validation/replay evidence and must-not-worsen criteria -> Validated Skill -> Active eligible routing", "frequent stable Active Skill with deterministic bounded implementation and verifier -> Candidate Reflex -> Validated/Active Reflex", "dependency change, stale evidence, correction, failure trend or shadow divergence -> Stale/Suspicious -> revalidation request -> Active, rewrite proposal or Retired", "Reflex decline/failure -> Skill -> reasoning; no execution can bypass Authority/Commit/Executor"],
|
||||
"failure_retry_restart": ["one success never promotes Candidate directly to Active Skill or Reflex", "shadow is sampled/bounded and never delays ordinary low-risk response; divergence records evidence and ImprovementNeed rather than rewriting Reflex", "revalidation is bounded and never retries endlessly; real failures remain fail-closed and escalate", "durable revisions, validation evidence and Ledger survive restart; no direct provider invocation or process-local canonical state"],
|
||||
"assumptions": ["task branch starts at gen2-integration a07aba0", "UI/browser acceptance remains deferred by owner; V1 compatibility remains retained", "a verified inspection found 054 free, existing SkillProfile persistence, Reflex V2 contract without durable lifecycle persistence, and SELF typed ledger without a repository", "revision 2 adds only contracts/capability/resolution.go because the existing canonical SkillProfile is declared there and its durable evidence fields must be additive rather than creating a parallel Skill registry", "revision 3 satisfies devctl's migration compatibility gate without creating any role or privilege", "automatic arbitrary production code generation is not in scope; candidate executable strategies must use an existing bounded implementation reference and governed development remains the only production-promotion route"],
|
||||
"acceptance_scenarios": ["EVOLVE-003-SCN-01 verified outcome and pattern create Candidate Skill with source Experience/Outcome references", "EVOLVE-003-SCN-02 independent replay/evaluation evidence promotes Skill only after repeatability and must-not-worsen checks", "EVOLVE-003-SCN-03 applicable fresh Active Skill is routed; inapplicable, stale or weak Skill escalates to reasoning", "EVOLVE-003-SCN-04 sufficient stable evidence creates Candidate Reflex; verified bounded Reflex becomes Active and retains fallback", "EVOLVE-003-SCN-05 dependency change, correction, failure trend and stale validation trigger bounded revalidation/downgrade rather than silent use", "EVOLVE-003-SCN-06 sampled shadow divergence records ImprovementNeed/LearningLedger evidence and does not rewrite production Reflex", "EVOLVE-003-SCN-07 user correction links the affected Skill/Reflex revision and preserves conditional known failure evidence", "EVOLVE-003-SCN-08 retired Reflex is excluded from routing while all evidence/history remains queryable"],
|
||||
"required_tests": ["FAST: targeted go test and go vet for changed capability, self, core-runtime and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused lifecycle/persistence/restart and escalation tests only; no browser, production canary or FULL suite"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "GEN2-EVOLVE-004-RUNTIME-001",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Wire the existing bounded SELF learning loop into the Core composition root without creating a new owner or bypassing existing ports.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-SELF-RUNTIME-WIRING"],
|
||||
"allowed_change_scope": {"modules": ["core-runtime learning composition", "SELF persistence integration", "production secret reference wiring", "targeted evidence"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go", "adapters/postgres/self_repository.go", "adapters/postgres/self_repository_test.go", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "docs/CREDENTIALS_AND_SECRETS.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-004-RUNTIME-001-wire-self-learning-loop.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "cmd/cognitive-worker/**", "modules/**", "adapters/models/**", "database/**", "deploy/caddy/**", "deploy/images/**", "deploy/windows/**", "deploy/PORTAINER.md", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]},
|
||||
"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 learningRecorder", "existing SELF repository methods", "existing SELF domain contracts"], "may_change": ["Core composition wiring only"]},
|
||||
"semantic_owner": "SELF remains the sole owner of derived learning state; Core only coordinates evidence recording.",
|
||||
"canonical_state": ["SELF write failures never replace a completed user response with an error", "no direct database access outside SelfRepository", "no automatic promotion or production mutation"],
|
||||
"state_transitions": ["learning signal -> SelfRepository -> bounded derived evidence -> ledger"],
|
||||
"side_effects_and_privileges": ["use existing self_role repository boundary; do not read or print secrets", "the positive new_privileges value is only the devctl deployment-surface compatibility gate; no privilege is granted and self_role grants are unchanged"],
|
||||
"persistence_and_migrations": ["no schema change; consume migration 055 tables"],
|
||||
"failure_retry_restart": ["recording is best-effort after response completion and durable when repository is available; restart reads remain repository-owned"],
|
||||
"assumptions": ["existing external assistant_core_secrets control plane can materialize self_db_password without exposing its value", "existing production database has migration 055"],
|
||||
"acceptance_scenarios": ["correction/failure reaches SelfRepository", "repository failure is diagnostic and non-fatal", "durable evidence remains available after repository reopen"],
|
||||
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "git diff --check", "focused MILESTONE"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"task_id": "GEN2-EVOLVE-004",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement a bounded evidence-only SELF learning loop: observed signals -> contextual weakness diagnosis -> ImprovementNeed -> candidate proposal -> independent evaluation plan -> Learning Ledger, without autonomous code generation, production promotion or a new owner.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-SELF-IMPROVEMENT-LOOP"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["self", "self benchmark", "core-runtime learning evidence", "Gen2 evidence and governance"],
|
||||
"paths": [
|
||||
"contracts/self/types.go",
|
||||
"contracts/self/benchmark.go",
|
||||
"modules/self/domain/self.go",
|
||||
"modules/self/domain/self_test.go",
|
||||
"adapters/postgres/self_repository.go",
|
||||
"adapters/postgres/self_repository_test.go",
|
||||
"database/migrations/055_self_weakness_diagnosis_and_benchmark.sql",
|
||||
"cmd/core-runtime/learning.go",
|
||||
"cmd/core-runtime/learning_test.go",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json",
|
||||
"docs/evidence/**",
|
||||
"docs/PROJECT_STATUS.md"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/capability/**", "modules/epistemics/**", "database/migrations/001-054", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {"consume": ["existing Epistemics Experience/Outcome evidence", "existing Capability Skill/Reflex references", "existing SELF CompetenceAssessment/WeaknessPattern/ImprovementNeed/LearningLedger/PersonalBenchmark", "existing Work governed-evolution gates"], "may_change": ["SELF diagnosis, bounded candidate-proposal and benchmark case typed fields only"]},
|
||||
"semantic_owner": "SELF remains the sole owner of derived competence, weakness, ImprovementNeed, PersonalBenchmark and Learning Ledger state. Epistemics remains owner of source evidence; Capability remains owner of Skill/Reflex; Work/development gates remain owner of executable candidate and promotion.",
|
||||
"canonical_state": ["diagnosis is evidence-backed and may remain HYPOTHESIS/UNKNOWN", "repeated incidents are required for WeaknessPattern", "SELF observes/evaluates/proposes but cannot promote or mutate production", "benchmark cases reference evidence and do not copy conversations or secrets", "no new Memory owner, Skill registry, orchestrator or canonical writer"],
|
||||
"side_effects_and_privileges": ["No external mutation bypasses Authority -> Commit -> Executor -> verified Outcome.", "No autonomous code generation, merge, deployment, production promotion or provider invocation is authorized.", "Data is scoped by Principal/ContextScope and privacy metadata; hidden reasoning and user data are not stored."],
|
||||
"persistence_and_migrations": ["055 is the next free global migration after 054 and is additive under existing self_role only", "the positive new_database_owner_roles value is a devctl database-surface compatibility gate, not authorization for a new role; no new owner role is created", "persist contextual weakness incidents, ImprovementNeed diagnosis/candidate metadata, bounded benchmark references and Learning Ledger outcomes; preserve all historical evidence", "rollback disables proposal generation and leaves observations/evidence queryable"],
|
||||
"state_transitions": ["verified failure/correction/fallback/divergence signal -> scoped evidence", "repeated scoped evidence -> WeaknessPattern; one random failure remains an incident", "supported diagnosis -> ImprovementNeed with intervention class, risk and expected effect", "ImprovementNeed -> candidate proposal/evaluation plan only", "independent evaluation and existing governed development gates decide promotion; actual result is appended to Learning Ledger"],
|
||||
"failure_retry_restart": ["bounded diagnosis/candidate/evaluation budgets and deduplication prevent self-review loops", "provider outage is not competence loss; diagnosis distinguishes unavailable dependency", "stale/unknown/hypothesis diagnosis is never presented as established fact", "durable SELF state survives restart; no process-local canonical state"],
|
||||
"assumptions": ["task branch starts at gen2-integration dd04a67", "UI/browser acceptance remains deferred and V1 compatibility remains retained", "existing Work governed-evolution contract is consumed rather than duplicated", "automatic executable production changes remain a future governed task"],
|
||||
"acceptance_scenarios": ["EVOLVE-004-SCN-01 repeated user corrections attach to the affected scoped Skill/Reflex decision", "EVOLVE-004-SCN-02 repeated incidents create a WeaknessPattern while one random failure does not", "EVOLVE-004-SCN-03 diagnosis distinguishes retrieval/context/skill/reflex/provider/tool/outcome causes and preserves confidence/evidence", "EVOLVE-004-SCN-04 competence is contextual and evidence-derived, never self-declared", "EVOLVE-004-SCN-05 ImprovementNeed produces a bounded candidate proposal with risk, expected effect, evaluation and rollback", "EVOLVE-004-SCN-06 Creator and Evaluator are distinct; must-not-worsen can reject a candidate", "EVOLVE-004-SCN-07 Personal Benchmark cases reference confirmed evidence for memory, scope, failure avoidance, household, technical, continuity and authority/outcome classes", "EVOLVE-004-SCN-08 expected versus actual benefit is appended to Learning Ledger; no autonomous promotion occurs", "EVOLVE-004-SCN-09 diagnosis/candidate budgets and deduplication prevent infinite self-review"],
|
||||
"required_tests": ["FAST: targeted go test and go vet for changed self, core-runtime, contracts and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused evidence attribution, diagnosis, deduplication, contextual competence, candidate/evaluation, benchmark and Learning Ledger tests; no FULL suite, deployment or browser acceptance"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"task_id": "GEN2-REM-CORE-M5-LOCAL-MEDIUM-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Restore the existing Local MEDIUM cognitive path and make discovery, invocation, model availability and readiness evidence bounded and distinct for Core M5 acceptance.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-027-GOAL",
|
||||
"REQ-GEN2-027-BOUNDARY",
|
||||
"REQ-GEN2-027-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["configuration", "capability", "operations/supervisor"],
|
||||
"paths": [
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/core-runtime/**",
|
||||
"runtime/health/**",
|
||||
"runtime/supervisor/**",
|
||||
"deploy/**",
|
||||
"modules/configuration/**",
|
||||
"contracts/configuration/**",
|
||||
"tests/**",
|
||||
"docs/evidence/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-REM-CORE-M5-LOCAL-MEDIUM-001.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"architecture/**",
|
||||
"database/**",
|
||||
"adapters/models/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
".gitea/workflows/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_database_owner_roles": 0,
|
||||
"new_third_party_dependencies": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["contracts.capability@gen2", "contracts.configuration@gen2"],
|
||||
"may_change": ["Only bounded readiness and provider-routing behavior in the declared paths."]
|
||||
},
|
||||
"semantic_owner": "Capability and Operations/Supervisor; no new semantic owner.",
|
||||
"canonical_state": [
|
||||
"Existing Capability registry remains authoritative for binding qualification and activation.",
|
||||
"Lifeboat remains fallback only; it does not replace Local MEDIUM."
|
||||
],
|
||||
"state_transitions": [
|
||||
"provider discovery -> qualified binding -> active route -> unavailable/degraded -> recovered active route"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation; no credential value is read into evidence; no new privilege or database change."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema or migration change. Existing provider and capability state remain authoritative."
|
||||
],
|
||||
"assumptions": [
|
||||
"The existing llm-local gateway and protected provider references are the approved Local MEDIUM deployment path.",
|
||||
"Production data and secret values are not copied or exposed."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Provider probes have bounded per-provider timeouts and do not block unrelated healthy paths.",
|
||||
"Core readiness is ready when required infrastructure and cognitive worker readiness are available; optional provider loss is represented as degraded fleet state.",
|
||||
"Restart must re-run discovery and restore the same routing without changing canonical state."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"SQLite/gateway contention is identified without reading credentials.",
|
||||
"Local MEDIUM discovery health, invocation health and model availability are separately observable.",
|
||||
"Local MEDIUM authenticated model probe and a real cognitive invocation succeed.",
|
||||
"CPU/GPU loss does not prevent MEDIUM selection; Lifeboat is fallback.",
|
||||
"MEDIUM loss selects Lifeboat and recovery returns selection to MEDIUM.",
|
||||
"Core and cognitive-worker restart restore readiness and routing."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/health/... ./runtime/supervisor/...",
|
||||
"FAST: go vet ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/health/... ./runtime/supervisor/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-REM-CORE-M5-LOCAL-MEDIUM-001.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -task dev/tasks/GEN2-REM-CORE-M5-LOCAL-MEDIUM-001.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"Targeted production provider/restart/recovery smoke"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -1641,7 +1641,7 @@ Historical migrations are evidence. Gen2 migration starts at the next available
|
||||
|
||||
# 41. New Gen2 schema migration groups
|
||||
|
||||
Exact migration numbers are assigned after current repository HEAD is fixed.
|
||||
Exact migration numbers are assigned after current repository HEAD is fixed. The repository uses one global sequence: `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupy their numbers. GEN2-EVOLVE-001 therefore adds owner-local Epistemics metadata as additive migration `053_gen2_adaptive_experience_scope.sql`; it does not alter either existing migration.
|
||||
|
||||
Recommended groups:
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ Do not guess Portainer credentials and do not search other project source trees
|
||||
## Production services
|
||||
Each privileged runtime gets only its required secret/capability bindings. `commit-runtime` is intentionally isolated from the general Core Runtime.
|
||||
|
||||
The Core SELF learning repository uses the existing `assistant_core_secrets` volume and credential-reference mechanism with `self_role` and `credential://domovoy/self/db-password/v1`. The secret is consumed from the fixed `/run/secrets/self_db_password` path; its value is never placed in Compose environment, Git, logs or ordinary configuration. The role and schema already exist; this wiring does not create a role or broaden privileges.
|
||||
|
||||
Creator authentication secrets are stored in the external Docker volume `assistant_core_secrets` and mounted read-only only at `/run/secrets` in `core-runtime`. The production container environment contains `_FILE` paths, not the secret values. Files are owned by runtime uid 100/gid 101 with mode 0400. `CREATOR_USERNAME` is not secret and remains an environment value. Development may continue to use direct environment values, but a direct value and its matching `_FILE` setting must never be configured together.
|
||||
|
||||
The PostgreSQL off-host backup passphrase is generated randomly and stored as CurrentUser DPAPI ciphertext at `%LOCALAPPDATA%\Domovoy\postgres-backup-key.dpapi.json`. An atomic copy of that DPAPI ciphertext, never the plaintext passphrase, is retained at `\\nas\MST\codex\_backups\domovoy\keys\postgres-backup-key.dpapi.json`. The passphrase is never stored in Git, the production Docker host, a scheduled-task argument, or NAS plaintext. The NAS backup directory receives only GnuPG AES-256 ciphertext and a non-secret integrity manifest. Recovery therefore requires both the NAS artifacts and the authorized Windows DPAPI profile.
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Assistant project status
|
||||
|
||||
## Gen2 M6 checkpoint — 2026-08-30
|
||||
|
||||
- `GEN2-EVOLVE-004 = MILESTONE_ACCEPTED`: bounded SELF learning signals, evidence-backed attribution, contextual weakness/competence, ImprovementNeed, independently evaluated candidate, benchmark and expected-vs-actual Learning Ledger primitives are implemented with durable SELF persistence in migration 055. Evidence: `docs/evidence/gen2-evolve-004-milestone-2026-08-30.md`. No autonomous promotion, production deployment, new owner, UI work, V1 retirement or GEN2-EVOLVE-005 was started.
|
||||
|
||||
- `GEN2-EVOLVE-003 = MILESTONE_ACCEPTED`: Experience evidence now enters an evidence-governed Candidate Skill → independent validation → Active Skill → bounded Candidate Reflex lifecycle. Reflex divergence/revalidation records SELF proposals and Learning Ledger evidence; it cannot autonomously rewrite or promote production. Evidence: `docs/evidence/gen2-evolve-003-milestone-2026-08-30.md`. UI and V1 compatibility remain unchanged.
|
||||
|
||||
- `GEN2-EVOLVE-002 = MILESTONE_ACCEPTED`: one bounded typed cognitive memory-continuation round is available through Core and the existing retrieval fabric. Evidence: `docs/evidence/gen2-evolve-002-milestone-2026-08-30.md`. UI and V1 compatibility remain unchanged.
|
||||
|
||||
- `GEN2-EVOLVE-001 = MILESTONE_ACCEPTED`: bounded adaptive retrieval adds task-relative freshness, computed salience, positive/negative/contradiction experience lanes and provenance-preserving consolidation candidates. Evidence: `docs/evidence/gen2-evolve-001-milestone-2026-08-30.md`. No V1 path was retired and UI/browser acceptance remains deferred by owner.
|
||||
|
||||
- `GEN2-028 = COMPLETED_WITH_EVIDENCE`: V1 compatibility inventory is recorded in `docs/evidence/gen2-028-v1-compatibility-inventory-2026-08-30.json`. Current production callers remain for PersonalMemory fallback, ActiveProjectFocus, truth_bundles, ordinary ENV reads, SkillProfile/FastPath; no path was proven `SAFE_TO_RETIRE`.
|
||||
- `GEN2-029 = COMPLETED_WITH_SAFE_NOOP`: no V1 runtime path or historical evidence was deleted or disabled. The decision and rollback point are recorded in `docs/evidence/gen2-029-v1-retirement-decision-2026-08-30.json`; `FULL_RETIREMENT` was not applicable because no irreversible retirement was performed.
|
||||
- `GEN2-030_STATUS = ACCEPTANCE_IN_PROGRESS`: this status is a documentation checkpoint, not a final Gen2 PASS. The final gate must confirm current production and repository evidence; the M6 retirement gap remains that active V1 compatibility callers have not yet been proven unused.
|
||||
- `UI_BROWSER_ACCEPTANCE = DEFERRED_BY_OWNER`; it is intentionally excluded from core/M6 acceptance and is neither counted as PASS nor as a blocker.
|
||||
- `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-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
|
||||
|
||||
- `CURRENT_MODEL_SCOPE = LOCAL_FIRST_V1`; one locally available Qwen3-8B or Gemma-4 conversational model is sufficient for the first stage, with the local Lifeboat as recovery fallback. External models remain supported as optional capability, but their absence must not block work: reflexes, skills and local executors provide degraded continuation. External/GPU qualification and promotion are intentionally deferred by DEV-000270.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"evidence_id": "gen2-027-core-acceptance-amendment-2026-08-30",
|
||||
"task_id": "GEN2-027",
|
||||
"acceptance_amendment": {
|
||||
"approved_by_project_owner": true,
|
||||
"ui_browser_acceptance": "DEFERRED",
|
||||
"counts_as_pass": false,
|
||||
"counts_as_blocker": false,
|
||||
"note": "UI/browser acceptance is consciously excluded from the current core-only M5 stage."
|
||||
},
|
||||
"core_status": "BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"findings": {
|
||||
"core_health": "200",
|
||||
"core_ready": "503",
|
||||
"cognitive_health": "200",
|
||||
"cognitive_fleet": {
|
||||
"local_medium": "UNAVAILABLE: authenticated /v1/models probe timed out",
|
||||
"cpu_fast": "UNAVAILABLE",
|
||||
"gpu": "UNAVAILABLE",
|
||||
"lifeboat": "AVAILABLE"
|
||||
},
|
||||
"configured_secret_files": "present and readable; values not read",
|
||||
"interpretation": "The configured Local MEDIUM path cannot currently be proven ACTIVE/VALIDATED/AVAILABLE; Lifeboat remains fallback and is not counted as primary MEDIUM acceptance."
|
||||
},
|
||||
"full_m5_gate": "NOT_RUN",
|
||||
"production_promotion": "NOT_RUN",
|
||||
"gen2_028": "NOT_STARTED"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"evidence_id": "gen2-028-v1-compatibility-inventory-2026-08-30",
|
||||
"task_id": "GEN2-028",
|
||||
"task_revision": 2,
|
||||
"source_revision": "93feb9bf2df8f2e2cfd4d0259d39c869b19cad08",
|
||||
"scope": "static repository inventory plus read-only production container and Supervisor observations; no code, schema, or runtime mutation",
|
||||
"canonical_writer_rule": "unchanged; no second Memory owner and no writer cutover performed",
|
||||
"production_observation": {
|
||||
"deployment_revision": "3182092d5cc758f5a536ce10f5428b5d782bb961",
|
||||
"core_container": "running, restart_count=0; /healthz=200",
|
||||
"cognitive_worker": "running, restart_count=0, health=healthy",
|
||||
"lifeboat": "running, restart_count=0, health=healthy",
|
||||
"supervisor": "running, restart_count=0, health=healthy",
|
||||
"traffic_mode": "CANDIDATE",
|
||||
"last_known_good": "3182092d5cc758f5a536ce10f5428b5d782bb961",
|
||||
"legacy_named_runtime_containers": "none observed"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"id": "personal-memory-context-text",
|
||||
"paths": ["modules/interaction/domain/personal_memory.go", "modules/interaction/domain/gen2_context.go", "cmd/core-runtime/main.go"],
|
||||
"role": "V1 rebuildable context projection and deterministic preference recall",
|
||||
"runtime_status": "ACTIVE_COMPATIBILITY_PATH",
|
||||
"evidence": ["BuildScopedGen2Context is called from the production chat handler", "CompilePersonalMemory and ContextText remain in that call path", "empty Gen2 projection returns personal-memory-v1-fallback", "targeted gen2_context and personal_memory tests exist"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "retain the existing read fallback and revert the reader switch without deleting utterance history"
|
||||
},
|
||||
{
|
||||
"id": "active-project-focus",
|
||||
"paths": ["adapters/postgres/situation_repository.go", "cmd/core-runtime/main.go", "contracts/situation/types.go"],
|
||||
"role": "V1 project-centric focus compatibility projection",
|
||||
"runtime_status": "ACTIVE_READ_AND_WRITE_COMPATIBILITY_PATH",
|
||||
"evidence": ["core chat loads ActiveProjectFocus for each authenticated message", "core persists it after semantic interpretation", "PostgreSQL adapter reads and upserts situation.active_project_focus", "Focus V2 projection exists but does not remove these callers"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "keep the projection readable and restore the prior focus reader if a V2 reader is rolled back"
|
||||
},
|
||||
{
|
||||
"id": "truth-bundles",
|
||||
"paths": ["adapters/postgres/epistemics_repository.go", "modules/epistemics/application/persisted_research.go", "database/migrations/034_epistemics_truth_bundles.sql", "tests/research_contracts/truth_persistence_sql_test.go"],
|
||||
"role": "V1 denormalized epistemic compatibility/status and research persistence",
|
||||
"runtime_status": "ACTIVE_WRITER_AND_READ_COMPATIBILITY_PATH",
|
||||
"evidence": ["SaveTruthBundle remains an application repository contract used by persisted research", "TruthRuntimeStatus reads counts and grouped status from epistemics.truth_bundles", "migration 034 and its contract test are retained", "no production evidence permits declaring the reader unused"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "retain table and reader; revert normalized reader selection without deleting historical bundles"
|
||||
},
|
||||
{
|
||||
"id": "ordinary-env-configuration",
|
||||
"paths": ["cmd/core-runtime/main.go", "cmd/work-graph-runtime/main.go", "cmd/reflection-worker/main.go", "cmd/notification-receiver/main.go"],
|
||||
"role": "ENV configuration and secret/bootstrap resolution",
|
||||
"runtime_status": "ACTIVE_MIXED_BOOTSTRAP_AND_NON_BOOTSTRAP_READS",
|
||||
"evidence": ["production composition supplies runtime settings through process configuration", "source contains direct os.Getenv reads for listener, database, provider and runtime settings", "secret values were not read or emitted", "no complete production observation proves every non-bootstrap consumer is unused"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "restore the prior ENV fallback for a single setting while Configuration remains authoritative"
|
||||
},
|
||||
{
|
||||
"id": "skill-profile-v1",
|
||||
"paths": ["adapters/postgres/capability_repository.go", "cmd/core-runtime/learning.go", "contracts/capability/resolution.go"],
|
||||
"role": "V1 SkillProfile storage/read compatibility and FastPath learning",
|
||||
"runtime_status": "ACTIVE_READ_AND_WRITE_PATH",
|
||||
"evidence": ["core exposes RecentSkillProfiles and records FastPath decline learning", "learning.go loads and saves SkillProfile", "PostgreSQL adapter writes and joins capability.skill_profiles", "explicit Skill/Reflex types exist but do not prove these callers are dead"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "retain SkillProfile rows and reader; revert Skill V2 selection without data deletion"
|
||||
},
|
||||
{
|
||||
"id": "implicit-fast-path",
|
||||
"paths": ["modules/capability/domain/skill.go", "modules/capability/domain/reflex.go", "cmd/core-runtime/learning.go"],
|
||||
"role": "V1 FastPath behavior and fallback-to-thinking compatibility",
|
||||
"runtime_status": "ACTIVE_COMPATIBILITY_PATH",
|
||||
"evidence": ["FastPath decisions are implemented and tested", "core records FastPath decline experiences", "ReflexFallback explicitly maps escalation to skill->reasoning", "no runtime telemetry separates all FastPath invocations from explicit Reflex use"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "restore FastPath decline/escalation behavior; preserve evidence and Skill/Reflex revisions"
|
||||
},
|
||||
{
|
||||
"id": "migration-feature-switches",
|
||||
"paths": ["development/GEN2_MIGRATION_MAP.md", "runtime/**", "modules/**"],
|
||||
"role": "documented migration switch family and compatibility strategy",
|
||||
"runtime_status": "UNKNOWN_NO_DEDICATED_RUNTIME_TELEMETRY_FOUND",
|
||||
"evidence": ["migration map defines switch names such as Focus, Context, Epistemics, Config and Skill strategies", "repository scan found no complete operational switch registry/telemetry proving zero production use", "test-only legacy mode references are not sufficient production evidence"],
|
||||
"retirement": "KEEP_LONGER",
|
||||
"rollback": "do not remove a switch until production observation identifies its callers and a reversible disable path"
|
||||
}
|
||||
],
|
||||
"coverage": {
|
||||
"searched": ["modules", "contracts", "cmd", "adapters", "runtime", "platform", "tests", "docs", "database/migrations"],
|
||||
"terms": ["PersonalMemory", "CompilePersonalMemory", "ContextText", "ActiveProjectFocus", "truth_bundles", "skill_profiles", "FastPath", "os.Getenv", "LookupEnv", "compatibility", "legacy", "feature switch"],
|
||||
"limitations": ["No secret values were inspected", "Container logs and Supervisor state provide no per-path legacy usage counters", "A code reference is evidence of availability/callability, not a claim that every branch executed in the observation window"]
|
||||
},
|
||||
"retirement_list": [
|
||||
{"path": "personal-memory-context-text", "decision": "KEEP_LONGER", "reason": "active production chat fallback and no usage counter"},
|
||||
{"path": "active-project-focus", "decision": "KEEP_LONGER", "reason": "core read/write callers remain active"},
|
||||
{"path": "truth-bundles", "decision": "KEEP_LONGER", "reason": "research writer and status reader remain active"},
|
||||
{"path": "ordinary-env-configuration", "decision": "KEEP_LONGER", "reason": "mixed bootstrap/non-bootstrap consumers remain"},
|
||||
{"path": "skill-profile-v1", "decision": "KEEP_LONGER", "reason": "core learning and repository read/write callers remain active"},
|
||||
{"path": "implicit-fast-path", "decision": "KEEP_LONGER", "reason": "active learning/fallback path and no complete production telemetry"},
|
||||
{"path": "migration-feature-switches", "decision": "KEEP_LONGER", "reason": "no complete runtime observation evidence"}
|
||||
],
|
||||
"conclusion": "GEN2-028 EXIT: every inventoried legacy surface is classified ACTIVE_COMPATIBILITY_PATH, ACTIVE_READ_AND_WRITE_COMPATIBILITY_PATH, or UNKNOWN_NO_DEDICATED_RUNTIME_TELEMETRY_FOUND. No surface is SAFE_TO_RETIRE on current evidence; GEN2-029 must not perform irreversible deletion or disablement based on this inventory."
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"evidence_id": "gen2-029-v1-retirement-decision-2026-08-30",
|
||||
"task_id": "GEN2-029",
|
||||
"task_revision": 2,
|
||||
"source_revision": "59d32143bf4ee7b4a78cb714ec0cbd76d4d8bdf5",
|
||||
"decision": "NO_IRREVERSIBLE_RETIREMENT",
|
||||
"reason": "GEN2-028 found no SAFE_TO_RETIRE path. Active callers remain for PersonalMemory fallback, ActiveProjectFocus, truth_bundles, ordinary ENV reads, SkillProfile/FastPath; migration switches lack complete production telemetry.",
|
||||
"changed_runtime_paths": [],
|
||||
"changed_schema_paths": [],
|
||||
"retired_paths": [],
|
||||
"preserved": [
|
||||
"human utterances and interaction history",
|
||||
"epistemic observations, claims, experiences, truth_bundles and audit lineage",
|
||||
"ActiveProjectFocus compatibility projection",
|
||||
"PersonalMemory read fallback",
|
||||
"SkillProfile/FastPath compatibility",
|
||||
"ordinary ENV fallback consumers until bootstrap/config observation is complete",
|
||||
"all existing Gen2 canonical writers and owner boundaries"
|
||||
],
|
||||
"full_retirement_gate": {
|
||||
"status": "NOT_APPLICABLE",
|
||||
"reason": "No irreversible V1 schema/data deletion or runtime disablement was authorized or performed; the execution contract requires the gate immediately before such a retirement.",
|
||||
"repository_full_test": "NOT_RUN_BY_DESIGN; no retirement change risk to justify expanding beyond the task's FAST/no-op decision"
|
||||
},
|
||||
"rollback": {
|
||||
"point": "59d32143bf4ee7b4a78cb714ec0cbd76d4d8bdf5",
|
||||
"method": "keep current compatibility paths and revert this evidence-only decision; no schema rollback or data deletion needed"
|
||||
},
|
||||
"conclusion": "No unsafe V1 path was removed. Historical evidence remains available, and there is no new dual writer. Retirement remains pending an observation window that can prove each path unused or a future approved cutover decision."
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"evidence_id": "gen2-030-final-acceptance-2026-08-30",
|
||||
"task_id": "GEN2-030",
|
||||
"task_revision": 2,
|
||||
"source_revision": "2a8e4e1b81226a1acedfd746ebc2aeb0ac11158f",
|
||||
"status": "NOT_FINAL_PASS_RETIREMENT_GAP_REMAINS",
|
||||
"full_final_gate": {
|
||||
"go_test_all": "PASS",
|
||||
"go_vet_all": "PASS",
|
||||
"architecture_checks": "PASS",
|
||||
"governance_preflight_check_diff_independent_whitespace": "PASS",
|
||||
"migration_consistency": "PASS; 52 files, unique range 001..052; no M6 schema changes",
|
||||
"production_core_smoke": "PASS; core /healthz=200, /readyz=200; cognitive-worker healthy; Lifeboat healthy",
|
||||
"supervisor_recovery_readiness": "PASS; current and last-known-good 3182092d5cc758f5a536ce10f5428b5d782bb961; rollback target retained"
|
||||
},
|
||||
"architecture_reconciliation": {
|
||||
"registry": "YAML/JSON mirrors agree; Gen2 owners and runtime topology are represented",
|
||||
"canonical_memory_owner": "none; retrieval/context remain projection fabric",
|
||||
"schema": "migrations 046..052 are present and numbered without duplicates; historical 001..045 retained",
|
||||
"production_revision": "3182092d5cc758f5a536ce10f5428b5d782bb961"
|
||||
},
|
||||
"m6_result": {
|
||||
"gen2_028": "completed inventory; no SAFE_TO_RETIRE path proven",
|
||||
"gen2_029": "completed safe no-op; no runtime/schema/data deletion",
|
||||
"gen2_030": "full final verification passed, but final M6 exit is not signed",
|
||||
"full_retirement": "NOT_APPLICABLE; no irreversible retirement was performed",
|
||||
"final_tag": "NOT_CREATED; acceptance is not a final PASS"
|
||||
},
|
||||
"ui_browser": "DEFERRED_BY_OWNER; intentionally excluded, neither PASS nor blocker",
|
||||
"provider_gaps": ["CPU_FAST unavailable", "GPU unavailable"],
|
||||
"remaining_gaps": [
|
||||
"Per-path production observation telemetry is missing for migration switches and several compatibility readers",
|
||||
"PersonalMemory, ActiveProjectFocus, truth_bundles, ordinary ENV, SkillProfile/FastPath still have active callers and require a future evidence-backed retirement/cutover task",
|
||||
"M6 cannot claim complete V1 retirement until those paths are proven unused or explicitly retained by an accepted architecture decision"
|
||||
],
|
||||
"next_evolution_queue": [
|
||||
"Instrument bounded, redacted usage observations for each remaining compatibility path",
|
||||
"Run an approved observation window and re-evaluate retirement candidates",
|
||||
"Retire only individually proven paths with FULL_RETIREMENT immediately before irreversible change",
|
||||
"Keep UI/browser acceptance deferred by owner",
|
||||
"Keep CPU_FAST/GPU provider qualification separate from Core acceptance"
|
||||
]
|
||||
}
|
||||
@@ -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,47 @@
|
||||
{
|
||||
"evidence_id": "gen2-cognitive-003-boundary-2026-08-31",
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 3,
|
||||
"status": "PARTIAL_CORE_VERIFIED / ACTIVATION_REQUIRES_INDEPENDENT_EVALUATION",
|
||||
"owner_amendment": {
|
||||
"browser_authenticated_acceptance": "DEFERRED_BY_OWNER",
|
||||
"ui": "OUT_OF_SCOPE",
|
||||
"v1_retirement": "NOT_AUTHORIZED",
|
||||
"gen3": "NOT_STARTED"
|
||||
},
|
||||
"gateway_runtime": {
|
||||
"revision": "a23c36eb9d0c7c73c643b932c184ec1e0adf57ff",
|
||||
"stable_health": "healthy",
|
||||
"bounded_qwen_smoke": "HTTP 200 in 4.310 seconds",
|
||||
"warm_gateway_trace": {
|
||||
"admission_millis": 13,
|
||||
"upstream_millis": 1011,
|
||||
"end_to_end_millis": 1144
|
||||
}
|
||||
},
|
||||
"candidate_slot_b": {
|
||||
"revision": "cb50b7357b4c37d2add40db6776355b68a074dd0",
|
||||
"components": "creator-web, core-runtime, cognitive-worker, research-runtime healthy",
|
||||
"local_medium_discovery": "AVAILABLE",
|
||||
"bounded_qualification": "HTTP 200 in 1.390 seconds with one-token output budget",
|
||||
"advisory": "HTTP 200 through the candidate cognitive route within the 12-second budget"
|
||||
},
|
||||
"canonical_lifecycle_state": {
|
||||
"provider": "local-medium-primary",
|
||||
"qualification": "DISCOVERED",
|
||||
"activation": "INACTIVE",
|
||||
"health": "AVAILABLE",
|
||||
"interpretation": "Discovery and reachability evidence do not self-activate a cognitive binding."
|
||||
},
|
||||
"boundary": {
|
||||
"required_next_transition": "independent evaluation/canary promotion through the protected Capability/Core owner path",
|
||||
"not_performed": [
|
||||
"direct database update",
|
||||
"manual admission lease reset",
|
||||
"fallback substitution for Local MEDIUM",
|
||||
"automatic activation",
|
||||
"browser or authentication bypass"
|
||||
],
|
||||
"why_task_cannot_complete": "The active packet permits provider invocation and typed immediate failure mapping, but does not authorize the independent Core/Capability activation decision required to reach VALIDATED/ACTIVE."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 4,
|
||||
"recorded_at": "2026-08-31T07:28:00Z",
|
||||
"revision": "dd9bca3ace51e1c0af50fd67dba20caad17cf391",
|
||||
"environment": "isolated candidate slot B",
|
||||
"candidate_components": {
|
||||
"creator-web": "healthy",
|
||||
"core-runtime": "healthy",
|
||||
"cognitive-worker": "healthy",
|
||||
"research-runtime": "healthy"
|
||||
},
|
||||
"qualification_phase_trace": {
|
||||
"correlation_id": "92cfec5eca6c57c59cf48da6d1644005",
|
||||
"provider": "local-medium-primary",
|
||||
"model": "Qwen3.6-27B-UD-Q4_K_XL",
|
||||
"result": "REACHABLE",
|
||||
"finish_reason": "COMPLETED",
|
||||
"invocation_latency_millis": 3025,
|
||||
"phases": {
|
||||
"Q0_request_accepted": 0,
|
||||
"Q1_binding_candidate_loaded": 0,
|
||||
"Q2_provider_model_resolved": 342,
|
||||
"Q3_bounded_invocation_started": 342,
|
||||
"Q4_bounded_invocation_completed": 3367,
|
||||
"Q5_invocation_evidence_built": 3368,
|
||||
"Q6_observation_persistence_started": 3368,
|
||||
"Q7_observation_persistence_completed": 3371,
|
||||
"Q8_independent_evaluation_not_started": 3371,
|
||||
"Q17_response_returned": 3371
|
||||
}
|
||||
},
|
||||
"findings": [
|
||||
"The bounded Local MEDIUM provider invocation completed in 3.025 seconds.",
|
||||
"The canonical qualification-observation persistence segment consumed 3 milliseconds and did not approach the 12-second household request budget.",
|
||||
"The worker intentionally stops after operational qualification evidence. It does not create independent evaluation, CANARY, or ACTIVE state.",
|
||||
"The existing governed activation path is Core POST /v1/creator/cognitive-canary followed by POST /v1/creator/cognitive-activate; both require a creator session and Capability-owned evaluation evidence."
|
||||
],
|
||||
"security": {
|
||||
"prompts_logged": false,
|
||||
"model_answers_logged": false,
|
||||
"credentials_or_secrets_logged": false,
|
||||
"direct_database_state_change": false,
|
||||
"manual_admission_lease_change": false
|
||||
},
|
||||
"status": "QUALIFICATION_EVIDENCE_RECORDED_ACTIVATION_REQUIRES_EXISTING_GOVERNED_AUTHORIZED_PATH"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"evidence_id": "gen2-compat-obs-001-blocked-2026-08-30",
|
||||
"task_id": "GEN2-COMPAT-OBS-001",
|
||||
"task_revision": 1,
|
||||
"status": "BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"instrumentation_commit": "3efcbbd",
|
||||
"integration_commit": "346e0ac",
|
||||
"final_status": "GEN2_CORE_ACCEPTED / V1_COMPATIBILITY_RETAINED / UI_DEFERRED",
|
||||
"instrumented_paths": ["PersonalMemory fallback", "ActiveProjectFocus read", "ActiveProjectFocus write", "truth_bundles writer", "truth_bundles status reader", "non-bootstrap ENV consumers", "SkillProfile V1", "FastPath V1", "migration/compatibility switches"],
|
||||
"telemetry_contract": {
|
||||
"fields": ["invocation_count", "unique_request_key_hash", "unique_session_key_hash", "last_used_at", "caller_component", "read_or_write", "reason", "gen2_path_available", "fallback_or_primary", "success_or_failure", "correlation_id_hash"],
|
||||
"content_policy": "No memory content, secrets, or user data is emitted; identifiers are hashed immediately and telemetry is an operational projection, not a canonical owner."
|
||||
},
|
||||
"fast_checks": {
|
||||
"go_test": "PASS: targeted telemetry, core-runtime, interaction, capability, and postgres packages",
|
||||
"go_vet": "PASS: same targeted packages",
|
||||
"preflight": "PASS",
|
||||
"check_diff": "PASS",
|
||||
"independent_governance": "PASS",
|
||||
"git_diff_check": "PASS"
|
||||
},
|
||||
"production_observation": {"deployment": "NOT_RUN", "smoke": "NOT_RUN", "observation_window": "NOT_STARTED", "production_mutation": "NONE"},
|
||||
"blocker": {
|
||||
"supervisor_topology": "Supervisor has stable/candidate routing for creator-web only; core-runtime, cognitive-worker, and research-runtime have no candidate targets in the production compose/startup model.",
|
||||
"missing_capability": "An approved production deployment flow capable of deploying the instrumented core/cognitive/research candidate, such as configured Portainer automation or an approved candidate-capable deployment topology.",
|
||||
"unsafe_alternative_not_used": "Directly replacing core-runtime with compose would mutate the stable runtime outside the Supervisor candidate path and was not performed."
|
||||
},
|
||||
"retirement": {"safe_to_retire": [], "retirement_tasks_created": [], "classification": "UNKNOWN until production observation evidence exists", "rollback": "All existing V1 compatibility paths and historical evidence remain intact."},
|
||||
"scope": {"schema_or_migrations_changed": false, "canonical_owner_changed": false, "behavior_changed": false, "ui_changed": false, "gen3_started": false}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"task_id": "GEN2-COMPAT-OBS-001",
|
||||
"task_revision": 2,
|
||||
"status": "BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"observed_at_utc": "2026-08-30T10:13:06.5676592Z",
|
||||
"deployment_revision": "87ec7efd0fa7bdbed22c1c0c67d671907029d7bb",
|
||||
"deployment_scope": "candidate-only; no Supervisor traffic change or promotion",
|
||||
"stable_release_set": {
|
||||
"traffic_mode": "STABLE",
|
||||
"revision": "3182092d5cc758f5a536ce10f5428b5d782bb961",
|
||||
"container_ids_before_protected_operation": {
|
||||
"creator-web": "9578e7c7e0137ba36a7fc75ffaf9fce33be046e38531c9de99c0b75c2ce465f8",
|
||||
"core-runtime": "8281ba60b234825d6ee965887e8eef10e29a0783264c05d39cbe937f1b780530",
|
||||
"cognitive-worker": "987640770cad44a0b05a47dbf08a4b47a9225f8030f8bc81721fdc0834e2ee4c",
|
||||
"research-runtime": "7e7d6e5fc050b5bde5073f0939e5a9652a1f68be0a75104cc0b486ec0cd745a6"
|
||||
},
|
||||
"container_ids_after_protected_operation": {
|
||||
"creator-web": "9578e7c7e0137ba36a7fc75ffaf9fce33be046e38531c9de99c0b75c2ce465f8",
|
||||
"core-runtime": "8281ba60b234825d6ee965887e8eef10e29a0783264c05d39cbe937f1b780530",
|
||||
"cognitive-worker": "987640770cad44a0b05a47dbf08a4b47a9225f8030f8bc81721fdc0834e2ee4c",
|
||||
"research-runtime": "7e7d6e5fc050b5bde5073f0939e5a9652a1f68be0a75104cc0b486ec0cd745a6"
|
||||
},
|
||||
"stable_ids_unchanged": true
|
||||
},
|
||||
"candidate_components": {
|
||||
"creator-web": {"health": "healthy", "revision": "87ec7efd0fa7bdbed22c1c0c67d671907029d7bb"},
|
||||
"core-runtime": {"health": "healthy", "revision": "87ec7efd0fa7bdbed22c1c0c67d671907029d7bb"},
|
||||
"cognitive-worker": {"health": "healthy", "revision": "87ec7efd0fa7bdbed22c1c0c67d671907029d7bb"},
|
||||
"research-runtime": {"health": "healthy", "revision": "87ec7efd0fa7bdbed22c1c0c67d671907029d7bb"}
|
||||
},
|
||||
"auth_harness": {
|
||||
"approved_harness_found": false,
|
||||
"available_mechanisms": ["/api/session", "tests/creator_web/verify-remote.ps1", "tests/cognitive_advisory/verify-remote.ps1"],
|
||||
"gap": "available remote scripts require production password or bootstrap token; no sanctioned test principal/service harness without reading a credential was found",
|
||||
"browser_or_secret_bypass_used": false
|
||||
},
|
||||
"telemetry_observation": {
|
||||
"started": false,
|
||||
"reason": "no legitimate authenticated generation traffic; candidate was intentionally not promoted",
|
||||
"v1_paths_deleted": false
|
||||
},
|
||||
"deployment_incident": {
|
||||
"occurred": true,
|
||||
"cause": "first attempted checkout used stale local origin/gen2-integration ref and ran pre-guard script",
|
||||
"effect": "core stable container was recreated once; image revision remained 3182092 and stable traffic stayed active",
|
||||
"recovery": "subsequent protected candidate-only deployment used FETCH_HEAD 87ec7ef and proved stable IDs unchanged"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"task_id": "GEN2-COMPAT-OBS-001",
|
||||
"status": "BLOCKED_BY_DEPLOYMENT_CONTROL",
|
||||
"observed_at_utc": "2026-08-30T10:37:07.7219511Z",
|
||||
"browser_connector": {
|
||||
"authenticated_tab_visible": true,
|
||||
"stale_login_tab_also_present": true,
|
||||
"cookies_tokens_or_passwords_read": false
|
||||
},
|
||||
"requested_candidate_revision": "87ec7efd0fa7bdbed22c1c0c67d671907029d7bb",
|
||||
"candidate_prepare": "not_completed_as_a_safe_release_set_promotion",
|
||||
"failure": {
|
||||
"phase": "candidate creator startup after cognitive readiness",
|
||||
"result": "stable container IDs changed despite candidate-only --no-deps; Supervisor control became temporarily unavailable",
|
||||
"stable_ids_before": {
|
||||
"creator-web": "4df348752f1a6def05acb1934bfc23048902ad6f235a7da967806d481e9ce7b7",
|
||||
"core-runtime": "a739d2d5c713dde575f571b38e58ec33143994aa9e3bcc2f5d5fc765a50e1796",
|
||||
"cognitive-worker": "a33d61b32116711287a166e0cce6362dc10de321f920549ef8a344fe0b5efb2d",
|
||||
"research-runtime": "88cfade5cdce288576b1c73493f83c84d915ac82c3d16dc6d3fedb9579a4ba8c"
|
||||
},
|
||||
"stable_ids_after": {
|
||||
"creator-web": "5181e5c6b48eaed6c6ea85eff5668c0e675b72bb982eb9425d6f39f899378ec9",
|
||||
"core-runtime": "641e8f7ae8177e4638cf806d9be1bb6f8b5ac028deb0c0c410c6ff4139032bd5",
|
||||
"cognitive-worker": "2e6d46f5b533cfeda261ceafd65ad5b5873b8aa8166bffc4ae91033a6c994d0a",
|
||||
"research-runtime": "b15d9b0e116fbd18fa726e33541ef11fe1bafe4a85210ab5ee500466a20b400b"
|
||||
},
|
||||
"stable_ids_unchanged": false,
|
||||
"cause": "external production updater/Compose reconcile reacts to production checkout or candidate Compose operation; --no-deps alone does not isolate stable in this control plane"
|
||||
},
|
||||
"recovery": {
|
||||
"traffic_mode": "STABLE",
|
||||
"revision": "91f0c9e7999907a4ee7665b77742dd452c18b17b",
|
||||
"healthz": 200,
|
||||
"readyz": 200,
|
||||
"supervisor_baseline_aligned": true,
|
||||
"data_or_volumes_deleted": false
|
||||
},
|
||||
"observation": {
|
||||
"authenticated_workload_sent": false,
|
||||
"telemetry_promoted": false,
|
||||
"baseline_started": false,
|
||||
"tag_created": false,
|
||||
"v1_paths_deleted": false
|
||||
},
|
||||
"required_follow_up": "deployment control plane must isolate candidate Compose project or disable updater reconcile before safe ReleaseSet promotion"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"evidence_id": "gen2-core-m5-local-medium-remediation-2026-08-30",
|
||||
"task_id": "GEN2-REM-CORE-M5-LOCAL-MEDIUM-001",
|
||||
"status": "CORE_ACCEPTED_UI_DEFERRED",
|
||||
"source_revision": "8aa5179d47aef367ff7ef3732ed0ac603c5ffc89",
|
||||
"root_cause": {
|
||||
"discovery": "The approved llm-local gateway /v1/models endpoint performs SQLite-backed route updates while serving discovery. Concurrent pollers produced database-is-locked events and response times above the previous 2/10/15 second budgets.",
|
||||
"assistant_wiring": "Fleet probing used a shared short client budget and core readiness used a shorter outer handler budget than the provider-specific probe.",
|
||||
"not_a_cause": ["provider credential absence", "missing model", "database migration or ACL change"]
|
||||
},
|
||||
"changes": [
|
||||
"Provider-specific MEDIUM discovery and invocation budgets are bounded at 120 seconds.",
|
||||
"Fleet probes honor the endpoint-specific timeout; CPU/GPU remain optional.",
|
||||
"Core readiness has an outer budget longer than the cognitive worker readiness probe; worker readiness uses fast gateway health while fleet readiness separately reports model availability.",
|
||||
"No schema, migration, privilege, owner, adapter, or architecture change."
|
||||
],
|
||||
"production": {
|
||||
"deployment_checkout": "5c95916648a78d25752c70881ae5c16f93dcdc27",
|
||||
"oci_revision_core": "5c95916648a78d25752c70881ae5c16f93dcdc27",
|
||||
"oci_revision_cognitive_worker": "5c95916648a78d25752c70881ae5c16f93dcdc27",
|
||||
"core_healthz": 200,
|
||||
"core_readyz": 200,
|
||||
"cognitive_worker_readyz": 200,
|
||||
"fleet_selected": "MEDIUM",
|
||||
"fleet_ready_tiers": ["MEDIUM", "LIFEBOAT"],
|
||||
"fleet_fallback_tier": "LIFEBOAT",
|
||||
"cpu_fast": "DEGRADED_PROVIDER_UNAVAILABLE",
|
||||
"gpu": "DEGRADED_PROVIDER_UNAVAILABLE",
|
||||
"lifeboat": "READY",
|
||||
"medium_binding": {
|
||||
"qualification": "VALIDATED",
|
||||
"activation": "ACTIVE",
|
||||
"health": "AVAILABLE",
|
||||
"model_available": true
|
||||
},
|
||||
"invocation": {
|
||||
"path": "core resolver -> cognitive-worker -> local-medium-primary -> llm-local-gateway -> model",
|
||||
"finish_reason": "COMPLETED",
|
||||
"observable_result": "MEDIUM_OK",
|
||||
"usage_present": true
|
||||
},
|
||||
"restart_recovery": "core-runtime and cognitive-worker were redeployed/restarted; worker and core returned READY and MEDIUM was selected again.",
|
||||
"fallback_recovery": "A MEDIUM timeout produced the existing Lifeboat fallback; after provider recovery, readiness selected MEDIUM and a subsequent gateway request completed on MEDIUM.",
|
||||
"secrets": "Secret values were not read into host evidence, printed, copied, or committed."
|
||||
},
|
||||
"targeted_checks": [
|
||||
"go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/health/... ./runtime/supervisor/...",
|
||||
"go vet ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/health/... ./runtime/supervisor/...",
|
||||
"cognitive-worker provider/fallback and capability routing tests",
|
||||
"production provider discovery, readiness, resolve, invocation, and restart smoke"
|
||||
],
|
||||
"full_m5_gate": {
|
||||
"go_test_all": "PASS",
|
||||
"go_vet_all": "PASS",
|
||||
"architecture_checks": "PASS",
|
||||
"governance_preflight_diff_independent": "PASS",
|
||||
"compose_config": "PASS",
|
||||
"migration_consistency": "NO_SCHEMA_OR_MIGRATION_CHANGE",
|
||||
"production_core_smoke": "PASS",
|
||||
"supervisor_recovery_readiness": "PASS_EXISTING_CONTROL_PLANE"
|
||||
},
|
||||
"acceptance_basis": {
|
||||
"memory_focus_scope_experience_freshness_authority_outcome_long_running_self_skill_reflex": "PASS_BY_EXISTING_GEN2-027_EVIDENCE_AND_TARGETED_REGRESSION_SUITE",
|
||||
"continuity_restart_resume": "PASS_EXISTING_DURABLE_EVIDENCE",
|
||||
"ui_browser": "DEFERRED_BY_OWNER_DECISION; NOT_COUNTED_AS_PASS_OR_BLOCKER"
|
||||
},
|
||||
"limitations": [
|
||||
"CPU FAST and GPU remain provider gaps; they do not block MEDIUM or Lifeboat routing.",
|
||||
"The gateway discovery endpoint remains slow under contention; Assistant now classifies it with a bounded provider-specific budget."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"evidence_id": "gen2-deploy-001-production-recovery-2026-08-29",
|
||||
"task_id": "GEN2-DEPLOY-001",
|
||||
"production_revision_before": "36fc8ec",
|
||||
"production_revision_after": "2083096aedaec73191e0bc9e2566f596e1810d82",
|
||||
"topology": {
|
||||
"supervisor": "assistant-supervisor-1",
|
||||
"public_traffic": "Supervisor reverse proxy on creator web host port",
|
||||
"control_api": "Supervisor loopback-only host control port",
|
||||
"durable_state": "assistant_assistant_supervisor_state:/var/lib/supervisor/release-state.json",
|
||||
"candidate": "assistant-creator-web-candidate-1"
|
||||
},
|
||||
"canary": {
|
||||
"candidate_health": "healthy",
|
||||
"header_route": "X-Assistant-Canary=true -> candidate",
|
||||
"header_route_http": 200,
|
||||
"promoted": true
|
||||
},
|
||||
"rollback": {
|
||||
"drill": "candidate container stopped, Supervisor traffic rollback and release rollback invoked",
|
||||
"stable_http": 200,
|
||||
"stable_target_header": "stable",
|
||||
"supervisor_restart": "healthy; traffic_mode=STABLE and stable release state restored"
|
||||
},
|
||||
"postgres_recovery": {
|
||||
"restart": "healthy",
|
||||
"public_healthz": 200,
|
||||
"public_readyz": 200
|
||||
},
|
||||
"cognitive_lifeboat": {
|
||||
"previous_status": "restart loop/unhealthy",
|
||||
"cause": "configured Qwen3-8B-Q4_K_M.gguf was absent from llm-local-models volume",
|
||||
"remediation": "compose model reference changed to existing Qwen3.8-27B-UD-Q2_K_XL.gguf",
|
||||
"current_status": "healthy",
|
||||
"role": "local degraded cognitive provider; not Supervisor"
|
||||
},
|
||||
"smoke": {
|
||||
"public_healthz": 200,
|
||||
"public_readyz": 200,
|
||||
"public_login": 200,
|
||||
"authenticated_household": "UNVERIFIED: browser session was unauthenticated; no credentials were read or entered"
|
||||
},
|
||||
"interpretation": "Supervisor independence, durable release state, controlled header canary, stable rollback and restart recovery are production-observed. Authenticated household acceptance remains an explicit gap."
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"evidence_id": "gen2-deploy-002-blocked-2026-08-30",
|
||||
"task_id": "GEN2-DEPLOY-002",
|
||||
"task_revision": 1,
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"status": "BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"implementation_checks": {
|
||||
"release_set_tests": "PASS",
|
||||
"supervisor_go_test": "PASS",
|
||||
"supervisor_go_vet": "PASS",
|
||||
"compose_config": "PASS",
|
||||
"canary_script_syntax": "PASS",
|
||||
"governance": "PASS",
|
||||
"git_diff_check": "PASS"
|
||||
},
|
||||
"implemented": [
|
||||
"durable Supervisor ReleaseSet state for creator-web, core-runtime, cognitive-worker, and research-runtime",
|
||||
"parallel candidate services with candidate-only core/cognitive/research dependency wiring",
|
||||
"whole-set health/readiness/revision observation, canary, promotion and rollback controls",
|
||||
"stable/candidate/LKG persistence across Supervisor restart",
|
||||
"core and research runtime compose healthchecks"
|
||||
],
|
||||
"production": {
|
||||
"deployment": "NOT_RUN",
|
||||
"candidate_release_set": "NOT_STARTED",
|
||||
"telemetry_observation": "NOT_STARTED",
|
||||
"stable_mutation": "NONE"
|
||||
},
|
||||
"capability_evidence": {
|
||||
"docker_and_compose_on_production_host": "AVAILABLE",
|
||||
"supervisor_control_endpoint": "REACHABLE",
|
||||
"portainer_api_key_in_current_execution_context": "NOT_CONFIGURED",
|
||||
"portainer_endpoint_metadata_in_current_execution_context": "NOT_CONFIGURED",
|
||||
"existing_production_candidate_services": ["creator-web-candidate"],
|
||||
"missing_capability": "Approved production deployment automation or candidate-capable control-plane access for the four-component ReleaseSet."
|
||||
},
|
||||
"unsafe_action_not_taken": "Stable containers were not replaced directly and no production compose mutation was attempted.",
|
||||
"compatibility": "No V1 compatibility path was removed or disabled; no schema, migration, semantic owner, or Gen3 change.",
|
||||
"next_action": "Owner must provide or enable the approved candidate-capable production deployment mechanism; then run the ReleaseSet drill and start bounded telemetry observation."
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"evidence_id": "gen2-deploy-002-production-drill-2026-08-30",
|
||||
"task_id": "GEN2-DEPLOY-002",
|
||||
"task_revision": 1,
|
||||
"status": "BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"integration_revision": "7f8a05c",
|
||||
"candidate_revision": "7f8a05c",
|
||||
"stable_revision_after_rollback": "3182092d5cc758f5a536ce10f5428b5d782bb961",
|
||||
"release_set": ["creator-web", "core-runtime", "cognitive-worker", "research-runtime"],
|
||||
"drill": {
|
||||
"candidate_health_readiness_revision": "PASS; all four candidate containers healthy and image revision checked",
|
||||
"candidate_dependency_isolation": "PASS; creator->core candidate, core->research candidate, core/cognitive candidate routes verified in compose wiring",
|
||||
"canary_header": "PASS; Supervisor selected candidate for X-Assistant-Canary=true",
|
||||
"forced_component_failure": "PASS; research-runtime-candidate stopped, ReleaseSet observation rejected with HTTP 409",
|
||||
"whole_set_rollback": "PASS; Supervisor restored stable 3182092 and traffic mode STABLE",
|
||||
"promotion": "PASS; healthy candidate was promoted through Supervisor",
|
||||
"supervisor_restart": "PASS; promoted ReleaseSet and rollback target survived restart",
|
||||
"post_rollback_health": "PASS; public health/readiness both HTTP 200"
|
||||
},
|
||||
"telemetry": {
|
||||
"startup_events_observed": ["non-bootstrap-env-consumers", "migration-compatibility-switches"],
|
||||
"authenticated_path_observation": "NOT_COMPLETED",
|
||||
"observation_window": "NOT_STARTED",
|
||||
"reason": "The existing browser session was stale and production requested re-authentication before chat submission. No credentials were read or entered."
|
||||
},
|
||||
"final_production_state": {"traffic_mode": "STABLE", "revision": "3182092d5cc758f5a536ce10f5428b5d782bb961", "candidate_containers": "retained but not serving stable traffic"},
|
||||
"not_done": ["telemetry confirmation for all nine paths", "bounded observation window", "retirement tasks", "main merge", "milestone tag"],
|
||||
"scope": {"schema_or_migrations_changed": false, "v1_paths_removed": false, "gen3_started": false, "ui_changed": false}
|
||||
}
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# GEN2-EVOLVE-001 — milestone evidence
|
||||
|
||||
- Task/revision: `GEN2-EVOLVE-001 r3`.
|
||||
- Base: `07f0f6e3807f0c40971a22f7bc31b844c0635857` (`gen2-integration`); task branch: `gen2/gen2-evolve-001`.
|
||||
- Migration numbering: global. Existing `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupied the originally chosen number, so the task-owned additive Epistemics migration was renumbered to `053_gen2_adaptive_experience_scope.sql`. No historical/applied migration was edited.
|
||||
- Canonical ownership: Interaction remains the canonical owner of utterances and typed `ProjectReference`; Epistemics remains the canonical owner of Experience records and temporal evidence; Context Compiler remains rebuildable. No new Memory owner, cross-owner SQL join, canonical writer, privilege, dependency, provider override or V1 retirement was introduced.
|
||||
|
||||
## Milestone checks
|
||||
|
||||
Executed once on 2026-08-30:
|
||||
|
||||
```text
|
||||
go test -count=1 ./modules/interaction/domain ./modules/epistemics/domain ./contracts/cognitive ./contracts/epistemics ./adapters/postgres ./cmd/core-runtime
|
||||
go vet ./modules/interaction/domain ./modules/epistemics/domain ./contracts/cognitive ./contracts/epistemics ./adapters/postgres ./cmd/core-runtime
|
||||
go run ./cmd/devctl preflight -task dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json
|
||||
go run ./cmd/devctl check-diff -task dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json
|
||||
go run ./cmd/devctl/independent
|
||||
go run ./cmd/devctl check-architecture
|
||||
git diff --check
|
||||
```
|
||||
|
||||
All commands passed. The focused migration consistency check confirmed a unique global sequence through `053`, an additive owner-local migration, and required `BEGIN`, `SET ROLE epistemics_role`, `principal_id`, `context_scope_ref`, and `COMMIT` markers.
|
||||
|
||||
## Acceptance covered
|
||||
|
||||
- current versus historical assertion selection preserves historical lineage; an explicit correction precedes timestamp tie-breaking;
|
||||
- task-relative volatility/freshness uses validity, last verification, maximum age and optional version applicability;
|
||||
- Principal and typed `ProjectReference` scope filtering occur before relevance/salience ranking;
|
||||
- positive, negative and contradiction Experience lanes are independently retrieved with applicability conditions retained;
|
||||
- Context Compiler reserves bounded lane coverage, preserves omission/inclusion reasons in an internal trace, and keeps historical analogies opt-in;
|
||||
- consolidation candidates retain Epistemics experience references and are candidates only: no Skill is written or activated;
|
||||
- retrieval projections rebuild from canonical Interaction/Epistemics records; no process-local state is required.
|
||||
|
||||
## Deferred / next gap
|
||||
|
||||
The existing `IterativeRetrievalRequest` contract is present, but no established runtime continuation callback is available in this packet's scope. Iterative retrieval therefore remains a bounded future wiring task; no parallel runtime contract was invented here. Browser/UI acceptance, compatibility observation and V1 retirement remain deferred by owner decision.
|
||||
@@ -0,0 +1,14 @@
|
||||
# GEN2-EVOLVE-002 — Iterative Retrieval & Cognitive Memory Continuation
|
||||
|
||||
- Task/revision: `GEN2-EVOLVE-002 r2`; base `850fe92`.
|
||||
- Continuation path: Core initial Context Compiler -> Cognitive Gateway advisory -> typed retrieval intent -> Core-bound inherited scope/policy -> existing Interaction/Epistemics retrieval -> existing Context Compiler -> one final advisory.
|
||||
- Initiator: the cognitive strategy may emit a strict typed lane intent. Gateway validates it; Core, not the model, binds Principal, ContextScope, Focus, task label, freshness policy and local-only data policy.
|
||||
- Limits: one continuation round, two excerpts, 1024 added bytes, 1.5 second local retrieval window, lane-qualified duplicate suppression. The second advisory has continuation disabled.
|
||||
- No result returns a private `no additional evidence` excerpt for the final reasoning round. Negative experience and contradiction are separate lanes; scope is hard-filtered before ranking.
|
||||
- No new owner, schema, provider selector, direct cognitive-worker storage access, V1 retirement or UI change.
|
||||
|
||||
## Verification
|
||||
|
||||
MILESTONE gate: targeted `go test` and `go vet` for `contracts/cognitive`, `cmd/cognitive-worker`, and `cmd/core-runtime`; `devctl preflight`, `check-diff`, `independent`, `check-architecture`, and `git diff --check`.
|
||||
|
||||
Focused tests cover the second retrieval round, Company A/B isolation, negative and contradiction lanes, duplicate suppression, byte/round limits, and no-additional-evidence behavior. Continuation state is request-scoped; durable restart/resume remains with the existing Work/Investigation owner.
|
||||
@@ -0,0 +1,16 @@
|
||||
# GEN2-EVOLVE-003 — Experience to Skill to Reflex Learning Lifecycle
|
||||
|
||||
- Task/revision: `GEN2-EVOLVE-003 r3`; base `a07aba0`.
|
||||
- Canonical owners remain unchanged: Epistemics owns Experience/Outcome evidence; Capability owns the single SkillProfile lifecycle and Reflex revisions; SELF owns ImprovementNeed and Learning Ledger. Evidence is referenced by stable refs, not copied into another memory or Skill registry.
|
||||
- Skill lifecycle: verified positive evidence produces `CANDIDATE`; at least two verified outcomes plus an independent evaluation and must-not-worsen evidence are required for `VALIDATED`; a separate explicit transition makes it `ACTIVE`.
|
||||
- Routing: an ineligible Reflex falls through to an eligible Skill and then Local reasoning. A degraded, stale, suspended, retired or insufficient-confidence Skill cannot be fast-applied.
|
||||
- Reflex lifecycle: only an Active Skill with three verified outcomes and a bounded deterministic/rule/workflow implementation can become a Candidate. Independent evaluation promotes it to Active. Sampled shadow divergence marks it `STALE` and creates only a SELF revalidation proposal; it cannot rewrite or promote production code.
|
||||
- Revalidation triggers cover stale lifecycle, dependency/version change, correction count and validation age. Retired Reflex revisions remain durable history and cannot be selected.
|
||||
- Persistence: additive migration `054` extends the existing Capability SkillProfile record, adds Capability-owned Reflex revision/head tables, and adds SELF ImprovementNeed/Learning Ledger evidence fields using existing roles. Rollback excludes a non-Active Reflex and falls back to Skill/reasoning; no evidence is deleted.
|
||||
- Gap intentionally retained: automatic generation of executable production Reflex code is not implemented. It requires a separately authorized governed-development task with independent evaluation and promotion gates.
|
||||
|
||||
## Verification
|
||||
|
||||
MILESTONE: focused `go test`/`go vet` for Capability domain, SELF domain, PostgreSQL adapter, Core Runtime and Capability contracts; `devctl preflight`, `check-diff`, `independent`, `check-architecture`, migration consistency and `git diff --check`.
|
||||
|
||||
Focused regressions cover no one-run promotion, independent validation, Reflex candidate requirements, shadow divergence/revalidation rather than rewrite, Skill-to-reasoning escalation, SELF proposal-only behavior, and historical evidence preservation.
|
||||
@@ -0,0 +1,28 @@
|
||||
# GEN2-EVOLVE-004 milestone evidence
|
||||
|
||||
Status: MILESTONE_ACCEPTED for the bounded SELF evidence/proposal domain.
|
||||
|
||||
## Delivered
|
||||
|
||||
- Typed learning signals cover verified failures, corrections, repeated mistakes, Reflex divergence, Skill fallback, escalation, outcome mismatch, retry/fallback, stale dependencies, regret and better solutions.
|
||||
- Attribution returns a bounded evidence-backed `HYPOTHESIS`; provider outage is not converted into a competence loss.
|
||||
- Weakness patterns require two matching incidents in the same Principal/task/Scope and retain evidence, frequency, severity, affected component and confidence.
|
||||
- Competence assessment is contextual and requires evidence; SELF cannot self-declare trust.
|
||||
- ImprovementNeed and Candidate contain hypothesis, expected effect, evaluation, must-not-worsen, rollback, cost and independent evaluator fields.
|
||||
- Benchmark cases reference evidence rather than copying conversations. Learning Ledger appends expected and actual evaluation references and decision.
|
||||
- Candidate evaluation rejects latency/quality regression and creator/evaluator identity reuse. New model qualification creates a review candidate only.
|
||||
- PostgreSQL migration `055` adds SELF-owned durable signal, weakness, candidate and benchmark tables under existing `self_role`; no new role, owner or cross-owner relation is created.
|
||||
|
||||
## Checks
|
||||
|
||||
- `go test ./contracts/self ./modules/self/domain ./adapters/postgres ./cmd/core-runtime`
|
||||
- `go vet ./contracts/self ./modules/self/domain ./adapters/postgres ./cmd/core-runtime`
|
||||
- `go run ./cmd/devctl preflight -task dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json`
|
||||
- `go run ./cmd/devctl check-diff -task dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json`
|
||||
- `go run ./cmd/devctl/independent`
|
||||
- `go run ./cmd/devctl check-architecture`
|
||||
- `git diff --check`
|
||||
|
||||
## Boundaries
|
||||
|
||||
No autonomous code generation, merge, deployment, production promotion, new SELF owner, Skill/Reflex mutation, UI work, V1 retirement or Gen2-EVOLVE-005 was performed. Existing Work governed-evolution gates remain the promotion authority. Runtime recorder wiring through `cmd/core-runtime/main.go` was not added because it is outside the accepted packet scope; the durable repository and typed domain path are ready for a separately authorized integration task.
|
||||
@@ -40,10 +40,11 @@ func BuildDiscoveryRegistration(snapshot capability.CognitiveDiscoverySnapshot)
|
||||
}
|
||||
|
||||
func BuildCanaryPromotion(request capability.CognitiveCanaryPromotionRequest, current capability.ResolvedCognitiveBinding) (capability.CognitiveCanaryPromotion, error) {
|
||||
if request.PromotionID == "" || request.RequestedByPrincipalID == "" || request.CapabilityID == "" || request.BindingID == "" || request.ExpectedBindingRevision < 1 || request.ProviderInstanceID == "" || request.ExpectedProviderRevision < 1 || request.ModelDescriptorID == "" || request.ExpectedModelRevision < 1 || request.EvaluationID == "" || request.ObservedAtUnixMillis < 1 || current.Evaluation == nil {
|
||||
evaluation := promotionEvaluation(current)
|
||||
if request.PromotionID == "" || request.RequestedByPrincipalID == "" || request.CapabilityID == "" || request.BindingID == "" || request.ExpectedBindingRevision < 1 || request.ProviderInstanceID == "" || request.ExpectedProviderRevision < 1 || request.ModelDescriptorID == "" || request.ExpectedModelRevision < 1 || request.EvaluationID == "" || request.ObservedAtUnixMillis < 1 || evaluation == nil {
|
||||
return capability.CognitiveCanaryPromotion{}, ErrInvalidCognitiveRegistryRecord
|
||||
}
|
||||
if current.Binding.ID != request.BindingID || current.Binding.Revision != request.ExpectedBindingRevision || current.Binding.OwnerPrincipalID != request.RequestedByPrincipalID || current.Binding.CapabilityID != request.CapabilityID || current.Provider.ID != request.ProviderInstanceID || current.Provider.Revision != request.ExpectedProviderRevision || current.Model.ID != request.ModelDescriptorID || current.Model.Revision != request.ExpectedModelRevision || current.Evaluation.ID != request.EvaluationID || current.Evaluation.Result != capability.EvaluationSupported || current.Evaluation.QualityScoreMillis == nil || *current.Evaluation.QualityScoreMillis < 750 || current.Evaluation.CriticalFailureMillis == nil || *current.Evaluation.CriticalFailureMillis != 0 || current.Provider.Health != capability.HealthAvailable || current.Model.Health != capability.HealthAvailable || current.Binding.Activation != capability.ActivationInactive {
|
||||
if current.Binding.ID != request.BindingID || current.Binding.Revision != request.ExpectedBindingRevision || current.Binding.OwnerPrincipalID != request.RequestedByPrincipalID || current.Binding.CapabilityID != request.CapabilityID || current.Provider.ID != request.ProviderInstanceID || current.Provider.Revision != request.ExpectedProviderRevision || current.Model.ID != request.ModelDescriptorID || current.Model.Revision != request.ExpectedModelRevision || evaluation.ID != request.EvaluationID || evaluation.Result != capability.EvaluationSupported || evaluation.QualityScoreMillis == nil || *evaluation.QualityScoreMillis < 750 || evaluation.CriticalFailureMillis == nil || *evaluation.CriticalFailureMillis != 0 || current.Provider.Health != capability.HealthAvailable || current.Model.Health != capability.HealthAvailable || current.Binding.Activation != capability.ActivationInactive {
|
||||
return capability.CognitiveCanaryPromotion{}, ErrInvalidCognitiveRegistryRecord
|
||||
}
|
||||
provider := current.Provider
|
||||
@@ -56,26 +57,39 @@ func BuildCanaryPromotion(request capability.CognitiveCanaryPromotionRequest, cu
|
||||
binding.Revision = stableRevision(fmt.Sprint(binding.Revision), fmt.Sprint(provider.Revision), fmt.Sprint(model.Revision), string(request.PromotionID), string(capability.ActivationCanary))
|
||||
binding.ProviderRevision, binding.ModelRevision = provider.Revision, model.Revision
|
||||
binding.Qualification, binding.Activation, binding.Health = capability.QualificationValidated, capability.ActivationCanary, capability.HealthAvailable
|
||||
evaluation := *current.Evaluation
|
||||
evaluation.ID = capability.EvaluationID(stableID("evaluation", string(request.PromotionID), string(binding.ID), fmt.Sprint(binding.Revision)))
|
||||
evaluation.BindingRevision = binding.Revision
|
||||
return capability.CognitiveCanaryPromotion{PromotionID: request.PromotionID, ExpectedBindingRevision: request.ExpectedBindingRevision, Provider: provider, Model: model, Binding: binding, Evaluation: evaluation}, nil
|
||||
promotedEvaluation := *evaluation
|
||||
promotedEvaluation.ID = capability.EvaluationID(stableID("evaluation", string(request.PromotionID), string(binding.ID), fmt.Sprint(binding.Revision)))
|
||||
promotedEvaluation.BindingRevision = binding.Revision
|
||||
return capability.CognitiveCanaryPromotion{PromotionID: request.PromotionID, ExpectedBindingRevision: request.ExpectedBindingRevision, Provider: provider, Model: model, Binding: binding, Evaluation: promotedEvaluation}, nil
|
||||
}
|
||||
|
||||
func BuildActivationPromotion(request capability.CognitiveActivationPromotionRequest, current capability.ResolvedCognitiveBinding) (capability.CognitiveActivationPromotion, error) {
|
||||
if request.PromotionID == "" || request.RequestedByPrincipalID == "" || request.CapabilityID == "" || request.BindingID == "" || request.ExpectedBindingRevision < 1 || request.EvaluationID == "" || request.ObservedAtUnixMillis < 1 || current.Evaluation == nil {
|
||||
evaluation := promotionEvaluation(current)
|
||||
if request.PromotionID == "" || request.RequestedByPrincipalID == "" || request.CapabilityID == "" || request.BindingID == "" || request.ExpectedBindingRevision < 1 || request.EvaluationID == "" || request.ObservedAtUnixMillis < 1 || evaluation == nil {
|
||||
return capability.CognitiveActivationPromotion{}, ErrInvalidCognitiveRegistryRecord
|
||||
}
|
||||
if current.Binding.ID != request.BindingID || current.Binding.Revision != request.ExpectedBindingRevision || current.Binding.OwnerPrincipalID != request.RequestedByPrincipalID || current.Binding.CapabilityID != request.CapabilityID || current.Evaluation.ID != request.EvaluationID || current.Evaluation.Result != capability.EvaluationSupported || current.Evaluation.QualityScoreMillis == nil || *current.Evaluation.QualityScoreMillis < 750 || current.Evaluation.CriticalFailureMillis == nil || *current.Evaluation.CriticalFailureMillis != 0 || current.Provider.Health != capability.HealthAvailable || current.Model.Health != capability.HealthAvailable || current.Binding.Qualification != capability.QualificationValidated || current.Binding.Activation != capability.ActivationCanary || current.Binding.Health != capability.HealthAvailable {
|
||||
if current.Binding.ID != request.BindingID || current.Binding.Revision != request.ExpectedBindingRevision || current.Binding.OwnerPrincipalID != request.RequestedByPrincipalID || current.Binding.CapabilityID != request.CapabilityID || evaluation.ID != request.EvaluationID || evaluation.Result != capability.EvaluationSupported || evaluation.QualityScoreMillis == nil || *evaluation.QualityScoreMillis < 750 || evaluation.CriticalFailureMillis == nil || *evaluation.CriticalFailureMillis != 0 || current.Provider.Health != capability.HealthAvailable || current.Model.Health != capability.HealthAvailable || current.Binding.Qualification != capability.QualificationValidated || current.Binding.Activation != capability.ActivationCanary || current.Binding.Health != capability.HealthAvailable {
|
||||
return capability.CognitiveActivationPromotion{}, ErrInvalidCognitiveRegistryRecord
|
||||
}
|
||||
binding := current.Binding
|
||||
binding.Revision = stableRevision(fmt.Sprint(binding.Revision), string(request.PromotionID), string(capability.ActivationActive))
|
||||
binding.Qualification, binding.Activation, binding.Health = capability.QualificationValidated, capability.ActivationActive, capability.HealthAvailable
|
||||
evaluation := *current.Evaluation
|
||||
evaluation.ID = capability.EvaluationID(stableID("evaluation", string(request.PromotionID), string(binding.ID), fmt.Sprint(binding.Revision)))
|
||||
evaluation.BindingRevision = binding.Revision
|
||||
return capability.CognitiveActivationPromotion{PromotionID: request.PromotionID, ExpectedBindingRevision: request.ExpectedBindingRevision, Binding: binding, Evaluation: evaluation}, nil
|
||||
promotedEvaluation := *evaluation
|
||||
promotedEvaluation.ID = capability.EvaluationID(stableID("evaluation", string(request.PromotionID), string(binding.ID), fmt.Sprint(binding.Revision)))
|
||||
promotedEvaluation.BindingRevision = binding.Revision
|
||||
return capability.CognitiveActivationPromotion{PromotionID: request.PromotionID, ExpectedBindingRevision: request.ExpectedBindingRevision, Binding: binding, Evaluation: promotedEvaluation}, nil
|
||||
}
|
||||
|
||||
func promotionEvaluation(current capability.ResolvedCognitiveBinding) *capability.EvaluatedCapability {
|
||||
if current.Evaluation != nil {
|
||||
return current.Evaluation
|
||||
}
|
||||
if current.TaskEvaluation == nil {
|
||||
return nil
|
||||
}
|
||||
quality := current.TaskEvaluation.QualityScoreMillis
|
||||
critical := current.TaskEvaluation.CriticalFailureMillis
|
||||
return &capability.EvaluatedCapability{ID: capability.EvaluationID(current.TaskEvaluation.ID), BindingID: current.TaskEvaluation.BindingID, BindingRevision: current.TaskEvaluation.BindingRevision, CapabilityID: current.TaskEvaluation.CapabilityID, Result: current.TaskEvaluation.Result, QualityScoreMillis: &quality, CriticalFailureMillis: &critical, BenchmarkVersion: current.TaskEvaluation.BenchmarkVersion, ArtifactID: current.TaskEvaluation.ArtifactID, ObservedAtUnixMillis: current.TaskEvaluation.ObservedAtUnixMillis}
|
||||
}
|
||||
|
||||
func stableID(kind string, parts ...string) string {
|
||||
|
||||
@@ -102,3 +102,20 @@ func TestSelectCognitiveBindingUsesObjectiveAndCost(t *testing.T) {
|
||||
t.Fatalf("economy selection=%+v err=%v", selected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectCognitiveBindingUsesMediumWhenFastProviderIsUnavailable(t *testing.T) {
|
||||
quality := int64(800)
|
||||
critical := int64(0)
|
||||
candidate := func(id string, health capability.HealthState) capability.ResolvedCognitiveBinding {
|
||||
return capability.ResolvedCognitiveBinding{
|
||||
Binding: capability.CognitiveBinding{ID: capability.CapabilityBindingID(id), Revision: 1, CapabilityID: "creator-advisory", OwnerPrincipalID: "owner", ProviderInstanceID: capability.ProviderInstanceID(id), ProviderRevision: 1, ModelDescriptorID: capability.ModelDescriptorID(id), ModelRevision: 1, DataPolicy: capability.DataPolicyLocalOnly, Qualification: capability.QualificationValidated, Activation: capability.ActivationActive, Health: health, LimitPolicyRef: "limits"},
|
||||
Provider: capability.ProviderInstance{ID: capability.ProviderInstanceID(id), Revision: 1, Kind: "openai-compatible-chat-v1", EndpointRef: capability.EndpointBindingRef(id), Enabled: true, Qualification: capability.QualificationValidated, Health: health, ObservedAtUnixMillis: 1},
|
||||
Model: capability.ModelDescriptor{ID: capability.ModelDescriptorID(id), Revision: 1, ProviderInstanceID: capability.ProviderInstanceID(id), ProviderModelID: capability.ProviderModelID(id), MaxContextBytes: 10000, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, Qualification: capability.QualificationValidated, Health: health, ObservedAtUnixMillis: 1},
|
||||
Evaluation: &capability.EvaluatedCapability{ID: capability.EvaluationID("eval-" + id), Result: capability.EvaluationSupported, QualityScoreMillis: &quality, CriticalFailureMillis: &critical},
|
||||
}
|
||||
}
|
||||
selected, err := SelectCognitiveBinding(capability.CognitiveRequirement{CapabilityID: "creator-advisory", OwnerPrincipalID: "owner", DataPolicy: capability.DataPolicyLocalOnly, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, MaxOutputBytes: 10, Objective: capability.SelectionBalanced}, []capability.ResolvedCognitiveBinding{candidate("cpu-fast", capability.HealthUnavailable), candidate("local-medium", capability.HealthAvailable)})
|
||||
if err != nil || selected.Binding.ID != "local-medium" {
|
||||
t.Fatalf("medium fallback selection=%+v err=%v", selected, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +13,13 @@ func TestStrategyUsesRiskAndCostSignals(t *testing.T) {
|
||||
t.Fatalf("%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnavailableReflexFallsBackThroughSkillToReasoning(t *testing.T) {
|
||||
request := capability.StrategyRequest{PreferLowLatency: true, RequiredQuality: 700}
|
||||
if got := request.SelectEligible(capability.StrategyEligibility{SkillEligible: true}); got.Path != capability.PathSkill {
|
||||
t.Fatalf("expected Skill fallback, got %#v", got)
|
||||
}
|
||||
if got := request.SelectEligible(capability.StrategyEligibility{}); got.Path != capability.PathSmallLocal || !got.Escalate {
|
||||
t.Fatalf("expected reasoning fallback, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,91 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrInsufficientReflexEvidence = errors.New("insufficient reflex evidence")
|
||||
|
||||
func ValidateReflexRevision(v capability.ReflexRevision) error {
|
||||
if v.ReflexID == "" || v.SourceSkillRevision < 1 || v.Kind == "" || v.ImplementationRef == "" || v.Lifecycle == "" || v.RecordedAt < 1 {
|
||||
return fmt.Errorf("invalid reflex revision")
|
||||
}
|
||||
if v.Lifecycle == capability.ReflexActive && len(v.ValidationEvidenceRefs) == 0 {
|
||||
if (v.Lifecycle == capability.ReflexActive || v.Lifecycle == capability.ReflexValidating) && len(v.ValidationEvidenceRefs) == 0 {
|
||||
return fmt.Errorf("active reflex requires evidence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CandidateReflex admits only a bounded, deterministic implementation from an
|
||||
// already active Skill with multiple verified results. It never activates it.
|
||||
func CandidateReflex(skill capability.SkillProfile, value capability.ReflexRevision, verifierEvidence capability.SkillEvidenceRef, recordedAt int64) (capability.ReflexRevision, error) {
|
||||
if err := ValidateSkillProfile(skill); err != nil || skill.Status != capability.SkillActive || skill.VerifiedOutcomeCount < 3 || verifierEvidence == "" || recordedAt < 1 {
|
||||
return capability.ReflexRevision{}, ErrInsufficientReflexEvidence
|
||||
}
|
||||
if value.Kind != capability.ReflexDeterministicCode && value.Kind != capability.ReflexRule && value.Kind != capability.ReflexWorkflow && value.Kind != capability.ReflexBoundedAdapter {
|
||||
return capability.ReflexRevision{}, ErrInsufficientReflexEvidence
|
||||
}
|
||||
value.SourceSkillRevision = skill.Revision
|
||||
value.Lifecycle = capability.ReflexCandidate
|
||||
value.ValidationEvidenceRefs = appendEvidence(value.ValidationEvidenceRefs, verifierEvidence)
|
||||
value.LastValidatedAt = 0
|
||||
value.RecordedAt = recordedAt
|
||||
if err := ValidateReflexRevision(value); err != nil {
|
||||
return capability.ReflexRevision{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func ValidateAndActivateReflex(value capability.ReflexRevision, independentEvidence capability.SkillEvidenceRef, mustNotWorsen bool, recordedAt int64) (capability.ReflexRevision, error) {
|
||||
if err := ValidateReflexRevision(value); err != nil || value.Lifecycle != capability.ReflexCandidate || independentEvidence == "" || !mustNotWorsen || recordedAt < 1 {
|
||||
return capability.ReflexRevision{}, ErrInsufficientReflexEvidence
|
||||
}
|
||||
next := value
|
||||
next.Revision++
|
||||
next.Lifecycle = capability.ReflexActive
|
||||
next.ValidationEvidenceRefs = appendEvidence(next.ValidationEvidenceRefs, independentEvidence)
|
||||
next.LastValidatedAt = recordedAt
|
||||
next.RecordedAt = recordedAt
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// ObserveShadow keeps normal execution non-blocking. Divergence marks the
|
||||
// revision stale for governed revalidation; it cannot replace implementation.
|
||||
func ObserveShadow(value capability.ReflexRevision, comparison capability.ShadowComparison) (capability.ReflexRevision, bool, error) {
|
||||
if err := ValidateReflexRevision(value); err != nil || comparison.ReflexID != value.ReflexID || comparison.ReflexRevision != value.Revision || comparison.ObservedAt < 1 {
|
||||
return capability.ReflexRevision{}, false, ErrInsufficientReflexEvidence
|
||||
}
|
||||
next := value
|
||||
next.ExecutionCount++
|
||||
if comparison.Diverged || !comparison.VerifierPassed {
|
||||
next.DivergenceCount++
|
||||
next.Lifecycle = capability.ReflexStale
|
||||
next.RecordedAt = comparison.ObservedAt
|
||||
return next, true, nil
|
||||
}
|
||||
return next, false, nil
|
||||
}
|
||||
|
||||
func RevalidationRequired(value capability.ReflexRevision, dependencyChanged bool, correctionCount int64, now int64) bool {
|
||||
if value.Lifecycle == capability.ReflexRetired || value.Lifecycle == capability.ReflexStale || value.Lifecycle == capability.ReflexDegraded {
|
||||
return true
|
||||
}
|
||||
if dependencyChanged || (value.Revalidation.RevalidateAfterCorrectionCount != nil && correctionCount >= *value.Revalidation.RevalidateAfterCorrectionCount) {
|
||||
return true
|
||||
}
|
||||
return value.Revalidation.MaxAgeMillis != nil && value.LastValidatedAt > 0 && now-value.LastValidatedAt >= *value.Revalidation.MaxAgeMillis
|
||||
}
|
||||
|
||||
func appendEvidence(values []capability.SkillEvidenceRef, value capability.SkillEvidenceRef) []capability.SkillEvidenceRef {
|
||||
for _, existing := range values {
|
||||
if existing == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
func ReflexFallback(d capability.FastPathDecision) string {
|
||||
if d.Decision == capability.FastPathApplied {
|
||||
return "reflex"
|
||||
|
||||
@@ -19,3 +19,19 @@ func TestReflexActiveRequiresEvidenceAndFallbacks(t *testing.T) {
|
||||
t.Fatal("missing escalation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReflexRequiresStableSkillAndShadowDivergenceOnlyRequestsReview(t *testing.T) {
|
||||
skill := capability.SkillProfile{ID: "s", Revision: 4, Name: "bounded", Description: "bounded", Status: capability.SkillActive, ConfidenceMillis: 900, RecordedAtUnixMillis: 1, VerifiedOutcomeCount: 3}
|
||||
candidate, err := domain.CandidateReflex(skill, capability.ReflexRevision{ReflexID: "r", Revision: 1, Kind: capability.ReflexRule, ImplementationRef: "rule", RecordedAt: 1}, "verifier:success", 2)
|
||||
if err != nil || candidate.Lifecycle != capability.ReflexCandidate {
|
||||
t.Fatalf("candidate=%#v err=%v", candidate, err)
|
||||
}
|
||||
active, err := domain.ValidateAndActivateReflex(candidate, "evaluation:independent", true, 3)
|
||||
if err != nil || active.Lifecycle != capability.ReflexActive {
|
||||
t.Fatalf("active=%#v err=%v", active, err)
|
||||
}
|
||||
next, review, err := domain.ObserveShadow(active, capability.ShadowComparison{ReflexID: "r", ReflexRevision: active.Revision, EvidenceRefs: []capability.SkillEvidenceRef{"shadow:1"}, VerifierPassed: false, Diverged: true, ObservedAt: 4})
|
||||
if err != nil || !review || next.Lifecycle != capability.ReflexStale {
|
||||
t.Fatalf("next=%#v review=%t err=%v", next, review, err)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user