package providermemory import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" "data-terminal-cloud/internal/outbox" ) type Memory struct { TenantID string `json:"tenant_id"` HouseholdID string `json:"household_id"` SubjectID string `json:"subject_id"` MemoryID string `json:"memory_id"` MemoryVersion string `json:"memory_version_id"` VersionNo int64 `json:"version_no"` Content string `json:"content"` ContentDigest string `json:"content_digest"` IdempotencyKey string `json:"idempotency_key"` } type Client interface { Upsert(context.Context, Memory) (string, error) Delete(context.Context, string, string) error } type HTTPClient struct { baseURL string apiKey string client *http.Client } func NewHTTPClient(baseURL, apiKey string, timeout time.Duration) (*HTTPClient, error) { parsed, err := url.Parse(strings.TrimRight(strings.TrimSpace(baseURL), "/")) if err != nil || parsed.Scheme == "" || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return nil, errors.New("provider memory base URL must be an absolute HTTP(S) URL") } if strings.TrimSpace(apiKey) == "" || timeout <= 0 { return nil, errors.New("provider memory API key and positive timeout are required") } return &HTTPClient{baseURL: parsed.String(), apiKey: strings.TrimSpace(apiKey), client: &http.Client{Timeout: timeout}}, nil } func (c *HTTPClient) Upsert(ctx context.Context, memory Memory) (string, error) { payload, err := json.Marshal(memory) if err != nil { return "", err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/memories:upsert", bytes.NewReader(payload)) if err != nil { return "", err } request.Header.Set("Authorization", "Bearer "+c.apiKey) request.Header.Set("Content-Type", "application/json") request.Header.Set("Idempotency-Key", memory.IdempotencyKey) response, err := c.client.Do(request) if err != nil { return "", outbox.Retryable(fmt.Errorf("call provider memory upsert: %w", err)) } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { return "", providerError("upsert", response) } var result struct { ProviderRef string `json:"provider_ref"` } if err := json.NewDecoder(io.LimitReader(response.Body, 64*1024)).Decode(&result); err != nil { return "", outbox.Retryable(errors.New("provider memory upsert returned invalid JSON")) } result.ProviderRef = strings.TrimSpace(result.ProviderRef) if result.ProviderRef == "" || len(result.ProviderRef) > 300 { return "", outbox.Retryable(errors.New("provider memory upsert returned an invalid reference")) } return result.ProviderRef, nil } func (c *HTTPClient) Delete(ctx context.Context, providerRef, idempotencyKey string) error { providerRef = strings.TrimSpace(providerRef) if providerRef == "" { return nil } request, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.baseURL+"/v1/memories/"+url.PathEscape(providerRef), nil) if err != nil { return err } request.Header.Set("Authorization", "Bearer "+c.apiKey) request.Header.Set("Idempotency-Key", idempotencyKey) response, err := c.client.Do(request) if err != nil { return outbox.Retryable(fmt.Errorf("call provider memory delete: %w", err)) } defer response.Body.Close() if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone { return nil } if response.StatusCode < 200 || response.StatusCode >= 300 { return providerError("delete", response) } return nil } func providerError(action string, response *http.Response) error { err := fmt.Errorf("provider memory %s returned HTTP %d", action, response.StatusCode) if response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500 { return outbox.Retryable(err) } return err }