p2: digest with in-sift FIFO, SQLite caches, Honcho context, recover
Dixie is capped (max_inflight 2, queue_depth 8, queue_wait 20s) and never queued on llama-server. Streaming chat, thinking off, 45s fallback to clean. Digest and chunk-embedding caches are content-addressed SQLite (Honcho context is not cached). Recover uses the guarded Fetcher. guardian-star is refused in code.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
// Package fleetctx is Honcho retrieval for sift. Read-only. Never guardian-star.
|
||||
package fleetctx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
Forbidden = "guardian-star"
|
||||
Timeout = 3 * time.Second
|
||||
Limit = 5
|
||||
MaxChars = 300
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Peer string `json:"peer"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Session string `json:"session"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Base string
|
||||
Allow []string
|
||||
HTTP *http.Client
|
||||
Log *slog.Logger
|
||||
Calls uint64
|
||||
Hits uint64
|
||||
Failures uint64
|
||||
}
|
||||
|
||||
func refused(ws string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(ws), Forbidden)
|
||||
}
|
||||
|
||||
func allowed(ws string, allow []string) bool {
|
||||
if refused(ws) {
|
||||
return false
|
||||
}
|
||||
for _, a := range allow {
|
||||
if a == ws {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Search returns prior fleet knowledge. Failure → (nil, warning). Never an error
|
||||
// that fails the fetch. guardian-star is refused in code, not only config.
|
||||
func (c *Client) Search(ctx context.Context, workspace, query string) (items []Item, warn string) {
|
||||
if c == nil || query == "" {
|
||||
return nil, ""
|
||||
}
|
||||
if refused(workspace) {
|
||||
if c.Log != nil {
|
||||
c.Log.Warn("context refused", "workspace", workspace)
|
||||
}
|
||||
c.Failures++
|
||||
return nil, "context refused: workspace"
|
||||
}
|
||||
if !allowed(workspace, c.Allow) {
|
||||
if c.Log != nil {
|
||||
c.Log.Warn("context workspace not allowlisted", "workspace", workspace)
|
||||
}
|
||||
return nil, "context skipped: workspace"
|
||||
}
|
||||
c.Calls++
|
||||
sctx, cancel := context.WithTimeout(ctx, Timeout)
|
||||
defer cancel()
|
||||
url := strings.TrimRight(c.Base, "/") + "/v3/workspaces/" + workspace + "/search"
|
||||
payload, _ := json.Marshal(map[string]any{"query": query, "limit": Limit})
|
||||
req, err := http.NewRequestWithContext(sctx, http.MethodPost, url, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
c.Failures++
|
||||
return nil, "context failed"
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
httpClient := c.HTTP
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{Timeout: Timeout}
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
c.Failures++
|
||||
return nil, "context failed"
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
c.Failures++
|
||||
return nil, "context failed"
|
||||
}
|
||||
var raw []struct {
|
||||
Content string `json:"content"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PeerID string `json:"peer_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||
c.Failures++
|
||||
return nil, "context failed"
|
||||
}
|
||||
for _, m := range raw {
|
||||
content := m.Content
|
||||
if utf8.RuneCountInString(content) > MaxChars {
|
||||
r := []rune(content)
|
||||
content = string(r[:MaxChars])
|
||||
}
|
||||
items = append(items, Item{
|
||||
Peer: m.PeerID,
|
||||
Content: content,
|
||||
CreatedAt: m.CreatedAt,
|
||||
Session: m.SessionID,
|
||||
})
|
||||
}
|
||||
if len(items) > 0 {
|
||||
c.Hits++
|
||||
}
|
||||
return items, ""
|
||||
}
|
||||
|
||||
func Query(question, title string, headings []string) string {
|
||||
if q := strings.TrimSpace(question); q != "" {
|
||||
return q
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(title)
|
||||
n := 0
|
||||
for _, h := range headings {
|
||||
if n >= 3 {
|
||||
break
|
||||
}
|
||||
if strings.TrimSpace(h) == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(h)
|
||||
n++
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func (c *Client) String() string { return fmt.Sprintf("fleetctx base=%s", c.Base) }
|
||||
@@ -0,0 +1,73 @@
|
||||
package fleetctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGuardianStarRefusedEvenIfAllowlisted(t *testing.T) {
|
||||
c := &Client{Base: "http://127.0.0.1:1", Allow: []string{"guardian-star", "hermes-fleet"}}
|
||||
items, warn := c.Search(context.Background(), "guardian-star", "anything")
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("items %v", items)
|
||||
}
|
||||
if warn == "" {
|
||||
t.Fatal("want warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowlist(t *testing.T) {
|
||||
c := &Client{Base: "http://127.0.0.1:1", Allow: []string{"hermes-fleet"}}
|
||||
_, warn := c.Search(context.Background(), "other-ws", "q")
|
||||
if warn == "" {
|
||||
t.Fatal("want skip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutWarning(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(4 * time.Second)
|
||||
w.WriteHeader(200)
|
||||
_, _ = w.Write([]byte("[]"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
c := &Client{Base: ts.URL, Allow: []string{"hermes-fleet"}, HTTP: ts.Client()}
|
||||
start := time.Now()
|
||||
_, warn := c.Search(context.Background(), "hermes-fleet", "q")
|
||||
if time.Since(start) > 3500*time.Millisecond {
|
||||
t.Fatalf("took %s", time.Since(start))
|
||||
}
|
||||
if warn == "" {
|
||||
t.Fatal("want warning on timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHappyPath(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var req map[string]any
|
||||
_ = json.Unmarshal(b, &req)
|
||||
if req["limit"] != float64(5) {
|
||||
t.Errorf("limit %v", req["limit"])
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode([]map[string]any{{
|
||||
"content": "fleet already knows X",
|
||||
"created_at": "2026-09-22T00:00:00Z",
|
||||
"peer_id": "kyle",
|
||||
"session_id": "s1",
|
||||
"workspace_id": "hermes-fleet",
|
||||
"token_count": 4,
|
||||
}})
|
||||
}))
|
||||
defer ts.Close()
|
||||
c := &Client{Base: ts.URL, Allow: []string{"hermes-fleet"}, HTTP: ts.Client()}
|
||||
items, warn := c.Search(context.Background(), "hermes-fleet", "X")
|
||||
if warn != "" || len(items) != 1 || items[0].Peer != "kyle" {
|
||||
t.Fatalf("%v %q", items, warn)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user